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