Source: components/forms/_toggle-help.js

'use strict';

/**
 * Methods for displaying and hiding form help.
 * @namespace toggle_help
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object.
 * @param {object} _u - UCASUtilities object.
 */
(function (global, _u) {
  var userIsTouching

  /**
   * Initialise toggle_help.
   * @function init
   * @memberof! UCASDesignFramework.toggle_help
   * @public
   * @param  {Node} [context=document] - DOM element we're limiting selection to
   * @param  {string} [selector='[data-toggle-help]'] - CSS selector
  */
  function init (context, selector) {
    context = context || document
    selector = selector || '[data-toggle-help]'
    userIsTouching = 'ontouchstart' in document.documentElement
    _u.log.log('userIsTouching', userIsTouching)

    // @todo Convert to something global.
    // @todo If we're confident that userIsTouching has been set correctly we can do away with this here.
    if (!userIsTouching) {
      window.addEventListener('touchstart', function onFirstTouch () {
        userIsTouching = true
        _u.log.log('userIsTouching', userIsTouching)
        addToggleEventListeners(context, selector)
        window.removeEventListener('touchstart', onFirstTouch, false)
      }, false)
    }

    // 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', '')
      })
    }

    addToggleEventListeners(context, selector)
  }

  /**
   * Add toggle event listeners.
   * @param {object} context - DOM element we're limiting selection to
   * @param {string} selector - CSS selector
   */
  function addToggleEventListeners (context, selector) {
    var items = context.querySelectorAll(selector + ' [aria-describedby]')

    _u.forEach(items, function (i, el) {
      var descriptionId = el.getAttribute('aria-describedby')
      if (descriptionId) {
        var description = document.getElementById(descriptionId)

        // Set initial state.
        description.setAttribute('aria-hidden', 'true')

        // Add a button if asked for and we don't have one already.
        if (el.getAttribute('data-toggle-help-button') === 'required') {
          var button = el.parentNode.querySelector('.button--help')
          if (button === null) {
            // Create the button.
            button = document.createElement('button')
            button.setAttribute('class', 'button button--help hide-text')
            button.appendChild(document.createTextNode(el.getAttribute('data-toggle-help-button-label') || 'Help'))
            el.parentNode.insertBefore(button, el.nextSibling)
          }

          addButtonEventListeners(button)
          // Store the descriptionId on the button..
          button.descriptionId = descriptionId
        } else {
          // Otherwise just add normal listeners.
          addFormItemEventListeners(el)
        }

        // Add the descriptionId for future reference.
        description.descriptionId = el.descriptionId = descriptionId
      }
    })
  }

  /**
   * Add toggle event listeners for buttons.
   * @param {node} el - DOM node
   */
  function addButtonEventListeners (el) {
    _u.log.log('addButtonEventListeners', el)
    // Add default event handler.
    el.addEventListener('click', toggleHelpHandler, false)

    if (!userIsTouching) {
      _u.log.log('adding extra event listeners')
      // Add event handlers for mouse and keyboard.
      el.addEventListener('focusin', toggleHelpHandler, false)
      el.addEventListener('blur', toggleHelpHandler, false)
      el.addEventListener('mouseover', toggleHelpHandler, false)
      el.addEventListener('mouseout', toggleHelpHandler, false)

      // Remove any event handlers we don't need for touch.
      // Logic is this way around as addToggleEventListeners() may be called more than once.
    } else {
      _u.log.log('removing event listeners')
      el.removeEventListener('focusin', toggleHelpHandler, false)
      el.removeEventListener('blur', toggleHelpHandler, false)
      el.removeEventListener('mouseover', toggleHelpHandler, false)
      el.removeEventListener('mouseout', toggleHelpHandler, false)
    }
  }

  /**
   * Add toggle event listeners for form items.
   * @param {Node} el - DOM node
   */
  function addFormItemEventListeners (el) {
    _u.log.log('addFormItemEventListeners', el)
    // Add default event handler.
    el.addEventListener('click', toggleHelpHandler, false)
    el.addEventListener('focusin', toggleHelpHandler, false)
    el.addEventListener('blur', toggleHelpHandler, false)

    if (!userIsTouching) {
      _u.log.log('adding extra event listeners')
      // Add event handlers for mouse and keyboard.

      el.addEventListener('mouseover', toggleHelpHandler, false)
      el.addEventListener('mouseout', toggleHelpHandler, false)

      // Remove any event handlers we don't need for touch.
      // Logic is this way around as addToggleEventListeners() may be called more than once.
    } else {
      _u.log.log('removing event listeners')
      // el.removeEventListener('focusin', toggleHelpHandler, false)
      // el.removeEventListener('blur', toggleHelpHandler, false)
      el.removeEventListener('mouseover', toggleHelpHandler, false)
      el.removeEventListener('mouseout', toggleHelpHandler, false)
    }
  }

  /**
   * Event handler.
   * @param {Event} event - DOM event
   */
  function toggleHelpHandler (event) {
    _u.log.log(event.type)
    var descriptionId = event.target.descriptionId
    var isButton = event.target.className.includes('button')
    var targetHelp = prepareToggleHelp(descriptionId)
    if (targetHelp === null) {
      return
    }
    var targetHelpHidden = targetHelp.getAttribute('aria-hidden')
    var previousEventType = targetHelp.lastEventType
    targetHelp.lastEventType = event.type

    event.stopPropagation()

    // If it is a button, we want to prevent it triggering anything else
    // as triggering the help should be the button's singular purpose.
    // Normal buttons shouldn't use this pattern for displaying help as
    // the click can only trigger its default behaviour (think touch screens).
    // We also tweak the behaviour depending on how the user is interacting.
    if (isButton) {
      event.preventDefault()
      // Treat mouseout as a blur.
      if (event.type === 'mouseout' && document.activeElement === event.target && !userIsTouching) {
        document.activeElement.blur()
        return
      }
      if (event.type === 'click' && targetHelpHidden === 'false') {
        // Ignore the first click after receiving focus, as this is probably
        // isn't intended to close the help.
        if (previousEventType === 'focusin') {
          return
        }
        // Treat a click to close the help as a blur.
        if (!userIsTouching) {
          document.activeElement.blur()
          return
        }
      }
    } else {
      // For form elements, don't respond to the mouse if the item is the current focus,
      // thus keeping the help open while the form element is being interacted with.
      if (document.activeElement === event.target && (event.type === 'mouseover' || event.type === 'mouseout')) {
        return
      }
      // For form elements, keep the help open if user focuses on the element.
      // Likely to happen when the user mouses over then clicks.
      if (targetHelpHidden === 'false' && (event.type === 'focusin' || event.type === 'click')) {
        return
      }
    }

    // Logic for all other situations.
    if (event.type === 'focusin' || event.type === 'mouseover') {
      showHelp(targetHelp)
    } else if (event.type === 'blur' || event.type === 'mouseout') {
      hideHelp(targetHelp)
    } else {
      toggleHelp(descriptionId)
    }
  }

  /**
   * Preparation for toggling help.
   * @param {string} descriptionId - the description id
   * @return {null|Node} - null or the target help node
   */
  function prepareToggleHelp (descriptionId) {
    var targetHelp = document.getElementById(descriptionId)
    if (!targetHelp) {
      _u.log.error('Description with ID ' + descriptionId + ' does not exist.')
      return null
    }

    // Clear the timer used for the hide animation.
    if (targetHelp.timer) {
      clearTimeout(targetHelp.timer)
      targetHelp.removeAttribute('data-toggle-hiding')
      targetHelp.timer = 0
    }

    return targetHelp
  }

  /**
   * Toggles help text.
   * @function toggleHelp
   * @memberof! UCASDesignFramework.toggle_help
   * @public
   * @param {string} descriptionId - the description id
   */
  function toggleHelp (descriptionId) {
    var targetHelp = prepareToggleHelp(descriptionId)
    if (targetHelp === null) {
      return
    }

    // Toggle.
    if (targetHelp.getAttribute('aria-hidden') === 'true') {
      _u.log.log('toggle Show ', targetHelp)
      showHelp(targetHelp)
    } else {
      _u.log.log('toggle Hide ', targetHelp)
      hideHelp(targetHelp)
    }
  }

  /**
   * Displays help text.
   * @param {Node} targetHelp - the help node
   */
  function showHelp (targetHelp) {
    // Display immediately.
    _u.log.log('Show ', targetHelp)
    targetHelp.removeAttribute('data-toggle-hiding')
    targetHelp.setAttribute('aria-hidden', 'false')

    // We now look to see if we should work out some positioning for the description text.
    // By doing this at the last minute we avoid unnecessary computations and don't have to worry about window resizing.
    if (!targetHelp.classList.contains('layout-scope__breakout') && targetHelp.parentNode.classList.contains('text-with-toggle-help--attached')) {
      var left = targetHelp.parentNode.offsetWidth - targetHelp.offsetWidth
      var button = targetHelp.parentNode.querySelector('.button--help')
      if (button) {
        var buttonLeft = button.offsetLeft
        targetHelp.style.left = (buttonLeft < left ? buttonLeft : left) + 'px'
        targetHelp.style.top = '20px'
      }
    }
    // If we're using .layout-scope, then the calculations are different.
    var scope = targetHelp.closest('.layout-scope')
    if (_u.position.breakout && scope) {
      var legacy = scope.getAttribute('data-legacy-example') === 'for testing only' // Only used for testing.
      // params: element, scope, origin, left, top, legacy
      _u.position.breakout(
        targetHelp,
        scope,
        targetHelp.closest('.text-with-toggle-help').querySelector('button'),
        30,
        30,
        legacy
      )
    }
  }

  /**
   * Hides help text.
   * @param {Node} targetHelp - the help node
   */
  function hideHelp (targetHelp) {
    // Hide with an animation.
    _u.log.log('Hide ', targetHelp)
    targetHelp.setAttribute('data-toggle-hiding', '')
    targetHelp.setAttribute('aria-hidden', 'true')
    targetHelp.timer = window.setTimeout(function () {
      targetHelp.removeAttribute('data-toggle-hiding')
    }, 800)
  }

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