'use strict';
/**
* Accordion setup and control scripts
* @namespace accordion
* @memberof UCASDesignFramework
* @param {object} global - UCASDesignFramework object
* @param {object} _u - UCASUtilities object
*/
(function (global, _u) {
// Create namespace for public methods
global.accordion = {}
var attributePrefix = 'data-'
/**
* Initialise the Accordions
* @function init
* @memberof! UCASDesignFramework.accordion
* @public
* @param {Node} context - DOM element we're limiting selection to
* @todo DF-1123 Make the context optional
*/
function init (context) {
context = context || document
var targets = context.querySelectorAll('[accordion-trigger]')
if (targets.length > 0) {
attributePrefix = ''
} else {
targets = document.querySelectorAll('[' + attributePrefix + 'accordion-trigger]')
}
// Support legacy markup by updating it.
targets = Array.prototype.slice.call(targets)
targets = targets.concat(updateLegacyMarkup(document.querySelectorAll('.accordion__toggle:not([' + attributePrefix + 'accordion-trigger])')))
// Add event listeners for all toggles.
_u.forEach(targets, function (i, el) {
el.addEventListener('click', toggleAccordion)
el.addEventListener('keypress', function (ev) {
toggleAccordion(ev)
})
})
// Set up the controls for any accordions that reqest them.
createControls(context)
var accordionControls = context.querySelectorAll('[' + attributePrefix + 'accordion-controler]')
_u.forEach(accordionControls, function (i, el) {
if (el.attributes[attributePrefix + 'accordion-controler'].value === 'expand') {
el.addEventListener('click', expandAll)
el.addEventListener('keypress', function (ev) {
expandAll(ev)
})
} else if (el.attributes[attributePrefix + 'accordion-controler'].value === 'collapse') {
el.addEventListener('click', collapseAll)
el.addEventListener('keypress', function (ev) {
collapseAll(ev)
})
}
})
}
/**
* Support legacy markup by updating it.
* @param {NodeList} legacyTargets - nodelist of nodes with legacy markup
* @return {Array} - array of updated nodes
*/
function updateLegacyMarkup (legacyTargets) {
if (legacyTargets.length === 0) {
return []
}
_u.forEach(legacyTargets, function (i, el) {
el.setAttribute('aria-expanded', 'false')
var inner = el.parentElement.querySelector('.accordion__inner')
inner.setAttribute(attributePrefix + 'accordion-state', 'collapsed')
inner.setAttribute('aria-hidden', 'true')
})
return Array.prototype.slice.call(legacyTargets)
}
function createControls (context) {
// Create expand/collapse control
var accordionParents = context.querySelectorAll('[' + attributePrefix + 'accordion-controls="true"]')
if (accordionParents.length === 0) {
return
}
var controlMarkup = '<button ' + attributePrefix + 'accordion-controler="expand" tabindex="0" class="button button--secondary">expand all</button><button ' + attributePrefix + 'accordion-controler="collapse" tabindex="0" class="button button--secondary">collapse all</button>'
_u.forEach(accordionParents, function (i, el) {
if (el.querySelector('.accordion-controller-wrapper') === null) {
var expandCollapseControl = context.createElement('div')
expandCollapseControl.classList.add('accordion-controller-wrapper')
expandCollapseControl.innerHTML = controlMarkup
el.insertBefore(expandCollapseControl, el.firstChild)
}
})
}
/**
* Handles accordion behaviour and collapses/expands children and/or siblings
* @param {Event} ev - DOM event
*/
function toggleAccordion (ev) {
var target
if (clickOrKeypress(ev) === 'click') { // Is this a click?
target = this
} else if (clickOrKeypress(ev) === 'enter' || clickOrKeypress(ev) === 'space') { // Is this the enter or space key?
ev.preventDefault()
target = ev.target
} else { // Is this any other key?
return
}
var targetAccordion = target.parentElement.querySelector('.accordion__inner')
// To support legacy markup, we also have to cope with accordion-state not being set:
// !targetAccordion.attributes[attributePrefix + 'accordion-state']
if (!targetAccordion.attributes[attributePrefix + 'accordion-state'] || targetAccordion.attributes[attributePrefix + 'accordion-state'].value === 'collapsed') {
if (getAccordionMode(target) === 'toggle') {
collapseExpandedSiblings(targetAccordion)
}
expandAccordion(targetAccordion)
target.setAttribute('aria-expanded', 'true')
} else {
collapseExpandedChildren(targetAccordion)
collapseAccordion(targetAccordion)
target.setAttribute('aria-expanded', 'false')
}
}
function clickOrKeypress (ev) {
switch (ev.which) {
case 1:
return 'click'
case 13:
return 'enter'
case 32:
return 'space'
default:
return false
}
}
/**
* Collapse all expanded children of passed in element
* @param {DOMelement} el // Parent element
*/
function collapseExpandedChildren (el) {
var elementsTocollapse = el.querySelectorAll('[' + attributePrefix + 'accordion-state="expanded"]')
var accordionToggles = el.querySelectorAll('[aria-expanded="true"]')
_u.forEach(accordionToggles, function (i, el) {
el.setAttribute('aria-expanded', 'false')
})
_u.forEach(elementsTocollapse, function (i, el) {
collapseAccordion(el)
})
}
function collapseAll (ev) {
var target
if (clickOrKeypress(ev) === 'click') { // Is this a click?
target = this
} else if (clickOrKeypress(ev) === 'enter' || clickOrKeypress(ev) === 'space') { // Is this the enter or space key?
target = ev.target
} else { // Is this any other key?
return
}
ev.preventDefault()
var parent = target.parentElement.parentElement
var elementsToCollapse = parent.querySelectorAll('[' + attributePrefix + 'accordion-state="expanded"]')
var expandedToggles = parent.querySelectorAll('[aria-expanded="true"]')
_u.forEach(expandedToggles, function (i, el) {
el.setAttribute('aria-expanded', 'false')
})
_u.forEach(elementsToCollapse, function (i, el) {
collapseAccordion(el)
})
}
function expandAll (ev) {
var target
if (clickOrKeypress(ev) === 'click') { // Is this a click?
target = this
} else if (clickOrKeypress(ev) === 'enter' || clickOrKeypress(ev) === 'space') { // Is this the enter or space key?
target = ev.target
} else { // Is this any other key?
return
}
ev.preventDefault()
var parent = target.parentElement.parentElement
var elementsToExpand = parent.querySelectorAll('[' + attributePrefix + 'accordion-state="collapsed"]')
var expandedToggles = parent.querySelectorAll('[aria-expanded="false"]')
_u.forEach(expandedToggles, function (i, el) {
el.setAttribute('aria-expanded', 'true')
})
_u.forEach(elementsToExpand, function (i, el) {
expandAccordion(el)
})
}
/**
* Collapse all expanded siblings of passed in element
* @param {DOMelement} el // Parent element
*/
function collapseExpandedSiblings (el) {
var accordionParent = el.parentElement.parentElement
var accordionToggles = accordionParent.querySelectorAll('[aria-expanded="true"]')
_u.forEach(accordionToggles, function (i, el) {
el.setAttribute('aria-expanded', 'false')
})
var expandedTargets = accordionParent.querySelectorAll('[' + attributePrefix + 'accordion-state="expanded"]')
_u.forEach(expandedTargets, function (i, el) {
collapseAccordion(el)
})
}
/**
* Gets data attribute from parent element
* Returns value of accordion-mode attribute of the parent accordion or false if not present
* @param {DOMelement} el // Target element
* @return {string|false} - accordion mode or false
*/
function getAccordionMode (el) {
if (el.parentElement.parentElement.attributes[attributePrefix + 'accordion-mode']) {
return el.parentElement.parentElement.attributes[attributePrefix + 'accordion-mode'].value
} else {
return false
}
}
function expandAccordion (target) {
target.setAttribute(attributePrefix + 'accordion-state', 'expanded')
target.setAttribute('aria-hidden', 'false')
target.previousElementSibling.setAttribute('aria-expanded', true)
// Make sure we remove the hiding attribute if the accordion is quickly re-opened.
target.removeAttribute('data-accordion-hiding')
// Setting a data attribute so we can control when the element is set to height:auto
// which we want to do to make sure all content is visible.
target.setAttribute('data-accordion-expanding', '')
// This timeout is not strictly necessary but allows for CSS animations to take place beforehand.
target.timer = window.setTimeout(function () {
target.removeAttribute('data-accordion-expanding')
}, 400)
// @event designFrameworkAccordionExpandAccordion
var event = document.createEvent('Event')
event.initEvent('designFrameworkAccordionExpandAccordion', true, true)
target.dispatchEvent(event)
}
function collapseAccordion (target) {
target.setAttribute(attributePrefix + 'accordion-state', 'collapsed')
target.setAttribute('aria-hidden', 'true')
target.previousElementSibling.setAttribute('aria-expanded', false)
var expandedChildren = target.parentElement.querySelectorAll('[' + attributePrefix + 'accordion-state="expanded"]')
_u.forEach(expandedChildren, function (i, el) {
el.setAttribute(attributePrefix + 'accordion-state', 'collapsed')
el.setAttribute('aria-hidden', 'true')
el.previousElementSibling.setAttribute('aria-expanded', false)
})
// Make sure we remove the expanding attribute if the accordion is quickly re-closed.
target.removeAttribute('data-accordion-expanding')
// Setting a data attribute so we can control when the element is set to display:none
// which we want to do to avoid it being tabable when hidden.
target.setAttribute('data-accordion-hiding', '')
target.setAttribute('aria-hidden', 'true')
// This timeout is not strictly necessary but allows for CSS animations to take place beforehand.
target.timer = window.setTimeout(function () {
target.removeAttribute('data-accordion-hiding')
}, 400)
// @event designFrameworkAccordionCollapseAccordion
var event = document.createEvent('Event')
event.initEvent('designFrameworkAccordionCollapseAccordion', true, true)
target.dispatchEvent(event)
}
/**
* Assign public methods.
*/
global.accordion = {
addSubscriptions: true,
init: init
}
})(UCASDesignFramework, UCASUtilities)