Source: components/help/floating-help/_floating-help.js

'use strict';

/**
 * Floating help.
 * @namespace floatingHelp
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object.
 * @param {object} _u - UCASUtilities object.
 */
(function (global, _u) {
  var floatingHelpContainer
  var floatingHelp
  var floatingHelpTrigger
  var floatingHelpBackButton
  var floatingHelpBottom
  var floatingHelpMaster
  var floatingHelpDetails
  var footer
  var clientWidth
  var currentScreenSize
  var observer = null

  /**
   * Initialise the floating help.
   * @function init
   * @memberof! UCASDesignFramework.floatingHelp
   * @public
   */
  function init () {
    // Only one floating help component is expected to be on the page,
    // so these selectors only take account of the first one they encounter.
    floatingHelpContainer = document.querySelector('.floating-help-container')
    floatingHelp = document.querySelector('.floating-help-container .floating-help')
    floatingHelpTrigger = document.querySelector('.floating-help-container .floating-help-trigger')
    floatingHelpMaster = document.querySelector('.floating-help-container .floating-help__master')
    footer = document.getElementById('section--footer')

    if (floatingHelp === null || floatingHelpContainer === null || floatingHelpMaster === null) {
      return
    }

    // Set the details to their initial state.
    floatingHelpDetails = floatingHelp.querySelectorAll('.floating-help__detail')
    floatingHelpDetails.forEach(function (detail) {
      detail.hidden = true
      detail.setAttribute('tabIndex', '0')
      detail.setAttribute('aria-hidden', 'true')
    })

    // Make sure we can give the master focus.
    floatingHelpMaster.setAttribute('tabIndex', '0')

    // Give the main section an ID so we can refer to it using aria-controls.
    floatingHelp.id = 'floatingHelp'

    clientWidth = document.body.clientWidth
    // IE won't get this Observer, which adds a scroll listener, but that's OK,
    // it's a nicety and not essential.
    if ('IntersectionObserver' in window && observer === null) {
      observer = new IntersectionObserver(intersectionHandler)
    }

    prepareButtons()
    preparefloatingHelp(calculateScreenSize())
    addAnchorListeners()

    window.addEventListener('dfResize', resizeEventHandler, false)
  }

  /**
   * Clear out all the local variables and remove the event listeners.
   * @function destroy
   * @memberof! UCASDesignFramework.floatingHelp
   * @public
   */
  function destroy () {
    closeAndReset()

    if (floatingHelpTrigger) {
      floatingHelpTrigger.parentElement.removeChild(floatingHelpTrigger)
    }

    if (floatingHelpBackButton) {
      floatingHelpBackButton.parentElement.removeChild(floatingHelpBackButton)
    }

    if (observer) {
      observer.disconnect()
    }

    floatingHelpContainer = null
    floatingHelp = null
    floatingHelpTrigger = null
    floatingHelpBackButton = null
    floatingHelpBottom = null
    footer = null
    clientWidth = null
    currentScreenSize = null
    observer = null

    window.removeEventListener('dfResize', resizeEventHandler, false)
    window.removeEventListener('dfScroll', scrollEventHandler, false)
  }

  /**
   * Make adjustments according to screen size.
   */
  function resizeEventHandler () {
    // Check width as triggering the keyboard on Android changes page size.
    // We're only interested if the width changes.
    if (clientWidth !== document.body.clientWidth) {
      clientWidth = document.body.clientWidth
      closeFloatingHelp()
      preparefloatingHelp(calculateScreenSize())
    }
  }

  /**
   * Recalculate the position on scroll.
   */
  function scrollEventHandler () {
    avoidFooter()
  }

  /**
   * Position above the footer if necessary.
   */
  function avoidFooter () {
    var distanceFromBottom = parseFloat(window.innerHeight + window.scrollY - footer.offsetTop)
    if (distanceFromBottom > 0) {
      floatingHelpTrigger.style.bottom = distanceFromBottom + 'px'
      floatingHelp.style.bottom = (floatingHelpBottom + distanceFromBottom) + 'px'
    } else {
      floatingHelpTrigger.style.bottom = null
      floatingHelp.style.bottom = null
    }
  }

  /**
   * Decide how to display the floating help.
   * @return {string} - mobile|desktop
   */
  function calculateScreenSize () {
    return global.size.cssBreakpoint === 'xsmall' ? 'mobile' : 'desktop'
  }

  /**
   * Prepare the toggle button.
   * @function createButton
   * @memberof! UCASDesignFramework.floatingHelp
   */
  function prepareButtons () {
    if (floatingHelpTrigger == null || floatingHelpBackButton == null) {
      createButtons()
    }
    addButtonListeners()
  }

  /**
   * Create the toggle and back buttons if they don't already exist.
   * @function createButtons
   * @memberof! UCASDesignFramework.floatingHelp
   */
  function createButtons () {
    // Add the trigger button.
    if (!floatingHelpTrigger) {
      // Create the trigger button.
      floatingHelpTrigger = document.createElement('button')
      floatingHelpTrigger.setAttribute('class', 'button floating-help-trigger')
      floatingHelpTrigger.setAttribute('aria-controls', 'floatingHelp')
      floatingHelpTrigger.setAttribute('aria-expanded', 'false')
      floatingHelpTrigger.innerText = floatingHelp.getAttribute('data-floating-help-trigger-label') || 'Help'

      floatingHelpContainer.insertBefore(floatingHelpTrigger, floatingHelp)
    }
    // Add the back button.
    if (!floatingHelpBackButton) {
      // Create the trigger button.
      floatingHelpBackButton = document.createElement('button')
      floatingHelpBackButton.setAttribute('class', 'button button--back floating-help__back')
      floatingHelpBackButton.innerText = floatingHelp.getAttribute('data-floating-help-back-label') || 'Back'

      floatingHelpDetails[0].insertBefore(floatingHelpBackButton, floatingHelpDetails[0].firstChild)
    }
  }

  /**
   * Add the button listeners.
   * @function addButtonListeners
   * @memberof! UCASDesignFramework.floatingHelp
   */
  function addButtonListeners () {
    floatingHelpTrigger.addEventListener('click', toggleFloatingHelp, false)
    floatingHelpBackButton.addEventListener('click', floatingHelpBackClickHandler, false)
  }

  /**
   * Toggle the help.
   * @function toggleFloatingHelp
   * @memberof! UCASDesignFramework.floatingHelp
   */
  function toggleFloatingHelp () {
    floatingHelp.getAttribute('aria-hidden') === 'false' ? closeFloatingHelp() : openFloatingHelp()
  }

  /**
   * Close the help.
   * @function closeFloatingHelp
   * @memberof! UCASDesignFramework.floatingHelp
   */
  function closeFloatingHelp () {
    if (floatingHelp) {
      floatingHelp.setAttribute('aria-hidden', 'true')
      floatingHelpTrigger.setAttribute('aria-expanded', 'false')
      window.setTimeout(function () {
        floatingHelp.hidden = true
        global.size.unlockPosition() // Not just doing this on mobile as we may have switched size while the help was open.
      }, 300)
    }
  }

  /**
   * Close the help with no delay.
   * @function closeImmediately
   * @memberof! UCASDesignFramework.floatingHelp
   */
  function closeAndReset () {
    if (floatingHelp) {
      floatingHelp.setAttribute('aria-hidden', 'true')
      floatingHelpTrigger.setAttribute('aria-expanded', 'false')
      floatingHelp.hidden = true
      global.size.unlockPosition() // Not just doing this on mobile as we may have switched size while the help was open.
      floatingHelpBackClickHandler()
    }
  }

  /**
   * Open the help.
   * @function openFloatingHelp
   * @memberof! UCASDesignFramework.floatingHelp
   */
  function openFloatingHelp () {
    if (floatingHelp) {
      floatingHelp.hidden = false
      floatingHelpMaster.focus()
      window.setTimeout(function () {
        floatingHelp.setAttribute('aria-hidden', 'false')
        floatingHelpTrigger.setAttribute('aria-expanded', 'true')
        if (calculateScreenSize() === 'mobile') {
          global.size.lockPosition()
        }
      }, 10)
    }
  }

  /**
   * Render the floating help.
   * @param {string} screenSize - mobile|desktop
   */
  function preparefloatingHelp (screenSize) {
    if (screenSize === 'desktop') {
      avoidFooter()
    }

    if (screenSize === currentScreenSize) {
      // Don't do anything if nothing has changed.
      return
    }

    currentScreenSize = screenSize
    floatingHelp.hidden = true

    if (screenSize === 'mobile') {
      // We don't have to worry about the position of the help text.
      if (footer && observer) {
        observer.unobserve(footer)
      }
      window.removeEventListener('dfScroll', scrollEventHandler, false)
      floatingHelp.style.bottom = floatingHelpTrigger.style.bottom = ''
    } else if (screenSize === 'desktop') {
      // Use the IntersectionObserver to see whether we need to add the scroll event listener.
      var options = {
        root: null,
        threshold: '0'
      }
      if (observer) {
        observer.observe(footer, options)
      }
    }
  }

  /**
   * Add the anchor listeners.
   * @function addAnchorListeners
   * @memberof! UCASDesignFramework.floatingHelp
   */
  function addAnchorListeners () {
    var floatingHelpAnchors = floatingHelp.querySelectorAll('.floating-help__anchor')
    floatingHelpAnchors.forEach(function (anchor) {
      anchor.addEventListener('click', floatingHelpAnchorClickHandler, false)
    })
  }

  /**
   * Handle the intersectionObserver.
   * This is used to add a scroll event listener if required.
   * @param {Array.<IntersectionObserverEntry>} entries - the list of entries
   * @param {IntersectionObserver} observer - the observer
   */
  function intersectionHandler (entries, observer) {
    entries.forEach(function (entry) {
      if (entry.isIntersecting) {
        // Calculate the bottom of the help text.
        // Only do this the once here, as otherwise we may risk adding to existing values if we do it in the scroll handler.
        floatingHelpBottom = parseFloat(window.getComputedStyle(floatingHelp).bottom)
        // Add the scroll handler.
        scrollEventHandler()
        window.addEventListener('dfScroll', scrollEventHandler, false)
      } else {
        // Remove the scroll handler.
        scrollEventHandler()
        floatingHelpBottom = null
        window.removeEventListener('dfScroll', scrollEventHandler, false)
      }
    })
  }

  /**
   * Handle the back button.
   * @function floatingHelpBackClickHandler
   * @memberof! UCASDesignFramework.floatingHelp
   */
  function floatingHelpBackClickHandler () {
    // When we click back, we're hiding the detail panel and the back button
    // both visually and also from the tab index.
    var detail = floatingHelp.querySelector('[aria-hidden="false"]')
    if (detail) {
      floatingHelpMaster.hidden = false
      detail.setAttribute('aria-hidden', 'true')
      // The parent has an attribute set so it can change its styles.
      floatingHelp.setAttribute('data-floating-help-detail-state', 'hidden')
      // We delay hiding the detail so that the animation can play out.
      window.setTimeout(function () {
        detail.hidden = true
        floatingHelpMaster.focus()
      }, 300)
    }
  }

  /**
   * Handle the anchors.
   * @function floatingHelpAnchorClickHandler
   * @memberof! UCASDesignFramework.floatingHelp
   * @param {Event} e - the click event.
   */
  function floatingHelpAnchorClickHandler (e) {
    e.preventDefault()
    var detail = document.getElementById(this.hash.substring(1))
    if (detail) {
      detail.hidden = false
      detail.insertBefore(floatingHelpBackButton, detail.firstChild)
      // The parent has an attribute set so it can change its styles.
      floatingHelp.setAttribute('data-floating-help-detail-state', 'visible')
      // Slight delay needed to give the animation a chance to start.
      window.setTimeout(function () {
        detail.setAttribute('aria-hidden', 'false')
      }, 10)
      // We delay hiding the master so that the animation can play out.
      window.setTimeout(function () {
        floatingHelpMaster.hidden = true
        detail.focus()
      }, 300)
    }
  }

  /**
   * Assign public methods.
   */
  global.floatingHelp = {
    addSubscriptions: true,
    init: init,
    destroy: destroy,
    open: openFloatingHelp,
    close: closeFloatingHelp,
    closeAndReset: closeAndReset,
    refresh: init
  }
})(UCASDesignFramework, UCASUtilities)