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