collapsible-block
Version:
Lightweight Collapse/Expand component library.
976 lines (763 loc) • 32.2 kB
JavaScript
/**
* File collapsible-block.js.
*
* Implement collapsible block functionality.
*/
(function (root, factory) {
if ( typeof define === 'function' && define.amd ) {
define([], factory(root));
} else if ( typeof exports === 'object' ) {
module.exports = factory(root);
} else {
root.CollapsibleBlock = factory(root);
}
})(typeof global !== 'undefined' ? global : this.window || this.global, function (root) {
'use strict';
var _hasInitialized = false;
var _publicMethods = {
managers: [],
states: {
COLLAPSED: 'collapsed',
FIRST_EXPANDED: 'first-expanded',
EXPANDED: 'expanded',
},
};
var _settings = { };
var _defaults = {
bodyClass: 'has-collapsible-block',
elementSelector: '[data-collapsible]',
contentElementSelector: '[data-collapsible-content]',
contentInnerSelector: '.collapsible-content__inner',
handlerSelector: '[data-collapsible-handler]',
handlerMultiTargetSelector: '[data-collapsible-targets]',
autoFocusSelector: '[data-autofocus]',
focusableElementsSelector: 'a[href], a[role="button"]:not([disabled]), button:not([disabled]), input:not([disabled]):not([type="hidden"]), textarea:not([disabled]), select:not([disabled]), details, summary, iframe, object, embed, [contenteditable] [tabindex]:not([tabindex="-1"])',
selectContentsOnFocus: true,
isCollapsedClass: 'is-collapsed',
isExpandedClass: 'is-expanded',
isActivatedClass: 'is-activated',
isTransitioningClass: 'is-transitioning',
cssTransition: 'height .15s linear',
targetAttribute: 'aria-controls',
multiTargetAttribute: 'data-collapsible-targets',
maxHeightAttribute: 'data-collapsible-max-height',
createHandlerAttribute: 'data-collapsible-create-handler',
changeStateOnResizeAttribute: 'data-collapsible-change-state-resize',
initialState: _publicMethods.states.FIRST_EXPANDED,
initialStateAttribute: 'data-collapsible-initial-state',
idPrefix: 'collapsible',
createHandler: false,
maxHeight: 0,
handlerTemplate: '<a href="#collapsible" role="button" data-collapsible-handler>Read more</a>',
contentInnerTemplate: '<div class="collapsible-content__inner"></div>',
};
var _key = {
ENTER: 'Enter',
SPACE: ' ',
}
var _canChangeFocus = false;
/**!
* Merge two or more objects together.
* (c) 2017 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {Boolean} deep If true, do a deep (or recursive) merge [optional]
* @param {Object} objects The objects to merge together
* @returns {Object} Merged values of defaults and options
*/
var extend = function () {
// Variables
var extended = {};
var deep = false;
var i = 0;
// Check if a deep merge
if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
deep = arguments[0];
i++;
}
// Merge the object into the extended object
var merge = function (obj) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
// If property is an object, merge properties
if (deep && Object.prototype.toString.call(obj[prop]) === '[object Object]') {
extended[prop] = extend(extended[prop], obj[prop]);
} else {
extended[prop] = obj[prop];
}
}
}
};
// Loop through each object and conduct a merge
for (; i < arguments.length; i++) {
var obj = arguments[i];
merge(obj);
}
return extended;
};
/**
* Provide a crossbrowser way to determine which
* transitionend event is supported by the current browser.
*
* Based on the work of:
* Jonathan Suh - https://jonsuh.com/blog/detect-the-end-of-css-animations-and-transitions-with-javascript/
* David Walsh - https://davidwalsh.name/css-animation-callback
*
* @return {String} The transitionend event name
*/
var getTransitionEndEvent = function() {
var t;
var el = document.createElement('fakeelement');
var transitions = {
'transition':'transitionend',
'OTransition':'oTransitionEnd',
'MozTransition':'transitionend',
'WebkitTransition':'webkitTransitionEnd'
}
for( t in transitions ){
if( el.style[t] !== undefined ){
return transitions[t];
}
}
return 'transitionend';
};
/**
* Trigger a reflow, flushing the CSS changes.
*
* @param HTMLElement element Element to get the computed height value.
*
* @see https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
var reflow = function( element ) {
// Set element as the body when not provided
if ( ! element ) {
element = document.body;
}
element.offsetHeight;
}
/**
* Check if the element is considered visible. Does not consider the CSS property `visibility: hidden;`.
*/
var isVisible = function( element ) {
return !!( element.offsetWidth || element.offsetHeight || element.getClientRects().length );
}
/**
* Gets keyboard-focusable elements within a specified element
*
* @param HTMLElement element The element to search within. Defaults to the `document` root element.
*
* @return NodeList All focusable elements withing the element passed in.
*/
var getFocusableElements = function( element ) {
// Set element to `document` root if not passed in
if ( ! element ) { element = document; }
// Get elements that are keyboard-focusable
return element.querySelectorAll( _settings.focusableElementsSelector );
}
/**
* Handle toggle events for handlers with multiple targets.
*
* @param HTMLElement handlerElement Handler element.
*/
var handleMultipleTargets = function( handlerElement ) {
// Bail if handler element is not valid
if ( ! handlerElement ) { return; }
// Get target ids
var multiTargetIds = handlerElement.getAttribute( _settings.multiTargetAttribute );
var targetIds = multiTargetIds.split( ',' );
// Iterate targetIds
for ( var i = 0; i < targetIds.length; i++) {
var targetId = targetIds[i];
// Get target element
var targetElement = document.querySelector( '#' + targetId.trim() );
if ( targetElement ) {
// Get the collapsbile element
var element = targetElement.closest( _settings.elementSelector );
// Maybe toggle collapsbile element state
if ( element ) {
_publicMethods.toggleState( element );
}
}
}
}
/**
* Handle toggle events for handlers with single targets.
*
* @param HTMLElement handlerElement Handler element.
*/
var handleSingleTarget = function( handlerElement ) {
// Bail if handler element is not valid
if ( ! handlerElement ) { return; }
// Get target element
var targetElement = document.querySelector( '#' + handlerElement.getAttribute( _settings.targetAttribute ) );
// Get target element from the handler element
if ( ! targetElement ) {
targetElement = handlerElement;
}
// Get the collapsbile element
var element = targetElement.closest( _settings.elementSelector );
// Maybe toggle collapsbile element state
if ( element ) {
_publicMethods.toggleState( element );
}
}
/**
* Route click events
*/
var handleClick = function( e ) {
if ( e.target.closest( _settings.handlerSelector ) && e.target.closest( _settings.handlerMultiTargetSelector ) ) {
e.preventDefault();
var handlerElement = e.target.closest( _settings.handlerMultiTargetSelector );
handleMultipleTargets( handlerElement );
}
else if ( e.target.closest( _settings.handlerSelector ) ) {
e.preventDefault();
var handlerElement = e.target.closest( _settings.handlerSelector );
handleSingleTarget( handlerElement );
}
}
/**
* Handle keypress event.
*/
var handleKeyDown = function( e ) {
// Should do nothing if the default action has been cancelled
if ( e.defaultPrevented ) { return; }
// ENTER or SPACE on handler element
if ( ( e.key == _key.ENTER || e.key == _key.SPACE ) && e.target.closest( _settings.handlerSelector ) ) {
// Similate click
handleClick( e );
}
};
/**
* Create handler element
*/
var createHandlerElement = function( manager ) {
var element = manager.element;
var contentElement = manager.contentElement;
var handler = document.createElement('div');
handler.innerHTML = manager.settings.handlerTemplate.trim();
manager.handlerElement = handler.childNodes[0];
manager.handlerElement.setAttribute( manager.settings.targetAttribute, contentElement.id );
element.insertBefore( handler.childNodes[0], contentElement.nextSibling );
}
/**
* Create content inner element
*/
var maybeCreateContentInnerElement = function( manager ) {
// Bail if content inner element already exists
if ( manager.contentElement.querySelector( manager.settings.contentInnerSelector ) ) { return; }
var contentElement = manager.contentElement;
var newContentPlaceholder = document.createElement('div');
newContentPlaceholder.innerHTML = manager.settings.contentInnerTemplate.trim();
var contentInner = newContentPlaceholder.childNodes[0];
// Move content to new content inner element
contentInner.innerHTML = contentElement.innerHTML;
contentElement.innerHTML = newContentPlaceholder.innerHTML;
}
/**
* Get the element's computed `height` even when hidden or collapsed.
*
* @param HTMLElement element Element to get the computed height value.
*
* @return Number The computed height value of the element.
*/
var getComputedHeight = function( element ) {
// Get original element style values
var originalPosition = element.style.position;
var originalDisplay = element.style.display;
var originalVisibility = element.style.visibility;
var originalTransition = element.style.transition;
var originalHeight = element.style.height;
// Set element styles prior to getting its height
element.style.position = 'absolute';
element.style.display = 'block';
element.style.visibility = 'hidden';
element.style.transition = 'none';
element.style.height = '';
// Get the element's natural height
var computedHeight = element.scrollHeight;
// Set element styles back to original values
element.style.position = originalPosition;
element.style.display = originalDisplay;
element.style.visibility = originalVisibility;
element.style.transition = originalTransition;
element.style.height = originalHeight;
return computedHeight;
}
/**
* Get the element's current used `height` space, even in the middle of a transition.
*
* @param HTMLElement element Element to get the current height value.
*
* @return Number The current height value of the element.
*/
var getCurrentHeight = function( element ) {
return element.getBoundingClientRect().height;
}
/**
* Set the height of the content element.
*
* @param HTMLElement element Collapsible block content element.
* @param Number size New height value for the content element in pixels. The string `px` will be added to the value before setting it to the element's style property.
* @param Boolean withTransition Whether to use transitions between states.
*/
var setHeight = function( element, size, withTransition ) {
// Set default value for withTransition
withTransition = withTransition === false ? false : true;
// Remove element's transition
var originalTransition;
if ( ! withTransition ) {
originalTransition = element.style.transition;
element.style.transition = 'none';
}
// Set the element's new height
element.style.height = size + 'px';
// Restore element's transition
if ( ! withTransition ) {
// Trigger a reflow, flushing the CSS changes
reflow( element );
// Set element styles back to original values
element.style.transition = originalTransition;
}
}
/**
* Get the CSS transition duration in milliseconds for the element.
*
* @param HTMLElement element Element to get the transition duration value.
*/
var getHeightTransitionDuration = function( element ) {
// Bail as zero if element is invalid
if ( ! element ) { return 0; }
// Get transition duration for the height property
var transitionProperties = window.getComputedStyle( element ).transitionProperty;
var transitionDurations = window.getComputedStyle( element ).transitionDuration;
// Bail as zero if transition styles are not set
if ( ! transitionProperties || ! transitionDurations ) { return 0; }
// Get transition properties and durations in lists
var transitionPropertiesList = transitionProperties.split( ', ' );
var transitionDurationsList = transitionDurations.split( ', ' );
// Get index of the `height` property in the transition properties list
var heightPropertyIndex = transitionPropertiesList.indexOf( 'height' );
// Get the duration of the `height` property transition in milliseconds
var heightTransitionValue = transitionDurationsList[ heightPropertyIndex ];
var heightTransitionInMilliseconds = heightTransitionValue && heightTransitionValue.indexOf( 'ms' ) > -1;
var heightTransitionDuration = heightTransitionValue && heightPropertyIndex > -1 ? parseFloat( heightTransitionValue ) : 0;
// Maybe convert the duration to milliseconds
heightTransitionDuration = heightTransitionInMilliseconds ? heightTransitionDuration : heightTransitionDuration * 1000;
// Return the duration of the `height` property transition in milliseconds
return heightTransitionDuration;
}
/**
* Resize element
*/
var maybeChangeStateOnResize = function( manager ) {
// TODO: REFACTOR THIS FUNCTION TO BE MORE EFFICIENT
// Reset collapsed state
_publicMethods.expand( manager.element );
requestAnimationFrame( function() {
// Maybe collapse
if ( getComputedHeight( manager.contentElement ) > manager.settings.maxHeight ) {
_publicMethods.collapse( manager.element );
}
} );
}
/**
* Syncronize `aria-expanded` attribute for every handler of the collapsible-block on the page
*
* @param mixed HTMLElement The content element of the collapsible block
*/
var syncAriaExpanded = function ( element, expanded ) {
// Bail if `element` or `expanded` are invalid
if ( ! element && typeof expanded !== 'boolean' ) { return; }
var handlers = document.querySelectorAll( '[' + _settings.targetAttribute + '=' + element.id + ']' );
for ( var i = 0; i < handlers.length; i++ ) {
var handler = handlers[ i ];
handler.setAttribute( 'aria-expanded', expanded );
}
}
/**
* Finish the change to the "expanded" state.
*
* @param mixed element The content element of the collapsible block as a HTMLElement, or an Event dispatched on that element.
*/
var finishExpand = function ( element ) {
// Bail if element is invalid
if ( ! element ) { return; }
// Bail if element height property is empty, as it indicates that the element has already completed expanding
if ( element.style && '' === element.style.height ) { return; }
// Maybe bail when handling a transition event but not for the right property
if ( 'propertyName' in element && element.propertyName !== 'height' ) { return; }
// Get target element from property, usually passed in an event object
if ( 'target' in element && element.target ) {
element = element.target;
}
// Get the manager
var manager = _publicMethods.getInstance( element );
// Remove content element properties when transition is complete
element.style.height = '';
element.style.overflow = '';
// Remove `is-transitioning` class
element.classList.remove( manager.settings.isTransitioningClass );
// Syncronize `aria-expanded` for every handler on the page
syncAriaExpanded( element, true );
// Maybe set focus state
if ( _canChangeFocus && manager && manager.isActivated === true && manager.withFocus ) {
var focusElement = null;
// Maybe set focus to the child element marked as auto-focus that is visible, skipping those in nested collapsible blocks
var autofocusChildren = element.querySelectorAll( _settings.autoFocusSelector );
if ( ! focusElement && autofocusChildren ) {
for ( var i = 0; i < autofocusChildren.length; i++ ) {
var autofocusChild = autofocusChildren[i];
if ( autofocusChild.closest( _settings.contentElementSelector ) === element && isVisible( autofocusChild ) ) {
focusElement = autofocusChild;
}
}
}
// Maybe set focusElement to the first focusable element that is visible
if ( ! focusElement && element.matches( _settings.autoFocusSelector ) ) {
var focusableElements = Array.from( getFocusableElements( element ) );
for ( var i = 0; i < focusableElements.length; i++ ) {
var focusableElement = focusableElements[i];
if ( isVisible( focusableElement ) ) {
focusElement = focusableElement;
break;
}
}
}
// Set focus to focusElement
if ( focusElement ) {
focusElement.focus();
if ( _settings.selectContentsOnFocus && 'select' in focusElement ) {
focusElement.select();
}
}
}
// Reset `withFocus` flag on the manager back to default behavior
manager.withFocus = true;
// Remove the event handler so it runs only once
element.removeEventListener( getTransitionEndEvent(), finishExpand );
}
/**
* Finish the change to the "collapsed" state.
*
* @param mixed element The content element of the collapsible block as a HTMLElement, or an Event dispatched on that element.
*/
var finishCollapse = function ( element ) {
// Bail if element is invalid
if ( ! element ) { return; }
// Maybe bail when handling a transition event but not for the right property
if ( 'propertyName' in element && element.propertyName !== 'height' ) return;
// Get target element from property, usually passed in an event object
if ( 'target' in element && element.target ) {
element = element.target;
}
// Get the manager instance from the element
var manager = _publicMethods.getInstance( element );
// Hide the element from the screen and from the accessibility tree
element.style.display = 'none';
// Remove `is-transitioning` class
element.classList.remove( manager.settings.isTransitioningClass );
// Syncronize `aria-expanded` for every handler on the page
syncAriaExpanded( element, false );
// Remove the event handler so it runs only once
element.removeEventListener( getTransitionEndEvent(), finishCollapse );
}
/**
* Get slider manager instance from slider element.
*
* @param HTMLElement element Collapsible block main element.
*
* @return Object Collapsible block `manager` instance.
*/
_publicMethods.getInstance = function ( element ) {
var instance;
for ( var i = 0; i < _publicMethods.managers.length; i++ ) {
var manager = _publicMethods.managers[i];
if ( manager.element == element ) { instance = manager; break; }
}
return instance;
}
/**
* Collapse element.
*
* @param HTMLElement element Collapsible block main element.
* @param Boolean withTransition Whether to use transitions between states.
*/
_publicMethods.collapse = function( element, withTransition ) {
var manager = _publicMethods.getInstance( element );
// Bail if manager not found
// TODO: Maybe try to initialize collapsible and manager on the fly
if ( ! manager ) { return; }
// Set default value for withTransition
withTransition = withTransition === false ? false : true;
// Set element as transitioning
manager.element.classList.add( _settings.isTransitioningClass );
// Update element's state to `collapsed`
manager.element.classList.remove( manager.settings.isExpandedClass );
manager.element.classList.add( manager.settings.isCollapsedClass );
// Remove `finishExpand` event listener to prevent block from expanding at the end of the transition
manager.contentElement.removeEventListener( getTransitionEndEvent(), finishExpand );
// Set content element to hide overflowing content
manager.contentElement.style.overflow = 'hidden';
// Set height of the element to the current height value
// Without knowing the value of `height` property the browser can't calculate the steps of the `height` values
// related to the transition time and therefore won't be able to display the transition.
setHeight( manager.contentElement, getCurrentHeight( manager.contentElement ), false );
// Set event listener to finish the "collapse" state change
if ( withTransition ) {
manager.contentElement.addEventListener( getTransitionEndEvent(), finishCollapse );
}
// Trigger a reflow, flushing the CSS changes
reflow( element );
// Set height of the element to the `collapsed` state
setHeight( manager.contentElement, manager.settings.maxHeight, withTransition );
// Make sure to finish the "collapse" state change when transitions are not used
if ( ! withTransition ) {
finishCollapse( manager.contentElement );
}
}
/**
* Expand element.
*
* @param HTMLElement element Collapsible block main element.
* @param Boolean withTransition Whether to use transitions between states. Defaults to `true`.
* @param Boolean withFocus Whether to set the focus to the field when expanding. Cannot be used with `withTransition = true`. Defaults to `true`.
*/
_publicMethods.expand = function( element, withTransition, withFocus ) {
// Get element manager
var manager = _publicMethods.getInstance( element );
// Bail if manager not found
// TODO: Maybe try to initialize collapsible and manager on the fly
if ( ! manager ) { return; }
// Set default value for parameters
withTransition = withTransition === false ? false : true;
withFocus = withFocus === false ? false : true;
// Set flag `withFocus` to the element manager
manager.withFocus = withFocus;
// Show the element again on the screen and add it back to the accessibility tree
manager.contentElement.style.display = '';
// Remove `finishCollapse` event listener to prevent block from collapsing at the end of the transition
manager.contentElement.removeEventListener( getTransitionEndEvent(), finishCollapse );
// Set height of the element to the current height value
setHeight( manager.contentElement, getCurrentHeight( manager.contentElement ), false );
// Set event listener to finish the "expand" state change
if ( withTransition ) {
manager.contentElement.addEventListener( getTransitionEndEvent(), finishExpand );
}
// Get the duration of the `height` property transition in milliseconds
var heightTransitionDuration = getHeightTransitionDuration( manager.contentElement );
// Set element as transitioning
// Needs to happen before setting delay to the height transition
// to prevent jerkiness when transitioning from `collapsed` to `expanded` state
manager.element.classList.add( manager.settings.isTransitioningClass );
// Expand element to its content height
requestAnimationFrame( function() {
var computedHeight = getComputedHeight( manager.contentElement );
// Trigger a reflow, flushing the CSS changes
reflow( element );
// Set height of the element to the `expanded` state
setHeight( manager.contentElement, computedHeight, withTransition );
// Update element's state to `expanded`
manager.element.classList.remove( manager.settings.isCollapsedClass );
manager.element.classList.add( manager.settings.isExpandedClass );
// Make sure to finish the "expand" state change when transitions are not used
if ( ! withTransition ) {
finishExpand( manager.contentElement );
}
// Make sure to finish the "expand" state change after the expected duration of the height transition,
// so we don't need to rely completely on the browser's transitionend event.
setTimeout( function() {
finishExpand( manager.contentElement );
}, heightTransitionDuration + 50 );
} );
}
/**
* Enable focus on expand.
*/
_publicMethods.enableFocusOnExpand = function() {
_canChangeFocus = true;
}
/**
* Disable focus on expand.
*/
_publicMethods.disableFocusOnExpand = function() {
_canChangeFocus = false;
}
/**
* Toggle between collapsed/expanded states of the element.
*
* @param HTMLElement element Collapsible block main element.
* @param Boolean withTransition Whether to use transitions between states.
*/
_publicMethods.toggleState = function( element, withTransition ) {
var manager = _publicMethods.getInstance( element );
// Bail if manager not found
if ( ! manager ) { return; }
// Toggle state
if ( element.classList.contains( manager.settings.isCollapsedClass ) ) {
_publicMethods.expand( element, withTransition );
}
else {
_publicMethods.collapse( element, withTransition );
}
}
/**
* /**
* Get current state of the collapsible block.
*
* @param HTMLElement element Collapsible block main element.
*
* @return string Either `collapsed` or `expanded`. Can be compared to the constants present in `CollapsibleBlock.states`.
*/
_publicMethods.getState = function( element ) {
var manager = _publicMethods.getInstance( element );
// Bail if manager not found
if ( ! manager ) { return false; }
// Get current state
var currentState = _publicMethods.states.EXPANDED;
if ( element.classList.contains( manager.settings.isCollapsedClass ) ) {
currentState = _publicMethods.states.COLLAPSED;
}
return currentState;
}
/**
* Initialize a handler element.
*/
_publicMethods.initializeHandler = function( handler ) {
// Enable the handler element
handler.removeAttribute( 'disabled' );
handler.removeAttribute( 'aria-hidden' );
// Add the element to the natural tab order
handler.setAttribute( 'tabindex', '0' );
// Set handler role to `button`
if ( handler.tagName.toUpperCase() != 'BUTTON' ) {
handler.setAttribute( 'role', 'button' );
}
// Get target attributes
var targetId = handler.getAttribute( _settings.targetAttribute );
var multiTargetIds = handler.getAttribute( _settings.multiTargetAttribute );
// Maybe get target element id from attributes or parent elements
if ( ( ! targetId || targetId == '' ) && ( ! multiTargetIds || multiTargetIds == '' ) ) {
var parentCollapsible = handler.closest( _settings.elementSelector );
// Check if collapsbile blocks is also the content element
if ( parentCollapsible && parentCollapsible.matches( _settings.contentElementSelector ) ) {
targetId = parentCollapsible.id;
}
// Else, try to get content element from the collapsible block
else if ( parentCollapsible && parentCollapsible.querySelector( _settings.contentElementSelector ) ) {
var contentElement = parentCollapsible.querySelector( _settings.contentElementSelector );
targetId = contentElement.id;
}
// Maybe set target attribute
if ( targetId && targetId != '' ) {
handler.setAttribute( _settings.targetAttribute, targetId );
}
}
// Remove the `href` attribute
handler.removeAttribute( 'href' );
}
/**
* Initialize an element.
*
* @param HTMLElement element Collapsible block main element.
*/
_publicMethods.initializeElement = function( element ) {
var manager = {};
_publicMethods.managers.push( manager );
manager.element = element;
// TODO: Refactor to remove `manager.settings` as it will always be a copy of the high-level `_settings` variable, with more properties that can be added directly to the `manager` variable.
manager.settings = extend( _settings );
// Set default behavior for setting focus when expanding the element
manager.withFocus = true;
// Get content element
manager.contentElement = manager.element.matches( _settings.contentElementSelector ) ? manager.element : manager.element.querySelector( manager.settings.contentElementSelector );
if ( ! manager.contentElement ) {
manager.contentElement = manager.element;
}
// Maybe create element ID
if ( manager.element.id == '' ) {
manager.element.id = manager.settings.idPrefix + '_' + _publicMethods.managers.length;
}
// Maybe create contentElement ID
if ( manager.contentElement.id == '' ) {
manager.contentElement.id = manager.element.id + '__content';
}
// Get maxHeight from attributes
var maxHeightAttribute = manager.contentElement.getAttribute( manager.settings.maxHeightAttribute );
manager.settings.maxHeight = maxHeightAttribute && maxHeightAttribute != '' ? parseInt( maxHeightAttribute ) : manager.settings.maxHeight;
// Get createHandler from attributes
var createHandler = manager.element.getAttribute( manager.settings.createHandlerAttribute );
manager.settings.createHandler = createHandler == 'true' || createHandler == 'false' ? Boolean( createHandler ) : manager.settings.createHandler;
if ( manager.settings.createHandler ) {
createHandlerElement( manager );
}
// Maybe create content inner element
maybeCreateContentInnerElement( manager );
// Set initial state at element initialization
var initialStateAttribute = manager.contentElement.getAttribute( manager.settings.initialStateAttribute );
var initialState = initialStateAttribute ? initialStateAttribute : manager.settings.initialState;
var index = Array.prototype.indexOf.call( manager.element.parentNode.children, manager.element );
if ( initialState == _publicMethods.states.EXPANDED || ( initialState == _publicMethods.states.FIRST_EXPANDED && index == 0 ) ) {
_publicMethods.expand( manager.element, false, false ); // No transition, no focus
}
else {
_publicMethods.collapse( manager.element, false ); // No transition
}
// Maybe change state on resize
var changeStateOnResizeAttribute = manager.element.getAttribute( manager.settings.changeStateOnResizeAttribute );
manager.settings.changeStateOnResize = changeStateOnResizeAttribute && changeStateOnResizeAttribute != '' ? Boolean( changeStateOnResizeAttribute ) : false;
if ( manager.settings.changeStateOnResize ) {
maybeChangeStateOnResize( manager );
// TODO: Maybe move event handler to a single listener
window.addEventListener( 'resize', function() { maybeChangeStateOnResize( manager ); } );
}
// Set css transition property
var computedTransition = window.getComputedStyle( manager.contentElement ).transition;
var cssTransition = computedTransition != '' ? computedTransition + ', ' + manager.settings.cssTransition : manager.settings.cssTransition;
manager.contentElement.style.transition = cssTransition;
// Set element as activated
requestAnimationFrame( function(){
manager.isActivated = true;
manager.element.classList.add( manager.settings.isActivatedClass );
} );
}
/**
* Initialize.
*/
_publicMethods.init = function( options ) {
if ( _hasInitialized ) return;
// Merge with general settings with options
_settings = extend( _defaults, options );
// Initialize collapsible elements
var elements = document.querySelectorAll( _settings.elementSelector );
for ( var i = 0; i < elements.length; i++ ) {
_publicMethods.initializeElement( elements[ i ] );
}
// Initialize handler elements
var handlers = document.querySelectorAll( _settings.handlerSelector );
for ( var i = 0; i < handlers.length; i++ ) {
_publicMethods.initializeHandler( handlers[ i ] );
}
// Trigger a reflow, flushing the CSS changes
reflow();
// Syncronize `aria-expanded` for every handler on the page
for ( var i = 0; i < elements.length; i++ ) {
var element = elements[ i ];
var contentElement = element.matches( _settings.contentElementSelector ) ? element : element.querySelector( _settings.contentElementSelector );
syncAriaExpanded( contentElement, _publicMethods.getState( element ) == _publicMethods.states.EXPANDED );
}
// Add event listeners
document.addEventListener( 'click', handleClick );
document.addEventListener( 'keydown', handleKeyDown, true );
// Set body class
document.body.classList.add( _settings.bodyClass );
// Enable focus
_publicMethods.enableFocusOnExpand();
// Set as initialized
requestAnimationFrame( function() {
_hasInitialized = true;
} );
};
//
// Public APIs
//
return _publicMethods;
});