1// dropdownsHandler.js 2 3if (!crypto.randomUUID) { 4 crypto.randomUUID = function () { 5 return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => 6 (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 7 ); 8 }; 9} 10 11export class Dropdown { 12 static registry = new Map(); // keeps track of all dropdown instances 13 14 constructor(element) { 15 this.element = element; 16 this.label = element.querySelector('.dropdown__label'); 17 this.content = element.querySelector('.dropdown__content'); 18 this.isOpen = element.classList.contains('expanded'); 19 this.container = element.closest('.section-dropdowns'); 20 21 if (!this.label || !this.content) { 22 console.warn('Dropdown element missing required structure:', element); 23 return; 24 } 25 26 // Setup accessibility 27 this.id = element.id || crypto.randomUUID(); 28 element.id = this.id; 29 30 this.label.setAttribute('role', 'button'); 31 this.label.setAttribute('tabindex', '0'); 32 this.label.setAttribute('aria-controls', this.id + '-content'); 33 this.label.setAttribute('aria-expanded', this.isOpen ? 'true' : 'false'); 34 this.content.id = this.id + '-content'; 35 this.content.setAttribute('aria-hidden', this.isOpen ? 'false' : 'true'); 36 this.transitionDuration = parseFloat( 37 getComputedStyle(document.documentElement).getPropertyValue('--dropdown-transition-duration') 38 ) || 0.3; 39 40 // Register instance 41 Dropdown.registry.set(this.id, this); 42 43 // Set initial styles 44 this.content.style.display = this.isOpen ? `block` : 'none'; 45 this.content.style.height = this.isOpen ? `${this.content.scrollHeight}px` : '0px'; 46 47 // Bind events 48 this.label.addEventListener('click', () => this.toggle()); 49 this.label.addEventListener('keydown', (e) => { 50 if (e.key === 'Enter' || e.key === ' ') { 51 e.preventDefault(); 52 this.toggle(); 53 } 54 }); 55 } 56 57 toggle() { 58 this.isOpen ? this.collapse() : this.expand(); 59 } 60 61 expand() { 62 if (!this.content) return; 63 64 // Collapse siblings in the same container 65 if (this.container) { 66 Array.from(this.container.querySelectorAll('.dropdown')).forEach((sibling) => { 67 if (sibling !== this.element) { 68 const siblingInstance = Dropdown.registry.get(sibling.id); 69 siblingInstance?.collapse(); 70 } 71 }); 72 } 73 74 this.content.style.display = 'block'; 75 const fullHeight = this.content.scrollHeight + 'px'; 76 this.content.style.height = fullHeight; 77 78 this.element.classList.add('expanded'); 79 this.content.setAttribute('aria-hidden', 'false'); 80 this.label.setAttribute('aria-expanded', 'true'); 81 this.isOpen = true; 82 83 // Reset height after transition to allow dynamic content 84 setTimeout(() => { 85 if (this.isOpen) this.content.style.height = 'auto'; 86 }, this.transitionDuration * 1000); 87 } 88 89 collapse() { 90 if (!this.content) return; 91 92 const currentHeight = this.content.scrollHeight + 'px'; 93 this.content.style.height = currentHeight; 94 95 // Force reflow to trigger transition 96 void this.content.offsetHeight; 97 98 this.content.style.height = '0px'; 99 this.element.classList.remove('expanded'); 100 this.content.setAttribute('aria-hidden', 'true'); 101 this.label.setAttribute('aria-expanded', 'false'); 102 this.isOpen = false; 103 104 setTimeout(() => { 105 this.content.style.display = 'none'; 106 }, this.transitionDuration * 1000); 107 } 108} 109 110export function initDropdowns() { 111 const elements = Array.from(document.querySelectorAll('.dropdown')); 112 elements.forEach((el) => new Dropdown(el)); 113} 114 115/** 116 * Expand the dropdown containing a given element 117 * @param {HTMLElement} element 118 * @returns {boolean} - true if a dropdown was expanded 119 */ 120export function expandDropdownContaining(element) { 121 if (!element) return false; 122 const dropdown = element.closest('.dropdown'); 123 if (!dropdown) return false; 124 125 const instance = Dropdown.registry.get(dropdown.id); 126 if (!instance) return false; 127 128 if (!instance.isOpen) instance.expand(); 129 return true; 130}
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.