1(function (Drupal) { 2 'use strict'; 3 4 /** 5 * Accordion configuration for @accede-web/accordion 6 */ 7 const ACCORDION_CONFIG = { 8 duration: 300, 9 ariaEnabled: true, 10 collapse: true, 11 showMultiple: false, 12 onlyChildNodes: true, 13 elementClass: 'apb-accordion-item', 14 triggerClass: 'apb-accordion-title', 15 panelClass: 'apb-accordion-panel', 16 activeClass: 'apb-accordion-active', 17 // beforeOpen: (element) => scrollToAccordionItem(element), 18 onOpen: element => scrollToAccordionItem(element), 19 }; 20 21 /** 22 * Use global APB utils 23 */ 24 const DOM = window.APB.DOM; 25 26 /** 27 * Check if GSAP is available 28 */ 29 const isGSAP = () => window?.gsap && typeof window.gsap.to === 'function'; 30 31 const docTop = el => el.getBoundingClientRect().top + window.scrollY; 32 33 const canGSAPScroll = () => 34 !!( 35 window.gsap && 36 typeof window.gsap.to === 'function' && 37 window.ScrollToPlugin 38 ); 39 40 const scrollToAccordionItem = element => { 41 const header = DOM.one('.apb-accordion-title', element) || element; 42 if (!header) return; 43 44 const headerOffset = DOM.hasClass(document.body, 'mobile') 45 ? 16 46 : (DOM.one('header.header')?.offsetHeight || 0) + 16; 47 48 const target = Math.max(0, docTop(header) - headerOffset); 49 50 requestAnimationFrame(() => { 51 if (canGSAPScroll()) { 52 gsap.to(window, { 53 duration: 0.8, 54 scrollTo: { y: target, autoKill: false }, 55 ease: 'power2.out', 56 overwrite: 'auto', 57 }); 58 } else { 59 window.scrollTo({ top: target, behavior: 'smooth' }); 60 } 61 }); 62 }; 63 64 /** 65 * Parse color style block from wrapper 66 */ 67 const parseColorStyle = wrapper => { 68 const el = DOM.one('.field--name-field-apb-color-style-02', wrapper); 69 if (!el) return null; 70 71 return { 72 name: DOM.text(DOM.one('.term-machine-name', el)), 73 title: DOM.text(DOM.one('.apb-tax-cs-title', el)), 74 mainColor: DOM.text(DOM.one('.apb-tax-cs-main-color', el)), 75 titleHover: DOM.text(DOM.one('.apb-tax-cs-title-hover', el)), 76 hover: DOM.text(DOM.one('.apb-tax-cs-hover', el)), 77 titleActive: DOM.text(DOM.one('.apb-tax-cs-title-active', el)), 78 active: DOM.text(DOM.one('.apb-tax-cs-active', el)), 79 }; 80 }; 81 82 /** 83 * Extract accordion configuration from fields 84 */ 85 const getAccordionConfig = wrapper => ({ 86 sectionTitle: DOM.text( 87 DOM.one('.field--name-field-apb-section-title', wrapper) 88 ), 89 showSectionTitle: DOM.text( 90 DOM.one('.field--name-field-apb-show-section-title', wrapper) 91 ), 92 customClassGroup: DOM.text( 93 DOM.one('.field--name-field-apb-custom-class', wrapper) 94 ), 95 colorStyle: parseColorStyle(wrapper), 96 contentWidth: DOM.text( 97 DOM.one('.field--name-field-apb-width-tx .term-machine-name', wrapper) 98 ).replace('_', ''), 99 accordionId: DOM.text(DOM.one('.paragraphs-item-id', wrapper)), 100 }); 101 102 /** 103 * Apply extracted configuration to wrapper 104 */ 105 const applyConfig = (config, wrapper) => { 106 if ( 107 config.sectionTitle && 108 config.showSectionTitle.toLowerCase() === 'yes' 109 ) { 110 DOM.removeClass( 111 DOM.one('.field--name-field-apb-section-title', wrapper), 112 'apb-setup' 113 ); 114 } 115 116 if (config.contentWidth && config.contentWidth !== '100') { 117 const style = ` 118 max-width: ${config.contentWidth}% !important; 119 margin-left: auto !important; 120 margin-right: auto !important; 121 `; 122 DOM.setAttr(wrapper, 'style', style); 123 } 124 125 if (config.customClassGroup) { 126 DOM.addClass(wrapper, config.customClassGroup); 127 } 128 129 if (config.colorStyle?.name) { 130 DOM.addClass(wrapper, `ac-${config.colorStyle.name}`); 131 } 132 133 if (config.colorStyle && config.accordionId) { 134 const { mainColor, hover, active, title, titleHover, titleActive } = 135 config.colorStyle; 136 137 const cssId = `apb-accordion-${config.accordionId}`; 138 const customCSS = ` 139 #${cssId} .apb-accordion-title { background: ${mainColor} !important; color: ${title} !important; } 140 #${cssId} .apb-accordion-title:hover { background: ${hover} !important; color: ${titleHover} !important; } 141 #${cssId} .apb-accordion-title.apb-accordion-active, 142 #${cssId} .apb-accordion-title.apb-accordion-active:hover { background: ${active} !important; color: ${titleActive} !important; } 143 #${cssId} .apb-accordion-active .apb-accordion-title, 144 #${cssId} .apb-accordion-active .apb-accordion-title.apb-accordion-active:hover { background: ${active} !important; color: ${titleActive} !important; } 145 `; 146 DOM.injectCSS(customCSS); 147 } 148 149 if (config.accordionId) { 150 wrapper.id = `apb-accordion-${config.accordionId}`; 151 } 152 }; 153 154 /** 155 * Ensure the accordion structure matches what the library expects 156 */ 157 const prepareHTMLOld = accordionView => { 158 const items = (() => { 159 try { 160 return DOM.all(':scope > div', accordionView); 161 } catch { 162 return Array.from(accordionView.children).filter( 163 el => el.tagName.toLowerCase() === 'div' 164 ); 165 } 166 })(); 167
168 items.forEach(item => { 169 if (!item) return; 170 171 if (!item.textContent.trim()) { 172 console.warn('Skipping empty accordion item:', item); 173 return; 174 } 175 176 // Remove admin noise 177 item 178 .querySelectorAll( 179 '.contextual-links, .contextual-links-wrapper, .contextual-links__trigger' 180 ) 181 .forEach(el => el.remove()); 182 183 item 184 .querySelectorAll('a[href*="edit"], .edit-link') 185 .forEach(el => el.remove()); 186 187 // Ensure header 188 let header = DOM.one('.apb-accordion-title', item); 189 if (!header) { 190 const maybeTitle = item.querySelector('.title, h3, span'); 191 if (maybeTitle) { 192 header = document.createElement('div'); 193 header.className = 'apb-accordion-title'; 194 header.innerHTML = maybeTitle.innerHTML; 195 maybeTitle.remove(); 196 item.insertBefore(header, item.firstChild); 197 } else { 198 console.warn('Could not create header, removing item:', item); 199 item.remove(); 200 return; 201 } 202 } 203 204 if (header.tagName !== 'BUTTON') { 205 header.setAttribute('role', 'button'); 206 header.setAttribute('tabindex', '0'); 207 208 header.addEventListener('keydown', event => { 209 if (event.key === 'Enter' || event.key === ' ') { 210 event.preventDefault(); 211 header.click(); 212 } 213 }); 214 } 215 216 // Ensure panel 217 let panel = DOM.one('.accordion-body', item); 218 if (!panel) { 219 console.warn('No panel found, removing item:', item); 220 item.remove(); 221 return; 222 } 223 if (!DOM.hasClass(panel, 'apb-accordion-panel')) { 224 DOM.addClass(panel, 'apb-accordion-panel'); 225 } 226 227 // Add base item class 228 if (!DOM.hasClass(item, 'apb-accordion-item')) { 229 DOM.addClass(item, 'apb-accordion-item'); 230 } 231 232 // Apply custom class if present 233 const customClass = DOM.text( 234 DOM.one('.apb-accordion-title .apb-ac-custom-class', item) 235 ); 236 if (customClass) DOM.addClass(item, customClass); 237 }); 238 }; 239 240 const prepareHTML = accordionView => { 241 // 1) Remove root-level noise that is not an item 242 accordionView 243 .querySelectorAll(':scope > .contextual, :scope > header') 244 .forEach(el => el.remove()); 245 246 // 2) Collect candidate items among remaining direct children 247 const children = Array.from(accordionView.children).filter( 248 el => el.tagName === 'DIV' 249 ); 250 251 const items = children.filter( 252 el => 253 el.classList.contains('apb-accordion-item') || 254 el.querySelector('.apb-accordion-title') || 255 el.querySelector('.accordion-body') 256 ); 257 258 // 3) Normalize each item 259 items.forEach(item => { 260 // Remove admin noise INSIDE items 261 item 262 .querySelectorAll( 263 '.contextual, .contextual-links, .contextual-links-wrapper, .contextual-links__trigger, a[href*="edit"], .edit-link' 264 ) 265 .forEach(el => el.remove()); 266 267 // Base item class 268 item.classList.add('apb-accordion-item'); 269 270 // Ensure header (prefer existing) 271 let header = item.querySelector('.apb-accordion-title'); 272 273 if (!header) { 274 const seed = item.querySelector('.title, h3, span'); 275 if (!seed) { 276 console.warn('Item missing title; removing:', item); 277 item.remove(); 278 return; 279 } 280 const btn = document.createElement('button'); 281 btn.type = 'button'; 282 btn.className = 'apb-accordion-title'; 283 btn.innerHTML = seed.innerHTML; 284 seed.remove(); 285 item.insertBefore(btn, item.firstChild); 286 header = btn; 287 } else if (header.tagName !== 'BUTTON') { 288 // Upgrade to real <button> for a11y & consistency 289 const btn = document.createElement('button'); 290 btn.type = 'button'; 291 btn.className = header.className; 292 btn.innerHTML = header.innerHTML; 293 header.replaceWith(btn); 294 header = btn; 295 } 296 297 // Ensure panel 298 let panel = item.querySelector('.apb-accordion-panel'); 299 if (!panel) { 300 panel = item.querySelector('.accordion-body'); 301 if (!panel) { 302 console.warn('Item missing panel; removing:', item); 303 item.remove(); 304 return; 305 } 306 panel.classList.add('apb-accordion-panel'); 307 } 308 309 // Ensure trigger BEFORE panel 310 if (panel.previousElementSibling !== header) { 311 item.insertBefore(header, panel); 312 } 313 }); 314 315 // 4) Remove any leftover non-items at root
316 Array.from(accordionView.children).forEach(ch => { 317 if (!ch.classList.contains('apb-accordion-item')) ch.remove(); 318 }); 319 }; 320 321 /** 322 * Initialize accordion instance 323 */ 324 const initAccordion = wrapper => { 325 if (!wrapper) return console.error('Invalid wrapper for accordion'); 326 327 if (DOM.hasClass(wrapper, 'apb-accordion-initialized')) { 328 // console.log('Accordion already initialized, skipping:', wrapper); 329 return; 330 } 331 332 const config = getAccordionConfig(wrapper); 333 applyConfig(config, wrapper); 334 335 const view = DOM.one('.view-apb-pi-accordion-item', wrapper); 336 if (!view) return console.warn('Accordion view not found'); 337 338 prepareHTML(view); 339 340 if (typeof window.Accordion === 'undefined') { 341 console.error('@accede-web/accordion not loaded'); 342 return; 343 } 344 345 if (view.children.length === 0) { 346 console.warn('No accordion items found in view'); 347 return; 348 } 349 350 try { 351 const accordion = new window.Accordion(view, ACCORDION_CONFIG); 352 // console.log(`Accordion initialized with ${view.children.length} items`); 353 354 DOM.addClass(wrapper, 'apb-accordion-initialized'); 355 356 // Nested support 357 const nestedAccordions = DOM.all( 358 '.view-apb-pi-accordion-item', 359 wrapper 360 ); 361 nestedAccordions.forEach(nested => { 362 if (nested !== view) { 363 const nestedWrapper = nested.closest( 364 '.paragraph--type--apb-pb-accordion' 365 ); 366 if ( 367 nestedWrapper && 368 !DOM.hasClass(nestedWrapper, 'apb-accordion-initialized') 369 ) { 370 console.log('Initializing nested accordion:', nestedWrapper); 371 initAccordion(nestedWrapper); 372 } 373 } 374 }); 375 376 return accordion; 377 } catch (err) { 378 console.error('Error initializing accordion:', err); 379 } 380 }; 381 382 /** 383 * Drupal behavior for APB Accordion 384 */ 385 Drupal.behaviors.apbAccordion = { 386 attach: context => { 387 const wrappers = DOM.all( 388 '.paragraph--type--apb-pb-accordion', 389 context 390 ); 391 if (!wrappers.length) return; 392 393 if (isGSAP() && window.gsap.registerPlugin) { 394 window.gsap.registerPlugin(window.ScrollToPlugin); 395 } 396 397 const apbWrapper = DOM.one('#apb-wrapper'); 398 399 wrappers.forEach(wrapper => { 400 if (apbWrapper && apbWrapper.contains(wrapper)) { 401 initAccordion(wrapper); 402 } else { 403 // Fallback: run even if not inside #apb-wrapper 404 initAccordion(wrapper); 405 } 406 }); 407 }, 408 }; 409})(Drupal);
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.