badger-accordion
Version:
An accessible vanilla JS accordion with extensible API.
812 lines (659 loc) • 24.2 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.BadgerAccordion = factory());
}(this, (function () { 'use strict';
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _toConsumableArray(arr) {
return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
}
}
function _iterableToArray(iter) {
if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter);
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance");
}
if (!Array.from) {
Array.from = function () {
var toStr = Object.prototype.toString;
var isCallable = function isCallable(fn) {
return typeof fn === 'function' || toStr.call(fn) === '[object Function]';
};
var toInteger = function toInteger(value) {
var number = Number(value);
if (isNaN(number)) {
return 0;
}
if (number === 0 || !isFinite(number)) {
return number;
}
return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number));
};
var maxSafeInteger = Math.pow(2, 53) - 1;
var toLength = function toLength(value) {
var len = toInteger(value);
return Math.min(Math.max(len, 0), maxSafeInteger);
}; // The length property of the from method is 1.
return function from(arrayLike
/* , mapFn, thisArg */
) {
// 1. Let C be the this value.
var C = this; // 2. Let items be ToObject(arrayLike).
var items = Object(arrayLike); // 3. ReturnIfAbrupt(items).
if (arrayLike == null) {
throw new TypeError('Array.from requires an array-like object - not null or undefined');
} // 4. If mapfn is undefined, then let mapping be false.
var mapFn = arguments.length > 1 ? arguments[1] : void undefined;
var T;
if (typeof mapFn !== 'undefined') {
// 5. else
// 5. a If IsCallable(mapfn) is false, throw a TypeError exception.
if (!isCallable(mapFn)) {
throw new TypeError('Array.from: when provided, the second argument must be a function');
} // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 2) {
T = arguments[2];
}
} // 10. Let lenValue be Get(items, "length").
// 11. Let len be ToLength(lenValue).
var len = toLength(items.length); // 13. If IsConstructor(C) is true, then
// 13. a. Let A be the result of calling the [[Construct]] internal method
// of C with an argument list containing the single item len.
// 14. a. Else, Let A be ArrayCreate(len).
var A = isCallable(C) ? Object(new C(len)) : new Array(len); // 16. Let k be 0.
var k = 0; // 17. Repeat, while k < len… (also steps a - h)
var kValue;
while (k < len) {
kValue = items[k];
if (mapFn) {
A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k);
} else {
A[k] = kValue;
}
k += 1;
} // 18. Let putStatus be Put(A, "length", len, true).
A.length = len; // 20. Return A.
return A;
};
}();
}
/*
By Osvaldas Valutis, www.osvaldas.info
Available for use under the MIT License
*/
/* eslint-disable no-unused-vars */
(function (document, window) {
var el = document.body || document.documentElement,
s = el.style,
prefixAnimation = '',
prefixTransition = '';
if (s.WebkitAnimation == '') prefixAnimation = '-webkit-';
if (s.MozAnimation == '') prefixAnimation = '-moz-';
if (s.OAnimation == '') prefixAnimation = '-o-';
if (s.WebkitTransition == '') prefixTransition = '-webkit-';
if (s.MozTransition == '') prefixTransition = '-moz-';
if (s.OTransition == '') prefixTransition = '-o-';
Object.defineProperty(Object.prototype, 'onCSSAnimationEnd', {
value: function value(callback) {
var runOnce = function runOnce(e) {
callback();
e.target.removeEventListener(e.type, runOnce);
};
this.addEventListener('webkitAnimationEnd', runOnce);
this.addEventListener('mozAnimationEnd', runOnce);
this.addEventListener('oAnimationEnd', runOnce);
this.addEventListener('oanimationend', runOnce);
this.addEventListener('animationend', runOnce);
if (prefixAnimation == '' && !('animation' in s) || getComputedStyle(this)[prefixAnimation + 'animation-duration'] == '0s') callback();
return this;
},
enumerable: false,
writable: true
});
Object.defineProperty(Object.prototype, 'onCSSTransitionEnd', {
value: function value(callback) {
var runOnce = function runOnce(e) {
callback();
e.target.removeEventListener(e.type, runOnce);
};
this.addEventListener('webkitTransitionEnd', runOnce);
this.addEventListener('mozTransitionEnd', runOnce);
this.addEventListener('oTransitionEnd', runOnce);
this.addEventListener('transitionend', runOnce);
this.addEventListener('transitionend', runOnce);
if (prefixTransition == '' && !('transition' in s) || getComputedStyle(this)[prefixTransition + 'transition-duration'] == '0s') callback();
return this;
},
enumerable: false,
writable: true
});
})(document, window, 0);
/**
* ACCORDION
*
* A lightwight vanilla JS accordion with an exstensible API
*/
// import uuid from 'uuid/v4';
// const uuidV4 = uuid;
/* eslint-disable no-unused-vars */
/**
* CONSTRUCTOR
* Initializes the object
*/
var BadgerAccordion =
/*#__PURE__*/
function () {
function BadgerAccordion(el, options) {
var _this2 = this;
_classCallCheck(this, BadgerAccordion);
var container = typeof el === 'string' ? document.querySelector(el) : el; // If el is not defined
if (container == null) {
return;
}
var defaults = {
headerClass: '.js-badger-accordion-header',
panelClass: '.js-badger-accordion-panel',
panelInnerClass: '.js-badger-accordion-panel-inner',
hiddenClass: '-ba-is-hidden',
activeClass: '-ba-is-active',
get hidenClass() {
return this.hiddenClass;
},
initializedClass: 'badger-accordion--initialized',
get initalisedClass() {
return this.initializedClass;
},
headerDataAttr: 'data-badger-accordion-header-id',
openMultiplePanels: false,
openHeadersOnLoad: [],
addListenersOnInit: true,
headerOpenLabel: '',
headerCloseLabel: '',
roles: true // toggleEl: // If you want to use a different element to trigger the accordion
}; // Options
this.settings = _extends({}, defaults, options); // Setting getting elements
this.container = container; // Selecting children of the current accordion instance
var children = Array.from(this.container.children); // Since the Accordions header button is nested inside an element with class
// of `badger-accordion__header` it is a grandchild of the accordion instance.
// In order to have nested accordions we need each to only get all the button
// elements for this instance. Here an array is created to show all the children
// of the element `badger-accordion__header`.
var headerParent = children.filter(function (header) {
return !header.classList.contains(_this2.settings.panelClass.substr(1));
}); // Creating an array of all DOM nodes that are Accordion headers
this.headers = headerParent.reduce(function (acc, header) {
var _ref;
// Gets all the elements that have the headerClass
var a = Array.from(header.children).filter(function (child) {
return child.classList.contains(_this2.settings.headerClass.substr(1));
}); // Merges the current `badger-accordion__header` accordion triggers
// with all the others.
acc = (_ref = []).concat.apply(_ref, _toConsumableArray(acc).concat([a]));
return acc;
}, []); // Creates an array of all panel elements for this instance of the accordion
this.panels = children.filter(function (panel) {
return panel.classList.contains(_this2.settings.panelClass.substr(1));
});
this.toggleEl = this.settings.toggleEl !== undefined ? Array.from(this.container.querySelectorAll(this.settings.toggleEl)) : this.headers; // This is for managing state of the accordion. It by default sets
// all accordion panels to be closed
this.states = [].map.call(this.headers, function () {
return {
state: 'closed'
};
});
this.ids = [].map.call(this.headers, function () {
return {
id: Math.floor(Math.random() * 1000000 + 1)
};
}); // This is to ensure that once an open/close event has been fired
// another cannot start until the first event has finished.
// @TODO - get this working...
this.toggling = false; // Initiating the accordion
if (this.container) {
this.init();
} else {
/* eslint-disable no-console */
console.log('Something is wrong with you markup...');
}
}
/**
* INIT
*
* Initalises the accordion
*/
_createClass(BadgerAccordion, [{
key: "init",
value: function init() {
// Sets up ID, aria attrs & data-attrs
this._setupAttributes(); // Setting up the inital view of the accordion
this._initalState(); // Setting the height of each panel
this.calculateAllPanelsHeight(); // Inserting data-attribute onto each `header`
this._insertDataAttrs(); // Adding listeners to headers
this._addListeners(); // Adds class to accordion for initalisation
this._finishInitialization();
}
/**
* CHECK ROLES ETTING
* @return {[boolean]}
* Checks roles setting for all roles or a single role.
* First checks if a `boolean` has been used to set all
* roles to either true or false. If the setting is an
* object it will only set the attribute where each
* attribute has explicitly been set as true, eg;
* ```
* roles: {
* region: true
* }
* ```
*/
}, {
key: "_setRole",
value: function _setRole(role, el) {
if (typeof this.settings.roles === 'boolean' && this.settings.roles || this.settings.roles[role] !== undefined && this.settings.roles[role] !== false) {
el.setAttribute('role', role);
}
}
/**
* INSERT DATA ATTRS
*
* Updates state object for inital loading of the accordion
*/
}, {
key: "_initalState",
value: function _initalState() {
// Sets state object as per `this.settings.openHeadersOnLoad`
var headersToOpen = this.settings.openHeadersOnLoad;
if (headersToOpen.length) {
this._openHeadersOnLoad(headersToOpen);
} // Render DOM as per the updates `this.states` object
this._renderDom();
}
/**
* INSERT DATA ATTRS
*
* Adds `headerDataAttr` to all headers
*/
}, {
key: "_insertDataAttrs",
value: function _insertDataAttrs() {
var _this3 = this;
this.headers.forEach(function (header, index) {
header.setAttribute(_this3.settings.headerDataAttr, index);
});
}
/**
* FINISH INITALISATION
*
* Adds in `initializedClass` to accordion
*/
}, {
key: "_finishInitialization",
value: function _finishInitialization() {
this.container.classList.add(this.settings.initializedClass);
this._setRole('presentation', this.container);
}
/**
* ADD LISTENERS
*
* Adds click event to each header
*/
}, {
key: "_addListeners",
value: function _addListeners() {
if (!this.settings.addListenersOnInit) return; // So we can reference the badger-accordion object inside out eventListener
var _this = this; // Adding click event to accordion
this.headers.forEach(function (header, index) {
header.addEventListener('click', function () {
// Getting the target of the click
// const clickedEl = event.target;
_this.handleClick(header, index);
});
});
}
/**
* HANDLE CLICK
*
* Handles click and checks if click was on an header element
* @param {object} targetHeader - The header node you want to open
*/
}, {
key: "handleClick",
value: function handleClick(targetHeader, headerIndex) {
// Removing current `.` from `this.settings.headerClass` class so it can
// be checked against the `targetHeader` classList
var targetHeaderClass = this.settings.headerClass.substr(1); // Checking that the thing that was clicked on was the accordions header
if (targetHeader.classList.contains(targetHeaderClass) && this.toggling === false) {
this.toggling = true; // Updating states
this.setState(headerIndex); // Render DOM as per the updates `this.states` object
this._renderDom();
}
}
/**
* SET STATES
*
* Sets the state for all headers. The 'target header' will have its state toggeled
* @param {object} targetHeaderId - The header node you want to open
*/
}, {
key: "setState",
value: function setState(targetHeaderId) {
var _this4 = this;
var states = this.getState(); // If `this.settings.openMultiplePanels` is false we need to ensure only one panel
// be can open at once. If it is false then all panels state APART from the one that
// has just been clicked needs to be set to 'closed'.
if (!this.settings.openMultiplePanels) {
states.filter(function (state, index) {
if (index != targetHeaderId) {
state.state = 'closed';
}
});
} // Toggles the state value of the target header. This was `array.find` but `find`
// isnt supported in IE11
states.filter(function (state, index) {
if (index == targetHeaderId) {
var newState = _this4.toggleState(state.state);
return state.state = newState;
}
});
}
/**
* RENDER DOM
*
* Renders the accordion in the DOM using the `this.states` object
*/
}, {
key: "_renderDom",
value: function _renderDom() {
var _this5 = this;
// Filter through all open headers and open them
this.states.filter(function (state, index) {
if (state.state === 'open') {
// Opening the current panel but _NOT_ updating the state
_this5.open(index, false);
}
}); // Filter through all closed headers and closes them
this.states.filter(function (state, index) {
if (state.state === 'closed') {
// Closing the current panel but _NOT_ updating the state
_this5.close(index, false);
}
});
}
/**
* OPEN
*
* Closes a specific panel
* @param {integer} headerIndex - The header node index you want to open
*/
}, {
key: "open",
value: function open(headerIndex) {
var setState = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
// 1. If being fired directly the state needs to be updated.
if (setState) {
this.setState(headerIndex);
}
this.togglePanel('open', headerIndex);
}
/**
* CLOSE
*
* Closes a specific panel
* @param {integer} headerIndex - The header node index you want to close
*/
}, {
key: "close",
value: function close(headerIndex) {
var setState = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
// 1. If being fired directly the state needs to be updated.
if (setState) {
this.setState(headerIndex);
}
this.togglePanel('closed', headerIndex);
}
/**
* OPEN ALL
*
* Opens all panels
*/
}, {
key: "openAll",
value: function openAll() {
var _this6 = this;
this.headers.forEach(function (header, headerIndex) {
_this6.togglePanel('open', headerIndex);
});
}
/**
* CLOSE ALL
*
* Closes all panels
*/
}, {
key: "closeAll",
value: function closeAll() {
var _this7 = this;
this.headers.forEach(function (header, headerIndex) {
_this7.togglePanel('closed', headerIndex);
});
}
/**
* GET STATE
*
* Getting state of headers. By default gets state of all headers
* @param {string} animationAction - The animation you want to invoke
* @param {integer} headerIndex - The header node index you want to animate
*/
}, {
key: "togglePanel",
value: function togglePanel(animationAction, headerIndex) {
var _this8 = this;
if (animationAction !== undefined && headerIndex !== undefined) {
if (animationAction === 'closed') {
// 1. Getting ID of panel that we want to close
var header = this.headers[headerIndex];
var panelToClose = this.panels[headerIndex]; // 2. Closeing panel
panelToClose.classList.add(this.settings.hiddenClass); // 3. Removing active classes
panelToClose.classList.remove(this.settings.activeClass);
header.classList.remove(this.settings.activeClass); // 4. Set aria attrs
header.setAttribute('aria-expanded', false); // 5. Resetting toggling so a new event can be fired
panelToClose.onCSSTransitionEnd(function () {
return _this8.toggling = false;
});
} else if (animationAction === 'open') {
// 1. Getting ID of panel that we want to open
var _header = this.headers[headerIndex];
var panelToOpen = this.panels[headerIndex]; // 2. Opening panel
panelToOpen.classList.remove(this.settings.hiddenClass); // 3. Adding active classes
panelToOpen.classList.add(this.settings.activeClass);
_header.classList.add(this.settings.activeClass); // 4. Set aria attrs
_header.setAttribute('aria-expanded', true); // 5. Resetting toggling so a new event can be fired
panelToOpen.onCSSTransitionEnd(function () {
return _this8.toggling = false;
});
}
}
} // @TODO - is this needed anymore?
// checkState(headerId) {
// let state = this.states[headerId].state;
//
// if(state === 'closed') {
// return state;
// } else if(state === 'open') {
// return state;
// }
// }
/**
* GET STATE
*
* Getting state of headers. By default gets state of all headers
* @param {array} headerIds - Id/'s of the headers you want to check
*/
}, {
key: "getState",
value: function getState() {
var _this9 = this;
var headerIds = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
if (headerIds.length && Array.isArray(headerIds)) {
var states = headerIds.map(function (header) {
return _this9.states[header];
});
return states;
} else {
return this.states;
}
}
/**
* TOGGLE STATE
*
* Toggling the state value
* @param {string} currentState - Current state value for a header
*/
}, {
key: "toggleState",
value: function toggleState(currentState) {
if (currentState !== undefined) {
return currentState === 'closed' ? 'open' : 'closed';
}
}
/**
* HEADERS TO OPEN
*
* Setting which headers should be open when accordion is initalised
* @param {array} headersToOpen - Array of ID's for the headers to be open
*/
}, {
key: "_openHeadersOnLoad",
value: function _openHeadersOnLoad() {
var _this10 = this;
var headersToOpen = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
if (headersToOpen.length && Array.isArray(headersToOpen)) {
var headers = headersToOpen.filter(function (header) {
return header != undefined;
});
headers.forEach(function (header) {
_this10.setState(header);
});
}
}
/**
* SET UP ATTRIBUTES
*
* Initalises accordion attribute methods
*/
}, {
key: "_setupAttributes",
value: function _setupAttributes() {
// Adding ID & aria-controls
this._setupHeaders(); // Adding ID & aria-labelledby
this._setupPanels(); // Inserting data-attribute onto each `header`
this._insertDataAttrs();
}
/**
* SET PANEL HEIGHT - ** DEPRICATED **
*
* Depreicated as this method is becoming public and
* I want to name it something that lets devs know
* it's not just for using inside the `init()` method.
*/
}, {
key: "_setPanelHeight",
value: function _setPanelHeight() {
this.calculateAllPanelsHeight();
}
/**
* CALCULATE PANEL HEIGHT
*
* Setting height for panels using pannels inner element
*/
}, {
key: "calculatePanelHeight",
value: function calculatePanelHeight(panel) {
var panelInner = panel.querySelector(this.settings.panelInnerClass);
var activeHeight = panelInner.offsetHeight;
return panel.style.maxHeight = "".concat(activeHeight, "px");
}
/**
* CALCULATE PANEL HEIGHT
*
* Setting height for panels using pannels inner element
*/
}, {
key: "calculateAllPanelsHeight",
value: function calculateAllPanelsHeight() {
var _this11 = this;
this.panels.forEach(function (panel) {
_this11.calculatePanelHeight(panel);
});
}
/**
* SET UP HEADERS
*/
}, {
key: "_setupHeaders",
value: function _setupHeaders() {
var _this12 = this;
this.headers.forEach(function (header, index) {
header.setAttribute('id', "badger-accordion-header-".concat(_this12.ids[index].id));
header.setAttribute('aria-controls', "badger-accordion-panel-".concat(_this12.ids[index].id));
});
}
/**
* SET UP PANELS
*/
}, {
key: "_setupPanels",
value: function _setupPanels() {
var _this13 = this;
this.panels.forEach(function (panel, index) {
panel.setAttribute('id', "badger-accordion-panel-".concat(_this13.ids[index].id));
panel.setAttribute('aria-labelledby', "badger-accordion-header-".concat(_this13.ids[index].id));
if (_this13.settings.roles === true || _this13.settings.roles.region !== false) {
_this13._setRole('region', panel);
}
});
}
}]);
return BadgerAccordion;
}(); // Export
return BadgerAccordion;
})));
//# sourceMappingURL=badger-accordion.js.map