1class EnhancedCoreRegistry { 2 constructor() { 3 this.pluginDataRegistry = new Map(); 4 } 5 6 pluginExists(pluginIdentifier, selectorIdentifier) { 7 if (!selectorIdentifier) { 8 return this.pluginDataRegistry.has(pluginIdentifier); 9 } 10 11 if (!this.pluginDataRegistry.has(pluginIdentifier)) { 12 this.pluginDataRegistry.set(pluginIdentifier, new Map()); 13 } 14 15 const pluginConfigurations = this.pluginDataRegistry.get(pluginIdentifier); 16 const hasRegistrations = pluginConfigurations.has('registrations'); 17 return hasRegistrations ? pluginConfigurations.get('registrations').has(selectorIdentifier) : false; 18 } 19 20 addPlugin(pluginIdentifier, pluginClass, selectorIdentifier, configurationOptions) { 21 if (!this.pluginExists(pluginIdentifier)) { 22 this.pluginDataRegistry.set(pluginIdentifier, new Map()); 23 } 24 25 const pluginConfigurations = this.pluginDataRegistry.get(pluginIdentifier); 26 27 pluginConfigurations.set('class', pluginClass); 28 pluginConfigurations.set('identifier', pluginIdentifier); 29 30 if (!pluginConfigurations.has('registrations')) { 31 pluginConfigurations.set('registrations', new Map()); 32 } 33 34 if (!pluginConfigurations.has('instances')) { 35 pluginConfigurations.set('instances', []); 36 } 37 38 const registrationDetails = pluginConfigurations.get('registrations'); 39 if (selectorIdentifier) { 40 registrationDetails.set(selectorIdentifier, {selector: selectorIdentifier, options: configurationOptions}); 41 } 42 43 return this; 44 } 45 46 getPlugin(pluginIdentifier) { 47 return this.pluginDataRegistry.get(pluginIdentifier); 48 } 49 50 removePlugin(pluginIdentifier, selectorIdentifier) { 51 if (!selectorIdentifier) { 52 return this.pluginDataRegistry.delete(pluginIdentifier); 53 } 54 55 const pluginConfigurations = this.pluginDataRegistry.get(pluginIdentifier); 56 if (!pluginConfigurations) { 57 return false; 58 } 59 60 const registrationDetails = pluginConfigurations.get('registrations'); 61 if (!registrationDetails) { 62 return false; 63 } 64 65 registrationDetails.delete(selectorIdentifier); 66 67 return this; 68 } 69 70 clearPlugins() { 71 this.pluginDataRegistry.clear(); 72 73 return this; 74 } 75 76 getPluginNamesAndData() { 77 return Array.from(this.pluginDataRegistry).reduce((accumulator, values) => { 78 const [key, value] = values; 79 accumulator[key] = value; 80 return accumulator; 81 }, {}); 82 } 83} 84 85class Iterator { 86 87 /** 88 * This callback is displayed as a global member. 89 * @callback ObjectIterateCallback 90 * @param {value} value 91 * @param {key} key 92 */ 93 94 /** 95 * Iterates over an object 96 * 97 * @param {Array|Object} source 98 * @param {ObjectIterateCallback} callback 99 * 100 * @returns {*} 101 */ 102 static iterate(source, callback) { 103 if (source instanceof Map) { 104 return source.forEach(callback); 105 } 106 107 if (Array.isArray(source)) { 108 return source.forEach(callback); 109 } 110 111 if (source instanceof FormData) { 112 for (let entry of source.entries()) { 113 callback(entry[1], entry[0]); 114 } 115 return; 116 } 117 118 if (source instanceof NodeList) { 119 return source.forEach(callback); 120 } 121 122 if (source instanceof HTMLCollection) { 123 return Array.from(source).forEach(callback); 124 } 125 126 if (source instanceof Object) { 127 return Object.keys(source).forEach(key => { 128 callback(source[key], key); 129 }); 130 } 131 132 throw new Error(`The element type ${typeof source} is not iterable!`); 133 } 134} 135 136class CoreConfigRegistry { 137 138 constructor() { 139 this.registry = new Map(); 140 } 141 142 setConfig(pluginName, configName, config) { 143 let pluginConfigs = this.registry.get(pluginName) || new Map(); 144 pluginConfigs.set(configName, config); 145 this.registry.set(pluginName, pluginConfigs); 146 147 return this; 148 } 149 150 getConfig(pluginName, configName) { 151 const pluginConfigs = this.registry.get(pluginName); 152 if (configName) { 153 if (pluginConfigs && pluginConfigs.has(configName)) { 154 return pluginConfigs.get(configName); 155 } 156 return undefined; 157 } 158 159 return pluginConfigs || new Map(); 160 } 161 162 deleteConfig(pluginName, configName) { 163 const pluginConfigs = this.registry.get(pluginName); 164 if (pluginConfigs) { 165 pluginConfigs.delete(configName); 166 } 167 168 return this; 169 } 170} 171 172class CoreConfigManagerSingleton { 173 174 constructor() { 175 this.configRegistry = new CoreConfigRegistry(); 176 } 177 178 retrieveConfig(pluginName, configName = null) { 179 return this.configRegistry.getConfig(pluginName, configName); 180 } 181 182 storeConfig(pluginName, configName, config) { 183 return this.configRegistry.setConfig(pluginName, configName, config); 184 } 185 186 deleteConfig(pluginName, configName) { 187 return this.configRegistry.deleteConfig(pluginName, configName); 188 } 189 190 obtainRegistry() { 191 return this.configRegistry; 192 } 193 194} 195 196const CoreConfigManagerInstance = Object.freeze(new CoreConfigManagerSingleton()); 197 198class CoreConfigManager { 199 200 static retrieve(pluginName, configName = false) { 201 return CoreConfigManagerInstance.retrieveConfig(pluginName, configName); 202 } 203 204 static store(pluginName, configName, config) { 205 return CoreConfigManagerInstance.storeConfig(pluginName, configName, config); 206 } 207 208 static delete(pluginName, configName) { 209 return CoreConfigManagerInstance.deleteConfig(pluginName, configName); 210 } 211 212 static getRegistry() { 213 return CoreConfigManagerInstance.obtainRegistry(); 214 } 215 216} 217 218window.CoreConfigManager = CoreConfigManager; 219 220class PluginControlSingleton { 221 222 constructor() { 223 this.controlRegistry = new EnhancedCoreRegistry(); 224 } 225 226 addPlugin( 227 pluginIdentifier, pluginBlueprint, targetSelector = document, 228 settings = {}) { 229 if (this.controlRegistry.pluginExists(pluginIdentifier, targetSelector)) { 230 throw new Error(`Plugin "${pluginIdentifier}" ist bereits registriert.`); 231 } 232 233 return this.controlRegistry.addPlugin(pluginIdentifier, pluginBlueprint, 234 targetSelector, settings); 235 } 236 237 retrievePlugins() { 238 return this.controlRegistry.getPluginNamesAndData(); 239 } 240 241 fetchPlugin(pluginIdentifier, enforce = true) { 242 if (!pluginIdentifier) { 243 throw new Error('Es muss ein Plugin-Name übergeben werden!'); 244 } 245 246 if (!this.controlRegistry.pluginExists(pluginIdentifier)) { 247 if (enforce) { 248 throw new Error( 249 `Das Plugin "${pluginIdentifier}" ist nicht registriert. Möglicherweise müssen Sie es zuerst registrieren.`); 250 } else { 251 this.controlRegistry.addPlugin(pluginIdentifier); 252 } 253 } 254 255 return this.controlRegistry.getPlugin(pluginIdentifier); 256 } 257 258 fetchPluginInstances(pluginIdentifier) { 259 const plugin = this.fetchPlugin(pluginIdentifier); 260 261 return plugin.get('instances'); 262 } 263 264 static _queryElements(targetSelector) {
265 if (targetSelector.startsWith('.')) { 266 const regexEl = /^\.([\w-]+)$/.exec(targetSelector); 267 if (regexEl) { 268 return document.getElementsByClassName(regexEl[1]); 269 } 270 } else if (targetSelector.startsWith('#')) { 271 const regexEl = /^#([\w-]+)$/.exec(targetSelector); 272 if (regexEl) { 273 const el = document.getElementById(regexEl[1]); 274 275 return (el) ? [el] : []; 276 } 277 } else if (/^([\w-]+)$/.exec(targetSelector)) { 278 return document.getElementsByTagName(targetSelector); 279 } 280 281 return document.querySelectorAll(targetSelector); 282 } 283 284 static retrievePluginInstanceFromElement(el, pluginIdentifier) { 285 const instances = PluginControlSingleton.retrievePluginInstancesFromElement( 286 el); 287 288 return instances.get(pluginIdentifier); 289 } 290 291 static _isNode(el) { 292 return el instanceof HTMLElement || el === document || el === window; 293 } 294 295 static retrievePluginInstancesFromElement(el) { 296 if (!PluginControlSingleton._isNode(el)) { 297 throw new Error('Passed element is not an Html element!'); 298 } 299 300 el.pluginInstances = el.pluginInstances || new Map(); 301 302 return el.pluginInstances; 303 } 304 305 static instantiatePluginOnElement( 306 el, pluginBlueprint, settings, pluginIdentifier) { 307 if (typeof pluginBlueprint !== 'function') { 308 throw new Error('Das übergebene Plugin ist keine Funktion oder Classe.'); 309 } 310 311 const instance = PluginController.retrievePluginInstanceFromElement(el, 312 pluginIdentifier); 313 if (!instance) { 314 return new pluginBlueprint(el, settings, pluginIdentifier); 315 } 316 317 return instance._update(); 318 } 319 320 activatePlugin( 321 pluginBlueprint, targetSelector, settings, pluginIdentifier = false) { 322 if (PluginControlSingleton._isNode(targetSelector)) { 323 return PluginControlSingleton.instantiatePluginOnElement(targetSelector, 324 pluginBlueprint, settings, pluginIdentifier); 325 } 326 327 if (typeof targetSelector === 'string') { 328 targetSelector = PluginControlSingleton._queryElements(targetSelector); 329 } 330 331 return Iterator.iterate(targetSelector, el => { 332 PluginControlSingleton.instantiatePluginOnElement(el, pluginBlueprint, 333 settings, pluginIdentifier); 334 }); 335 } 336 337 initializeAllPlugins() { 338 const failures = []; 339 Iterator.iterate(this.retrievePlugins(), 340 (pluginData, pluginIdentifier) => { 341 if (pluginIdentifier && 342 this.controlRegistry.pluginExists(pluginIdentifier)) { 343 const plugin = this.controlRegistry.getPlugin(pluginIdentifier); 344 if (plugin.has('registrations')) { 345 Iterator.iterate(plugin.get('registrations'), 346 registration => { 347 try { 348 this.activatePlugin(plugin.get('class'), 349 registration.selector, registration.options, 350 plugin.get('name')); 351 } 352 catch (error) { 353 failures.push(error); 354 } 355 }); 356 } 357 } 358 }); 359 360 failures.forEach((error) => console.error(error)); 361 } 362 363 deepMerge(...objects) { 364 return objects.reduce((prev, obj) => { 365 Object.keys(obj).forEach(key => { 366 const pVal = prev[key]; 367 const oVal = obj[key]; 368 369 if (Array.isArray(pVal) && Array.isArray(oVal)) { 370 prev[key] = pVal.concat(...oVal); 371 } else { 372 prev[key] = oVal; 373 } 374 }); 375 return prev; 376 }, {}); 377 } 378 379 _isNode(el) { 380 return el instanceof HTMLElement || el === document || el === window; 381 } 382 383 initializePlugin(pluginIdentifier, selector, options) { 384 let plugin; 385 let pluginClass; 386 let mergedOptions; 387 388 if (this.controlRegistry.pluginExists(pluginIdentifier, selector)) { 389 plugin = this.controlRegistry.getPlugin(pluginIdentifier); 390 const registrationOptions = plugin.get('registrations').get(selector); 391 pluginClass = plugin.get('class'); 392 mergedOptions = this.deepMerge(pluginClass.options || {}, 393 this.deepMerge(registrationOptions.options || {}, options || {})); 394 } else { 395 plugin = this.controlRegistry.getPlugin(pluginIdentifier); 396 pluginClass = plugin.get('class'); 397 mergedOptions = this.deepMerge(pluginClass.options || {}, options || {}); 398 } 399 400 this._initializePlugin(pluginClass, selector, mergedOptions, 401 plugin.get('name')); 402 } 403 404 _initializePlugin(pluginClass, selector, options, pluginName = false) { 405 if (this._isNode(selector)) { 406 return PluginControlSingleton._initializePluginOnElement(selector, 407 pluginClass, options, pluginName); 408 } 409 410 if (typeof selector === 'string') { 411 selector = PluginControlSingleton._queryElements(selector); 412 } 413 414 return Iterator.iterate(selector, el => { 415 PluginControlSingleton._initializePluginOnElement(el, pluginClass, 416 options, pluginName); 417 }); 418 } 419 420 static _initializePluginOnElement(el, pluginClass, options, pluginName) { 421 if (typeof pluginClass !== 'function') { 422 throw new Error('The passed plugin is not a function or a class.'); 423 } 424 425 const instance = PluginController.retrievePluginInstanceFromElement(el, 426 pluginName); 427 if (!instance) { 428 return new pluginClass(el, options, pluginName); 429 } 430 431 return instance._update(); 432 } 433 434 static getPluginInstanceFromElement(el, pluginName) { 435 const instances = PluginControlSingleton.getPluginInstancesFromElement(el); 436 437 return instances.get(pluginName); 438 } 439 440 static getPluginInstancesFromElement(el) { 441 if (!this._isNode(el)) { 442 throw new Error('Passed element is not an Html element!'); 443 } 444 445 el.__plugins = el.__plugins || new Map(); 446 447 return el.__plugins; 448 } 449} 450 451const PluginControlInstance = Object.freeze( 452 new PluginControlSingleton()); 453 454class PluginController { 455 456 constructor() { 457 window.PluginController = this; 458 } 459 460 static addPlugin( 461 pluginIdentifier, pluginBlueprint, targetSelector = document, 462 settings = {}) { 463 return PluginControlInstance.addPlugin(pluginIdentifier, pluginBlueprint, 464 targetSelector, 465 settings); 466 } 467 468 static retrievePluginList() { 469 return PluginControlInstance.retrievePlugins(); 470 } 471 472 static fetchPlugin(pluginIdentifier) { 473 return PluginControlInstance.fetchPlugin(pluginIdentifier); 474 } 475 476 static retrievePluginInstances(pluginIdentifier) { 477 return PluginControlInstance.fetchPluginInstances(pluginIdentifier); 478 } 479 480 static retrievePluginInstanceFromElement(el, pluginIdentifier) { 481 return PluginControlSingleton.retrievePluginInstanceFromElement(el, 482 pluginIdentifier); 483 } 484 485 static retrievePluginInstancesFromElement(el) { 486 return PluginControlSingleton.retrievePluginInstancesFromElement(el); 487 } 488 489 static initializeAllPlugins() { 490 PluginControlInstance.initializeAllPlugins(); 491 } 492 493 static initializePlugin(pluginIdentifier, targetSelector, settings) { 494 PluginControlInstance.initializePlugin(pluginIdentifier, targetSelector, 495 settings); 496 } 497} 498 499window.PluginController = PluginController; 500 501class Core { 502 /** 503 * @param {HTMLElement} el 504 * @param {Object} options 505 * @param {string} pluginName 506 */ 507 constructor(el, options = {}, pluginName = false) { 508 if (!this._isNode(el)) { 509 throw new Error('Es ist kein gültiges Element angegeben'); 510 } 511 512 this.el = el; 513 this._pluginName = pluginName || this.constructor.name; 514 this.options = this._mergeOptions(options); 515 this._initialized = false;
516 517 this._registerInstance(); 518 this._init(); 519 } 520 521 init() { 522 throw new Error(`Die "init" Methode für die Klasse "${this._pluginName}" ist nicht definiert.`); 523 } 524 525 _isNode(el) { 526 return el instanceof HTMLElement || el === document || el === window; 527 } 528 529 _init() { 530 if (!this._initialized) { 531 this.init(); 532 this._initialized = true; 533 } 534 } 535 536 _hasAttribute(element, attribute) { 537 if (!this._isNode(element)) { 538 throw new Error('Es ist kein gültiges Element angegeben'); 539 } 540 541 if (typeof element.hasAttribute !== 'function') { 542 return false; 543 } 544 545 return element.hasAttribute(attribute); 546 } 547 548 _getAttribute(element, attribute, strict = true) { 549 if (strict && this._hasAttribute(element, attribute) === false) { 550 throw new Error(`Das benötigte "${attribute}" existiert nicht!`); 551 } 552 553 if (typeof element.getAttribute !== 'function') { 554 if (strict) { 555 throw new Error( 556 'Diese Node unterstützt nicht die getAttribute Funktion!'); 557 } 558 return undefined; 559 } 560 return element.getAttribute(attribute); 561 } 562 563 _parseAnalyze(value) { 564 try { 565 value = value.replace(',', '.'); 566 return JSON.parse(value); 567 } 568 catch { 569 return value.toString(); 570 } 571 } 572 573 _stringHelper(string, separator = '') { 574 return string.split(separator).map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join('').replace(/^./, 575 match => match.toLowerCase()); 576 } 577 578 _getDataAttribute(element, key, strict = true) { 579 const parsedKey = this._stringHelper(key.replace(/^data-/, ''), '-'); 580 if (!this._isNode(element)) { 581 if (strict) throw new Error('Es ist kein gültiges Element angegeben!'); 582 return undefined; 583 } 584 585 if (typeof element.dataset === 'undefined') { 586 if (strict) { 587 throw new Error('Diese Node unterstützt kein dataset attribute!'); 588 } 589 return undefined; 590 } 591 592 const attribute = element.dataset[parsedKey]; 593 if (typeof attribute === 'undefined') { 594 if (strict) { 595 throw new Error(`Das benötigte Attribut "${key}" existiert nicht am Element: ${element}!`); 596 } 597 return attribute; 598 } 599 600 return this._parseAnalyze(attribute); 601 } 602 603 static deepMerge(...objects) { 604 return objects.reduce((prev, obj) => { 605 Object.keys(obj).forEach(key => { 606 const pVal = prev[key]; 607 const oVal = obj[key]; 608 609 if (Array.isArray(pVal) && Array.isArray(oVal)) { 610 prev[key] = pVal.concat(...oVal); 611 } else if (typeof pVal === 'object' && typeof oVal === 'object') { 612 prev[key] = Core.deepMerge(pVal, oVal); 613 } else { 614 prev[key] = oVal; 615 } 616 }); 617 return prev; 618 }, {}); 619 } 620 621 _mergeOptions(options) { 622 const dashedPluginName = this._pluginName.replace(/([A-Z])/g, '$1').toLowerCase(); 623 const dataAttributeConfig = this._getDataAttribute(this.el, `data-${dashedPluginName}-config`, false); 624 const dataAttributeOptions = this._getAttribute(this.el, `data-${dashedPluginName}-options`, false); 625 626 let configOptions = {}; 627 if (dataAttributeConfig) { 628 const configFromManager = CoreConfigManager.retrieve(this._pluginName, dataAttributeConfig); 629 if (typeof configFromManager === 'object' && configFromManager !== null && !Array.isArray(configFromManager)) { 630 configOptions = configFromManager; 631 } 632 } 633 634 let parsedDataAttributeOptions = {}; 635 if (dataAttributeOptions) { 636 try { 637 const parsed = JSON.parse(dataAttributeOptions); 638 if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { 639 parsedDataAttributeOptions = parsed; 640 } 641 } 642 catch (e) { 643 console.error(`Fehler beim Parsen von data-${dashedPluginName}-options: ${e}`); 644 throw e; 645 } 646 } 647 648 const mergeableOptions = [this.constructor.options, this.options, options, configOptions, parsedDataAttributeOptions].map(option => { 649 return (typeof option === 'object' && option !== null && !Array.isArray(option)) ? option : {}; 650 }); 651 652 // Verwenden von Core.deepMerge, um alle validen Objekte zu vereinen 653 return Core.deepMerge(...mergeableOptions); 654 } 655 656 _registerInstance() { 657 const elementPluginInstances = PluginController.retrievePluginInstancesFromElement(this.el); 658 elementPluginInstances.set(this._pluginName, this); 659 660 const plugin = PluginController.fetchPlugin(this._pluginName, false); 661 if (plugin && plugin.get('instances')) { 662 plugin.get('instances').push(this); 663 } else { 664 console.warn(`Plugin ${this._pluginName} ist nicht im PluginController registriert.`); 665 } 666 } 667 668 update() { 669 670 } 671 672 _update() { 673 if (!this._initialized) { 674 return; 675 } 676 677 this.update(); 678 } 679} 680 681class TranslatedLang { 682 /** 683 * Extracts translations from window.TYPO3.lang and stores them 684 * under a freely chosen object key (default: "csConfig"). 685 * 686 * @param {string} configKey Prefix of the language keys, e.g. "splide" 687 * @param {string} objectName Name of the returned object key, default "csConfig" 688 * @returns {Object} e.g. { config: { arrowNext: "Weiter", ⦠} } 689 */ 690 static translatedLangConfig(configKey = '', objectName = 'csConfig') { 691 // Punkt automatisch anhängen, damit "splide" â "splide." 692 const prefix = configKey.endsWith('.') ? configKey : `${configKey}.`; 693 694 const src = window?.TYPO3?.lang ?? {}; 695 const translations = {}; 696 697 for (const [key, value] of Object.entries(src)) { 698 if (key.startsWith(prefix)) { 699 const prop = key.slice(prefix.length); // Präfix + Punkt abschneiden 700 translations[prop] = value; 701 } 702 } 703 704 return { [objectName]: translations }; 705 } 706} 707 708class CsPhoneLinks extends Core { 709 710 static options = { 711 globalWrapperSelector: '.js-global-wrapper' 712 }; 713 714 /** 715 * Formats any given phone number into the format "0 1 2 3 4 5 6" while preserving the leading "+" 716 * @param {string} phoneNumber - The phone number to format 717 * @returns {string} - Formatted phone number 718 * 719 * Examples: 720 * +49 123-456-7890 -> + 4 9 1 2 3 4 5 6 7 8 9 0 721 * (0123) 456-789 -> 0 1 2 3 4 5 6 7 8 9 722 * 00491234567890 -> 0 0 4 9 1 2 3 4 5 6 7 8 9 0 723 * 123456789 -> 1 2 3 4 5 6 7 8 9 724 */ 725 formatPhoneNumber(phoneNumber) { 726 // Remove all non-numeric characters except "+" 727 const cleanedNumber = phoneNumber.replace(/(?!^\+)[^0-9]/g, ''); 728 729 // Add a space between each digit 730 return cleanedNumber.split('').join(' '); 731 } 732 733 /** 734 * Adds ARIA labels to phone links for accessibility 735 */ 736 initPhoneLinks() { 737 const base = this; 738 739 if (!base.el.hasAttribute('aria-label')) { // Skip if already labeled 740 const rawPhoneNumber = base.el.getAttribute('href').replace('tel:', ''); 741 if (rawPhoneNumber) { // Ensures the phone number is valid 742 const translatedStringObject = TranslatedLang.translatedLangConfig('links', 'postFixString'); 743 const ariaPrefixText = translatedStringObject.postFixString.phone; 744 745 const formattedPhoneNumber = this.formatPhoneNumber(rawPhoneNumber); 746 base.el.setAttribute('aria-label', `${ariaPrefixText} ${formattedPhoneNumber}`); 747 } 748 } 749 } 750 751 init() { 752 this.initPhoneLinks(); 753 } 754} 755 756class CsExternalLinks extends Core { 757 758 static options = { 759 classNameForOnlyScreenreaderText: 'h-visible-only-for-screenreader', 760 globalWrapperSelector: '.js-global-wrapper' 761 }; 762 763 /** 764 * Appends a screenreader-only text to an external link if not already present 765 */
766 initExternalLinks() { 767 const base = this; 768 if (!base.el.querySelector('.' + base.options.classNameForOnlyScreenreaderText)) { // Check if span already exists 769 const screenReaderText = document.createElement('span'); 770 screenReaderText.className = base.options.classNameForOnlyScreenreaderText; 771 772 const translatedStringObject = TranslatedLang.translatedLangConfig('links', 'postFixString'); 773 const postfixText = translatedStringObject.postFixString.external; 774 775 screenReaderText.textContent = ` (${postfixText})`; 776 base.el.appendChild(screenReaderText); 777 } 778 } 779 780 init() { 781 this.initExternalLinks(); 782 } 783} 784 785class CsScrollbar extends Core { 786 787 /** 788 * recalculates and sets css variable 789 */ 790 static setScrollbarScreenWidth() { 791 const scrollbarWidth = 2 * Math.round((window.innerWidth - document.body.clientWidth) / 2); 792 793 document.documentElement.style.setProperty('--scrollbar-width', scrollbarWidth + 'px'); 794 } 795 796 init() { 797 CsScrollbar.setScrollbarScreenWidth(); 798 799 window.addEventListener('resize', function() { 800 CsScrollbar.setScrollbarScreenWidth(); 801 }); 802 } 803} 804 805class Aria { 806 /** 807 * Sets the `aria-expanded` attribute on the given element. 808 * 809 * @param {HTMLElement} el - The DOM element on which to set the attribute. 810 * @param {boolean} [isExpanded=false] - Whether the element is expanded (`true`) or collapsed (`false`). 811 */ 812 static setExpanded(el, isExpanded = false) { 813 if (!el) { 814 return; 815 } 816 817 if (isExpanded) { 818 el.setAttribute('aria-expanded', 'true'); 819 } else { 820 el.setAttribute('aria-expanded', 'false'); 821 } 822 } 823} 824 825class Focus { 826 /** 827 * Sets focus to the given element after an optional delay. 828 * 829 * @param {HTMLElement} el - The element to focus. 830 * @param {number} [timeout=1] - Delay in milliseconds before setting focus. 831 */ 832 static setFocusToElement(el, timeout = 1) { 833 if (!el) { 834 return; 835 } 836 837 setTimeout(() => { 838 el.focus(); 839 }, timeout); 840 } 841 842 /** 843 * Finds the next focusable element in the document, optionally skipping elements within 844 * a given container or restricting focus to a parent element. 845 * 846 * Useful for handling custom keyboard navigation (e.g. in modals or menus). 847 * 848 * @param {HTMLElement|null} [elementWhichChildrenAreExcluded=null] - If set, all focusable children inside this element will be excluded from navigation. 849 * @param {boolean} [forward=true] - Direction of navigation: `true` for forward (Tab), `false` for backward (Shift+Tab). 850 * @param {HTMLElement|null} [activeElementParam=null] - Optional override for the currently active element (defaults to `document.activeElement`). 851 * @param {HTMLElement|null} [allowedParentElement=null] - If set, only elements within this container are considered focusable. 852 * @returns {HTMLElement|null} The next focusable element or `null` if none is found. 853 */ 854 static getNextFocusableElement(elementWhichChildrenAreExcluded
854= null, forward = true, activeElementParam = null, allowedParentElement = null) { 855 const activeElement = activeElementParam || document.activeElement; 856 857 const selector = [ 858 'a[href]', 859 'button:not([disabled])', 860 'input:not([type="hidden"]):not([disabled])', 861 'select:not([disabled])', 862 'textarea:not([disabled])', 863 '[tabindex]:not([tabindex="-1"])' 864 ].join(','); 865 866 const focusableElements = Array.from(document.querySelectorAll(selector)) 867 .filter(node => node.offsetParent !== null); 868 869 let idx = focusableElements.indexOf(activeElement); 870 const step = forward ? 1 : -1; 871 let next = null; 872 873 idx += step; 874 while (idx >= 0 && idx < focusableElements.length) { 875 const candidate = focusableElements[idx]; 876 877 // Skip if the current slot is empty (null or undefined) 878 if (!candidate) { 879 idx += step; 880 continue; 881 } 882 883 // Skip if the candidate sits inside a container whose children are excluded 884 if (elementWhichChildrenAreExcluded?.contains(candidate)) { 885 idx += step; 886 continue; 887 } 888 889 // Skip if any ancestor of the candidate has the [inert] attribute 890 if (candidate.closest('[inert]')) { 891 idx += step; 892 continue; 893 } 894 895 // If an "allowed" selector is set, skip everything outside of it 896 if (allowedParentElement && !allowedParentElement.contains(candidate)) { 897 idx += step; continue; 898 } 899 900 // Valid focus target found 901 next = candidate; 902 break; 903 } 904 905 // if no next element left -> focus on document body 906 if (!next) { 907 document.body.tabIndex = -1; 908 Focus.setFocusToElement(document.body); 909 setTimeout(() => document.body.removeAttribute('tabindex'), 10); 910 // leave next on null to prevent other function reactions 911 } 912 913 return next; 914 } 915} 916 917let Scroll$1 = class Scroll { 918 919 /** 920 * Toggles the scrolling behavior of a specified element. 921 * When activated, the element regains its normal scroll behavior. 922 * When deactivated, vertical scrolling is hidden. If the element is the BODY, 923 * additional styles are applied to prevent unintended layout shifts. 924 * 925 * @param {HTMLElement} el - The HTML element to modify scrolling for. 926 * @param {boolean} [activate=true] - Determines whether scrolling should be enabled (true) or disabled (false). 927 */ 928 static toggleScrollBehavior(el, activateScrolling = true) { 929 if (!el) { 930 return; 931 } 932 933 if (activateScrolling) { 934 el.style.overflowY = 'visible'; 935 936 if (el.tagName === 'BODY') { 937 el.style.position = 'initial'; 938 el.style.width = 'initial'; 939 940 const scrollY = parseInt(document.body.style.top || '0', 10) * -1; 941 el.style.top = ''; 942 window.scrollTo(0, scrollY); 943 } 944 } else { 945 el.style.overflowY = 'hidden'; 946 947 if (el.tagName === 'BODY') { 948 el.style.position = 'fixed'; 949 el.style.width = '100%'; 950 951 const scrollY = window.scrollY; 952 el.style.top = `-${scrollY}px`; 953 } 954 } 955 956 CsScrollbar.setScrollbarScreenWidth(); 957 } 958 959 /** 960 * Sets the vertical overflow behavior of the element to 'auto'. 961 * 962 * This allows the element to scroll vertically if its content exceeds its height. 963 * 964 * @param {HTMLElement} el - The element to modify. 965 */ 966 static setScrollBehaviorToAuto(el) { 967 if (!el) { 968 return; 969 } 970 971 el.style.overflowY = 'auto'; 972 973 CsScrollbar.setScrollbarScreenWidth(); 974 } 975}; 976 977class Inert { 978 /** 979 * Adds or removes the `inert` attribute on a single element. 980 * 981 * The `inert` attribute makes the element and its children unfocusable 982 * and inaccessible to assistive technologies. 983 * 984 * @param {HTMLElement} el - The element to modify. 985 * @param {boolean} [hideElement=true] - If `true`, adds the `inert` attribute; if `false`, removes it. 986 */ 987 static setInertAttr(el, hideElement = true) { 988 if (!el) { 989 return; 990 } 991 992 if (hideElement) { 993 el.setAttribute('inert', ''); 994 } else { 995 el.removeAttribute('inert'); 996 } 997 } 998 999 /** 1000 * Adds or removes the `inert` attribute on multiple elements. 1001 * 1002 * @param {HTMLElement[]} elements - Array of elements to modify. 1003 * @param {boolean} [hideElements=true] - If `true`, adds the `inert` attribute to each element; if `false`, removes it. 1004 */ 1005 static setInertAttrOnElements(elements, hideElements = true) {
1006 elements.forEach(el => Inert.setInertAttr(el, hideElements)); 1007 } 1008} 1009 1010class CssProperties { 1011 /** 1012 * Retrieves the value of a CSS property from a specified element. 1013 * If an array of units is provided via `replaceUnits`, the method checks whether the value ends with 1014 * one of these units. If so, the unit is removed and the numeric part is returned as a float. 1015 * If no element is provided, the document root element (document.documentElement) is used by default. 1016 * 1017 * @param {string} name - The name of the CSS property (e.g., '--main-color' or '--container-width'). 1018 * @param {string[]} [replaceUnits='px'] - An array of CSS units to remove (e.g., ['px', 'em', 'rem', 'ms', 's']). 1019 * @param {HTMLElement} [element=document.documentElement] - The HTML element from which the CSS property is read. 1020 * @returns {(number|string|null)} - The numeric value if a unit match was found and stripped, 1021 * or the original value as a trimmed string. Returns null if the element or property is not valid. 1022 */ 1023 static getCssProperty(name, replaceUnits = ['px'], element = document.documentElement) { 1024 if (!element || !window.getComputedStyle) { 1025 return null; 1026 } 1027 1028 const value = getComputedStyle(element).getPropertyValue(name).trim(); 1029 1030 for (const unit of replaceUnits) { 1031 if (value.endsWith(unit)) { 1032 const numeric = parseFloat(value.replace(unit, '').trim()); 1033 if (isNaN(numeric)) { 1034 return null; 1035 } 1036 1037 return unit === 's' ? numeric * 1000 : numeric; // Convert seconds to milliseconds 1038 } 1039 } 1040 1041 return value; // Return original trimmed value if no matching unit was found 1042 } 1043 1044 1045 /** 1046 * Retrieves the value of a CSS property (in pixels) from a specified element. 1047 * If the property is defined in pixels (e.g., '20px'), the 'px' unit is stripped 1048 * and the numeric value is returned. If the property has a different unit, 1049 * the original value is returned unchanged. If no element is provided, 1050 * the document root element (document.documentElement) is used by default. 1051 * 1052 * 1053 * @param {string} name - The name of the CSS property (e.g., '--main-color' or 'width'). 1054 * @param {HTMLElement} [element=document.documentElement] - The HTML element from which the CSS property is read. Defaults to the document root element. 1055 * @returns {(number|string|null)} - The numeric value if the property is in pixels, or the original value as a string if it is in another unit. Returns null if the element or property is not valid. 1056 */ 1057 static getCssPixelProperty(name, element) { 1058 if (!element) { 1059 element = document.documentElement; 1060 } 1061 1062 // Check if the element exists and getComputedStyle is available 1063 if (window.getComputedStyle && element) { 1064 const value = getComputedStyle(element).getPropertyValue(name); 1065 if (value.includes('px')) { 1066 return parseFloat(value.replace('px', '')); // Remove 'px' and convert to a number 1067 } 1068 if (value.includes('ms')) { 1069 return parseFloat(value.replace('ms', '')); // Remove 'ms' and convert to a number 1070 } 1071 return value; // Return the original value if it's not in 'px' 1072 } 1073 1074 return null; // Return null if the element or property name is not valid 1075 } 1076 1077 /** 1078 * Retrieves a CSS time property's value from a specified element. 1079 * If the value ends with 'ms' or 's', the unit is removed and the numeric part is returned as a float. 1080 * If no element is provided, the document root element (document.documentElement) is used by default. 1081 * 1082 * This is a convenience wrapper around `getCssProperty()` with fixed units ['ms', 's']. 1083 * 1084 * @param {string} name - The name of the CSS time property (e.g., '--animation-delay'). 1085 * @param {HTMLElement} [element=document.documentElement] - The HTML element from which the CSS property is read. 1086 * @returns {(number|string|null)} - The numeric value (unit stripped) if a known time unit was found, 1087 * the raw value as a string otherwise, or null if invalid. 1088 */ 1089 static getCssTimeProperty(name, element = document.documentElement) { 1090 return CssProperties.getCssProperty(name, ['ms', 's'], element); 1091 } 1092} 1093 1094// helper/breakpoint.helper.js 1095 1096class Breakpoint { 1097 /** 1098 * Returns a media query string based on a data attribute that contains a CSS variable. 1099 * 1100 * @param {HTMLElement} element - The element with the data attribute. 1101 * @param {string} dataAttr - The data attribute name (e.g. 'data-breakpoint-max'). 1102 * @returns {string|null} The constructed media query string or null if attribute is missing. 1103 */ 1104 static getMediaQueryFromDataAttr(element, dataAttr) { 1105 const value = element.getAttribute(dataAttr); 1106 if (!value) return null; 1107 1108 const px = Number(CssProperties.getCssPixelProperty(value)); 1109 if (dataAttr.includes('max')) { 1110 return `(max-width: ${px}px)`; 1111 } 1112 if (dataAttr.includes('min')) { 1113 return `(min-width: ${px + 1}px)`; 1114 } 1115 1116 return `(max-width: ${px}px)`; // fallback 1117 } 1118 1119 /** 1120 * Returns a media query string based on a CSS custom property (e.g. '--breakpoint-mobile'). 1121 * 1122 * @param {string} cssVar - The CSS variable name (e.g. '--breakpoint-mobile'). 1123 * @returns {string} The constructed media query string. 1124 */ 1125 static getMediaQueryFromCssVar(cssVar) { 1126 const px = Number(CssProperties.getCssPixelProperty(cssVar)); 1127 return `(max-width: ${px}px)`; 1128 } 1129 1130 /** 1131 * Checks if the current viewport matches the media query defined via a data attribute. 1132 * 1133 * @param {HTMLElement} element - The element with the data attribute. 1134 * @param {string} dataAttr - The name of the data attribute. 1135 * @returns {boolean} True if the media query matches, or no attribute is present. 1136 */ 1137 static matches(element, dataAttr) { 1138 if (!element) return true; 1139 const query = this.getMediaQueryFromDataAttr(element, dataAttr); 1140 if (!query) return true; // no attr means "always matches" 1141 return window.matchMedia(query).matches; 1142 } 1143 1144 /** 1145 * Checks if the current viewport is considered "mobile" based on a CSS variable. 1146 * 1147 * @param {string} cssVar - The CSS variable name (e.g. '--breakpoint-mobile'). 1148 * @returns {boolean} True if the viewport width is smaller than or equal to the breakpoint. 1149 */ 1150 static isMobileByCssVar(cssVar) { 1151 return window.matchMedia(this.getMediaQueryFromCssVar(cssVar)).matches; 1152 } 1153 1154 /** 1155 * Checks if the current viewport is considered "desktop" based on a CSS variable. 1156 * 1157 * @param {string} cssVar - The CSS variable name (e.g. '--breakpoint-mobile'). 1158 * @returns {boolean} True if the viewport width is larger than the breakpoint. 1159 */ 1160 static isDesktopByCssVar(cssVar) { 1161 return !this.isMobileByCssVar(cssVar); 1162 } 1163} 1164 1165class CsModalManager extends Core { 1166 static options = { 1167 triggerDataAttribute: 'data-cs-modal-trigger', 1168 triggerLabelOpen: 'data-cs-modal-trigger-label-open', 1169 triggerLabelClose: 'data-cs-modal-trigger-label-close', 1170 triggerActiveClass: 'is-opened', 1171 modalDataAttribute: 'data-cs-modal', 1172 activeClass: 'is-active', 1173 modalInitiallyOpenDataAttribute: 'data-cs-modal-initial-open', 1174 modalMaxBreakpointDataAttribute: 'data-cs-modal-max-breakpoint', 1175 modalMinBreakpointDataAttribute: 'data-cs-modal-min-breakpoint',
1176 modalBreakpointLeaveInertDataAttribute: 'data-cs-modal-breakpoint-leave-inert', 1177 modalContentWrapperSelector: '.js-cs-modal-content-wrapper', 1178 dimmingAreaDataAttribute: 'data-cs-modal-dimming-area', 1179 hiddenAreasDesktopDataAttribute: 'data-cs-modal-hidden-areas-desktop', 1180 hiddenAreasMobileDataAttribute: 'data-cs-modal-hidden-areas-mobile', 1181 hiddenAreasBreakpointDataAttribute: 'data-cs-modal-hidden-areas-breakpoint', 1182 closeSelector: '.js-cs-modal-close', 1183 modalFocusOnOpenDataAttribute: 'data-cs-modal-focus-on-open-selector', 1184 fallbackFocusSelector: '.js-cs-modal-focus-fallback' 1185 }; 1186 1187 /* ------------------------------------------------------------------ 1188 * PUBLIC API -------------------------------------------------------- 1189 * ---------------------------------------------------------------- */ 1190 1191 open(name, setFocus = true, trigger = null) { 1192 1193 const modal = this.findModalByName(name); 1194 if (!modal) return; 1195 1196 if (trigger) { 1197 this.lastOpenTrigger = trigger; 1198 } 1199 1200 // check if modal is deactivated for some breakpoint 1201 if ( 1202 !Breakpoint.matches(modal, CsModalManager.options.modalMaxBreakpointDataAttribute) || 1203 !Breakpoint.matches(modal, CsModalManager.options.modalMinBreakpointDataAttribute) 1204 ) { 1205 return; 1206 } 1207 1208 // close any other open modals 1209 this.close(false); 1210 1211 this.activeModal = modal; 1212 this.activeModalName = name; 1213 1214 CsModalManager.setModalAttributes(modal, true); 1215 this.setTriggerAttributes(name, true); 1216 1217 // set hidden areas to inert 1218 this.setHiddenAreas(modal, true); 1219 1220 // set focus to modal close button 1221 if (setFocus) { 1222 let focusElement = null; 1223 // focus to individual element via data attribute 1224 const focusSelector = modal.getAttribute( 1225 this.options.modalFocusOnOpenDataAttribute 1226 ); 1227 if (focusSelector) { 1228 focusElement = modal.querySelector(focusSelector); 1229 } 1230 // fallback for focus: close button 1231 if (!focusElement) { 1232 const closeSelectorClass = this.options.closeSelector.replace('.', ''); 1233 focusElement = modal.getElementsByClassName(closeSelectorClass)[0]; 1234 } 1235 if (focusElement) { 1236 Focus.setFocusToElement(focusElement); 1237 } else { 1238 console.warn( 1239 `CsModalManager: kein Fokus-Element für Modal "${name}" gefunden.` 1240 ); 1241 } 1242 } 1243 1244 // add listener for background click once per modal instance 1245 const contentWrapper = modal.querySelector(this.options.modalContentWrapperSelector); 1246 if (contentWrapper && !modal.csModalBackgroundBound) { 1247 modal.addEventListener('click', this.handleBackgroundClick); 1248 modal.csModalBackgroundBound = true; 1249 } 1250 1251 // prevent body from scrolling 1252 Scroll$1.toggleScrollBehavior(this.body, false); 1253 1254 // add individual dimming areas 1255 this.bindDimmingArea(modal); 1256 1257 this.dispatchModalEvent('open', modal, name); 1258 } 1259 1260 close(returnFocusToOpenTrigger = true, closeTrigger = null) { 1261 1262 if (!this.activeModal) return; 1263 1264 // activate body scrolling 1265 Scroll$1.toggleScrollBehavior(this.body); 1266 1267 CsModalManager.setModalAttributes(this.activeModal, false); 1268 this.setTriggerAttributes(this.activeModalName, false); 1269 1270 // remove inert from hidden areas 1271 this.setHiddenAreas(this.activeModal, false); 1272 1273 // remove individual dimming areas 1274 this.unbindDimmingArea(); 1275 1276 // return focus to its trigger element 1277 if (returnFocusToOpenTrigger) { 1278 const openTriggerElement = this.lastOpenTrigger; 1279 if (openTriggerElement) { 1280 Focus.setFocusToElement(openTriggerElement); 1281 } else { 1282 const defaultFocusElement = document.querySelector(this.options.fallbackFocusSelector); 1283 1284 if (defaultFocusElement) { 1285 Focus.setFocusToElement(defaultFocusElement); 1286 } else { 1287 console.warn( 1288 `CsModalManager: weder Trigger noch Fallback (${this.options.fallbackFocusSelector}) sichtbar.` 1289 ); 1290 } 1291 } 1292 } 1293 1294 // remove background click listener if it was attached
1295 if (this.activeModal.csModalBackgroundBound) { 1296 this.activeModal.removeEventListener('click', this.handleBackgroundClick); 1297 delete this.activeModal.csModalBackgroundBound; 1298 } 1299 1300 this.dispatchModalEvent('close', this.activeModal, this.activeModalName, closeTrigger); 1301 1302 this.activeModal = null; 1303 this.activeModalName = null; 1304 } 1305 1306 toggle(name, trigger = null) { 1307 (this.activeModalName === name) ? this.close(true, trigger) : this.open(name, true, trigger); 1308 } 1309 1310 /* ------------------------------------------------------------------ 1311 * BINDINGS ---------------------------------------------------------- 1312 * ---------------------------------------------------------------- */ 1313 1314 bindTriggers() { 1315 this.triggers = Array.from( 1316 this.el.querySelectorAll(`[${this.options.triggerDataAttribute}]`) 1317 ); 1318 1319 this.triggers.forEach(trigger => { 1320 this.setInitialTriggerLabel(trigger); 1321 1322 trigger.csModalHandler = (e) => { 1323 e.preventDefault(); 1324 const name = trigger.dataset.csModalTrigger; 1325 if (name) this.toggle(name, trigger); 1326 }; 1327 trigger.addEventListener('click', trigger.csModalHandler); 1328 }); 1329 } 1330 1331 bindCloseSelectors(container = this.el) { 1332 this.closeSelectors = Array.from( 1333 container.querySelectorAll(this.options.closeSelector) 1334 ); 1335 1336 this.closeSelectors.forEach(closeSelector => { 1337 if (closeSelector.csModalHandler) return; // prevent double binding 1338 1339 closeSelector.csModalHandler = (e) => { 1340 e.preventDefault(); 1341 this.close(true, closeSelector); 1342 }; 1343 1344 closeSelector.addEventListener('click', closeSelector.csModalHandler); 1345 }); 1346 } 1347 1348 /* ------------------------------------------------------------------ 1349 * SETUP ------------------------------------------------------------- 1350 * ---------------------------------------------------------------- */ 1351 1352 setInitialModalAttributes() { 1353 this.modals = this.getAllModals(); 1354 1355 this.modals.forEach(modal => { 1356 const shouldBeInitialized = Breakpoint.matches(modal, CsModalManager.options.modalMaxBreakpointDataAttribute) && Breakpoint.matches(modal, CsModalManager.options.modalMinBreakpointDataAttribute); 1357 1358 if (shouldBeInitialized) { 1359 // initially set modal to inert 1360 Inert.setInertAttr(modal); 1361 } 1362 }); 1363 } 1364 1365 openModalOnInit() { 1366 // check for initially open modal 1367 const modals = this.getAllModals(); 1368 const initialOpenModals = modals.filter(modal => 1369 modal.hasAttribute(CsModalManager.options.modalInitiallyOpenDataAttribute) 1370 ); 1371 1372 if (initialOpenModals.length > 1) { 1373 console.warn( 1374 `CsModalManager: Mehr als ein Modal hat das Attribut "${CsModalManager.options.modalInitiallyOpenDataAttribute}". Es wird nur das erste geöffnet.`); 1375 } 1376 1377 if (initialOpenModals.length == 1) { 1378 const firstModal = initialOpenModals[0]; 1379 const name = firstModal.getAttribute(this.options.modalDataAttribute); 1380 if (name) { 1381 setTimeout(() => this.open(name), 0); 1382 } 1383 } 1384 } 1385 1386 /* ------------------------------------------------------------------ 1387 * ATTRIBUTE HELPERS ------------------------------------------------- 1388 * ---------------------------------------------------------------- */ 1389 1390 static setModalAttributes(modal, shouldBeOpened) { 1391 if (shouldBeOpened) { 1392 modal.classList.add(CsModalManager.options.activeClass); 1393 Inert.setInertAttr(modal, false); 1394 } else { 1395 modal.classList.remove(CsModalManager.options.activeClass); 1396 Inert.setInertAttr(modal); 1397 } 1398 } 1399 1400 static destroyModalAttributes(modal) { 1401 modal.classList.remove(CsModalManager.options.activeClass); 1402 if (modal.getAttribute(this.options.modalBreakpointLeaveInertDataAttribute) != '1') { 1403 Inert.setInertAttr(modal, false); 1404 } 1405 } 1406 1407 setInitialTriggerLabel(trigger) { 1408 const openLabel = trigger.getAttribute(this.options.triggerLabelOpen); 1409 if (openLabel) this.setLabelAttributes(trigger, openLabel); 1410 } 1411 1412 setTriggerAttributes(name, shouldBeOpened) { 1413 const triggers = this.triggers.filter(t => t.dataset.csModalTrigger === name); 1414 if (triggers.length === 0) return; 1415
1416 triggers.forEach(trigger => { 1417 trigger.classList.toggle(this.options.triggerActiveClass, shouldBeOpened); 1418 1419 if (trigger.hasAttribute('aria-expanded')) { 1420 Aria.setExpanded(trigger, shouldBeOpened); 1421 } 1422 1423 const openLabel = trigger.getAttribute(this.options.triggerLabelOpen); 1424 const closeLabel = trigger.getAttribute(this.options.triggerLabelClose); 1425 if (!openLabel || !closeLabel) return; 1426 this.setLabelAttributes(trigger, shouldBeOpened ? closeLabel : openLabel); 1427 }); 1428 } 1429 1430 setLabelAttributes(trigger, label) { 1431 trigger.setAttribute('aria-label', label); 1432 trigger.setAttribute('title', label); 1433 } 1434 1435 /* ------------------------------------------------------------------ 1436 * HIDDEN AREA HANDLING ---------------------------------------------- 1437 * ---------------------------------------------------------------- */ 1438 1439 setHiddenAreas(modal, hideAreas = true) { 1440 // always release previous inerted areas first 1441 if (this.inertedAreas) { 1442 this.inertedAreas.forEach(el => { 1443 // modal, which remains closed, must remain inert 1444 if (!this.isClosedModal(el)) { 1445 Inert.setInertAttr(el, false); 1446 } 1447 }); 1448 this.inertedAreas = null; 1449 } 1450 1451 const hiddenAreasValue = this.getHiddenAreasJSON(modal); 1452 if (!hiddenAreasValue) return; 1453 1454 if (!hideAreas) return; 1455 1456 if (hideAreas) { 1457 this.inertedAreas = []; 1458 1459 let hiddenAreaSelectors; 1460 try { 1461 hiddenAreaSelectors = JSON.parse(hiddenAreasValue); 1462 } catch { 1463 return; 1464 } 1465 1466 hiddenAreaSelectors.forEach(hiddenAreaSelector => { 1467 document.querySelectorAll(hiddenAreaSelector).forEach(el => { 1468 Inert.setInertAttr(el); 1469 this.inertedAreas.push(el); 1470 }); 1471 }); 1472 } 1473 } 1474 1475 getHiddenAreasJSON(modal) { 1476 const hiddenAreasValue = this.isMobile(modal) 1477 ? this.options.hiddenAreasMobileDataAttribute 1478 : this.options.hiddenAreasDesktopDataAttribute; 1479 1480 return modal.getAttribute(hiddenAreasValue); 1481 } 1482 1483 /* --------------------------------------------------------------- 1484 * DIMMING-AREA HANDLING 1485 * ------------------------------------------------------------- */ 1486 1487 bindDimmingArea(modal) { 1488 this.unbindDimmingArea(); 1489 1490 const selector = modal.getAttribute(this.options.dimmingAreaDataAttribute); 1491 if (!selector) return; 1492 1493 const el = document.querySelector(selector); 1494 if (!el) { 1495 console.warn(`CsModalManager: dimming element "${selector}" not gefunden.`); 1496 return; 1497 } 1498 1499 const handler = () => this.close(false, el); 1500 el.addEventListener('click', handler); 1501 1502 el.classList.add(this.options.activeClass); 1503 1504 this.dimmingAreaEl = el; 1505 this.dimmingAreaEl.csModalDimmingHandler = handler; 1506 } 1507 1508 unbindDimmingArea() { 1509 if (!this.dimmingAreaEl) return; 1510 1511 this.dimmingAreaEl.removeEventListener( 1512 'click', 1513 this.dimmingAreaEl.csModalDimmingHandler 1514 ); 1515 delete this.dimmingAreaEl.csModalDimmingHandler; 1516 1517 this.dimmingAreaEl.classList.remove(this.options.activeClass); 1518 this.dimmingAreaEl = null; 1519 } 1520 1521 /* ------------------------------------------------------------------ 1522 * BREAKPOINT UTILS -------------------------------------------------- 1523 * ---------------------------------------------------------------- */ 1524 1525 isMobile(modal) { 1526 return Breakpoint.matches(modal, this.options.hiddenAreasBreakpointDataAttribute); 1527 } 1528 1529 isClosedModal(modal) { 1530 return modal.hasAttribute(this.options.modalDataAttribute) && 1531 Breakpoint.matches(modal, this.options.modalMaxBreakpointDataAttribute) && 1532 Breakpoint.matches(modal, this.options.modalMinBreakpointDataAttribute) && 1533 !modal.classList.contains(this.options.activeClass); 1534 } 1535 1536 /* ------------------------------------------------------------------ 1537 * EVENTS ------------------------------------------------------------- 1538 * ---------------------------------------------------------------- */ 1539 1540 dispatchModalEvent(type, modal, name, triggerLink = null, goToMode = null) { 1541 const evt = new CustomEvent(`cs-modal-${type}`, { 1542 detail: {name, modal, triggerLink, goToMode}, 1543 bubbles: true, 1544 composed: true 1545 }); 1546 1547 document.dispatchEvent(evt); 1548 } 1549 1550 /* ------------------------------------------------------------------ 1551 * HELPERS ----------------------------------------------------------- 1552 * ---------------------------------------------------------------- */ 1553 1554 findModalByName(name) { 1555 return this.el.querySelector(`[${this.options.modalDataAttribute}="${name}"]`); 1556 } 1557 1558 static getFirstTriggerLinkFromModalName(modalName) { 1559 return document.querySelectorAll(`[${CsModalManager.options.triggerDataAttribute}="${modalName}"]`)[0]; 1560 } 1561 1562 getAllModals() { 1563 return Array.from( 1564 document.querySelectorAll(`[${this.options.modalDataAttribute}]`) 1565 ); 1566 } 1567 1568 handleKeydown(e) { 1569 switch (e.key) { 1570 case 'Escape': 1571 if (this.activeModal) { 1572 e.preventDefault(); 1573 this.close(); 1574 } 1575 break; 1576 case ' ': 1577 case 'Spacebar': 1578 case 'ArrowDown': { 1579 const activeEl = document.activeElement; 1580 const isModalTrigger = activeEl?.hasAttribute(this.options.triggerDataAttribute); 1581 1582 if (isModalTrigger) { 1583 e.preventDefault(); 1584 this.dispatchModalEvent('keyevent-on-trigger-to-open-modal', null, null, document.activeElement); 1585 } 1586 break; 1587 } 1588 case 'ArrowLeft': 1589 case 'ArrowRight': 1590 if (this.activeModal) { 1591 e.preventDefault(); 1592 this.dispatchModalEvent('keyevent-on-trigger-to-close-modal', this.activeModal, this.activeModalName, document.activeElement, e.key === 'ArrowLeft' ? 'previous' : 'next'); 1593 } 1594 break; 1595 } 1596 } 1597
1598 handleBackgroundClick(e) { 1599 const contentWrapper = e.currentTarget.querySelector(this.options.modalContentWrapperSelector); 1600 if (!contentWrapper) return; 1601 1602 if (!contentWrapper.contains(e.target)) { 1603 this.close(true, e.target); 1604 } 1605 } 1606 1607 handleResize() { 1608 const modalsWithBreakpoints = Array.from( 1609 document.querySelectorAll(`[${CsModalManager.options.modalMaxBreakpointDataAttribute}],` + `[${CsModalManager.options.modalMinBreakpointDataAttribute}]`) 1610 ); 1611 1612 modalsWithBreakpoints.forEach(modal => { 1613 const inRange = 1614 Breakpoint.matches(modal, CsModalManager.options.modalMaxBreakpointDataAttribute) && 1615 Breakpoint.matches(modal, CsModalManager.options.modalMinBreakpointDataAttribute); 1616 1617 if (inRange) { 1618 if (modal !== this.activeModal) { 1619 CsModalManager.setModalAttributes(modal, false); 1620 } 1621 } else { 1622 if (this.activeModal === modal) { 1623 this.close(false); 1624 } 1625 CsModalManager.destroyModalAttributes(modal); 1626 } 1627 }); 1628 1629 if (this.activeModal) { 1630 this.setHiddenAreas(this.activeModal, true); 1631 } 1632 } 1633 1634 /* ------------------------------------------------------------------ 1635 * LIFECYCLE --------------------------------------------------------- 1636 * ---------------------------------------------------------------- */ 1637 1638 init() { 1639 this.body = document.body; 1640 this.handleKeydown = this.handleKeydown.bind(this); 1641 this.handleBackgroundClick = this.handleBackgroundClick.bind(this); 1642 this.handleResize = this.handleResize.bind(this); 1643 1644 this.activeModal = null; 1645 this.activeModalName = null; 1646 this.lastOpenTrigger = null; 1647 1648 this.setInitialModalAttributes(); 1649 this.bindTriggers(); 1650 this.bindCloseSelectors(); 1651 1652 document.addEventListener('keydown', this.handleKeydown); 1653 window.addEventListener('resize', this.handleResize); 1654 1655 this.openModalOnInit(); 1656 } 1657} 1658 1659const cfg = { 1660 selectors: { 1661 header: '.js-header', 1662 globalWrapper: '.js-global-wrapper', 1663 menuMain: '.js-menu-main', 1664 flyoutContainer: '.js-flyout-container', 1665 flyout: '.js-flyout', 1666 flyoutInner: '.js-flyout-inner', 1667 menuList: '.js-menu-main__list', 1668 menuListHeader: '.js-main-menu-mobile-header', 1669 backButton: '.js-menu-main-mobile-level-up', 1670 menuItem: '.js-menu-main__item', 1671 menuLink: '.js-menu-main__link', 1672 menuListOverview: '.js-menu-main__item--overview', 1673 menuLinkWithChildren: '.js-menu-main__link--has-children', 1674 menuLinkWithChildrenCss: '.menu-main__link--has-children' 1675 }, 1676 dataAttrs: { 1677 linkItemUid: 'data-menu-item-uid', 1678 menuLevel: 'data-menu-level', 1679 activeLinkUid: 'data-menu-active-uid' 1680 }, 1681 breakpoints: { 1682 mobileMenu: '--breakpoint-mobile-menu' 1683 }, 1684 classes: { 1685 active: 'active'}, 1686 levelPrefix: '--level-' 1687}; 1688 1689class MenuHelper { 1690 1691 /** 1692 * Returns all sibling elements of a given DOM element. 1693 * 1694 * @param {HTMLElement} el - The reference element. 1695 * @returns {HTMLElement[]} Array of sibling elements. 1696 */ 1697 static getSiblings(el) { 1698 return Array.from(el.parentElement.children).filter(child => child !== el); 1699 } 1700 1701 /** 1702 * Returns the subnavigation layer (e.g., submenu) for a given link element. 1703 * 1704 * @param {HTMLElement} link - The link element. 1705 * @returns {HTMLElement|null} The next sibling element (submenu), or `null` if none exists. 1706 */ 1707 static getSubNavigationLayerFromLink(link) { 1708 return link.nextElementSibling; 1709 } 1710 1711 /** 1712 * Returns the parent navigation layer of a given link. 1713 * 1714 * @param {HTMLElement} link - The link element inside a menu. 1715 * @param {boolean} [useFlyoutOuterContainer=false] - Whether to search for the flyout outer container, otherwise flyout inner container. 1716 * @param {number|null} [level=null] - Optional override for menu level, e.g. if link does not have the data attribute for level. 1717 * @returns {HTMLElement|null} The parent navigation container element. 1718 */ 1719 static getParentNavigationLayerFromLink(link, useFlyoutOuterContainer = false, level = null) { 1720 let parentNavigationLayer = null; 1721 let levelValue = level ? level : Number(link.getAttribute(cfg.dataAttrs.menuLevel)); 1722 1723 if (levelValue == 1) { 1724 parentNavigationLayer = link.closest(cfg.selectors.menuMain); 1725 } else if (levelValue == 2) { 1726 parentNavigationLayer = link.closest(useFlyoutOuterContainer ? cfg.selectors.flyoutContainer : cfg.selectors.flyoutInner); 1727 } else { 1728 parentNavigationLayer = link.closest(cfg.selectors.menuList); 1729 } 1730 return parentNavigationLayer; 1731 } 1732 1733 /** 1734 * Gets the trigger link (e.g., the menu item that opened a submenu) for a given submenu link. 1735 * 1736 * @param {HTMLElement} subLink - A link inside a submenu. 1737 * @returns {HTMLElement|null} The trigger link element. 1738 */ 1739 static getParentTriggerLink(subLink) { 1740 let parentNavigationLayer = this.getParentNavigationLayerFromLink(subLink, true); 1741 return parentNavigationLayer.previousElementSibling; 1742 } 1743 1744 /** 1745 * Resets all expanded ARIA states for submenus starting from a given level. 1746 * 1747 * @param {HTMLElement} flyoutContainer - The container element of the flyout. 1748 * @param {number} startingLevel - The starting menu level. 1749 */ 1750 static resetExpandedSubLevels(flyoutContainer, startingLevel) { 1751 let level = startingLevel; 1752 1753 while (true) { 1754 const selector = `${cfg.selectors.menuLinkWithChildren}[aria-expanded="true"][${cfg.dataAttrs.menuLevel}="${level}"]`; 1755 const expandedLinks = flyoutContainer.querySelectorAll(selector); 1756 1757 if (expandedLinks.length === 0) { 1758 break; 1759 } 1760 1761 expandedLinks.forEach(link => {
1762 const subNavigationLayer = this.getSubNavigationLayerFromLink(link); 1763 Aria.setExpanded(link, false); 1764 Inert.setInertAttr(subNavigationLayer); 1765 }); 1766 1767 level++; 1768 } 1769 } 1770 1771 /** 1772 * Returns the currently active menu link based on a data attribute in the global wrapper. 1773 * 1774 * @returns {HTMLElement|null} The active menu link element, or `null` if not found. 1775 */ 1776 static getActiveMenuLink() { 1777 const wrapper = document.querySelector(cfg.selectors.globalWrapper); 1778 if (!wrapper) return null; 1779 1780 const activeUid = wrapper.getAttribute(cfg.dataAttrs.activeLinkUid); 1781 if (!activeUid) return null; 1782 1783 return document.querySelector(`${cfg.selectors.menuLink}[${cfg.dataAttrs.linkItemUid}="${activeUid}"]`); 1784 } 1785 1786 /** 1787 * Toggles the `inert` attribute for elements in a mobile menu level to manage focus and accessibility. 1788 * 1789 * Affects the clicked link, its sibling menu items, and the associated list header. Additionally, 1790 * it inverts the `inert` state for the corresponding subnavigation layer. 1791 * 1792 * @param {HTMLElement} link - The menu link element that was interacted with. 1793 * @param {boolean} [activate=true] - Whether to activate (`true`) or deactivate (`false`) the `inert` state on surrounding elements. 1794 */ 1795 static toggleInertLevelMobile(link, activate = true) { 1796 const parentItem = link.closest(cfg.selectors.menuItem); 1797 const parentItemSiblings = MenuHelper.getSiblings(parentItem); 1798 const parentList = link.closest(cfg.selectors.menuList); 1799 const parentListHeader = parentList.previousElementSibling; 1800 1801 // add or remove inert for: 1802 // clicked link 1803 // list item siblings 1804 // list header 1805 const inertTargets = [ 1806 link, 1807 ...parentItemSiblings, 1808 ...(parentListHeader?.matches(cfg.selectors.menuListHeader) ? [parentListHeader] : []) 1809 ]; 1810 1811 Inert.setInertAttrOnElements(inertTargets, activate); 1812 1813 let subNavigationLayer = MenuHelper.getSubNavigationLayerFromLink(link); 1814 Inert.setInertAttr(subNavigationLayer, !activate); 1815 } 1816} 1817 1818class CsMainNavigation extends Core { 1819 1820 static options = { 1821 openActiveLevelOnMobile: true, 1822 maxVisibleMenuLevelOnDesktop: 3 1823 }; 1824 1825 // ============================== 1826 // Utility Methods 1827 // ============================== 1828 1829 isMobile() { 1830 return Breakpoint.isMobileByCssVar(cfg.breakpoints.mobileMenu); 1831 } 1832 1833 isDesktop() { 1834 return !this.isMobile(); 1835 } 1836 1837 resetOnBreakpointSwitch(isDesktop) { 1838 const nowIsDesktop = this.isDesktop(); 1839 1840 // fix to prevent mobile navigation flicker 1841 this.applyTransitionFix(); 1842 1843 if (nowIsDesktop && !isDesktop) { 1844 // switch from mobile to desktop 1845 this.resetMobileMenu(); 1846 this.updateAriaAttributesForMenuLevels(); 1847 return true; 1848 } 1849 1850 if (!nowIsDesktop && isDesktop) { 1851 // switch from desktop to mobile 1852 this.resetDesktopSubLevels(); 1853 // reset flyout height 1854 const flyoutContainer = this.el.querySelector(`${cfg.selectors.menuLink}[aria-expanded="true"] + ${cfg.selectors.flyoutContainer}`); 1855 if (flyoutContainer) { 1856 this.setFlyoutHeight(flyoutContainer); 1857 } 1858 this.updateAriaAttributesForMenuLevels(); 1859 return false; 1860 } 1861 1862 return isDesktop; 1863 } 1864 1865 applyTransitionFix() { 1866 this.el.classList.add(cfg.classes.stopTransition); 1867 window.clearTimeout(this.transitionTimer); 1868 1869 this.transitionTimer = window.setTimeout(() => { 1870 this.el.classList.remove(cfg.classes.stopTransition); 1871 this.transitionTimer = null; 1872 }, 100); 1873 } 1874 1875 // direction 1 => next, -1 => previous 1876 focusSiblingItem(currentLink, level, direction = 1) {
1877 let currentList = currentLink.closest('li')?.parentElement; 1878 1879 if (this.isDesktop() && currentLink.parentElement.matches(cfg.selectors.flyoutInner)) { 1880 // desktop menu, close flyout button 1881 currentList = currentLink.nextElementSibling; 1882 } else if (this.isMobile() && !currentList) { 1883 // mobile menu, first level close button 1884 currentList = currentLink.closest('nav').querySelector('ul'); 1885 } 1886 1887 const currentNavLayer = MenuHelper.getParentNavigationLayerFromLink(currentLink, false, level); 1888 1889 let target = Focus.getNextFocusableElement(null, direction > 0, currentLink, currentNavLayer); 1890 1891 if (!target) { 1892 if (this.isDesktop() && level === 2 && direction > 0) { 1893 target = currentList?.previousElementSibling; 1894 } else if (this.isDesktop() && level > 2 && direction > 0) { 1895 target = currentList.firstElementChild.nextElementSibling.querySelector('a, button'); 1896 } else if (this.isMobile() && level == 1 && direction > 0) { 1897 target = currentList.previousElementSibling.querySelector('a, button'); 1898 } else { 1899 const fallback = direction > 0 ? currentList?.firstElementChild : currentList?.lastElementChild; 1900 target = fallback?.querySelector('a, button'); 1901 } 1902 } 1903 1904 Focus.setFocusToElement(target); 1905 } 1906 1907 // ============================== 1908 // Navigation State Control 1909 // ============================== 1910 1911 setActiveLinkToActiveState() { 1912 const activeLink = MenuHelper.getActiveMenuLink(); 1913 if (!activeLink) return false; 1914 1915 activeLink.classList.add(cfg.classes.active); 1916 activeLink.setAttribute('aria-current', 'page'); 1917 1918 // climb up the DOM and mark parent links 1919 let parentLi = activeLink.closest('li'); 1920 while (parentLi) { 1921 const parentUl = parentLi.parentElement; 1922 if (parentUl && parentUl.closest('li')) { 1923 const parentLink = parentUl.closest('li').querySelector('a'); 1924 if (parentLink) { 1925 parentLink.classList.add(cfg.classes.active); 1926 } 1927 parentLi = parentUl.closest('li'); // weiter nach oben 1928 } else { 1929 parentLi = null; // oberste Ebene erreicht 1930 } 1931 } 1932 } 1933 1934 resetExpandedLinks(callbackPerLink) { 1935 const expandedLinks = this.el.querySelectorAll(`${cfg.selectors.menuLink}[aria-expanded="true"]`); 1936 expandedLinks.forEach(link => { 1937 if (typeof callbackPerLink === 'function') { 1938 callbackPerLink(link); 1939 } else { 1940 Aria.setExpanded(link, false); 1941 } 1942 }); 1943 } 1944 1945 resetMobileMenu() { 1946 this.resetExpandedLinks((link) => this.toggleMobileLevel(link, false)); 1947 } 1948 1949 resetDesktopSubLevels() { 1950 this.resetExpandedLinks(); 1951 } 1952 1953 setFlyoutHeight(flyoutContainer) { 1954 if (!flyoutContainer) return; 1955 1956 // reset flyout height 1957 const flyout = flyoutContainer.querySelector(cfg.selectors.flyout); 1958 flyoutContainer.removeAttribute('style'); 1959 flyout?.removeAttribute('style'); 1960 1961 if (!this.isDesktop()) return; 1962 1963 // set flyout and container height 1964 const header = document.querySelector(cfg.selectors.header); 1965 const headerHeight = header?.clientHeight ?? 0; 1966 1967 const flyoutContainerTargetHeight = window.innerHeight - headerHeight; 1968 const flyoutContentTargetHeight = flyoutContainer.scrollHeight; 1969 flyoutContainer.style.height = flyoutContainerTargetHeight + 'px'; 1970 if (flyoutContentTargetHeight > flyoutContainerTargetHeight) { 1971 flyout.style.height = flyoutContentTargetHeight + 'px'; 1972 } 1973 } 1974 1975 updateAriaAttributesForMenuLevels() { 1976 const linksWithChildren = this.el.querySelectorAll('[data-has-children="true"]'); 1977 linksWithChildren.forEach(link => { 1978 const level = Number(link.getAttribute(cfg.dataAttrs.menuLevel)); 1979 const isTooDeepOnDesktop = this.isDesktop() && level >= this.options.maxVisibleMenuLevelOnDesktop; 1980 1981 if (isTooDeepOnDesktop) { 1982 link.removeAttribute('aria-expanded'); 1983 link.removeAttribute('aria-controls'); 1984 link.classList.remove(cfg.selectors.menuLinkWithChildren.slice(1)); 1985 link.classList.remove(cfg.selectors.menuLinkWithChildrenCss.slice(1)); 1986 } else { 1987 link.setAttribute('aria-expanded', 'false'); 1988 1989 const uid = link.getAttribute('data-menu-item-uid'); 1990 if (uid) { 1991 link.setAttribute('aria-controls', `main-menu-list-${uid}`); 1992 } 1993 1994 link.classList.add(cfg.selectors.menuLinkWithChildrenCss.slice(1)); 1995 link.classList.add(cfg.selectors.menuLinkWithChildren.slice(1)); 1996 } 1997 }); 1998 1999 const flyoutContainer = this.el.querySelectorAll(cfg.selectors.flyoutContainer); 2000 const subLevelLists = this.el.querySelectorAll(`${cfg.selectors.menuList}:not([${cfg.dataAttrs.menuLevel}="1"]):not([${cfg.dataAttrs.menuLevel}="2"])`); 2001 2002 // add inert for: 2003 // all submenu layers 2004 const inertTargets = [ 2005 ...flyoutContainer, 2006 ...subLevelLists 2007 ]; 2008 2009 Inert.setInertAttrOnElements(inertTargets); 2010 } 2011 2012 markLinksWithChildrenOnce() { 2013 const links = this.el.querySelectorAll(cfg.selectors.menuLinkWithChildren); 2014 links.forEach(link => { 2015 link.setAttribute('data-has-children', 'true'); 2016 }); 2017 } 2018 2019 2020 // ============================== 2021 // Flyout & Menu Logic 2022 // ============================== 2023
2024 openMobileMenuOnActiveLevel() { 2025 if (!this.options.openActiveLevelOnMobile || this.isDesktop()) return; 2026 2027 if (this.openActiveLevel()) return; 2028 } 2029 2030 toggleMobileLevel(link, toBeOpened = true) { 2031 let navigationLayerToClose = null; 2032 let nextNavigationLayer = null; 2033 2034 if (toBeOpened) { 2035 nextNavigationLayer = MenuHelper.getSubNavigationLayerFromLink(link); 2036 navigationLayerToClose = MenuHelper.getParentNavigationLayerFromLink(link); 2037 } else { 2038 nextNavigationLayer = MenuHelper.getParentNavigationLayerFromLink(link); 2039 navigationLayerToClose = MenuHelper.getSubNavigationLayerFromLink(link); 2040 } 2041 2042 Scroll$1.toggleScrollBehavior(navigationLayerToClose, false); 2043 Scroll$1.toggleScrollBehavior(nextNavigationLayer, true); 2044 2045 const focusTarget = toBeOpened 2046 ? nextNavigationLayer?.querySelector(cfg.selectors.menuListOverview).nextElementSibling.querySelector(cfg.selectors.menuLink) 2047 : link; 2048 Focus.setFocusToElement(focusTarget); 2049 2050 MenuHelper.toggleInertLevelMobile(link, toBeOpened); 2051 2052 Aria.setExpanded(link, toBeOpened); 2053 } 2054 2055 toggleDesktopSubLevel(link, toBeOpened = true) { 2056 const activeFlyoutContainer = link.closest(cfg.selectors.flyoutContainer); 2057 if (!activeFlyoutContainer) return; 2058 2059 //check if another link is aria-expand=true in level 2 2060 let expandedMenuLink = activeFlyoutContainer.querySelector(`${cfg.selectors.menuLinkWithChildren}[aria-expanded="true"]`); 2061 if (expandedMenuLink) { 2062 const level = Number(link.getAttribute(cfg.dataAttrs.menuLevel)); 2063 MenuHelper.resetExpandedSubLevels(activeFlyoutContainer, level); 2064 } 2065 2066 Aria.setExpanded(link, toBeOpened); 2067 const subNavigationLayer = MenuHelper.getSubNavigationLayerFromLink(link); 2068 Inert.setInertAttr(subNavigationLayer, !toBeOpened); 2069 2070 if (toBeOpened) { 2071 const currentLevel = Number(link.getAttribute(cfg.dataAttrs.menuLevel)); 2072 const nextLevel = currentLevel + 1; 2073 const firstChildLink = link.nextElementSibling.querySelector( 2074 `${cfg.selectors.menuLink}[${cfg.dataAttrs.menuLevel}="${nextLevel}"]` 2075 ); 2076 if (firstChildLink) { 2077 Focus.setFocusToElement(firstChildLink); 2078 } 2079 } else { 2080 Focus.setFocusToElement(link); 2081 } 2082 2083 this.setFlyoutHeight(activeFlyoutContainer); 2084 } 2085 2086 openParentLevelsUpTo(activeLink, targetLevel) { 2087 for (let lvl = 1; lvl < targetLevel; lvl++) { 2088 const selector = `${cfg.selectors.menuItem}${cfg.levelPrefix}${lvl}`; 2089 const parentLink = activeLink.closest(selector)?.querySelector(cfg.selectors.menuLink); 2090 if (parentLink) { 2091 this.toggleMobileLevel(parentLink); 2092 } 2093 } 2094 } 2095 2096 openActiveLevel() { 2097 const activeLink = MenuHelper.getActiveMenuLink(); 2098 if (!activeLink) return false; 2099 2100 let levelToOpen = Number(activeLink.getAttribute(cfg.dataAttrs.menuLevel)); 2101 // Level 0 is the home screen of the mobile menu, so nothing to do if level < 1 2102 if (levelToOpen < 1) return false; 2103 2104 const hasChildren = activeLink.matches(cfg.selectors.menuLinkWithChildren); 2105 2106 const linkToOpen = hasChildren ? activeLink : activeLink.closest(`${cfg.selectors.menuItem}${cfg.levelPrefix}${levelToOpen - 1}`)?.querySelector(cfg.selectors.menuLink); 2107 2108 if (!linkToOpen) return false; 2109 2110 // open the parent levels too 2111 if (levelToOpen >= 2) { 2112 this.openParentLevelsUpTo(activeLink, levelToOpen); 2113 } 2114 2115 this.toggleMobileLevel(linkToOpen); 2116 return true; 2117 } 2118 2119 // ============================== 2120 // Event Handling 2121 // ============================== 2122 2123 handleClickOnMenuLink() { 2124 this.el.addEventListener('click', e => { 2125 const link = e.target.closest(cfg.selectors.menuLinkWithChildren); 2126 if (!link || !this.el.contains(link)) return; 2127 2128 const level = Number(link.getAttribute(cfg.dataAttrs.menuLevel)); 2129 const isDesktop = this.isDesktop(); 2130 const isDesktopLevel2 = level === 2; 2131 2132 if (isDesktop && isDesktopLevel2) { 2133 e.preventDefault(); 2134 this.toggleDesktopSubLevel(link); 2135 return; 2136 } 2137 2138 if (!isDesktop) { 2139 e.preventDefault(); 2140 this.toggleMobileLevel(link); 2141 return; 2142 } 2143 }); 2144 } 2145 2146 handleClickOnBackButton() { 2147 this.el.addEventListener('click', (e) => { 2148 const backButton = e.target.closest(cfg.selectors.backButton); 2149 if (!backButton) return; 2150 if (this.isDesktop()) return; 2151 2152 const parentTriggerLink = MenuHelper.getParentTriggerLink(backButton); 2153 this.toggleMobileLevel(parentTriggerLink, false); 2154 }); 2155 } 2156 2157 handleModalEvents() { 2158 const onModalOpen = (e) => { 2159 const {modal} = e.detail; 2160 2161 if (modal && modal.contains(this.el)) { 2162 this.openMobileMenuOnActiveLevel(); 2163 } 2164 if (modal && modal.matches(cfg.selectors.flyoutContainer)) { 2165 this.setFlyoutHeight(modal); 2166 } 2167 }; 2168 2169 const onModalClose = (e) => { 2170 const {modal} = e.detail; 2171 2172 if (modal && modal.contains(this.el)) { 2173 this.resetMobileMenu(); 2174 } 2175 if (modal && modal.matches(cfg.selectors.flyoutContainer)) { 2176 this.resetDesktopSubLevels(); 2177 } 2178 }; 2179 2180 const onKeyEventOnTriggerToOpenModal = (e) => { 2181 if (this.isMobile()) return true; 2182 const {triggerLink} = e.detail; 2183 2184 triggerLink.click(); 2185 }; 2186 2187 const onKeyEventOnTriggerToCloseModal = (e) => { 2188 if (this.isMobile()) return true; 2189 2190 const {goToMode} = e.detail; 2191 const {triggerLink} = e.detail; 2192 const level = Number(triggerLink.getAttribute(cfg.dataAttrs.menuLevel)); 2193 const linkHasChildren = triggerLink.matches(cfg.selectors.menuLinkWithChildren); 2194 let targetTriggerLink = null; 2195 2196 if (goToMode == 'next' && linkHasChildren || goToMode == 'next' && level == 1) return true; 2197 if (goToMode == 'previous' && level != 2) return true; 2198 2199 const parentItemLevel1 = triggerLink.closest(`${cfg.selectors.menuItem}${cfg.levelPrefix}1`); 2200 const parentLinkLevel1 = parentItemLevel1.querySelector(cfg.selectors.menuLink); 2201 2202 // close active modal 2203 parentLinkLevel1.click(); 2204 2205 if (goToMode == 'previous') { 2206 // focus the previous item 2207 targetTriggerLink = parentItemLevel1.previousElementSibling.querySelector(cfg.selectors.menuLink); 2208 } else if (goToMode == 'next') { 2209 // focus the next item 2210 targetTriggerLink = parentItemLevel1.nextElementSibling.querySelector(cfg.selectors.menuLink); 2211 } 2212 2213 if (!targetTriggerLink) { 2214 targetTriggerLink = parentLinkLevel1; 2215 } 2216 Focus.setFocusToElement(targetTriggerLink); 2217 }; 2218 2219 document.addEventListener('cs-modal-open', onModalOpen); 2220 document.addEventListener('cs-modal-close', onModalClose); 2221 document.addEventListener('cs-modal-keyevent-on-trigger-to-open-modal', onKeyEventOnTriggerToOpenModal); 2222 document.addEventListener('cs-modal-keyevent-on-trigger-to-close-modal', onKeyEventOnTriggerToCloseModal); 2223 } 2224 2225 handleResize() { 2226 let isDesktop = this.isDesktop(); 2227 2228 window.addEventListener('resize', () => { 2229 isDesktop = this.resetOnBreakpointSwitch(isDesktop); 2230 }); 2231 } 2232 2233 handleKeydown() { 2234 this.el.addEventListener('keydown', (e) => { 2235 const link = e.target.closest('a, button'); 2236 if (!link || !this.el.contains(link)) return; 2237 2238 const levelFallback = this.isMobile() ? 1 : 2; 2239 2240 let level = link.closest('ul') ? Number(link.closest('ul')?.getAttribute(cfg.dataAttrs.menuLevel)) : levelFallback; 2241 2242 // is desktop and event on flyout close button 2243 if (this.isDesktop() && link.parentElement.matches(cfg.selectors.flyoutInner)) { 2244 level = 2; 2245 } 2246 2247 const linkHasChildren = link.matches(cfg.selectors.menuLinkWithChildren); 2248 const isMobile = this.isMobile(); 2249 const isDesktop = !isMobile; 2250 2251 switch (e.key) { 2252 case ' ': 2253 case 'Spacebar': 2254 // Desktop Level 1 is handled in CsModalManager 2255 if (level >= 2 && isDesktop || isMobile) { 2256 e.preventDefault(); 2257 e.stopPropagation(); 2258 link.click(); 2259 } 2260 break; 2261 case 'ArrowRight': 2262 if (level === 1 && isDesktop) { 2263 e.preventDefault(); 2264 this.focusSiblingItem(link, level, 1); 2265 } else if (level === 2 && !linkHasChildren && isDesktop) { 2266 e.preventDefault(); 2267 // will be handled in CsModalManager 2268 } else if (level === 2 && linkHasChildren && isDesktop) { 2269 e.preventDefault(); 2270 this.toggleDesktopSubLevel(link); 2271 } else if (level > 2 && isDesktop) { 2272 e.preventDefault(); 2273 // will be handled in CsModalManager 2274 } else if (isMobile && linkHasChildren) { 2275 e.preventDefault(); 2276 this.toggleMobileLevel(link); 2277 } else if (isMobile && !linkHasChildren) { 2278 // do nothing 2279 e.preventDefault(); 2280 e.stopPropagation(); 2281 } 2282 break; 2283 case 'ArrowLeft': 2284 if (level === 1 && isDesktop) { 2285 e.preventDefault(); 2286 this.focusSiblingItem(link, level, -1); 2287 } else if (level === 3 && isDesktop) { 2288 e.preventDefault(); 2289 e.stopPropagation(); 2290 const triggerLink = link.closest(cfg.selectors.menuList).previousElementSibling; 2291 this.toggleDesktopSubLevel(triggerLink, false); 2292 } else if (level >= 2 && isMobile) { 2293 e.preventDefault(); 2294 e.stopPropagation(); 2295 const parentTriggerLink = MenuHelper.getParentTriggerLink(link); 2296 this.toggleMobileLevel(parentTriggerLink, false); 2297 } 2298 break; 2299 case 'ArrowDown': 2300 if (level == 1 && isDesktop) break; 2301 2302 e.preventDefault(); 2303 e.stopPropagation(); 2304 this.focusSiblingItem(link, level, 1); 2305 break; 2306 case 'ArrowUp': 2307 if (level == 1 && isDesktop) break; 2308 2309 e.preventDefault(); 2310 this.focusSiblingItem(link, level, -1); 2311 break; 2312 case 'Escape': 2313 if (level === 3 && isDesktop) { 2314 e.preventDefault(); 2315 e.stopPropagation(); 2316 const triggerLink = link.closest(cfg.selectors.menuList).previousElementSibling; 2317 this.toggleDesktopSubLevel(triggerLink, false); 2318 } else if (level >= 2 && isMobile) { 2319 e.preventDefault(); 2320 e.stopPropagation(); 2321 const parentTriggerLink = MenuHelper.getParentTriggerLink(link); 2322 this.toggleMobileLevel(parentTriggerLink, false); 2323 } 2324 break; 2325 } 2326 }); 2327 } 2328 2329 // ============================== 2330 // Init 2331 // ============================== 2332 2333 init() { 2334 this.setActiveLinkToActiveState(); 2335 2336 this.handleModalEvents(); 2337 2338 this.handleClickOnMenuLink(); 2339 this.handleClickOnBackButton(); 2340 2341 this.handleKeydown(); 2342 2343 this.handleResize(); 2344 2345 this.markLinksWithChildrenOnce(); 2346 this.updateAriaAttributesForMenuLevels(); 2347 } 2348} 2349 2350function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 2351 2352function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } 2353 2354/*! 2355 * Splide.js 2356 * Version : 4.1.4 2357 * License : MIT 2358 * Copyright: 2022 Naotoshi Fujita 2359 */ 2360var MEDIA_PREFERS_REDUCED_MOTION = "(prefers-reduced-motion: reduce)"; 2361var CREATED = 1; 2362var MOUNTED = 2; 2363var IDLE = 3; 2364var MOVING = 4; 2365var SCROLLING = 5; 2366var DRAGGING = 6; 2367var DESTROYED = 7; 2368var STATES = { 2369 CREATED: CREATED, 2370 MOUNTED: MOUNTED, 2371 IDLE: IDLE, 2372 MOVING: MOVING, 2373 SCROLLING: SCROLLING, 2374 DRAGGING: DRAGGING, 2375 DESTROYED: DESTROYED 2376}; 2377 2378function empty(array) { 2379 array.length = 0; 2380} 2381 2382function slice(arrayLike, start, end) { 2383 return Array.prototype.slice.call(arrayLike, start, end); 2384} 2385 2386function apply(func) { 2387 return func.bind.apply(func, [null].concat(slice(arguments, 1))); 2388} 2389 2390var nextTick = setTimeout; 2391 2392var noop = function noop() {}; 2393 2394function raf(func) { 2395 return requestAnimationFrame(func); 2396} 2397 2398function typeOf(type, subject) { 2399 return typeof subject === type; 2400} 2401 2402function isObject(subject) { 2403 return !isNull(subject) && typeOf("object", subject); 2404} 2405 2406var isArray = Array.isArray; 2407var isFunction = apply(typeOf, "function"); 2408var isString = apply(typeOf, "string"); 2409var isUndefined = apply(typeOf, "undefined"); 2410 2411function isNull(subject) { 2412 return subject === null; 2413} 2414 2415function isHTMLElement(subject) { 2416 try { 2417 return subject instanceof (subject.ownerDocument.defaultView || window).HTMLElement; 2418 } catch (e) { 2419 return false;
vendor: 4,687 bytes, lines 2420-2640
2420 } 2421} 2422 2423function toArray(value) { 2424 return isArray(value) ? value : [value]; 2425} 2426 2427function forEach(values, iteratee) { 2428 toArray(values).forEach(iteratee); 2429} 2430 2431function includes(array, value) { 2432 return array.indexOf(value) > -1; 2433} 2434 2435function push(array, items) { 2436 array.push.apply(array, toArray(items)); 2437 return array; 2438} 2439 2440function toggleClass(elm, classes, add) { 2441 if (elm) { 2442 forEach(classes, function (name) { 2443 if (name) { 2444 elm.classList[add ? "add" : "remove"](name); 2445 } 2446 }); 2447 } 2448} 2449 2450function addClass(elm, classes) { 2451 toggleClass(elm, isString(classes) ? classes.split(" ") : classes, true); 2452} 2453 2454function append(parent, children) { 2455 forEach(children, parent.appendChild.bind(parent)); 2456} 2457 2458function before(nodes, ref) { 2459 forEach(nodes, function (node) { 2460 var parent = (ref || node).parentNode; 2461 2462 if (parent) { 2463 parent.insertBefore(node, ref); 2464 } 2465 }); 2466} 2467 2468function matches(elm, selector) { 2469 return isHTMLElement(elm) && (elm["msMatchesSelector"] || elm.matches).call(elm, selector); 2470} 2471 2472function children(parent, selector) { 2473 var children2 = parent ? slice(parent.children) : []; 2474 return selector ? children2.filter(function (child) { 2475 return matches(child, selector); 2476 }) : children2; 2477} 2478 2479function child(parent, selector) { 2480 return selector ? children(parent, selector)[0] : parent.firstElementChild; 2481} 2482 2483var ownKeys = Object.keys; 2484 2485function forOwn(object, iteratee, right) { 2486 if (object) { 2487 (right ? ownKeys(object).reverse() : ownKeys(object)).forEach(function (key) { 2488 key !== "__proto__" && iteratee(object[key], key); 2489 }); 2490 } 2491 2492 return object; 2493} 2494 2495function assign$1(object) { 2496 slice(arguments, 1).forEach(function (source) { 2497 forOwn(source, function (value, key) { 2498 object[key] = source[key]; 2499 }); 2500 }); 2501 return object; 2502} 2503 2504function merge(object) { 2505 slice(arguments, 1).forEach(function (source) { 2506 forOwn(source, function (value, key) { 2507 if (isArray(value)) { 2508 object[key] = value.slice(); 2509 } else if (isObject(value)) { 2510 object[key] = merge({}, isObject(object[key]) ? object[key] : {}, value); 2511 } else { 2512 object[key] = value; 2513 } 2514 }); 2515 }); 2516 return object; 2517} 2518 2519function omit(object, keys) { 2520 forEach(keys || ownKeys(object), function (key) { 2521 delete object[key]; 2522 }); 2523} 2524 2525function removeAttribute(elms, attrs) { 2526 forEach(elms, function (elm) { 2527 forEach(attrs, function (attr) { 2528 elm && elm.removeAttribute(attr); 2529 }); 2530 }); 2531} 2532 2533function setAttribute(elms, attrs, value) { 2534 if (isObject(attrs)) { 2535 forOwn(attrs, function (value2, name) { 2536 setAttribute(elms, name, value2); 2537 }); 2538 } else { 2539 forEach(elms, function (elm) { 2540 isNull(value) || value === "" ? removeAttribute(elm, attrs) : elm.setAttribute(attrs, String(value)); 2541 }); 2542 } 2543} 2544 2545function create(tag, attrs, parent) { 2546 var elm = document.createElement(tag); 2547 2548 if (attrs) { 2549 isString(attrs) ? addClass(elm, attrs) : setAttribute(elm, attrs); 2550 } 2551 2552 parent && append(parent, elm); 2553 return elm; 2554} 2555 2556function style(elm, prop, value) { 2557 if (isUndefined(value)) { 2558 return getComputedStyle(elm)[prop]; 2559 } 2560 2561 if (!isNull(value)) { 2562 elm.style[prop] = "" + value; 2563 } 2564} 2565 2566function display(elm, display2) { 2567 style(elm, "display", display2); 2568} 2569 2570function focus(elm) { 2571 elm["setActive"] && elm["setActive"]() || elm.focus({ 2572 preventScroll: true 2573 }); 2574} 2575 2576function getAttribute(elm, attr) { 2577 return elm.getAttribute(attr); 2578} 2579 2580function hasClass(elm, className) { 2581 return elm && elm.classList.contains(className); 2582} 2583 2584function rect(target) { 2585 return target.getBoundingClientRect(); 2586} 2587 2588function remove(nodes) { 2589 forEach(nodes, function (node) { 2590 if (node && node.parentNode) { 2591 node.parentNode.removeChild(node); 2592 } 2593 }); 2594} 2595 2596function parseHtml(html) { 2597 return child(new DOMParser().parseFromString(html, "text/html").body); 2598} 2599 2600function prevent(e, stopPropagation) { 2601 e.preventDefault(); 2602 2603 if (stopPropagation) { 2604 e.stopPropagation(); 2605 e.stopImmediatePropagation(); 2606 } 2607} 2608 2609function query(parent, selector) { 2610 return parent && parent.querySelector(selector); 2611} 2612 2613function queryAll(parent, selector) { 2614 return selector ? slice(parent.querySelectorAll(selector)) : []; 2615} 2616 2617function removeClass(elm, classes) { 2618 toggleClass(elm, classes, false); 2619} 2620 2621function timeOf(e) { 2622 return e.timeStamp; 2623} 2624 2625function unit(value) { 2626 return isString(value) ? value : value ? value + "px" : ""; 2627} 2628 2629var PROJECT_CODE = "splide"; 2630var DATA_ATTRIBUTE = "data-" + PROJECT_CODE; 2631 2632function assert(condition, message) { 2633 if (!condition) { 2634 throw new Error("[" + PROJECT_CODE + "] " + (message || "")); 2635 } 2636} 2637 2638var min = Math.min, 2639 max = Math.max, 2640 floor = Math.floor,
vendor: 4,681 bytes, lines 2641-2816
2641 ceil = Math.ceil, 2642 abs = Math.abs; 2643 2644function approximatelyEqual(x, y, epsilon) { 2645 return abs(x - y) < epsilon; 2646} 2647 2648function between(number, x, y, exclusive) { 2649 var minimum = min(x, y); 2650 var maximum = max(x, y); 2651 return exclusive ? minimum < number && number < maximum : minimum <= number && number <= maximum; 2652} 2653 2654function clamp(number, x, y) { 2655 var minimum = min(x, y); 2656 var maximum = max(x, y); 2657 return min(max(minimum, number), maximum); 2658} 2659 2660function sign(x) { 2661 return +(x > 0) - +(x < 0); 2662} 2663 2664function format(string, replacements) { 2665 forEach(replacements, function (replacement) { 2666 string = string.replace("%s", "" + replacement); 2667 }); 2668 return string; 2669} 2670 2671function pad(number) { 2672 return number < 10 ? "0" + number : "" + number; 2673} 2674 2675var ids = {}; 2676 2677function uniqueId(prefix) { 2678 return "" + prefix + pad(ids[prefix] = (ids[prefix] || 0) + 1); 2679} 2680 2681function EventBinder() { 2682 var listeners = []; 2683 2684 function bind(targets, events, callback, options) { 2685 forEachEvent(targets, events, function (target, event, namespace) { 2686 var isEventTarget = ("addEventListener" in target); 2687 var remover = isEventTarget ? target.removeEventListener.bind(target, event, callback, options) : target["removeListener"].bind(target, callback); 2688 isEventTarget ? target.addEventListener(event, callback, options) : target["addListener"](callback); 2689 listeners.push([target, event, namespace, callback, remover]); 2690 }); 2691 } 2692 2693 function unbind(targets, events, callback) { 2694 forEachEvent(targets, events, function (target, event, namespace) { 2695 listeners = listeners.filter(function (listener) { 2696 if (listener[0] === target && listener[1] === event && listener[2] === namespace && (!callback || listener[3] === callback)) { 2697 listener[4](); 2698 return false; 2699 } 2700 2701 return true; 2702 }); 2703 }); 2704 } 2705 2706 function dispatch(target, type, detail) { 2707 var e; 2708 var bubbles = true; 2709 2710 if (typeof CustomEvent === "function") { 2711 e = new CustomEvent(type, { 2712 bubbles: bubbles, 2713 detail: detail 2714 }); 2715 } else { 2716 e = document.createEvent("CustomEvent"); 2717 e.initCustomEvent(type, bubbles, false, detail); 2718 } 2719 2720 target.dispatchEvent(e); 2721 return e; 2722 } 2723 2724 function forEachEvent(targets, events, iteratee) { 2725 forEach(targets, function (target) { 2726 target && forEach(events, function (events2) { 2727 events2.split(" ").forEach(function (eventNS) { 2728 var fragment = eventNS.split("."); 2729 iteratee(target, fragment[0], fragment[1]); 2730 }); 2731 }); 2732 }); 2733 } 2734 2735 function destroy() { 2736 listeners.forEach(function (data) { 2737 data[4](); 2738 }); 2739 empty(listeners); 2740 } 2741 2742 return { 2743 bind: bind, 2744 unbind: unbind, 2745 dispatch: dispatch, 2746 destroy: destroy 2747 }; 2748} 2749 2750var EVENT_MOUNTED = "mounted"; 2751var EVENT_READY = "ready"; 2752var EVENT_MOVE = "move"; 2753var EVENT_MOVED = "moved"; 2754var EVENT_CLICK = "click"; 2755var EVENT_ACTIVE = "active"; 2756var EVENT_INACTIVE = "inactive"; 2757var EVENT_VISIBLE = "visible"; 2758var EVENT_HIDDEN = "hidden"; 2759var EVENT_REFRESH = "refresh"; 2760var EVENT_UPDATED = "updated"; 2761var EVENT_RESIZE = "resize"; 2762var EVENT_RESIZED = "resized"; 2763var EVENT_DRAG = "drag"; 2764var EVENT_DRAGGING = "dragging"; 2765var EVENT_DRAGGED = "dragged"; 2766var EVENT_SCROLL = "scroll"; 2767var EVENT_SCROLLED = "scrolled"; 2768var EVENT_OVERFLOW = "overflow"; 2769var EVENT_DESTROY = "destroy"; 2770var EVENT_ARROWS_MOUNTED = "arrows:mounted"; 2771var EVENT_ARROWS_UPDATED = "arrows:updated"; 2772var EVENT_PAGINATION_MOUNTED = "pagination:mounted"; 2773var EVENT_PAGINATION_UPDATED = "pagination:updated"; 2774var EVENT_NAVIGATION_MOUNTED = "navigation:mounted"; 2775var EVENT_AUTOPLAY_PLAY = "autoplay:play"; 2776var EVENT_AUTOPLAY_PLAYING = "autoplay:playing"; 2777var EVENT_AUTOPLAY_PAUSE = "autoplay:pause"; 2778var EVENT_LAZYLOAD_LOADED = "lazyload:loaded"; 2779var EVENT_SLIDE_KEYDOWN = "sk"; 2780var EVENT_SHIFTED = "sh"; 2781var EVENT_END_INDEX_CHANGED = "ei"; 2782 2783function EventInterface(Splide2) { 2784 var bus = Splide2 ? Splide2.event.bus : document.createDocumentFragment(); 2785 var binder = EventBinder(); 2786 2787 function on(events, callback) { 2788 binder.bind(bus, toArray(events).join(" "), function (e) { 2789 callback.apply(callback, isArray(e.detail) ? e.detail : []); 2790 }); 2791 } 2792 2793 function emit(event) { 2794 binder.dispatch(bus, event, slice(arguments, 1)); 2795 } 2796 2797 if (Splide2) { 2798 Splide2.event.on(EVENT_DESTROY, binder.destroy); 2799 } 2800 2801 return assign$1(binder, { 2802 bus: bus, 2803 on: on, 2804 off: apply(binder.unbind, bus), 2805 emit: emit 2806 }); 2807} 2808 2809function RequestInterval(interval, onInterval, onUpdate, limit) { 2810 var now = Date.now; 2811 var startTime; 2812 var rate = 0; 2813 var id; 2814 var paused = true; 2815 var count = 0; 2816
vendor: 10,121 bytes, lines 2817-3196
2817 function update() { 2818 if (!paused) { 2819 rate = interval ? min((now() - startTime) / interval, 1) : 1; 2820 onUpdate && onUpdate(rate); 2821 2822 if (rate >= 1) { 2823 onInterval(); 2824 startTime = now(); 2825 2826 if (limit && ++count >= limit) { 2827 return pause(); 2828 } 2829 } 2830 2831 id = raf(update); 2832 } 2833 } 2834 2835 function start(resume) { 2836 resume || cancel(); 2837 startTime = now() - (resume ? rate * interval : 0); 2838 paused = false; 2839 id = raf(update); 2840 } 2841 2842 function pause() { 2843 paused = true; 2844 } 2845 2846 function rewind() { 2847 startTime = now(); 2848 rate = 0; 2849 2850 if (onUpdate) { 2851 onUpdate(rate); 2852 } 2853 } 2854 2855 function cancel() { 2856 id && cancelAnimationFrame(id); 2857 rate = 0; 2858 id = 0; 2859 paused = true; 2860 } 2861 2862 function set(time) { 2863 interval = time; 2864 } 2865 2866 function isPaused() { 2867 return paused; 2868 } 2869 2870 return { 2871 start: start, 2872 rewind: rewind, 2873 pause: pause, 2874 cancel: cancel, 2875 set: set, 2876 isPaused: isPaused 2877 }; 2878} 2879 2880function State(initialState) { 2881 var state = initialState; 2882 2883 function set(value) { 2884 state = value; 2885 } 2886 2887 function is(states) { 2888 return includes(toArray(states), state); 2889 } 2890 2891 return { 2892 set: set, 2893 is: is 2894 }; 2895} 2896 2897function Throttle(func, duration) { 2898 var interval = RequestInterval(0, func, null, 1); 2899 return function () { 2900 interval.isPaused() && interval.start(); 2901 }; 2902} 2903 2904function Media(Splide2, Components2, options) { 2905 var state = Splide2.state; 2906 var breakpoints = options.breakpoints || {}; 2907 var reducedMotion = options.reducedMotion || {}; 2908 var binder = EventBinder(); 2909 var queries = []; 2910 2911 function setup() { 2912 var isMin = options.mediaQuery === "min"; 2913 ownKeys(breakpoints).sort(function (n, m) { 2914 return isMin ? +n - +m : +m - +n; 2915 }).forEach(function (key) { 2916 register(breakpoints[key], "(" + (isMin ? "min" : "max") + "-width:" + key + "px)"); 2917 }); 2918 register(reducedMotion, MEDIA_PREFERS_REDUCED_MOTION); 2919 update(); 2920 } 2921 2922 function destroy(completely) { 2923 if (completely) { 2924 binder.destroy(); 2925 } 2926 } 2927 2928 function register(options2, query) { 2929 var queryList = matchMedia(query); 2930 binder.bind(queryList, "change", update); 2931 queries.push([options2, queryList]); 2932 } 2933 2934 function update() { 2935 var destroyed = state.is(DESTROYED); 2936 var direction = options.direction; 2937 var merged = queries.reduce(function (merged2, entry) { 2938 return merge(merged2, entry[1].matches ? entry[0] : {}); 2939 }, {}); 2940 omit(options); 2941 set(merged); 2942 2943 if (options.destroy) { 2944 Splide2.destroy(options.destroy === "completely"); 2945 } else if (destroyed) { 2946 destroy(true); 2947 Splide2.mount(); 2948 } else { 2949 direction !== options.direction && Splide2.refresh(); 2950 } 2951 } 2952 2953 function reduce(enable) { 2954 if (matchMedia(MEDIA_PREFERS_REDUCED_MOTION).matches) { 2955 enable ? merge(options, reducedMotion) : omit(options, ownKeys(reducedMotion)); 2956 } 2957 } 2958 2959 function set(opts, base, notify) { 2960 merge(options, opts); 2961 base && merge(Object.getPrototypeOf(options), opts); 2962 2963 if (notify || !state.is(CREATED)) { 2964 Splide2.emit(EVENT_UPDATED, options); 2965 } 2966 } 2967 2968 return { 2969 setup: setup, 2970 destroy: destroy, 2971 reduce: reduce, 2972 set: set 2973 }; 2974} 2975 2976var ARROW = "Arrow"; 2977var ARROW_LEFT = ARROW + "Left"; 2978var ARROW_RIGHT = ARROW + "Right"; 2979var ARROW_UP = ARROW + "Up"; 2980var ARROW_DOWN = ARROW + "Down"; 2981var RTL = "rtl"; 2982var TTB = "ttb"; 2983var ORIENTATION_MAP = { 2984 width: ["height"], 2985 left: ["top", "right"], 2986 right: ["bottom", "left"], 2987 x: ["y"], 2988 X: ["Y"], 2989 Y: ["X"], 2990 ArrowLeft: [ARROW_UP, ARROW_RIGHT], 2991 ArrowRight: [ARROW_DOWN, ARROW_LEFT] 2992}; 2993 2994function Direction(Splide2, Components2, options) { 2995 function resolve(prop, axisOnly, direction) { 2996 direction = direction || options.direction; 2997 var index = direction === RTL && !axisOnly ? 1 : direction === TTB ? 0 : -1; 2998 return ORIENTATION_MAP[prop] && ORIENTATION_MAP[prop][index] || prop.replace(/width|left|right/i, function (match, offset) { 2999 var replacement = ORIENTATION_MAP[match.toLowerCase()][index] || match; 3000 return offset > 0 ? replacement.charAt(0).toUpperCase() + replacement.slice(1) : replacement; 3001 }); 3002 } 3003 3004 function orient(value) { 3005 return value * (options.direction === RTL ? 1 : -1); 3006 } 3007 3008 return { 3009 resolve: resolve, 3010 orient: orient 3011 }; 3012} 3013 3014var ROLE = "role"; 3015var TAB_INDEX = "tabindex"; 3016var DISABLED = "disabled"; 3017var ARIA_PREFIX = "aria-"; 3018var ARIA_CONTROLS = ARIA_PREFIX + "controls"; 3019var ARIA_CURRENT = ARIA_PREFIX + "current"; 3020var ARIA_SELECTED = ARIA_PREFIX + "selected"; 3021var ARIA_LABEL = ARIA_PREFIX + "label"; 3022var ARIA_LABELLEDBY = ARIA_PREFIX + "labelledby"; 3023var ARIA_HIDDEN = ARIA_PREFIX + "hidden"; 3024var ARIA_ORIENTATION = ARIA_PREFIX + "orientation"; 3025var ARIA_ROLEDESCRIPTION = ARIA_PREFIX + "roledescription"; 3026var ARIA_LIVE = ARIA_PREFIX + "live"; 3027var ARIA_BUSY = ARIA_PREFIX + "busy"; 3028var ARIA_ATOMIC = ARIA_PREFIX + "atomic"; 3029var ALL_ATTRIBUTES = [ROLE, TAB_INDEX, DISABLED, ARIA_CONTROLS, ARIA_CURRENT, ARIA_LABEL, ARIA_LABELLEDBY, ARIA_HIDDEN, ARIA_ORIENTATION, ARIA_ROLEDESCRIPTION]; 3030var CLASS_PREFIX = PROJECT_CODE + "__"; 3031var STATUS_CLASS_PREFIX = "is-"; 3032var CLASS_ROOT = PROJECT_CODE; 3033var CLASS_TRACK = CLASS_PREFIX + "track"; 3034var CLASS_LIST = CLASS_PREFIX + "list"; 3035var CLASS_SLIDE = CLASS_PREFIX + "slide"; 3036var CLASS_CLONE = CLASS_SLIDE + "--clone"; 3037var CLASS_CONTAINER = CLASS_SLIDE + "__container"; 3038var CLASS_ARROWS = CLASS_PREFIX + "arrows"; 3039var CLASS_ARROW = CLASS_PREFIX + "arrow"; 3040var CLASS_ARROW_PREV = CLASS_ARROW + "--prev"; 3041var CLASS_ARROW_NEXT = CLASS_ARROW + "--next"; 3042var CLASS_PAGINATION = CLASS_PREFIX + "pagination"; 3043var CLASS_PAGINATION_PAGE = CLASS_PAGINATION + "__page"; 3044var CLASS_PROGRESS = CLASS_PREFIX + "progress"; 3045var CLASS_PROGRESS_BAR = CLASS_PROGRESS + "__bar"; 3046var CLASS_TOGGLE = CLASS_PREFIX + "toggle"; 3047var CLASS_SPINNER = CLASS_PREFIX + "spinner"; 3048var CLASS_SR = CLASS_PREFIX + "sr"; 3049var CLASS_INITIALIZED = STATUS_CLASS_PREFIX + "initialized"; 3050var CLASS_ACTIVE = STATUS_CLASS_PREFIX + "active"; 3051var CLASS_PREV = STATUS_CLASS_PREFIX + "prev"; 3052var CLASS_NEXT = STATUS_CLASS_PREFIX + "next"; 3053var CLASS_VISIBLE = STATUS_CLASS_PREFIX + "visible"; 3054var CLASS_LOADING = STATUS_CLASS_PREFIX + "loading"; 3055var CLASS_FOCUS_IN = STATUS_CLASS_PREFIX + "focus-in"; 3056var CLASS_OVERFLOW = STATUS_CLASS_PREFIX + "overflow"; 3057var STATUS_CLASSES = [CLASS_ACTIVE, CLASS_VISIBLE, CLASS_PREV, CLASS_NEXT, CLASS_LOADING, CLASS_FOCUS_IN, CLASS_OVERFLOW]; 3058var CLASSES = { 3059 slide: CLASS_SLIDE, 3060 clone: CLASS_CLONE, 3061 arrows: CLASS_ARROWS, 3062 arrow: CLASS_ARROW, 3063 prev: CLASS_ARROW_PREV, 3064 next: CLASS_ARROW_NEXT, 3065 pagination: CLASS_PAGINATION, 3066 page: CLASS_PAGINATION_PAGE, 3067 spinner: CLASS_SPINNER 3068}; 3069 3070function closest(from, selector) { 3071 if (isFunction(from.closest)) { 3072 return from.closest(selector); 3073 } 3074 3075 var elm = from; 3076 3077 while (elm && elm.nodeType === 1) { 3078 if (matches(elm, selector)) { 3079 break; 3080 } 3081 3082 elm = elm.parentElement; 3083 } 3084 3085 return elm; 3086} 3087 3088var FRICTION = 5; 3089var LOG_INTERVAL = 200; 3090var POINTER_DOWN_EVENTS = "touchstart mousedown"; 3091var POINTER_MOVE_EVENTS = "touchmove mousemove"; 3092var POINTER_UP_EVENTS = "touchend touchcancel mouseup click"; 3093 3094function Elements(Splide2, Components2, options) { 3095 var _EventInterface = EventInterface(Splide2), 3096 on = _EventInterface.on, 3097 bind = _EventInterface.bind; 3098 3099 var root = Splide2.root; 3100 var i18n = options.i18n; 3101 var elements = {}; 3102 var slides = []; 3103 var rootClasses = []; 3104 var trackClasses = []; 3105 var track; 3106 var list; 3107 var isUsingKey; 3108 3109 function setup() { 3110 collect(); 3111 init(); 3112 update(); 3113 } 3114 3115 function mount() { 3116 on(EVENT_REFRESH, destroy); 3117 on(EVENT_REFRESH, setup); 3118 on(EVENT_UPDATED, update); 3119 bind(document, POINTER_DOWN_EVENTS + " keydown", function (e) { 3120 isUsingKey = e.type === "keydown"; 3121 }, { 3122 capture: true 3123 }); 3124 bind(root, "focusin", function () { 3125 toggleClass(root, CLASS_FOCUS_IN, !!isUsingKey); 3126 }); 3127 } 3128 3129 function destroy(completely) { 3130 var attrs = ALL_ATTRIBUTES.concat("style"); 3131 empty(slides); 3132 removeClass(root, rootClasses); 3133 removeClass(track, trackClasses); 3134 removeAttribute([track, list], attrs); 3135 removeAttribute(root, completely ? attrs : ["style", ARIA_ROLEDESCRIPTION]); 3136 } 3137 3138 function update() { 3139 removeClass(root, rootClasses); 3140 removeClass(track, trackClasses); 3141 rootClasses = getClasses(CLASS_ROOT); 3142 trackClasses = getClasses(CLASS_TRACK); 3143 addClass(root, rootClasses); 3144 addClass(track, trackClasses); 3145 setAttribute(root, ARIA_LABEL, options.label); 3146 setAttribute(root, ARIA_LABELLEDBY, options.labelledby); 3147 } 3148 3149 function collect() { 3150 track = find("." + CLASS_TRACK); 3151 list = child(track, "." + CLASS_LIST); 3152 assert(track && list, "A track/list element is missing."); 3153 push(slides, children(list, "." + CLASS_SLIDE + ":not(." + CLASS_CLONE + ")")); 3154 forOwn({ 3155 arrows: CLASS_ARROWS, 3156 pagination: CLASS_PAGINATION, 3157 prev: CLASS_ARROW_PREV, 3158 next: CLASS_ARROW_NEXT, 3159 bar: CLASS_PROGRESS_BAR, 3160 toggle: CLASS_TOGGLE 3161 }, function (className, key) { 3162 elements[key] = find("." + className); 3163 }); 3164 assign$1(elements, { 3165 root: root, 3166 track: track, 3167 list: list, 3168 slides: slides 3169 }); 3170 } 3171 3172 function init() { 3173 var id = root.id || uniqueId(PROJECT_CODE); 3174 var role = options.role; 3175 root.id = id; 3176 track.id = track.id || id + "-track"; 3177 list.id = list.id || id + "-list"; 3178 3179 if (!getAttribute(root, ROLE) && root.tagName !== "SECTION" && role) { 3180 setAttribute(root, ROLE, role); 3181 } 3182 3183 setAttribute(root, ARIA_ROLEDESCRIPTION, i18n.carousel); 3184 setAttribute(list, ROLE, "presentation"); 3185 } 3186 3187 function find(selector) { 3188 var elm = query(root, selector); 3189 return elm && closest(elm, "." + CLASS_ROOT) === root ? elm : void 0; 3190 } 3191 3192 function getClasses(base) { 3193 return [base + "--" + options.type, base + "--" + options.direction, options.drag && base + "--draggable", options.isNavigation && base + "--nav", base === CLASS_ROOT && CLASS_ACTIVE]; 3194 } 3195 3196 return assign$1
vendor: 25,309 bytes, lines 3196-4154
3196(elements, { 3197 setup: setup, 3198 mount: mount, 3199 destroy: destroy 3200 }); 3201} 3202 3203var SLIDE = "slide"; 3204var LOOP = "loop"; 3205var FADE = "fade"; 3206 3207function Slide$1(Splide2, index, slideIndex, slide) { 3208 var event = EventInterface(Splide2); 3209 var on = event.on, 3210 emit = event.emit, 3211 bind = event.bind; 3212 var Components = Splide2.Components, 3213 root = Splide2.root, 3214 options = Splide2.options; 3215 var isNavigation = options.isNavigation, 3216 updateOnMove = options.updateOnMove, 3217 i18n = options.i18n, 3218 pagination = options.pagination, 3219 slideFocus = options.slideFocus; 3220 var resolve = Components.Direction.resolve; 3221 var styles = getAttribute(slide, "style"); 3222 var label = getAttribute(slide, ARIA_LABEL); 3223 var isClone = slideIndex > -1; 3224 var container = child(slide, "." + CLASS_CONTAINER); 3225 var destroyed; 3226 3227 function mount() { 3228 if (!isClone) { 3229 slide.id = root.id + "-slide" + pad(index + 1); 3230 setAttribute(slide, ROLE, pagination ? "tabpanel" : "group"); 3231 setAttribute(slide, ARIA_ROLEDESCRIPTION, i18n.slide); 3232 setAttribute(slide, ARIA_LABEL, label || format(i18n.slideLabel, [index + 1, Splide2.length])); 3233 } 3234 3235 listen(); 3236 } 3237 3238 function listen() { 3239 bind(slide, "click", apply(emit, EVENT_CLICK, self)); 3240 bind(slide, "keydown", apply(emit, EVENT_SLIDE_KEYDOWN, self)); 3241 on([EVENT_MOVED, EVENT_SHIFTED, EVENT_SCROLLED], update); 3242 on(EVENT_NAVIGATION_MOUNTED, initNavigation); 3243 3244 if (updateOnMove) { 3245 on(EVENT_MOVE, onMove); 3246 } 3247 } 3248 3249 function destroy() { 3250 destroyed = true; 3251 event.destroy(); 3252 removeClass(slide, STATUS_CLASSES); 3253 removeAttribute(slide, ALL_ATTRIBUTES); 3254 setAttribute(slide, "style", styles); 3255 setAttribute(slide, ARIA_LABEL, label || ""); 3256 } 3257 3258 function initNavigation() { 3259 var controls = Splide2.splides.map(function (target) { 3260 var Slide2 = target.splide.Components.Slides.getAt(index); 3261 return Slide2 ? Slide2.slide.id : ""; 3262 }).join(" "); 3263 setAttribute(slide, ARIA_LABEL, format(i18n.slideX, (isClone ? slideIndex : index) + 1)); 3264 setAttribute(slide, ARIA_CONTROLS, controls); 3265 setAttribute(slide, ROLE, slideFocus ? "button" : ""); 3266 slideFocus && removeAttribute(slide, ARIA_ROLEDESCRIPTION); 3267 } 3268 3269 function onMove() { 3270 if (!destroyed) { 3271 update(); 3272 } 3273 } 3274 3275 function update() { 3276 if (!destroyed) { 3277 var curr = Splide2.index; 3278 updateActivity(); 3279 updateVisibility(); 3280 toggleClass(slide, CLASS_PREV, index === curr - 1); 3281 toggleClass(slide, CLASS_NEXT, index === curr + 1); 3282 } 3283 } 3284 3285 function updateActivity() { 3286 var active = isActive(); 3287 3288 if (active !== hasClass(slide, CLASS_ACTIVE)) { 3289 toggleClass(slide, CLASS_ACTIVE, active); 3290 setAttribute(slide, ARIA_CURRENT, isNavigation && active || ""); 3291 emit(active ? EVENT_ACTIVE : EVENT_INACTIVE, self); 3292 } 3293 } 3294 3295 function updateVisibility() { 3296 var visible = isVisible(); 3297 var hidden = !visible && (!isActive() || isClone); 3298 3299 if (!Splide2.state.is([MOVING, SCROLLING])) { 3300 setAttribute(slide, ARIA_HIDDEN, hidden || ""); 3301 } 3302 3303 setAttribute(queryAll(slide, options.focusableNodes || ""), TAB_INDEX, hidden ? -1 : ""); 3304 3305 if (slideFocus) { 3306 setAttribute(slide, TAB_INDEX, hidden ? -1 : 0); 3307 } 3308 3309 if (visible !== hasClass(slide, CLASS_VISIBLE)) { 3310 toggleClass(slide, CLASS_VISIBLE, visible); 3311 emit(visible ? EVENT_VISIBLE : EVENT_HIDDEN, self); 3312 } 3313 3314 if (!visible && document.activeElement === slide) { 3315 var Slide2 = Components.Slides.getAt(Splide2.index); 3316 Slide2 && focus(Slide2.slide); 3317 } 3318 } 3319 3320 function style$1(prop, value, useContainer) { 3321 style(useContainer && container || slide, prop, value); 3322 } 3323 3324 function isActive() { 3325 var curr = Splide2.index; 3326 return curr === index || options.cloneStatus && curr === slideIndex; 3327 } 3328 3329 function isVisible() { 3330 if (Splide2.is(FADE)) { 3331 return isActive(); 3332 } 3333 3334 var trackRect = rect(Components.Elements.track); 3335 var slideRect = rect(slide); 3336 var left = resolve("left", true); 3337 var right = resolve("right", true); 3338 return floor(trackRect[left]) <= ceil(slideRect[left]) && floor(slideRect[right]) <= ceil(trackRect[right]); 3339 } 3340 3341 function isWithin(from, distance) { 3342 var diff = abs(from - index); 3343 3344 if (!isClone && (options.rewind || Splide2.is(LOOP))) { 3345 diff = min(diff, Splide2.length - diff); 3346 } 3347 3348 return diff <= distance; 3349 } 3350 3351 var self = { 3352 index: index, 3353 slideIndex: slideIndex, 3354 slide: slide, 3355 container: container, 3356 isClone: isClone, 3357 mount: mount, 3358 destroy: destroy, 3359 update: update, 3360 style: style$1, 3361 isWithin: isWithin 3362 }; 3363 return self; 3364} 3365 3366function Slides(Splide2, Components2, options) { 3367 var _EventInterface2 = EventInterface(Splide2), 3368 on = _EventInterface2.on, 3369 emit = _EventInterface2.emit, 3370 bind = _EventInterface2.bind; 3371 3372 var _Components2$Elements = Components2.Elements, 3373 slides = _Components2$Elements.slides, 3374 list = _Components2$Elements.list; 3375 var Slides2 = []; 3376 3377 function mount() { 3378 init(); 3379 on(EVENT_REFRESH, destroy); 3380 on(EVENT_REFRESH, init); 3381 } 3382 3383 function init() { 3384 slides.forEach(function (slide, index) { 3385 register(slide, index, -1); 3386 }); 3387 } 3388 3389 function destroy() { 3390 forEach$1(function (Slide2) { 3391 Slide2.destroy(); 3392 }); 3393 empty(Slides2); 3394 } 3395 3396 function update() { 3397 forEach$1(function (Slide2) { 3398 Slide2.update(); 3399 }); 3400 } 3401 3402 function register(slide, index, slideIndex) { 3403 var object = Slide$1(Splide2, index, slideIndex, slide); 3404 object.mount(); 3405 Slides2.push(object); 3406 Slides2.sort(function (Slide1, Slide2) { 3407 return Slide1.index - Slide2.index; 3408 }); 3409 } 3410 3411 function get(excludeClones) { 3412 return excludeClones ? filter(function (Slide2) { 3413 return !Slide2.isClone; 3414 }) : Slides2; 3415 } 3416 3417 function getIn(page) { 3418 var Controller = Components2.Controller; 3419 var index = Controller.toIndex(page); 3420 var max = Controller.hasFocus() ? 1 : options.perPage; 3421 return filter(function (Slide2) { 3422 return between(Slide2.index, index, index + max - 1); 3423 }); 3424 } 3425 3426 function getAt(index) { 3427 return filter(index)[0]; 3428 } 3429 3430 function add(items, index) { 3431 forEach(items, function (slide) { 3432 if (isString(slide)) { 3433 slide = parseHtml(slide); 3434 } 3435 3436 if (isHTMLElement(slide)) { 3437 var ref = slides[index]; 3438 ref ? before(slide, ref) : append(list, slide); 3439 addClass(slide, options.classes.slide); 3440 observeImages(slide, apply(emit, EVENT_RESIZE)); 3441 } 3442 }); 3443 emit(EVENT_REFRESH); 3444 } 3445 3446 function remove$1(matcher) { 3447 remove(filter(matcher).map(function (Slide2) { 3448 return Slide2.slide; 3449 })); 3450 emit(EVENT_REFRESH); 3451 } 3452 3453 function forEach$1(iteratee, excludeClones) { 3454 get(excludeClones).forEach(iteratee); 3455 } 3456 3457 function filter(matcher) { 3458 return Slides2.filter(isFunction(matcher) ? matcher : function (Slide2) { 3459 return isString(matcher) ? matches(Slide2.slide, matcher) : includes(toArray(matcher), Slide2.index); 3460 }); 3461 } 3462 3463 function style(prop, value, useContainer) { 3464 forEach$1(function (Slide2) { 3465 Slide2.style(prop, value, useContainer); 3466 }); 3467 } 3468 3469 function observeImages(elm, callback) { 3470 var images = queryAll(elm, "img"); 3471 var length = images.length; 3472 3473 if (length) { 3474 images.forEach(function (img) { 3475 bind(img, "load error", function () { 3476 if (! --length) { 3477 callback(); 3478 } 3479 }); 3480 }); 3481 } else { 3482 callback(); 3483 } 3484 } 3485 3486 function getLength(excludeClones) { 3487 return excludeClones ? slides.length : Slides2.length; 3488 } 3489 3490 function isEnough() { 3491 return Slides2.length > options.perPage; 3492 } 3493 3494 return { 3495 mount: mount, 3496 destroy: destroy, 3497 update: update, 3498 register: register, 3499 get: get, 3500 getIn: getIn, 3501 getAt: getAt, 3502 add: add, 3503 remove: remove$1, 3504 forEach: forEach$1, 3505 filter: filter, 3506 style: style, 3507 getLength: getLength, 3508 isEnough: isEnough 3509 }; 3510} 3511 3512function Layout(Splide2, Components2, options) { 3513 var _EventInterface3 = EventInterface(Splide2), 3514 on = _EventInterface3.on, 3515 bind = _EventInterface3.bind, 3516 emit = _EventInterface3.emit; 3517 3518 var Slides = Components2.Slides; 3519 var resolve = Components2.Direction.resolve; 3520 var _Components2$Elements2 = Components2.Elements, 3521 root = _Components2$Elements2.root, 3522 track = _Components2$Elements2.track, 3523 list = _Components2$Elements2.list; 3524 var getAt = Slides.getAt, 3525 styleSlides = Slides.style; 3526 var vertical; 3527 var rootRect; 3528 var overflow; 3529 3530 function mount() { 3531 init(); 3532 bind(window, "resize load", Throttle(apply(emit, EVENT_RESIZE))); 3533 on([EVENT_UPDATED, EVENT_REFRESH], init); 3534 on(EVENT_RESIZE, resize); 3535 } 3536 3537 function init() { 3538 vertical = options.direction === TTB; 3539 style(root, "maxWidth", unit(options.width)); 3540 style(track, resolve("paddingLeft"), cssPadding(false)); 3541 style(track, resolve("paddingRight"), cssPadding(true)); 3542 resize(true); 3543 } 3544 3545 function resize(force) { 3546 var newRect = rect(root); 3547 3548 if (force || rootRect.width !== newRect.width || rootRect.height !== newRect.height) { 3549 style(track, "height", cssTrackHeight()); 3550 styleSlides(resolve("marginRight"), unit(options.gap)); 3551 styleSlides("width", cssSlideWidth()); 3552 styleSlides("height", cssSlideHeight(), true); 3553 rootRect = newRect; 3554 emit(EVENT_RESIZED); 3555 3556 if (overflow !== (overflow = isOverflow())) { 3557 toggleClass(root, CLASS_OVERFLOW, overflow); 3558 emit(EVENT_OVERFLOW, overflow); 3559 } 3560 } 3561 } 3562 3563 function cssPadding(right) { 3564 var padding = options.padding; 3565 var prop = resolve(right ? "right" : "left"); 3566 return padding && unit(padding[prop] || (isObject(padding) ? 0 : padding)) || "0px"; 3567 } 3568 3569 function cssTrackHeight() { 3570 var height = ""; 3571 3572 if (vertical) { 3573 height = cssHeight(); 3574 assert(height, "height or heightRatio is missing."); 3575 height = "calc(" + height + " - " + cssPadding(false) + " - " + cssPadding(true) + ")"; 3576 } 3577 3578 return height; 3579 } 3580 3581 function cssHeight() { 3582 return unit(options.height || rect(list).width * options.heightRatio); 3583 } 3584 3585 function cssSlideWidth() { 3586 return options.autoWidth ? null : unit(options.fixedWidth) || (vertical ? "" : cssSlideSize()); 3587 } 3588 3589 function cssSlideHeight() { 3590 return unit(options.fixedHeight) || (vertical ? options.autoHeight ? null : cssSlideSize() : cssHeight()); 3591 } 3592 3593 function cssSlideSize() { 3594 var gap = unit(options.gap); 3595 return "calc((100%" + (gap && " + " + gap) + ")/" + (options.perPage || 1) + (gap && " - " + gap) + ")"; 3596 } 3597 3598 function listSize() { 3599 return rect(list)[resolve("width")]; 3600 } 3601 3602 function slideSize(index, withoutGap) { 3603 var Slide = getAt(index || 0); 3604 return Slide ? rect(Slide.slide)[resolve("width")] + (withoutGap ? 0 : getGap()) : 0; 3605 } 3606 3607 function totalSize(index, withoutGap) { 3608 var Slide = getAt(index); 3609 3610 if (Slide) { 3611 var right = rect(Slide.slide)[resolve("right")]; 3612 var left = rect(list)[resolve("left")]; 3613 return abs(right - left) + (withoutGap ? 0 : getGap()); 3614 } 3615 3616 return 0; 3617 } 3618 3619 function sliderSize(withoutGap) { 3620 return totalSize(Splide2.length - 1) - totalSize(0) + slideSize(0, withoutGap); 3621 } 3622 3623 function getGap() { 3624 var Slide = getAt(0); 3625 return Slide && parseFloat(style(Slide.slide, resolve("marginRight"))) || 0; 3626 } 3627 3628 function getPadding(right) { 3629 return parseFloat(style(track, resolve("padding" + (right ? "Right" : "Left")))) || 0; 3630 } 3631 3632 function isOverflow() { 3633 return Splide2.is(FADE) || sliderSize(true) > listSize(); 3634 } 3635 3636 return { 3637 mount: mount, 3638 resize: resize, 3639 listSize: listSize, 3640 slideSize: slideSize, 3641 sliderSize: sliderSize, 3642 totalSize: totalSize, 3643 getPadding: getPadding, 3644 isOverflow: isOverflow 3645 }; 3646} 3647 3648var MULTIPLIER = 2; 3649 3650function Clones(Splide2, Components2, options) { 3651 var event = EventInterface(Splide2); 3652 var on = event.on; 3653 var Elements = Components2.Elements, 3654 Slides = Components2.Slides; 3655 var resolve = Components2.Direction.resolve; 3656 var clones = []; 3657 var cloneCount; 3658 3659 function mount() { 3660 on(EVENT_REFRESH, remount); 3661 on([EVENT_UPDATED, EVENT_RESIZE], observe); 3662 3663 if (cloneCount = computeCloneCount()) { 3664 generate(cloneCount); 3665 Components2.Layout.resize(true); 3666 } 3667 } 3668 3669 function remount() { 3670 destroy(); 3671 mount(); 3672 } 3673 3674 function destroy() { 3675 remove(clones); 3676 empty(clones); 3677 event.destroy(); 3678 } 3679 3680 function observe() { 3681 var count = computeCloneCount(); 3682 3683 if (cloneCount !== count) { 3684 if (cloneCount < count || !count) { 3685 event.emit(EVENT_REFRESH); 3686 } 3687 } 3688 } 3689 3690 function generate(count) { 3691 var slides = Slides.get().slice(); 3692 var length = slides.length; 3693 3694 if (length) { 3695 while (slides.length < count) { 3696 push(slides, slides); 3697 } 3698 3699 push(slides.slice(-count), slides.slice(0, count)).forEach(function (Slide, index) { 3700 var isHead = index < count; 3701 var clone = cloneDeep(Slide.slide, index); 3702 isHead ? before(clone, slides[0].slide) : append(Elements.list, clone); 3703 push(clones, clone); 3704 Slides.register(clone, index - count + (isHead ? 0 : length), Slide.index); 3705 }); 3706 } 3707 } 3708 3709 function cloneDeep(elm, index) { 3710 var clone = elm.cloneNode(true); 3711 addClass(clone, options.classes.clone); 3712 clone.id = Splide2.root.id + "-clone" + pad(index + 1); 3713 return clone; 3714 } 3715 3716 function computeCloneCount() { 3717 var clones2 = options.clones; 3718 3719 if (!Splide2.is(LOOP)) { 3720 clones2 = 0; 3721 } else if (isUndefined(clones2)) { 3722 var fixedSize = options[resolve("fixedWidth")] && Components2.Layout.slideSize(0); 3723 var fixedCount = fixedSize && ceil(rect(Elements.track)[resolve("width")] / fixedSize); 3724 clones2 = fixedCount || options[resolve("autoWidth")] && Splide2.length || options.perPage * MULTIPLIER; 3725 } 3726 3727 return clones2; 3728 } 3729 3730 return { 3731 mount: mount, 3732 destroy: destroy 3733 }; 3734} 3735 3736function Move(Splide2, Components2, options) { 3737 var _EventInterface4 = EventInterface(Splide2), 3738 on = _EventInterface4.on, 3739 emit = _EventInterface4.emit; 3740 3741 var set = Splide2.state.set; 3742 var _Components2$Layout = Components2.Layout, 3743 slideSize = _Components2$Layout.slideSize, 3744 getPadding = _Components2$Layout.getPadding, 3745 totalSize = _Components2$Layout.totalSize, 3746 listSize = _Components2$Layout.listSize, 3747 sliderSize = _Components2$Layout.sliderSize; 3748 var _Components2$Directio = Components2.Direction, 3749 resolve = _Components2$Directio.resolve, 3750 orient = _Components2$Directio.orient; 3751 var _Components2$Elements3 = Components2.Elements, 3752 list = _Components2$Elements3.list, 3753 track = _Components2$Elements3.track; 3754 var Transition; 3755 3756 function mount() { 3757 Transition = Components2.Transition; 3758 on([EVENT_MOUNTED, EVENT_RESIZED, EVENT_UPDATED, EVENT_REFRESH], reposition); 3759 } 3760 3761 function reposition() { 3762 if (!Components2.Controller.isBusy()) { 3763 Components2.Scroll.cancel(); 3764 jump(Splide2.index); 3765 Components2.Slides.update(); 3766 } 3767 } 3768 3769 function move(dest, index, prev, callback) { 3770 if (dest !== index && canShift(dest > prev)) { 3771 cancel(); 3772 translate(shift(getPosition(), dest > prev), true); 3773 } 3774 3775 set(MOVING); 3776 emit(EVENT_MOVE, index, prev, dest); 3777 Transition.start(index, function () { 3778 set(IDLE); 3779 emit(EVENT_MOVED, index, prev, dest); 3780 callback && callback(); 3781 }); 3782 } 3783 3784 function jump(index) { 3785 translate(toPosition(index, true)); 3786 } 3787 3788 function translate(position, preventLoop) { 3789 if (!Splide2.is(FADE)) { 3790 var destination = preventLoop ? position : loop(position); 3791 style(list, "transform", "translate" + resolve("X") + "(" + destination + "px)"); 3792 position !== destination && emit(EVENT_SHIFTED); 3793 } 3794 } 3795 3796 function loop(position) { 3797 if (Splide2.is(LOOP)) { 3798 var index = toIndex(position); 3799 var exceededMax = index > Components2.Controller.getEnd(); 3800 var exceededMin = index < 0; 3801 3802 if (exceededMin || exceededMax) { 3803 position = shift(position, exceededMax); 3804 } 3805 } 3806 3807 return position; 3808 } 3809 3810 function shift(position, backwards) { 3811 var excess = position - getLimit(backwards); 3812 var size = sliderSize(); 3813 position -= orient(size * (ceil(abs(excess) / size) || 1)) * (backwards ? 1 : -1); 3814 return position; 3815 } 3816 3817 function cancel() { 3818 translate(getPosition(), true); 3819 Transition.cancel(); 3820 } 3821 3822 function toIndex(position) { 3823 var Slides = Components2.Slides.get(); 3824 var index = 0; 3825 var minDistance = Infinity; 3826 3827 for (var i = 0; i < Slides.length; i++) { 3828 var slideIndex = Slides[i].index; 3829 var distance = abs(toPosition(slideIndex, true) - position); 3830 3831 if (distance <= minDistance) { 3832 minDistance = distance; 3833 index = slideIndex; 3834 } else { 3835 break; 3836 } 3837 } 3838 3839 return index; 3840 } 3841 3842 function toPosition(index, trimming) { 3843 var position = orient(totalSize(index - 1) - offset(index)); 3844 return trimming ? trim(position) : position; 3845 } 3846 3847 function getPosition() { 3848 var left = resolve("left"); 3849 return rect(list)[left] - rect(track)[left] + orient(getPadding(false)); 3850 } 3851 3852 function trim(position) { 3853 if (options.trimSpace && Splide2.is(SLIDE)) { 3854 position = clamp(position, 0, orient(sliderSize(true) - listSize())); 3855 } 3856 3857 return position; 3858 } 3859 3860 function offset(index) { 3861 var focus = options.focus; 3862 return focus === "center" ? (listSize() - slideSize(index, true)) / 2 : +focus * slideSize(index) || 0; 3863 } 3864 3865 function getLimit(max) { 3866 return toPosition(max ? Components2.Controller.getEnd() : 0, !!options.trimSpace); 3867 } 3868 3869 function canShift(backwards) { 3870 var shifted = orient(shift(getPosition(), backwards)); 3871 return backwards ? shifted >= 0 : shifted <= list[resolve("scrollWidth")] - rect(track)[resolve("width")]; 3872 } 3873 3874 function exceededLimit(max, position) { 3875 position = isUndefined(position) ? getPosition() : position; 3876 var exceededMin = max !== true && orient(position) < orient(getLimit(false)); 3877 var exceededMax = max !== false && orient(position) > orient(getLimit(true)); 3878 return exceededMin || exceededMax; 3879 } 3880 3881 return { 3882 mount: mount, 3883 move: move, 3884 jump: jump, 3885 translate: translate, 3886 shift: shift, 3887 cancel: cancel, 3888 toIndex: toIndex, 3889 toPosition: toPosition, 3890 getPosition: getPosition, 3891 getLimit: getLimit, 3892 exceededLimit: exceededLimit, 3893 reposition: reposition 3894 }; 3895} 3896 3897function Controller(Splide2, Components2, options) { 3898 var _EventInterface5 = EventInterface(Splide2), 3899 on = _EventInterface5.on, 3900 emit = _EventInterface5.emit; 3901 3902 var Move = Components2.Move; 3903 var getPosition = Move.getPosition, 3904 getLimit = Move.getLimit, 3905 toPosition = Move.toPosition; 3906 var _Components2$Slides = Components2.Slides, 3907 isEnough = _Components2$Slides.isEnough, 3908 getLength = _Components2$Slides.getLength; 3909 var omitEnd = options.omitEnd; 3910 var isLoop = Splide2.is(LOOP); 3911 var isSlide = Splide2.is(SLIDE); 3912 var getNext = apply(getAdjacent, false); 3913 var getPrev = apply(getAdjacent, true); 3914 var currIndex = options.start || 0; 3915 var endIndex; 3916 var prevIndex = currIndex; 3917 var slideCount; 3918 var perMove; 3919 var perPage; 3920 3921 function mount() { 3922 init(); 3923 on([EVENT_UPDATED, EVENT_REFRESH, EVENT_END_INDEX_CHANGED], init); 3924 on(EVENT_RESIZED, onResized); 3925 } 3926 3927 function init() { 3928 slideCount = getLength(true); 3929 perMove = options.perMove; 3930 perPage = options.perPage; 3931 endIndex = getEnd(); 3932 var index = clamp(currIndex, 0, omitEnd ? endIndex : slideCount - 1); 3933 3934 if (index !== currIndex) { 3935 currIndex = index; 3936 Move.reposition(); 3937 } 3938 } 3939 3940 function onResized() { 3941 if (endIndex !== getEnd()) { 3942 emit(EVENT_END_INDEX_CHANGED); 3943 } 3944 } 3945 3946 function go(control, allowSameIndex, callback) { 3947 if (!isBusy()) { 3948 var dest = parse(control); 3949 var index = loop(dest); 3950 3951 if (index > -1 && (allowSameIndex || index !== currIndex)) { 3952 setIndex(index); 3953 Move.move(dest, index, prevIndex, callback); 3954 } 3955 } 3956 } 3957 3958 function scroll(destination, duration, snap, callback) { 3959 Components2.Scroll.scroll(destination, duration, snap, function () { 3960 var index = loop(Move.toIndex(getPosition())); 3961 setIndex(omitEnd ? min(index, endIndex) : index); 3962 callback && callback(); 3963 }); 3964 } 3965 3966 function parse(control) { 3967 var index = currIndex; 3968 3969 if (isString(control)) { 3970 var _ref = control.match(/([+\-<>])(\d+)?/) || [], 3971 indicator = _ref[1], 3972 number = _ref[2]; 3973 3974 if (indicator === "+" || indicator === "-") { 3975 index = computeDestIndex(currIndex + +("" + indicator + (+number || 1)), currIndex); 3976 } else if (indicator === ">") { 3977 index = number ? toIndex(+number) : getNext(true); 3978 } else if (indicator === "<") { 3979 index = getPrev(true); 3980 } 3981 } else { 3982 index = isLoop ? control : clamp(control, 0, endIndex); 3983 } 3984 3985 return index; 3986 } 3987 3988 function getAdjacent(prev, destination) { 3989 var number = perMove || (hasFocus() ? 1 : perPage); 3990 var dest = computeDestIndex(currIndex + number * (prev ? -1 : 1), currIndex, !(perMove || hasFocus())); 3991 3992 if (dest === -1 && isSlide) { 3993 if (!approximatelyEqual(getPosition(), getLimit(!prev), 1)) { 3994 return prev ? 0 : endIndex; 3995 } 3996 } 3997 3998 return destination ? dest : loop(dest); 3999 } 4000 4001 function computeDestIndex(dest, from, snapPage) { 4002 if (isEnough() || hasFocus()) { 4003 var index = computeMovableDestIndex(dest); 4004 4005 if (index !== dest) { 4006 from = dest; 4007 dest = index; 4008 snapPage = false; 4009 } 4010 4011 if (dest < 0 || dest > endIndex) { 4012 if (!perMove && (between(0, dest, from, true) || between(endIndex, from, dest, true))) { 4013 dest = toIndex(toPage(dest)); 4014 } else { 4015 if (isLoop) { 4016 dest = snapPage ? dest < 0 ? -(slideCount % perPage || perPage) : slideCount : dest; 4017 } else if (options.rewind) { 4018 dest = dest < 0 ? endIndex : 0; 4019 } else { 4020 dest = -1; 4021 } 4022 } 4023 } else { 4024 if (snapPage && dest !== from) { 4025 dest = toIndex(toPage(from) + (dest < from ? -1 : 1)); 4026 } 4027 } 4028 } else { 4029 dest = -1; 4030 } 4031 4032 return dest; 4033 } 4034 4035 function computeMovableDestIndex(dest) { 4036 if (isSlide && options.trimSpace === "move" && dest !== currIndex) { 4037 var position = getPosition(); 4038 4039 while (position === toPosition(dest, true) && between(dest, 0, Splide2.length - 1, !options.rewind)) { 4040 dest < currIndex ? --dest : ++dest; 4041 } 4042 } 4043 4044 return dest; 4045 } 4046 4047 function loop(index) { 4048 return isLoop ? (index + slideCount) % slideCount || 0 : index; 4049 } 4050 4051 function getEnd() { 4052 var end = slideCount - (hasFocus() || isLoop && perMove ? 1 : perPage); 4053 4054 while (omitEnd && end-- > 0) { 4055 if (toPosition(slideCount - 1, true) !== toPosition(end, true)) { 4056 end++; 4057 break; 4058 } 4059 } 4060 4061 return clamp(end, 0, slideCount - 1); 4062 } 4063 4064 function toIndex(page) { 4065 return clamp(hasFocus() ? page : perPage * page, 0, endIndex); 4066 } 4067 4068 function toPage(index) { 4069 return hasFocus() ? min(index, endIndex) : floor((index >= endIndex ? slideCount - 1 : index) / perPage); 4070 } 4071 4072 function toDest(destination) { 4073 var closest = Move.toIndex(destination); 4074 return isSlide ? clamp(closest, 0, endIndex) : closest; 4075 } 4076 4077 function setIndex(index) { 4078 if (index !== currIndex) { 4079 prevIndex = currIndex; 4080 currIndex = index; 4081 } 4082 } 4083 4084 function getIndex(prev) { 4085 return prev ? prevIndex : currIndex; 4086 } 4087 4088 function hasFocus() { 4089 return !isUndefined(options.focus) || options.isNavigation; 4090 } 4091 4092 function isBusy() { 4093 return Splide2.state.is([MOVING, SCROLLING]) && !!options.waitForTransition; 4094 } 4095 4096 return { 4097 mount: mount, 4098 go: go, 4099 scroll: scroll, 4100 getNext: getNext, 4101 getPrev: getPrev, 4102 getAdjacent: getAdjacent, 4103 getEnd: getEnd, 4104 setIndex: setIndex, 4105 getIndex: getIndex, 4106 toIndex: toIndex, 4107 toPage: toPage, 4108 toDest: toDest, 4109 hasFocus: hasFocus, 4110 isBusy: isBusy 4111 }; 4112} 4113 4114var XML_NAME_SPACE = "http://www.w3.org/2000/svg"; 4115var PATH = "m15.5 0.932-4.3 4.38 14.5 14.6-14.5 14.5 4.3 4.4 14.6-14.6 4.4-4.3-4.4-4.4-14.6-14.6z"; 4116var SIZE = 40; 4117 4118function Arrows(Splide2, Components2, options) { 4119 var event = EventInterface(Splide2); 4120 var on = event.on, 4121 bind = event.bind, 4122 emit = event.emit; 4123 var classes = options.classes, 4124 i18n = options.i18n; 4125 var Elements = Components2.Elements, 4126 Controller = Components2.Controller; 4127 var placeholder = Elements.arrows, 4128 track = Elements.track; 4129 var wrapper = placeholder; 4130 var prev = Elements.prev; 4131 var next = Elements.next; 4132 var created; 4133 var wrapperClasses; 4134 var arrows = {}; 4135 4136 function mount() { 4137 init(); 4138 on(EVENT_UPDATED, remount); 4139 } 4140 4141 function remount() { 4142 destroy(); 4143 mount(); 4144 } 4145 4146 function init() { 4147 var enabled = options.arrows; 4148 4149 if (enabled && !(prev && next)) { 4150 createArrows(); 4151 } 4152 4153 if (prev && next) { 4154 assign
4154$1
vendor: 30,354 bytes, lines 4154-5367
4154(arrows, { 4155 prev: prev, 4156 next: next 4157 }); 4158 display(wrapper, enabled ? "" : "none"); 4159 addClass(wrapper, wrapperClasses = CLASS_ARROWS + "--" + options.direction); 4160 4161 if (enabled) { 4162 listen(); 4163 update(); 4164 setAttribute([prev, next], ARIA_CONTROLS, track.id); 4165 emit(EVENT_ARROWS_MOUNTED, prev, next); 4166 } 4167 } 4168 } 4169 4170 function destroy() { 4171 event.destroy(); 4172 removeClass(wrapper, wrapperClasses); 4173 4174 if (created) { 4175 remove(placeholder ? [prev, next] : wrapper); 4176 prev = next = null; 4177 } else { 4178 removeAttribute([prev, next], ALL_ATTRIBUTES); 4179 } 4180 } 4181 4182 function listen() { 4183 on([EVENT_MOUNTED, EVENT_MOVED, EVENT_REFRESH, EVENT_SCROLLED, EVENT_END_INDEX_CHANGED], update); 4184 bind(next, "click", apply(go, ">")); 4185 bind(prev, "click", apply(go, "<")); 4186 } 4187 4188 function go(control) { 4189 Controller.go(control, true); 4190 } 4191 4192 function createArrows() { 4193 wrapper = placeholder || create("div", classes.arrows); 4194 prev = createArrow(true); 4195 next = createArrow(false); 4196 created = true; 4197 append(wrapper, [prev, next]); 4198 !placeholder && before(wrapper, track); 4199 } 4200 4201 function createArrow(prev2) { 4202 var arrow = "<button class=\"" + classes.arrow + " " + (prev2 ? classes.prev : classes.next) + "\" type=\"button\"><svg xmlns=\"" + XML_NAME_SPACE + "\" viewBox=\"0 0 " + SIZE + " " + SIZE + "\" width=\"" + SIZE + "\" height=\"" + SIZE + "\" focusable=\"false\"><path d=\"" + (options.arrowPath || PATH) + "\" />"; 4203 return parseHtml(arrow); 4204 } 4205 4206 function update() { 4207 if (prev && next) { 4208 var index = Splide2.index; 4209 var prevIndex = Controller.getPrev(); 4210 var nextIndex = Controller.getNext(); 4211 var prevLabel = prevIndex > -1 && index < prevIndex ? i18n.last : i18n.prev; 4212 var nextLabel = nextIndex > -1 && index > nextIndex ? i18n.first : i18n.next; 4213 prev.disabled = prevIndex < 0; 4214 next.disabled = nextIndex < 0; 4215 setAttribute(prev, ARIA_LABEL, prevLabel); 4216 setAttribute(next, ARIA_LABEL, nextLabel); 4217 emit(EVENT_ARROWS_UPDATED, prev, next, prevIndex, nextIndex); 4218 } 4219 } 4220 4221 return { 4222 arrows: arrows, 4223 mount: mount, 4224 destroy: destroy, 4225 update: update 4226 }; 4227} 4228 4229var INTERVAL_DATA_ATTRIBUTE = DATA_ATTRIBUTE + "-interval"; 4230 4231function Autoplay(Splide2, Components2, options) { 4232 var _EventInterface6 = EventInterface(Splide2), 4233 on = _EventInterface6.on, 4234 bind = _EventInterface6.bind, 4235 emit = _EventInterface6.emit; 4236 4237 var interval = RequestInterval(options.interval, Splide2.go.bind(Splide2, ">"), onAnimationFrame); 4238 var isPaused = interval.isPaused; 4239 var Elements = Components2.Elements, 4240 _Components2$Elements4 = Components2.Elements, 4241 root = _Components2$Elements4.root, 4242 toggle = _Components2$Elements4.toggle; 4243 var autoplay = options.autoplay; 4244 var hovered; 4245 var focused; 4246 var stopped = autoplay === "pause"; 4247 4248 function mount() { 4249 if (autoplay) { 4250 listen(); 4251 toggle && setAttribute(toggle, ARIA_CONTROLS, Elements.track.id); 4252 stopped || play(); 4253 update(); 4254 } 4255 } 4256 4257 function listen() { 4258 if (options.pauseOnHover) { 4259 bind(root, "mouseenter mouseleave", function (e) { 4260 hovered = e.type === "mouseenter"; 4261 autoToggle(); 4262 }); 4263 } 4264 4265 if (options.pauseOnFocus) { 4266 bind(root, "focusin focusout", function (e) { 4267 focused = e.type === "focusin"; 4268 autoToggle(); 4269 }); 4270 } 4271 4272 if (toggle) { 4273 bind(toggle, "click", function () { 4274 stopped ? play() : pause(true); 4275 }); 4276 } 4277 4278 on([EVENT_MOVE, EVENT_SCROLL, EVENT_REFRESH], interval.rewind); 4279 on(EVENT_MOVE, onMove); 4280 } 4281 4282 function play() { 4283 if (isPaused() && Components2.Slides.isEnough()) { 4284 interval.start(!options.resetProgress); 4285 focused = hovered = stopped = false; 4286 update(); 4287 emit(EVENT_AUTOPLAY_PLAY); 4288 } 4289 } 4290 4291 function pause(stop) { 4292 if (stop === void 0) { 4293 stop = true; 4294 } 4295 4296 stopped = !!stop; 4297 update(); 4298 4299 if (!isPaused()) { 4300 interval.pause(); 4301 emit(EVENT_AUTOPLAY_PAUSE); 4302 } 4303 } 4304 4305 function autoToggle() { 4306 if (!stopped) { 4307 hovered || focused ? pause(false) : play(); 4308 } 4309 } 4310 4311 function update() { 4312 if (toggle) { 4313 toggleClass(toggle, CLASS_ACTIVE, !stopped); 4314 setAttribute(toggle, ARIA_LABEL, options.i18n[stopped ? "play" : "pause"]); 4315 } 4316 } 4317 4318 function onAnimationFrame(rate) { 4319 var bar = Elements.bar; 4320 bar && style(bar, "width", rate * 100 + "%"); 4321 emit(EVENT_AUTOPLAY_PLAYING, rate); 4322 } 4323 4324 function onMove(index) { 4325 var Slide = Components2.Slides.getAt(index); 4326 interval.set(Slide && +getAttribute(Slide.slide, INTERVAL_DATA_ATTRIBUTE) || options.interval); 4327 } 4328 4329 return { 4330 mount: mount, 4331 destroy: interval.cancel, 4332 play: play, 4333 pause: pause, 4334 isPaused: isPaused 4335 }; 4336} 4337 4338function Cover(Splide2, Components2, options) { 4339 var _EventInterface7 = EventInterface(Splide2), 4340 on = _EventInterface7.on; 4341 4342 function mount() { 4343 if (options.cover) { 4344 on(EVENT_LAZYLOAD_LOADED, apply(toggle, true)); 4345 on([EVENT_MOUNTED, EVENT_UPDATED, EVENT_REFRESH], apply(cover, true)); 4346 } 4347 } 4348 4349 function cover(cover2) { 4350 Components2.Slides.forEach(function (Slide) { 4351 var img = child(Slide.container || Slide.slide, "img"); 4352 4353 if (img && img.src) { 4354 toggle(cover2, img, Slide); 4355 } 4356 }); 4357 } 4358 4359 function toggle(cover2, img, Slide) { 4360 Slide.style("background", cover2 ? "center/cover no-repeat url(\"" + img.src + "\")" : "", true); 4361 display(img, cover2 ? "none" : ""); 4362 } 4363 4364 return { 4365 mount: mount, 4366 destroy: apply(cover, false) 4367 }; 4368} 4369 4370var BOUNCE_DIFF_THRESHOLD = 10; 4371var BOUNCE_DURATION = 600; 4372var FRICTION_FACTOR = 0.6; 4373var BASE_VELOCITY = 1.5; 4374var MIN_DURATION = 800; 4375 4376function Scroll(Splide2, Components2, options) { 4377 var _EventInterface8 = EventInterface(Splide2), 4378 on = _EventInterface8.on, 4379 emit = _EventInterface8.emit; 4380 4381 var set = Splide2.state.set; 4382 var Move = Components2.Move; 4383 var getPosition = Move.getPosition, 4384 getLimit = Move.getLimit, 4385 exceededLimit = Move.exceededLimit, 4386 translate = Move.translate; 4387 var isSlide = Splide2.is(SLIDE); 4388 var interval; 4389 var callback; 4390 var friction = 1; 4391 4392 function mount() { 4393 on(EVENT_MOVE, clear); 4394 on([EVENT_UPDATED, EVENT_REFRESH], cancel); 4395 } 4396 4397 function scroll(destination, duration, snap, onScrolled, noConstrain) { 4398 var from = getPosition(); 4399 clear(); 4400 4401 if (snap && (!isSlide || !exceededLimit())) { 4402 var size = Components2.Layout.sliderSize(); 4403 var offset = sign(destination) * size * floor(abs(destination) / size) || 0; 4404 destination = Move.toPosition(Components2.Controller.toDest(destination % size)) + offset; 4405 } 4406 4407 var noDistance = approximatelyEqual(from, destination, 1); 4408 friction = 1; 4409 duration = noDistance ? 0 : duration || max(abs(destination - from) / BASE_VELOCITY, MIN_DURATION); 4410 callback = onScrolled; 4411 interval = RequestInterval(duration, onEnd, apply(update, from, destination, noConstrain), 1); 4412 set(SCROLLING); 4413 emit(EVENT_SCROLL); 4414 interval.start(); 4415 } 4416 4417 function onEnd() { 4418 set(IDLE); 4419 callback && callback(); 4420 emit(EVENT_SCROLLED); 4421 } 4422 4423 function update(from, to, noConstrain, rate) { 4424 var position = getPosition(); 4425 var target = from + (to - from) * easing(rate); 4426 var diff = (target - position) * friction; 4427 translate(position + diff); 4428 4429 if (isSlide && !noConstrain && exceededLimit()) { 4430 friction *= FRICTION_FACTOR; 4431 4432 if (abs(diff) < BOUNCE_DIFF_THRESHOLD) { 4433 scroll(getLimit(exceededLimit(true)), BOUNCE_DURATION, false, callback, true); 4434 } 4435 } 4436 } 4437 4438 function clear() { 4439 if (interval) { 4440 interval.cancel(); 4441 } 4442 } 4443 4444 function cancel() { 4445 if (interval && !interval.isPaused()) { 4446 clear(); 4447 onEnd(); 4448 } 4449 } 4450 4451 function easing(t) { 4452 var easingFunc = options.easingFunc; 4453 return easingFunc ? easingFunc(t) : 1 - Math.pow(1 - t, 4); 4454 } 4455 4456 return { 4457 mount: mount, 4458 destroy: clear, 4459 scroll: scroll, 4460 cancel: cancel 4461 }; 4462} 4463 4464var SCROLL_LISTENER_OPTIONS = { 4465 passive: false, 4466 capture: true 4467}; 4468 4469function Drag(Splide2, Components2, options) { 4470 var _EventInterface9 = EventInterface(Splide2), 4471 on = _EventInterface9.on, 4472 emit = _EventInterface9.emit, 4473 bind = _EventInterface9.bind, 4474 unbind = _EventInterface9.unbind; 4475 4476 var state = Splide2.state; 4477 var Move = Components2.Move, 4478 Scroll = Components2.Scroll, 4479 Controller = Components2.Controller, 4480 track = Components2.Elements.track, 4481 reduce = Components2.Media.reduce; 4482 var _Components2$Directio2 = Components2.Direction, 4483 resolve = _Components2$Directio2.resolve, 4484 orient = _Components2$Directio2.orient; 4485 var getPosition = Move.getPosition, 4486 exceededLimit = Move.exceededLimit; 4487 var basePosition; 4488 var baseEvent; 4489 var prevBaseEvent; 4490 var isFree; 4491 var dragging; 4492 var exceeded = false; 4493 var clickPrevented; 4494 var disabled; 4495 var target; 4496 4497 function mount() { 4498 bind(track, POINTER_MOVE_EVENTS, noop, SCROLL_LISTENER_OPTIONS); 4499 bind(track, POINTER_UP_EVENTS, noop, SCROLL_LISTENER_OPTIONS); 4500 bind(track, POINTER_DOWN_EVENTS, onPointerDown, SCROLL_LISTENER_OPTIONS); 4501 bind(track, "click", onClick, { 4502 capture: true 4503 }); 4504 bind(track, "dragstart", prevent); 4505 on([EVENT_MOUNTED, EVENT_UPDATED], init); 4506 } 4507 4508 function init() { 4509 var drag = options.drag; 4510 disable(!drag); 4511 isFree = drag === "free"; 4512 } 4513 4514 function onPointerDown(e) { 4515 clickPrevented = false; 4516 4517 if (!disabled) { 4518 var isTouch = isTouchEvent(e); 4519 4520 if (isDraggable(e.target) && (isTouch || !e.button)) { 4521 if (!Controller.isBusy()) { 4522 target = isTouch ? track : window; 4523 dragging = state.is([MOVING, SCROLLING]); 4524 prevBaseEvent = null; 4525 bind(target, POINTER_MOVE_EVENTS, onPointerMove, SCROLL_LISTENER_OPTIONS); 4526 bind(target, POINTER_UP_EVENTS, onPointerUp, SCROLL_LISTENER_OPTIONS); 4527 Move.cancel(); 4528 Scroll.cancel(); 4529 save(e); 4530 } else { 4531 prevent(e, true); 4532 } 4533 } 4534 } 4535 } 4536 4537 function onPointerMove(e) { 4538 if (!state.is(DRAGGING)) { 4539 state.set(DRAGGING); 4540 emit(EVENT_DRAG); 4541 } 4542 4543 if (e.cancelable) { 4544 if (dragging) { 4545 Move.translate(basePosition + constrain(diffCoord(e))); 4546 var expired = diffTime(e) > LOG_INTERVAL; 4547 var hasExceeded = exceeded !== (exceeded = exceededLimit()); 4548 4549 if (expired || hasExceeded) { 4550 save(e); 4551 } 4552 4553 clickPrevented = true; 4554 emit(EVENT_DRAGGING); 4555 prevent(e); 4556 } else if (isSliderDirection(e)) { 4557 dragging = shouldStart(e); 4558 prevent(e); 4559 } 4560 } 4561 } 4562 4563 function onPointerUp(e) { 4564 if (state.is(DRAGGING)) { 4565 state.set(IDLE); 4566 emit(EVENT_DRAGGED); 4567 } 4568 4569 if (dragging) { 4570 move(e); 4571 prevent(e); 4572 } 4573 4574 unbind(target, POINTER_MOVE_EVENTS, onPointerMove); 4575 unbind(target, POINTER_UP_EVENTS, onPointerUp); 4576 dragging = false; 4577 } 4578 4579 function onClick(e) { 4580 if (!disabled && clickPrevented) { 4581 prevent(e, true); 4582 } 4583 } 4584 4585 function save(e) { 4586 prevBaseEvent = baseEvent; 4587 baseEvent = e; 4588 basePosition = getPosition(); 4589 } 4590 4591 function move(e) { 4592 var velocity = computeVelocity(e); 4593 var destination = computeDestination(velocity); 4594 var rewind = options.rewind && options.rewindByDrag; 4595 reduce(false); 4596 4597 if (isFree) { 4598 Controller.scroll(destination, 0, options.snap); 4599 } else if (Splide2.is(FADE)) { 4600 Controller.go(orient(sign(velocity)) < 0 ? rewind ? "<" : "-" : rewind ? ">" : "+"); 4601 } else if (Splide2.is(SLIDE) && exceeded && rewind) { 4602 Controller.go(exceededLimit(true) ? ">" : "<"); 4603 } else { 4604 Controller.go(Controller.toDest(destination), true); 4605 } 4606 4607 reduce(true); 4608 } 4609 4610 function shouldStart(e) { 4611 var thresholds = options.dragMinThreshold; 4612 var isObj = isObject(thresholds); 4613 var mouse = isObj && thresholds.mouse || 0; 4614 var touch = (isObj ? thresholds.touch : +thresholds) || 10; 4615 return abs(diffCoord(e)) > (isTouchEvent(e) ? touch : mouse); 4616 } 4617 4618 function isSliderDirection(e) { 4619 return abs(diffCoord(e)) > abs(diffCoord(e, true)); 4620 } 4621 4622 function computeVelocity(e) { 4623 if (Splide2.is(LOOP) || !exceeded) { 4624 var time = diffTime(e); 4625 4626 if (time && time < LOG_INTERVAL) { 4627 return diffCoord(e) / time; 4628 } 4629 } 4630 4631 return 0; 4632 } 4633 4634 function computeDestination(velocity) { 4635 return getPosition() + sign(velocity) * min(abs(velocity) * (options.flickPower || 600), isFree ? Infinity : Components2.Layout.listSize() * (options.flickMaxPages || 1)); 4636 } 4637 4638 function diffCoord(e, orthogonal) { 4639 return coordOf(e, orthogonal) - coordOf(getBaseEvent(e), orthogonal); 4640 } 4641 4642 function diffTime(e) { 4643 return timeOf(e) - timeOf(getBaseEvent(e)); 4644 } 4645 4646 function getBaseEvent(e) { 4647 return baseEvent === e && prevBaseEvent || baseEvent; 4648 } 4649 4650 function coordOf(e, orthogonal) { 4651 return (isTouchEvent(e) ? e.changedTouches[0] : e)["page" + resolve(orthogonal ? "Y" : "X")]; 4652 } 4653 4654 function constrain(diff) { 4655 return diff / (exceeded && Splide2.is(SLIDE) ? FRICTION : 1); 4656 } 4657 4658 function isDraggable(target2) { 4659 var noDrag = options.noDrag; 4660 return !matches(target2, "." + CLASS_PAGINATION_PAGE + ", ." + CLASS_ARROW) && (!noDrag || !matches(target2, noDrag)); 4661 } 4662 4663 function isTouchEvent(e) { 4664 return typeof TouchEvent !== "undefined" && e instanceof TouchEvent; 4665 } 4666 4667 function isDragging() { 4668 return dragging; 4669 } 4670 4671 function disable(value) { 4672 disabled = value; 4673 } 4674 4675 return { 4676 mount: mount, 4677 disable: disable, 4678 isDragging: isDragging 4679 }; 4680} 4681 4682var NORMALIZATION_MAP = { 4683 Spacebar: " ", 4684 Right: ARROW_RIGHT, 4685 Left: ARROW_LEFT, 4686 Up: ARROW_UP, 4687 Down: ARROW_DOWN 4688}; 4689 4690function normalizeKey(key) { 4691 key = isString(key) ? key : key.key; 4692 return NORMALIZATION_MAP[key] || key; 4693} 4694 4695var KEYBOARD_EVENT = "keydown"; 4696 4697function Keyboard(Splide2, Components2, options) { 4698 var _EventInterface10 = EventInterface(Splide2), 4699 on = _EventInterface10.on, 4700 bind = _EventInterface10.bind, 4701 unbind = _EventInterface10.unbind; 4702 4703 var root = Splide2.root; 4704 var resolve = Components2.Direction.resolve; 4705 var target; 4706 var disabled; 4707 4708 function mount() { 4709 init(); 4710 on(EVENT_UPDATED, destroy); 4711 on(EVENT_UPDATED, init); 4712 on(EVENT_MOVE, onMove); 4713 } 4714 4715 function init() { 4716 var keyboard = options.keyboard; 4717 4718 if (keyboard) { 4719 target = keyboard === "global" ? window : root; 4720 bind(target, KEYBOARD_EVENT, onKeydown); 4721 } 4722 } 4723 4724 function destroy() { 4725 unbind(target, KEYBOARD_EVENT); 4726 } 4727 4728 function disable(value) { 4729 disabled = value; 4730 } 4731 4732 function onMove() { 4733 var _disabled = disabled; 4734 disabled = true; 4735 nextTick(function () { 4736 disabled = _disabled; 4737 }); 4738 } 4739 4740 function onKeydown(e) { 4741 if (!disabled) { 4742 var key = normalizeKey(e); 4743 4744 if (key === resolve(ARROW_LEFT)) { 4745 Splide2.go("<"); 4746 } else if (key === resolve(ARROW_RIGHT)) { 4747 Splide2.go(">"); 4748 } 4749 } 4750 } 4751 4752 return { 4753 mount: mount, 4754 destroy: destroy, 4755 disable: disable 4756 }; 4757} 4758 4759var SRC_DATA_ATTRIBUTE = DATA_ATTRIBUTE + "-lazy"; 4760var SRCSET_DATA_ATTRIBUTE = SRC_DATA_ATTRIBUTE + "-srcset"; 4761var IMAGE_SELECTOR = "[" + SRC_DATA_ATTRIBUTE + "], [" + SRCSET_DATA_ATTRIBUTE + "]"; 4762 4763function LazyLoad(Splide2, Components2, options) { 4764 var _EventInterface11 = EventInterface(Splide2), 4765 on = _EventInterface11.on, 4766 off = _EventInterface11.off, 4767 bind = _EventInterface11.bind, 4768 emit = _EventInterface11.emit; 4769 4770 var isSequential = options.lazyLoad === "sequential"; 4771 var events = [EVENT_MOVED, EVENT_SCROLLED]; 4772 var entries = []; 4773 4774 function mount() { 4775 if (options.lazyLoad) { 4776 init(); 4777 on(EVENT_REFRESH, init); 4778 } 4779 } 4780 4781 function init() { 4782 empty(entries); 4783 register(); 4784 4785 if (isSequential) { 4786 loadNext(); 4787 } else { 4788 off(events); 4789 on(events, check); 4790 check(); 4791 } 4792 } 4793 4794 function register() { 4795 Components2.Slides.forEach(function (Slide) { 4796 queryAll(Slide.slide, IMAGE_SELECTOR).forEach(function (img) { 4797 var src = getAttribute(img, SRC_DATA_ATTRIBUTE); 4798 var srcset = getAttribute(img, SRCSET_DATA_ATTRIBUTE); 4799 4800 if (src !== img.src || srcset !== img.srcset) { 4801 var className = options.classes.spinner; 4802 var parent = img.parentElement; 4803 var spinner = child(parent, "." + className) || create("span", className, parent); 4804 entries.push([img, Slide, spinner]); 4805 img.src || display(img, "none"); 4806 } 4807 }); 4808 }); 4809 } 4810 4811 function check() { 4812 entries = entries.filter(function (data) { 4813 var distance = options.perPage * ((options.preloadPages || 1) + 1) - 1; 4814 return data[1].isWithin(Splide2.index, distance) ? load(data) : true; 4815 }); 4816 entries.length || off(events); 4817 } 4818 4819 function load(data) { 4820 var img = data[0]; 4821 addClass(data[1].slide, CLASS_LOADING); 4822 bind(img, "load error", apply(onLoad, data)); 4823 setAttribute(img, "src", getAttribute(img, SRC_DATA_ATTRIBUTE)); 4824 setAttribute(img, "srcset", getAttribute(img, SRCSET_DATA_ATTRIBUTE)); 4825 removeAttribute(img, SRC_DATA_ATTRIBUTE); 4826 removeAttribute(img, SRCSET_DATA_ATTRIBUTE); 4827 } 4828 4829 function onLoad(data, e) { 4830 var img = data[0], 4831 Slide = data[1]; 4832 removeClass(Slide.slide, CLASS_LOADING); 4833 4834 if (e.type !== "error") { 4835 remove(data[2]); 4836 display(img, ""); 4837 emit(EVENT_LAZYLOAD_LOADED, img, Slide); 4838 emit(EVENT_RESIZE); 4839 } 4840 4841 isSequential && loadNext(); 4842 } 4843 4844 function loadNext() { 4845 entries.length && load(entries.shift()); 4846 } 4847 4848 return { 4849 mount: mount, 4850 destroy: apply(empty, entries), 4851 check: check 4852 }; 4853} 4854 4855function Pagination(Splide2, Components2, options) { 4856 var event = EventInterface(Splide2); 4857 var on = event.on, 4858 emit = event.emit, 4859 bind = event.bind; 4860 var Slides = Components2.Slides, 4861 Elements = Components2.Elements, 4862 Controller = Components2.Controller; 4863 var hasFocus = Controller.hasFocus, 4864 getIndex = Controller.getIndex, 4865 go = Controller.go; 4866 var resolve = Components2.Direction.resolve; 4867 var placeholder = Elements.pagination; 4868 var items = []; 4869 var list; 4870 var paginationClasses; 4871 4872 function mount() { 4873 destroy(); 4874 on([EVENT_UPDATED, EVENT_REFRESH, EVENT_END_INDEX_CHANGED], mount); 4875 var enabled = options.pagination; 4876 placeholder && display(placeholder, enabled ? "" : "none"); 4877 4878 if (enabled) { 4879 on([EVENT_MOVE, EVENT_SCROLL, EVENT_SCROLLED], update); 4880 createPagination(); 4881 update(); 4882 emit(EVENT_PAGINATION_MOUNTED, { 4883 list: list, 4884 items: items 4885 }, getAt(Splide2.index)); 4886 } 4887 } 4888 4889 function destroy() { 4890 if (list) { 4891 remove(placeholder ? slice(list.children) : list); 4892 removeClass(list, paginationClasses); 4893 empty(items); 4894 list = null; 4895 } 4896 4897 event.destroy(); 4898 } 4899 4900 function createPagination() { 4901 var length = Splide2.length; 4902 var classes = options.classes, 4903 i18n = options.i18n, 4904 perPage = options.perPage; 4905 var max = hasFocus() ? Controller.getEnd() + 1 : ceil(length / perPage); 4906 list = placeholder || create("ul", classes.pagination, Elements.track.parentElement); 4907 addClass(list, paginationClasses = CLASS_PAGINATION + "--" + getDirection()); 4908 setAttribute(list, ROLE, "tablist"); 4909 setAttribute(list, ARIA_LABEL, i18n.select); 4910 setAttribute(list, ARIA_ORIENTATION, getDirection() === TTB ? "vertical" : ""); 4911 4912 for (var i = 0; i < max; i++) { 4913 var li = create("li", null, list); 4914 var button = create("button", { 4915 class: classes.page, 4916 type: "button" 4917 }, li); 4918 var controls = Slides.getIn(i).map(function (Slide) { 4919 return Slide.slide.id; 4920 }); 4921 var text = !hasFocus() && perPage > 1 ? i18n.pageX : i18n.slideX; 4922 bind(button, "click", apply(onClick, i)); 4923 4924 if (options.paginationKeyboard) { 4925 bind(button, "keydown", apply(onKeydown, i)); 4926 } 4927 4928 setAttribute(li, ROLE, "presentation"); 4929 setAttribute(button, ROLE, "tab"); 4930 setAttribute(button, ARIA_CONTROLS, controls.join(" ")); 4931 setAttribute(button, ARIA_LABEL, format(text, i + 1)); 4932 setAttribute(button, TAB_INDEX, -1); 4933 items.push({ 4934 li: li, 4935 button: button, 4936 page: i 4937 }); 4938 } 4939 } 4940 4941 function onClick(page) { 4942 go(">" + page, true); 4943 } 4944 4945 function onKeydown(page, e) { 4946 var length = items.length; 4947 var key = normalizeKey(e); 4948 var dir = getDirection(); 4949 var nextPage = -1; 4950 4951 if (key === resolve(ARROW_RIGHT, false, dir)) { 4952 nextPage = ++page % length; 4953 } else if (key === resolve(ARROW_LEFT, false, dir)) { 4954 nextPage = (--page + length) % length; 4955 } else if (key === "Home") { 4956 nextPage = 0; 4957 } else if (key === "End") { 4958 nextPage = length - 1; 4959 } 4960 4961 var item = items[nextPage]; 4962 4963 if (item) { 4964 focus(item.button); 4965 go(">" + nextPage); 4966 prevent(e, true); 4967 } 4968 } 4969 4970 function getDirection() { 4971 return options.paginationDirection || options.direction; 4972 } 4973 4974 function getAt(index) { 4975 return items[Controller.toPage(index)]; 4976 } 4977 4978 function update() { 4979 var prev = getAt(getIndex(true)); 4980 var curr = getAt(getIndex()); 4981 4982 if (prev) { 4983 var button = prev.button; 4984 removeClass(button, CLASS_ACTIVE); 4985 removeAttribute(button, ARIA_SELECTED); 4986 setAttribute(button, TAB_INDEX, -1); 4987 } 4988 4989 if (curr) { 4990 var _button = curr.button; 4991 addClass(_button, CLASS_ACTIVE); 4992 setAttribute(_button, ARIA_SELECTED, true); 4993 setAttribute(_button, TAB_INDEX, ""); 4994 } 4995 4996 emit(EVENT_PAGINATION_UPDATED, { 4997 list: list, 4998 items: items 4999 }, prev, curr); 5000 } 5001 5002 return { 5003 items: items, 5004 mount: mount, 5005 destroy: destroy, 5006 getAt: getAt, 5007 update: update 5008 }; 5009} 5010 5011var TRIGGER_KEYS = [" ", "Enter"]; 5012 5013function Sync(Splide2, Components2, options) { 5014 var isNavigation = options.isNavigation, 5015 slideFocus = options.slideFocus; 5016 var events = []; 5017 5018 function mount() { 5019 Splide2.splides.forEach(function (target) { 5020 if (!target.isParent) { 5021 sync(Splide2, target.splide); 5022 sync(target.splide, Splide2); 5023 } 5024 }); 5025 5026 if (isNavigation) { 5027 navigate(); 5028 } 5029 } 5030 5031 function destroy() { 5032 events.forEach(function (event) { 5033 event.destroy(); 5034 }); 5035 empty(events); 5036 } 5037 5038 function remount() { 5039 destroy(); 5040 mount(); 5041 } 5042 5043 function sync(splide, target) { 5044 var event = EventInterface(splide); 5045 event.on(EVENT_MOVE, function (index, prev, dest) { 5046 target.go(target.is(LOOP) ? dest : index); 5047 }); 5048 events.push(event); 5049 } 5050 5051 function navigate() { 5052 var event = EventInterface(Splide2); 5053 var on = event.on; 5054 on(EVENT_CLICK, onClick); 5055 on(EVENT_SLIDE_KEYDOWN, onKeydown); 5056 on([EVENT_MOUNTED, EVENT_UPDATED], update); 5057 events.push(event); 5058 event.emit(EVENT_NAVIGATION_MOUNTED, Splide2.splides); 5059 } 5060 5061 function update() { 5062 setAttribute(Components2.Elements.list, ARIA_ORIENTATION, options.direction === TTB ? "vertical" : ""); 5063 } 5064 5065 function onClick(Slide) { 5066 Splide2.go(Slide.index); 5067 } 5068 5069 function onKeydown(Slide, e) { 5070 if (includes(TRIGGER_KEYS, normalizeKey(e))) { 5071 onClick(Slide); 5072 prevent(e); 5073 } 5074 } 5075 5076 return { 5077 setup: apply(Components2.Media.set, { 5078 slideFocus: isUndefined(slideFocus) ? isNavigation : slideFocus 5079 }, true), 5080 mount: mount, 5081 destroy: destroy, 5082 remount: remount 5083 }; 5084} 5085 5086function Wheel(Splide2, Components2, options) { 5087 var _EventInterface12 = EventInterface(Splide2), 5088 bind = _EventInterface12.bind; 5089 5090 var lastTime = 0; 5091 5092 function mount() { 5093 if (options.wheel) { 5094 bind(Components2.Elements.track, "wheel", onWheel, SCROLL_LISTENER_OPTIONS); 5095 } 5096 } 5097 5098 function onWheel(e) { 5099 if (e.cancelable) { 5100 var deltaY = e.deltaY; 5101 var backwards = deltaY < 0; 5102 var timeStamp = timeOf(e); 5103 5104 var _min = options.wheelMinThreshold || 0; 5105 5106 var sleep = options.wheelSleep || 0; 5107 5108 if (abs(deltaY) > _min && timeStamp - lastTime > sleep) { 5109 Splide2.go(backwards ? "<" : ">"); 5110 lastTime = timeStamp; 5111 } 5112 5113 shouldPrevent(backwards) && prevent(e); 5114 } 5115 } 5116 5117 function shouldPrevent(backwards) { 5118 return !options.releaseWheel || Splide2.state.is(MOVING) || Components2.Controller.getAdjacent(backwards) !== -1; 5119 } 5120 5121 return { 5122 mount: mount 5123 }; 5124} 5125 5126var SR_REMOVAL_DELAY = 90; 5127 5128function Live(Splide2, Components2, options) { 5129 var _EventInterface13 = EventInterface(Splide2), 5130 on = _EventInterface13.on; 5131 5132 var track = Components2.Elements.track; 5133 var enabled = options.live && !options.isNavigation; 5134 var sr = create("span", CLASS_SR); 5135 var interval = RequestInterval(SR_REMOVAL_DELAY, apply(toggle, false)); 5136 5137 function mount() { 5138 if (enabled) { 5139 disable(!Components2.Autoplay.isPaused()); 5140 setAttribute(track, ARIA_ATOMIC, true); 5141 sr.textContent = "\u2026"; 5142 on(EVENT_AUTOPLAY_PLAY, apply(disable, true)); 5143 on(EVENT_AUTOPLAY_PAUSE, apply(disable, false)); 5144 on([EVENT_MOVED, EVENT_SCROLLED], apply(toggle, true)); 5145 } 5146 } 5147 5148 function toggle(active) { 5149 setAttribute(track, ARIA_BUSY, active); 5150 5151 if (active) { 5152 append(track, sr); 5153 interval.start(); 5154 } else { 5155 remove(sr); 5156 interval.cancel(); 5157 } 5158 } 5159 5160 function destroy() { 5161 removeAttribute(track, [ARIA_LIVE, ARIA_ATOMIC, ARIA_BUSY]); 5162 remove(sr); 5163 } 5164 5165 function disable(disabled) { 5166 if (enabled) { 5167 setAttribute(track, ARIA_LIVE, disabled ? "off" : "polite"); 5168 } 5169 } 5170 5171 return { 5172 mount: mount, 5173 disable: disable, 5174 destroy: destroy 5175 }; 5176} 5177 5178var ComponentConstructors = /*#__PURE__*/Object.freeze({ 5179 __proto__: null, 5180 Media: Media, 5181 Direction: Direction, 5182 Elements: Elements, 5183 Slides: Slides, 5184 Layout: Layout, 5185 Clones: Clones, 5186 Move: Move, 5187 Controller: Controller, 5188 Arrows: Arrows, 5189 Autoplay: Autoplay, 5190 Cover: Cover, 5191 Scroll: Scroll, 5192 Drag: Drag, 5193 Keyboard: Keyboard, 5194 LazyLoad: LazyLoad, 5195 Pagination: Pagination, 5196 Sync: Sync, 5197 Wheel: Wheel, 5198 Live: Live 5199}); 5200var I18N = { 5201 prev: "Previous slide", 5202 next: "Next slide", 5203 first: "Go to first slide", 5204 last: "Go to last slide", 5205 slideX: "Go to slide %s", 5206 pageX: "Go to page %s", 5207 play: "Start autoplay", 5208 pause: "Pause autoplay", 5209 carousel: "carousel", 5210 slide: "slide", 5211 select: "Select a slide to show", 5212 slideLabel: "%s of %s" 5213}; 5214var DEFAULTS = { 5215 type: "slide", 5216 role: "region", 5217 speed: 400, 5218 perPage: 1, 5219 cloneStatus: true, 5220 arrows: true, 5221 pagination: true, 5222 paginationKeyboard: true, 5223 interval: 5e3, 5224 pauseOnHover: true, 5225 pauseOnFocus: true, 5226 resetProgress: true, 5227 easing: "cubic-bezier(0.25, 1, 0.5, 1)", 5228 drag: true, 5229 direction: "ltr", 5230 trimSpace: true, 5231 focusableNodes: "a, button, textarea, input, select, iframe", 5232 live: true, 5233 classes: CLASSES, 5234 i18n: I18N, 5235 reducedMotion: { 5236 speed: 0, 5237 rewindSpeed: 0, 5238 autoplay: "pause" 5239 } 5240}; 5241 5242function Fade(Splide2, Components2, options) { 5243 var Slides = Components2.Slides; 5244 5245 function mount() { 5246 EventInterface(Splide2).on([EVENT_MOUNTED, EVENT_REFRESH], init); 5247 } 5248 5249 function init() { 5250 Slides.forEach(function (Slide) { 5251 Slide.style("transform", "translateX(-" + 100 * Slide.index + "%)"); 5252 }); 5253 } 5254 5255 function start(index, done) { 5256 Slides.style("transition", "opacity " + options.speed + "ms " + options.easing); 5257 nextTick(done); 5258 } 5259 5260 return { 5261 mount: mount, 5262 start: start, 5263 cancel: noop 5264 }; 5265} 5266 5267function Slide(Splide2, Components2, options) { 5268 var Move = Components2.Move, 5269 Controller = Components2.Controller, 5270 Scroll = Components2.Scroll; 5271 var list = Components2.Elements.list; 5272 var transition = apply(style, list, "transition"); 5273 var endCallback; 5274 5275 function mount() { 5276 EventInterface(Splide2).bind(list, "transitionend", function (e) { 5277 if (e.target === list && endCallback) { 5278 cancel(); 5279 endCallback(); 5280 } 5281 }); 5282 } 5283 5284 function start(index, done) { 5285 var destination = Move.toPosition(index, true); 5286 var position = Move.getPosition(); 5287 var speed = getSpeed(index); 5288 5289 if (abs(destination - position) >= 1 && speed >= 1) { 5290 if (options.useScroll) { 5291 Scroll.scroll(destination, speed, false, done); 5292 } else { 5293 transition("transform " + speed + "ms " + options.easing); 5294 Move.translate(destination, true); 5295 endCallback = done; 5296 } 5297 } else { 5298 Move.jump(index); 5299 done(); 5300 } 5301 } 5302 5303 function cancel() { 5304 transition(""); 5305 Scroll.cancel(); 5306 } 5307 5308 function getSpeed(index) { 5309 var rewindSpeed = options.rewindSpeed; 5310 5311 if (Splide2.is(SLIDE) && rewindSpeed) { 5312 var prev = Controller.getIndex(true); 5313 var end = Controller.getEnd(); 5314 5315 if (prev === 0 && index >= end || prev >= end && index === 0) { 5316 return rewindSpeed; 5317 } 5318 } 5319 5320 return options.speed; 5321 } 5322 5323 return { 5324 mount: mount, 5325 start: start, 5326 cancel: cancel 5327 }; 5328} 5329 5330var _Splide = /*#__PURE__*/function () { 5331 function _Splide(target, options) { 5332 this.event = EventInterface(); 5333 this.Components = {}; 5334 this.state = State(CREATED); 5335 this.splides = []; 5336 this._o = {}; 5337 this._E = {}; 5338 var root = isString(target) ? query(document, target) : target; 5339 assert(root, root + " is invalid."); 5340 this.root = root; 5341 options = merge({ 5342 label: getAttribute(root, ARIA_LABEL) || "", 5343 labelledby: getAttribute(root, ARIA_LABELLEDBY) || "" 5344 }, DEFAULTS, _Splide.defaults, options || {}); 5345 5346 try { 5347 merge(options, JSON.parse(getAttribute(root, DATA_ATTRIBUTE))); 5348 } catch (e) { 5349 assert(false, "Invalid JSON"); 5350 } 5351 5352 this._o = Object.create(merge({}, options)); 5353 } 5354 5355 var _proto = _Splide.prototype; 5356 5357 _proto.mount = function mount(Extensions, Transition) { 5358 var _this = this; 5359 5360 var state = this.state, 5361 Components2 = this.Components; 5362 assert(state.is([CREATED, DESTROYED]), "Already mounted!"); 5363 state.set(CREATED); 5364 this._C = Components2; 5365 this._T = Transition || this._T || (this.is(FADE) ? Fade : Slide); 5366 this._E = Extensions || this._E; 5367 var Constructors = assign
vendor: 2,918 bytes, lines 5367-5506
5367$1({}, ComponentConstructors, this._E, { 5368 Transition: this._T 5369 }); 5370 forOwn(Constructors, function (Component, key) { 5371 var component = Component(_this, Components2, _this._o); 5372 Components2[key] = component; 5373 component.setup && component.setup(); 5374 }); 5375 forOwn(Components2, function (component) { 5376 component.mount && component.mount(); 5377 }); 5378 this.emit(EVENT_MOUNTED); 5379 addClass(this.root, CLASS_INITIALIZED); 5380 state.set(IDLE); 5381 this.emit(EVENT_READY); 5382 return this; 5383 }; 5384 5385 _proto.sync = function sync(splide) { 5386 this.splides.push({ 5387 splide: splide 5388 }); 5389 splide.splides.push({ 5390 splide: this, 5391 isParent: true 5392 }); 5393 5394 if (this.state.is(IDLE)) { 5395 this._C.Sync.remount(); 5396 5397 splide.Components.Sync.remount(); 5398 } 5399 5400 return this; 5401 }; 5402 5403 _proto.go = function go(control) { 5404 this._C.Controller.go(control); 5405 5406 return this; 5407 }; 5408 5409 _proto.on = function on(events, callback) { 5410 this.event.on(events, callback); 5411 return this; 5412 }; 5413 5414 _proto.off = function off(events) { 5415 this.event.off(events); 5416 return this; 5417 }; 5418 5419 _proto.emit = function emit(event) { 5420 var _this$event; 5421 5422 (_this$event = this.event).emit.apply(_this$event, [event].concat(slice(arguments, 1))); 5423 5424 return this; 5425 }; 5426 5427 _proto.add = function add(slides, index) { 5428 this._C.Slides.add(slides, index); 5429 5430 return this; 5431 }; 5432 5433 _proto.remove = function remove(matcher) { 5434 this._C.Slides.remove(matcher); 5435 5436 return this; 5437 }; 5438 5439 _proto.is = function is(type) { 5440 return this._o.type === type; 5441 }; 5442 5443 _proto.refresh = function refresh() { 5444 this.emit(EVENT_REFRESH); 5445 return this; 5446 }; 5447 5448 _proto.destroy = function destroy(completely) { 5449 if (completely === void 0) { 5450 completely = true; 5451 } 5452 5453 var event = this.event, 5454 state = this.state; 5455 5456 if (state.is(CREATED)) { 5457 EventInterface(this).on(EVENT_READY, this.destroy.bind(this, completely)); 5458 } else { 5459 forOwn(this._C, function (component) { 5460 component.destroy && component.destroy(completely); 5461 }, true); 5462 event.emit(EVENT_DESTROY); 5463 event.destroy(); 5464 completely && empty(this.splides); 5465 state.set(DESTROYED); 5466 } 5467 5468 return this; 5469 }; 5470 5471 _createClass(_Splide, [{ 5472 key: "options", 5473 get: function get() { 5474 return this._o; 5475 }, 5476 set: function set(options) { 5477 this._C.Media.set(options, true, true); 5478 } 5479 }, { 5480 key: "length", 5481 get: function get() { 5482 return this._C.Slides.getLength(true); 5483 } 5484 }, { 5485 key: "index", 5486 get: function get() { 5487 return this._C.Controller.getIndex(); 5488 } 5489 }]); 5490 5491 return _Splide; 5492}(); 5493 5494var Splide = _Splide; 5495Splide.defaults = {}; 5496Splide.STATES = STATES; 5497 5498class CsSplide extends Core { 5499 5500 static options = { 5501 sliderConfigBase: { 5502 type: 'slide', 5503 lazyLoad: 'nearby' 5504 }, 5505 sliderMultipleItemsConfig: { 5506 perPage: 5,
5507 arrows: false, 5508 gap: '60px', 5509 breakpoints: { 5510 1160: { 5511 gap: '30px' 5512 }, 5513 920: { 5514 perPage: 5 5515 }, 5516 620: { 5517 perPage: 3 5518 }, 5519 420: { 5520 perPage: 2 5521 } 5522 }, 5523 }, 5524 sliderHomeConfig: { 5525 arrows: false, 5526 autoplay: true, 5527 }, 5528 slider3ItemsConfig: { 5529 perPage: 3, 5530 gap: '32px', 5531 breakpoints: { 5532 620: { 5533 perPage: 2 5534 }, 5535 420: { 5536 perPage: 1 5537 } 5538 }, 5539 }, 5540 slider3LargeItemsConfig: { 5541 perPage: 3, 5542 gap: '32px', 5543 breakpoints: { 5544 920: { 5545 perPage: 2 5546 }, 5547 620: { 5548 perPage: 1 5549 } 5550 }, 5551 }, 5552 sliderDefaultI18n: { 5553 i18n: { 5554 prev: 'Previous Slide', 5555 next: 'Next Slide', 5556 first: 'Go to the first slide', 5557 last: 'Go to the last slide', 5558 slideX: 'Go to slide %s', 5559 pageX: 'Go to page %s', 5560 play: 'Start autoplay', 5561 pause: 'Pause autoplay', 5562 carousel: 'Slider', 5563 slide: 'Slide', 5564 select: 'Select a slide to display', 5565 slideLabel: '%s of %s' 5566 } 5567 } 5568 }; 5569 5570 initializeSplide(options = {}) { 5571 const splide = new Splide(this.el, options); 5572 splide.on('mounted', () => { 5573 this.startStopAnimation(splide); 5574 }); 5575 splide.mount(); 5576 } 5577 5578 startStopAnimation(splide) { 5579 const autoplay = splide.Components.Autoplay; 5580 5581 // Check initial state: if animations are already disabled (e.g. cookie was set before slider mounted) 5582 if (document.body.classList.contains('h-stop-animations')) { 5583 autoplay.pause(); 5584 } 5585 5586 // Listen for future toggle events from the accessibility animation 5587 document.addEventListener('splideSlidersFound', function(e) { 5588 if (e.detail.found === true) { 5589 if (autoplay.isPaused()) { 5590 autoplay.play(); 5591 } 5592 } else { 5593 autoplay.pause(); 5594 } 5595 }); 5596 } 5597 5598 init() { 5599 let sliderOptions = CsSplide.options.sliderConfigBase; 5600 5601 try { 5602 const langConfig = TranslatedLang.translatedLangConfig('splide', 'i18n'); 5603 sliderOptions = { ...sliderOptions, ...langConfig }; 5604 } catch { 5605 sliderOptions = { ...sliderOptions, ...CsSplide.options.sliderDefaultI18n }; 5606 } 5607 5608 if (this.el.matches('.js-splide-slider--multiple-items')) { 5609 sliderOptions = { ...sliderOptions, ...CsSplide.options.sliderMultipleItemsConfig }; 5610 this.initializeSplide(sliderOptions); 5611 } else if (this.el.matches('.js-splide-slider--3-items')) { 5612 sliderOptions = { ...sliderOptions, ...CsSplide.options.slider3ItemsConfig }; 5613 this.initializeSplide(sliderOptions); 5614 } else if (this.el.matches('.js-splide-slider--home')) { 5615 sliderOptions = { ...sliderOptions, ...CsSplide.options.sliderHomeConfig }; 5616 this.initializeSplide(sliderOptions); 5617 } else if (this.el.matches('.js-splide-slider--3-large-items')) { 5618 sliderOptions = { ...sliderOptions, ...CsSplide.options.slider3LargeItemsConfig }; 5619 this.initializeSplide(sliderOptions); 5620 } else { 5621 sliderOptions = { ...sliderOptions }; 5622 this.initializeSplide(sliderOptions); 5623 } 5624 } 5625} 5626 5627class CsResponsiveVideo extends Core { 5628 5629 static options = { 5630 5631 }; 5632 5633 /** 5634 * init responsive video 5635 */ 5636 initResponsiveVideo() { 5637 const videos = document.querySelectorAll('.js-responsive-video'); 5638 5639 const setSources = (video) => { 5640 5641 const source = video.querySelector('source'); 5642 const { small, large } = video.dataset; 5643 5644 let newSrc = ''; 5645 5646 /* @ToDo FE: use breakpoints from css config */ 5647 if (window.innerWidth <= 820) { 5648 newSrc = small; 5649 } else { 5650 newSrc = large; 5651 } 5652 5653 if (source.getAttribute('src') !== newSrc) { 5654 source.setAttribute('src', newSrc); 5655 video.load(); 5656 } 5657 }; 5658 5659 const handleResize = () => { 5660 videos.forEach(video => { 5661 setSources(video); 5662 }); 5663 }; 5664 5665 videos.forEach(video => { 5666 setSources(video); 5667 }); 5668 5669 let resizeTimeout; 5670 window.addEventListener('resize', () => { 5671 clearTimeout(resizeTimeout); 5672 resizeTimeout = setTimeout(() => { 5673 handleResize(); 5674 }, 100); 5675 5676 }); 5677 } 5678 5679 init() { 5680 this.initResponsiveVideo(); 5681 } 5682} 5683 5684function _extends() { 5685 return _extends = Object.assign ? Object.assign.bind() : function (n) { 5686 for (var e = 1; e < arguments.length; e++) { 5687 var t = arguments[e]; 5688 for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); 5689 } 5690 return n; 5691 }, _extends.apply(null, arguments); 5692} 5693
vendor: 10,807 bytes, lines 5694-5999
5694class ImageType { 5695 constructor() { 5696 this.figcaptionId = 0; 5697 this.userSettings = null; 5698 } 5699 init(el, container, userSettings) { 5700 this.userSettings = userSettings; 5701 const FIGURE = document.createElement('figure'); 5702 const IMAGE = document.createElement('img'); 5703 const THUMBNAIL = el.querySelector('img'); 5704 const LOADING_INDICATOR = document.createElement('div'); 5705 5706 // Accessibility: allow setting focus programmatically on figure elements. 5707 FIGURE.tabIndex = -1; 5708 5709 // Add role="group" to figure 5710 FIGURE.setAttribute('role', 'group'); 5711 5712 // Hide figure until the image is loaded 5713 FIGURE.style.opacity = '0'; 5714 if (THUMBNAIL) { 5715 IMAGE.alt = THUMBNAIL.alt || ''; 5716 } 5717 IMAGE.setAttribute('data-src', el.href); 5718 if (el.hasAttribute('data-srcset')) { 5719 IMAGE.setAttribute('data-srcset', el.getAttribute('data-srcset')); 5720 } 5721 if (el.hasAttribute('data-sizes')) { 5722 IMAGE.setAttribute('data-sizes', el.getAttribute('data-sizes')); 5723 } 5724 5725 // Add image to figure 5726 FIGURE.appendChild(IMAGE); 5727 5728 // Create figcaption 5729 let captionContent; 5730 if (typeof this.userSettings.captionText === 'function') { 5731 captionContent = this.userSettings.captionText(el); 5732 } else if (this.userSettings.captionsSelector === 'self' && el.hasAttribute(this.userSettings.captionAttribute)) { 5733 captionContent = el.getAttribute(this.userSettings.captionAttribute); 5734 } else if (this.userSettings.captionsSelector === 'img' && THUMBNAIL && THUMBNAIL.hasAttribute(this.userSettings.captionAttribute)) { 5735 captionContent = THUMBNAIL.getAttribute(this.userSettings.captionAttribute); 5736 } 5737 if (this.userSettings.captions && captionContent) { 5738 const FIGCAPTION = document.createElement('figcaption'); 5739 FIGCAPTION.id = `tobii-figcaption-${this.figcaptionId}`; 5740 const SPAN = document.createElement('span'); 5741 if (this.userSettings.captionHTML) { 5742 SPAN.innerHTML = captionContent; 5743 } else { 5744 SPAN.textContent = captionContent; 5745 } 5746 FIGCAPTION.appendChild(SPAN); 5747 if (this.userSettings.captionToggle) { 5748 const isMobile = window.innerWidth < 768; 5749 const BUTTON = document.createElement('button'); 5750 BUTTON.className = 'caption-toggle'; 5751 BUTTON.textContent = BUTTON.title = this.userSettings.captionToggleLabel[isMobile ? 1 : 0]; 5752 BUTTON.setAttribute('aria-controls', FIGCAPTION.id); 5753 BUTTON.setAttribute('aria-expanded', !isMobile); 5754 if (isMobile) { 5755 FIGCAPTION.classList.add('caption-hidden'); 5756 } 5757 SPAN.setAttribute('aria-hidden', isMobile); 5758 const preventAndStopEvent = event => { 5759 event.preventDefault(); 5760 event.stopPropagation(); 5761 }; 5762 BUTTON.addEventListener('pointerdown', event => preventAndStopEvent(event)); 5763 BUTTON.addEventListener('pointerup', event => preventAndStopEvent(event)); 5764 BUTTON.addEventListener('click', event => { 5765 preventAndStopEvent(event); 5766 const isExpanded = BUTTON.getAttribute('aria-expanded') === 'true'; 5767 const buttonLabel = isExpanded ? this.userSettings.captionToggleLabel[1] : this.userSettings.captionToggleLabel[0]; 5768 BUTTON.textContent = BUTTON.title = buttonLabel; 5769 BUTTON.setAttribute('aria-expanded', !isExpanded); 5770 FIGCAPTION.classList.toggle('caption-hidden'); 5771 SPAN.setAttribute('aria-hidden', isExpanded); 5772 }); 5773 FIGCAPTION.appendChild(BUTTON); 5774 } 5775 FIGURE.appendChild(FIGCAPTION); 5776 IMAGE.setAttribute('aria-labelledby', FIGCAPTION.id); 5777 5778 // Add aria-label to the figure containing the caption content 5779 FIGURE.setAttribute('aria-label', SPAN.textContent); 5780 ++this.figcaptionId; 5781 } 5782 5783 // Add figure to container 5784 container.appendChild(FIGURE); 5785 5786 // Create loading indicator 5787 LOADING_INDICATOR.className = 'tobii__loader'; 5788 LOADING_INDICATOR.setAttribute('role', 'progressbar'); 5789 LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel); 5790 5791 // Add loading indicator to container 5792 container.appendChild(LOADING_INDICATOR); 5793 5794 // Register type 5795 container.setAttribute('data-type', 'image'); 5796 container.classList.add('tobii-image'); 5797 } 5798 onPreload(container) { 5799 // Same as preload 5800 this.onLoad(container); 5801 } 5802 onLoad(container) { 5803 const IMAGE = container.querySelector('img'); 5804 if (!IMAGE.hasAttribute('data-src')) { 5805 return; 5806 } 5807 const FIGURE = container.querySelector('figure'); 5808 const LOADING_INDICATOR = container.querySelector('.tobii__loader'); 5809 const handleImageEvent = () => { 5810 container.removeChild(LOADING_INDICATOR); 5811 FIGURE.style.opacity = '1'; 5812 }; 5813 IMAGE.addEventListener('load', handleImageEvent); 5814 IMAGE.addEventListener('error', handleImageEvent); 5815 if (IMAGE.hasAttribute('data-srcset')) { 5816 IMAGE.setAttribute('srcset', IMAGE.getAttribute('data-srcset')); 5817 IMAGE.removeAttribute('data-srcset'); 5818 } 5819 if (IMAGE.hasAttribute('data-sizes')) { 5820 IMAGE.setAttribute('sizes', IMAGE.getAttribute('data-sizes')); 5821 IMAGE.removeAttribute('data-sizes'); 5822 } 5823 IMAGE.setAttribute('src', IMAGE.getAttribute('data-src')); 5824 IMAGE.removeAttribute('data-src'); 5825 } 5826 onLeave(container) { 5827 // Nothing 5828 } 5829 onCleanup(container) { 5830 // Nothing 5831 } 5832 onReset() { 5833 this.figcaptionId = 0; 5834 } 5835} 5836 5837class IframeType { 5838 constructor() { 5839 this.userSettings = null; 5840 } 5841 init(el, container, userSettings) { 5842 this.userSettings = userSettings; 5843 const HREF = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href'); 5844 container.setAttribute('data-HREF', HREF); 5845 if (el.hasAttribute('data-allow')) { 5846 container.setAttribute('data-allow', el.getAttribute('data-allow')); 5847 } 5848 if (el.hasAttribute('data-width')) { 5849 container.setAttribute('data-width', `${el.getAttribute('data-width')}`); 5850 } 5851 if (el.hasAttribute('data-height')) { 5852 container.setAttribute('data-height', `${el.getAttribute('data-height')}`); 5853 } 5854 5855 // dont create empty iframes here - very slow 5856 5857 // Register type 5858 container.setAttribute('data-type', 'iframe'); 5859 container.classList.add('tobii-iframe'); 5860 } 5861 onPreload(container) { 5862 // Nothing 5863 } 5864 onLoad(container) { 5865 let IFRAME = container.querySelector('iframe'); 5866 5867 // Create loading indicator 5868 const LOADING_INDICATOR = document.createElement('div'); 5869 LOADING_INDICATOR.className = 'tobii__loader'; 5870 LOADING_INDICATOR.setAttribute('role', 'progressbar'); 5871 LOADING_INDICATOR.setAttribute('aria-label', this.userSettings.loadingIndicatorLabel); 5872 container.appendChild(LOADING_INDICATOR); 5873 if (IFRAME == null) { 5874 // create iframe 5875 IFRAME = document.createElement('iframe'); 5876 const HREF = container.getAttribute('data-href'); 5877 IFRAME.setAttribute('frameborder', '0'); 5878 IFRAME.setAttribute('src', HREF); 5879 5880 // Set allow parameters 5881 let allowValue = 'fullscreen'; 5882 if (HREF.includes('youtube.com')) { 5883 allowValue += '; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'; 5884 } else if (HREF.includes('vimeo.com')) { 5885 allowValue += '; autoplay; picture-in-picture'; 5886 } else if (container.hasAttribute('data-allow')) { 5887 allowValue = container.getAttribute('data-allow'); 5888 } 5889 IFRAME.setAttribute('allow', allowValue); 5890 if (container.hasAttribute('data-width')) { 5891 IFRAME.style.maxWidth = `${container.getAttribute('data-width')}`; 5892 } 5893 if (container.hasAttribute('data-height')) { 5894 IFRAME.style.maxHeight = `${container.getAttribute('data-height')}`; 5895 } 5896 5897 // Hide until loaded 5898 IFRAME.style.opacity = '0'; 5899 5900 // Add iframe to container 5901 container.appendChild(IFRAME); 5902 5903 // Handle load and error 5904 const removeLoader = () => { 5905 IFRAME.style.opacity = '1'; 5906 const LOADING_INDICATOR = container.querySelector('.tobii__loader'); 5907 if (LOADING_INDICATOR) container.removeChild(LOADING_INDICATOR); 5908 }; 5909 IFRAME.addEventListener('load', removeLoader); 5910 IFRAME.addEventListener('error', removeLoader); 5911 } else { 5912 // was already created 5913 IFRAME.setAttribute('src', container.getAttribute('data-href')); 5914 } 5915 } 5916 onLeave(container) { 5917 // Nothing 5918 } 5919 onCleanup(container) { 5920 const IFRAME = container.querySelector('iframe'); 5921 IFRAME.removeAttribute('src'); 5922 IFRAME.style.opacity = '0'; 5923 } 5924 onReset() { 5925 // Nothing 5926 } 5927} 5928 5929class HtmlType { 5930 constructor() { 5931 this.userSettings = null; 5932 } 5933 init(el, container, userSettings) { 5934 this.userSettings = userSettings; 5935 const TARGET_SELECTOR = el.hasAttribute('data-target') ? el.getAttribute('data-target') : el.getAttribute('href'); 5936 const TARGET = document.querySelector(TARGET_SELECTOR); 5937 if (!TARGET) { 5938 throw new Error(`Ups, I can't find the target ${TARGET_SELECTOR}.`); 5939 } 5940 5941 // Add content to container 5942 container.appendChild(TARGET); 5943 5944 // Register type 5945 container.setAttribute('data-type', 'html'); 5946 container.classList.add('tobii-html'); 5947 } 5948 onPreload(container) { 5949 // Nothing 5950 } 5951 onLoad(container, group) { 5952 const VIDEO = container.querySelector('video'); 5953 if (VIDEO) { 5954 if (VIDEO.hasAttribute('data-time') && VIDEO.readyState > 0) { 5955 // Continue where video was stopped 5956 VIDEO.currentTime = VIDEO.getAttribute('data-time'); 5957 } 5958 5959 // Start playback (and loading if necessary) 5960 VIDEO.play(); 5961 } 5962 const audio = container.querySelector('audio'); 5963 if (audio) { 5964 // Start playback (and loading if necessary) 5965 audio.play(); 5966 } 5967 container.classList.add('tobii-group-' + group); 5968 } 5969 onLeave(container) { 5970 const VIDEO = container.querySelector('video'); 5971 if (VIDEO) { 5972 if (!VIDEO.paused) { 5973 // Stop if video is playing 5974 VIDEO.pause(); 5975 } 5976 5977 // Backup currentTime (needed for revisit) 5978 if (VIDEO.readyState > 0) { 5979 VIDEO.setAttribute('data-time', VIDEO.currentTime); 5980 } 5981 } 5982 const audio = container.querySelector('audio'); 5983 if (audio) { 5984 if (!audio.paused) { 5985 // Stop if is playing 5986 audio.pause(); 5987 } 5988 } 5989 } 5990 onCleanup(container) { 5991 const VIDEO = container.querySelector('video'); 5992 if (VIDEO) { 5993 if (VIDEO.readyState > 0 && VIDEO.readyState < 3 && VIDEO.duration !== VIDEO.currentTime) { 5994 // Some data has been loaded but not the whole package. 5995 // In order to save bandwidth, stop downloading as soon as possible. 5996 const VIDEO_CLONE = VIDEO.cloneNode(true); 5997 this._removeSources(VIDEO); 5998 VIDEO.load(); 5999 VIDEO.parentNode.removeChild(VIDEO);
vendor: 14,366 bytes, lines 6000-6437
6000 container.appendChild(VIDEO_CLONE); 6001 } 6002 } 6003 } 6004 onReset() { 6005 // Nothing 6006 } 6007 6008 /** 6009 * Remove all `src` attributes 6010 * 6011 * @param {HTMLElement} el - Element to remove all `src` attributes 6012 */ 6013 _removeSources(el) { 6014 const SOURCES = el.querySelectorAll('src'); 6015 if (SOURCES) { 6016 SOURCES.forEach(source => { 6017 source.removeAttribute('src'); 6018 }); 6019 } 6020 } 6021} 6022 6023class YoutubeType { 6024 constructor() { 6025 this.playerId = 0; 6026 this.PLAYER = []; 6027 this.userSettings = null; 6028 } 6029 init(el, container, userSettings) { 6030 this.userSettings = userSettings; 6031 const IFRAME_PLACEHOLDER = document.createElement('div'); 6032 6033 // Add iframePlaceholder to container 6034 container.appendChild(IFRAME_PLACEHOLDER); 6035 this.PLAYER[this.playerId] = new window.YT.Player(IFRAME_PLACEHOLDER, { 6036 host: 'https://www.youtube-nocookie.com', 6037 height: el.getAttribute('data-height') || '360', 6038 width: el.getAttribute('data-width') || '640', 6039 videoId: el.getAttribute('data-id'), 6040 playerVars: { 6041 controls: el.getAttribute('data-controls') || 1, 6042 rel: 0, 6043 playsinline: 1 6044 } 6045 }); 6046 6047 // Set player ID 6048 container.setAttribute('data-player', this.playerId); 6049 6050 // Register type 6051 container.setAttribute('data-type', 'youtube'); 6052 container.classList.add('tobii-youtube'); 6053 this.playerId++; 6054 } 6055 onPreload(container) { 6056 // Nothing 6057 } 6058 onLoad(container) { 6059 this.PLAYER[container.getAttribute('data-player')].playVideo(); 6060 } 6061 onLeave(container) { 6062 if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) { 6063 this.PLAYER[container.getAttribute('data-player')].pauseVideo(); 6064 } 6065 } 6066 onCleanup(container) { 6067 if (this.PLAYER[container.getAttribute('data-player')].getPlayerState() === 1) { 6068 this.PLAYER[container.getAttribute('data-player')].pauseVideo(); 6069 } 6070 } 6071 onReset() { 6072 // Nothing 6073 } 6074} 6075 6076function Tobii(userOptions) { 6077 /** 6078 * Global variables 6079 * 6080 */ 6081 const SUPPORTED_ELEMENTS = { 6082 image: new ImageType(), 6083 // default 6084 html: new HtmlType(), 6085 iframe: new IframeType(), 6086 youtube: new YoutubeType() 6087 }; 6088 const FOCUSABLE_ELEMENTS = ['a[href]:not([tabindex^="-"]):not([inert])', 'area[href]:not([tabindex^="-"]):not([inert])', 'input:not([disabled]):not([inert])', 'select:not([disabled]):not([inert])', 'textarea:not([disabled]):not([inert])', 'button:not([disabled]):not([inert])', 'iframe:not([tabindex^="-"]):not([inert])', 'audio:not([tabindex^="-"]):not([inert])', 'video:not([tabindex^="-"]):not([inert])', '[contenteditable]:not([tabindex^="-"]):not([inert])', '[tabindex]:not([tabindex^="-"]):not([inert])']; 6089 let userSettings = {}; 6090 const WAITING_ELS = []; 6091 const GROUP_ATTS = { 6092 gallery: [], 6093 slider: null, 6094 sliderElements: [], 6095 elementsLength: 0, 6096 currentIndex: 0, 6097 x: 0 6098 }; 6099 let lightbox = null; 6100 let prevButton = null; 6101 let nextButton = null; 6102 let closeButton = null; 6103 let counter = null; 6104 let lastFocus = null; 6105 let offset = null; 6106 let isYouTubeDependencyLoaded = false; 6107 let groups = {}; 6108 let activeGroup = null; 6109 let pointerDownCache = []; 6110 let lastTapTime = 0; 6111 let liveRegion = null; 6112 const MIN_SCALE = 1; 6113 const MAX_SCALE = 4; 6114 const DOUBLE_TAP_TIME = 500; // milliseconds 6115 const SCALE_SENSITIVITY = 10; 6116 const TRANSFORM = { 6117 element: null, 6118 originX: 0, 6119 originY: 0, 6120 translateX: 0, 6121 translateY: 0, 6122 scale: MIN_SCALE 6123 }; 6124 const DRAG = { 6125 startX: 0, 6126 startY: 0, 6127 x: 0, 6128 y: 0, 6129 distance: 0 6130 }; 6131 6132 /** 6133 * Merge default options with user options 6134 * 6135 * @param {Object} userOptions - Optional user options 6136 * @returns {Object} - Custom options 6137 */ 6138 const mergeOptions = userOptions => { 6139 // Default options 6140 const OPTIONS = { 6141 selector: '.lightbox', 6142 captions: true, 6143 captionsSelector: 'img', 6144 captionAttribute: 'alt', 6145 captionText: null, 6146 captionHTML: false, 6147 captionToggle: true, 6148 captionToggleLabel: ['Hide caption', 'Show caption'], 6149 nav: 'auto', 6150 navText: ['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke="none" d="M0 0h24v24H0z"/><polyline points="15 6 9 12 15 18" /></svg>', '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke="none" d="M0 0h24v24H0z"/><polyline points="9 6 15 12 9 18" /></svg>'], 6151 navLabel: ['Previous image', 'Next image'], 6152 announcementLabel: ['Slide', 'of'], 6153 close: true, 6154 closeText: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke="none" d="M0 0h24v24H0z"/><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>', 6155 closeLabel: 'Close lightbox', 6156 dialogTitle: 'Lightbox', 6157 loadingIndicatorLabel: 'Image loading', 6158 counter: true, 6159 keyboard: true, 6160 zoom: false, 6161 zoomText: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path stroke="none" d="M0 0h24v24H0z"/><polyline points="16 4 20 4 20 8" /><line x1="14" y1="10" x2="20" y2="4" /><polyline points="8 20 4 20 4 16" /><line x1="4" y1="20" x2="10" y2="14" /><polyline points="16 20 20 20 20 16" /><line x1="14" y1="14" x2="20" y2="20" /><polyline points="8 4 4 4 4 8" /><line x1="4" y1="4" x2="10" y2="10" /></svg>', 6162 docClose: true, 6163 swipeClose: true, 6164 hideScrollbar: true, 6165 draggable: true, 6166 threshold: 100, 6167 theme: 'tobii--theme-default' 6168 }; 6169 return _extends({}, OPTIONS, userOptions); 6170 }; 6171 6172 /** 6173 * Init 6174 * 6175 */ 6176 const init = userOptions => { 6177 // Merge user options into defaults 6178 userSettings = mergeOptions(userOptions); 6179 6180 // Create the lightbox container 6181 lightbox = document.createElement('div'); 6182 lightbox.setAttribute('role', 'dialog'); 6183 lightbox.setAttribute('aria-hidden', 'true'); 6184 lightbox.setAttribute('aria-modal', 'true'); 6185 lightbox.setAttribute('aria-label', userSettings.dialogTitle); 6186 lightbox.classList.add('tobii'); 6187 6188 // Add theme class 6189 lightbox.classList.add(userSettings.theme); 6190 6191 // Create the previous button 6192 prevButton = document.createElement('button'); 6193 prevButton.className = 'tobii__btn tobii__btn--previous'; 6194 prevButton.setAttribute('type', 'button'); 6195 prevButton.setAttribute('aria-label', userSettings.navLabel[0]); 6196 prevButton.innerHTML = userSettings.navText[0]; 6197 lightbox.appendChild(prevButton); 6198 6199 // Create the next button 6200 nextButton = document.createElement('button'); 6201 nextButton.className = 'tobii__btn tobii__btn--next'; 6202 nextButton.setAttribute('type', 'button'); 6203 nextButton.setAttribute('aria-label', userSettings.navLabel[1]); 6204 nextButton.innerHTML = userSettings.navText[1]; 6205 lightbox.appendChild(nextButton); 6206 6207 // Create the close button 6208 closeButton = document.createElement('button'); 6209 closeButton.className = 'tobii__btn tobii__btn--close'; 6210 closeButton.setAttribute('type', 'button'); 6211 closeButton.setAttribute('aria-label', userSettings.closeLabel); 6212 closeButton.innerHTML = userSettings.closeText; 6213 lightbox.appendChild(closeButton); 6214 6215 // Create the counter 6216 counter = document.createElement('div'); 6217 counter.className = 'tobii__counter'; 6218 lightbox.appendChild(counter); 6219 6220 // Create the live region 6221 liveRegion = document.createElement('div'); 6222 liveRegion.className = 'tobii__sr'; 6223 liveRegion.setAttribute('aria-live', 'polite'); 6224 liveRegion.setAttribute('aria-atomic', 'true'); 6225 lightbox.appendChild(liveRegion); 6226 6227 // Append to body 6228 document.body.appendChild(lightbox); 6229 6230 // Init only 6231 if (!userSettings.selector) return; 6232 6233 // Get a list of all elements within the document 6234 const LIGHTBOX_TRIGGER_ELS = document.querySelectorAll(userSettings.selector); 6235 if (!LIGHTBOX_TRIGGER_ELS) { 6236 throw new Error(`Ups, I can't find the selector ${userSettings.selector} on this website.`); 6237 } 6238 LIGHTBOX_TRIGGER_ELS.forEach(el => checkDependencies(el)); 6239 }; 6240 6241 /** 6242 * Check dependencies 6243 * 6244 * @param {HTMLElement} el - Element to add 6245 */ 6246 const checkDependencies = el => { 6247 // Check if there is a YouTube video and if the YouTube iframe-API is ready 6248 if (document.querySelector('[data-type="youtube"]') !== null && !isYouTubeDependencyLoaded) { 6249 if (document.getElementById('iframe_api') === null) { 6250 const TAG = document.createElement('script'); 6251 const FIRST_SCRIPT_TAG = document.getElementsByTagName('script')[0]; 6252 TAG.id = 'iframe_api'; 6253 TAG.src = 'https://www.youtube.com/iframe_api'; 6254 FIRST_SCRIPT_TAG.parentNode.insertBefore(TAG, FIRST_SCRIPT_TAG); 6255 } 6256 if (WAITING_ELS.indexOf(el) === -1) { 6257 WAITING_ELS.push(el); 6258 } 6259 window.onYouTubePlayerAPIReady = () => { 6260 WAITING_ELS.forEach(waitingEl => { 6261 add(waitingEl); 6262 }); 6263 isYouTubeDependencyLoaded = true; 6264 }; 6265 } else { 6266 add(el); 6267 } 6268 }; 6269 6270 /** 6271 * Get group name from element 6272 * 6273 * @param {HTMLElement} el 6274 * @return {string} 6275 */ 6276 const getGroupName = el => { 6277 return el.hasAttribute('data-group') ? el.getAttribute('data-group') : 'default'; 6278 }; 6279 6280 /** 6281 * Copy an object. (The secure way) 6282 * 6283 * @param {object} object 6284 * @return {object} 6285 */ 6286 const copyObject = object => { 6287 return JSON.parse(JSON.stringify(object)); 6288 }; 6289 6290 /** 6291 * Add element 6292 * 6293 * @param {HTMLElement} el - Element to add 6294 */ 6295 const add = el => { 6296 const newGroup = getGroupName(el); 6297 if (!Object.prototype.hasOwnProperty.call(groups, newGroup)) { 6298 groups[newGroup] = copyObject(GROUP_ATTS); 6299 6300 // Create slider 6301 groups[newGroup].slider = document.createElement('div'); 6302 groups[newGroup].slider.className = 'tobii__slider'; 6303 6304 // Hide slider 6305 groups[newGroup].slider.setAttribute('aria-hidden', 'true'); 6306 lightbox.appendChild(groups[newGroup].slider); 6307 } 6308 6309 // Check if element already exists 6310 if (groups[newGroup].gallery.indexOf(el) === -1) { 6311 groups[newGroup].gallery.push(el); 6312 groups[newGroup].elementsLength++; 6313 6314 // Set zoom icon if necessary 6315 if (userSettings.zoom && el.querySelector('img') && el.getAttribute('data-zoom') !== 'false' || el.getAttribute('data-zoom') === 'true') { 6316 const TOBII_ZOOM = document.createElement('div'); 6317 TOBII_ZOOM.className = 'tobii-zoom__icon'; 6318 TOBII_ZOOM.innerHTML = userSettings.zoomText; 6319 el.classList.add('tobii-zoom'); 6320 el.appendChild(TOBII_ZOOM); 6321 } 6322 6323 // Bind click event handler 6324 el.addEventListener('click', triggerTobii); 6325 6326 // Create slide 6327 const SLIDE_ELEMENT = document.createElement('div'); 6328 const SLIDE_ELEMENT_CONTENT = document.createElement('div'); 6329 SLIDE_ELEMENT.className = 'tobii__slide'; 6330 SLIDE_ELEMENT.style.position = 'absolute'; 6331 SLIDE_ELEMENT.style.left = `${groups[newGroup].x * 100}%`; 6332 6333 // Hide slide 6334 SLIDE_ELEMENT.setAttribute('aria-hidden', 'true'); 6335 6336 // Create type elements 6337 const model = getModel(el); 6338 model.init(el, SLIDE_ELEMENT_CONTENT, userSettings); 6339 6340 // Add slide content container to slide element 6341 SLIDE_ELEMENT.appendChild(SLIDE_ELEMENT_CONTENT); 6342 6343 // Add slide element to slider 6344 groups[newGroup].slider.appendChild(SLIDE_ELEMENT); 6345 groups[newGroup].sliderElements.push(SLIDE_ELEMENT); 6346 ++groups[newGroup].x; 6347 if (isOpen() && newGroup === activeGroup) { 6348 updateConfig(); 6349 updateLightbox(); 6350 } 6351 } else { 6352 throw new Error('Ups, element already added.'); 6353 } 6354 }; 6355 6356 /** 6357 * Remove element 6358 * 6359 * @param {HTMLElement} el - Element to remove 6360 */ 6361 const remove = el => { 6362 const GROUP_NAME = getGroupName(el); 6363 6364 // Check if element exists 6365 const galleryIndex = groups[GROUP_NAME].gallery.indexOf(el); 6366 if (galleryIndex === -1) { 6367 throw new Error(`Ups, I can't find a slide for the element ${el}.`); 6368 } 6369 const SLIDE_ELEMENT = groups[GROUP_NAME].sliderElements[galleryIndex]; 6370 6371 // If the element to be removed is the currently visible slide 6372 if (isOpen() && GROUP_NAME === activeGroup && galleryIndex === groups[GROUP_NAME].currentIndex) { 6373 if (groups[GROUP_NAME].elementsLength === 1) { 6374 close(); 6375 throw new Error('Ups, I\'ve closed. There are no slides more to show.'); 6376 } else { 6377 // Navigate away before removal 6378 if (groups[GROUP_NAME].currentIndex === 0) { 6379 next(); 6380 } else { 6381 previous(); 6382 } 6383 updateConfig(); 6384 updateLightbox(); 6385 } 6386 } 6387 groups[GROUP_NAME].gallery.splice(galleryIndex, 1); 6388 groups[GROUP_NAME].sliderElements.splice(galleryIndex, 1); 6389 groups[GROUP_NAME].elementsLength--; 6390 --groups[GROUP_NAME].x; 6391 6392 // Remove zoom icon if necessary 6393 if (userSettings.zoom && el.querySelector('.tobii-zoom__icon')) { 6394 const ZOOM_ICON = el.querySelector('.tobii-zoom__icon'); 6395 ZOOM_ICON.parentNode.classList.remove('tobii-zoom'); 6396 ZOOM_ICON.parentNode.removeChild(ZOOM_ICON); 6397 } 6398 6399 // Unbind click event handler 6400 el.removeEventListener('click', triggerTobii); 6401 6402 // Remove slide 6403 SLIDE_ELEMENT.parentNode.removeChild(SLIDE_ELEMENT); 6404 }; 6405 const getModel = el => { 6406 const type = el.getAttribute('data-type'); 6407 if (SUPPORTED_ELEMENTS[type] !== undefined) { 6408 return SUPPORTED_ELEMENTS[type]; 6409 } else { 6410 // unknown - use default 6411 if (el.hasAttribute('data-type')) { 6412 console.log('Unknown lightbox element type: ' + type); 6413 } 6414 return SUPPORTED_ELEMENTS.image; 6415 } 6416 }; 6417 6418 /** 6419 * Open Tobii 6420 * 6421 * @param {number} index - Index to load 6422 */ 6423 const open = (index = 0) => { 6424 if (isOpen()) { 6425 throw new Error('Ups, I\'m aleady open.'); 6426 } 6427 if (index === -1 || index >= groups[activeGroup].elementsLength) { 6428 throw new Error(`Ups, I can't find slide ${index}.`); 6429 } 6430 document.documentElement.classList.add('tobii-is-open'); 6431 document.body.classList.add('tobii-is-open'); 6432 document.body.classList.add('tobii-is-open-' + activeGroup); 6433 updateConfig(); 6434 6435 // Hide close if necessary 6436 if (!userSettings.close) { 6437 closeButton.disabled = false;
vendor: 12,804 bytes, lines 6438-6884
6438 closeButton.setAttribute('aria-hidden', 'true'); 6439 } 6440 6441 // Save userâs focus 6442 lastFocus = document.activeElement; 6443 6444 // Use `history.pushState()` to make sure the "Back" button behavior 6445 // that aligns with the user's expectations 6446 const stateObj = { 6447 tobii: 'close' 6448 }; 6449 const url = window.location.href; 6450 window.history.pushState(stateObj, 'Image', url); 6451 6452 // Set current index 6453 groups[activeGroup].currentIndex = index; 6454 bindEvents(); 6455 6456 // Load slide 6457 load(groups[activeGroup].currentIndex); 6458 6459 // Show slider 6460 groups[activeGroup].slider.setAttribute('aria-hidden', 'false'); 6461 6462 // Show lightbox 6463 lightbox.setAttribute('aria-hidden', 'false'); 6464 updateLightbox(); 6465 6466 // Preload previous and next slide 6467 preload(groups[activeGroup].currentIndex + 1); 6468 preload(groups[activeGroup].currentIndex - 1); 6469 groups[activeGroup].slider.classList.add('tobii__slider--animate'); 6470 6471 // Create and dispatch a new event 6472 const openEvent = new window.CustomEvent('open', { 6473 detail: { 6474 group: activeGroup 6475 } 6476 }); 6477 lightbox.dispatchEvent(openEvent); 6478 }; 6479 6480 /** 6481 * Close Tobii 6482 * 6483 */ 6484 const close = () => { 6485 if (!isOpen()) { 6486 throw new Error('Ups, I\'m already closed.'); 6487 } 6488 document.documentElement.classList.remove('tobii-is-open'); 6489 document.body.classList.remove('tobii-is-open'); 6490 document.body.classList.remove('tobii-is-open-' + activeGroup); 6491 unbindEvents(); 6492 6493 // Remove entry in browser history 6494 if (window.history.state !== null) { 6495 if (window.history.state.tobii === 'close') { 6496 window.history.back(); 6497 } 6498 } 6499 6500 // Reenable the userâs focus 6501 lastFocus.focus(); 6502 6503 // Don't forget to cleanup our current element 6504 leave(groups[activeGroup].currentIndex); 6505 cleanup(groups[activeGroup].currentIndex); 6506 6507 // Hide lightbox 6508 lightbox.setAttribute('aria-hidden', 'true'); 6509 6510 // Hide slider 6511 groups[activeGroup].slider.setAttribute('aria-hidden', 'true'); 6512 6513 // Reset current index 6514 groups[activeGroup].currentIndex = 0; 6515 6516 // Remove the hack to prevent animation during opening 6517 groups[activeGroup].slider.classList.remove('tobii__slider--animate'); 6518 6519 // Create and dispatch a new event 6520 const closeEvent = new window.CustomEvent('close', { 6521 detail: { 6522 group: activeGroup 6523 } 6524 }); 6525 lightbox.dispatchEvent(closeEvent); 6526 }; 6527 6528 /** 6529 * Preload slide 6530 * 6531 * @param {number} index - Index to preload 6532 */ 6533 const preload = index => { 6534 if (groups[activeGroup].sliderElements[index] === undefined) { 6535 return; 6536 } 6537 const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]'); 6538 const model = getModel(CONTAINER); 6539 model.onPreload(CONTAINER); 6540 }; 6541 6542 /** 6543 * Load slide 6544 * Will be called when opening the lightbox or moving index 6545 * 6546 * @param {number} index - Index to load 6547 */ 6548 const load = index => { 6549 if (groups[activeGroup].sliderElements[index] === undefined) { 6550 return; 6551 } 6552 const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]'); 6553 const model = getModel(CONTAINER); 6554 model.onLoad(CONTAINER, activeGroup); 6555 }; 6556 6557 /** 6558 * Select a slide 6559 * 6560 * @param {number} index - Index to select 6561 */ 6562 const select = index => { 6563 const currIndex = groups[activeGroup].currentIndex; 6564 if (!isOpen()) { 6565 throw new Error('Ups, I\'m closed.'); 6566 } 6567 if (isOpen()) { 6568 if (!index && index !== 0) { 6569 throw new Error('Ups, no slide specified.'); 6570 } 6571 if (index === groups[activeGroup].currentIndex) { 6572 throw new Error(`Ups, slide ${index} is already selected.`); 6573 } 6574 if (index === -1 || index >= groups[activeGroup].elementsLength) { 6575 throw new Error(`Ups, I can't find slide ${index}.`); 6576 } 6577 } 6578 6579 // Set current index 6580 groups[activeGroup].currentIndex = index; 6581 leave(currIndex); 6582 load(index); 6583 if (index < currIndex) { 6584 updateLightbox('left'); 6585 cleanup(currIndex); 6586 preload(index - 1); 6587 } 6588 if (index > currIndex) { 6589 updateLightbox('right'); 6590 cleanup(currIndex); 6591 preload(index + 1); 6592 } 6593 }; 6594 6595 /** 6596 * Select the previous slide 6597 * 6598 */ 6599 const previous = () => { 6600 if (!isOpen()) { 6601 throw new Error('Ups, I\'m closed.'); 6602 } 6603 if (groups[activeGroup].currentIndex > 0) { 6604 leave(groups[activeGroup].currentIndex); 6605 load(--groups[activeGroup].currentIndex); 6606 updateLightbox('left'); 6607 cleanup(groups[activeGroup].currentIndex + 1); 6608 preload(groups[activeGroup].currentIndex - 1); 6609 } 6610 6611 // Create and dispatch a new event 6612 const previousEvent = new window.CustomEvent('previous', { 6613 detail: { 6614 group: activeGroup 6615 } 6616 }); 6617 lightbox.dispatchEvent(previousEvent); 6618 }; 6619 6620 /** 6621 * Select the next slide 6622 * 6623 */ 6624 const next = () => { 6625 if (!isOpen()) { 6626 throw new Error('Ups, I\'m closed.'); 6627 } 6628 if (groups[activeGroup].currentIndex < groups[activeGroup].elementsLength - 1) { 6629 leave(groups[activeGroup].currentIndex); 6630 load(++groups[activeGroup].currentIndex); 6631 updateLightbox('right'); 6632 cleanup(groups[activeGroup].currentIndex - 1); 6633 preload(groups[activeGroup].currentIndex + 1); 6634 } 6635 6636 // Create and dispatch a new event 6637 const nextEvent = new window.CustomEvent('next', { 6638 detail: { 6639 group: activeGroup 6640 } 6641 }); 6642 lightbox.dispatchEvent(nextEvent); 6643 }; 6644 6645 /** 6646 * Select a group 6647 * 6648 * @param {string} name - Name of the group to select 6649 */ 6650 const selectGroup = name => { 6651 if (isOpen()) { 6652 throw new Error('Ups, I\'m open.'); 6653 } 6654 if (!name) { 6655 throw new Error('Ups, no group specified.'); 6656 } 6657 if (name && !Object.prototype.hasOwnProperty.call(groups, name)) { 6658 throw new Error(`Ups, I don't have a group called "${name}".`); 6659 } 6660 activeGroup = name; 6661 }; 6662 6663 /** 6664 * Leave slide 6665 * Will be called before moving index 6666 * 6667 * @param {number} index - Index to leave 6668 */ 6669 const leave = index => { 6670 if (groups[activeGroup].sliderElements[index] === undefined) { 6671 return; 6672 } 6673 const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]'); 6674 const model = getModel(CONTAINER); 6675 model.onLeave(CONTAINER); 6676 }; 6677 6678 /** 6679 * Cleanup slide 6680 * Will be called after moving index 6681 * 6682 * @param {number} index - Index to cleanup 6683 */ 6684 const cleanup = index => { 6685 if (groups[activeGroup].sliderElements[index] === undefined) { 6686 return; 6687 } 6688 const CONTAINER = groups[activeGroup].sliderElements[index].querySelector('[data-type]'); 6689 const model = getModel(CONTAINER); 6690 model.onCleanup(CONTAINER); 6691 DRAG.startX = 0; 6692 DRAG.startY = 0; 6693 DRAG.x = 0; 6694 DRAG.y = 0; 6695 DRAG.distance = 0; 6696 lastTapTime = 0; 6697 if (isZoomed()) resetZoom(); 6698 TRANSFORM.element = null; 6699 }; 6700 6701 /** 6702 * Update offset 6703 * 6704 */ 6705 const updateOffset = () => { 6706 offset = -groups[activeGroup].currentIndex * lightbox.offsetWidth; 6707 groups[activeGroup].slider.style.transform = `translate(${offset}px, 0)`; 6708 }; 6709 6710 /** 6711 * Update counter 6712 * 6713 */ 6714 const updateCounter = () => { 6715 counter.innerHTML = `<p>${groups[activeGroup].currentIndex + 1}/${groups[activeGroup].elementsLength}</p>`; 6716 }; 6717 6718 /** 6719 * Update focus 6720 * 6721 * @param {string|null} dir - Current slide direction 6722 */ 6723 const updateFocus = dir => { 6724 const group = groups[activeGroup]; 6725 const isNavEnabled = userSettings.nav === true || userSettings.nav === 'auto'; 6726 const hasMultipleSlides = group.elementsLength > 1; 6727 if (isNavEnabled && !isTouchDevice() && hasMultipleSlides) { 6728 setButtonState(prevButton, true, true); 6729 setButtonState(nextButton, true, true); 6730 if (group.currentIndex === 0) { 6731 setButtonState(nextButton, false, false); 6732 nextButton.focus(); 6733 } else if (group.currentIndex === group.elementsLength - 1) { 6734 setButtonState(prevButton, false, false); 6735 prevButton.focus(); 6736 } else { 6737 setButtonState(prevButton, false, false); 6738 setButtonState(nextButton, false, false); 6739 if (dir === 'left') { 6740 prevButton.focus(); 6741 } else { 6742 nextButton.focus(); 6743 } 6744 } 6745 } else if (userSettings.close) { 6746 closeButton.focus(); 6747 } 6748 }; 6749 6750 /** 6751 * Resize event 6752 * 6753 */ 6754 const resizeHandler = () => { 6755 updateOffset(); 6756 }; 6757 6758 /** 6759 * Click event handler to trigger Tobii 6760 * 6761 */ 6762 const triggerTobii = event => { 6763 event.preventDefault(); 6764 activeGroup = getGroupName(event.currentTarget); 6765 open(groups[activeGroup].gallery.indexOf(event.currentTarget)); 6766 }; 6767 6768 /** 6769 * Click event handler 6770 * 6771 */ 6772 const clickHandler = event => { 6773 if (event.target === prevButton) { 6774 previous(); 6775 } else if (event.target === nextButton) { 6776 next(); 6777 } else if (event.target === closeButton || event.target.classList.contains('tobii__slide') || event.target.classList.contains('tobii') && userSettings.docClose) { 6778 close(); 6779 } 6780 event.stopPropagation(); 6781 }; 6782 6783 /** 6784 * Set the hidden/disabled state of a button 6785 * 6786 */ 6787 const setButtonState = (button, hidden, disabled) => { 6788 button.setAttribute('aria-hidden', hidden ? 'true' : 'false'); 6789 button.disabled = disabled; 6790 }; 6791 6792 /** 6793 * Keydown event handler 6794 * 6795 */ 6796 const keydownHandler = event => { 6797 if (event.code === 'Tab') { 6798 const FOCUSABLE = Array.from(lightbox.querySelectorAll(FOCUSABLE_ELEMENTS.join(', '))); 6799 if (FOCUSABLE.length === 0) return; 6800 const FOCUSED_INDEX = FOCUSABLE.findIndex(el => el === document.activeElement); 6801 if (event.shiftKey && FOCUSED_INDEX === 0) { 6802 // SHIFT+Tab on first â jump to last 6803 FOCUSABLE[FOCUSABLE.length - 1].focus(); 6804 event.preventDefault(); 6805 } else if (!event.shiftKey && FOCUSED_INDEX === FOCUSABLE.length - 1) { 6806 // Tab on last â jump to first 6807 FOCUSABLE[0].focus(); 6808 event.preventDefault(); 6809 } 6810 } else if (event.code === 'Escape') { 6811 // `ESC` Key: Close Tobii 6812 event.preventDefault(); 6813 close(); 6814 } else if (event.code === 'ArrowLeft') { 6815 // `PREV` Key: Show the previous slide 6816 event.preventDefault(); 6817 previous(); 6818 } else if (event.code === 'ArrowRight') { 6819 // `NEXT` Key: Show the next slide 6820 event.preventDefault(); 6821 next(); 6822 } 6823 }; 6824 6825 /** 6826 * Contextmenu event handler 6827 * This is a fix for chromium based browser on mac. 6828 * The 'contextmenu' terminates a mouse event sequence. 6829 * https://bugs.chromium.org/p/chromium/issues/detail?id=506801 6830 * 6831 */ 6832 const contextmenuHandler = () => { 6833 pointerDownCache = []; 6834 updateOffset(); 6835 groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging')); 6836 }; 6837 6838 /** 6839 * Pointerdown event handler 6840 * 6841 */ 6842 const pointerdownHandler = event => { 6843 // Prevent dragging / swiping on textareas, inputs and selects 6844 if (isIgnoreElement(event.target)) { 6845 return; 6846 } 6847 event.preventDefault(); 6848 event.stopPropagation(); 6849 DRAG.startX = DRAG.x = event.clientX; 6850 DRAG.startY = DRAG.y = event.clientY; 6851 DRAG.distance = 0; 6852 6853 // This event is cached to support 2-finger gestures 6854 pointerDownCache.push(event); 6855 if (pointerDownCache.length === 2) { 6856 const { 6857 x, 6858 y 6859 } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY); 6860 DRAG.startX = DRAG.x = x; 6861 DRAG.startY = DRAG.y = y; 6862 DRAG.distance = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / TRANSFORM.scale; 6863 } 6864 }; 6865 6866 /** 6867 * Pointermove event handler 6868 * 6869 */ 6870 const pointermoveHandler = event => { 6871 if (!pointerDownCache.length) return; 6872 groups[activeGroup].slider.classList.add('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging')); 6873 6874 // Find this event in the cache and update its record with this event 6875 const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId); 6876 pointerDownCache[index] = event; 6877 if (pointerDownCache.length === 2) { 6878 // 2-pointer horizontal pinch/zoom gesture 6879 const { 6880 x, 6881 y 6882 } = midPoint(pointerDownCache[0].clientX, pointerDownCache[0].clientY, pointerDownCache[1].clientX, pointerDownCache[1].clientY); 6883 const scale = distance(pointerDownCache[0].clientX - pointerDownCache[1].clientX, pointerDownCache[0].clientY - pointerDownCache[1].clientY) / DRAG.distance; 6884 zoomPan(event.target, clamp(scale, MIN_SCALE, MAX_SCALE), x, y, x - DRAG.x, y - DRAG.
vendor: 11,275 bytes, lines 6884-7292
6884y); 6885 DRAG.x = x; 6886 DRAG.y = y; 6887 return; 6888 } 6889 if (isZoomed()) { 6890 const deltaX = event.clientX - DRAG.x; 6891 const deltaY = event.clientY - DRAG.y; 6892 pan(deltaX, deltaY); 6893 } 6894 DRAG.x = event.clientX; 6895 DRAG.y = event.clientY; 6896 if (!isZoomed()) { 6897 // Drag animation 6898 const deltaX = DRAG.startX - DRAG.x; 6899 const deltaY = DRAG.startY - DRAG.y; 6900 6901 // Skip animation if drag distance is too low 6902 if (distance(deltaX, deltaY) < 10) return; 6903 if (Math.abs(deltaX) > Math.abs(deltaY) && groups[activeGroup].elementsLength > 1) { 6904 // Horizontal swipe 6905 groups[activeGroup].slider.style.transform = `translate(${offset - Math.round(deltaX)}px, 0)`; 6906 } else if (userSettings.swipeClose) { 6907 // Vertical swipe 6908 groups[activeGroup].slider.style.transform = `translate(${offset}px, -${Math.round(deltaY)}px)`; 6909 } 6910 } 6911 }; 6912 6913 /** 6914 * Pointerup event handler 6915 * 6916 */ 6917 const pointerupHandler = event => { 6918 // Intercept regular click handler 6919 if (!pointerDownCache.length) return; 6920 groups[activeGroup].slider.classList.remove('tobii__slider--is-' + (isZoomed() ? 'moving' : 'dragging')); 6921 6922 // Remove this event from the target's cache 6923 const index = pointerDownCache.findIndex(cachedEv => cachedEv.pointerId === event.pointerId); 6924 pointerDownCache.splice(index, 1); 6925 const x = event.clientX; 6926 const y = event.clientY; 6927 const deltaX = DRAG.startX - x; 6928 const deltaY = DRAG.startY - y; 6929 const distanceX = Math.abs(deltaX); 6930 const distanceY = Math.abs(deltaY); 6931 if (distanceX > 8 || distanceY > 8) { 6932 if (!isZoomed()) { 6933 // Evaluate drag 6934 if (deltaX < 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex > 0) { 6935 previous(); 6936 } else if (deltaX > 0 && distanceX > userSettings.threshold && groups[activeGroup].currentIndex !== groups[activeGroup].elementsLength - 1) { 6937 next(); 6938 } else if (deltaY > 0 && distanceY > userSettings.threshold && userSettings.swipeClose) { 6939 close(); 6940 } else { 6941 updateOffset(); 6942 } 6943 } 6944 } else { 6945 // Evaluate tap 6946 const now = Date.now(); 6947 const tapLength = now - lastTapTime; 6948 if (tapLength < DOUBLE_TAP_TIME && tapLength > 100) { 6949 // Double click 6950 event.preventDefault(); 6951 lastTapTime = 0; 6952 if (isZoomed()) { 6953 resetZoom(); 6954 } else { 6955 zoomPan(event.target, MAX_SCALE / 2, x, y, 0, 0); 6956 } 6957 } else { 6958 lastTapTime = now; 6959 if (isTouchDevice()) { 6960 // Delayed tap on mobile 6961 window.setTimeout(() => { 6962 const { 6963 left, 6964 top, 6965 bottom, 6966 right, 6967 width 6968 } = event.target.getBoundingClientRect(); 6969 if (y < top || y > bottom || !lastTapTime) return; 6970 if (x > left && x < left + width / 2) { 6971 previous(); 6972 } else if (x < right && x > right - width / 2) { 6973 next(); 6974 } 6975 }, DOUBLE_TAP_TIME); 6976 } 6977 } 6978 } 6979 }; 6980 6981 /** 6982 * Wheel event handler 6983 * 6984 */ 6985 const wheelHandler = event => { 6986 const deltaScale = Math.sign(event.deltaY) > 0 ? -1 : 1; 6987 if (!isZoomed() && !deltaScale) return; 6988 event.preventDefault(); 6989 const newScale = TRANSFORM.scale + deltaScale / (SCALE_SENSITIVITY / TRANSFORM.scale); 6990 zoomPan(event.target, clamp(newScale, MIN_SCALE, MAX_SCALE), event.clientX, event.clientY, 0, 0); 6991 }; 6992 const clampedTranslate = (axis, translate) => { 6993 // Whole clamping functionality heavily inspired 6994 // by https://github.com/Neophen/pinch-zoom-pan 6995 const { 6996 element, 6997 scale, 6998 originX, 6999 originY 7000 } = TRANSFORM; 7001 const axisIsX = axis === 'x'; 7002 const origin = axisIsX ? originX : originY; 7003 const axisKey = axisIsX ? 'offsetWidth' : 'offsetHeight'; 7004 const containerSize = element.parentNode[axisKey]; 7005 const imageSize = element[axisKey]; 7006 const bounds = element.getBoundingClientRect(); 7007 const imageScaledSize = axisIsX ? bounds.width : bounds.height; 7008 const defaultOrigin = imageSize / 2; 7009 const originOffset = (origin - defaultOrigin) * (scale - 1); 7010 const range = Math.max(0, Math.round(imageScaledSize) - containerSize); 7011 const max = Math.round(range / 2); 7012 const min = 0 - max; 7013 return clamp(translate, min + originOffset, max + originOffset); 7014 }; 7015 const clamp = (value, min, max) => Math.max(Math.min(value, max), min); 7016 const isZoomed = () => TRANSFORM.scale !== MIN_SCALE; 7017 const pan = (deltaX, deltaY) => { 7018 if (deltaX !== 0) { 7019 TRANSFORM.translateX = clampedTranslate('x', TRANSFORM.translateX + deltaX); 7020 } 7021 if (deltaY !== 0) { 7022 TRANSFORM.translateY = clampedTranslate('y', TRANSFORM.translateY + deltaY); 7023 } 7024 const { 7025 element, 7026 originX, 7027 originY, 7028 translateX, 7029 translateY, 7030 scale 7031 } = TRANSFORM; 7032 element.style.transformOrigin = `${originX}px ${originY}px`; 7033 element.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`; 7034 }; 7035 const zoomPan = (el, newScale, x, y, deltaX, deltaY) => { 7036 if (el.tagName !== 'IMG') return; 7037 const { 7038 left, 7039 top 7040 } = el.getBoundingClientRect(); 7041 const originX = x - left; 7042 const originY = y - top; 7043 const newOriginX = originX / TRANSFORM.scale; 7044 const newOriginY = originY / TRANSFORM.scale; 7045 TRANSFORM.element = el; 7046 TRANSFORM.originX = newOriginX; 7047 TRANSFORM.originY = newOriginY; 7048 TRANSFORM.scale = newScale; 7049 pan(deltaX, deltaY); 7050 }; 7051 const distance = (dx, dy) => Math.hypot(dx, dy); 7052 const midPoint = (x1, y1, x2, y2) => ({ 7053 x: (x1 + x2) / 2, 7054 y: (y1 + y2) / 2 7055 }); 7056 const resetZoom = () => { 7057 TRANSFORM.scale = MIN_SCALE; 7058 TRANSFORM.originX = 0; 7059 TRANSFORM.originY = 0; 7060 TRANSFORM.translateX = 0; 7061 TRANSFORM.translateY = 0; 7062 pan(0, 0); 7063 }; 7064 7065 /** 7066 * Bind events 7067 * 7068 */ 7069 const bindEvents = () => { 7070 if (userSettings.keyboard) { 7071 window.addEventListener('keydown', keydownHandler); 7072 } 7073 7074 // Resize event 7075 window.addEventListener('resize', resizeHandler); 7076 7077 // Popstate event 7078 window.addEventListener('popstate', close); 7079 7080 // Click event 7081 on('click', clickHandler); 7082 if (userSettings.draggable) { 7083 // Pointer events 7084 on('pointerdown', pointerdownHandler); 7085 on('pointermove', pointermoveHandler); 7086 on('pointerup', pointerupHandler); 7087 on('pointercancel', contextmenuHandler); 7088 on('pointerout', contextmenuHandler); 7089 on('pointerleave', contextmenuHandler); 7090 on('contextmenu', contextmenuHandler); 7091 } 7092 7093 // Wheel event 7094 on('wheel', wheelHandler); 7095 }; 7096 7097 /** 7098 * Unbind events 7099 * 7100 */ 7101 const unbindEvents = () => { 7102 if (userSettings.keyboard) { 7103 window.removeEventListener('keydown', keydownHandler); 7104 } 7105 7106 // Resize event 7107 window.removeEventListener('resize', resizeHandler); 7108 7109 // Popstate event 7110 window.removeEventListener('popstate', close); 7111 7112 // Click event 7113 off('click', clickHandler); 7114 if (userSettings.draggable) { 7115 // Pointer events 7116 off('pointerdown', pointerdownHandler); 7117 off('pointermove', pointermoveHandler); 7118 off('pointerup', pointerupHandler); 7119 off('pointercancel', contextmenuHandler); 7120 off('pointerout', contextmenuHandler); 7121 off('pointerleave', contextmenuHandler); 7122 off('contextmenu', contextmenuHandler); 7123 } 7124 7125 // Wheel event 7126 off('wheel', wheelHandler); 7127 }; 7128 7129 /** 7130 * Update userSettings 7131 * 7132 */ 7133 const updateConfig = () => { 7134 const group = groups[activeGroup]; 7135 const slider = group.slider; 7136 if (userSettings.draggable && !slider.classList.contains('tobii__slider--is-draggable')) { 7137 slider.classList.add('tobii__slider--is-draggable'); 7138 } 7139 const hideButtons = !userSettings.nav || group.elementsLength === 1 || userSettings.nav === 'auto' && isTouchDevice(); 7140 setButtonState(prevButton, hideButtons, hideButtons); 7141 setButtonState(nextButton, hideButtons, hideButtons); 7142 const hideCounter = !userSettings.counter || group.elementsLength === 1; 7143 counter.setAttribute('aria-hidden', hideCounter ? 'true' : 'false'); 7144 }; 7145 7146 /** 7147 * Update live region 7148 * 7149 */ 7150 const updateAnnouncement = () => { 7151 const group = groups[activeGroup]; 7152 const currIndex = group.currentIndex; 7153 const total = group.elementsLength; 7154 const trigger = group.gallery[currIndex]; 7155 const [slide, of] = userSettings.announcementLabel; 7156 let extra; 7157 if (trigger.hasAttribute('data-label')) { 7158 extra = trigger.getAttribute('data-label'); 7159 } else { 7160 const img = trigger.querySelector('img'); 7161 extra = (img == null ? void 0 : img.alt) || ''; 7162 } 7163 const base = `${slide} ${currIndex + 1} ${of} ${total}`; 7164 7165 // Announce reliably 7166 liveRegion.textContent = ''; 7167 window.setTimeout(() => { 7168 liveRegion.textContent = extra ? `${base}. ${extra}` : base; 7169 }, 10); 7170 }; 7171 7172 /** 7173 * Update lightbox 7174 * 7175 * @param {string|null} dir - Current slide direction 7176 */ 7177 const updateLightbox = (dir = null) => { 7178 updateOffset(); 7179 updateCounter(); 7180 updateAnnouncement(); 7181 updateFocus(dir); 7182 }; 7183 7184 /** 7185 * Reset Tobii 7186 * 7187 */ 7188 const reset = () => { 7189 if (isOpen()) close(); 7190 Object.values(groups).forEach(group => group.gallery.forEach(remove)); 7191 groups = {}; 7192 activeGroup = null; 7193 Object.values(SUPPORTED_ELEMENTS).forEach(type => type.onReset()); 7194 }; 7195 7196 /** 7197 * Destroy Tobii 7198 * 7199 */ 7200 const destroy = () => { 7201 reset(); 7202 lightbox.parentNode.removeChild(lightbox); 7203 }; 7204 7205 /** 7206 * Check if Tobii is open 7207 * 7208 */ 7209 const isOpen = () => { 7210 return lightbox.getAttribute('aria-hidden') === 'false'; 7211 }; 7212 7213 /** 7214 * Detect whether device is touch capable 7215 * 7216 */ 7217 const isTouchDevice = () => { 7218 return 'ontouchstart' in window; 7219 }; 7220 7221 /** 7222 * Checks whether element's tagName is part of array 7223 * 7224 */ 7225 const isIgnoreElement = el => { 7226 return ['TEXTAREA', 'OPTION', 'INPUT', 'SELECT'].indexOf(el.tagName) !== -1 || el === prevButton || el === nextButton || el === closeButton; 7227 }; 7228 7229 /** 7230 * Return current index 7231 * 7232 */ 7233 const slidesIndex = () => { 7234 return groups[activeGroup].currentIndex; 7235 }; 7236 7237 /** 7238 * Return elements length 7239 * 7240 */ 7241 const slidesCount = () => { 7242 return groups[activeGroup].elementsLength; 7243 }; 7244 7245 /** 7246 * Return current group 7247 * 7248 */ 7249 const currentGroup = () => { 7250 return activeGroup; 7251 }; 7252 7253 /** 7254 * Bind events 7255 * @param {String} eventName 7256 * @param {function} callback - callback to call 7257 * 7258 */ 7259 const on = (eventName, callback) => { 7260 lightbox.addEventListener(eventName, callback); 7261 }; 7262 7263 /** 7264 * Unbind events 7265 * @param {String} eventName 7266 * @param {function} callback - callback to call 7267 * 7268 */ 7269 const off = (eventName, callback) => { 7270 lightbox.removeEventListener(eventName, callback); 7271 }; 7272 init(userOptions); 7273 return { 7274 open, 7275 previous, 7276 next, 7277 close, 7278 add: checkDependencies, 7279 remove, 7280 reset, 7281 destroy, 7282 isOpen, 7283 slidesIndex, 7284 select, 7285 slidesCount, 7286 selectGroup, 7287 currentGroup, 7288 on, 7289 off 7290 }; 7291} 7292
7293class CsLightbox extends Core { 7294 7295 static options = { 7296 zoomImageSelector: '[data-type="image"] img, .js-zoom-image' 7297 }; 7298 7299 applyTobiiWheelFix() { 7300 const minScale = 1; 7301 const maxScale = 4; 7302 const tolerance = 0.01; 7303 7304 const getScale = (image) => { 7305 const transform = getComputedStyle(image).transform; 7306 7307 if (!transform || transform === 'none') { 7308 return minScale; 7309 } 7310 7311 try { 7312 const matrix = new DOMMatrixReadOnly(transform); 7313 7314 return Math.hypot(matrix.a, matrix.b); 7315 } catch { 7316 return minScale; 7317 } 7318 }; 7319 7320 this.lightbox.addEventListener( 7321 'wheel', 7322 (event) => { 7323 const target = event.target instanceof Element ? event.target : null; 7324 7325 const image = target?.closest('img'); 7326 7327 const isZoomImage = image?.matches( 7328 CsLightbox.options.zoomImageSelector 7329 ); 7330 7331 // Keep native scrolling for non-zoomable content. 7332 if (!isZoomImage || event.deltaY === 0) { 7333 event.stopImmediatePropagation(); 7334 return; 7335 } 7336 7337 const scale = getScale(image); 7338 const zoomsIn = event.deltaY < 0; 7339 const zoomsOut = event.deltaY > 0; 7340 7341 const isAtMinimum = scale <= minScale + tolerance; 7342 const isAtMaximum = scale >= maxScale - tolerance; 7343 7344 // Keep native scrolling when the zoom limit is reached. 7345 if ((zoomsOut && isAtMinimum) || (zoomsIn && isAtMaximum)) { 7346 event.stopImmediatePropagation(); 7347 } 7348 }, 7349 { 7350 capture: true, 7351 passive: true 7352 } 7353 ); 7354 } 7355 7356 setCloseButtonPosition(closeButton, innerPosition = false, activeSlideMedia = null, delay = 0) { 7357 if (delay > 0) { 7358 closeButton.classList.remove('active'); 7359 } 7360 7361 if (!innerPosition) { 7362 closeButton.removeAttribute('style'); 7363 } else { 7364 const containerRect = this.lightbox.getBoundingClientRect(); 7365 const mediaRect = activeSlideMedia.getBoundingClientRect(); 7366 7367 const buttonHalfWidth = closeButton.offsetWidth / 2; 7368 const buttonHalfHeight = closeButton.offsetHeight / 2; 7369 7370 closeButton.style.top = `${mediaRect.top - buttonHalfHeight}px`; 7371 7372 const spaceOnSide = (containerRect.width - mediaRect.width) / 2; 7373 closeButton.style.right = spaceOnSide < closeButton.offsetWidth ? '0' : `${spaceOnSide - buttonHalfWidth}px`; 7374 } 7375 7376 if (delay > 0) { 7377 setTimeout (() => { 7378 closeButton.classList.add('active'); 7379 }, delay); 7380 } else { 7381 closeButton.classList.add('active'); 7382 } 7383 } 7384 7385 checkCloseButtonPosition(delay = 0) { 7386 const closeButton = this.lightbox.querySelector('.tobii__btn--close'); 7387 if (!(closeButton instanceof HTMLElement) || !this.tobii.isOpen()) { 7388 // consider resize event could be triggered if lightbox is closed 7389 return; 7390 } 7391 7392 // check if close button position is set to inner 7393 const currentGroup = this.tobii.currentGroup(); 7394 const trigger = document.querySelector(`.js-lightbox[data-group="${currentGroup}"]`); 7395 const activeSlideContainer = this.lightbox.querySelector('.tobii__slider[aria-hidden="false"]'); 7396 if (!(activeSlideContainer instanceof HTMLElement) || !trigger?.closest('.js-lightbox-close-button-inner')) { 7397 this.setCloseButtonPosition(closeButton); 7398 return; 7399 } 7400 7401 const slides = activeSlideContainer.querySelectorAll('.tobii__slide'); 7402 const activeSlide = slides[this.tobii.slidesIndex()]; 7403 const activeSlideMedia = activeSlide?.querySelector('.tobii-image, .tobii-html, [data-type="image"], [data-type="html"]'); 7404 if (!(activeSlideMedia instanceof HTMLElement)) { 7405 this.setCloseButtonPosition(closeButton); 7406 return; 7407 } 7408 7409 this.setCloseButtonPosition(closeButton, true, activeSlideMedia, delay); 7410 } 7411 7412 handleCloseButtonPosition() { 7413 const delay = 250; 7414 7415 this.tobii.on('open', () => this.checkCloseButtonPosition()); 7416 this.tobii.on('previous', () => this.checkCloseButtonPosition(delay)); 7417 this.tobii.on('next', () => this.checkCloseButtonPosition(delay)); 7418 7419 window.addEventListener('resize', () => this.checkCloseButtonPosition(delay)); 7420 } 7421 7422 initLightbox() { 7423 7424 let langConfig = {}; 7425 7426 try { 7427 langConfig = TranslatedLang.translatedLangConfig('lightbox').csConfig; 7428 } catch (error){ 7429 console.error('Labels for lightbox not defined', error); 7430 } 7431 7432 // see https://github.com/midzer/tobii 7433 this.tobii = new Tobii({ 7434 selector: '.js-lightbox', 7435 captionsSelector: 'self', 7436 captionAttribute: 'data-lightbox-caption', 7437 captionToggle: true, 7438 captionToggleLabel: [langConfig['captionToggleLabel.show'], langConfig['captionToggleLabel.hide']], 7439 navLabel: [langConfig['navLabel.prev'], langConfig['navLabel.next']], 7440 announcementLabel: [langConfig['announcementLabel.page'], langConfig['announcementLabel.of']], 7441 closeLabel: langConfig['closeLabel'], 7442 dialogTitle: langConfig['dialogTitle'], 7443 loadingIndicatorLabel: langConfig['loadingIndicatorLabel'] 7444 }); 7445 7446 this.lightbox = document.querySelector('.tobii:not(.js-wheel-fix-initialized)'); 7447 if (!this.lightbox) { 7448 return; 7449 } 7450 7451 // fix for mouse wheel in tobii lightbox 3.2.0 7452 this.lightbox.classList.add('js-wheel-fix-initialized'); 7453 this.applyTobiiWheelFix(); 7454 7455 // set custom close button position, if option is set 7456 this.handleCloseButtonPosition(); 7457 } 7458 7459 init() { 7460 this.initLightbox(); 7461 } 7462} 7463 7464class CsScrollToNextFrame extends Core { 7465 7466 init() { 7467 this.el.addEventListener('click', (e) => { 7468 e.preventDefault(); 7469 7470 const target = this.el.closest('.frame').nextElementSibling; 7471 7472 target.scrollIntoView({ behavior: 'smooth' }); 7473 }); 7474 } 7475} 7476 7477class DeviceDetection { 7478 7479 /** 7480 * Returns whether the current device is a touch device 7481 * @returns {boolean} 7482 */ 7483 static isTouchDevice() { 7484 return ('ontouchstart' in document.documentElement); 7485 } 7486 7487 /** 7488 * Returns whether the current userAgent is an IOS device 7489 * @returns {boolean} 7490 */ 7491 static isIOSDevice() { 7492 return (DeviceDetection.isIPhoneDevice() || DeviceDetection.isIPadDevice()); 7493 } 7494 7495 /** 7496 * Returns if we're dealing with a native Windows browser 7497 * @returns {boolean} 7498 */ 7499 static isNativeWindowsBrowser() { 7500 return (DeviceDetection.isIEBrowser() || DeviceDetection.isEdgeBrowser()); 7501 } 7502 7503 /** 7504 * Returns whether the current userAgent is an iPhone device 7505 * @returns {boolean} 7506 */ 7507 static isIPhoneDevice() { 7508 const userAgent = navigator.userAgent; 7509 return !!userAgent.match(/iPhone/i); 7510 } 7511 7512 /** 7513 * Returns whether the current userAgent is an iPad device 7514 * @returns {boolean} 7515 */ 7516 static isIPadDevice() { 7517 const userAgent = navigator.userAgent; 7518 return !!userAgent.match(/iPad/i); 7519 } 7520 7521 /** 7522 * Returns if we're dealing with the Internet Explorer. 7523 * @returns {boolean} 7524 */ 7525 static isIEBrowser() { 7526 const userAgent = navigator.userAgent.toLowerCase(); 7527 return userAgent.indexOf('msie') !== -1 || 7528 !!navigator.userAgent.match(/Trident.*rv:\d+\./); 7529 } 7530 7531 /** 7532 * Returns if we're dealing with the Windows Edge browser. 7533 * @returns {boolean} 7534 */ 7535 static isEdgeBrowser() { 7536 const userAgent = navigator.userAgent; 7537 return !!userAgent.match(/Edge\/\d+/i); 7538 } 7539 7540 /** 7541 * Returns a list of css classes with the boolean result of all device 7542 * detection functions. 7543 * @returns {object} 7544 */ 7545 static getList() { 7546 return { 7547 'is-touch': DeviceDetection.isTouchDevice(), 7548 'is-ios': DeviceDetection.isIOSDevice(), 7549 'is-native-windows': DeviceDetection.isNativeWindowsBrowser(), 7550 'is-iphone': DeviceDetection.isIPhoneDevice(), 7551 'is-ipad': DeviceDetection.isIPadDevice(), 7552 'is-ie': DeviceDetection.isIEBrowser(), 7553 'is-edge': DeviceDetection.isEdgeBrowser(), 7554 }; 7555 } 7556} 7557 7558/*! js-cookie v3.0.5 | MIT */ 7559/* eslint-disable no-var */ 7560function assign (target) { 7561 for (var i = 1; i < arguments.length; i++) { 7562 var source = arguments[i]; 7563 for (var key in source) { 7564 target[key] = source[key]; 7565 } 7566 } 7567 return target 7568} 7569/* eslint-enable no-var */ 7570 7571/* eslint-disable no-var */ 7572var defaultConverter = { 7573 read: function (value) { 7574 if (value[0] === '"') { 7575 value = value.slice(1, -1); 7576 } 7577 return value.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent) 7578 }, 7579 write: function (value) { 7580 return encodeURIComponent(value).replace( 7581 /%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, 7582 decodeURIComponent 7583 ) 7584 } 7585}; 7586/* eslint-enable no-var */ 7587 7588/* eslint-disable no-var */ 7589 7590function init (converter, defaultAttributes) {
7591 function set (name, value, attributes) { 7592 if (typeof document === 'undefined') { 7593 return 7594 } 7595 7596 attributes = assign({}, defaultAttributes, attributes); 7597 7598 if (typeof attributes.expires === 'number') { 7599 attributes.expires = new Date(Date.now() + attributes.expires * 864e5); 7600 } 7601 if (attributes.expires) { 7602 attributes.expires = attributes.expires.toUTCString(); 7603 } 7604 7605 name = encodeURIComponent(name) 7606 .replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent) 7607 .replace(/[()]/g, escape); 7608 7609 var stringifiedAttributes = ''; 7610 for (var attributeName in attributes) { 7611 if (!attributes[attributeName]) { 7612 continue 7613 } 7614 7615 stringifiedAttributes += '; ' + attributeName; 7616 7617 if (attributes[attributeName] === true) { 7618 continue 7619 } 7620 7621 // Considers RFC 6265 section 5.2: 7622 // ... 7623 // 3. If the remaining unparsed-attributes contains a %x3B (";") 7624 // character: 7625 // Consume the characters of the unparsed-attributes up to, 7626 // not including, the first %x3B (";") character. 7627 // ... 7628 stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]; 7629 } 7630 7631 return (document.cookie = 7632 name + '=' + converter.write(value, name) + stringifiedAttributes) 7633 } 7634 7635 function get (name) { 7636 if (typeof document === 'undefined' || (arguments.length && !name)) { 7637 return 7638 } 7639 7640 // To prevent the for loop in the first place assign an empty array 7641 // in case there are no cookies at all. 7642 var cookies = document.cookie ? document.cookie.split('; ') : []; 7643 var jar = {}; 7644 for (var i = 0; i < cookies.length; i++) { 7645 var parts = cookies[i].split('='); 7646 var value = parts.slice(1).join('='); 7647 7648 try { 7649 var found = decodeURIComponent(parts[0]); 7650 jar[found] = converter.read(value, found); 7651 7652 if (name === found) { 7653 break 7654 } 7655 } catch (e) {} 7656 } 7657 7658 return name ? jar[name] : jar 7659 } 7660 7661 return Object.create( 7662 { 7663 set, 7664 get, 7665 remove: function (name, attributes) { 7666 set( 7667 name, 7668 '', 7669 assign({}, attributes, { 7670 expires: -1 7671 }) 7672 ); 7673 }, 7674 withAttributes: function (attributes) { 7675 return init(this.converter, assign({}, this.attributes, attributes)) 7676 }, 7677 withConverter: function (converter) { 7678 return init(assign({}, this.converter, converter), this.attributes) 7679 } 7680 }, 7681 { 7682 attributes: { value: Object.freeze(defaultAttributes) }, 7683 converter: { value: Object.freeze(converter) } 7684 } 7685 ) 7686} 7687 7688var api = init(defaultConverter, { path: '/' }); 7689 7690class CsEdgeNav extends Core { 7691 7692 static options = { 7693 scrollCollapseAt: 400, 7694 itemSelector: '.js-edge-nav-item', 7695 linkSelector: '.js-edge-nav-link', 7696 closeSelector: '.js-edge-nav-close', 7697 smallClass: 'edge-nav__item--small', 7698 cookieDays: 7 7699 }; 7700 7701 initItems() { 7702 const items = Array.from(this.el.querySelectorAll(this.options.itemSelector)); 7703 7704 // Hide items with existing cookies 7705 items.forEach((item) => { 7706 const key = item.dataset.dismissKey; 7707 if (key && api.get(key) === '1') { 7708 this.hideItem(item); 7709 } 7710 }); 7711 7712 // Hide region if no visible items 7713 if (!this.anyVisible()) { 7714 this.hideRegion(); 7715 return; 7716 } 7717 7718 this.showRegion(); 7719 7720 // Pointer + focus interactions 7721 items.forEach((item) => { 7722 item.addEventListener('mouseenter', () => { 7723 if (DeviceDetection.isTouchDevice()) { 7724 return; 7725 } 7726 this.expand(item); 7727 }); 7728 }); 7729 7730 this.el.addEventListener('focusin', (e) => { 7731 if (DeviceDetection.isTouchDevice()) { 7732 return; 7733 } 7734 const item = e.target.closest(this.options.itemSelector); 7735 if (item) this.expand(item); 7736 }); 7737 7738 this.el.addEventListener('click', (e) => { 7739 // for touch devices 7740 const link = e.target.closest(this.options.linkSelector); 7741 if (link && this.el.contains(link)) { 7742 const item = link.closest(this.options.itemSelector); 7743 if (item && item.classList.contains(this.options.smallClass)) { 7744 e.preventDefault(); 7745 this.expand(item); 7746 } 7747 } 7748 7749 // close button 7750 const close = e.target.closest(this.options.closeSelector); 7751 if (close && this.el.contains(close)) { 7752 e.preventDefault(); 7753 7754 const item = close.closest(this.options.itemSelector);
7755 const key = item?.dataset.dismissKey; 7756 7757 this.fadeOut(item, 150).then(() => { 7758 if (key) api.set(key, '1', {expires: this.options.cookieDays, path: '/'}); 7759 this.hideItem(item); 7760 if (!this.anyVisible()) this.hideRegion(); 7761 }); 7762 } 7763 }); 7764 7765 // Scroll collapse 7766 window.addEventListener('scroll', () => { 7767 if (window.scrollY >= this.options.scrollCollapseAt) { 7768 this.collapseAll(); 7769 } 7770 }, { passive: true }); 7771 } 7772 7773 /* ------------------------------------------------------------ 7774 * DOM helpers 7775 * ------------------------------------------------------------ */ 7776 7777 hideItem(item) { 7778 item.hidden = true; 7779 item.setAttribute('aria-hidden', 'true'); 7780 item.style.display = 'none'; 7781 } 7782 7783 showRegion() { 7784 this.el.hidden = false; 7785 this.el.removeAttribute('aria-hidden'); 7786 } 7787 7788 hideRegion() { 7789 this.el.hidden = true; 7790 this.el.setAttribute('aria-hidden', 'true'); 7791 } 7792 7793 anyVisible() { 7794 return Array.from(this.el.querySelectorAll(this.options.itemSelector)) 7795 .some(i => !i.hidden && i.style.display !== 'none'); 7796 } 7797 7798 expand(item) { 7799 item.classList.remove(this.options.smallClass); 7800 } 7801 7802 collapse(item) { 7803 item.classList.add(this.options.smallClass); 7804 } 7805 7806 collapseAll() { 7807 this.el.querySelectorAll(this.options.itemSelector).forEach((i) => { 7808 if (!i.hidden) this.collapse(i); 7809 }); 7810 } 7811 7812 fadeOut(el, duration = 150) { 7813 return new Promise((resolve) => { 7814 if (!el) return resolve(); 7815 el.style.transition = `opacity ${duration}ms ease`; 7816 el.style.opacity = '1'; 7817 requestAnimationFrame(() => { el.style.opacity = '0'; setTimeout(resolve, duration); }); 7818 }); 7819 } 7820 7821 init() { 7822 this.initItems(); 7823 } 7824} 7825 7826class StringHelper { 7827 7828 /** 7829 * turns first character of word to uppercase 7830 * 7831 * @param {string} string 7832 * @returns {string} 7833 * @private 7834 */ 7835 static ucFirst(string) { 7836 return string.charAt(0).toUpperCase() + string.slice(1); 7837 } 7838 7839 7840 /** 7841 * turns first character of string to lowercase 7842 * 7843 * @param {string} string 7844 * @returns {string} 7845 * @private 7846 */ 7847 static lcFirst(string) { 7848 return string.charAt(0).toLowerCase() + string.slice(1); 7849 } 7850 7851 /** 7852 * converts a camel case string 7853 * into a dash case string 7854 * 7855 * @param string 7856 * @returns {string} 7857 */ 7858 static toDashCase(string) { 7859 return string.replace(/([A-Z])/g, '-$1').replace(/^-/, '').toLowerCase(); 7860 } 7861 7862 /** 7863 * 7864 * @param {string} string 7865 * @param {string} separator 7866 * 7867 * @returns {string} 7868 */ 7869 static toLowerCamelCase(string, separator) { 7870 const upperCamelCase = StringHelper.toUpperCamelCase(string, separator); 7871 return StringHelper.lcFirst(upperCamelCase); 7872 } 7873 7874 /** 7875 * 7876 * @param {string} string 7877 * @param {string} separator 7878 * 7879 * @returns {string} 7880 */ 7881 static toUpperCamelCase(string, separator) { 7882 if (!separator) { 7883 return StringHelper.ucFirst(string.toLowerCase()); 7884 } 7885 7886 const stringParts = string.split(separator); 7887 return stringParts.map(string => StringHelper.ucFirst(string.toLowerCase())).join(''); 7888 } 7889 7890 /** 7891 * returns primitive value of a string 7892 * 7893 * @param value 7894 * @returns {*} 7895 * @private 7896 */ 7897 static parsePrimitive(value) { 7898 try { 7899 // replace comma with dot 7900 // if value only contains numbers and commas 7901 if (/^\d+(.|,)\d+$/.test(value)) { 7902 value = value.replace(',', '.'); 7903 } 7904 7905 return JSON.parse(value); 7906 } 7907 catch { 7908 return value.toString(); 7909 } 7910 } 7911} 7912 7913class DomAccess { 7914 7915 /** 7916 * Returns whether or not the element is an HTML node 7917 * 7918 * @param {HTMLElement} element 7919 * @returns {boolean} 7920 */ 7921 static isNode(element) { 7922 if (typeof element !== 'object' || element === null) { 7923 return false; 7924 } 7925 7926 if (element === document || element === window) { 7927 return true; 7928 } 7929 7930 return element instanceof Node; 7931 } 7932 7933 /** 7934 * Returns if the given element has the requested attribute/property 7935 * @param {HTMLElement} element 7936 * @param {string} attribute 7937 * @returns {boolean} 7938 */ 7939 static hasAttribute(element, attribute) { 7940 if (!DomAccess.isNode(element)) { 7941 throw new Error('The element must be a valid HTML Node!'); 7942 } 7943 7944 if (typeof element.hasAttribute !== 'function') { 7945 return false;
7946 } 7947 7948 return element.hasAttribute(attribute); 7949 } 7950 7951 /** 7952 * Returns the value of a given element's attribute/property 7953 * @param {HTMLElement|EventTarget} element 7954 * @param {string} attribute 7955 * @param {boolean} strict 7956 * @returns {*|this|string} 7957 */ 7958 static getAttribute(element, attribute, strict = true) { 7959 if (strict && DomAccess.hasAttribute(element, attribute) === false) { 7960 throw new Error(`The required property "${attribute}" does not exist!`); 7961 } 7962 7963 if (typeof element.getAttribute !== 'function') { 7964 if (strict) { 7965 throw new Error( 7966 'This node doesn\'t support the getAttribute function!'); 7967 } 7968 7969 return undefined; 7970 } 7971 7972 return element.getAttribute(attribute); 7973 } 7974 7975 /** 7976 * Returns the value of a given element's attribute/property 7977 * @param {HTMLElement|EventTarget} element 7978 * @param {string} attribute 7979 * @param {string} value 7980 * @param {boolean} strict 7981 * @returns {*|this|string} 7982 */ 7983 static setAttribute(element, attribute, value, strict = true) { 7984 if (strict && DomAccess.hasAttribute(element, attribute) === false) { 7985 throw new Error(`The required property "${attribute}" does not exist!`); 7986 } 7987 7988 if (typeof element.setAttribute !== 'function') { 7989 if (strict) { 7990 throw new Error( 7991 'This node doesn\'t support the setAttribute function!'); 7992 } 7993 7994 return undefined; 7995 } 7996 7997 return element.setAttribute(attribute, value); 7998 } 7999 8000 /** 8001 * Returns the value of a given css property name of an element - without px 8002 * 8003 * @param {HTMLElement|EventTarget} element 8004 * @param {string} cssPropertyName 8005 * @param {boolean} strict 8006 * @returns {string} 8007 */ 8008 static getCssPropertyWithoutPx(element, cssPropertyName, strict = true) { 8009 if (strict && !(element instanceof Element)) { 8010 throw new Error('Erstes Argument muss ein DOM-Element sein.'); 8011 } 8012 if (strict && typeof cssPropertyName !== 'string') { 8013 throw new Error('Zweites Argument muss eine Zeichenkette sein.'); 8014 } 8015 8016 return window.getComputedStyle(element).getPropertyValue(cssPropertyName).replace('px', ''); 8017 } 8018 8019 /** 8020 * Returns the value of a given elements dataset entry 8021 * 8022 * @param {HTMLElement|EventTarget} element 8023 * @param {string} key 8024 * @param {boolean} strict 8025 * @returns {*|this|string} 8026 */ 8027 static getDataAttribute(element, key, strict = true) { 8028 const keyWithoutData = key.replace(/^data(|-)/, ''); 8029 const parsedKey = StringHelper.toLowerCamelCase(keyWithoutData, '-'); 8030 if (!DomAccess.isNode(element)) { 8031 if (strict) { 8032 throw new Error('The passed node is not a valid HTML Node!'); 8033 } 8034 8035 return undefined; 8036 } 8037 8038 if (typeof element.dataset === 'undefined') { 8039 if (strict) { 8040 throw new Error('This node doesn\'t support the dataset attribute!'); 8041 } 8042 8043 return undefined; 8044 } 8045 8046 const attribute = element.dataset[parsedKey]; 8047 8048 if (typeof attribute === 'undefined') { 8049 if (strict) { 8050 throw new Error( 8051 `The required data attribute "${key}" does not exist on ${element}!`); 8052 } 8053 8054 return attribute; 8055 } 8056 8057 return StringHelper.parsePrimitive(attribute); 8058 } 8059 8060 /** 8061 * Returns the selected element of a defined parent node 8062 * @param {HTMLElement|EventTarget} parentNode 8063 * @param {string} selector 8064 * @param {boolean} strict 8065 * @returns {HTMLElement} 8066 */ 8067 static querySelector(parentNode, selector, strict = true) { 8068 if (strict && !DomAccess.isNode(parentNode)) { 8069 throw new Error('The parent node is not a valid HTML Node!'); 8070 } 8071 8072 const element = parentNode.querySelector(selector) || false; 8073 8074 if (strict && element === false) { 8075 throw new Error( 8076 `The required element "${selector}" does not exist in parent node!`); 8077 } 8078 8079 return element; 8080 } 8081 8082 /** 8083 * Returns the selected elements of a defined parent node 8084 * 8085 * @param {HTMLElement|EventTarget} parentNode 8086 * @param {string} selector 8087 * @param {boolean} strict 8088 * @returns {NodeList|false} 8089 */ 8090 static querySelectorAll(parentNode, selector, strict = true) { 8091 if (strict && !DomAccess.isNode(parentNode)) { 8092 throw new Error('The parent node is not a valid HTML Node!'); 8093 } 8094 8095 let elements = parentNode.querySelectorAll(selector); 8096 if (elements.length === 0) { 8097 elements = false;
8098 } 8099 8100 if (strict && elements === false) { 8101 throw new Error( 8102 `At least one item of "${selector}" must exist in parent node!`); 8103 } 8104 8105 return elements; 8106 } 8107} 8108 8109// reinit on ajax event 8110(function bindAccordionReinitOnce() { 8111 if (document.__accordionReinitBound) return; 8112 document.__accordionReinitBound = true; 8113 8114 document.addEventListener('accordion:reinit', (e) => { 8115 const root = (e.detail && e.detail.root instanceof HTMLElement) 8116 ? e.detail.root 8117 : document; 8118 8119 root.querySelectorAll('.js-accordion').forEach((el) => { 8120 el.classList.remove('js-accordion-init'); 8121 const acc = new CsAccordion(el); 8122 acc.init(); 8123 }); 8124 }); 8125})(); 8126 8127class CsAccordion extends Core { 8128 8129 // openItemOnLoad: count from zero, negative values won't open any item at all -> is the number behind id="accordion-trigger-XXX" 8130 // openMultipleItems: if true, the active item won't close on trigger 8131 static options = { 8132 animationSpeed: 500, 8133 openItemOnLoad: -1, 8134 openMultipleItems: false, 8135 openAllOnLoad: false, 8136 ignoreHash: false 8137 }; 8138 8139 /** 8140 * toggle item 8141 */ 8142 toggleItem(trigger) { 8143 const open = StringHelper.parsePrimitive(DomAccess.getAttribute(trigger, 'aria-expanded', false)); 8144 const panel = document.getElementById(DomAccess.getAttribute(trigger, 'aria-controls')); 8145 8146 panel.style.transition = `height ${this.settingAnimationSpeed}ms ease`; 8147 8148 // toggle panel 8149 if (open) { 8150 8151 panel.style.height = panel.scrollHeight + 'px'; 8152 panel.offsetHeight; 8153 panel.style.height = '0px'; 8154 8155 setTimeout(() => { 8156 panel.setAttribute('hidden', ''); 8157 }, this.settingAnimationSpeed); 8158 8159 } else { 8160 8161 // consider closing the active item 8162 if (!this.settingOpenMultipleItems) { 8163 const expandedTrigger = DomAccess.querySelector(this.el, '.js-accordion-trigger[aria-expanded="true"]', false); 8164 if (expandedTrigger) { 8165 this.toggleItem(expandedTrigger); 8166 } 8167 } 8168 8169 panel.removeAttribute('hidden'); 8170 8171 // animate panel 8172 panel.style.height = '0px'; 8173 panel.offsetHeight; 8174 panel.style.height = panel.scrollHeight + 'px'; 8175 } 8176 8177 // toggle trigger attribute 8178 DomAccess.setAttribute(trigger, 'aria-expanded', `${!open}`); 8179 8180 // on open panel 8181 if (!open) { 8182 // Wait until the height transition finished to avoid wrong scroll position 8183 const onDone = (evt) => { 8184 if (evt && evt.propertyName !== 'height') return; // only once for height 8185 panel.removeEventListener('transitionend', onDone); 8186 // Scroll to the trigger (or use panel if you prefer) 8187 this.scrollToElement(trigger, 8); 8188 }; 8189 8190 // If there's a transition, listen for it; else fallback immediately 8191 panel.addEventListener('transitionend', onDone, { once: true }); 8192 8193 // Fallback timeout in case transitionend doesn't fire (display changes, etc.) 8194 window.setTimeout(() => { 8195 panel.removeEventListener('transitionend', onDone); 8196 this.scrollToElement(trigger, 8); 8197 }, this.settingAnimationSpeed + 50); 8198 } 8199 } 8200 8201 /** 8202 * open item from initial setting 8203 */ 8204 openItemOnLoad() { 8205 const trigger = DomAccess.querySelector(this.el, '#tab-' + this.settingOpenItemOnLoad, false ); 8206 8207 if (this.settingOpenItemOnLoad < 0 || !trigger) { 8208 return; 8209 } 8210 8211 this.toggleItem(trigger); 8212 } 8213 8214 openAllOnLoad() { 8215 if (!this.settingOpenAllOnLoad) { 8216 return; 8217 } 8218 8219 const accordionTriggers = DomAccess.querySelectorAll(this.el, '[id^="tab-"]'); 8220 Iterator.iterate(accordionTriggers, (trigger) => this.toggleItem(trigger)); 8221 } 8222 8223 setOptionsFromDataAttribute() { 8224 const dataOptions = this.el.dataset.accordionOptions; 8225 const defaults = CsAccordion.options; 8226 8227 let config = {}; 8228 if (dataOptions) { 8229 try { 8230 config = JSON.parse(dataOptions); 8231 } catch(error) { 8232 console.warn('data-accordion-options ist kein gültiges JSON:', error); 8233 } 8234 } 8235 8236 this.settingOpenMultipleItems = config.openMultipleItems ?? CsAccordion.options.openMultipleItems; 8237 this.settingAnimationSpeed = config.animationSpeed ?? CsAccordion.options.animationSpeed; 8238 this.settingOpenItemOnLoad = config.openItemOnLoad ?? CsAccordion.options.openItemOnLoad; 8239 this.settingIgnoreHash = config.ignoreHash ?? CsAccordion.options.ignoreHash; 8240 this.settingOpenAllOnLoad = config.openAllOnLoad ?? defaults.openAllOnLoad; 8241 } 8242 8243 isOwnTrigger(el) { 8244 return el.closest('.js-accordion') === this.el; 8245 } 8246 8247 /** 8248 * opens the panel that is currently in the url hash. 8249 * trigger must be in the same accordion 8250 */ 8251 openFromHash() { 8252 8253 if (this.settingIgnoreHash) return false;
8254 8255 const id = window.location.hash.slice(1); 8256 if (!id) return false; 8257 8258 const targetTrigger = document.getElementById(id); 8259 if (!targetTrigger) return false; 8260 8261 if (!this.isOwnTrigger(targetTrigger)) return false; 8262 8263 const openTriggers = DomAccess.querySelectorAll( 8264 this.el, 8265 '.js-accordion-trigger[aria-expanded="true"]', false 8266 ); 8267 8268 if (openTriggers) { 8269 Iterator.iterate(openTriggers, (trigger) => { 8270 if (trigger !== targetTrigger) { 8271 this.toggleItem(trigger); 8272 } 8273 }); 8274 } 8275 8276 const alreadyOpen = StringHelper.parsePrimitive( 8277 DomAccess.getAttribute(targetTrigger, 'aria-expanded', false) 8278 ); 8279 8280 if (!alreadyOpen) { 8281 this.toggleItem(targetTrigger); 8282 } 8283 8284 return true; 8285 } 8286 8287 isAlreadyInitialized() { 8288 return this.el.classList.contains('js-accordion-init'); 8289 } 8290 8291 // -- Helpers --------------------------------------------------------------- 8292 8293 /** Get current header height (handles CSS-set heights) */ 8294 getHeaderHeight() { 8295 const header = document.querySelector('header'); 8296 if (!header) return 0; 8297 const rect = header.getBoundingClientRect(); 8298 return rect.height || parseFloat(getComputedStyle(header).height) || 0; 8299 } 8300 8301 /** Smoothly scroll to an element accounting for a sticky/fixed header */ 8302 scrollToElement(el, extraOffset = 8) { 8303 // Respect reduced motion 8304 const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 8305 8306 const headerH = this.getHeaderHeight(); 8307 const rect = el.getBoundingClientRect(); 8308 const absoluteTop = window.pageYOffset + rect.top; 8309 const targetY = Math.max(absoluteTop - headerH - extraOffset, 0); 8310 8311 window.scrollTo({ 8312 top: targetY, 8313 behavior: prefersReduced ? 'auto' : 'smooth' 8314 }); 8315 } 8316 8317 /** 8318 * init accordion 8319 */ 8320 initAccordion() { 8321 if (this.isAlreadyInitialized()) return; 8322 8323 this.setOptionsFromDataAttribute(); 8324 8325 const openedByHash = this.openFromHash(); 8326 if (!openedByHash) { 8327 if (this.settingOpenMultipleItems && this.settingOpenAllOnLoad) { 8328 this.openAllOnLoad(); 8329 } else { 8330 this.openItemOnLoad(); 8331 } 8332 } 8333 8334 // click event handler for all accordion items 8335 const allTriggers = DomAccess.querySelectorAll(this.el, '.js-accordion-trigger'); 8336 Iterator.iterate(allTriggers, (trigger) => { 8337 if (!this.isOwnTrigger(trigger)) return; 8338 8339 trigger.addEventListener('click', (e) => { 8340 e.stopPropagation(); 8341 this.toggleItem(trigger); 8342 if (!this.settingIgnoreHash) { 8343 history.replaceState(null, '', `#${trigger.id}`); 8344 } 8345 }); 8346 }); 8347 8348 // add init class 8349 this.el.classList.add('js-accordion-init'); 8350 } 8351 8352 init() { 8353 this.initAccordion(); 8354 } 8355} 8356 8357class CsFlipText extends Core { 8358 8359 togglePanel(panel, open) { 8360 this.el.setAttribute('aria-expanded', open); 8361 8362 if (open) { 8363 panel.removeAttribute('hidden'); 8364 panel.focus(); 8365 } else { 8366 panel.setAttribute('hidden', ''); 8367 this.el.focus(); 8368 } 8369 } 8370 8371 init() { 8372 const panelId = this.el.getAttribute('aria-controls'); 8373 const panel = document.getElementById(panelId); 8374 8375 this.el.addEventListener('click', () => { 8376 this.togglePanel(panel, true); 8377 }); 8378 8379 document.addEventListener('keydown', (e) => { 8380 if (this.el.getAttribute('aria-expanded') === 'true') { 8381 if (e.key === 'Escape' || e.key === 'Enter') { 8382 this.togglePanel(panel, false); 8383 } 8384 } 8385 }); 8386 8387 panel.addEventListener('click', () => { 8388 this.togglePanel(panel, false); 8389 }); 8390 } 8391} 8392 8393class CsAccessibilityAnimation extends Core { 8394 8395 static options = { 8396 playAnimationsByDefault: true, 8397 cookieName: 'cs-animations', 8398 playIconClass: 'cs-icon-play-circle-solid', 8399 pauseIconClass: 'cs-icon-pause-circle' 8400 }; 8401 8402 handleAnimations(startAnimation) { 8403 const body = document.querySelector('body'); 8404 const splideSliders = document.getElementsByClassName('splide splide--home'); 8405 const triggerButton = this.el; 8406 8407 if (startAnimation) { 8408 8409 // If there is a Progress Slider, a custom event is triggered 8410 if (splideSliders.length > 0) { 8411 const event = new CustomEvent('splideSlidersFound', { 8412 detail: { 8413 found: true 8414 } 8415 }); 8416 document.dispatchEvent(event); 8417 } 8418 8419 // start css animation 8420 body.classList.remove('h-stop-animations'); 8421 8422 // swap icon for trigger 8423 triggerButton.classList.remove('cs-icon-play-circle-solid'); 8424 triggerButton.classList.add('cs-icon-pause-circle'); 8425 8426 // set title and aria-label 8427 triggerButton.setAttribute('title', triggerButton.dataset.titleStopAnimations); 8428 triggerButton.setAttribute('aria-label', triggerButton.dataset.titleStopAnimations); 8429 8430 // set Cookie 8431 api.set(CsAccessibilityAnimation.options.cookieName, '1'); 8432 } else { 8433 8434 // If there is a Progress Slider, a custom event is triggered 8435 if (splideSliders.length > 0) { 8436 const event = new CustomEvent('splideSlidersFound', { 8437 detail: { 8438 found: false 8439 } 8440 }); 8441 document.dispatchEvent(event); 8442 } 8443 8444 // stop css animation immediately 8445 body.classList.add('h-stop-animations'); 8446 8447 // swap icon for trigger 8448 triggerButton.classList.add('cs-icon-play-circle-solid'); 8449 triggerButton.classList.remove('cs-icon-pause-circle'); 8450 8451 // set title and aria-label 8452 triggerButton.setAttribute('title', triggerButton.dataset.titleStartAnimations); 8453 triggerButton.setAttribute('aria-label', triggerButton.dataset.titleStartAnimations); 8454 8455 // set Cookie 8456 api.set(CsAccessibilityAnimation.options.cookieName, '0'); 8457 } 8458 } 8459 8460 initAnimations() { 8461 if (!api.get(CsAccessibilityAnimation.options.cookieName)) { 8462 8463 // Cookie is not set yet 8464 this.handleAnimations(CsAccessibilityAnimation.options.playAnimationsByDefault); 8465 } else { 8466 8467 // Cookie is set 8468 this.handleAnimations(api.get(CsAccessibilityAnimation.options.cookieName) === '1'); 8469 } 8470 } 8471 8472 initToggleButton() { 8473 this.el.addEventListener('click', () => { 8474 this.handleAnimations(api.get(CsAccessibilityAnimation.options.cookieName) !== '1'); 8475 }); 8476 } 8477 8478 init() { 8479 this.initAnimations(); 8480 this.initToggleButton(); 8481 } 8482} 8483 8484class CsCookieBannerRenderer extends Core { 8485 8486 static options = { 8487 layerPosition: 'center', 8488 language: 'de-DE', 8489 initializeErrMsg: 'Cookie Banner: Fehler beim Initialisieren', 8490 loadConfigErrMsg: 'Cookie Banner: Fehler beim Laden der JSON-Konfiguration', 8491 gtmCookieCategories: ['statistics', 'marketing'], 8492 path: { 8493 configJson: '/_assets/7b3ba8bb3871137dffa4d9cccacf730f/JavaScript/config/config-cookie-banner.json', 8494 contrastVersionCss: '/_assets/7b3ba8bb3871137dffa4d9cccacf730f/Css/template-contrast.css?1750927915' 8495 }, 8496 selector: { 8497 globalWrapper: '', 8498 accordionClasses: ['js-accordion', 'js-cscb-accordion'], 8499 accordionItemClass: 'accordion__item', 8500 checkboxContainerClass: 'js-cscb-checkbox-container', 8501 templateHeaderPlaceholder: '.js-cscb-header-placeholder', 8502 templateHeaderId: 'cscb-template-header', 8503 templateTitlePlaceholder: '.js-cscb-title-placeholder', 8504 templateTitleId: 'cscb-template-title', 8505 templateButtonGroupPlaceholder: '.js-cscb-button-group-placeholder', 8506 templateButtonGroupId: 'cscb-template-button-group', 8507 templateFooterPlaceholder: '.js-cscb-footer-placeholder', 8508 templateFooterId: 'cscb-template-footer', 8509 templateCategoriesWrapperClass: 'cscb-edit-box__settings-wrapper', 8510 templateCategoriesPlaceholder: '.js-cscb-categories-placeholder', 8511 templateCategoryId: 'cscb-template-category-setting', 8512 templateCategoryWrapperClass: 'cscb-edit-box__settings-item', 8513 templateCookieTablePlaceholder: '.js-cscb-cookie-tables', 8514 templateCookieTableId: 'cscb-template-cookie-table', 8515 templateMediaCheckboxPlaceholder: '.js-cscb-cookie-media-items', 8516 templateMediaCheckboxId: 'cscb-template-media-checkbox', 8517 templateLayerPlaceholder: '.js-cscb-layer-placeholder', 8518 templateLayerId: 'cscb-template-layer', 8519 templateMediaPreviewId: 'cscb-template-media-preview', 8520 templateMediaPreviewPlaceholderClass: 'js-cscb-media-preview-placeholder',
8521 modalTitleId: 'modal-cscb-headline', 8522 modalContentWrapperSelector: '.js-cscb-edit-box .js-cs-modal-content-wrapper' 8523 } 8524 }; 8525 8526 8527 8528 /** 8529 * read settings 8530 */ 8531 setLanguage() { 8532 this.language = document.getElementsByTagName('html')[0].getAttribute('lang'); 8533 if (!this.language) { 8534 this.language = CsCookieBannerRenderer.options.language; 8535 } 8536 } 8537 8538 async loadSettings() { 8539 const response = await fetch(CsCookieBannerRenderer.options.path.configJson); 8540 if (!response.ok) throw new Error(CsCookieBannerRenderer.options.loadConfigErrMsg); 8541 8542 const configData = await response.json(); 8543 8544 this.data = configData['language'][this.language]; 8545 } 8546 8547 8548 8549 /** 8550 * general template rendering helper functions 8551 */ 8552 renderTemplate(templateString, data = {}) { 8553 return templateString.replace(/\[\[([^\]]+)\]\]/g, (match, key) => { 8554 return typeof data[key] !== 'undefined' ? data[key] : ''; 8555 }); 8556 } 8557 8558 hydrateDataLinks(containerEl) { 8559 containerEl.querySelectorAll('a[data-href]').forEach(a => { 8560 a.href = a.dataset.href; 8561 a.removeAttribute('data-href'); 8562 }); 8563 } 8564 renderPartialToSelector(selector, htmlString, root = document) { 8565 const el = root.querySelector(selector); 8566 if (!el) { 8567 console.warn(`Placeholder ${selector} not found.`); 8568 return; 8569 } 8570 8571 const wrapper = document.createElement('div'); 8572 wrapper.innerHTML = htmlString; 8573 8574 this.hydrateDataLinks(wrapper); 8575 8576 const fragment = document.createDocumentFragment(); 8577 Array.from(wrapper.childNodes).forEach(node => fragment.appendChild(node)); 8578 8579 el.replaceWith(fragment); 8580 } 8581 8582 insertPartialFromTemplate(templateId, data, placeholderSelector, root = document) { 8583 8584 // get template 8585 const template = document.getElementById(templateId); 8586 if (!template) return; 8587 8588 // fill template with data 8589 const rendered = this.renderTemplate(template.innerHTML, data); 8590 8591 this.renderPartialToSelector(placeholderSelector, rendered, root); 8592 } 8593 8594 8595 8596 /** 8597 * cookie categories: specific template rendering helper functions 8598 */ 8599 renderMediaItems(categoryNode, mediaData = {}) { 8600 const mediaWrapper = categoryNode.querySelector(CsCookieBannerRenderer.options.selector.templateMediaCheckboxPlaceholder); 8601 if (!mediaWrapper) return; 8602 8603 const mediaKeys = Object.keys(mediaData); 8604 if (mediaKeys.length === 0) { 8605 mediaWrapper.remove(); 8606 return; 8607 } 8608 8609 const template = document.getElementById(CsCookieBannerRenderer.options.selector.templateMediaCheckboxId); 8610 if (!template) return; 8611 8612 const renderedItems = mediaKeys.map(key => { 8613 const media = mediaData[key]; 8614 8615 return this.renderTemplate(template.innerHTML, { 8616 mediaKey: key, 8617 label: media.label || '', 8618 urlToPrivacy: media.urlToPrivacy || '#', 8619 dataProtectionExternalName: this.data.dataProtectionExternalName, 8620 dataProtectionAriaLabel: this.data.dataProtectionAriaLabel + ' ' + media.label 8621 }); 8622 }).join(''); 8623 8624 this.renderPartialToSelector( 8625 CsCookieBannerRenderer.options.selector.templateMediaCheckboxPlaceholder, 8626 renderedItems, 8627 categoryNode 8628 ); 8629 } 8630 8631 renderCookieTables(categoryNode, cookies = []) { 8632 const placeholder = categoryNode.querySelector(CsCookieBannerRenderer.options.selector.templateCookieTablePlaceholder); 8633 if (!placeholder) return; 8634 8635 if (!Array.isArray(cookies) || cookies.length === 0) { 8636 placeholder.remove(); 8637 return; 8638 } 8639 8640 const template = document.getElementById(CsCookieBannerRenderer.options.selector.templateCookieTableId); 8641 if (!template) return; 8642 8643 const renderedTables = cookies.map(cookie => { 8644 const tableData = { 8645 nameTitle: this.data.nameTitle, 8646 name: cookie.name, 8647 link: cookie.link, 8648 dataProtectionExternalName: this.data.dataProtectionExternalName, 8649 purposeTitle: this.data.purposeTitle, 8650 purpose: cookie.purpose 8651 }; 8652 8653 return this.renderTemplate(template.innerHTML, tableData); 8654 }).join(''); 8655 8656 this.renderPartialToSelector( 8657 CsCookieBannerRenderer.options.selector.templateCookieTablePlaceholder, 8658 renderedTables, 8659 categoryNode 8660 ); 8661 } 8662 8663 renderCategoryItem(categoryKey, categoryData) { 8664 const template = document.getElementById(CsCookieBannerRenderer.options.selector.templateCategoryId); 8665 if (!template) return ''; 8666 8667 const data = { 8668 key: categoryKey, 8669 title: categoryData.title, 8670 contrastModeAdditionalTitle: categoryData.contrastModeAdditionalTitle, 8671 description: categoryData.description, 8672 tagManager: CsCookieBannerRenderer.options.gtmCookieCategories.includes(categoryKey) ? 'active' : 'inactive' 8673 }; 8674 8675 // render item with data 8676 const categoryNode = document.createElement('div'); 8677 categoryNode.innerHTML = this.renderTemplate(template.innerHTML, data); 8678 categoryNode.classList.add( 8679 CsCookieBannerRenderer.options.selector.templateCategoryWrapperClass, 8680 CsCookieBannerRenderer.options.selector.accordionItemClass, 8681 CsCookieBannerRenderer.options.selector.checkboxContainerClass 8682 ); 8683 8684 // disable essential checkbox 8685 const checkboxEssential = categoryNode.querySelector('#cscb-essential'); 8686 if (checkboxEssential) { 8687 checkboxEssential.disabled = true; 8688 checkboxEssential.checked = true; 8689 } 8690 8691 return categoryNode; 8692 } 8693 8694 renderCookieCategories(cookieSettings) { 8695 const placeholder = document.querySelector(CsCookieBannerRenderer.options.selector.templateCategoriesPlaceholder); 8696 if (!placeholder) return; 8697 8698 const wrapperNode = document.createElement('div'); 8699 wrapperNode.classList.add(CsCookieBannerRenderer.options.selector.templateCategoriesWrapperClass); 8700 CsCookieBannerRenderer.options.selector.accordionClasses.forEac
8700h(className => { 8701 wrapperNode.classList.add(className); 8702 }); 8703 wrapperNode.dataset.accordionOptions = JSON.stringify({ 8704 ignoreHash: true 8705 }); 8706 8707 placeholder.replaceWith(wrapperNode); 8708 8709 Object.entries(cookieSettings).forEach(([key, data]) => { 8710 const categoryNode = this.renderCategoryItem(key, data); 8711 8712 // render media checkboxes 8713 this.renderMediaItems(categoryNode, data.media); 8714 8715 // render table data (e.g. statistics) 8716 this.renderCookieTables(categoryNode, data.cookies); 8717 8718 // add cookie category to wrapper 8719 wrapperNode.appendChild(categoryNode); 8720 }); 8721 } 8722 8723 8724 8725 /** 8726 * build cookie banner - main function 8727 */ 8728 buildCookieBanner() { 8729 const settings = CsCookieBannerRenderer.options; 8730 8731 // insert header 8732 this.insertPartialFromTemplate(settings.selector.templateHeaderId, { 8733 closeButtonAriaLabel: this.data.closeButtonAriaLabel, 8734 contrastButtonIncreaseLabel: this.data.contrastButtonIncreaseLabel, 8735 contrastButtonResetLabel: this.data.contrastButtonResetLabel, 8736 contrastVersionPath: settings.path.contrastVersionCss 8737 }, settings.selector.templateHeaderPlaceholder); 8738 8739 // insert title and description 8740 this.insertPartialFromTemplate(settings.selector.templateTitleId, { 8741 headline: this.data.headline, 8742 infoText: this.data.infoText, 8743 modalTitleId: settings.selector.modalTitleId 8744 }, settings.selector.templateTitlePlaceholder); 8745 8746 // set aria-labelledby after title is inserted 8747 const modalContentWrapper = document.querySelector(settings.selector.modalContentWrapperSelector); 8748 if (modalContentWrapper) { 8749 modalContentWrapper.setAttribute('aria-labelledby', settings.selector.modalTitleId); 8750 } 8751 8752 // insert button group 8753 this.insertPartialFromTemplate(settings.selector.templateButtonGroupId, { 8754 buttonAcceptText: this.data.buttonAcceptText, 8755 buttonSaveText: this.data.buttonSaveText, 8756 buttonDeclineText: this.data.buttonDeclineText 8757 }, settings.selector.templateButtonGroupPlaceholder); 8758 8759 // insert footer 8760 this.insertPartialFromTemplate(settings.selector.templateFooterId, { 8761 dataProtectionLink: this.data.dataProtectionLink, 8762 dataProtectionName: this.data.dataProtectionName, 8763 imprintLink: this.data.imprintLink, 8764 imprintName: this.data.imprintName 8765 }, settings.selector.templateFooterPlaceholder); 8766 8767 // insert cookie categories 8768 this.renderCookieCategories(this.data.cookieSettings); 8769 8770 // insert layer template 8771 this.insertPartialFromTemplate(settings.selector.templateLayerId, { 8772 layerPosition: settings.layerPosition, 8773 layerInfoText: this.data.layerInfoText, 8774 buttonAcceptText: this.data.buttonAcceptText, 8775 buttonDeclineText: this.data.buttonDeclineText, 8776 buttonDenyText: this.data.buttonDenyText 8777 }, settings.selector.templateLayerPlaceholder); 8778 8779 // fire event 8780 document.dispatchEvent(new CustomEvent('cscb:built')); 8781 } 8782 8783 buildMediaPreviewBox(externalMediaElement, cookieName) { 8784 const settings = CsCookieBannerRenderer.options; 8785 8786 // create placeholder node 8787 const placeholderNode = document.createElement('div'); 8788 placeholderNode.classList.add(settings.selector.templateMediaPreviewPlaceholderClass); 8789 externalMediaElement.insertAdjacentElement('afterend', placeholderNode); 8790 8791 // insert data if provided 8792 const previewBoxData = this.data.cookieSettings.externalMedia?.media[cookieName]?.previewBox; 8793 if (!previewBoxData) return; 8794 8795 const placeholderSelector = `.${settings.selector.templateMediaPreviewPlaceholderClass}`; 8796 this.insertPartialFromTemplate(settings.selector.templateMediaPreviewId, { 8797 description: previewBoxData.description, 8798 buttonText: previewBoxData.button, 8799 cookieKey: cookieName 8800 }, placeholderSelector); 8801 8802 // fire event 8803 document.dispatchEvent(new CustomEvent('cscb:preview-built')); 8804 } 8805 8806 8807 8808 /** 8809 * main init function 8810 */ 8811 init() { 8812 this.setLanguage(); 8813 8814 this.loadSettings().then(() => { 8815 this.buildCookieBanner(); 8816 }).catch(error => { 8817 console.warn(CsCookieBannerRenderer.options.initializeErrMsg, error); 8818 8819 // rejects all cookies in case of an initialization error 8820 this.cookieBannerHandler = PluginController.retrievePluginInstanceFromElement(document, 'CsModalManager'); 8821 if (!this.cookieBannerHandler) return; 8822 8823 this.cookieBannerHandler.declineAllCookies(); 8824 }); 8825 } 8826} 8827 8828class CsCookieBannerHandler extends Core { 8829 8830 /** 8831 * options 8832 */ 8833 static options = { 8834 8835 modalName: 'cscb-cookie-banner', // must match the name in the template (trigger and modal) 8836 openModalOnStart: true, 8837 openModalOnExternalMediaFrame: true, 8838 openModalIgnoreUrlList: ['intranet.bbw-leipzig.de', 'bbw-intranet-13.ddev.site'], 8839 cookieName: 'cs-cookie-banner', 8840 expireDays: 30, 8841 gtmCookieText: 'gtm', 8842 gtmMainCategory: 'statistics', 8843 loadGtmConfigErrMsg: 'Cookie Banner: Fehler beim Laden der GTM-JSON-Konfiguration', 8844 path: { 8845 gtmConfigJson: '/_assets/7b3ba8bb3871137dffa4d9cccacf730f/JavaScript/config/config-gtm-domain-keys.json', 8846 }, 8847 selector: { 8848 firstFocusableElement: 'body a:first-of-type', 8849 contrastButtonPage: '.js-toggle-high-contrast', 8850 showClass: 'h-show', 8851 animationClass: 'h-animate', 8852 cookieBannerModal: '.js-cscb-edit-box',
8853 cookieBannerLayer: '.js-cscb-info-box', 8854 contrastButton: '.js-cscb-toggle-high-contrast', 8855 checkbox: '.js-cscb-checkbox', 8856 checkboxParentClass: 'js-cscb-checkbox--parent', 8857 checkboxChildClass: 'js-cscb-checkbox--child', 8858 acceptAllCookies: '.js-cscb-accept-all-cookies', 8859 declineAllCookies: '.js-cscb-decline-cookies', 8860 customizeSettings: '.js-cscb-open-edit-box', 8861 saveSettings: '.js-cscb-save-cookie-settings', 8862 externalMedia: '.js-cscb-external-media', 8863 externalMediaWrapper: '.js-cscb-preview-box-wrapper', 8864 externalMediaWrapperPreviewBoxIsVisibleClass: 'preview-box-is-visible', 8865 externalMediaPreviewBoxClass: 'js-cscb-preview-box', 8866 externalMediaAcceptButtonClass: 'js-cscb-preview-box-button', 8867 externalMediaAccordionTriggerId: 'cscb-accordion-trigger-externalMedia', 8868 externalMediaLoadingClass: 'has-loader', 8869 externalMediaLiveRegion: '.js-cscb-preview-box-live-region' 8870 }, 8871 cssVariables: { 8872 cookieBannerAnimationTime: '--cscb-default-transition-time' 8873 } 8874 }; 8875 8876 8877 8878 /** 8879 * helper functions 8880 */ 8881 reserveSpaceForCookieBannerLayer(stateOpen = true) { 8882 const cookieBannerLayer = document.querySelector(CsCookieBannerHandler.options.selector.cookieBannerLayer); 8883 const bodyOffset = stateOpen ? cookieBannerLayer.offsetHeight : 0; 8884 const body = document.querySelector('body'); 8885 if (body) { 8886 body.style.marginBottom = `${bodyOffset}px`; 8887 } 8888 } 8889 8890 isUrlOnIgnoreList() { 8891 const ignoreList = CsCookieBannerHandler.options.openModalIgnoreUrlList; 8892 if (!Array.isArray(ignoreList) || ignoreList.length === 0) return false; 8893 8894 const currentUrl = window.location.href; 8895 return ignoreList.some(url => currentUrl.includes(url)); 8896 } 8897 8898 8899 8900 /** 8901 * cookie settings - init content 8902 */ 8903 initializeAccordion() { 8904 8905 const accordionSelector = CsCookieBannerRenderer.options.selector.accordionClasses.map(cls => `.${cls}`).join(''); 8906 const accordionNode = document.querySelector(accordionSelector); 8907 if (accordionNode) { 8908 PluginController.initializePlugin('CsAccordion', accordionNode); 8909 } 8910 } 8911 8912 initializeCheckboxes() { 8913 const cookieString = api.get(CsCookieBannerHandler.options.cookieName); 8914 if (!cookieString) return; 8915 8916 const values = cookieString.split('|').filter(key => key && key !== 'gtm'); 8917 8918 values.forEach(key => { 8919 const checkbox = document.getElementById(`cscb-${key}`); 8920 if (checkbox) { 8921 checkbox.checked = true; 8922 } 8923 }); 8924 } 8925 8926 initializeCookieBannerContent() { 8927 this.initializeAccordion(); 8928 this.initializeCheckboxes(); 8929 } 8930 8931 8932 8933 /** 8934 * cookie settings - checkbox handling 8935 */ 8936 handleCheckboxChildrenStatePropagation(target) { 8937 const container = target.closest('.' + CsCookieBannerRenderer.options.selector.checkboxContainerClass); 8938 const childCheckboxes = container?.querySelectorAll('.' + CsCookieBannerHandler.options.selector.checkboxChildClass); 8939 if (childCheckboxes?.length) { 8940 childCheckboxes.forEach(child => { 8941 child.checked = target.checked; 8942 }); 8943 } 8944 } 8945 8946 handleCheckboxParentStatePropagation(target) { 8947 const wrapper = target.closest('.' + CsCookieBannerRenderer.options.selector.checkboxContainerClass); 8948 if (!wrapper) return; 8949 8950 const parentCheckbox = wrapper.querySelector('.' + CsCookieBannerHandler.options.selector.checkboxParentClass); 8951 const childCheckboxes = wrapper.querySelectorAll('.' + CsCookieBannerHandler.options.selector.checkboxChildClass); 8952 8953 if (!parentCheckbox || !childCheckboxes.length) return; 8954 8955 const allUnchecked = Array.from(childCheckboxes).every(cb => !cb.checked); 8956 8957 parentCheckbox.checked = !allUnchecked; 8958 } 8959 8960 activateAllCheckboxes() { 8961 const checkboxes = document.querySelectorAll(CsCookieBannerHandler.options.selector.checkbox); 8962 checkboxes.forEach(cb => cb.checked = true); 8963 } 8964 8965 deactivateAllCheckboxes() { 8966 const checkboxes = document.querySelectorAll(CsCookieBannerHandler.options.selector.checkbox); 8967 checkboxes.forEach(cb => { 8968 if (cb.id !== 'cscb-essential') { 8969 cb.checked = false;
8970 } 8971 }); 8972 } 8973 8974 8975 8976 /** 8977 * google tag manager (gtm) handling 8978 */ 8979 async loadGtmSettings() { 8980 const response = await fetch(CsCookieBannerHandler.options.path.gtmConfigJson); 8981 if (!response.ok) throw new Error(CsCookieBannerHandler.options.loadGtmConfigErrMsg); 8982 8983 const configData = await response.json(); 8984 8985 this.gtmData = configData['googleTagManagerKeys']; 8986 } 8987 8988 initDataLayer() { 8989 if (this._tagManagerDataLayerSet) return; 8990 window.dataLayer = window.dataLayer || []; 8991 window.gtag = window.gtag || function(){ window.dataLayer.push(arguments); }; 8992 this._tagManagerDataLayerSet = true; 8993 } 8994 8995 setGoogleTag(method = 'default', value = 'denied', command = 'consent') { 8996 // eslint-disable-next-line 8997 gtag(command, method, { 8998 ad_user_data: value, 8999 ad_personalization: value, 9000 ad_storage: value, 9001 analytics_storage: value 9002 }); 9003 } 9004 9005 setTagManagerDefaults() { 9006 this.initDataLayer(); 9007 if (!this._consentDefaultSet) { 9008 this.setGoogleTag(); 9009 this._consentDefaultSet = true; 9010 } 9011 } 9012 9013 revokeTagManager() { 9014 this.initDataLayer(); 9015 this.setGoogleTag('update', 'denied'); 9016 this._gtmLoaded = false; 9017 } 9018 9019 loadGtmScript(gtmContainerId) { 9020 if (this._gtmLoaded || document.querySelector('script[src*="googletagmanager.com/gtm.js"]')) return; 9021 9022 this.initDataLayer(); 9023 window.dataLayer.push({ 'gtm.start': Date.now(), event: 'gtm.js' }); 9024 9025 const s = document.createElement('script'); 9026 s.async = true; 9027 s.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(gtmC
9027ontainerId)}`; 9028 document.head.appendChild(s); 9029 9030 this._gtmLoaded = true; 9031 } 9032 9033 loadTagManager() { 9034 9035 const { cookieName, gtmCookieText } = CsCookieBannerHandler.options; 9036 9037 // set defaults 9038 this.setTagManagerDefaults(); 9039 9040 // check if user accepted gtm cookie 9041 const cookieString = api.get(cookieName) || ''; 9042 const hasConsent = cookieString.includes(gtmCookieText); 9043 9044 // no consent 9045 if (!hasConsent) { 9046 this.revokeTagManager(); 9047 return; 9048 } 9049 9050 // set consent 9051 this.setGoogleTag('update', 'granted'); 9052 9053 // get gtm container id 9054 let domain = window.location.hostname; 9055 const gtmContainerId = this.gtmData?.[domain] || null; 9056 if (!gtmContainerId || !/^GTM-\w+/.test(gtmContainerId)) { 9057 console.warn('GTM container ID ist ungültig oder fehlt: ', gtmContainerId); 9058 return; 9059 } 9060 9061 // load gtm script 9062 this.loadGtmScript(gtmContainerId); 9063 } 9064 9065 9066 9067 /** 9068 * external media content 9069 */ 9070 screenreaderAnnouncement(wrapper, message = '') { 9071 const region = wrapper?.querySelector(CsCookieBannerHandler.options.selector.externalMediaLiveRegion); 9072 if (!region) return; 9073 9074 region.textContent = ''; 9075 requestAnimationFrame(() => { region.textContent = message; }); 9076 } 9077 9078 showExternalMediaContent(externalMediaElement) { 9079 if (!externalMediaElement) return; 9080 9081 const wrapper = externalMediaElement.closest(CsCookieBannerHandler.options.selector.externalMediaWrapper); 9082 const message = wrapper?.dataset.srActivateMessage; 9083 const isIframe = externalMediaElement instanceof HTMLIFrameElement || externalMediaElement.tagName === 'IFRAME'; 9084 9085 // add loading state while the external media element is fetching its content 9086 if (wrapper) wrapper.classList.add(CsCookieBannerHandler.options.selector.externalMediaLoadingClass); 9087 9088 if (isIframe) { 9089 const src = externalMediaElement.dataset.src; 9090 if (!src) return; 9091 9092 // remove loading class once the external media element has finished loading 9093 externalMediaElement.addEventListener('load', () => { 9094 9095 // remove loading class 9096 if (wrapper) wrapper.classList.remove(CsCookieBannerHandler.options.selector.externalMediaLoadingClass); 9097 9098 // include in focus tab order 9099 externalMediaElement.tabIndex = 0; 9100 9101 // live region announcement 9102 this.screenreaderAnnouncement(wrapper, message); 9103 9104 // focus on iframe 9105 externalMediaElement.focus(); 9106 }, { once: true }); // prevents stacking multiple listeners on repeated calls 9107 9108 // set src from data-src 9109 externalMediaElement.setAttribute('src', src); 9110 externalMediaElement.removeAttribute('data-src'); 9111 9112 // show the iframe 9113 externalMediaElement.hidden = false; 9114 } else { 9115 // remove loading class 9116 if (wrapper) wrapper.classList.remove(CsCookieBannerHandler.options.selector.externalMediaLoadingClass); 9117 9118 // live region announcement 9119 this.screenreaderAnnouncement(wrapper, message); 9120 9121 // show the external media element 9122 externalMediaElement.hidden = false; 9123 } 9124 } 9125 9126 removeExternalMediaContent(externalMediaElement) { 9127 9128 // for iframes 9129 if (externalMediaElement.hasAttribute('src')) { 9130 // remove src and set data-src (e.g. if iframe) 9131 const src = externalMediaElement.getAttribute('src'); 9132 externalMediaElement.setAttribute('data-src', src); 9133 externalMediaElement.removeAttribute('src'); 9134 9135 externalMediaElement.tabIndex = -1; 9136 } 9137 9138 // hide the external media element 9139 externalMediaElement.hidden = true; 9140 } 9141 9142 refreshExternalMediaContent() { 9143 const cookieString = api.get(CsCookieBannerHandler.options.cookieName); 9144 9145 const externalMediaElements = document.querySelectorAll(CsCookieBannerHandler.options.selector.externalMedia); 9146 9147 externalMediaElements.forEach(externalMediaElement => { 9148 const cookieName = externalMediaElement.dataset.cookieName; 9149 if (!cookieName) { 9150 console.warn('No cookie name defined for media element (e.g. check "CookieBannerMedia.html")'); 9151 return; 9152 } 9153 9154 const externalMediaElementConfig = this.cookieBannerRenderer.data.cookieSettings.externalMedia.media[cookieName]; 9155 const wrapper = externalMediaElement.closest(CsCookieBannerHandler.options.selector.externalMediaWrapper); 9156 9157 if (externalMediaElementConfig.previewBox) { 9158 const previewBox = externalMediaElement.nextElementSibling; 9159 if (cookieString && cookieString.includes(cookieName)) { 9160 9161 // show content 9162 this.showExternalMediaContent(externalMediaElement); 9163 9164 // remove preview box
9165 if (previewBox && previewBox.classList.contains(CsCookieBannerHandler.options.selector.externalMediaPreviewBoxClass)) { 9166 previewBox.remove(); 9167 } 9168 if (wrapper) wrapper.classList.remove(CsCookieBannerHandler.options.selector.externalMediaWrapperPreviewBoxIsVisibleClass); 9169 } else { 9170 9171 // remove content 9172 this.removeExternalMediaContent(externalMediaElement); 9173 9174 // create preview box 9175 if (!previewBox || !previewBox.classList.contains(CsCookieBannerHandler.options.selector.externalMediaPreviewBoxClass)) { 9176 this.cookieBannerRenderer.buildMediaPreviewBox(externalMediaElement, cookieName); 9177 } 9178 if (wrapper) wrapper.classList.add(CsCookieBannerHandler.options.selector.externalMediaWrapperPreviewBoxIsVisibleClass); 9179 } 9180 } 9181 9182 }); 9183 } 9184 9185 customFunctions = { 9186 activateGoogleMapsExt: () => { 9187 const googleMapsDivs = document.querySelectorAll('div[data-cookie-name="maps_google"]'); 9188 9189 googleMapsDivs.forEach((div) => { 9190 const libraryUrl = div.dataset.library; 9191 9192 if (libraryUrl) { 9193 const script = document.createElement('script'); 9194 script.src = libraryUrl; 9195 document.body.appendChild(script); 9196 9197 document.querySelector('.js-gme-container').removeAttribute('style'); 9198 } 9199 }); 9200 }, 9201 activateSmartsupp: () => { 9202 const smartsuppDiv = document.querySelector('div[data-cookie-name="chat_smartsupp"]'); 9203 9204 if (smartsuppDiv) { 9205 const smartsuppCode = ` 9206 var _smartsupp = _smartsupp || {}; 9207 _smartsupp.key = '4b057508eb9b06f5cb0d4d8d087669bb0a8535a7'; 9208 window.smartsupp || (function (d) { 9209 var s, c, o = smartsupp = function () { o._.push(arguments) }; o._ = []; 9210 s = d.getElementsByTagName('script')[0]; c = d.createElement('script'); 9211 c.type = 'text/javascript'; c.charset = 'utf-8'; c.async = true; 9212 c.src = 'https://www.smartsuppchat.com/loader.js?'; 9213 s.parentNode.insertBefore(c, s); 9214 })(document); 9215 `; 9216 9217 const scriptEl = document.createElement('script'); 9218 scriptEl.type = 'text/javascript'; 9219 scriptEl.text = smartsuppCode; 9220 9221 document.body.appendChild(scriptEl); 9222 9223 // @TODO magnific was replaced by tobii 9224 // eslint-disable-next-line 9225 if (window.jQuery && $.magnificPopup) { 9226 // eslint-disable-next-line 9227 $.magnificPopup.close(); 9228 } 9229 } 9230 }, 9231 activateTrustYou: () => { 9232 const trustYouIframes = document.querySelectorAll('iframe[data-cookie-name="trust_you"]'); 9233 9234 trustYouIframes.forEach((iframe) => { 9235 const libraryUrl = iframe.dataset.library; 9236 9237 // If library URL is present, load resizer 9238 if (libraryUrl) { 9239 const script = document.createElement('script'); 9240 script.src = libraryUrl; 9241 document.body.appendChild(script); 9242 } 9243 9244 // Wait a couple of milliseconds before calling the resize function, 9245 // because the iframe is loaded asynchronously 9246 setTimeout(() => { 9247 9248 if (typeof iFrameResize === 'function') { 9249 // eslint-disable-next-line 9250 iFrameResize({ log: false }, iframe); 9251 } else { 9252 console.warn('iFrameResize function not found. Is the library loaded?'); 9253 } 9254 }, 500); 9255 }); 9256 }, 9257 activateHotelSpider: () => { 9258 9259 const hotelSpiderDiv = document.querySelector('div[data-cookie-name="hotelspider_cookie"]'); 9260 9261 if (hotelSpiderDiv) { 9262 // load CSS 9263 const linkHotelSpider = document.createElement('link'); 9264 linkHotelSpider.rel = 'stylesheet'; 9265 linkHotelSpider.href = 'https://wbe-static.hotel-spider.com/widget/spiderBooking4.css'; 9266 linkHotelSpider.type = 'text/css'; 9267 document.head.appendChild(linkHotelSpider); 9268 9269 // load JS 9270 const scriptHotelSpider = document.createElement('script'); 9271 scriptHotelSpider.src = 'https://wbe-static.hotel-spider.com/widget/spiderBooking4.js'; 9272 scriptHotelSpider.async = true; 9273 scriptHotelSpider.onload = function () { 9274 // eslint-disable-next-line 9275 if (typeof jQuery.fn.SpiderBooking4 === 'function') { 9276 const spiderBookingContainer = document.getElementById('spiderBooking'); 9277 if (spiderBookingContainer) { 9278 // eslint-disable-next-line 9279 jQuery(spiderBookingContainer).SpiderBooking4({ 9280 hotelId: '02H5e00cb52afeb0', 9281 template: spiderBookingContainer.dataset.template, 9282 lang: spiderBookingContainer.dataset.lang 9283 }); 9284 } 9285 } 9286 }; 9287 document.head.appendChild(scriptHotelSpider); 9288 } 9289 } 9290 }; 9291 9292 loadExternalScript(url) { 9293 return new Promise((resolve, reject) => { 9294 // check if script is already loaded 9295 if (document.querySelector(`script[src="${url}"]`)) { 9296 resolve(); 9297 return; 9298 } 9299 9300 // create script element 9301 const s = document.createElement('script'); 9302 s.src = url; 9303 s.async = true; 9304 s.onload = () => resolve(); 9305 s.onerror = () => reject(new Error(`Failed to load ${url}`)); 9306 document.head.appendChild(s); 9307 }); 9308 } 9309 9310 initAdditionalFunctions() { 9311 const externalMediaElementConfig = this.cookieBannerRenderer.data.cookieSettings.externalMedia.media; 9312 const cookieString = api.get(CsCookieBannerHandler.options.cookieName); 9313 9314 Object.entries(externalMediaElementConfig).forEach(([cookieName, item]) => { 9315 if (item.function && this.customFunctions[item.function] && cookieString && cookieString.includes(cookieName)) { 9316 this.customFunctions[item.function](); 9317 } 9318 9319 if (item.urlToScript && cookieString && cookieString.includes(cookieName)) { 9320 this.loadExternalScript(item.urlToScript).catch(err => console.warn(err)); 9321 } 9322 }); 9323 } 9324 9325 9326 9327 /** 9328 * cookie persistence (cookie string handling) 9329 */ 9330 saveCookies() { 9331 const checkboxes = document.querySelectorAll(CsCookieBannerHandler.options.selector.checkbox); 9332 const selectedKeys = []; 9333
9334 checkboxes.forEach(cb => { 9335 if (cb.checked) { 9336 const id = cb.id || ''; 9337 if (id.startsWith('cscb-')) { 9338 const key = id.replace('cscb-', ''); 9339 selectedKeys.push(key); 9340 } 9341 } 9342 }); 9343 9344 if (selectedKeys.includes(this.options.gtmMainCategory) && !selectedKeys.includes(CsCookieBannerHandler.options.gtmCookieText)) { 9345 selectedKeys.push(CsCookieBannerHandler.options.gtmCookieText); 9346 } 9347 9348 const cookieValue = `|${selectedKeys.join('|')}|`; 9349 9350 api.set(CsCookieBannerHandler.options.cookieName, cookieValue, { 9351 expires: CsCookieBannerHandler.options.expireDays, 9352 path: '/' 9353 }); 9354 9355 this.loadTagManager(); 9356 9357 this.refreshExternalMediaContent(); 9358 9359 this.initAdditionalFunctions(); 9360 } 9361 9362 9363 9364 /** 9365 * event handling (interaction logic) 9366 */ 9367 handleContrastToggle() { 9368 const contrastButtonPageSelector = document.querySelector(CsCookieBannerHandler.options.selector.contrastButtonPage); 9369 if (!contrastButtonPageSelector) return; 9370 9371 contrastButtonPageSelector.click(); 9372 } 9373 9374 handleCheckboxChange(target) { 9375 if (target.classList.contains(CsCookieBannerHandler.options.selector.checkboxParentClass)) { 9376 this.handleCheckboxChildrenStatePropagation(target); 9377 } 9378 9379 if (target.classList.contains(CsCookieBannerHandler.options.selector.checkboxChildClass)) { 9380 this.handleCheckboxParentStatePropagation(target); 9381 } 9382 } 9383 9384 acceptAllCookies() { 9385 this.activateAllCheckboxes(); 9386 this.saveCookies(); 9387 9388 const cookieBannerLayer = document.querySelector(CsCookieBannerHandler.options.selector.cookieBannerLayer); 9389 if (cookieBannerLayer) { 9390 this.closeCookieBannerLayer(); 9391 } 9392 } 9393 9394 acceptExternalMediaCookie(trigger) { 9395 9396 const cookieKey = trigger.dataset.csCookieKey; 9397 const checkboxId = `cscb-${cookieKey}`; 9398 const checkbox = document.getElementById(checkboxId); 9399 9400 if (this.options.openModalOnExternalMediaFrame) { 9401 9402 // open modal 9403 if (this.modalManager) { 9404 this.modalManager.open(CsCookieBannerHandler.options.modalName, false, trigger); 9405 } 9406 9407 // trigger accordion item, if it is not expanded 9408 const accordionTrigger = document.getElementById(CsCookieBannerHandler.options.selector.externalMediaAccordionTriggerId); 9409 if (accordionTrigger && accordionTrigger.getAttribute('aria-expanded') === 'false') { 9410 accordionTrigger.click(); 9411 } 9412 9413 // scroll to checkbox 9414 checkbox.scrollIntoView({ 9415 behavior: 'smooth' 9416 }); 9417 9418 // focus checkbox after accordion item has opened 9419 requestAnimationFrame(() => { 9420 checkbox.focus(); 9421 }); 9422 9423 } else { 9424 9425 // set checkbox and save cookies 9426 checkbox.checked = true; 9427 this.handleCheckboxChange(checkbox); 9428 this.saveCookies(); 9429 } 9430 } 9431 9432 declineAllCookies() { 9433 this.deactivateAllCheckboxes(); 9434 this.saveCookies(); 9435 9436 const cookieBannerLayer = document.querySelector(CsCookieBannerHandler.options.selector.cookieBannerLayer); 9437 if (cookieBannerLayer) { 9438 this.closeCookieBannerLayer(); 9439 } 9440 } 9441 9442 customizeSettings(trigger) { 9443 9444 // open modal 9445 if (this.modalManager) { 9446 this.modalManager.open(CsCookieBannerHandler.options.modalName, true, trigger); 9447 } 9448 } 9449 9450 saveSettings() { 9451 this.saveCookies(); 9452 } 9453 9454 9455 9456 /** 9457 * event binding (listeners) 9458 */ 9459 bindModalOpenEvent() { 9460 document.addEventListener('cs-modal-open', () => { 9461 9462 // close cookie banner layer 9463 this.closeCookieBannerLayer(false); 9464 }); 9465 } 9466 9467 bindCloseEvent() { 9468 /* modal close is handled by modal manager, 9469 * open cookie banner layer if no cookies are set 9470 * and modal does not open on start 9471 * or it does open, but is on ignore list for this url 9472 */ 9473 document.addEventListener('cs-modal-close', (e) => { 9474 const { name: modalName, triggerLink } = e.detail; 9475 if (modalName !== 'cscb-cookie-banner') return; 9476 9477 // background layer (= modal) and close button have dismiss event 9478 // triggerLink is empty for escape key 9479 if (triggerLink?.dataset.cscbDismissEvent === 'true' || !triggerLink) { 9480 const cookieString = api.get(CsCookieBannerHandler.options.cookieName); 9481 if (cookieString || (CsCookieBannerHandler.options.openModalOnStart && !this.isUrlOnIgnoreList(
9481))) return; 9482 9483 this.openCookieBannerLayer(); 9484 } 9485 }); 9486 } 9487 9488 bindContrastButton() { 9489 const contrastBtn = document.querySelector(CsCookieBannerHandler.options.selector.contrastButton); 9490 if (contrastBtn) { 9491 contrastBtn.addEventListener('click', () => { 9492 this.handleContrastToggle(); 9493 }); 9494 } 9495 } 9496 9497 bindCheckboxEvents() { 9498 const checkboxes = document.querySelectorAll(CsCookieBannerHandler.options.selector.checkbox); 9499 checkboxes.forEach(checkbox => { 9500 checkbox.addEventListener('change', (event) => { 9501 this.handleCheckboxChange(event.target); 9502 }); 9503 }); 9504 } 9505 9506 bindConsentButtons() { 9507 const acceptButtons = document.querySelectorAll(CsCookieBannerHandler.options.selector.acceptAllCookies); 9508 const declineButtons = document.querySelectorAll(CsCookieBannerHandler.options.selector.declineAllCookies); 9509 const customizeSettingsButton = document.querySelector(CsCookieBannerHandler.options.selector.customizeSettings); 9510 const saveSettingsBtn = document.querySelector(CsCookieBannerHandler.options.selector.saveSettings); 9511 const acceptExternalMediaFrame = document.querySelectorAll(CsCookieBannerHandler.options.selector.externalMediaWrapper); 9512 9513 acceptExternalMediaFrame.forEach(acceptFrame => { 9514 acceptFrame.addEventListener('click', (e) => { 9515 if (e.target.classList.contains(CsCookieBannerHandler.options.selector.externalMediaAcceptButtonClass)) { 9516 this.acceptExternalMediaCookie(e.target); 9517 } 9518 }); 9519 }); 9520 9521 acceptButtons.forEach(acceptBtn => { 9522 acceptBtn.addEventListener('click', () => { 9523 this.acceptAllCookies(); 9524 }); 9525 }); 9526 9527 declineButtons.forEach(declineBtn => { 9528 declineBtn.addEventListener('click', () => { 9529 this.declineAllCookies(); 9530 }); 9531 }); 9532 9533 customizeSettingsButton.addEventListener('click', () => { 9534 this.customizeSettings(customizeSettingsButton); 9535 }); 9536 9537 if (saveSettingsBtn) saveSettingsBtn.addEventListener('click', () => { 9538 this.saveSettings(); 9539 }); 9540 } 9541 9542 bindEvents() { 9543 9544 // modal events 9545 this.modalManager.bindCloseSelectors(document.querySelector(this.options.selector.cookieBannerModal)); 9546 9547 // specific cscb events 9548 this.bindCloseEvent(); 9549 this.bindContrastButton(); 9550 this.bindCheckboxEvents(); 9551 this.bindConsentButtons(); 9552 this.bindModalOpenEvent(); 9553 } 9554 9555 9556 9557 /** 9558 * display state for layer and modal 9559 */ 9560 openCookieBannerModal() { 9561 if (this.modalManager) { 9562 this.modalManager.open(CsCookieBannerHandler.options.modalName); 9563 } 9564 } 9565 9566 toggleCookieBannerLayer(stateOpen = true, setCloseFocus = true) { 9567 const cookieBannerLayer = document.querySelector(CsCookieBannerHandler.options.selector.cookieBannerLayer); 9568 if (!cookieBannerLayer) return; 9569 9570 if (stateOpen) { 9571 9572 // open layer 9573 cookieBannerLayer.classList.add(CsCookieBannerHandler.options.selector.showClass); 9574 requestAnimationFrame(() => { 9575 cookieBannerLayer.classList.add(CsCookieBannerHandler.options.selector.animationClass); 9576 }); 9577 } else { 9578 9579 // close layer 9580 cookieBannerLayer.classList.remove(CsCookieBannerHandler.options.selector.animationClass); 9581 setTimeout(() => { 9582 cookieBannerLayer.classList.remove(CsCookieBannerHandler.options.selector.showClass); 9583 }, CssProperties.getCssTimeProperty(CsCookieBannerHandler.options.cssVariables.cookieBannerAnimationTime)); 9584 9585 // focus first element in body, if set 9586 if (setCloseFocus) { 9587 const firstFocusableElement = document.querySelector(CsCookieBannerHandler.options.selector.firstFocusableElement); 9588 if (firstFocusableElement) { 9589 firstFocusableElement.focus(); 9590 } 9591 } 9592 } 9593 9594 // add or remove space 9595 this.reserveSpaceForCookieBannerLayer(stateOpen); 9596 } 9597 9598 closeCookieBannerLayer(setFocus = true) { 9599 this.toggleCookieBannerLayer(false, setFocus); 9600 } 9601 9602 openCookieBannerLayer() { 9603 this.toggleCookieBannerLayer(); 9604 } 9605 9606 openCookieBanner() { 9607 const cookieString = api.get(CsCookieBannerHandler.options.cookieName); 9608 if (cookieString) return; 9609 9610 if (CsCookieBannerHandler.options.openModalOnStart && !this.isUrlOnIgnoreList(
9610)) { 9611 this.openCookieBannerModal(); 9612 } else { 9613 this.openCookieBannerLayer(); 9614 } 9615 } 9616 9617 9618 9619 /** 9620 * main init function 9621 */ 9622 init() { 9623 document.addEventListener('cscb:built', () => { 9624 this.modalManager = PluginController.retrievePluginInstanceFromElement(document, 'CsModalManager'); 9625 this.cookieBannerRenderer = PluginController.retrievePluginInstanceFromElement(this.el, 'CsCookieBannerRenderer'); 9626 9627 this.initializeCookieBannerContent(); 9628 this.bindEvents(); 9629 this.openCookieBanner(); 9630 this.refreshExternalMediaContent(); 9631 this.initAdditionalFunctions(); 9632 9633 this.loadGtmSettings().then(() => { 9634 this.loadTagManager(); 9635 }).catch(error => { 9636 console.warn('Could not load Google Tag Manager keys.', error); 9637 }); 9638 9639 window.addEventListener('resize', () => { 9640 const layer = document.querySelector(CsCookieBannerHandler.options.selector.cookieBannerLayer); 9641 if (layer && layer.classList.contains(CsCookieBannerHandler.options.selector.showClass)) { 9642 this.reserveSpaceForCookieBannerLayer(true); 9643 } 9644 }); 9645 9646 document.dispatchEvent(new CustomEvent('cscb:initialized')); 9647 }); 9648 } 9649} 9650 9651// core feature do not remove this line 9652 9653// register plugins for initializing 9654PluginController.addPlugin('CsModalManager', CsModalManager); 9655 9656PluginController.addPlugin('CsPhoneLinks', CsPhoneLinks, 'a[href^="tel:"]'); 9657PluginController.addPlugin('CsExternalLinks', CsExternalLinks, 'a[target="_blank"]'); 9658PluginController.addPlugin('CsAccessibilityAnimation', CsAccessibilityAnimation, '.js-toggle-animations'); 9659 9660PluginController.addPlugin('CsScrollbar', CsScrollbar); 9661PluginController.addPlugin('CsMainNavigation', CsMainNavigation, '.js-menu-main'); 9662PluginController.addPlugin('CsSplide', CsSplide, '.js-splide-slider'); 9663PluginController.addPlugin('CsResponsiveVideo', CsResponsiveVideo, '.js-responsive-video'); 9664PluginController.addPlugin('CsLightbox', CsLightbox); 9665PluginController.addPlugin('CsScrollToNextFrame', CsScrollToNextFrame, '.js-scroll-to-next-frame'); 9666PluginController.addPlugin('CsEdgeNav', CsEdgeNav, '.js-edge-nav'); 9667PluginController.addPlugin('CsAccordion', CsAccordion, '.js-accordion'); 9668PluginController.addPlugin('CsFlipText', CsFlipText, '.js-flip-text-trigger'); 9669PluginController.addPlugin('CsCookieBannerRenderer', CsCookieBannerRenderer, '.js-cscb-edit-box'); 9670PluginController.addPlugin('CsCookieBannerHandler', CsCookieBannerHandler, '.js-cscb-edit-box'); 9671 9672// core feature must be placed at the bottom of this script 9673document.addEventListener('DOMContentLoaded', 9674 () => PluginController.initializeAllPlugins(), false); 9675 9676export { PluginControlInstance }; 9677//# sourceMappingURL=bundle.main.js.map
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.