Source: components/forms/_form-help.js

'use strict';

/**
 * Dynamic form help.
 * @namespace form_help
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object.
 * @param {object} _u - UCASUtilities object.
 */
(function (global, _u) {
  var breakpoint
  var currentForm
  var currentFormItem
  var resizeEventHandlers = []

  /**
   * Initialise form_help.
   * @function init
   * @memberof! UCASDesignFramework.form_help
   * @public
   * @param  {Node} [context=document] - DOM element we're limiting selection to
   * @param  {string} [selector='.form--dynamic-help'] - CSS selector
  */
  function init (context, selector) {
    context = context || document
    selector = selector || '.form--dynamic-help'
    breakpoint = global.size.cssBreakpoint

    // Add event listeners.
    var forms = context.querySelectorAll(selector)
    if (forms.length > 0) {
      addHelpEventListeners(forms, breakpoint)
      // On window resize, recheck breakpoint and remove/add event listeners as necessary.
      var resizeEventHandler = function dynamicHelpResizeHandler () {
        breakpoint = global.size.cssBreakpoint
        addHelpEventListeners(forms, breakpoint)
      }
      resizeEventHandlers.push(resizeEventHandler)
      window.addEventListener('dfResize', resizeEventHandler)
    }

    // Set an attribute on .layout-scope for IE.
    if (document.documentMode) {
      var scopes = context.querySelectorAll('.layout-scope')
      scopes.forEach(function (scope) {
        scope.setAttribute('data-layout-scope-legacy', '')
      })
    }
  }

  /**
   * Destroy plugin.
   * Assumes the form has been removed from the DOM.
   * @function destroy
   * @memberof! UCASDesignFramework.form_help
   * @public
   */
  function destroy () {
    document.removeEventListener('click', formHelpDocumentClickHandler) // In practice unnecessary as the click handler removes the listener.
    _u.forEach(resizeEventHandlers, function (i, resizeEventHandler) {
      document.removeEventListener('dfResize', resizeEventHandler)
    })
    breakpoint = currentForm = currentFormItem = undefined
  }

  /**
   * Add help event listeners.
   * @param {NodeList} forms - nodelist of forms
   * @param {string} breakpoint - the breakpoint
   */
  function addHelpEventListeners (forms, breakpoint) {
    if (breakpoint === 'xsmall') {
      addHelpButtons(forms)
    } else {
      addDynamicHelpEventListeners(forms)
    }
  }

  /**
   * Add dynamic help event listeners.
   * @param {NodeList} forms - nodelist of forms
   */
  function addDynamicHelpEventListeners (forms) {
    _u.forEach(forms, function (i, form) {
      if (form.getAttribute('data-initialised') !== 'true') {
        form.addEventListener('focusin', formHelpFocusHandler)
        form.addEventListener('mouseleave', formHelpMouseleaveHelper)
        form.setAttribute('data-initialised', 'true')
      }

      var formItems = form.querySelectorAll('.form-item--text, .form-item__radio, legend')
      _u.forEach(formItems, function (i, formItem) {
        if (formItem.getAttribute('data-initialised') !== 'true') {
          formItem.addEventListener('mouseover', formHelpMouseoverHelper)
          formItem.setAttribute('data-initialised', 'true')
          _u.log.log('form_help.addDynamicHelpEventListeners', formItem)
        }
      })
    })
  }

  /**
   * Add help buttons.
   * @param {NodeList} forms - nodelist of forms
   */
  function addHelpButtons (forms) {
    _u.forEach(forms, function (i, form) {
      form.setAttribute('data-initialised', 'true')
      var formItems = form.querySelectorAll('[class^="form-item"].dynamic-help')
      _u.forEach(formItems, function (i, formItem) {
        var button = formItem.querySelector('.button--help')
        if (button === null) {
          var label = formItem.querySelector('label')
          var input = formItem.querySelector('input')
          // Create the button.
          button = document.createElement('button')
          button.setAttribute('class', 'button button--help hide-text')
          button.appendChild(document.createTextNode(formItem.getAttribute('data-toggle-help-button-label') || 'Help'))
          button.id = 'button-' + input.getAttribute('aria-describedby')
          formItem.insertBefore(button, label.nextSibling)
          addButtonEventListeners(button)
        }
      })
    })
  }

  /**
   * Add button event listeners.
   * @param {node} button - the button
   */
  function addButtonEventListeners (button) {
    UCASUtilities.toggle.create(
      button.id,
      function open () {
        var formItem = this.$element.parentNode
        var description = formItem.querySelector('.form-item__description')
        formItem.setAttribute('data-form-item-active', 'true')
        formItem.removeAttribute('data-toggle-transition')
        _u.position.resetBreakout(description)
        description.style.maxHeight = description.scrollHeight + 20 + 'px'
        clearTimeout(formItem.transitionTimeout)
        UCASUtilities.toggle.hideOthers(this.id)
      },
      function close () {
        var formItem = this.$element.parentNode
        var description = formItem.querySelector('.form-item__description')
        clearTimeout(formItem.transitionTimeout)
        formItem.setAttribute('data-toggle-transition', '')
        description.style.maxHeight = 0
        formItem.transitionTimeout = setTimeout(function () {
          formItem.removeAttribute('data-toggle-transition')
          formItem.setAttribute('data-form-item-active', 'false')
        }, 500)
      },
      false
    )
  }

  /**
   * Help mouseover handler.
   * @param {Event} e - DOM event
   */
  function formHelpMouseoverHelper (e) {
    // Don't use mouseover if we're focused on a form item.
    if (e.target.closest('.form--dynamic-help[data-form-item-has-focus="true"]')) {
      return
    }

    // If we're not wrapped in .dynamic-help, then exit.
    if (!e.target.closest('.dynamic-help')) {
      return
    }

    if (document.activeElement !== e.target) {
      if (currentFormItem) {
        currentFormItem.setAttribute('data-form-item-mouseover', 'false')
      }
      this.setAttribute('data-form-item-mouseover', 'true')
      currentFormItem = this

      setBreakout(e)
    }
  }

  /**
   * Help mouseleave handler.
   * @param {Event} e - DOM event
   */
  function formHelpMouseleaveHelper (e) {
    if (currentFormItem) {
      currentFormItem.setAttribute('data-form-item-mouseover', 'false')
    }
  }

  /**
   * Help focus handler.
   * @param {Event} e - DOM event
   */
  function formHelpFocusHandler (e) {
    if (document.activeElement === e.target) {
      setBreakout(e)
      currentForm = this
      this.setAttribute('data-form-item-has-focus', 'true')
      this.addEventListener('focusout', formHelpFocusOutHandler)
      document.addEventListener('click', formHelpDocumentClickHandler)
    }
  }

  /**
   * Document click handler.
   * @param {Event} e - DOM event
   */
  function formHelpDocumentClickHandler (e) {
    if (currentFormItem !== undefined && (e.target.form !== currentForm || document.activeElement !== e.target)) {
      currentFormItem.setAttribute('data-form-item-mouseover', 'false')
      currentForm.setAttribute('data-form-item-has-focus', 'false')
      document.removeEventListener('click', formHelpDocumentClickHandler)
    }
  }

  /**
   * Help focus out handler.
   * @param {Event} e - DOM event
   */
  function formHelpFocusOutHandler (e) {
    this.setAttribute('data-form-item-has-focus', 'false')
    this.removeEventListener('focusout', formHelpFocusOutHandler)
  }

  /**
   * Set breakout styles.
   * @param {Event} e - DOM event
   */
  function setBreakout (e) {
    // If we're using .layout-scope, then set a breakout so items don't get cropped.
    if (currentFormItem) {
      var scope = currentFormItem.closest('.layout-scope')
    }
    if (_u.position.breakout && scope) {
      // params: element, scope, origin, left, top
      _u.position.breakout(
        currentFormItem.querySelector('.form-item__description'),
        scope,
        currentFormItem.querySelector('.form-item__description'),
        0,
        0
      )
    }
  }

  // Expose public methods
  global.form_help = {
    addSubscriptions: true,
    init: init,
    destroy: destroy
  }
})(UCASDesignFramework, UCASUtilities)