1 const collapsedClass = 'is-collapsed'; 2 const expandedClass = 'is-expanded'; 3 /** 4 * Collapsible - A plug and play plugin for expanding and 5 * collapsing elements (i.e. accordion) on a website. 6 * 7 * @author Murtada al Mousawy (https://murtada.nl) 8 */ 9 (function() { 10 'use strict'; 11 12 /** 13 * Creates an instance of Collapsible. 14 * 15 * @constructor 16 * @param {Object} options 17 * @param {(HTMLElement|NodeList)} options.node The HTML elements that will be manipulated. 18 * @param {HTMLElement} [options.eventNode] The HTML element on which the eventListener will be attached. 19 * @param {Boolean} [options.isCollapsed] Assign the state of the node element. 20 * @param {Boolean} [options.observe] Assign a MutationObserver to observe child DOM changes. 21 * @param {Function} [options.expandCallback] Assign a callback for the [{@link Collapsible.prototype.expand} event. 22 * @param {Function} [options.collapseCallback] Assign a callback for the {@link Collapsible.prototype.collapse} event. 23 * @param {Function} [options.observeCallback] Assign a callback for the {@link Collapsible.prototype.initObserver} event. 24 */ 25 var Collapsible = function(options) { 26 // Initialize HTML nodes 27 if (NodeList.prototype.isPrototypeOf(options.node)) { 28 options.node.forEach(function(nodeItem) { 29 var singleNodeOptions = options; 30 singleNodeOptions.node = nodeItem; 31 new Collapsible(singleNodeOptions); 32 }); 33 return; 34 } else if (options.node instanceof HTMLElement) { 35 this.node = options.node; 36 this.eventNode = (options.eventNode ? this.node.querySelector(options.eventNode) : this.node); 37 38 this.stopBreakpoint = this.node.dataset.stopBreakpoint; 39 40 this.isCollapsed = (typeof this.node.dataset.collapsibleCollapsed !== 'undefined' 41 ? true 42 : null); 43 44 if (!this.isCollapsed) { 45 this.isCollapsed = ((options.isCollapsed 46 && typeof options.isCollapsed === 'boolean') 47 ? options.isCollapsed 48 : false); 49 } 50 51 this.observe = (typeof options.observe === 'boolean' ? options.observe : false); 52 this.expandCallback = (typeof options.expandCallback === 'function' ? options.expandCallback : null); 53 this.collapseCallback = (typeof options.collapseCallback === 'function' ? options.collapseCallback : null); 54 this.observeCallback = (typeof options.observeCallback === 'function' ? options.observeCallback : null); 55 this.mutationCallback = (typeof options.mutationCallback === 'function' ? options.mutationCallback : null); 56 57 this.init(); 58 } else { 59 console.error(options.node, 'is not a NodeList or an instance of HTMLElement'); 60 } 61 }; 62 63 /** 64 * Initialize the collapsing and expanding events. 65 */ 66 Collapsible.prototype.init = function() { 67 this.updateHeights(); 68 69 if (this.isCollapsed) { 70 this.node.style.height = this.collapsedHeight + 'px'; 71 this.node.classList.add(collapsedClass); 72 } else { 73 this.node.classList.add(expandedClass); 74 } 75 76 this.eventNode.addEventListener('click', function(e) { 77 let windowWidth = window.innerWidth; 78 79 if (!(this.stopBreakpoint === 'small-desktop' && windowWidth >= bpLG) && !(this.stopBreakpoint === 'desktop' && windowWidth >= bpXL)) { 80 e.preventDefault(); 81 } 82 83 this.toggleCollapse(); 84 }.bind(this)); 85 86 window.addEventListener('resize', this.updateHeights.bind(this, null)); 87 window.addEventListener('orientationchange', this.updateHeights.bind(this, null)); 88 89 // Observe children of the node 90 if (this.observe) { 91 this.initObserver(); 92 } 93 94 // Attach the prototype instance to the node 95 this.node.collapsible = this; 96 }; 97 98 /** 99 * Update the collapsed and expanded heights on page resize. 100 * 101 * @param {int} [heightDifference] Height value to add or subtract from the parent. 102 */ 103 Collapsible.prototype.updateHeights = function(heightDifference) { 104 let windowWidth = window.innerWidth; 105 106 if ((this.stopBreakpoint === 'small-desktop' && windowWidth >= bpLG) || (this.stopBreakpoint === 'desktop' && windowWidth >= bpXL)) { 107 return false;
108 } 109 110 heightDifference = heightDifference || 0; 111 112 // Calculate the collapsed height 113 this.collapsedHeight = Collapsible.parseNumber( 114 window.getComputedStyle(this.eventNode)['height'] 115 ) + Collapsible.parseNumber( 116 window.getComputedStyle(this.eventNode)['margin-bottom']); 117 118 // Calculate the expanded height 119 this.node.style.height = 'auto'; 120 121 this.expandedHeight = Collapsible.parseNumber( 122 window.getComputedStyle(this.node)['height'] 123 ); 124 125 // Add or subtract the childNode's height difference 126 this.expandedHeight += heightDifference; 127 this.expandedHeight = Math.max(this.expandedHeight, this.collapsedHeight); 128 129 // Reset height to what it was before 130 if (this.isCollapsed) { 131 this.node.style.height = this.collapsedHeight + 'px'; 132 } 133 134 this.updateParentNode(this.expandedHeight - this.collapsedHeight); 135 }; 136 137 /** 138 * Toggle the node state and calls the appropriate function. 139 */ 140 Collapsible.prototype.toggleCollapse = function() { 141 if (this.isCollapsed) { 142 this.expand(); 143 } else { 144 this.collapse(); 145 } 146 }; 147 148 /** 149 * Expand the node. 150 */ 151 Collapsible.prototype.expand = function() { 152 this.updateHeights(); 153 154 void this.node.offsetWidth; 155 156 this.node.style.height = 'auto'; 157 this.node.style.height = this.expandedHeight + 'px'; 158 this.node.classList.remove(collapsedClass); 159 this.node.classList.add(expandedClass); 160 161 this.isCollapsed = false; 162 163 // Create a custom event 164 var expandEvent = new CustomEvent('toggle', { 165 bubbles: true, 166 detail: { 167 action: 'expand', 168 origin: this.eventNode 169 } 170 }); 171 172 this.node.dispatchEvent(expandEvent); 173 174 // Run callback if it exists 175 if (this.expandCallback) { 176 this.expandCallback.bind(this, expandEvent)(); 177 } 178 179 this.updateParentNode(this.expandedHeight - this.collapsedHeight); 180 }; 181 182 /** 183 * Collapse the node. 184 */ 185 Collapsible.prototype.collapse = function() { 186 this.node.style.height = window.getComputedStyle(this.node)['height']; 187 188 void this.node.offsetWidth; 189 190 this.node.style.height = this.collapsedHeight + 'px'; 191 this.node.classList.remove(expandedClass); 192 this.node.classList.add(collapsedClass); 193 194 this.isCollapsed = true; 195 196 // Create a custom event 197 var collapseEvent = new CustomEvent('toggle', { 198 bubbles: true, 199 detail: { 200 action: 'collapse', 201 origin: this.eventNode 202 } 203 }); 204 205 this.node.dispatchEvent(collapseEvent); 206 207 // Run callback if it exists 208 if (this.collapseCallback) { 209 this.collapseCallback.bind(this, collapseEvent)(); 210 } 211 212 this.updateParentNode(-(this.expandedHeight - this.collapsedHeight)); 213 }; 214 215 /** 216 * Update parent heights if collapsible. 217 * 218 * @see {@link Collapsible.prototype.updateHeights} 219 */ 220 Collapsible.prototype.updateParentNode = function(heightDifference) { 221 if (this.node.parentNode && this.node.parentNode.collapsible) { 222 this.node.parentNode.collapsible.updateHeights(heightDifference); 223 } 224 }; 225 226 /** 227 * Observe the direct children list of the node. 228 * Will adjust the expanded height automatically if necessary. 229 */ 230 Collapsible.prototype.initObserver = function() { 231 this.mutationObserver = new window.MutationObserver(function(mutationsList) { 232 var mutatedNode, 233 mutationAction; 234 235 if (!this.isCollapsed) { 236 // Calculate the collapsed height 237 this.collapsedHeight = Collapsible.parseNumber( 238 window.getComputedStyle(this.eventNode)['height'] 239 ); 240 241 // Calculate the expanded height 242 this.node.style.height = 'auto'; 243 244 this.updateParentNode(this.expandedHeight - this.collapsedHeight); 245 } 246 247 // Create a custom event 248 var mutationEvent = new CustomEvent('mutate', { 249 bubbles: true, 250 detail: { 251 action: mutationAction, 252 node: mutatedNode 253 } 254 }); 255 256 this.node.dispatchEvent(mutationEvent); 257 258 // Run callback if it exists 259 if (this.mutationCallback) { 260 this.mutationCallback.bind(this, mutationEvent)(); 261 } 262 }.bind(this)); 263 264 this.mutationObserver.observe(this.node, { 265 childList: true, 266 subtree: true 267 }); 268 }; 269 270 // Helper functions 271 Collapsible.parseNumber = function(numberString) { 272 return Number.parseInt(numberString.slice(0, -2)); 273 }; 274 275 // Expose the prototype function to the global scope 276 window.Collapsible = Collapsible; 277 })(); 278 279 280 (function() { 281 let collapsibleParents = document.querySelectorAll('[data-collapsible-parent]'); 282 let collapsibleChildren = document.querySelectorAll('[data-collapsible-child]'); 283 284 new Collapsible({ 285 node: collapsibleChildren, 286 eventNode: '[data-collapsible-child-toggle]', 287 isCollapsed: true, 288 expandCallback: toggleCallback, 289 collapseCallback: toggleCallback 290 }); 291 292 new Collapsible({ 293 node: collapsibleParents, 294 eventNode: '[data-collapsible-parent-toggle]',
295 isCollapsed: false, 296 observe: true, 297 expandCallback: toggleCallback, 298 collapseCallback: toggleCallback 299 }); 300 301 // Listen to 'toggle' event 302 collapsibleParents.forEach(function(category) { 303 category.addEventListener('toggle', function(event) { 304 }); 305 306 category.addEventListener('mutate', function(event) { 307 }); 308 }); 309 310 // The callback used in options 311 function toggleCallback(event) { 312 let $toggle = this.eventNode; 313 let toggleTextOn = $toggle.dataset.textOn; 314 let toggleTextOff = $toggle.dataset.textOff; 315 316 if (!toggleTextOn && !toggleTextOff) { 317 return false; 318 } 319 320 if (event.detail.action === 'expand') { 321 $toggle.innerHTML = toggleTextOff; 322 } else if (event.detail.action === 'collapse') { 323 $toggle.innerHTML = toggleTextOn; 324 } 325 } 326 })();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.