Source: components/widgets/modal/_modal-v5.js

'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)