1class DotflyAccordion { 2 classes = { 3 element: 'js-accordion', 4 item: 'js-accordion-item', 5 trigger: 'js-accordion-trigger', 6 content: 'js-accordion-content', 7 close: 'js-accordion-close', 8 open: 'is--open' 9 }; 10 11 nodes = { 12 triggers: null, 13 closes: null 14 }; 15 16 constructor() { 17 this.nodes.triggers = document.querySelectorAll(`.${this.classes.element} .${this.classes.trigger}`); 18 this.nodes.closes = document.querySelectorAll(`.${this.classes.element} .${this.classes.close}`); 19 20 // Set initial height and tabindex on active rows. 21 const openItems = document.querySelectorAll(`.${this.classes.item}.${this.classes.open}`); 22 openItems.forEach(item => { 23 const content = item.querySelector(`.${this.classes.content}`); 24 this.setContentHeight(content); 25 this.setFocusable(content, true); 26 }); 27 28 this.addEventListeners(); 29 } 30 31 /** 32 * Add all event handlers 33 */ 34 addEventListeners() { 35 this.nodes.triggers.forEach(trigger => { 36 trigger.addEventListener('click', () => { 37 this.toggleItem(trigger); 38 }); 39 }); 40 41 this.nodes.closes.forEach(close => { 42 close.addEventListener('click', () => { 43 const item = close.closest(`.${this.classes.item}`); 44 if (item) { 45 const trigger = item.querySelector(`.${this.classes.trigger}`); 46 this.closeItem(trigger); 47 } 48 }); 49 }); 50 } 51 52 /** 53 * Toggle the state of an accordion item. 54 * @param {HTMLElement} trigger The trigger button for the accordion item. 55 */ 56 toggleItem(trigger) { 57 const item = trigger.closest(`.${this.classes.item}`); 58 59 if (item.classList.contains(this.classes.open)) { 60 this.closeItem(trigger); // Close if already open 61 } else { 62 // Close all open items first 63 const openItems = document.querySelectorAll(`.${this.classes.item}.${this.classes.open}`); 64 openItems.forEach(openItem => { 65 const openTrigger = openItem.querySelector(`.${this.classes.trigger}`); 66 this.closeItem(openTrigger); 67 }); 68 69 this.openItem(trigger); // Then open the new one 70 } 71 } 72 73 /** 74 * Open a closed accordion row. 75 * @param {HTMLElement} trigger The trigger button for the accordion item. 76 * @param {boolean} scrollIntoView Scroll element into viewport. 77 */ 78 async openItem(trigger, scrollIntoView = true) { 79 if (!trigger) return; 80 81 const item = trigger.closest(`.${this.classes.item}`); 82 if (!item) return; 83 84 const content = item.querySelector(`.${this.classes.content}`); 85 this.setContentHeight(content); 86 item.classList.add(this.classes.open); 87 trigger.setAttribute('aria-expanded', 'true'); 88 content.setAttribute('aria-hidden', 'false'); 89 90 // Make content focusable 91 this.setFocusable(content, true); 92 93 // Scroll into view 94 if (scrollIntoView && window.innerWidth < 1440) { 95 await new Promise(resolve => setTimeout(resolve, 500)); 96 trigger.scrollIntoView({ behavior: 'smooth', block: 'start' }); 97 } 98 } 99 100 /** 101 * Close an open accordion row. 102 * @param {HTMLElement} trigger The trigger button for the accordion item. 103 */ 104 closeItem(trigger) { 105 if (!trigger) return; 106 107 const item = trigger.closest(`.${this.classes.item}`); 108 if (!item) return; 109 110 const content = item.querySelector(`.${this.classes.content}`); 111 112 item.classList.remove(this.classes.open); 113 trigger.setAttribute('aria-expanded', 'false'); 114 content.setAttribute('aria-hidden', 'true'); 115 116 // Make content non-focusable 117 this.setFocusable(content, false); 118 } 119 120 /** 121 * Set or remove focusable elements inside the content. 122 * @param {HTMLElement} content The content of the accordion item. 123 * @param {boolean} focusable Whether the content should be focusable or not. 124 */ 125 setFocusable(content, focusable) { 126 if (!content) return; 127 128 const focusableElements = content.querySelectorAll('a, button'); 129 const tabindexValue = focusable ? '0' : '-1'; 130 131 focusableElements.forEach(element => { 132 element.setAttribute('tabindex', tabindexValue); 133 }); 134 135 const closeButton = content.closest(`.${this.classes.item}`).querySelector(`.${this.classes.close}`); 136 if (closeButton) { 137 closeButton.setAttribute('tabindex', tabindexValue); 138 } 139 } 140 141 /** 142 * Calculate and set the content height as a CSS variable. 143 * @param {HTMLElement} content The content of the accordion item. 144 */ 145 setContentHeight(content) { 146 if (content && content.firstElementChild) { 147 const height = content.firstElementChild.offsetHeight; 148 content.style.setProperty('--accordion-content-height', `${height}px`); 149 } 150 } 151} 152 153document.addEventListener('DOMContentLoaded', () => { 154 new DotflyAccordion(); 155});
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.