Source: components/navigation/sticky-nav/_sticky-nav.js

'use strict';

/**
 * Sticky nav.
 * @namespace stickyNav
 * @memberof UCASDesignFramework
 * @param {object} global - UCASDesignFramework object.
 * @param {object} _u - UCASUtilities object.
 */
(function (global, _u) {
  var stickyNavContainer
  var stickyNavContainerOffsetTop
  var stickyNavContainerOffsetHeight
  var stickyNav
  var stickyNavFixed = false
  var stickyNavLabel
  var stickyNavToggleButton // Used to toggle navigation.
  var stickyNavActionsIds = []
  var stickyNavActions = [] // Reference to existing buttons/links that perform arbitary actions.
  var stickyNavMenuItemsIds = []
  var stickyNavMenuItems = [] // Reference to existing buttons/links that perform arbitary actions.
  var resizeTimeout
  var scrollTimeout
  var clientWidth
  var currentScreenSize

  /**
   * Initialise the sticky nav.
   * @function init
   * @memberof! UCASDesignFramework.stickyNav
   * @public
   */
  function init () {
    stickyNavContainer = document.querySelector('.sticky-nav-container')
    stickyNav = document.querySelector('.sticky-nav-container .sticky-nav')

    if (stickyNav === null || stickyNavContainer === null) {
      return
    }

    stickyNavActionsIds = _u.json.getJson(stickyNavContainer.getAttribute('data-sticky-nav-actions'))
    stickyNavMenuItemsIds = _u.json.getJson(stickyNavContainer.getAttribute('data-sticky-nav-menuitems'))

    clientWidth = document.body.clientWidth
    prepareStickyNav(calculateScreenSize())

    window.addEventListener('resize', resizeThrottler, false)
  }

  /**
   * Set to prepared desktop state, the local variables to their initial state and remove the event listener
   * @function destroy
   * @memberof! UCASDesignFramework.stickyNav
   * @public
   */
  function destroy () {
    // set to prepared desktop state
    prepareStickyNav('desktop')

    // initialise local variables
    stickyNavContainer = null
    stickyNavContainerOffsetTop = null
    stickyNavContainerOffsetHeight = null
    stickyNav = null
    stickyNavFixed = false
    stickyNavLabel = null
    stickyNavToggleButton = null
    stickyNavActionsIds = []
    stickyNavActions = []
    stickyNavMenuItemsIds = []
    stickyNavMenuItems = []
    resizeTimeout = null
    scrollTimeout = null
    clientWidth = null
    currentScreenSize = null

    // remove listener
    window.removeEventListener('resize', resizeThrottler, false)
  }

  /**
   * Slow down the resize handler so we don't repaint too much.
   * @TODO This can be generalised and put into _size.js
   */
  function resizeThrottler () {
    if (!resizeTimeout) {
      resizeTimeout = setTimeout(function () {
        resizeTimeout = null

        // 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
          // Set fixed state back to false.
          unsetFixed()
          prepareStickyNav(calculateScreenSize())
        }
      }, 100)
    }
  }

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

  /**
   * Create the toggle button if it doesn't already exist.
   * @function createButton
   * @memberof! UCASDesignFramework.stickyNav
   * @public
   */
  function createButton () {
    // Add the trigger button.
    if (!stickyNavToggleButton) {
      // Create the trigger button.
      stickyNav.id = stickyNav.id || _u.unique.genId()
      stickyNavLabel = stickyNav.getAttribute('data-sticky-nav-label')
      stickyNavToggleButton = document.createElement('button')
      stickyNavToggleButton.setAttribute('class', 'button sticky-nav-button')
      stickyNavToggleButton.setAttribute('id', 'sticky-nav-button')
      stickyNavToggleButton.setAttribute('aria-controls', stickyNav.id)
      stickyNavToggleButton.setAttribute('aria-expanded', 'false')
      setTitle()
      stickyNavContainer.insertBefore(stickyNavToggleButton, stickyNav)
      // https://www.w3.org/WAI/GL/wiki/Using_aria-expanded_to_indicate_the_state_of_a_collapsible_element#Examples
      _u.toggle.create(
        'sticky-nav-button',
        function () {
          stickyNav.style.display = ''
          stickyNav.style.overflowY = 'hidden' // No overflow is nicer for the animation.
          setTimeout(function () {
            // Slight pause so the DOM has a chance to realise the node is visible.
            stickyNavToggleButton.setAttribute('aria-expanded', 'true')
          }, 10)
          setTimeout(function () {
            // Restore the overflow after the animation has finished.
            stickyNav.style.overflowY = ''
          }, 500)
        },
        function () {
          stickyNavToggleButton.setAttribute('aria-expanded', 'false')
          setTimeout(function () {
            // Slow down the hiding while we wait for the animation to finish.
            stickyNav.style.display = 'none'
          }, 500)
        },
        true
      )
    }
  }

  /**
   * Render the sticky nav.
   * @param {string} screenSize - mobile|desktop
   */
  function prepareStickyNav (screenSize) {
    if (screenSize === currentScreenSize) {
      // Don't do anything if nothing has changed.
      return
    }

    currentScreenSize = screenSize

    if (screenSize === 'mobile') {
      // Set state.
      createButton()
      stickyNavContainerOffsetHeight = stickyNavContainer.offsetHeight
      stickyNavContainerOffsetTop = stickyNavContainer.offsetTop
      document.documentElement.setAttribute('data-sticky-nav', 'active')
      // Move in actions from elsewhere.
      if (stickyNavActionsIds && stickyNavActionsIds.length) {
        stickyNavActions = moveItems(screenSize, stickyNavActionsIds, stickyNavActions, stickyNavContainer)
      }
      // Move in menuItems from elsewhere.
      if (stickyNavMenuItemsIds && stickyNavMenuItemsIds.length) {
        stickyNavMenuItems = moveItems(screenSize, stickyNavMenuItemsIds, stickyNavMenuItems, stickyNav)
      }
      // Set an attribute on the parent if needed.
      if (!stickyNavContainer.previousElementSibling && !stickyNavContainer.nextElementSibling) {
        stickyNavContainer.parentElement.setAttribute('data-sticky-nav-container-parent-empty', '')
      }
      // Give a height to the parent, to avoid scroll jumps when the menu is set to fixed.
      stickyNavContainer.parentElement.style.height = stickyNavContainer.parentElement.clientHeight + 'px'
      // Set the top of the stickynav so it can cope with the container expanding.
      stickyNav.style.top = stickyNavContainerOffsetHeight + 'px'
      // Hide the menu from screen readers.
      stickyNav.style.display = 'none'
      // Add the event listener.
      window.addEventListener('scroll', scrollThrottler)
    } else if (screenSize === 'desktop') {
      // Remove fixed attribute.
      stickyNavContainer.removeAttribute('data-sticky-nav-container-fixed')
      stickyNavContainer.parentElement.removeAttribute('data-sticky-nav-container-parent-empty')
      stickyNavContainer.parentElement.style.height = ''
      // Remove the trigger button.
      if (stickyNavToggleButton) {
        stickyNavContainer.removeChild(stickyNavToggleButton)
        _u.toggle.destroy(stickyNavToggleButton.id)
        stickyNavToggleButton = null
      }
      // Reset actions.
      if (stickyNavActionsIds && stickyNavActionsIds.length) {
        stickyNavActions = moveItems(screenSize, stickyNavActionsIds, stickyNavActions, stickyNavContainer)
      }
      // reset menuItems.
      if (stickyNavMenuItemsIds && stickyNavMenuItemsIds.length) {
        stickyNavMenuItems = moveItems(screenSize, stickyNavMenuItemsIds, stickyNavMenuItems, stickyNav)
      }
      // Remove the top.
      stickyNav.style.top = ''
      // Remove the display override.
      stickyNav.style.display = ''
      // Remove scroll listener.
      window.removeEventListener('scroll', scrollThrottler)
      document.documentElement.removeAttribute('data-sticky-nav')
    }
  }

  /**
   * Render the additional actions for the sticky nav.
   * @param {string} screenSize - mobile|desktop
   * @param {Array} itemsIds - an array of IDs of items to be moved
   * @param {Array} itemsReference - an internal array of references to the DOM elements
   * @param {Node} container - the node to move elements into or out of
   * @return {Array} empty or a list of nodes that have been moved.
   */
  function moveItems (screenSize, itemsIds, itemsReference, container) {
    var items = []

    if (screenSize === 'mobile') {
      if (itemsReference.length === 0) {
        itemsIds.forEach(function (id) {
          var el = document.getElementById(id)
          if (el) {
            var item = {}
            item.el = el
            item.originalParent = el.parentElement
            item.originalNextSibling = el.nextElementSibling
            items.push(item)
            container.appendChild(el)

            if (item.originalParent.childElementCount === 0) {
              // Empty out the parent element properly if there's nothing left in it.
              item.originalParent.innerHTML = ''
            }
          }
        })
      }
    } else if (screenSize === 'desktop') {
      // Reverse the array, so last added items are dealt with first.
      if (itemsReference.length) {
        itemsReference.reverse().forEach(function (item) {
          item.originalParent.insertBefore(item.el, item.originalNextSibling)
        })
      }
    }

    return items
  }

  /**
   * Slow down the scroll handler so we don't repaint too much.
   * @TODO This can be generalised and put into _size.js
   */
  function scrollThrottler () {
    if (!scrollTimeout) {
      scrollTimeout = setTimeout(function () {
        scrollTimeout = null
        var scrollTop = (document.scrollingElement || document.documentElement).scrollTop

        if (stickyNavContainerOffsetTop - scrollTop <= 0) {
          setFixed()
        } else {
          unsetFixed()
        }
      }, 100)
    }
  }

  /**
   * Set the fixed state of the sticky nav.
   */
  function setFixed () {
    if (!stickyNavFixed) {
      stickyNavContainer.setAttribute('data-sticky-nav-container-fixed', 'true')
      stickyNavFixed = true
    }
  }

  /**
   * Set the title.
   * @function setTitle
   * @memberof! UCASDesignFramework.stickyNav
   * @public
   * @param {string} text - the plain text title
   */
  function setTitle (text) {
    stickyNavToggleButton.innerText = text || stickyNavLabel || 'Skip to section'
  }

  /**
   * Unset the fixed state of the sticky nav.
   */
  function unsetFixed () {
    if (stickyNavFixed) {
      stickyNavContainer.setAttribute('data-sticky-nav-container-fixed', 'false')
      stickyNavFixed = false
    }
  }

  /**
   * Assign public methods.
   */
  global.stickyNav = {
    addSubscriptions: true,
    init: init,
    destroy: destroy,
    setTitle: setTitle
  }
})(UCASDesignFramework, UCASUtilities)