Author: | Giana |
---|---|
Views Total: | 6,307 views |
Official Page: | Go to website |
Last Update: | January 4, 2019 |
License: | MIT |
Preview:

Description:
Create a smoothly collapsible accordion using JavaScript, CSS3 animations, and <details>
and <summary>
elements.
How to use it:
Code the accordion.
<div class="container collapse"> <details> <summary>Accordion 1</summary> <div class="details-wrapper"> <div class="details-styling"> Details 1 </div> </div> </details> <details> <summary>Accordion 2</summary> <div class="details-wrapper"> <div class="details-styling"> Details 2 </div> </div> </details> <details> <summary>Accordion 3</summary> <div class="details-wrapper"> <div class="details-styling"> Details 3 </div> </div> </details> </div>
The necessary CSS & CSS3 styles for the accordion.
/* Please wrap your collapsible content in a single element or so help me Must add a transition or it breaks because that's the whole purpose of this Only one transition-duration works (see explanation on line #141 in JS) You can add more to an inner wrapper (.details-styling) */ .collapse-init summary + * { transition: all 0.25s ease-in-out; overflow: hidden; } /* Closed state. Any CSS transitions work here The JS has a height calculation to make sliding opened/closed easier, but it's not necessary Remove the height prop for a simple toggle on/off (after all that work I did for you?) */ .collapse-init :not(.panel-active) summary + * { height: 0; opacity: 0; -webkit-transform: scale(0.9); transform: scale(0.9); -webkit-transform-origin: bottom center; transform-origin: bottom center; } .collapse-init summary { list-style: none; } .collapse-init summary::-webkit-details-marker { display: none; } .collapse-init summary::before { display: none; } .collapse-init summary { cursor: pointer; } etails { background: #fff; border: 1px solid #d6d1e0; border-bottom: 0; list-style: none; } details:first-child { border-radius: 6px 6px 0 0; } details:last-child { border-bottom: 1px solid #d6d1e0; border-radius: 0 0 6px 6px; } summary { display: block; transition: 0.2s; font-weight: 700; padding: 1em; } summary:focus { outline: 2px solid #5b13ec; } .collapse-init summary::after { border-right: 2px solid; border-bottom: 2px solid; content: ''; float: right; width: 0.5em; height: 0.5em; margin-top: 0.25em; -webkit-transform: rotate(45deg); transform: rotate(45deg); transition: inherit; } [open] summary { background: #5b13ec; color: #f8f5fe; } [open] summary::after { margin-top: 0.5em; -webkit-transform: rotate(225deg); transform: rotate(225deg); }
Load the details-element-polyfill for IE/Edge support.
<script src="https://cdn.jsdelivr.net/gh/javan/details-element-polyfill@master/dist/details-element-polyfill.js"></script>
The primary JavaScript for the accordion.
var _createClass = 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);}}return function (Constructor, protoProps, staticProps) {if (protoProps) defineProperties(Constructor.prototype, protoProps);if (staticProps) defineProperties(Constructor, staticProps);return Constructor;};}();function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function");}}miscPolyfillsForIE(); // main function Collapse = function () { function Collapse(container) {var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};_classCallCheck(this, Collapse); var defaults = { accordion: false, initClass: 'collapse-init', activeClass: 'panel-active', heightClass: 'collapse-reading-height' }; this.settings = Object.assign({}, defaults, options); this._container = container; this._panels = container.querySelectorAll("details"); this.events = { openingPanel: new CustomEvent('openingPanel'), openedPanel: new CustomEvent('openedPanel'), closingPanel: new CustomEvent('closingPanel'), closedPanel: new CustomEvent('closedPanel') }; } // Sets height of panel content _createClass(Collapse, [{ key: '_setPanelHeight', value: function _setPanelHeight(panel) { var contents = panel.querySelector("summary + *"); contents.style.height = contents.scrollHeight + "px"; } // Removes height of panel content }, { key: '_removePanelHeight', value: function _removePanelHeight(panel) { var contents = panel.querySelector("summary + *"); contents.style.height = null; } //=== Open panel }, { key: 'open', value: function open(panel) { panel.dispatchEvent(this.events.openingPanel); panel.open = true; } // Add height and active class, this triggers opening animation }, { key: '_afterOpen', value: function _afterOpen(panel) { this._setPanelHeight(panel); panel.classList.add(this.settings.activeClass); } // Remove height on animation end since it's no longer needed }, { key: '_endOpen', value: function _endOpen(panel) { panel.dispatchEvent(this.events.openedPanel); this._removePanelHeight(panel); } //=== Close panel, not toggling the actual [open] attr! }, { key: 'close', value: function close(panel) { panel.dispatchEvent(this.events.closingPanel); this._afterClose(panel); } // Set height, wait a beat, then remove height to trigger closing animation }, { key: '_afterClose', value: function _afterClose(panel) {var _this = this; this._setPanelHeight(panel); setTimeout(function () { panel.classList.remove(_this.settings.activeClass); _this._removePanelHeight(panel); }, 100); //help, this is buggy and hacky } // Actually closes panel once animation finishes }, { key: '_endClose', value: function _endClose(panel) { panel.dispatchEvent(this.events.closedPanel); panel.open = false; } //=== Toggles panel... just in case anyone needs this }, { key: 'toggle', value: function toggle(panel) { panel.open ? this.close(panel) : this.open(panel); } //=== Accordion closes all panels except the current passed panel }, { key: 'openSinglePanel', value: function openSinglePanel(panel) {var _this2 = this; this._panels.forEach(function (element) { if (panel == element && !panel.open) { _this2.open(element); } else { _this2.close(element); } }); } //=== Opens all panels just because }, { key: 'openAll', value: function openAll() {var _this3 = this; this._panels.forEach(function (element) { _this3.open(element); }); } //=== Closes all panels just in case }, { key: 'closeAll', value: function closeAll() {var _this4 = this; this._panels.forEach(function (element) { _this4.close(element); }); } // Now put it all together }, { key: '_attachEvents', value: function _attachEvents() {var _this5 = this; this._panels.forEach(function (panel) { var toggler = panel.querySelector("summary"); var contents = panel.querySelector("summary + *"); // On panel open panel.addEventListener("toggle", function (e) { var isReadingHeight = panel.classList.contains(_this5.settings.heightClass); if (panel.open && !isReadingHeight) { _this5._afterOpen(panel); } }); toggler.addEventListener("click", function (e) { // If accordion, stop default toggle behavior if (_this5.settings.accordion) { _this5.openSinglePanel(panel); e.preventDefault(); } // On attempting close, stop default close behavior to substitute our own else if (panel.open) { _this5.close(panel); e.preventDefault(); } // On open, proceed as normal (see toggle listener above) }); /* transitionend fires once for each animated property, but we want it to fire once for each click. So let's make sure to watch only a single property Note this makes complex animations with multiple transition-durations impossible Sorry */ var propToWatch = ''; // On panel finishing open/close animation contents.addEventListener("transitionend", function (e) { // Ignore transitions from child elements if (e.target !== contents) { return; } // Set property to watch on first fire if (!propToWatch) propToWatch = e.propertyName; // If watched property matches currently animating property if (e.propertyName == propToWatch) { var wasOpened = panel.classList.contains(_this5.settings.activeClass); wasOpened ? _this5._endOpen(panel) : _this5._endClose(panel); } }); }); } }, { key: 'init', value: function init() { // Attach functionality this._attachEvents(); // If accordion, open the first panel if (this.settings.accordion) { this.openSinglePanel(this._panels[0]); } // For styling purposes this._container.classList.add(this.settings.initClass); return this; } }]);return Collapse;}(); // initialize the accordion var makeMePretty = document.querySelector(".collapse"); var accordion = new Collapse(makeMePretty, { accordion: true }).init();