Skip to content

Instantly share code, notes, and snippets.

@mvsde
Last active June 30, 2021 07:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mvsde/b7e4cdcf560b4ef288958076d23aacd0 to your computer and use it in GitHub Desktop.
Save mvsde/b7e4cdcf560b4ef288958076d23aacd0 to your computer and use it in GitHub Desktop.
JavaScript Collapse
/**
* Modern browsers support the `once` option for event listeners:
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters
*/
module.exports = function(target, type, callback) {
let eventWrapper = function(event) {
target.removeEventListener(type, eventWrapper);
callback(event);
};
target.addEventListener(type, eventWrapper);
};
<div class="js-collapse">
<button aria-controls="panel" class="js-collapse-button">Button</button>
<div aria-hidden="true" id="panel" class="js-collapse-content">Content</div>
</div>
/* COLLAPSE
* ========================================================================== */
/* DEPENDENCIES
* ====================================================== */
const Util = require('./utilities.js');
const addEventListenerOnce = require('./add-event-listener-once.js');
/* COLLAPSE
* ====================================================== */
let Collapse = function(options) {
let container = options.containerElement;
let buttonSelector = options.buttonSelector || '.js-collapse-button';
let contentSelector = options.contentSelector || '.js-collapse-content';
let activeClass = options.activeClass || 'is-active';
let collapsingClass = options.collapsingClass || 'is-collapsing';
// Abort if no container element was provided
if (!container) {
throw new Error('Collapse Error: Missing container element.');
}
// Get elements
let button = container.querySelector(buttonSelector);
let content = container.querySelector(contentSelector);
// Get and set states
let isActive = content.classList.contains(activeClass);
let isCollapsing = false;
// Hide the content
this.hide = function() {
// Abort if content is already collapsing or not visible
if (isCollapsing || !isActive) {
return;
}
isCollapsing = true;
// Set explicit content height
content.style.height = content.offsetHeight + 'px';
// Force reflow
Util.reflow(content);
// Set classes and attributes
button.classList.remove(activeClass);
content.classList.remove(activeClass);
content.classList.add(collapsingClass);
content.setAttribute('aria-hidden', true);
// Remove content height
content.style.height = null;
let complete = function() {
content.classList.remove(collapsingClass);
isActive = false;
isCollapsing = false;
};
// Check if browser supports transition event
if (!Util.supportsTransitionEnd) {
complete();
return;
}
// Add event listener once to transition end
addEventListenerOnce(content, 'transitionend', function() {
complete();
});
};
// Show the content
this.show = function() {
// Abort if content is already collapsing or visible
if (isCollapsing || isActive) {
return;
}
// Set collapsing state
isCollapsing = true;
// Get height by first showing the content,
// then retrieving the height and hiding the content again
content.style.display = 'block';
let contentHeight = content.offsetHeight;
content.style.display = null;
// Set classes and attributes
content.classList.add(collapsingClass);
// Force reflow
Util.reflow(content);
button.classList.add(activeClass);
content.setAttribute('aria-hidden', false);
// Set explicit content height
content.style.height = contentHeight + 'px';
let complete = function() {
// Set classes
content.classList.remove(collapsingClass);
content.classList.add(activeClass);
// Reset content height
content.style.height = null;
isActive = true;
isCollapsing = false;
};
// Check if browser supports transition event
if (!Util.supportsTransitionEnd) {
complete();
return;
}
// Add event listener once to transition end
addEventListenerOnce(content, 'transitionend', function() {
complete();
});
};
// Toggle content
this.toggle = function() {
// Abort early if content is already collapsing
if (isCollapsing) {
return;
}
// Decide whether to show or to hide content
if (isActive) {
this.hide();
} else {
this.show();
}
};
// Initialize button click event
this.init = function() {
let context = this;
button.addEventListener('click', function(event) {
event.preventDefault();
context.toggle();
});
};
};
/* INITIALIZE COLLAPSE
* ====================================================== */
let jsCollapse = document.querySelectorAll('.js-collapse');
let jsCollapseLength = jsCollapse.length;
for (let i = 0; i < jsCollapseLength; i++) {
new Collapse({
containerElement: jsCollapse[i],
buttonSelector: '.js-collapse-button',
contentSelector: '.js-collapse-content',
activeClass: 'is-active',
collapsingClass: 'is-collapsing'
}).init();
}
/* UTILITIES
*
* Small helper functions
* ========================================================================== */
module.exports = {
// Reflow by retrieving element height
reflow: function(element) {
return element.offsetHeight;
},
// Check if browser supports transition end event
supportsTransitionEnd: function() {
let testElement = document.createElement('div');
let transitionEndEventNames = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend'
};
for (let name in transitionEndEventNames) {
if (testElement.style[name] !== undefined) {
return transitionEndEventNames[name];
}
}
return false;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment