'use strict';
/**
* Modal component.
* @namespace modal
* @memberof UCASDesignFramework
* @param {object} global - UCASDesignFramework object.
* @param {object} _u - UCASUtilities object.
*/
(function (global, _u) {
var focusTracker
var activeModals = [] // Used to keep track of how many modals are open.
var iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)
/**
* Initialise plugin
* @function init
* @memberof! UCASDesignFramework.modal
* @param {Node} context - a DOM element to limit selection to.
* @public
*/
function init (context) {
// We're being temporarily safe here and checking we're using v5
// before overriding the existing namespace.
// @todo Remove the check in v5!
if (document.body.classList.contains('v5') || document.body.classList.contains('v5-modal')) {
global.modal = {
init: init,
destroy: destroy,
showModal: showModal,
hideModal: hideModal,
Modal: Modal,
get active () {
return activeModals
}
}
delete global.v5Modal
} else {
return
}
global.subscriptions.addSubscriptions(global.modal, 'modal')
context = context || document
var modalTriggers = context.querySelectorAll('[data-modal-trigger]')
modalTriggers.forEach(function (el) {
el.addEventListener('click', showModalClickHandler, false)
})
// Handle form item modals.
var formItemModals = context.querySelectorAll('[data-form-item-modal]')
if (formItemModals) { initFormItemModals(formItemModals) }
// Escape key should close modal for accessibility
document.addEventListener('keydown', keydownEventHandler, false)
// Trap the focus
focusTrapper()
// Listen for the hide event.
document.addEventListener('modalHide', modalHideEventListener, false)
}
/**
* Check for open modals.
* @param {Event} e - the DOM event
*/
function modalHideEventListener (e) {
var open = document.querySelectorAll('[data-modal-state="visible"]')
if (open.length === 0) {
_u.log.log('MODALS hideModal event cleanup', e)
activeModals = []
document.documentElement.removeAttribute('data-modal-active')
}
}
/**
* Hide the modal with the Escape key.
* @param {Event} e - the DOM event
*/
function keydownEventHandler (e) {
e = e || window.event
var modal = document.querySelector('[data-modal-state="visible"] .modal')
if (e.keyCode === 27 && modal) {
hideModal()
}
if (e.keyCode === 9 && modal) {
var focusable = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
// Check if this is the last focusable element in the modal.
if (e.target === focusable[focusable.length - 1]) {
topModal().querySelector('.modal').focus()
}
}
}
/**
* Initialise form item modals.
* @function initFormItemModals
* @memberof! UCASDesignFramework.modal
* @param {NodeList} formItemModals - the DOM elements. These should be .v5-form-items and contain a button.
* @private
*/
function initFormItemModals (formItemModals) {
formItemModals.forEach(function (item) {
var id = item.getAttribute('data-form-item-modal')
var button = item.querySelector('button')
if (document.getElementById(id) && button) {
var el = button.parentNode.firstElementChild
do { if (button !== el) { el.style.display = 'none' } } while ((el = el.nextElementSibling))
button.setAttribute('data-modal-id', id)
button.setAttribute('data-modal-trigger', '')
var label = button.getAttribute('data-form-item-modal-label')
if (label) { button.innerText = label }
button.addEventListener('click', showModalClickHandler, false)
}
})
}
/**
* Remove event listeners and close all open modals.
* @function destroy
* @memberof! UCASDesignFramework.modal
* @public
*/
function destroy () {
var modalTriggers = document.querySelectorAll('[data-modal-trigger]')
_u.forEach(modalTriggers, function (i, el) {
el.removeEventListener('click', showModal)
})
var targets = document.querySelectorAll('[data-modal-close]')
_u.forEach(targets, function (i, el) {
el.removeEventListener('click', hideModal)
})
var openModal = document.querySelector('[data-modal-state="visible"]')
if (openModal) {
openModal.setAttribute('data-modal-state', 'hidden')
}
activeModals = []
document.documentElement.removeAttribute('data-modal-active')
}
/**
* Works out which modal is at the top.
* @returns {Node} - the topmost modal
*/
function topModal () {
var open = document.querySelectorAll('[data-modal-state="visible"]')
return open[open.length - 1]
}
/**
* Click handler to display a modal.
* @function showModalClickHandler
* @memberof! UCASDesignFramework.modal
* @param {Event} e - click event, the target of which contains a data-modal-id attribute.
* @private
*/
function showModalClickHandler (e) {
e.preventDefault()
showModal(e)
}
/**
* Displays a modal.
* @function showModal
* @memberof! UCASDesignFramework.modal
* @param {string|event} modalID - ID of modal to show or click event, the target of which contains a data-modal-id attribute.
*/
function showModal (modalID) {
if (typeof modalID === 'object') {
modalID = modalID.target.attributes['data-modal-id'].value
} else if (typeof modalID !== 'string') {
_u.log.error('Can\'t open modal!', modalID, 'should be a string or event!')
return
}
focusTracker = modalID
var targetModal = document.getElementById(modalID)
if (!targetModal) {
_u.log.error('Modal with ID ' + modalID + ' does not exist.')
return
}
if (targetModal.getAttribute('data-modal-state') === 'visible') {
_u.log.warn('Modal with ID ' + modalID + ' is already open.')
return
}
var modal = targetModal.querySelector('.modal')
if (!modal) {
_u.log.error('Modal with ID ' + modalID + ' does not contain a div.modal.')
return
}
targetModal.setAttribute('data-modal-state', 'visible')
modal.focus()
var content = targetModal.querySelector('.modal__content')
if (content) {
content.scrollTop = 0 // Always start from the top. scrollTo() doesn't work in Edge.
}
attachCloseListener(targetModal)
// Add the modalID to the array of activeModals but check it's not already
// listed there first, as we want to avoid timing issues.
if (activeModals.indexOf(modalID) === -1) {
activeModals.push(modalID)
}
_u.log.log('MODALS activeModals++', activeModals)
document.documentElement.setAttribute('data-modal-active', '')
// Handling iOS issues.
if (iOS) {
document.body.classList.add('ios')
global.size.lockPosition()
}
/**
* @event modalShow
* @memberof! UCASDesignFramework.modal
*/
var event = document.createEvent('Event')
event.initEvent('modalShow', true, true)
event.modalId = modalID
targetModal.dispatchEvent(event)
}
/**
* Attaches event listeners to close modal
* @param {Node} targetModal - the modal
*/
function attachCloseListener (targetModal) {
targetModal.addEventListener('click', hideModalListener, false)
var targets = targetModal.querySelectorAll('[data-modal-close]')
_u.forEach(targets, function (i, el) {
el.addEventListener('click', hideModalListener, false)
})
}
/**
* Hide modal listener.
* @param {Event} e - the DOM event
*/
function hideModalListener (e) {
if (e.target.classList.contains('modal-container') || e.target.hasAttribute('data-modal-close')) {
e.preventDefault()
e.stopPropagation()
hideModal()
}
}
/**
* Hides all modals or specific modalID
* @function hideModal
* @memberof! UCASDesignFramework.modal
* @param {string} modalID] ID of modal to hide (optional)
*/
function hideModal (modalID) {
modalID = modalID || ''
var targetModal
if (modalID !== '') {
targetModal = document.getElementById(modalID)
} else {
// As there is not a specific target, apply only to the top modal.
targetModal = topModal()
if (!targetModal) {
_u.log.error('MODALS topModal() could not identify an open modal')
return
}
_u.log.log('MODALS targetModal', targetModal)
// We need a modal ID to keep track of things.
modalID = targetModal.id
// Handle the situation where the modals aren't properly registered.
// This is only a temporary fix, this whole thing is going to be rewritten. See DF-1299.
if (!focusTracker) {
_u.log.error('UCASDesignFramework.modal.hideModal has been called but no modals are registered.')
return
}
if (targetModal !== undefined) {
if (typeof focusTracker !== 'string') {
focusTracker.target.focus()
}
}
}
if (targetModal !== undefined) {
targetModal.setAttribute('data-modal-closing', '')
setTimeout(function () {
targetModal.removeAttribute('data-modal-closing')
targetModal.setAttribute('data-modal-state', 'hidden')
/**
* @event modalHide
* @memberof! UCASDesignFramework.modal
*/
var event = document.createEvent('Event')
event.initEvent('modalHide', true, true)
event.modalId = modalID
targetModal.dispatchEvent(event)
}, 800)
}
activeModals = activeModals.filter(function (e) { return e !== modalID })
_u.log.log('MODALS activeModals--', activeModals)
if (activeModals.length === 0) {
document.documentElement.removeAttribute('data-modal-active')
// Handling iOS issues.
if (iOS) {
global.size.unlockPosition()
}
}
}
/**
* Traps focus inside an active modal for accessibility
*/
function focusTrapper () {
document.addEventListener('focus', function (ev) {
var activeModal = topModal()
if ((activeModal && !activeModal.contains(ev.target))) {
ev.stopPropagation()
activeModal.querySelector('.modal').focus()
}
}, true)
}
/**
* Provide a simple way to reference a modal and show/hide it.
* @class
* @memberof! UCASDesignFramework.modal
* @public
* @example
* myLovelyModal = new UCASDesignFramework.modal.Modal("modal-1")
* myLovelyModal.show()
* myLovelyModal.hide()
*/
var Modal = (function () {
/**
* @param {string} id - id of the modal
*/
function Modal (id) {
this.id = id
}
Modal.prototype.show = function () {
showModal(this.id)
}
Modal.prototype.hide = function () {
hideModal(this.id)
}
return Modal
})()
// Expose public methods.
// Using a temporary namespace so this module gets noticed by the initialisation script
// but we don't accidentally override the modal namespace if we're not meant to.
// @todo Remove this temporary namespace in v5!
global.v5Modal = {
init: init,
initOnLoad: true
}
})(UCASDesignFramework, UCASUtilities)