https://aardbeienland.nl/wp-content/plugins/easy-accordion-free/Blocks/assets/js/sp-eap-accordion.js
1class EabHelpers { 2 constructor() { 3 this.headingSelector = '.sp-eab-accordion-heading'; 4 } 5 6 qs(selector, el = false) { 7 if (el) { 8 return el.querySelector(selector); 9 } 10 return document.querySelector(selector); 11 } 12 qsAll(selector, el = false) { 13 if (el) { 14 return el.querySelectorAll(selector); 15 } 16 return document.querySelectorAll(selector); 17 } 18} 19class EabAccordion { 20 constructor(selectorEl, options = {}) { 21 this.container = selectorEl; 22 this.items = this.container.querySelectorAll( 23 ':scope > .sp-eab-accordion-item' 24 ); 25 this.options = Object.assign( 26 { 27 openMultiItemAtaTime: true, 28 defaultAccordionOpen: 'close-all', 29 selectedItemOpen: 1, // only works when defaultAccordionOpen === open-selected-item; 30 mode: 'vertical', 31 activeEvent: 'click', 32 transitionDuration: 600, // in ms 33 autoplayDuration: 3000, // in ms 34 autoplayPauseOnHover: true, 35 scrollToTopOnLoad: false, 36 scrollToTopOnClick: false, 37 accordionItemToUrl: false, 38 animationEffect: false, 39 applyAccessibility: true, 40 }, 41 options 42 ); 43 this.isVerticalMode = this.options.mode === 'vertical'; 44 this.#accordionInit(); 45 // autoplay constants. 46 this.currentIndex = 0; 47 this.autoplayInterval = null; 48 this.isAutoplayPaused = false; 49 } 50 51 //Ensure event listeners don't stack when the same DOM 52 //is initialized multiple times (e.g. block re-render / AJAX). 53 #removeBoundEvent(el, eventName, storeKey) { 54 if (!el) { 55 return; 56 } 57 const key = storeKey || `__eab_${eventName}_handler`; 58 const prev = el[key]; 59 if (prev) { 60 el.removeEventListener(eventName, prev); 61 try { 62 delete el[key]; 63 } catch (e) { 64 // ignore if delete fails 65 } 66 } 67 } 68 69 #bindEvent(el, eventName, handler, storeKey) { 70 if (!el || typeof handler !== 'function') { 71 return; 72 } 73 const key = storeKey || `__eab_${eventName}_handler`; 74 this.#removeBoundEvent(el, eventName, key); 75 el.addEventListener(eventName, handler); 76 el[key] = handler; 77 } 78 79 /* ---------------------------- 80 * ACCESSIBILITY SETUP 81 * ----------------------------- */ 82 #getFocusableElements(item) { 83 const selectors = `a[href], area[href], button, input:not([type="hidden"]), select, textarea, iframe`; 84 return [...item.querySelectorAll(selectors)].filter((el) => { 85 return ( 86 !el.disabled && 87 el.getAttribute('aria-hidden') !== 'true' && 88 el.offsetParent !== null 89 ); 90 }); 91 } 92 #applyAccessibilityAttributes(item, index) { 93 const trigger = item.querySelector('.sp-eab-accordion-heading'); 94 const content = item.querySelector('.sp-eab-accordion-content'); 95 const uniqueId = item?.getAttribute('id'); 96 const shortId = uniqueId?.slice(-6) || index; 97 const triggerId = `eab-trigger${shortId}`; 98 const contentId = `eab-content${shortId}`; 99 // --- Accessible roles & attributes --- 100 trigger.setAttribute('id', triggerId); 101 trigger.setAttribute('role', 'button'); 102 trigger.setAttribute('tabindex', '0'); 103 trigger.setAttribute('aria-controls', contentId); 104 trigger.setAttribute('aria-expanded', 'false'); 105 trigger.setAttribute('aria-disabled', 'false'); 106 107 content.setAttribute('id', contentId); 108 content.setAttribute('role', 'region'); 109 content.setAttribute('aria-labelledby', triggerId); 110 111 // --- Keyboard Navigation --- 112 const onKeydown = (e) => { 113 const triggers = [ 114 ...this.container.querySelectorAll('.sp-eab-accordion-heading'), 115 ]; 116 if (e.key !== 'Tab') { 117 e.preventDefault(); 118 } 119 switch (e.key) { 120 case 'Enter': 121 case ' ': 122 this.#toggleAccordionItem(item); 123 break; 124 case 'ArrowDown': 125 case 'ArrowRight': 126 if (triggers[index + 1]) { 127 triggers[index + 1].focus(); 128 } 129 break; 130 case 'ArrowUp': 131 case 'ArrowLeft': 132 if (triggers[index - 1]) { 133 triggers[index - 1].focus(); 134 } 135 break; 136 case 'Home': 137 triggers[0].focus(); 138 break; 139 case 'End': 140 triggers[triggers?.length - 1].focus(); 141 break; 142 case 'Tab': 143 // Only handle forward tab. 144 if (e.shiftKey) { 145 return; 146 } 147 if ( 148 item.classList.contains('eab-expand') && 149 this.#getFocusableElements(item) 150 ) { 151 return; 152 } 153 const nextTrigger = triggers[index + 1]; 154 if (nextTrigger) { 155 e.preventDefault(); 156 nextTrigger.focus(); 157 } 158 break; 159 } 160 }; 161 this.#bindEvent(trigger, 'keydown', onKeydown, '__eabKeydownHandler'); 162 } 163
164 #toggleAriaControls(item, state = 'true') { 165 const trigger = item.querySelector('.sp-eab-accordion-heading'); 166 trigger.setAttribute('aria-expanded', state); 167 } 168 169 /* ------------------------- 170 * ACCORDION INIT 171 * -------------------------- */ 172 #accordionInit() { 173 // INITIALIZE EACH ACCORDION ITEM. 174 const activeEvent = this.options.activeEvent; 175 const defaultOpenItems = this.options.defaultAccordionOpen; 176 const isEabSlugExist = 177 window?.location?.href?.includes('eab-accordion-'); 178 const cssString = this.isVerticalMode 179 ? `max-height: 0; transition: max-height ${this.options.transitionDuration || 500}ms ease;` 180 : `width: 0; transition: width ${this.options.transitionDuration || 500}ms ease;`; 181 182 if (defaultOpenItems === 'close-all') { 183 this.collapseAll(); 184 } 185 186 // LOOP THROUGH EACH ITEM. 187 this.items.forEach((item, index) => { 188 const content = item.querySelector('.sp-eab-accordion-content'); 189 content.style.cssText = cssString; 190 // ADD EVENT LISTENER BASED ON ACTIVE EVENT TYPE. 191 const trigger = item.querySelector('.sp-eab-accordion-heading'); 192 // Clean up any previously bound handlers to avoid double toggles. 193 this.#removeBoundEvent( 194 trigger, 195 'mouseenter', 196 '__eabToggleMouseenterHandler' 197 ); 198 this.#removeBoundEvent(trigger, 'click', '__eabToggleClickHandler'); 199 this.#removeBoundEvent( 200 item, 201 'mouseenter', 202 '__eabAutoplayPauseHandler' 203 ); 204 this.#removeBoundEvent( 205 item, 206 'mouseleave', 207 '__eabAutoplayResumeHandler' 208 ); 209 if (activeEvent === 'hover') { 210 const onTriggerMouseenter = () => 211 this.#toggleAccordionItem(item); 212 this.#bindEvent( 213 trigger, 214 'mouseenter', 215 onTriggerMouseenter, 216 '__eabToggleMouseenterHandler' 217 ); 218 } else if (activeEvent === 'click') { 219 const onTriggerClick = () => this.#toggleAccordionItem(item); 220 this.#bindEvent( 221 trigger, 222 'click', 223 onTriggerClick, 224 '__eabToggleClickHandler' 225 ); 226 } else if (activeEvent === 'auto') { 227 const onTriggerClick = () => this.#toggleAccordionItem(item); 228 this.#bindEvent( 229 trigger, 230 'click', 231 onTriggerClick, 232 '__eabToggleClickHandler' 233 ); 234 // PAUSE AUTOPLAY ON HOVER. 235 if (this.options.autoplayPauseOnHover) { 236 const onPause = () => this.#pauseAutoplay(); 237 const onResume = () => this.#resumeAutoplay(); 238 this.#bindEvent( 239 item, 240 'mouseenter', 241 onPause, 242 '__eabAutoplayPauseHandler' 243 ); 244 this.#bindEvent( 245 item, 246 'mouseleave', 247 onResume, 248 '__eabAutoplayResumeHandler' 249 ); 250 } 251 } 252 253 // DEFAULT OPEN ITEM SET FROM ACCORDION ITEM BLOCK ONLY WORK ON VERTICAL ACCORDION. 254 if (this.isVerticalMode && item.getAttribute('data-default-open')) { 255 this.#openAccordionItem(item); 256 // SCROLL TO TOP ON LOAD. 257 if (this.options.scrollToTopOnLoad) { 258 this.#scrollItemIntoView(item); 259 } 260 } 261 // ACCESSIBILITY INIT. 262 if (this.options.applyAccessibility) { 263 this.#applyAccessibilityAttributes(item, index); 264 } 265 // variations product. 266 const variationsProduct = item.querySelector('.variations_form'); 267 if (variationsProduct) { 268 content.style.overflowY = 'auto'; 269 } 270 }); 271 // ACCORDION AUTOPLAY HANDLER. 272 if (activeEvent === 'auto') { 273 this.#startAutoplay(); 274 } 275 // ACCORDION DEFAULT OPEN HANDLER. 276 if (isEabSlugExist) { 277 return; // skip default open if URL has accordion slug. 278 } 279 if (defaultOpenItems === 'open-all') { 280 this.expandAll(); 281 } else if (defaultOpenItems === 'first-item') { 282 this.#openAccordionItem(this.items[0]); 283 } else if (defaultOpenItems === 'open-selected-item') { 284 const item = this.items[this.options.selectedItemOpen]; 285 if (item) { 286 this.#openAccordionItem(item); 287 } 288 } 289 } 290 291 /* --------------------------------------- 292 * TOGGLE ACCORDION ITEM 293 * ---------------------------------------- */ 294 #toggleAccordionItem(item) { 295 // UPDATE INDEX IF ACTIVATOR EVENT IS AUTOPLAY AND CLICKED ON ITEM. 296 if (this.options.activeEvent === 'auto') { 297 const index = Array.from(this.items).indexOf(item); 298 if (index !== -1) { 299 this.currentIndex = index; 300 } 301 } 302 // TOGGLE ITEM FUNCTIONALITY. 303 if (item.classList.contains('eab-expand')) { 304 this.#closeAccordionItem(item); 305 } else { 306 if (!this.options.openMultiItemAtaTime) { 307 this.collapseAll(); 308 } 309 this.#openAccordionItem(item); 310 // if (this.options.accordionItemToUrl) { 311 // this.#accordionToUrl(item); 312 // } 313 } 314 } 315 /* ----------------------------------- 316 * HANDLE OPEN ACCORDION ITEM 317 * ------------------------------------ */ 318 #openAccordionItem(item) { 319 if (!item) { 320 return; 321 } 322 // APPLY CONTENT ANIMATION. 323 this.#handleContentAnimation(item, 'APPLY'); 324 const content = item.querySelector('.sp-eab-accordion-content'); 325 item.classList.add('eab-expand'); 326 if (this.isVerticalMode) { 327 content.style.maxHeight = content.scrollHeight + 'px'; 328 } else {
329 content.style.width = content.scrollWidth + 'px'; 330 } 331 // ACCESSIBILITY TOGGLE. 332 if (this.options.applyAccessibility) { 333 this.#toggleAriaControls(item, 'true'); 334 } 335 } 336 /* ------------------------------------ 337 * HANDLE CLOSE ACCORDION ITEM 338 * ------------------------------------- */ 339 #closeAccordionItem(item) { 340 // REMOVE CONTENT ANIMATION. 341 this.#handleContentAnimation(item, 'REMOVE'); 342 const content = item.querySelector('.sp-eab-accordion-content'); 343 item.classList.remove('eab-expand'); 344 if (this.isVerticalMode) { 345 content.style.maxHeight = '0px'; 346 } else { 347 content.style.width = '0px'; 348 } 349 // ACCESSIBILITY TOGGLE. 350 if (this.options.applyAccessibility) { 351 this.#toggleAriaControls(item, 'false'); 352 } 353 } 354 /* --------------------------------- 355 * METHOD EXPAND ALL ACCORDION 356 * --------------------------------- */ 357 expandAll() { 358 this.items.forEach((item) => this.#openAccordionItem(item)); 359 } 360 /* ------------------------------- 361 * METHOD COLLAPSE ALL ACCORDION 362 * --------------------------------*/ 363 collapseAll() { 364 this.items.forEach((item) => this.#closeAccordionItem(item)); 365 } 366 367 /* -------------------------------- 368 * ACCORDION AUTOPLAY 369 * --------------------------------- */ 370 #startAutoplay() { 371 this.autoplayInterval = setInterval(() => { 372 if (this.isAutoplayPaused) { 373 return; 374 } // skip while paused 375 376 this.collapseAll(); 377 const currentItem = this.items[this.currentIndex]; 378 this.#openAccordionItem(currentItem); 379 380 this.currentIndex = (this.currentIndex + 1) % this.items.length; 381 }, this.options.autoplayDuration); 382 } 383 384 #pauseAutoplay() { 385 this.isAutoplayPaused = true; 386 } 387 388 #resumeAutoplay() { 389 this.isAutoplayPaused = false; 390 } 391 392 /* ---------------------------------- 393 * SCROLL OPENED ITEM TO TOP 394 * ---------------------------------*/ 395 #scrollItemIntoView(item) { 396 const offset = 300; // admin bar or extra spacing 397 const rect = item.getBoundingClientRect(); 398 const scrollTop = window.pageYOffset + rect.top - offset; 399 window.scrollTo({ top: scrollTop, behavior: 'smooth' }); 400 } 401 /* ---------------------------------- 402 * ACCORDION TO URL HASH ITEM 403 * ----------------------------------*/ 404 #accordionToUrl(item) { 405 const url = item 406 .querySelector('.sp-eab-accordion-header-wrapper') 407 ?.getAttribute('data-slug'); 408 if (url) { 409 const newLink = window.location.href.split('#')[0] + url; 410 window.location.href = newLink; 411 } 412 } 413 #accordionOpenByUrl(item) { 414 const slug = window.location?.href?.split('#')[1] || ''; 415 const dataSlug = item 416 .querySelector('.sp-eab-accordion-header-wrapper') 417 ?.getAttribute('data-slug'); 418 if (dataSlug === `#${slug}`) { 419 this.#openAccordionItem(item); 420 } 421 } 422 /* ----------------------------------- 423 * APPLY CONTENT ANIMATION 424 * ------------------------------------ */ 425 #handleContentAnimation(item, action) { 426 const effect = this.options.animationEffect; 427 const animatedWrapper = item?.querySelector( 428 '.sp-eab-accordion-content-wrapper' 429 ); 430 if (!effect || !animatedWrapper) { 431 return; 432 } 433 if (action === 'APPLY') { 434 animatedWrapper.classList.add('animated', effect); 435 } else if (action === 'REMOVE') { 436 animatedWrapper.classList.remove('animated', effect); 437 } 438 } 439} 440 441// ACCORDION SIDEBAR TAB SCRIPT. 442class EabTabAccordion extends EabHelpers { 443 // EabTabAccordion CLASS CONSTRUCTOR. 444 constructor(selectorEl, options = {}) { 445 super(); 446 this.block = selectorEl; 447 this.options = Object.assign( 448 { 449 defaultOpenItem: "first-item", 450 selectedItemOpen: 1, // only works when defaultOpenItem === open-selected-item; 451 activeEvent: "click", 452 transitionDuration: 600, // in ms 453 autoplayDuration: 3000, // in ms 454 scrollToTopOnLoad: false, 455 accordionItemToUrl: false, 456 contentHeight: 'auto', 457 template: '', 458 }, 459 options 460 ); 461 // Elements 462 this.headings = this.qsAll(this.headingSelector, this.block); 463 this.allContents = this.qsAll('.sp-eab-sidebar-tab-item', this.block); 464 this.#initBlock(); 465 } 466 467 // TAB BLOCK INIT. 468 #initBlock() {
469 this.headings.forEach((heading, index) => { 470 // event listener init. 471 const eventListener = 472 this.options.activeEvent === 'hover' ? 'mouseenter' : 'click'; 473 heading.addEventListener(eventListener, () => { 474 this.#openTabItem(heading); 475 }); 476 // default open items. 477 const itemNumber = index + 1; 478 if ( 479 this.options.defaultOpenItem === 'first-item' && 480 itemNumber === 1 481 ) { 482 this.#openTabItem(heading); 483 } 484 if ( 485 this.options.defaultOpenItem === 'open-selected-item' && 486 itemNumber === parseInt(this.options.selectedItemOpen) 487 ) { 488 this.#openTabItem(heading); 489 } 490 }); 491 // navigation wrappers. 492 if (this.options.contentHeight === 'auto') { 493 const contentWrapper = this.qs( 494 '.sp-eab-sidebar-tabs-content', 495 this.block 496 ); 497 const navWrapper = this.qs('.sp-eab-sidebar-tabs-nav', this.block); 498 contentWrapper.style.minHeight = `${navWrapper.offsetHeight}px`; 499 } 500 } 501 502 #openTabItem(heading) { 503 const contentId = heading?.getAttribute('data-tabid'); 504 if (!contentId) { 505 return; 506 } 507 // heading update. 508 this.headings?.forEach((h) => h.classList.remove('eab-expand')); 509 heading.classList.add('eab-expand'); 510 // content update. 511 const content = this.qs(`#${contentId}`, this.block); 512 this.allContents?.forEach((c) => c.classList.remove('eab-expand')); 513 content.classList.add('eab-expand'); 514 515 if ('sidebar-tab-accordion-one' === this.options.template) { 516 const navWrapper = heading.closest('.sp-eab-sidebar-tabs-nav'); 517 const contentWrapper = content.closest('.sp-eab-sidebar-tabs-content'); 518 if (!navWrapper || !contentWrapper) { 519 return; 520 } 521 contentWrapper.style.borderBottomLeftRadius = contentWrapper?.offsetHeight > navWrapper?.offsetHeight ? '' : '0'; 522 } 523 } 524}
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.