1window.isInEditor = function () { 2 try { 3 return window.self !== window.top && window.top.location.href.indexOf('/editor.html') ? true : false; 4 } catch (e) { 5 return false; 6 } 7}; 8 9/******************************************************************************* 10 * Copyright 2020 Adobe 11 * 12 * Licensed under the Apache License, Version 2.0 (the "License"); 13 * you may not use this file except in compliance with the License. 14 * You may obtain a copy of the License at 15 * 16 * http://www.apache.org/licenses/LICENSE-2.0 17 * 18 * Unless required by applicable law or agreed to in writing, software 19 * distributed under the License is distributed on an "AS IS" BASIS, 20 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 * See the License for the specific language governing permissions and 22 * limitations under the License. 23 ******************************************************************************/ 24(function() { 25 "use strict"; 26 27 window.CQ = window.CQ || {}; 28 window.CQ.CoreComponents = window.CQ.CoreComponents || {}; 29 window.CQ.CoreComponents.container = window.CQ.CoreComponents.container || {}; 30 window.CQ.CoreComponents.container.utils = {}; 31 32 /** 33 * Utilities for Container Components (accordion, tabs) 34 * 35 * @namespace 36 * @alias CQ.CoreComponents.container.utils 37 * @type {{}} 38 */ 39 window.CQ.CoreComponents.container.utils = { 40 41 /** 42 * Removes the hash from the URL. 43 */ 44 removeUrlHash: function() { 45 history.replaceState(undefined, undefined, " "); 46 }, 47 48 /** 49 * Updates the URL hash with the panel ID without scrolling to it. 50 * 51 * @param {Object} component The container component (e.g. Accordion, Carousel, Tabs). 52 * @param {String} itemType The type of the item as defined in the component. 53 * @param {Number} index The index of the container item 54 */ 55 updateUrlHash: function(component, itemType, index) { 56 if (component && component._elements && component._elements[itemType] && 57 component._elements[itemType][index] && component._elements[itemType][index].id) { 58 var ID = component._elements[itemType][index].id; 59 history.replaceState(undefined, undefined, "#" + ID); 60 } 61 }, 62 63 /** 64 * Returns the index of the component item (accordion, carousel, tabs) that: 65 * - either corresponds to the deep link in the URL fragment 66 * - or contains the element that corresponds to the deep link in the URL fragment 67 * 68 * @param {Object} component The container component (Accordion, Carousel or Tabs). 69 * @param {String} itemType The type of the item as defined in the component. 70 * @param {String} itemContentType The type of the item content as defined in the component. 71 * @returns {Number} the index within the items array if the item exists, -1 otherwise. 72 */ 73 getDeepLinkItemIdx: function(component, itemType, itemContentType) { 74 if (window.location.hash) { 75 var deepLinkId = window.location.hash.substring(1); 76 if (deepLinkId && document.getElementById(deepLinkId) && 77 component && component._config && component._config.element && 78 component._elements[itemType] && 79 component._config.element.querySelector("[id='" + deepLinkId + "']")) { 80 for (var i = 0; i < component._elements[itemType].length; i++) { 81 var item = component._elements[itemType][i]; 82 var itemContentContainsId = false; 83 if (component._elements[itemContentType]) { 84 var itemContent = component._elements[itemContentType][i]; 85 itemContentContainsId = itemContent && itemContent.querySelector("[id='" + deepLinkId + "']"); 86 } 87 if (item.id === deepLinkId || itemContentContainsId) { 88 return i; 89 } 90 } 91 } 92 return -1; 93 } 94 return -1; 95 }, 96 97 /** 98 * Returns the item of the container component (accordion, carousel, tabs) that: 99 * - either corresponds to the deep link in the URL fragment 100 * - or contains the element that corresponds to the deep link in the URL fragment 101 * 102 * @param {Object} component The Accordion or Tabs component. 103 * @param {String} itemType The type of the item as defined in the component. 104 * @param {String} itemContentType The type of the item content as defined in the component. 105 * @returns {Object} the item if it exists, undefined otherwise. 106 */ 107 getDeepLinkItem: function(component, itemType, itemContentType) {
108 var idx = window.CQ.CoreComponents.container.utils.getDeepLinkItemIdx(component, itemType, itemContentType); 109 if (component && component._elements && component._elements[itemType]) { 110 return component._elements[itemType][idx]; 111 } 112 }, 113 114 /** 115 * Scrolls the browser on page reload (if URI contains URI fragment) to the item of the container component (accordion, tabs) 116 that corresponds to the deep link in the URI fragment. 117 * This method fixes the issue existent with Chrome and related browsers, which are not scrolling on page reload (if URI contains URI fragment) 118 to the element that corresponds to the deep link in the URI fragment. 119 * Small setTimeout is needed, otherwise the scrolling will not work on Chrome. 120 */ 121 scrollToAnchor: function() { 122 setTimeout(function() { 123 if (window.location.hash) { 124 var id = decodeURIComponent(window.location.hash.substring(1)); 125 var anchorElement = document.getElementById(id); 126 if (anchorElement && anchorElement.offsetTop) { 127 anchorElement.scrollIntoView(); 128 } 129 } 130 }, 100); 131 } 132 }; 133}()); 134 135/******************************************************************************* 136 * Copyright 2019 Adobe 137 * 138 * Licensed under the Apache License, Version 2.0 (the "License"); 139 * you may not use this file except in compliance with the License. 140 * You may obtain a copy of the License at 141 * 142 * http://www.apache.org/licenses/LICENSE2.0 143 * 144 * Unless required by applicable law or agreed to in writing, software 145 * distributed under the License is distributed on an "AS IS" BASIS, 146 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 147 * See the License for the specific language governing permissions and 148 * limitations under the License. 149 ******************************************************************************/ 150 151/** 152 * Element.matches() 153 * https://developer.mozilla.org/enUS/docs/Web/API/Element/matches#Polyfill 154 */ 155if (!Element.prototype.matches) { 156 Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 157} 158 159// eslint-disable-next-line valid-jsdoc 160/** 161 * Element.closest() 162 * https://developer.mozilla.org/enUS/docs/Web/API/Element/closest#Polyfill 163 */ 164if (!Element.prototype.closest) { 165 Element.prototype.closest = function(s) { 166 "use strict"; 167 var el = this; 168 if (!document.documentElement.contains(el)) { 169 return null; 170 } 171 do { 172 if (el.matches(s)) { 173 return el; 174 } 175 el = el.parentElement || el.parentNode; 176 } while (el !== null && el.nodeType === 1); 177 return null; 178 }; 179} 180 181/******************************************************************************* 182 * Copyright 2019 Adobe 183 * 184 * Licensed under the Apache License, Version 2.0 (the "License"); 185 * you may not use this file except in compliance with the License. 186 * You may obtain a copy of the License at 187 * 188 * http://www.apache.org/licenses/LICENSE-2.0 189 * 190 * Unless required by applicable law or agreed to in writing, software 191 * distributed under the License is distributed on an "AS IS" BASIS, 192 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 * See the License for the specific language governing permissions and 194 * limitations under the License. 195 ******************************************************************************/ 196(function() { 197 "use strict"; 198 199 var containerUtils = window.CQ && window.CQ.CoreComponents && window.CQ.CoreComponents.container && window.CQ.CoreComponents.container.utils ? window.CQ.CoreComponents.container.utils : undefined; 200 if (!containerUtils) { 201 // eslint-disable-next-line no-console 202 console.warn("Accordion: container utilities at window.CQ.CoreComponents.container.utils are not available. This can lead to missing features. Ensure the core.wcm.components.c
202ommons.site.container client library is included on the page."); 203 } 204 var dataLayerEnabled; 205 var dataLayer; 206 var dataLayerName; 207 var delay = 100; 208 209 var NS = "cmp"; 210 var IS = "accordion"; 211 212 var keyCodes = { 213 ENTER: 13, 214 SPACE: 32, 215 END: 35, 216 HOME: 36, 217 ARROW_LEFT: 37, 218 ARROW_UP: 38, 219 ARROW_RIGHT: 39, 220 ARROW_DOWN: 40 221 }; 222 223 var selectors = { 224 self: "[data-" + NS + '-is="' + IS + '"]' 225 }; 226 227 var cssClasses = { 228 button: { 229 disabled: "cmp-accordion__button--disabled", 230 expanded: "cmp-accordion__button--expanded" 231 }, 232 panel: { 233 hidden: "cmp-accordion__panel--hidden", 234 expanded: "cmp-accordion__panel--expanded" 235 } 236 }; 237 238 var dataAttributes = { 239 item: { 240 expanded: "data-cmp-expanded" 241 } 242 }; 243 244 var properties = { 245 /** 246 * Determines whether a single accordion item is forced to be expanded at a time. 247 * Expanding one item will collapse all others. 248 * 249 * @memberof Accordion 250 * @type {Boolean} 251 * @default false 252 */ 253 "singleExpansion": { 254 "default": false, 255 "transform": function(value) { 256 return !(value === null || typeof value === "undefined"); 257 } 258 } 259 }; 260 261 /** 262 * Accordion Configuration. 263 * 264 * @typedef {Object} AccordionConfig Represents an Accordion configuration 265 * @property {HTMLElement} element The HTMLElement representing the Accordion 266 * @property {Object} options The Accordion options 267 */ 268 269 /** 270 * Accordion. 271 * 272 * @class Accordion 273 * @classdesc An interactive Accordion component for toggling panels of related content 274 * @param {AccordionConfig} config The Accordion configuration 275 */ 276 function Accordion(config) { 277 var that = this; 278 279 if (config && config.element) { 280 init(config); 281 } 282 283 /** 284 * Initializes the Accordion. 285 * 286 * @private 287 * @param {AccordionConfig} config The Accordion configuration 288 */ 289 function init(config) { 290 that._config = config; 291 292 // prevents multiple initialization 293 config.element.removeAttribute("data-" + NS + "-is"); 294 295 setupProperties(config.options); 296 cacheElements(config.element); 297 298 if (that._elements["item"]) { 299 // ensures multiple element types are arrays. 300 that._elements["item"] = Array.isArray(that._elements["item"]) ? that._elements["item"] : [that._elements["item"]]; 301 that._elements["button"] = Array.isArray(that._elements["button"]) ? that._elements["button"] : [that._elements["button"]]; 302 that._elements["panel"] = Array.isArray(that._elements["panel"]) ? that._elements["panel"] : [that._elements["panel"]]; 303 304 if (that._properties.singleExpansion) { 305 var expandedItems = getExpandedItems(); 306 // multiple expanded items annotated, display the last item open. 307 if (expandedItems.length > 1) { 308 toggle(expandedItems.length - 1); 309 } 310 } 311 312 refreshItems(); 313 bindEvents(); 314 scrollToDeepLinkIdInAccordion(); 315 } 316 if (window.Granite && window.Granite.author && window.Granite.author.MessageChannel) { 317 /* 318 * Editor message handling: 319 * - subscribe to "cmp.panelcontainer" message requests sent by the editor frame 320 * - check that the message data panel container type is correct and that the id (path) matches this specific Accordion component 321 * - if so, route the "navigate" operation to enact a navigation of the Accordion based on index data 322 */ 323 window.CQ.CoreComponents.MESSAGE_CHANNEL = window.CQ.CoreComponents.MESSAGE_CHANNEL || new window.Granite.author.MessageChannel("cqauthor", window); 324 window.CQ.CoreComponents.MESSAGE_CHANNEL.subscribeRequestMessage("cmp.panelcontainer", function(message) { 325 if (message.data && message.data.type === "cmp-accordion" && message.data.id === that._elements.self.dataset["cmpPanelcontainerId"]) { 326 if (message.data.operation === "navigate") { 327 // switch to single expansion mode when navigating in edit mode. 328 var singleExpansion = that._properties.singleExpansion; 329 that._properties.singleExpansion = true; 330 toggle(message.data.index); 331 332 // revert to the configured state. 333 that._properties.singleExpansion = singleExpansion; 334 } 335 } 336 }); 337 } 338 } 339 340 /**
341 * Displays the panel containing the element that corresponds to the deep link in the URI fragment 342 * and scrolls the browser to this element. 343 */ 344 function scrollToDeepLinkIdInAccordion() { 345 if (containerUtils) { 346 var deepLinkItemIdx = containerUtils.getDeepLinkItemIdx(that, "item", "item"); 347 if (deepLinkItemIdx > -1) { 348 var deepLinkItem = that._elements["item"][deepLinkItemIdx]; 349 if (deepLinkItem && !deepLinkItem.hasAttribute(dataAttributes.item.expanded)) { 350 // if single expansion: close all accordion items 351 if (that._properties.singleExpansion) { 352 for (var j = 0; j < that._elements["item"].length; j++) { 353 if (that._elements["item"][j].hasAttribute(dataAttributes.item.expanded)) { 354 setItemExpanded(that._elements["item"][j], false, true); 355 } 356 } 357 } 358 // expand the accordion item containing the deep link 359 setItemExpanded(deepLinkItem, true, true); 360 } 361 var hashId = window.location.hash.substring(1); 362 if (hashId) { 363 var hashItem = document.querySelector("[id='" + hashId + "']"); 364 if (hashItem) { 365 hashItem.scrollIntoView(); 366 } 367 } 368 } 369 } 370 } 371 372 /** 373 * Caches the Accordion elements as defined via the {@code data-accordion-hook="ELEMENT_NAME"} markup API. 374 * 375 * @private 376 * @param {HTMLElement} wrapper The Accordion wrapper element 377 */ 378 function cacheElements(wrapper) { 379 that._elements = {}; 380 that._elements.self = wrapper; 381 var hooks = that._elements.self.querySelectorAll("[data-" + NS + "-hook-" + IS + "]"); 382 383 for (var i = 0; i < hooks.length; i++) { 384 var hook = hooks[i]; 385 if (hook.closest("." + NS + "-" + IS) === that._elements.self) { // only process own accordion elements 386 var capitalized = IS; 387 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 388 var key = hook.dataset[NS + "Hook" + capitalized]; 389 if (that._elements[key]) { 390 if (!Array.isArray(that._elements[key])) { 391 var tmp = that._elements[key]; 392 that._elements[key] = [tmp]; 393 } 394 that._elements[key].push(hook); 395 } else { 396 that._elements[key] = hook; 397 } 398 } 399 } 400 } 401 402 /** 403 * Sets up properties for the Accordion based on the passed options. 404 * 405 * @private 406 * @param {Object} options The Accordion options 407 */ 408 function setupProperties(options) { 409 that._properties = {}; 410 411 for (var key in properties) { 412 if (Object.prototype.hasOwnProperty.call(properties, key)) { 413 var property = properties[key]; 414 var value = null; 415 416 if (options && options[key] != null) { 417 value = options[key]; 418 419 // transform the provided option 420 if (property && typeof property.transform === "function") { 421 value = property.transform(value); 422 } 423 } 424 425 if (value === null) { 426 // value still null, take the property default 427 value = properties[key]["default"]; 428 } 429 430 that._properties[key] = value; 431 } 432 } 433 } 434 435 /** 436 * Binds Accordion event handling. 437 * 438 * @private 439 */ 440 function bindEvents() { 441 window.addEventListener("hashchange", scrollToDeepLinkIdInAccordion, false); 442 var buttons = that._elements["button"]; 443 if (buttons) { 444 for (var i = 0; i < buttons.length; i++) { 445 (function(index) { 446 buttons[i].addEventListener("click", function(event) { 447 toggle(index); 448 focusButton(index); 449 }); 450 buttons[i].addEventListener("keydown", function(event) { 451 onButtonKeyDown(event, index); 452 }); 453 })(i); 454 } 455 } 456 } 457 458 /** 459 * Handles button keydown events. 460 * 461 * @private 462 * @param {Object} event The keydown event 463 * @param {Number} index The index of the button triggering the event 464 */ 465 function onButtonKeyDown(event, index) { 466 var lastIndex = that._elements["button"].length - 1; 467 468 switch (event.keyCode) { 469 case keyCodes.ARROW_LEFT: 470 case keyCodes.ARROW_UP: 471 event.preventDefault(); 472 if (index > 0) { 473 focusButton(index - 1); 474 } 475 break; 476 case keyCodes.ARROW_RIGHT: 477 case keyCodes.ARROW_DOWN: 478 event.preventDefault(); 479 if (index < lastIndex) { 480 focusButton(index + 1); 481 } 482 break; 483 case keyCodes.HOME: 484 event.preventDefault(); 485 focusButton(0); 486 break; 487 case keyCodes.END: 488 event.preventDefault(); 489 focusButton(lastIndex); 490 break;
491 case keyCodes.ENTER: 492 case keyCodes.SPACE: 493 event.preventDefault(); 494 toggle(index); 495 focusButton(index); 496 break; 497 default: 498 return; 499 } 500 } 501 502 /** 503 * General handler for toggle of an item. 504 * 505 * @private 506 * @param {Number} index The index of the item to toggle 507 */ 508 function toggle(index) { 509 var item = that._elements["item"][index]; 510 if (item) { 511 if (that._properties.singleExpansion) { 512 // ensure only a single item is expanded if single expansion is enabled. 513 for (var i = 0; i < that._elements["item"].length; i++) { 514 if (that._elements["item"][i] !== item) { 515 var expanded = getItemExpanded(that._elements["item"][i]); 516 if (expanded) { 517 setItemExpanded(that._elements["item"][i], false); 518 } 519 } 520 } 521 } 522 setItemExpanded(item, !getItemExpanded(item)); 523 524 if (dataLayerEnabled) { 525 var accordionId = that._elements.self.id; 526 var expandedItems = getExpandedItems() 527 .map(function(item) { 528 return getDataLayerId(item); 529 }); 530 531 var uploadPayload = { component: {} }; 532 uploadPayload.component[accordionId] = { shownItems: expandedItems }; 533 534 var removePayload = { component: {} }; 535 removePayload.component[accordionId] = { shownItems: undefined }; 536 537 dataLayer.push(removePayload); 538 dataLayer.push(uploadPayload); 539 } 540 } 541 } 542 543 /** 544 * Sets an item's expanded state based on the provided flag and refreshes its internals. 545 * 546 * @private 547 * @param {HTMLElement} item The item to mark as expanded, or not expanded 548 * @param {Boolean} expanded true to mark the item expanded, false otherwise 549 * @param {Boolean} keepHash true to keep the hash in the URL, false to update it 550 */ 551 function setItemExpanded(item, expanded, keepHash) { 552 if (expanded) { 553 item.setAttribute(dataAttributes.item.expanded, ""); 554 var index = that._elements["item"].indexOf(item); 555 if (!keepHash && containerUtils) { 556 containerUtils.updateUrlHash(that, "item", index); 557 } 558 if (dataLayerEnabled) { 559 dataLayer.push({ 560 event: "cmp:show", 561 eventInfo: { 562 path: "component." + getDataLayerId(item) 563 } 564 }); 565 } 566 567 } else { 568 item.removeAttribute(dataAttributes.item.expanded); 569 if (!keepHash && containerUtils) { 570 containerUtils.removeUrlHash(); 571 } 572 if (dataLayerEnabled) { 573 dataLayer.push({ 574 event: "cmp:hide", 575 eventInfo: { 576 path: "component." + getDataLayerId(item) 577 } 578 }); 579 } 580 } 581 refreshItem(item); 582 } 583 584 /** 585 * Gets an item's expanded state. 586 * 587 * @private 588 * @param {HTMLElement} item The item for checking its expanded state 589 * @returns {Boolean} true if the item is expanded, false otherwise 590 */ 591 function getItemExpanded(item) { 592 return item && item.dataset && item.dataset["cmpExpanded"] !== undefined; 593 } 594 595 /** 596 * Refreshes an item based on its expanded state. 597 * 598 * @private 599 * @param {HTMLElement} item The item to refresh 600 */ 601 function refreshItem(item) { 602 var expanded = getItemExpanded(item); 603 if (expanded) { 604 expandItem(item); 605 } else { 606 collapseItem(item); 607 } 608 } 609 610 /** 611 * Refreshes all items based on their expanded state. 612 * 613 * @private 614 */ 615 function refreshItems() { 616 for (var i = 0; i < that._elements["item"].length; i++) { 617 refreshItem(that._elements["item"][i]); 618 } 619 } 620 621 /** 622 * Returns all expanded items. 623 * 624 * @private 625 * @returns {HTMLElement[]} The expanded items 626 */ 627 function getExpandedItems() { 628 var expandedItems = []; 629 630 for (var i = 0; i < that._elements["item"].length; i++) { 631 var item = that._elements["item"][i]; 632 var expanded = getItemExpanded(item); 633 if (expanded) { 634 expandedItems.push(item); 635 } 636 } 637 638 return expandedItems; 639 } 640 641 /** 642 * Annotates the item and its internals with 643 * the necessary style and accessibility attributes to indicate it is expanded. 644 * 645 * @private 646 * @param {HTMLElement} item The item to annotate as expanded 647 */ 648 function expandItem(item) { 649 var index = that._elements["item"].indexOf(item); 650 if (index > -1) { 651 var button = that._elements["button"][index]; 652 var panel = that._elements["panel"][index];
653 button.classList.add(cssClasses.button.expanded); 654 // used to fix some known screen readers issues in reading the correct state of the 'aria-expanded' attribute 655 // e.g. https://bugs.webkit.org/show_bug.cgi?id=210934 656 setTimeout(function() { 657 button.setAttribute("aria-expanded", true); 658 }, delay); 659 panel.classList.add(cssClasses.panel.expanded); 660 panel.classList.remove(cssClasses.panel.hidden); 661 panel.setAttribute("aria-hidden", false); 662 } 663 } 664 665 /** 666 * Annotates the item and its internals with 667 * the necessary style and accessibility attributes to indicate it is not expanded. 668 * 669 * @private 670 * @param {HTMLElement} item The item to annotate as not expanded 671 */ 672 function collapseItem(item) { 673 var index = that._elements["item"].indexOf(item); 674 if (index > -1) { 675 var button = that._elements["button"][index]; 676 var panel = that._elements["panel"][index]; 677 button.classList.remove(cssClasses.button.expanded); 678 // used to fix some known screen readers issues in reading the correct state of the 'aria-expanded' attribute 679 // e.g. https://bugs.webkit.org/show_bug.cgi?id=210934 680 setTimeout(function() { 681 button.setAttribute("aria-expanded", false); 682 }, delay); 683 panel.classList.add(cssClasses.panel.hidden); 684 panel.classList.remove(cssClasses.panel.expanded); 685 panel.setAttribute("aria-hidden", true); 686 } 687 } 688 689 /** 690 * Focuses the button at the provided index. 691 * 692 * @private 693 * @param {Number} index The index of the button to focus 694 */ 695 function focusButton(index) { 696 var button = that._elements["button"][index]; 697 button.focus(); 698 } 699 } 700 701 /** 702 * Reads options data from the Accordion wrapper element, defined via {@code data-cmp-*} data attributes. 703 * 704 * @private 705 * @param {HTMLElement} element The Accordion element to read options data from 706 * @returns {Object} The options read from the component data attributes 707 */ 708 function readData(element) { 709 var data = element.dataset; 710 var options = []; 711 var capitalized = IS; 712 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 713 var reserved = ["is", "hook" + capitalized]; 714 715 for (var key in data) { 716 if (Object.prototype.hasOwnProperty.call(data, key)) { 717 var value = data[key]; 718 719 if (key.indexOf(NS) === 0) { 720 key = key.slice(NS.length); 721 key = key.charAt(0).toLowerCase() + key.substring(1); 722 723 if (reserved.indexOf(key) === -1) { 724 options[key] = value; 725 } 726 } 727 } 728 } 729 730 return options; 731 } 732 733 /** 734 * Parses the dataLayer string and returns the ID 735 * 736 * @private 737 * @param {HTMLElement} item the accordion item 738 * @returns {String} dataLayerId or undefined 739 */ 740 function getDataLayerId(item) { 741 if (item) { 742 if (item.dataset.cmpDataLayer) { 743 return Object.keys(JSON.parse(item.dataset.cmpDataLayer))[0]; 744 } else { 745 return item.id; 746 } 747 } 748 return null; 749 } 750 751 /** 752 * Document ready handler and DOM mutation observers. Initializes Accordion components as necessary. 753 * 754 * @private 755 */ 756 function onDocumentReady() { 757 dataLayerEnabled = document.body.hasAttribute("data-cmp-data-layer-enabled"); 758 if (dataLayerEnabled) { 759 dataLayerName = document.body.getAttribute("data-cmp-data-layer-name") || "adobeDataLayer"; 760 dataLayer = window[dataLayerName] = window[dataLayerName] || []; 761 } 762 763 var elements = document.querySelectorAll(selectors.self); 764 for (var i = 0; i < elements.length; i++) { 765 new Accordion({ element: elements[i], options: readData(elements[i]) }); 766 } 767 768 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; 769 var body = document.querySelector("body"); 770 var observer = new MutationObserver(function(mutations) { 771 mutations.forEach(function(mutation) { 772 // needed for IE 773 var nodesArray = [].slice.call(mutation.addedNodes); 774 if (nodesArray.length > 0) { 775 nodesArray.forEach(function(addedNode) { 776 if (addedNode.querySelectorAll) { 777 var elementsArray = [].slice.call(addedNode.querySelectorAll(selectors.self)); 778 elementsArray.forEach(function(element) { 779 new Accordion({ element: element, options: readData(element) }); 780 }); 781 } 782 }); 783 } 784 }); 785 }); 786 787 observer.observe(body, { 788 subtree: true, 789 childList: true, 790 characterData: true 791 }); 792 } 793 794 if (document.readyState !== "loading") { 795 onDocumentReady(); 796 } else { 797 document.addEventListener("DOMContentLoaded", onDocumentReady); 798 } 799 800 if (containerUtils) { 801 window.addEventListener("load", containerUtils.scrollToAnchor, false); 802 } 803 804}()); 805 806/******************************************************************************* 807 * Copyright 2018 Adobe 808 * 809 * Licensed under the Apache License, Version 2.0 (the "License");
810 * you may not use this file except in compliance with the License. 811 * You may obtain a copy of the License at 812 * 813 * http://www.apache.org/licenses/LICENSE2.0 814 * 815 * Unless required by applicable law or agreed to in writing, software 816 * distributed under the License is distributed on an "AS IS" BASIS, 817 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 818 * See the License for the specific language governing permissions and 819 * limitations under the License. 820 ******************************************************************************/ 821 822/** 823 * Element.matches() 824 * https://developer.mozilla.org/enUS/docs/Web/API/Element/matches#Polyfill 825 */ 826if (!Element.prototype.matches) { 827 Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; 828} 829 830// eslint-disable-next-line valid-jsdoc 831/** 832 * Element.closest() 833 * https://developer.mozilla.org/enUS/docs/Web/API/Element/closest#Polyfill 834 */ 835if (!Element.prototype.closest) { 836 Element.prototype.closest = function(s) { 837 "use strict"; 838 var el = this; 839 if (!document.documentElement.contains(el)) { 840 return null; 841 } 842 do { 843 if (el.matches(s)) { 844 return el; 845 } 846 el = el.parentElement || el.parentNode; 847 } while (el !== null && el.nodeType === 1); 848 return null; 849 }; 850} 851 852/******************************************************************************* 853 * Copyright 2018 Adobe 854 * 855 * Licensed under the Apache License, Version 2.0 (the "License"); 856 * you may not use this file except in compliance with the License. 857 * You may obtain a copy of the License at 858 * 859 * http://www.apache.org/licenses/LICENSE-2.0 860 * 861 * Unless required by applicable law or agreed to in writing, software 862 * distributed under the License is distributed on an "AS IS" BASIS, 863 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 864 * See the License for the specific language governing permissions and 865 * limitations under the License. 866 ******************************************************************************/ 867/* global 868 CQ 869 */ 870(function() { 871 "use strict"; 872 873 var containerUtils = window.CQ && window.CQ.CoreComponents && window.CQ.CoreComponents.container && window.CQ.CoreComponents.container.utils ? window.CQ.CoreComponents.container.utils : undefined; 874 if (!containerUtils) { 875 // eslint-disable-next-line no-console 876 console.warn("Tabs: container utilities at window.CQ.CoreComponents.container.utils are not available. This can lead to missing features. Ensure the core.wcm.components.commons.site.container client library is included on the page."); 877 } 878 var dataLayerEnabled; 879 var dataLayer; 880 var dataLayerName; 881 882 var NS = "cmp"; 883 var IS = "tabs"; 884 885 var keyCodes = { 886 END: 35, 887 HOME: 36, 888 ARROW_LEFT: 37, 889 ARROW_UP: 38, 890 ARROW_RIGHT: 39, 891 ARROW_DOWN: 40 892 }; 893 894 var selectors = { 895 self: "[data-" + NS + '-is="' + IS + '"]', 896 active: { 897 tab: "cmp-tabs__tab--active", 898 tabpanel: "cmp-tabs__tabpanel--active" 899 } 900 }; 901 902 /** 903 * Tabs Configuration 904 * 905 * @typedef {Object} TabsConfig Represents a Tabs configuration 906 * @property {HTMLElement} element The HTMLElement representing the Tabs 907 * @property {Object} options The Tabs options 908 */ 909 910 /** 911 * Tabs 912 * 913 * @class Tabs 914 * @classdesc An interactive Tabs component for navigating a list of tabs 915 * @param {TabsConfig} config The Tabs configuration 916 */ 917 function Tabs(config) { 918 var that = this; 919 920 if (config && config.element) { 921 init(config); 922 } 923 924 /** 925 * Initializes the Tabs 926 * 927 * @private 928 * @param {TabsConfig} config The Tabs configuration 929 */ 930 function init(config) { 931 that._config = config; 932 933 // prevents multiple initialization 934 config.element.removeAttribute("data-" + NS + "-is"); 935 936 cacheElements(config.element); 937 that._active = getActiveIndex(that._elements["tab"]); 938 939 if (that._elements.tabpanel) { 940 refreshActive(); 941 bindEvents(); 942 scrollToDeepLinkIdInTabs(); 943 } 944 945 if (window.Granite && window.Granite.author && window.Granite.author.MessageChannel) { 946 /* 947 * Editor message handling: 948 * - subscribe to "cmp.panelcontainer" message requests sent by the editor frame
949 * - check that the message data panel container type is correct and that the id (path) matches this specific Tabs component 950 * - if so, route the "navigate" operation to enact a navigation of the Tabs based on index data 951 */ 952 CQ.CoreComponents.MESSAGE_CHANNEL = CQ.CoreComponents.MESSAGE_CHANNEL || new window.Granite.author.MessageChannel("cqauthor", window); 953 CQ.CoreComponents.MESSAGE_CHANNEL.subscribeRequestMessage("cmp.panelcontainer", function(message) { 954 if (message.data && message.data.type === "cmp-tabs" && message.data.id === that._elements.self.dataset["cmpPanelcontainerId"]) { 955 if (message.data.operation === "navigate") { 956 navigate(message.data.index); 957 } 958 } 959 }); 960 } 961 } 962 963 /** 964 * Displays the panel containing the element that corresponds to the deep link in the URI fragment 965 * and scrolls the browser to this element. 966 */ 967 function scrollToDeepLinkIdInTabs() { 968 if (containerUtils) { 969 var deepLinkItemIdx = containerUtils.getDeepLinkItemIdx(that, "tab", "tabpanel"); 970 if (deepLinkItemIdx > -1) { 971 var deepLinkItem = that._elements["tab"][deepLinkItemIdx]; 972 if (deepLinkItem && that._elements["tab"][that._active].id !== deepLinkItem.id) { 973 navigateAndFocusTab(deepLinkItemIdx, true); 974 } 975 var hashId = window.location.hash.substring(1); 976 if (hashId) { 977 var hashItem = document.querySelector("[id='" + hashId + "']"); 978 if (hashItem) { 979 hashItem.scrollIntoView(); 980 } 981 } 982 } 983 } 984 } 985 986 /** 987 * Returns the index of the active tab, if no tab is active returns 0 988 * 989 * @param {Array} tabs Tab elements 990 * @returns {Number} Index of the active tab, 0 if none is active 991 */ 992 function getActiveIndex(tabs) { 993 if (tabs) { 994 for (var i = 0; i < tabs.length; i++) { 995 if (tabs[i].classList.contains(selectors.active.tab)) { 996 return i; 997 } 998 } 999 } 1000 return 0; 1001 } 1002 1003 /** 1004 * Caches the Tabs elements as defined via the {@code data-tabs-hook="ELEMENT_NAME"} markup API 1005 * 1006 * @private 1007 * @param {HTMLElement} wrapper The Tabs wrapper element 1008 */ 1009 function cacheElements(wrapper) { 1010 that._elements = {}; 1011 that._elements.self = wrapper; 1012 var hooks = that._elements.self.querySelectorAll("[data-" + NS + "-hook-" + IS + "]"); 1013 1014 for (var i = 0; i < hooks.length; i++) { 1015 var hook = hooks[i]; 1016 if (hook.closest("." + NS + "-" + IS) === that._elements.self) { // only process own tab elements 1017 var capitalized = IS; 1018 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 1019 var key = hook.dataset[NS + "Hook" + capitalized]; 1020 if (that._elements[key]) { 1021 if (!Array.isArray(that._elements[key])) { 1022 var tmp = that._elements[key]; 1023 that._elements[key] = [tmp]; 1024 } 1025 that._elements[key].push(hook); 1026 } else { 1027 that._elements[key] = hook; 1028 } 1029 } 1030 } 1031 } 1032 1033 /** 1034 * Binds Tabs event handling 1035 * 1036 * @private 1037 */ 1038 function bindEvents() { 1039 window.addEventListener("hashchange", scrollToDeepLinkIdInTabs, false); 1040 var tabs = that._elements["tab"]; 1041 if (tabs) { 1042 for (var i = 0; i < tabs.length; i++) { 1043 (function(index) { 1044 tabs[i].addEventListener("click", function(event) { 1045 navigateAndFocusTab(index); 1046 }); 1047 tabs[i].addEventListener("keydown", function(event) { 1048 onKeyDown(event); 1049 }); 1050 })(i); 1051 } 1052 } 1053 } 1054 1055 /** 1056 * Handles tab keydown events 1057 * 1058 * @private 1059 * @param {Object} event The keydown event 1060 */ 1061 function onKeyDown(event) { 1062 var index = that._active; 1063 var lastIndex = that._elements["tab"].length - 1; 1064 1065 switch (event.keyCode) { 1066 case keyCodes.ARROW_LEFT: 1067 case keyCodes.ARROW_UP: 1068 event.preventDefault(); 1069 if (index > 0) { 1070 navigateAndFocusTab(index - 1); 1071 } 1072 break; 1073 case keyCodes.ARROW_RIGHT: 1074 case keyCodes.ARROW_DOWN: 1075 event.preventDefault(); 1076 if (index < lastIndex) { 1077 navigateAndFocusTab(index + 1); 1078 } 1079 break; 1080 case keyCodes.HOME: 1081 event.preventDefault(); 1082 navigateAndFocusTab(0); 1083 break; 1084 case keyCodes.END: 1085 event.preventDefault(); 1086 navigateAndFocusTab(lastIndex); 1087 break; 1088 default: 1089 return; 1090 } 1091 } 1092 1093 /** 1094 * Refreshes the tab markup based on the current {@code Tabs#_active} index 1095 * 1096 * @private 1097 */ 1098 function refreshActive() { 1099 var tabpanels = that._elements["tabpanel"]; 1100 var tabs = that._elements["tab"]; 1101 1102 if (tabpanels) { 1103 if (Array.isArray(tabpanels)) { 1104 for (var i = 0; i < tabpanels.length; i++) { 1105 if (i === parseInt(that._active)) { 1106 tabpanels[i].classList.add(selectors.active.tabpanel); 1107 tabpanels[i].removeAttribute("aria-hidden"); 1108 tabs[i].classList.add(selectors.active.tab); 1109 tabs[i].setAttribute("aria-selected", true); 1110 tabs[i].setAttribute("tabindex", "0"); 1111 } else { 1112 tabpanels[i].classList.remove(selectors.active.tabpanel); 1113 tabpanels[i].setAttribute("aria-hidden", true); 1114 tabs[i].classList.remove(selectors.active.tab); 1115 tabs[i].setAttribute("aria-selected", false); 1116 tabs[i].setAttribute("tabindex", "-1"); 1117 } 1118 } 1119 } else { 1120 // only one tab 1121 tabpanels.classList.add(selectors.active.tabpanel); 1122 tabs.classList.add(selectors.active.tab); 1123 } 1124 } 1125 } 1126 1127 /** 1128 * Focuses the element and prevents scrolling the element into view 1129 * 1130 * @param {HTMLElement} element Element to focus 1131 */ 1132 function focusWithoutScroll(element) { 1133 var x = window.scrollX || window.pageXOffset; 1134 var y = window.scrollY || window.pageYOffset; 1135 element.focus(); 1136 window.scrollTo(x, y); 1137 } 1138 1139 /** 1140 * Navigates to the tab at the provided index 1141 * 1142 * @private 1143 * @param {Number} index The index of the tab to navigate to 1144 */ 1145 function navigate(index) { 1146 that._active = index; 1147 refreshActive(); 1148 } 1149 1150 /** 1151 * Navigates to the item at the provided index and ensures the active tab gains focus 1152 * 1153 * @private 1154 * @param {Number} index The index of the item to navigate to 1155 * @param {Boolean} keepHash true to keep the hash in the URL, false to update it 1156 */ 1157 function navigateAndFocusTab(index, keepHash) { 1158 var exActive = that._active; 1159 if (!keepHash && containerUtils) { 1160 containerUtils.updateUrlHash(that, "tab", index); 1161 } 1162 navigate(index); 1163 focusWithoutScroll(that._elements["tab"][index]); 1164 1165 if (dataLayerEnabled) { 1166 1167 var activeItem = getDataLayerId(that._elements.tabpanel[index]); 1168 var exActiveItem = getDataLayerId(that._elements.tabpanel[exActive]); 1169 1170 dataLayer.push({ 1171 event: "cmp:show", 1172 eventInfo: { 1173 path: "component." + activeItem 1174 } 1175 }); 1176 1177 dataLayer.push({ 1178 event: "cmp:hide", 1179 eventInfo: { 1180 path: "component." + exActiveItem 1181 } 1182 }); 1183 1184 var tabsId = that._elements.self.id; 1185 var uploadPayload = { component: {} }; 1186 uploadPayload.component[tabsId] = { shownItems: [activeItem] }; 1187 1188 var removePayload = { component: {} }; 1189 removePayload.component[tabsId] = { shownItems: undefined }; 1190 1191 dataLayer.push(removePayload); 1192 dataLayer.push(uploadPayload); 1193 } 1194 } 1195 } 1196 1197 /**
1198 * Reads options data from the Tabs wrapper element, defined via {@code data-cmp-*} data attributes 1199 * 1200 * @private 1201 * @param {HTMLElement} element The Tabs element to read options data from 1202 * @returns {Object} The options read from the component data attributes 1203 */ 1204 function readData(element) { 1205 var data = element.dataset; 1206 var options = []; 1207 var capitalized = IS; 1208 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 1209 var reserved = ["is", "hook" + capitalized]; 1210 1211 for (var key in data) { 1212 if (Object.prototype.hasOwnProperty.call(data, key)) { 1213 var value = data[key]; 1214 1215 if (key.indexOf(NS) === 0) { 1216 key = key.slice(NS.length); 1217 key = key.charAt(0).toLowerCase() + key.substring(1); 1218 1219 if (reserved.indexOf(key) === -1) { 1220 options[key] = value; 1221 } 1222 } 1223 } 1224 } 1225 1226 return options; 1227 } 1228 1229 /** 1230 * Parses the dataLayer string and returns the ID 1231 * 1232 * @private 1233 * @param {HTMLElement} item the accordion item 1234 * @returns {String} dataLayerId or undefined 1235 */ 1236 function getDataLayerId(item) { 1237 if (item) { 1238 if (item.dataset.cmpDataLayer) { 1239 return Object.keys(JSON.parse(item.dataset.cmpDataLayer))[0]; 1240 } else { 1241 return item.id; 1242 } 1243 } 1244 return null; 1245 } 1246 1247 /** 1248 * Document ready handler and DOM mutation observers. Initializes Tabs components as necessary. 1249 * 1250 * @private 1251 */ 1252 function onDocumentReady() { 1253 dataLayerEnabled = document.body.hasAttribute("data-cmp-data-layer-enabled"); 1254 if (dataLayerEnabled) { 1255 dataLayerName = document.body.getAttribute("data-cmp-data-layer-name") || "adobeDataLayer"; 1256 dataLayer = window[dataLayerName] = window[dataLayerName] || []; 1257 } 1258 1259 var elements = document.querySelectorAll(selectors.self); 1260 for (var i = 0; i < elements.length; i++) { 1261 new Tabs({ element: elements[i], options: readData(elements[i]) }); 1262 } 1263 1264 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; 1265 var body = document.querySelector("body"); 1266 var observer = new MutationObserver(function(mutations) { 1267 mutations.forEach(function(mutation) { 1268 // needed for IE 1269 var nodesArray = [].slice.call(mutation.addedNodes); 1270 if (nodesArray.length > 0) { 1271 nodesArray.forEach(function(addedNode) { 1272 if (addedNode.querySelectorAll) { 1273 var elementsArray = [].slice.call(addedNode.querySelectorAll(selectors.self)); 1274 elementsArray.forEach(function(element) { 1275 new Tabs({ element: element, options: readData(element) }); 1276 }); 1277 } 1278 }); 1279 } 1280 }); 1281 }); 1282 1283 observer.observe(body, { 1284 subtree: true, 1285 childList: true, 1286 characterData: true 1287 }); 1288 } 1289 1290 if (document.readyState !== "loading") { 1291 onDocumentReady(); 1292 } else { 1293 document.addEventListener("DOMContentLoaded", onDocumentReady); 1294 } 1295 1296 if (containerUtils) { 1297 window.addEventListener("load", containerUtils.scrollToAnchor, false); 1298 } 1299 1300}()); 1301 1302/******************************************************************************* 1303 * Copyright 2022 Adobe 1304 * 1305 * Licensed under the Apache License, Version 2.0 (the "License"); 1306 * you may not use this file except in compliance with the License. 1307 * You may obtain a copy of the License at 1308 * 1309 * http://www.apache.org/licenses/LICENSE-2.0 1310 * 1311 * Unless required by applicable law or agreed to in writing, software 1312 * distributed under the License is distributed on an "AS IS" BASIS, 1313 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1314 * See the License for the specific language governing permissions and 1315 * limitations under the License. 1316 ******************************************************************************/ 1317(function(document) { 1318 "use strict"; 1319 1320 window.CMP = window.CMP || {}; 1321 window.CMP.utils = (function() { 1322 var NS = "cmp"; 1323 1324 /**
1325 * Reads options data from the Component wrapper element, defined via {@code data-cmp-*} data attributes 1326 * 1327 * @param {HTMLElement} element The component element to read options data from 1328 * @param {String} is The component identifier 1329 * @returns {String[]} The options read from the component data attributes 1330 */ 1331 var readData = function(element, is) { 1332 var data = element.dataset; 1333 var options = []; 1334 var capitalized = is; 1335 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 1336 var reserved = ["is", "hook" + capitalized]; 1337 1338 for (var key in data) { 1339 if (Object.prototype.hasOwnProperty.call(data, key)) { 1340 var value = data[key]; 1341 1342 if (key.indexOf(NS) === 0) { 1343 key = key.slice(NS.length); 1344 key = key.charAt(0).toLowerCase() + key.substring(1); 1345 1346 if (reserved.indexOf(key) === -1) { 1347 options[key] = value; 1348 } 1349 } 1350 } 1351 } 1352 return options; 1353 }; 1354 1355 /** 1356 * Set up the final properties of a component by evaluating the transform function or fall back to the default value on demand 1357 * @param {String[]} options the options to transform 1358 * @param {Object} properties object of properties of property functions 1359 * @returns {Object} transformed properties 1360 */ 1361 var setupProperties = function(options, properties) { 1362 var transformedProperties = {}; 1363 1364 for (var key in properties) { 1365 if (Object.prototype.hasOwnProperty.call(properties, key)) { 1366 var property = properties[key]; 1367 if (options && options[key] != null) { 1368 if (property && typeof property.transform === "function") { 1369 transformedProperties[key] = property.transform(options[key]); 1370 } else { 1371 transformedProperties[key] = options[key]; 1372 } 1373 } else { 1374 transformedProperties[key] = properties[key]["default"]; 1375 } 1376 } 1377 } 1378 return transformedProperties; 1379 }; 1380 1381 1382 return { 1383 readData: readData, 1384 setupProperties: setupProperties 1385 }; 1386 }()); 1387}(window.document)); 1388 1389/******************************************************************************* 1390 * Copyright 2018 Adobe 1391 * 1392 * Licensed under the Apache License, Version 2.0 (the "License"); 1393 * you may not use this file except in compliance with the License. 1394 * You may obtain a copy of the License at 1395 * 1396 * http://www.apache.org/licenses/LICENSE-2.0 1397 * 1398 * Unless required by applicable law or agreed to in writing, software 1399 * distributed under the License is distributed on an "AS IS" BASIS, 1400 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1401 * See the License for the specific language governing permissions and 1402 * limitations under the License. 1403 ******************************************************************************/ 1404(function() { 1405 "use strict"; 1406 1407 var containerUtils = window.CQ && window.CQ.CoreComponents && window.CQ.CoreComponents.container && window.CQ.CoreComponents.container.utils ? window.CQ.CoreComponents.container.utils : undefined; 1408 if (!containerUtils) { 1409 // eslint-disable-next-line no-console 1410 console.warn("Tabs: container utilities at window.CQ.CoreComponents.container.utils are not available. This can lead to missing features. Ensure the core.wcm.components.commons.site.container client library is included on the page."); 1411 } 1412 var dataLayerEnabled; 1413 var dataLayer; 1414 var dataLayerName; 1415 1416 var NS = "cmp"; 1417 var IS = "carousel"; 1418 1419 var keyCodes = { 1420 SPACE: 32, 1421 END: 35, 1422 HOME: 36, 1423 ARROW_LEFT: 37, 1424 ARROW_UP: 38, 1425 ARROW_RIGHT: 39, 1426 ARROW_DOWN: 40 1427 }; 1428 1429 var selectors = { 1430 self: "[data-" + NS + '-is="' + IS + '"]' 1431 }; 1432 1433 var properties = { 1434 /** 1435 * Determines whether the Carousel will automatically transition between slides 1436 * 1437 * @memberof Carousel
1438 * @type {Boolean} 1439 * @default false 1440 */ 1441 "autoplay": { 1442 "default": false, 1443 "transform": function(value) { 1444 return !(value === null || typeof value === "undefined"); 1445 } 1446 }, 1447 /** 1448 * Duration (in milliseconds) before automatically transitioning to the next slide 1449 * 1450 * @memberof Carousel 1451 * @type {Number} 1452 * @default 5000 1453 */ 1454 "delay": { 1455 "default": 5000, 1456 "transform": function(value) { 1457 value = parseFloat(value); 1458 return !isNaN(value) ? value : null; 1459 } 1460 }, 1461 /** 1462 * Determines whether automatic pause on hovering the carousel is disabled 1463 * 1464 * @memberof Carousel 1465 * @type {Boolean} 1466 * @default false 1467 */ 1468 "autopauseDisabled": { 1469 "default": false, 1470 "transform": function(value) { 1471 return !(value === null || typeof value === "undefined"); 1472 } 1473 } 1474 }; 1475 1476 /** 1477 * Carousel Configuration 1478 * 1479 * @typedef {Object} CarouselConfig Represents a Carousel configuration 1480 * @property {HTMLElement} element The HTMLElement representing the Carousel 1481 * @property {*[]} options The Carousel options 1482 */ 1483 1484 /** 1485 * Carousel 1486 * 1487 * @class Carousel 1488 * @classdesc An interactive Carousel component for navigating a list of generic items 1489 * @param {CarouselConfig} config The Carousel configuration 1490 */ 1491 function Carousel(config) { 1492 var that = this; 1493 1494 if (config && config.element) { 1495 init(config); 1496 } 1497 1498 /** 1499 * Initializes the Carousel 1500 * 1501 * @private 1502 * @param {CarouselConfig} config The Carousel configuration 1503 */ 1504 function init(config) { 1505 that._config = config; 1506 1507 // prevents multiple initialization 1508 config.element.removeAttribute("data-" + NS + "-is"); 1509 1510 setupProperties(config.options); 1511 cacheElements(config.element); 1512 1513 that._active = 0; 1514 that._paused = false; 1515 1516 if (that._elements.item) { 1517 initializeActive(); 1518 bindEvents(); 1519 resetAutoplayInterval(); 1520 refreshPlayPauseActions(); 1521 scrollToDeepLinkIdInCarousel(); 1522 } 1523 1524 // TODO: This section is only relevant in edit mode and should move to the editor clientLib 1525 if (window.Granite && window.Granite.author && window.Granite.author.MessageChannel) { 1526 /* 1527 * Editor message handling: 1528 * - subscribe to "cmp.panelcontainer" message requests sent by the editor frame 1529 * - check that the message data panel container type is correct and that the id (path) matches this specific Carousel component 1530 * - if so, route the "navigate" operation to enact a navigation of the Carousel based on index data 1531 */ 1532 window.CQ = window.CQ || {}; 1533 window.CQ.CoreComponents = window.CQ.CoreComponents || {}; 1534 window.CQ.CoreComponents.MESSAGE_CHANNEL = window.CQ.CoreComponents.MESSAGE_CHANNEL || new window.Granite.author.MessageChannel("cqauthor", window); 1535 window.CQ.CoreComponents.MESSAGE_CHANNEL.subscribeRequestMessage("cmp.panelcontainer", function(message) { 1536 if (message.data && message.data.type === "cmp-carousel" && message.data.id === that._elements.self.dataset["cmpPanelcontainerId"]) { 1537 if (message.data.operation === "navigate") { 1538 navigate(message.data.index); 1539 } 1540 } 1541 }); 1542 } 1543 } 1544 1545 /** 1546 * Displays the slide containing the element that corresponds to the deep link in the URI fragment 1547 * and scrolls the browser to this element. 1548 */ 1549 function scrollToDeepLinkIdInCarousel() { 1550 if (containerUtils) { 1551 var deepLinkItemIdx = containerUtils.getDeepLinkItemIdx(that, "item", "item"); 1552 if (deepLinkItemIdx > -1) { 1553 var deepLinkItem = that._elements["item"][deepLinkItemIdx]; 1554 if (deepLinkItem && that._elements["item"][that._active].id !== deepLinkItem.id) { 1555 navigateAndFocusIndicator(deepLinkItemIdx, true); 1556 // pause the carousel auto-rotation 1557 pause(); 1558 } 1559 var hashId = window.location.hash.substring(1); 1560 if (hashId) { 1561 var hashItem = document.querySelector("[id='" + hashId + "']"); 1562 if (hashItem) { 1563 hashItem.scrollIntoView(); 1564 } 1565 } 1566 } 1567 } 1568 } 1569 1570 /** 1571 * Caches the Carousel elements as defined via the {@code data-carousel-hook="ELEMENT_NAME"} markup API 1572 * 1573 * @private 1574 * @param {HTMLElement} wrapper The Carousel wrapper element 1575 */ 1576 function cacheElements(wrapper) { 1577 that._elements = {}; 1578 that._elements.self = wrapper; 1579 var hooks = that._elements.self.querySelectorAll("[data-" + NS + "-hook-" + IS + "]"); 1580 1581 for (var i = 0; i < hooks.length; i++) { 1582 var hook = hooks[i]; 1583 var capitalized = IS; 1584 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 1585 var key = hook.dataset[NS + "Hook" + capitalized]; 1586 if (that._elements[key]) { 1587 if (!Array.isArray(that._elements[key])) { 1588 var tmp = that._elements[key]; 1589 that._elements[key] = [tmp]; 1590 } 1591 that._elements[key].push(hook); 1592 } else { 1593 that._elements[key] = hook; 1594 } 1595 } 1596 } 1597 1598 /** 1599 * Sets up properties for the Carousel based on the passed options. 1600 * 1601 * @private 1602 * @param {Object} options The Carousel options 1603 */ 1604 function setupProperties(options) { 1605 that._properties = {}; 1606 1607 for (var key in properties) { 1608 if (Object.prototype.hasOwnProperty.call(properties, key)) { 1609 var property = properties[key]; 1610 var value = null; 1611 1612 if (options && options[key] != null) { 1613 value = options[key]; 1614 1615 // transform the provided option 1616 if (property && typeof property.transform === "function") { 1617 value = property.transform(value); 1618 } 1619 } 1620 1621 if (value === null) { 1622 // value still null, take the property default 1623 value = properties[key]["default"]; 1624 } 1625 1626 that._properties[key] = value; 1627 } 1628 } 1629 } 1630 1631 /** 1632 * Binds Carousel event handling 1633 * 1634 * @private 1635 */ 1636 function bindEvents() { 1637 window.addEventListener("hashchange", scrollToDeepLinkIdInCarousel, false); 1638 if (that._elements["previous"]) { 1639 that._elements["previous"].addEventListener("click", function() { 1640 var index = getPreviousIndex(); 1641 navigate(index); 1642 if (dataLayerEnabled) { 1643 dataLayer.push({ 1644 event: "cmp:show", 1645 eventInfo: { 1646 path: "component." + getDataLayerId(that._elements.item[index]) 1647 } 1648 }); 1649 } 1650 }); 1651 } 1652 1653 if (that._elements["next"]) { 1654 that._elements["next"].addEventListener("click", function() { 1655 var index = getNextIndex(); 1656 navigate(index); 1657 if (dataLayerEnabled) { 1658 dataLayer.push({ 1659 event: "cmp:show", 1660 eventInfo: { 1661 path: "component." + getDataLayerId(that._elements.item[index]) 1662 } 1663 }); 1664 } 1665 }); 1666 } 1667 1668 var indicators = that._elements["indicator"]; 1669 if (indicators) { 1670 for (var i = 0; i < indicators.length; i++) { 1671 (function(index) { 1672 indicators[i].addEventListener("click", function(event) { 1673 navigateAndFocusIndicator(index); 1674 // pause the carousel auto-rotation 1675 pause(); 1676 }); 1677 })(i); 1678 } 1679 } 1680 1681 if (that._elements["pause"]) { 1682 if (that._properties.autoplay) { 1683 that._elements["pause"].addEventListener("click", onPauseClick); 1684 } 1685 } 1686 1687 if (that._elements["play"]) { 1688 if (that._properties.autoplay) { 1689 that._elements["play"].addEventListener("click", onPlayClick); 1690 } 1691 } 1692 1693 that._elements.self.addEventListener("keydown", onKeyDown); 1694 1695 if (!that._properties.autopauseDisabled) { 1696 that._elements.self.addEventListener("mouseenter", onMouseEnter); 1697 that._elements.self.addEventListener("mouseleave", onMouseLeave); 1698 } 1699 1700 // for accessibility we pause animation when a element get focused 1701 var items = that._elements["item"]; 1702 if (items) { 1703 for (var j = 0; j < items.length; j++) { 1704 items[j].addEventListener("focusin", onMouseEnter); 1705 items[j].addEventListener("focusout", onMouseLeave); 1706 } 1707 } 1708 } 1709 1710 /**
1711 * Handles carousel keydown events 1712 * 1713 * @private 1714 * @param {Object} event The keydown event 1715 */ 1716 function onKeyDown(event) { 1717 var index = that._active; 1718 var lastIndex = that._elements["indicator"].length - 1; 1719 1720 switch (event.keyCode) { 1721 case keyCodes.ARROW_LEFT: 1722 case keyCodes.ARROW_UP: 1723 event.preventDefault(); 1724 if (index > 0) { 1725 navigateAndFocusIndicator(index - 1); 1726 } 1727 break; 1728 case keyCodes.ARROW_RIGHT: 1729 case keyCodes.ARROW_DOWN: 1730 event.preventDefault(); 1731 if (index < lastIndex) { 1732 navigateAndFocusIndicator(index + 1); 1733 } 1734 break; 1735 case keyCodes.HOME: 1736 event.preventDefault(); 1737 navigateAndFocusIndicator(0); 1738 break; 1739 case keyCodes.END: 1740 event.preventDefault(); 1741 navigateAndFocusIndicator(lastIndex); 1742 break; 1743 case keyCodes.SPACE: 1744 if (that._properties.autoplay && (event.target !== that._elements["previous"] && event.target !== that._elements["next"])) { 1745 event.preventDefault(); 1746 if (!that._paused) { 1747 pause(); 1748 } else { 1749 play(); 1750 } 1751 } 1752 if (event.target === that._elements["pause"]) { 1753 that._elements["play"].focus(); 1754 } 1755 if (event.target === that._elements["play"]) { 1756 that._elements["pause"].focus(); 1757 } 1758 break; 1759 default: 1760 return; 1761 } 1762 } 1763 1764 /** 1765 * Handles carousel mouseenter events 1766 * 1767 * @private 1768 * @param {Object} event The mouseenter event 1769 */ 1770 function onMouseEnter(event) { 1771 clearAutoplayInterval(); 1772 } 1773 1774 /** 1775 * Handles carousel mouseleave events 1776 * 1777 * @private 1778 * @param {Object} event The mouseleave event 1779 */ 1780 function onMouseLeave(event) { 1781 resetAutoplayInterval(); 1782 } 1783 1784 /** 1785 * Handles pause element click events 1786 * 1787 * @private 1788 * @param {Object} event The click event 1789 */ 1790 function onPauseClick(event) { 1791 pause(); 1792 that._elements["play"].focus(); 1793 } 1794 1795 /** 1796 * Handles play element click events 1797 * 1798 * @private 1799 * @param {Object} event The click event 1800 */ 1801 function onPlayClick() { 1802 play(); 1803 that._elements["pause"].focus(); 1804 } 1805 1806 /** 1807 * Pauses the playing of the Carousel. Sets {@code Carousel#_paused} marker. 1808 * Only relevant when autoplay is enabled 1809 * 1810 * @private 1811 */ 1812 function pause() { 1813 that._paused = true; 1814 clearAutoplayInterval(); 1815 refreshPlayPauseActions(); 1816 } 1817 1818 /** 1819 * Enables the playing of the Carousel. Sets {@code Carousel#_paused} marker. 1820 * Only relevant when autoplay is enabled 1821 * 1822 * @private 1823 */ 1824 function play() { 1825 that._paused = false; 1826 1827 // If the Carousel is hovered, don't begin auto transitioning until the next mouse leave event 1828 var hovered = false; 1829 if (that._elements.self.parentElement) { 1830 hovered = that._elements.self.parentElement.querySelector(":hover") === that._elements.self; 1831 } 1832 if (that._properties.autopauseDisabled || !hovered) { 1833 resetAutoplayInterval(); 1834 } 1835 1836 refreshPlayPauseActions(); 1837 } 1838 1839 /** 1840 * Refreshes the play/pause action markup based on the {@code Carousel#_paused} state 1841 * 1842 * @private 1843 */ 1844 function refreshPlayPauseActions() {
1845 setActionDisabled(that._elements["pause"], that._paused); 1846 setActionDisabled(that._elements["play"], !that._paused); 1847 } 1848 1849 /** 1850 * Initialize {@code Carousel#_active} based on the active item of the carousel. 1851 */ 1852 function initializeActive() { 1853 var items = that._elements["item"]; 1854 if (items && Array.isArray(items)) { 1855 for (var i = 0; i < items.length; i++) { 1856 if (items[i].classList.contains("cmp-carousel__item--active")) { 1857 that._active = i; 1858 break; 1859 } 1860 } 1861 } 1862 } 1863 1864 /** 1865 * Refreshes the item markup based on the current {@code Carousel#_active} index 1866 * 1867 * @private 1868 */ 1869 function refreshActive() { 1870 var items = that._elements["item"]; 1871 var indicators = that._elements["indicator"]; 1872 1873 if (items) { 1874 if (Array.isArray(items)) { 1875 for (var i = 0; i < items.length; i++) { 1876 if (i === parseInt(that._active)) { 1877 items[i].classList.add("cmp-carousel__item--active"); 1878 items[i].removeAttribute("aria-hidden"); 1879 indicators[i].classList.add("cmp-carousel__indicator--active"); 1880 indicators[i].setAttribute("aria-selected", true); 1881 indicators[i].setAttribute("tabindex", "0"); 1882 } else { 1883 items[i].classList.remove("cmp-carousel__item--active"); 1884 items[i].setAttribute("aria-hidden", true); 1885 indicators[i].classList.remove("cmp-carousel__indicator--active"); 1886 indicators[i].setAttribute("aria-selected", false); 1887 indicators[i].setAttribute("tabindex", "-1"); 1888 } 1889 } 1890 } else { 1891 // only one item 1892 items.classList.add("cmp-carousel__item--active"); 1893 indicators.classList.add("cmp-carousel__indicator--active"); 1894 } 1895 } 1896 } 1897 1898 /** 1899 * Focuses the element and prevents scrolling the element into view 1900 * 1901 * @param {HTMLElement} element Element to focus 1902 */ 1903 function focusWithoutScroll(element) { 1904 var x = window.scrollX || window.pageXOffset; 1905 var y = window.scrollY || window.pageYOffset; 1906 element.focus(); 1907 window.scrollTo(x, y); 1908 } 1909 1910 /** 1911 * Retrieves the next active index, with looping 1912 * 1913 * @private 1914 * @returns {Number} Index of the next carousel item 1915 */ 1916 function getNextIndex() { 1917 return that._active === (that._elements["item"].length - 1) ? 0 : that._active + 1; 1918 } 1919 1920 /** 1921 * Retrieves the previous active index, with looping 1922 * 1923 * @private 1924 * @returns {Number} Index of the previous carousel item 1925 */ 1926 function getPreviousIndex() { 1927 return that._active === 0 ? (that._elements["item"].length - 1) : that._active - 1; 1928 } 1929 1930 /** 1931 * Navigates to the item at the provided index 1932 * 1933 * @private 1934 * @param {Number} index The index of the item to navigate to 1935 * @param {Boolean} keepHash true to keep the hash in the URL, false to update it 1936 */ 1937 function navigate(index, keepHash) { 1938 if (index < 0 || index > (that._elements["item"].length - 1)) { 1939 return; 1940 } 1941 1942 that._active = index; 1943 refreshActive(); 1944 1945 if (!keepHash && containerUtils) { 1946 containerUtils.updateUrlHash(that, "item", index); 1947 } 1948 1949 if (dataLayerEnabled) { 1950 var carouselId = that._elements.self.id; 1951 var activeItem = getDataLayerId(that._elements.item[index]); 1952 var updatePayload = { component: {} }; 1953 updatePayload.component[carouselId] = { shownItems: [activeItem] }; 1954 1955 var removePayload = { component: {} }; 1956 removePayload.component[carouselId] = { shownItems: undefined }; 1957 1958 dataLayer.push(removePayload); 1959 dataLayer.push(updatePayload); 1960 } 1961 1962 // reset the autoplay transition interval following navigation, if not already hovering the carousel
1963 if (that._elements.self.parentElement) { 1964 if (that._elements.self.parentElement.querySelector(":hover") !== that._elements.self) { 1965 resetAutoplayInterval(); 1966 } 1967 } 1968 } 1969 1970 /** 1971 * Navigates to the item at the provided index and ensures the active indicator gains focus 1972 * 1973 * @private 1974 * @param {Number} index The index of the item to navigate to 1975 * @param {Boolean} keepHash true to keep the hash in the URL, false to update it 1976 */ 1977 function navigateAndFocusIndicator(index, keepHash) { 1978 navigate(index, keepHash); 1979 focusWithoutScroll(that._elements["indicator"][index]); 1980 1981 if (dataLayerEnabled) { 1982 dataLayer.push({ 1983 event: "cmp:show", 1984 eventInfo: { 1985 path: "component." + getDataLayerId(that._elements.item[index]) 1986 } 1987 }); 1988 } 1989 } 1990 1991 /** 1992 * Starts/resets automatic slide transition interval 1993 * 1994 * @private 1995 */ 1996 function resetAutoplayInterval() { 1997 if (that._paused || !that._properties.autoplay) { 1998 return; 1999 } 2000 clearAutoplayInterval(); 2001 that._autoplayIntervalId = window.setInterval(function() { 2002 if (document.visibilityState && document.hidden) { 2003 return; 2004 } 2005 var indicators = that._elements["indicators"]; 2006 if (indicators !== document.activeElement && indicators.contains(document.activeElement)) { 2007 // if an indicator has focus, ensure we switch focus following navigation 2008 navigateAndFocusIndicator(getNextIndex(), true); 2009 } else { 2010 navigate(getNextIndex(), true); 2011 } 2012 }, that._properties.delay); 2013 } 2014 2015 /** 2016 * Clears/pauses automatic slide transition interval 2017 * 2018 * @private 2019 */ 2020 function clearAutoplayInterval() { 2021 window.clearInterval(that._autoplayIntervalId); 2022 that._autoplayIntervalId = null; 2023 } 2024 2025 /** 2026 * Sets the disabled state for an action and toggles the appropriate CSS classes 2027 * 2028 * @private 2029 * @param {HTMLElement} action Action to disable 2030 * @param {Boolean} [disable] {@code true} to disable, {@code false} to enable 2031 */ 2032 function setActionDisabled(action, disable) { 2033 if (!action) { 2034 return; 2035 } 2036 if (disable !== false) { 2037 action.disabled = true; 2038 action.classList.add("cmp-carousel__action--disabled"); 2039 } else { 2040 action.disabled = false; 2041 action.classList.remove("cmp-carousel__action--disabled"); 2042 } 2043 } 2044 } 2045 2046 /** 2047 * Parses the dataLayer string and returns the ID 2048 * 2049 * @private 2050 * @param {HTMLElement} item the accordion item 2051 * @returns {String} dataLayerId or undefined 2052 */ 2053 function getDataLayerId(item) { 2054 if (item) { 2055 if (item.dataset.cmpDataLayer) { 2056 return Object.keys(JSON.parse(item.dataset.cmpDataLayer))[0]; 2057 } else { 2058 return item.id; 2059 } 2060 } 2061 return null; 2062 } 2063 2064 /** 2065 * Document ready handler and DOM mutation observers. Initializes Carousel components as necessary. 2066 * 2067 * @private 2068 */ 2069 function onDocumentReady() { 2070 dataLayerEnabled = document.body.hasAttribute("data-cmp-data-layer-enabled"); 2071 if (dataLayerEnabled) { 2072 dataLayerName = document.body.getAttribute("data-cmp-data-layer-name") || "adobeDataLayer"; 2073 dataLayer = window[dataLayerName] = window[dataLayerName] || []; 2074 } 2075 2076 var elements = document.querySelectorAll(selectors.self); 2077 for (var i = 0; i < elements.length; i++) { 2078 new Carousel({ element: elements[i], options: CMP.utils.readData(elements[i], IS) }); 2079 } 2080 2081 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; 2082 var body = document.querySelector("body"); 2083 var observer = new MutationObserver(function(mutations) { 2084 mutations.forEach(function(mutation) { 2085 // needed for IE 2086 var nodesArray = [].slice.call(mutation.addedNodes); 2087 if (nodesArray.length > 0) { 2088 nodesArray.forEach(function(addedNode) { 2089 if (addedNode.querySelectorAll) { 2090 var elementsArray = [].slice.call(addedNode.querySelectorAll(selectors.self)); 2091 elementsArray.forEach(function(element) { 2092 new Carousel({ element: element, options: CMP.utils.readData(element, IS) }); 2093 }); 2094 } 2095 }); 2096 } 2097 }); 2098 }); 2099 2100 observer.observe(body, { 2101 subtree: true, 2102 childList: true, 2103 characterData: true 2104 }); 2105 } 2106 2107 var documentReady = document.readyState !== "loading" ? Promise.resolve() : new Promise(function(resolve) { 2108 document.addEventListener("DOMContentLoaded", resolve); 2109 }); 2110 Promise.all([documentReady]).then(onDocumentReady); 2111 2112 if (containerUtils) { 2113 window.addEventListener("load", containerUtils.scrollToAnchor, false); 2114 } 2115 2116}()); 2117 2118"use strict";(()=>{var d="cmp",g="image",R=0,T="{.width}",v="width={width}",W="{dpr}",w={self:`[data-${d}-is="${g}"]`,image:'[data-cmp-hook-image="image"]',map:'[data-cmp-hook-image="map"]',area:'[data-cmp-hook-image="area"]',viewer:'[data-cmp-hook-image="viewer"]'},O="op_blur=",I="cmp-image__image--is-loading",L="cmp-image__image--smart-crop-loading",y="SmartCrop:Auto",D="data-cmp-svg",p={widths:{default:[],transform:function(n){let e=[];return n.split(",").forEach(function(t){let i=parseFloat(t);isNaN(i)||e.push(i)}),e}},lazy:{default:!1,transform:function(n){return!(n===null||typeof n>"u")}},dmimage:{default:!1,transform:function(n){return!(n===null||typeof n>"u")}},lazythreshold:{default:0,transform:function(n){let e=parseInt(n);return isNaN(e)?R:e}},src:{transform:function(n){return decodeURIComponent(n)}},width:{default:1920,transform:function(n){let e=parseInt(n,10);return isNaN(e)?1920:e}}},E=window.devicePixelRatio||1,A=window.Granite;function S(n){let e=n.dataset,t={},i=g;i=i.charAt(0).toUpperCase()+i.slice(1);let a=["is",`hook${i}`];
2118for(let s in e)if(f(e,s)){let r=e[s];s.indexOf(d)===0&&(s=s.slice(d.length),s=s.charAt(0).toLowerCase()+s.substring(1),a.indexOf(s)===-1&&(t[s]=r))}return t}function f(n,e){return Object.prototype.hasOwnProperty.call(n,e)}function b(n,e){let t;return function(...i){clearTimeout(t),t=setTimeout(()=>n.apply(this,i),e)}}var u=class{smartCrops={};useAssetDelivery=!1;srcUriTemplateWidthVar=T;isImageV3=!1;constructor(e){e&&e.element&&(this.config=e,this.init())}async init(){let{element:e,options:t}=this.config;if(e.removeAttribute(`data-${d}-is`),t.src&&t.src.indexOf(v)>=0&&(this.useAssetDelivery=!0,this.srcUriTemplateWidthVar=v),this.setupProperties(t),this.cacheElements(e),this.isImageV3=this._elements.self.hasAttribute("data-cmp-hook-image")&&this._elements.self.getAttribute("data-cmp-hook-image")==="imageV3",this.isImageV3||this._elements.viewer||(await this.fetchSmartCropData(t),this._elements.container=this._elements.link?this._elements.link:this._elements.self,!this._elements.image))return;A?.author?.util===void 0&&!e.hasAttribute(D)&&this.addLoader(),this._elements.map&&this._elements.image.addEventListener("load",this.onImageMapLoad),this.hasSmartCropRendition&&!this.isSmartCropAuto&&this.removeLoader(),window.addEventListener("resize",this.onWindowResize);let i=["focus","click","transitionend","animationend","scroll","DOMContentLoaded"],a=["load"];i.forEach(r=>{document.addEventListener(r,this.update)}),a.forEach(r=>{window.addEventListener(r,this.update)});let s=this._elements.image.closest(".segmented-component");s&&s.addEventListener("segment-parsed",r=>{let o=r.target;!o||!o.contains(this._elements.image)||this.update()}),this._elements.image.addEventListener("cmp-image-redraw",this.update),this.update(),requestAnimationFrame(()=>{this.setInitialInlineWidth(),this._elements.image.classList.remove("wc-not-ready")})}async fetchSmartCropData(e){if(!(e.src&&f(e,"dmimage")&&e.smartcroprendition===y))return;let i=decodeURIComponent(e.src).split(this.srcUriTemplateWidthVar)[0],a=i.includes("?")?"&":"?",s=`${i}${a}req=set,json`;try{let r=await fetch(s,{method:"GET"});if(!r.ok)throw new Error(`Request failed with status ${r.status}`);let o=await r.text(),m=/^(?:\/\*jsonp\*\/)?\s*([^()]+)\(([\s\S]+),\s*"[0-9]*"\);?$/im,l=/^{[\s\S]*}$/im,_=m.exec(o),c={};if(_){let h=_[2];l.test(h)&&(c=JSON.parse(h))}if(c?.set?.relation?.length>0)for(let h=0;h<c.set.relation.length;h++)this.smartCrops[parseInt(c.set.relation[h].userdata.SmartCropWidth,10)]=`:${c.set.relation[h].userdata.SmartCropDef}`}catch{}}loadImage(){let e=this._properties.widths&&this._properties.widths.length>0||this.hasSmartCrops,t="";if(this.hasSmartCrops){let m=Object.keys(this.smartCrops).map(Number),l=this.getOptimalWidth(m,!1,!0);t=this.smartCrops[l]}else t=e?`${this._properties.dmimage?"":"."}${this.getOptimalWidth(this._properties.widths,!0,!1)}`:"";this.useAssetDelivery&&(t=t!==""?`width=${t.substring(1)}`:"");let i=this._properties.src.replace(this.srcUriTemplateWidthVar,t);i=i.replace(W,String(E));let a=this._elements.image,s=a.getAttribute("src"),r=s?decodeURIComponent(s):null;a.removeEventListener("load",this.onImageLoad),a.addEventListener("load",this.onImageLoad),a.removeEventListener("error",this.onImageError),a.addEventListener("error",this.onImageError);let o=!1;if(i!==r)if(r===null||r&&r.includes(O))a.setAttribute("src",i),o=!0;else{let m=this._properties.src.split(this.srcUriTemplateWidthVar),l=r.startsWith(m[0]);l&&m.length>1&&(l=r.endsWith(m[m.length-1])),l&&(a.setAttribute("src",i),o=!0,e||window.removeEventListener("scroll",this.update))}(this.isImageLoaded(i)||!o)&&this.onImageLoad()}isImageLoaded(e){let t=this._elements.image.getAttribute("src"),i=t?decodeURIComponent(t):null;return this._elements.image.complete&&this._elements.image.naturalWidth>0&&i===e}getOptimalWidth(e,t,i){let a=this._elements.self,s=a.clientWidth;for(;s===0&&a.parentNode;)a=a.parentNode,s=a.clientWidth;let o=s*(t?E:1),m=e.length,l=0;for(;l<m-1&&e[l]<o;)l++;return this._properties.dmimage&&!i&&e[l]>this._properties.width?this._properties.width:e[l]}addLoader(){this._elements.image.classList.add(I),this.hasSmartCrops&&this._elements.image.classList.add(L)}removeLoader=()=>{this._elements.image.classList.remove(I),this._elements.image.classList.remove(L)};onImageLoad=()=>
2118{let{naturalWidth:e,naturalHeight:t}=this._elements.image;this.hasSmartCropRendition||this.updateImageDimensions(e,t),this.isImageHasNaturalDimensions&&(this._elements.image.style.removeProperty("width"),this._elements.image.style.removeProperty("--cmp-image-loading-width"),this.removeLoader(),this._elements.image.removeEventListener("load",this.onImageLoad),this._elements.image.removeEventListener("error",this.onImageError))};onImageError=()=>{this.removeLoader(),this._elements.image.removeEventListener("error",this.onImageError),this._elements.image.removeEventListener("load",this.onImageLoad)};updateImageDimensions(e,t){e!==void 0&&e<=0||t!==void 0&&t<=0||(e?this._elements.image?.setAttribute("width",String(e)):this._elements.image?.removeAttribute("width"),t?this._elements.image?.setAttribute("height",String(t)):this._elements.image?.removeAttribute("height"))}setInitialInlineWidth(){this.isImageWidthAuto&&this._properties.width&&!this._elements.image.style.width&&this._elements.image.style.setProperty("--cmp-image-loading-width",`${this._properties.width}px`)}isLazyVisible(){if(this._elements.container?.offsetParent===null)return!1;let e=window.pageYOffset,t=e+document.documentElement.clientHeight,i=(this._elements.container?.getBoundingClientRect().top||0)+e;return i+(this._elements.container?.clientHeight||0)>=e-this._properties.lazythreshold&&i<=t+this._properties.lazythreshold}resizeAreas(){if(this._elements.areas&&this._elements.areas.length>0)for(let e=0;e<this._elements.areas.length;e++){let t=this._elements.image.width,i=this._elements.image.height;if(t&&i){let a=this._elements.areas[e].dataset.cmpRelcoords;if(a){let s=a.split(",").map(Number),r=new Array(s.length);for(let o=0;o<r.length;o++)o%2===0?r[o]=s[o]*t:r[o]=s[o]*i;this._elements.areas[e].coords=r.join(",")}}}}cacheElements(e){this._elements={},this._elements.self=e;let t=this._elements.self.querySelectorAll(`[data-${d}-hook-${g}]`);for(let i=0;i<t.length;i++){let a=t[i],s=g;s=s.charAt(0).toUpperCase()+s.slice(1);let r=a.dataset[`${d}Hook${s}`];this._elements[r]=a}}setupProperties(e){this._properties={};for(let t in p)if(f(p,t)){let i=p[t];e&&e[t]!=null?i&&typeof i.transform=="function"?this._properties[t]=i.transform(e[t]):this._properties[t]=e[t]:this._properties[t]=p[t].default}}onWindowResize=b(()=>{this.update(),this.resizeAreas()},100);onImageMapLoad=b(()=>{this.resizeAreas()},100);update=()=>{A?.author?.util===void 0&&this._properties.lazy?this.isLazyVisible()&&this.loadImage():this.loadImage()};get isImageHasNaturalDimensions(){return this._elements.image.naturalWidth>0&&this._elements.image.naturalHeight>0}get isImageWidthAuto(){return this._elements.self.classList.contains("cmp-image--do-not-stretch")||this._elements.image.closest("ehe-banner-product")!==null||window.getComputedStyle(this._elements.image).width==="auto"}get hasSmartCrops(){return Object.keys(this.smartCrops).length>0}get hasSmartCropRendition(){return!!this.config.options.smartcroprendition}get isSmartCropAuto(){return this.config.options.smartcroprendition===y}};function C(){let n=document.querySelectorAll(w.self);for(let i=0;i<n.length;i++)new u({element:n[i],options:S(n[i])});let e=document.querySelector("body");new MutationObserver(function(i){for(let a of i)Array.from(a.addedNodes).filter(r=>r instanceof Element).forEach(function(r){Array.from(r.querySelectorAll(w.self)).forEac
2118h(function(m){new u({element:m,options:S(m)})})})}).observe(e,{subtree:!0,childList:!0,characterData:!0})}document.readyState!=="loading"?C():document.addEventListener("DOMContentLoaded",C);})(); 2119 2120/******************************************************************************* 2121 * Copyright 2017 Adobe 2122 * 2123 * Licensed under the Apache License, Version 2.0 (the "License"); 2124 * you may not use this file except in compliance with the License. 2125 * You may obtain a copy of the License at 2126 * 2127 * http://www.apache.org/licenses/LICENSE-2.0 2128 * 2129 * Unless required by applicable law or agreed to in writing, software 2130 * distributed under the License is distributed on an "AS IS" BASIS, 2131 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 2132 * See the License for the specific language governing permissions and 2133 * limitations under the License. 2134 ******************************************************************************/ 2135(function() { 2136 "use strict"; 2137 2138 var NS = "cmp"; 2139 var IS = "search"; 2140 2141 var DELAY = 300; // time before fetching new results when the user is typing a search string 2142 var LOADING_DISPLAY_DELAY = 300; // minimum time during which the loading indicator is displayed 2143 var PARAM_RESULTS_OFFSET = "resultsOffset"; 2144 2145 var keyCodes = { 2146 TAB: 9, 2147 ENTER: 13, 2148 ESCAPE: 27, 2149 ARROW_UP: 38, 2150 ARROW_DOWN: 40 2151 }; 2152 2153 var selectors = { 2154 self: "[data-" + NS + '-is="' + IS + '"]', 2155 item: { 2156 self: "[data-" + NS + "-hook-" + IS + '="item"]', 2157 title: "[data-" + NS + "-hook-" + IS + '="itemTitle"]', 2158 focused: "." + NS + "-search__item--is-focused" 2159 } 2160 }; 2161 2162 var properties = { 2163 /** 2164 * The minimum required length of the search term before results are fetched. 2165 * 2166 * @memberof Search 2167 * @type {Number} 2168 * @default 3 2169 */ 2170 minLength: { 2171 "default": 3, 2172 transform: function(value) { 2173 value = parseFloat(value); 2174 return isNaN(value) ? null : value; 2175 } 2176 }, 2177 /** 2178 * The maximal number of results fetched by a search request. 2179 * 2180 * @memberof Search 2181 * @type {Number} 2182 * @default 10 2183 */ 2184 resultsSize: { 2185 "default": 10, 2186 transform: function(value) { 2187 value = parseFloat(value); 2188 return isNaN(value) ? null : value; 2189 } 2190 } 2191 }; 2192 2193 var idCount = 0; 2194 2195 function readData(element) { 2196 var data = element.dataset; 2197 var options = []; 2198 var capitalized = IS; 2199 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 2200 var reserved = ["is", "hook" + capitalized]; 2201 2202 for (var key in data) { 2203 if (Object.prototype.hasOwnProperty.call(data, key)) { 2204 var value = data[key]; 2205 2206 if (key.indexOf(NS) === 0) { 2207 key = key.slice(NS.length); 2208 key = key.charAt(0).toLowerCase() + key.substring(1); 2209 2210 if (reserved.indexOf(key) === -1) { 2211 options[key] = value; 2212 } 2213 } 2214 } 2215 } 2216 2217 return options; 2218 } 2219 2220 function toggleShow(element, show) { 2221 if (element) { 2222 if (show !== false) { 2223 element.style.display = "block"; 2224 element.setAttribute("aria-hidden", false); 2225 } else { 2226 element.style.display = "none"; 2227 element.setAttribute("aria-hidden", true); 2228 } 2229 } 2230 } 2231 2232 function serialize(form) { 2233 var query = []; 2234 if (form && form.elements) { 2235 for (var i = 0; i < form.elements.length; i++) { 2236 var node = form.elements[i]; 2237 if (!node.disabled && node.name) { 2238 var param = [node.name, encodeURIComponent(node.value)]; 2239 query.push(param.join("=")); 2240 } 2241 } 2242 } 2243 return query.join("&"); 2244 } 2245 2246 function mark(node, regex) { 2247 if (!node || !regex) { 2248 return; 2249 } 2250 2251 // text nodes 2252 if (node.nodeType === 3) { 2253 var nodeValue = node.nodeValue; 2254 var match = regex.exec(nodeValue); 2255 2256 if (nodeValue && match) { 2257 var element = document.createElement("mark"); 2258 element.className = NS + "-search__item-mark"; 2259 element.appendChild(document.createTextNode(match[0])); 2260 2261 var after = node.splitText(match.index); 2262 after.nodeValue = after.nodeValue.substring(match[0].length); 2263 node.parentNode.insertBefore(element, after); 2264 } 2265 } else if (node.hasChildNodes()) { 2266 for (var i = 0; i < node.childNodes.length; i++) { 2267 // recurse 2268 mark(node.childNodes[i], regex); 2269 } 2270 } 2271 } 2272 2273 function Search(config) { 2274 if (config.element) { 2275 // prevents multiple initialization 2276 config.element.removeAttribute("data-" + NS + "-is"); 2277 } 2278 2279 this._cacheElements(config.element); 2280 this._setupProperties(config.options); 2281 2282 this._action = this._elements.form.getAttribute("action"); 2283 this._resultsOffset = 0; 2284 this._hasMoreResults = true; 2285 2286 this._elements.input.addEventListener("input", this._onInput.bind(this)); 2287 this._elements.input.addEventListener("focus", this._onInput.bind(this)); 2288 this._elements.input.addEventListener("keydown", this._onKeydown.bind(this)); 2289 this._elements.clear.addEventListener("click", this._onClearClick.bind(this)); 2290 document.addEventListener("click", this._onDocumentClick.bind(this)); 2291 this._elements.results.addEventListener("scroll", this._onScroll.bind(this)); 2292 2293 this._makeAccessible(); 2294 } 2295 2296 Search.prototype._displayResults = function() { 2297 if (this._elements.input.value.length === 0) { 2298 toggleShow(this._elements.clear, false); 2299 this._cancelResults(); 2300 } else if (this._elements.input.value.length < this._properties.minLength) { 2301 toggleShow(this._elements.clear, true); 2302 } else { 2303 this._updateResults(); 2304 toggleShow(this._elements.clear, true); 2305 } 2306 }; 2307 2308 Search.prototype._onScroll = function(event) { 2309 // fetch new results when the results to be scrolled down are less than the visible results 2310 if (this._elements.results.scrollTop + 2 * this._elements.results.clientHeight >= this._elements.results.scrollHeight) { 2311 this._resultsOffset += this._properties.resultsSize; 2312 this._displayResults(); 2313 } 2314 }; 2315 2316 Search.prototype._onInput = function(event) { 2317 var self = this; 2318 self._cancelResults(); 2319 // start searching when the search term reaches the minimum length 2320 this._timeout = setTimeout(function() { 2321 self._displayResults(); 2322 }, DELAY); 2323 }; 2324 2325 Search.prototype._onKeydown = function(event) { 2326 var self = this; 2327 2328 switch (event.keyCode) { 2329 case keyCodes.TAB: 2330 if (self._resultsOpen()) { 2331 toggleShow(self._elements.results, false); 2332 self._elements.input.setAttribute("aria-expanded", "false"); 2333 } 2334 break;
2335 case keyCodes.ENTER: 2336 event.preventDefault(); 2337 if (self._resultsOpen()) { 2338 var focused = self._elements.results.querySelector(selectors.item.focused); 2339 if (focused) { 2340 focused.click(); 2341 } 2342 } 2343 break; 2344 case keyCodes.ESCAPE: 2345 self._cancelResults(); 2346 break; 2347 case keyCodes.ARROW_UP: 2348 if (self._resultsOpen()) { 2349 event.preventDefault(); 2350 self._stepResultFocus(true); 2351 } 2352 break; 2353 case keyCodes.ARROW_DOWN: 2354 if (self._resultsOpen()) { 2355 event.preventDefault(); 2356 self._stepResultFocus(); 2357 } else { 2358 // test the input and if necessary fetch and display the results 2359 self._onInput(); 2360 } 2361 break; 2362 default: 2363 return; 2364 } 2365 }; 2366 2367 Search.prototype._onClearClick = function(event) { 2368 event.preventDefault(); 2369 this._elements.input.value = ""; 2370 toggleShow(this._elements.clear, false); 2371 toggleShow(this._elements.results, false); 2372 this._elements.input.setAttribute("aria-expanded", "false"); 2373 }; 2374 2375 Search.prototype._onDocumentClick = function(event) { 2376 var inputContainsTarget = this._elements.input.contains(event.target); 2377 var resultsContainTarget = this._elements.results.contains(event.target); 2378 2379 if (!(inputContainsTarget || resultsContainTarget)) { 2380 toggleShow(this._elements.results, false); 2381 this._elements.input.setAttribute("aria-expanded", "false"); 2382 } 2383 }; 2384 2385 Search.prototype._resultsOpen = function() { 2386 return this._elements.results.style.display !== "none"; 2387 }; 2388 2389 Search.prototype._makeAccessible = function() { 2390 var id = NS + "-search-results-" + idCount; 2391 this._elements.input.setAttribute("aria-owns", id); 2392 this._elements.results.id = id; 2393 idCount++; 2394 }; 2395 2396 Search.prototype._generateItems = function(data, results) { 2397 var self = this; 2398 2399 data.forEach(function(item) { 2400 var el = document.createElement("span"); 2401 el.innerHTML = self._elements.itemTemplate.innerHTML; 2402 el.querySelectorAll(selectors.item.title)[0].appendChild(document.createTextNode(item.title)); 2403 el.querySelectorAll(selectors.item.self)[0].setAttribute("href", self._safeHref(item.url)); 2404 results.innerHTML += el.innerHTML; 2405 }); 2406 }; 2407 2408 Search.prototype._safeHref = function(href) { 2409 var a = document.createElement("a"); 2410 a.href = href; 2411 return a.pathname; 2412 }; 2413 2414 Search.prototype._markResults = function() { 2415 var nodeList = this._elements.results.querySelectorAll(selectors.item.self); 2416 var escapedTerm = this._elements.input.value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 2417 var regex = new RegExp("(" + escapedTerm + ")", "gi"); 2418 2419 for (var i = this._resultsOffset - 1; i < nodeList.length; ++i) { 2420 var result = nodeList[i]; 2421 mark(result, regex); 2422 } 2423 }; 2424 2425 Search.prototype._stepResultFocus = function(reverse) { 2426 var results = this._elements.results.querySelectorAll(selectors.item.self); 2427 var focused = this._elements.results.querySelector(selectors.item.focused); 2428 var newFocused; 2429 var index = Array.prototype.indexOf.call(results, focused); 2430 var focusedCssClass = NS + "-search__item--is-focused"; 2431 2432 if (results.length > 0) { 2433 2434 if (!reverse) { 2435 // highlight the next result 2436 if (index < 0) { 2437 results[0].classList.add(focusedCssClass); 2438 results[0].setAttribute("aria-selected", "true"); 2439 } else if (index + 1 < results.length) { 2440 results[index].classList.remove(focusedCssClass); 2441 results[index].setAttribute("aria-selected", "false"); 2442 results[index + 1].classList.add(focusedCssClass); 2443 results[index + 1].setAttribute("aria-selected", "true"); 2444 } 2445 2446 // if the last visible result is partially hidden, scroll up until it's completely visible 2447 newFocused = this._elements.results.querySelector(selectors.item.focused); 2448 if (newFocused) { 2449 var bottomHiddenHeight = newFocused.offsetTop + newFocused.offsetHeight - this._elements.results.scrollTop - this._elements.results.clientHeight; 2450 if (bottomHiddenHeight > 0) { 2451 this._elements.results.scrollTop += bottomHiddenHeight; 2452 } else { 2453 this._onScroll(); 2454 } 2455 } 2456 2457 } else { 2458 // highlight the previous result 2459 if (index >= 1) { 2460 results[index].classList.remove(focusedCssClass); 2461 results[index].setAttribute("aria-selected", "false"); 2462 results[index - 1].classList.add(focusedCssClass); 2463 results[index - 1].setAttribute("aria-selected", "true"); 2464 } 2465 2466 // if the first visible result is partially hidden, scroll down until it's completely visible 2467 newFocused = this._elements.results.querySelector(selectors.item.focused); 2468 if (newFocused) { 2469 var topHiddenHeight = this._elements.results.scrollTop - newFocused.offsetTop; 2470 if (topHiddenHeight > 0) { 2471 this._elements.results.scrollTop -= topHiddenHeight; 2472 } 2473 } 2474 } 2475 } 2476 }; 2477 2478 Search.prototype._updateResults = function() { 2479 var self = this; 2480 if (self._hasMoreResults) { 2481 var request = new XMLHttpRequest(); 2482 var url = self._action + "?" + serialize(self._elements.form) + "&" + PARAM_RESULTS_OFFSET + "=" + self._resultsOffset; 2483 2484 request.open("GET", url, true); 2485 request.onload = function() { 2486 // when the results are loaded: hide the loading indicator and display the search icon after a minimum period 2487 setTimeout(function() { 2488 toggleShow(self._elements.loadingIndicator, false); 2489 toggleShow(self._elements.icon, true); 2490 }, LOADING_DISPLAY_DELAY); 2491 if (request.status >= 200 && request.status < 400) { 2492 // success status 2493 var data = JSON.parse(request.responseText); 2494 if (data.length > 0) { 2495 self._generateItems(data, self._elements.results); 2496 self._markResults(); 2497 toggleShow(self._elements.results, true); 2498 self._elements.input.setAttribute("aria-expanded", "true"); 2499 } else { 2500 self._hasMoreResults = false;
2501 } 2502 // the total number of results is not a multiple of the fetched results: 2503 // -> we reached the end of the query 2504 if (self._elements.results.querySelectorAll(selectors.item.self).length % self._properties.resultsSize > 0) { 2505 self._hasMoreResults = false; 2506 } 2507 } else { 2508 // error status 2509 } 2510 }; 2511 // when the results are loading: display the loading indicator and hide the search icon 2512 toggleShow(self._elements.loadingIndicator, true); 2513 toggleShow(self._elements.icon, false); 2514 request.send(); 2515 } 2516 }; 2517 2518 Search.prototype._cancelResults = function() { 2519 clearTimeout(this._timeout); 2520 this._elements.results.scrollTop = 0; 2521 this._resultsOffset = 0; 2522 this._hasMoreResults = true; 2523 this._elements.results.innerHTML = ""; 2524 this._elements.input.setAttribute("aria-expanded", "false"); 2525 }; 2526 2527 Search.prototype._cacheElements = function(wrapper) { 2528 this._elements = {}; 2529 this._elements.self = wrapper; 2530 var hooks = this._elements.self.querySelectorAll("[data-" + NS + "-hook-" + IS + "]"); 2531 2532 for (var i = 0; i < hooks.length; i++) { 2533 var hook = hooks[i]; 2534 var capitalized = IS; 2535 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 2536 var key = hook.dataset[NS + "Hook" + capitalized]; 2537 this._elements[key] = hook; 2538 } 2539 }; 2540 2541 Search.prototype._setupProperties = function(options) { 2542 this._properties = {}; 2543 2544 for (var key in properties) { 2545 if (Object.prototype.hasOwnProperty.call(properties, key)) { 2546 var property = properties[key]; 2547 if (options && options[key] != null) { 2548 if (property && typeof property.transform === "function") { 2549 this._properties[key] = property.transform(options[key]); 2550 } else { 2551 this._properties[key] = options[key]; 2552 } 2553 } else { 2554 this._properties[key] = properties[key]["default"]; 2555 } 2556 } 2557 } 2558 }; 2559 2560 function onDocumentReady() { 2561 var elements = document.querySelectorAll(selectors.self); 2562 for (var i = 0; i < elements.length; i++) { 2563 new Search({ element: elements[i], options: readData(elements[i]) }); 2564 } 2565 2566 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; 2567 var body = document.querySelector("body"); 2568 var observer = new MutationObserver(function(mutations) { 2569 mutations.forEach(function(mutation) { 2570 // needed for IE 2571 var nodesArray = [].slice.call(mutation.addedNodes); 2572 if (nodesArray.length > 0) { 2573 nodesArray.forEach(function(addedNode) { 2574 if (addedNode.querySelectorAll) { 2575 var elementsArray = [].slice.call(addedNode.querySelectorAll(selectors.self)); 2576 elementsArray.forEach(function(element) { 2577 new Search({ element: element, options: readData(element) }); 2578 }); 2579 } 2580 }); 2581 } 2582 }); 2583 }); 2584 2585 observer.observe(body, { 2586 subtree: true, 2587 childList: true, 2588 characterData: true 2589 }); 2590 } 2591 2592 if (document.readyState !== "loading") { 2593 onDocumentReady(); 2594 } else { 2595 document.addEventListener("DOMContentLoaded", onDocumentReady); 2596 } 2597 2598})(); 2599 2600;(function(ContextHub, document) { 2601 const STORE_NAME = 'profile'; 2602 const DATA_PREFILL_SELECTOR = 'data-prefill'; 2603 const DATA_PREFILL_NOOP_VALUE = 'none'; 2604 const PROFILE_STORE = ContextHub.getStore(STORE_NAME); 2605 2606 const handleInput = function(element, prefillValue) { 2607 element.value = PROFILE_STORE.getItem(prefillValue) 2608 element.dispatchEvent(new CustomEvent('prefill:done')) 2609 } 2610 2611 const strategies = new Map(); 2612 strategies.set('input', handleInput); 2613 2614 const findAndHandleFields = function(parent, prefillValue) {
2615 strategies.forEach((strategy, selector) => { 2616 const fields = parent.querySelectorAll(selector); 2617 if (fields && fields.length) { 2618 fields.forEach((field) => { 2619 strategy(field, prefillValue) 2620 }) 2621 } 2622 }) 2623 } 2624 2625 const prefillFields = function() { 2626 const elements = document.querySelectorAll(`[${DATA_PREFILL_SELECTOR}]`); 2627 elements.forEach( el => { 2628 const prefillValue = el.getAttribute(DATA_PREFILL_SELECTOR) || ''; 2629 if (!prefillValue || prefillValue == DATA_PREFILL_NOOP_VALUE){ 2630 return; 2631 } 2632 findAndHandleFields(el, prefillValue); 2633 }); 2634 } 2635 2636 function attachEvents() { 2637 PROFILE_STORE.eventing.on(ContextHub.Constants.EVENT_STORE_READY, prefillFields, 'ngp:profile.prefill-ready', true); 2638 } 2639 2640 if (document.readyState !== 'loading') { 2641 attachEvents() 2642 } else { 2643 document.addEventListener('DOMContentLoaded', attachEvents) 2644 } 2645 2646})(window.ContextHub, document); 2647 2648/******************************************************************************* 2649 * Copyright 2016 Adobe 2650 * 2651 * Licensed under the Apache License, Version 2.0 (the "License"); 2652 * you may not use this file except in compliance with the License. 2653 * You may obtain a copy of the License at 2654 * 2655 * http://www.apache.org/licenses/LICENSE-2.0 2656 * 2657 * Unless required by applicable law or agreed to in writing, software 2658 * distributed under the License is distributed on an "AS IS" BASIS, 2659 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 2660 * See the License for the specific language governing permissions and 2661 * limitations under the License. 2662 ******************************************************************************/ 2663(function() { 2664 "use strict"; 2665 2666 var NS = "cmp"; 2667 var IS = "formText"; 2668 var IS_DASH = "form-text"; 2669 var SELF_SELECTOR = "[data-" + NS + '-is="' + IS + '"]'; 2670 2671 var selectors = { 2672 self: SELF_SELECTOR, 2673 validationMessage: SELF_SELECTOR + " .cmp-form-text__validation-message" 2674 }; 2675 2676 var displayValidationMessage = false; 2677 2678 var properties = { 2679 /** 2680 * A validation message to display if there is a type mismatch between the user input and expected input. 2681 * 2682 * @type {String} 2683 */ 2684 constraintMessage: "", 2685 /** 2686 * A validation message to display if no input is supplied, but input is expected for the field. 2687 * 2688 * @type {String} 2689 */ 2690 requiredMessage: "" 2691 }; 2692 2693 function readData(element) { 2694 var data = element.dataset; 2695 var options = []; 2696 var capitalized = IS; 2697 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 2698 var reserved = ["is", "hook" + capitalized]; 2699 2700 for (var key in data) { 2701 if (Object.prototype.hasOwnProperty.call(data, key)) { 2702 var value = data[key]; 2703 2704 if (key.indexOf(NS) === 0) { 2705 key = key.slice(NS.length); 2706 key = key.charAt(0).toLowerCase() + key.substring(1); 2707 2708 if (reserved.indexOf(key) === -1) { 2709 options[key] = value; 2710 } 2711 } 2712 } 2713 } 2714 2715 return options; 2716 } 2717 2718 function FormText(config) { 2719 if (config.element) { 2720 // prevents multiple initialization 2721 config.element.removeAttribute("data-" + NS + "-is"); 2722 } 2723 2724 this._cacheElements(config.element); 2725 this._setupProperties(config.options); 2726 2727 this._elements.input.addEventListener("invalid", this._onInvalid.bind(this)); 2728 this._elements.input.addEventListener("input", this._onInput.bind(this)); 2729 if (displayValidationMessage) { 2730 this._elements.input.checkValidity(); 2731 } 2732 } 2733 2734 FormText.prototype._onInvalid = function(event) { 2735 event.target.setCustomValidity(""); 2736 if (event.target.validity.typeMismatch) { 2737 if (this._properties.constraintMessage) { 2738 event.target.setCustomValidity(this._properties.constraintMessage); 2739 } 2740 } else if (event.target.validity.valueMissing) { 2741 if (this._properties.requiredMessage) { 2742 event.target.setCustomValidity(this._properties.requiredMessage); 2743 } 2744 } 2745 if (displayValidationMessage) {
2746 var validationMessage = event.target.parentElement.querySelector(".cmp-form-text__validation-message"); 2747 if (validationMessage) { 2748 validationMessage.innerText = event.target.validationMessage; 2749 } 2750 } 2751 }; 2752 2753 FormText.prototype._onInput = function(event) { 2754 event.target.setCustomValidity(""); 2755 if (displayValidationMessage) { 2756 event.target.checkValidity(); 2757 } 2758 }; 2759 2760 FormText.prototype._cacheElements = function(wrapper) { 2761 this._elements = {}; 2762 this._elements.self = wrapper; 2763 var hooks = this._elements.self.querySelectorAll("[data-" + NS + "-hook-" + IS_DASH + "]"); 2764 for (var i = 0; i < hooks.length; i++) { 2765 var hook = hooks[i]; 2766 var capitalized = IS; 2767 capitalized = capitalized.charAt(0).toUpperCase() + capitalized.slice(1); 2768 var key = hook.dataset[NS + "Hook" + capitalized]; 2769 this._elements[key] = hook; 2770 } 2771 }; 2772 2773 FormText.prototype._setupProperties = function(options) { 2774 this._properties = {}; 2775 2776 for (var key in properties) { 2777 if (Object.prototype.hasOwnProperty.call(properties, key)) { 2778 var property = properties[key]; 2779 if (options && options[key] != null) { 2780 if (property && typeof property.transform === "function") { 2781 this._properties[key] = property.transform(options[key]); 2782 } else { 2783 this._properties[key] = options[key]; 2784 } 2785 } else { 2786 this._properties[key] = properties[key]["default"]; 2787 } 2788 } 2789 } 2790 }; 2791 2792 function onDocumentReady() { 2793 var validationMessages = document.querySelectorAll(selectors.validationMessage); 2794 if (validationMessages && validationMessages.length > 0) { 2795 displayValidationMessage = true; 2796 } 2797 2798 var elements = document.querySelectorAll(selectors.self); 2799 for (var i = 0; i < elements.length; i++) { 2800 new FormText({ element: elements[i], options: readData(elements[i]) }); 2801 } 2802 2803 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; 2804 var body = document.querySelector("body"); 2805 var observer = new MutationObserver(function(mutations) { 2806 mutations.forEach(function(mutation) { 2807 // needed for IE 2808 var nodesArray = [].slice.call(mutation.addedNodes); 2809 if (nodesArray.length > 0) { 2810 nodesArray.forEach(function(addedNode) { 2811 if (addedNode.querySelectorAll) { 2812 var elementsArray = [].slice.call(addedNode.querySelectorAll(selectors.self)); 2813 elementsArray.forEach(function(element) { 2814 new FormText({ element: element, options: readData(element) }); 2815 }); 2816 } 2817 }); 2818 } 2819 }); 2820 }); 2821 2822 observer.observe(body, { 2823 subtree: true, 2824 childList: true, 2825 characterData: true 2826 }); 2827 } 2828 2829 if (document.readyState !== "loading") { 2830 onDocumentReady(); 2831 } else { 2832 document.addEventListener("DOMContentLoaded", onDocumentReady); 2833 } 2834 2835})(); 2836 2837/******************************************************************************* 2838 * Copyright 2020 Adobe 2839 * 2840 * Licensed under the Apache License, Version 2.0 (the "License"); 2841 * you may not use this file except in compliance with the License. 2842 * You may obtain a copy of the License at 2843 * 2844 * http://www.apache.org/licenses/LICENSE-2.0 2845 * 2846 * Unless required by applicable law or agreed to in writing, software 2847 * distributed under the License is distributed on an "AS IS" BASIS, 2848 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 2849 * See the License for the specific language governing permissions and 2850 * limitations under the License. 2851 ******************************************************************************/ 2852(function() { 2853 "use strict"; 2854 2855 var NS = "cmp"; 2856 var IS = "pdfviewer"; 2857 var SDK_URL = "https://acrobatservices.adobe.com/view-sdk/viewer.js"; 2858 var SDK_READY_EVENT = "adobe_dc_view_sdk.ready"; 2859 2860 var selectors = { 2861 self: "[data-" + NS + '-is="' + IS + '"]', 2862 sdkScript: 'script[src="' + SDK_URL + '"]' 2863 }; 2864 2865 function initSDK() { 2866 var sdkIncluded = document.querySelectorAll(selectors.sdkScript).length > 0; 2867 if (!window.adobe_dc_view_sdk && !sdkIncluded) { 2868 var dcv = document.createElement("script"); 2869 dcv.type = "text/javascript"; 2870 dcv.src = SDK_URL; 2871 document.body.appendChild(dcv); 2872 } 2873 } 2874 2875 function previewPdf(component) { 2876 // prevents multiple initialization 2877 component.removeAttribute("data-" + NS + "-is"); 2878 2879 // add the view sdk to the page 2880 initSDK(); 2881 2882 // manage the preview 2883 if (component.dataset && component.id) { 2884 if (window.AdobeDC && window.AdobeDC.View) { 2885 dcView(component); 2886 } else { 2887 document.addEventListener(SDK_READY_EVENT, function() { 2888 dcView(component); 2889 }); 2890 } 2891 } 2892 } 2893 2894 function dcView(component) { 2895 var adobeDCView = new window.AdobeDC.View({ 2896 clientId: component.dataset.cmpClientId, 2897 divId: component.id + "-content", 2898 reportSuiteId: component.dataset.cmpReportSuiteId 2899 }); 2900 adobeDCView.previewFile({ 2901 content: { location: { url: component.dataset.cmpDocumentPath } }, 2902 metaData: { fileName: component.dataset.cmpDocumentFileName } 2903 }, JSON.parse(component.dataset.cmpViewerConfigJson)); 2904 } 2905 2906 /** 2907 * Document ready handler and DOM mutation observers. Initializes Accordion components as necessary. 2908 * 2909 * @private 2910 */ 2911 function onDocumentReady() { 2912 var elements = document.querySelectorAll(selectors.self); 2913 for (var i = 0; i < elements.length; i++) { 2914 previewPdf(elements[i]); 2915 } 2916 2917 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; 2918 var body = document.querySelector("body"); 2919 var observer = new MutationObserver(function(mutations) { 2920 mutations.forEach(function(mutation) { 2921 // needed for IE 2922 var nodesArray = [].slice.call(mutation.addedNodes); 2923 if (nodesArray.length > 0) { 2924 nodesArray.forEach(function(addedNode) { 2925 if (addedNode.querySelectorAll) { 2926 var elementsArray = [].slice.call(addedNode.querySelectorAll(selectors.self)); 2927 elementsArray.forEach(function(element) { 2928 previewPdf(element); 2929 }); 2930 } 2931 }); 2932 } 2933 }); 2934 }); 2935 2936 observer.observe(body, { 2937 subtree: true, 2938 childList: true, 2939 characterData: true 2940 }); 2941 2942 } 2943 2944 if (document.readyState !== "loading") { 2945 onDocumentReady(); 2946 } else { 2947 document.addEventListener("DOMContentLoaded", onDocumentReady); 2948 } 2949}()); 2950 2951(function (window, document) { 2952 2953 function attachEvents() { 2954 2955 const elementUrl = document.querySelector("meta[property='og:url']"); 2956 const url = elementUrl ? elementUrl.getAttribute('content') : ''; 2957 const elementMedia = document.querySelector("meta[property='og:image']"); 2958 const media = elementMedia ? elementMedia.getAttribute('content') : ''; 2959 const elementTitle = document.querySelector("meta[property='og:title']"); 2960 const title = elementTitle ? elementTitle.getAttribute('content') : ''; 2961 2962 document.querySelectorAll('.shareButtonDirectLink').forEach(button => 2963 button.addEventListener("click", () => social_sharing.clipboardShare(url)) 2964 ); 2965 2966 document.querySelectorAll('.shareButtonFacebook').forEach(button => 2967 button.addEventListener("click", () => social_sharing.fbShare(url)) 2968 ); 2969 2970 document.querySelectorAll('.shareButtonTwitter').forEach(button => 2971 button.addEventListener("click", () => social_sharing.ttShare(button.dataset.twitterParams)) 2972 ); 2973 2974 document.querySelectorAll('.shareButtonLinkedIn').forEach(button => 2975 button.addEventListener("click", () => social_sharing.linekdInShare(url)) 2976 ); 2977 2978 document.querySelectorAll('.shareButtonPinterest').forEach(button => 2979 button.addEventListener("click", () => social_sharing.pinterestShare(url, media, title)) 2980 ); 2981 } 2982 2983 if (document.readyState !== 'loading') { 2984 attachEvents(); 2985 } else { 2986 document.addEventListener('DOMContentLoaded', attachEvents) 2987 } 2988 2989})(window, document); 2990 2991window.social_sharing = (function () { 2992 2993 const popupWindow = function (url, title, w, h) { 2994 const left = (screen.width / 2) - (w / 2); 2995 const top = (screen.height / 2) - (h / 2); 2996 return window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=' + w + ', height=' + h + ', top='
2996+ top + ', left=' + left); 2997 }; 2998 2999 return { 3000 fbShare: function (url) { 3001 popupWindow('https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url), "", 800, 600); 3002 }, 3003 ttShare: function (params) { 3004 popupWindow('https://twitter.com/intent/tweet' + params, "", 800, 600); 3005 }, 3006 3007 linekdInShare: function (url) { 3008 popupWindow('https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(url), "", 800, 600); 3009 }, 3010 pinterestShare: function (url, media, description) { 3011 popupWindow('http://pinterest.com/pin/create/button/?url=' + encodeURIComponent(url) + '&media=' + encodeURIComponent(media) + '&description=' + encodeURIComponent(description), "", 800, 600); 3012 }, 3013 clipboardShare: function (url) { 3014 const textArea = document.createElement("textarea"); 3015 textArea.value = url; 3016 3017 textArea.style.top = "0"; 3018 textArea.style.left = "0"; 3019 textArea.style.position = "fixed"; 3020 3021 document.body.appendChild(textArea); 3022 textArea.focus(); 3023 textArea.select(); 3024 try { 3025 document.execCommand('copy'); 3026 } catch (err) { 3027 console.error('Unable to copy to clipboard', err); 3028 } 3029 textArea.remove(); 3030 } 3031 } 3032})(); 3033 3034// TOOD we should consider migrating this to TypeScript in the future 3035// this should improve code modularity and maintainability 3036 3037; (function () { 3038 const inEditor = isInEditor && isInEditor() 3039 const dataOdl = JSON.parse(document.body.dataset.oneDataLayer || '{}') 3040 const is404 = dataOdl && dataOdl.page && dataOdl.page.errorType === '404' 3041 3042 const DUMMY_CONTEXTHUB_IMPLEMENTATION = { 3043 getItem: function () { 3044 return null 3045 } 3046 } 3047 3048 const ATTR_SKIP_DEFAULT_EVENT = 'skipdefaultevent' 3049 3050 const ASSETS_EXTENSIONS = [ 3051 'doc', 'xlsx', 'rar', 'docx', 'tab', 'exe', 'eps', 'csv', 'wma', 'jpg', 3052 'zip', 'mov', 'png', 'txt', 'avi', 'svg', 'vsd', 'wmv', 'xls', 'vxd', 3053 'mp3', 'ppt', 'xml', 'wav', 'pptx', 'js', 'm4v', 'pdf', 'css', 'ris' 3054 ]; 3055 3056 const clearSourceObject = (source) => { 3057 Object.keys(source) 3058 .forEach(key => source[key] === undefined && delete source[key]) 3059 } 3060 3061 const runWhenPageFinishedLoading = (callback) => { 3062 if (document.readyState !== 'loading') { 3063 callback() 3064 } else { 3065 document.addEventListener('DOMContentLoaded', callback) 3066 } 3067 } 3068 3069 /** 3070 * Determines the position/area of a link element within the header structure. 3071 * 3072 * @param {Element} chosenElement - The DOM element to analyze for its position in the header 3073 * @returns {string|undefined} The header area where the element is located: 3074 * - 'Utilities area' if the element is in utilities slot, EHE-LOGIN component, mobile country selector, or mobile language selector 3075 * - 'Navigation area' if the element is in the menu component or mobile menu component 3076 * - 'Brand area' if the element has a brand-href attribute 3077 * - undefined if the element is not in any recognized header area 3078 */ 3079 const handleLinkPositionHeader = (chosenElement) => { 3080 // TODO add also other areas of header if needed 3081 const isInsideUtilitySlot = chosenElement.closest('[slot="utilities"]'); 3082 const isEheLoginComponent = chosenElement.nodeName === 'EHE-LOGIN'; 3083 const isInsideMobileContrySelector = chosenElement.closest('[data-component="countryselector-mobile"]'); 3084 const isInsideMobileLanguageSelector = chosenElement.closest('#language-selector-mobile'); 3085 const isInsideMenuComponent = chosenElement.closest('ehe-menu'); 3086 const isInsideMobileMenuComponent = chosenElement.getRootNode().host?.nodeName === 'EHE-MOBILE-MENU' || chosenElement.nodeName === 'EHE-MOBILE-MENU' || chosenElement.closest('[slot="mobile-navigation-item"]'); 3087 const isInsideBrandArea = chosenElement.hasAttribute('brand-href'); 3088 3089 if (isInsideUtilitySlot || isEheLoginComponent || isInsideMobileContrySelector || isInsideMobileLanguageSelector) { 3090 return 'Utilities area'; 3091 } 3092 3093 if (isInsideMenuComponent || isInsideMobileMenuComponent) { 3094 return 'Navigation area'; 3095 } 3096 3097 if (isInsideBrandArea) { 3098 return 'Brand area'; 3099 } 3100 3101 return undefined; 3102 } 3103 3104 /** 3105 * Handles link position detection for site analytics by traversing the event's composed path. 3106 * Searches for the nearest ancestor element with a 'data-odl-position' attribute. 3107 * If found, returns the attribute value; otherwise, falls back to getLinkFallbackPosition. 3108 * 3109 * @param {Event}
3109 event - The DOM event object containing the composed path of elements. 3110 * @returns {string} The link position value from the 'data-odl-position' attribute, 3111 * or the fallback position determined by getLinkFallbackPosition. 3112 */ 3113 const handleLinkPositionSite = (event) => { 3114 const DATA_ODL_POSITION_ATTR = 'data-odl-position'; 3115 3116 /** @type Element[] */ 3117 const composedPath = event.composedPath().filter(el => el instanceof Element); 3118 const odlPositionElements = composedPath.filter((el => el.hasAttribute(DATA_ODL_POSITION_ATTR))) 3119 3120 if (odlPositionElements.length) { 3121 return odlPositionElements[0].getAttribute(DATA_ODL_POSITION_ATTR); 3122 } 3123 3124 return getLinkFallbackPosition(composedPath); 3125 } 3126 3127 /** 3128 * Determines a fallback position/label for a link based on its component hierarchy. 3129 * Searches the composed path for either an EHE component or a CMP component and 3130 * extracts a formatted position name from the component's tag name or class name. 3131 * 3132 * @param {Element[]} composedPath - The event's composed path containing the DOM element hierarchy 3133 * @returns {string|undefined} A formatted component name (e.g., "My Component Name") if found, 3134 * otherwise undefined 3135 * 3136 * @example 3137 * // Returns "Data Table" for <ehe-data-table> 3138 * getLinkFallbackPosition([elementRef, eheDataTable]); 3139 * 3140 * @example 3141 * // Returns "Card" for <div class="cmp-card"> 3142 * getLinkFallbackPosition([elementRef, cmpCardDiv]); 3143 */ 3144 const getLinkFallbackPosition = (composedPath) => { 3145 const eheComponent = composedPath.find(el => el.tagName.toLowerCase().startsWith('ehe-')) 3146 if (eheComponent) { 3147 return eheComponent 3148 .tagName 3149 .toLowerCase() 3150 .replace('ehe-', '') 3151 .split('-') 3152 .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 3153 .join(' '); 3154 } 3155 3156 const cmpElement = composedPath.find(el => { 3157 const classList = Array.from(el.classList || []); 3158 return classList.some(cls => cls.startsWith('cmp-')); 3159 }); 3160 if (cmpElement) { 3161 const cmpClass = Array.from(cmpElement.classList).find(cls => cls.startsWith('cmp-')); 3162 return cmpClass 3163 .replace('cmp-', '') 3164 .split('-') 3165 .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 3166 .join(' '); 3167 } 3168 3169 return undefined 3170 } 3171 3172 /** 3173 * Determines the position/location of a link element within the page structure. 3174 * @param {Element} rootElement - The root element to use for determining link position, takes precedence over originalElement 3175 * @param {Element} originalElement - The original element that triggered the link detection 3176 * @param {Event} event - The event object associated with the link interaction 3177 * @returns {*} The link position information from the appropriate handler function, or undefined if no valid element is provided 3178 */ 3179 const getLinkPosition = (rootElement, originalElement, event) => { 3180 const chosenElement = rootElement || originalElement; 3181 if (!chosenElement) { 3182 return undefined; 3183 } 3184 3185 let linkMainArea = 'site'; 3186 3187 const isInsideHeader = chosenElement.closest('header') || chosenElement.getRootNode().host?.nodeName === 'EHE-HEADER' || chosenElement.getRootNode().host?.nodeName === 'EHE-MOBILE-MENU'; 3188 if (isInsideHeader) { 3189 linkMainArea = 'header'; 3190 } 3191 3192 switch (linkMainArea) { 3193 case 'header': { 3194 return handleLinkPositionHeader(chosenElement); 3195 } 3196 case 'site': 3197 default: { 3198 return handleLinkPositionSite(event); 3199 } 3200 } 3201 } 3202 3203 const getTimestamp = () => Math.round(new Date().getTime() / 1e3); 3204 3205 class OneDataLayer { 3206 constructor(body) { 3207 this.body = body 3208 this.mediaState = [] 3209 this.scrollPositionReached = { '25': false, '50': false, '75': false, '90': false, '100': false } 3210 3211 this.bindDom() 3212 this.bindData() 3213 this.bindEvents() 3214 3215 runWhenPageFinishedLoading(() => { 3216 window.EHEBookmarkManager.bookmarksAreReady().then(() => { 3217 const hasNoBookmarks = !window.EHEBookmarkManager.bookmarks || !window.EHEBookmarkManager.bookmarks.length 3218 const timeout = hasNoBookmarks ? 1500 : 0 3219 setTimeout(() => { 3220 this.handlePageLoad() 3221 document.dispatchEvent(new CustomEvent('OneDataLayer:loaded')) 3222 }, timeout) 3223 }) 3224 }) 3225 } 3226 3227 bindDom() { 3228 this.dynamicVideoContainers = this.body.querySelectorAll('.dynamic-video-component') 3229 } 3230 3231 bindData() { 3232 // scroll flags 3233 this.scrolledFifty = false 3234 this.scrolledNinety = false 3235 3236 this.contextHubProfile = (ContextHub && ContextHub.getStore('profile')) || DUMMY_CONTEXTHUB_IMPLEMENTATION 3237 this.oneDataLayerData = this.body.dataset.oneDataLayer 3238 3239 // handled events 3240 this.events = { 3241 onsiteSearch: 'onsiteSearch', 3242 scroll: 'scroll', 3243 video: { 3244 error: 'videoError', 3245 start: 'videoStart', 3246 complete: 'videoComplete', 3247 progress: 'videoProgress' 3248 }, 3249 defaultLinkClick: 'defaultLinkClick', 3250 downloadLinkClick: 'downloadLinkClick', 3251 audio: { 3252 start: 'audioStart', 3253 complete: 'audioComplete', 3254 progress: 'audioProgress', 3255 error: 'audioError' 3256 } 3257 } 3258 } 3259 3260 bindEvents() { 3261 if (!is404) { 3262 document.addEventListener('scroll', () => this.handlePageScroll(), { passive: true }) 3263 } 3264 document.addEventListener('OneDataLayer:search-event', (e) => this.handleSiteSearch(e.detail)) 3265 document.addEventListener('OneDataLayer:form-event', (e) => this.handleFormEvents(e.detail)) 3266 3267 window.addEventListener('OneTrustReady', () => { 3268 OneTrust.OnConsentChanged(() => { 3269 const user = { consent: this.prepareConsent() } 3270 this.addToMainDataObject({ 3271 event: 'consentUpdated', 3272 user 3273 }); 3274 }); 3275 }) 3276 3277 const isIOS = ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes(navigator.platform) 3278 || (navigator.userAgent.includes('Mac') && 'ontouchend' in document) 3279 const layerEventName = isIOS ? 'pagehide' : 'beforeunload'; 3280 3281 window.addEventListener('beforeunload', () => { 3282 clearInterval(this.videoProgressInterval) 3283 clearInterval(this.audioProgressInterval) 3284 }); 3285 3286 window.addEventListener(layerEventName, () => { 3287 this.setNewPageData(this.parsedOneDataLayerJSON, OneDataLayer.PREVIOUS_PAGE_DATA) 3288 }) 3289 3290 this.dynamicVideoEventsHandler() 3291 this.audioEventsHandler() 3292 3293 this.body.addEventListener('click', (e) => { 3294 const getText = (textValue) => { 3295 return textValue && textValue.trim() 3296 } 3297 3298 const target = e.target || e.srcElement 3299 3300 // Skip click events for Download buttons in Audio Card, they are handled separately 3301 if (target.nodeName === 'EHE-CARD-AUDIO' || target.nodeName === 'EHE-AUDIO-LIST') { 3302 return 3303 } 3304 3305 /** 3306 * Handles click on link element with href 3307 * @param {Element} element 3308 */
3309 const handleTagClick = (element) => { 3310 const { href, text, textContent } = element 3311 const tagRootNodeHost = element.getRootNode() && element.getRootNode().host 3312 const tagRootNodeHostTextContent = tagRootNodeHost && getText(tagRootNodeHost.textContent) 3313 const innertext = getText(text) || getText(textContent) || tagRootNodeHostTextContent || '' 3314 const position = getLinkPosition(tagRootNodeHost, element, e) 3315 // FIXME the extension test misses the dot, so it matches any URL whose last characters 3316 // happen to spell an extension - '/en/paris' matches 'ris', '/steps' matches 'eps', 3317 // '/tab' matches 'tab' - and those links are then reported as downloads. It also fails 3318 // on real files carrying a query string or hash ('file.pdf?v=2'). Should compare 3319 // `.${ext}` against the lower-cased `new URL(href, location.origin).pathname` instead. 3320 const hasAssetExtension = ASSETS_EXTENSIONS.some((ext) => href && href.endsWith(ext)) 3321 // FIXME the download name does not follow the oneDataLayer spec, which wants the file 3322 // name (or the literal 'other' when it is unknown): 3323 // - `split('.').shift()` truncates names containing dots ('report.v2.pdf' -> 'report') 3324 // - the value is never decodeURIComponent'd, so percent-encoded names leak through 3325 // - when the link is a download because of the `download` attribute rather than a known 3326 // extension, the name falls back to the CSS class list (e.g. 'download-link') 3327 const fileName = href && href.split('/').pop().split('.').shift() 3328 const name = hasAssetExtension ? fileName : element.classList.value.trim().replaceAll(' ', '.') 3329 const isDownloadLink = (tagRootNodeHost && tagRootNodeHost.hasAttribute('download')) || hasAssetExtension; 3330 const fixedName = position === 'Brand area' ? 'Home link' : undefined; 3331 3332 let linkType = isDownloadLink ? 'downloadLink' : 'Link'; 3333 3334 href && this.handleLinkClick({ 3335 target, 3336 url: href, 3337 innertext, 3338 name: fixedName || name, 3339 type: linkType, 3340 ...(hasAssetExtension && { format: href.split('.').pop() }), 3341 ...(position && { position }) 3342 }) 3343 } 3344 3345 const hasSkipDefaultEvent = (element) => element.hasAttribute(ATTR_SKIP_DEFAULT_EVENT) 3346 const hasHref = (element) => element ? Boolean(element.href) : false 3347 3348 /** @type Element */ 3349 const closestElementWhichControlsClickEvents = e 3350 .composedPath() 3351 .filter((node) => node instanceof Element) 3352 .find((element) => hasHref(element) || hasSkipDefaultEvent(element)) 3353 3354 if (closestElementWhichControlsClickEvents && !hasSkipDefaultEvent(closestElementWhichControlsClickEvents)) { 3355 handleTagClick(closestElementWhichControlsClickEvents) 3356 } 3357 }) 3358 } 3359 3360 prepareConsent() { 3361 const cookieName = 'OptanonConsent'; 3362 const consentName = 'groups'; 3363 const cookieString = document.cookie.split(';').find((cookie) => cookie.trim().startsWith(cookieName)) 3364 const cookieValue = cookieString && cookieString.substring(cookieString.indexOf(cookieName) + 1) 3365 const cookieParams = cookieValue && new URLSearchParams(cookieValue) 3366 const consentValue = cookieParams && cookieParams.get(consentName) 3367 const consent = consentValue && consentValue.split(',').reduce((dst, val) => { 3368 const splitedVal = val.split(':'); 3369 if (splitedVal.length > 1) { 3370 const [key, checked] = splitedVal; 3371 return {...dst, [key]: checked === '1'} 3372 } 3373 return dst 3374 }, {}) 3375 3376 return consent 3377 } 3378 3379 prepareComponents() { 3380 const summaryElements = document.querySelectorAll('ehe-summary[id]') 3381 if (summaryElements.length === 0) { 3382 return {} 3383 } 3384 const items = Array.from(summaryElements) 3385 .filter(element => !element.closest('.segment-hidden')) 3386 .map(element => { 3387 return { id : element.id } 3388 }) 3389 if (items.length === 0) { 3390 return {} 3391 } 3392 return { 3393 contentSummary: { 3394 items 3395 } 3396 } 3397 } 3398 3399 getPercentage(currentTime, totalTime) { 3400 const steps = [10, 25, 50, 75, 100] 3401 const percentage = Number(((currentTime / totalTime) * 100).toFixed(0)) 3402 return steps.filter((step) => percentage >= step).pop() || percentage 3403 } 3404 3405 pushVideoEvent(event) { 3406 window.pushOneDataLayerEvent( 3407 event.eventName, 3408 { 3409 content: { 3410 video: { 3411 name: event.videoTitle, 3412 url: event.videoSrc, 3413 provider: 'Dynamic Media Video', 3414 durationTotal: event.duration, 3415 ...(event.campaignId && { campaignId: event.campaignId }), 3416 ...(event.uci && { uci: event.uci }), 3417 ...(event.progress && { progress: event.progress }), 3418 ...(event.error && { error: typeof event.error === 'string' ? event.error : 'Video could not be loaded' }), 3419 }, 3420 }, 3421 }, 3422 event.eventTarget, 3423 ); 3424 } 3425 3426 pushAudioEvent(event) { 3427 window.oneDataLayer.push({ 3428 event: event.eventName, 3429 timestamp: this.timestamp, 3430 content:{ 3431 audio: { 3432 name: event.url.split('/').pop(), 3433 url: event.url, 3434 provider: 'Audio player', 3435 durationTotal: event.durationTotal, 3436 ...event.progress && { progress: event.progress }, 3437 ...event.error && { error: 'Audio could not be loaded' } 3438 } 3439 } 3440 }); 3441 } 3442 3443 audioEventsHandler() { 3444 const audioElements = this.body.querySelectorAll('audio')
3445 audioElements.forEach((audio) => { 3446 let audioData = {} 3447 let progressReached = {} 3448 3449 audio.addEventListener('playing', (e) => { 3450 audioData.durationTotal = e.srcElement.duration 3451 audioData.url = e.target.currentSrc 3452 if (!progressReached[0]) { 3453 audioData.eventName = this.events.audio.start 3454 this.pushAudioEvent(audioData) 3455 progressReached[0] = true 3456 } 3457 this.audioProgressInterval = setInterval(() => { 3458 const currentPercentage = this.getPercentage(audio.currentTime, e.srcElement.duration) 3459 audioData.eventName = this.events.audio.progress 3460 switch (currentPercentage) { 3461 case 10: 3462 if (!progressReached[10]) { 3463 audioData.progress = 10 3464 this.pushAudioEvent(audioData) 3465 progressReached[10] = true 3466 } 3467 break 3468 case 25: 3469 if (!progressReached[25]) { 3470 audioData.progress = 25 3471 this.pushAudioEvent(audioData) 3472 progressReached[25] = true 3473 } 3474 break 3475 case 50: 3476 if (!progressReached[50]) { 3477 audioData.progress = 50 3478 this.pushAudioEvent(audioData) 3479 progressReached[50] = true 3480 } 3481 break 3482 case 75: 3483 if (!progressReached[75]) { 3484 audioData.progress = 75 3485 this.pushAudioEvent(audioData) 3486 progressReached[75] = true 3487 } 3488 } 3489 }, 1000) 3490 }) 3491 3492 audio.addEventListener('pause', () => { 3493 clearInterval(this.audioProgressInterval) 3494 }) 3495 3496 audio.addEventListener('ended', (e) => { 3497 audioData.durationTotal = e.srcElement.duration 3498 audioData.eventName = this.events.audio.complete 3499 audioData.url = e.target.currentSrc 3500 audioData.progress = 100 3501 this.pushAudioEvent(audioData) 3502 clearInterval(this.audioProgressInterval) 3503 audioData = {} 3504 progressReached = {} 3505 }) 3506 3507 audio.addEventListener('error', (e) => { 3508 audioData.eventName = this.events.audio.error 3509 audioData.error = true 3510 this.pushAudioEvent(audioData) 3511 }) 3512 }) 3513 } 3514 3515 dynamicVideoEventsHandler() { 3516 window.oneDataLayer = window.oneDataLayer || [] 3517 3518 window.addEventListener('notfUserEvent', (e) => { 3519 if(e.s7event.trackEvent === 'LOAD' && e.target.dataset.component === 'VideoPlayer') { 3520 const video = e.target.querySelector('video') 3521 const parentElement = video.closest('[data-asset-name]') 3522 const storyCardVideoElement = e.target.closest('[data-video-card]') 3523 3524 const videoTitle = parentElement.dataset.title || 'No video title provided' 3525 const videoSrc = parentElement.dataset.assetPath 3526 3527 const campaignId = parentElement.getAttribute('data-campaign-id'); 3528 const consentBasedTracking = parentElement.getAttribute('data-consent-based-tracking'); 3529 3530 let progressReached = {} 3531 3532 let eventData = { 3533 eventTarget: video, 3534 eventName: '', 3535 videoTitle: videoTitle, 3536 videoSrc: videoSrc, 3537 ...(campaignId && { campaignId }), 3538 ...(consentBasedTracking && { uci: this.getUser()?.uci }), 3539 } 3540 3541 video.addEventListener('loadedmetadata', () => { 3542 if (storyCardVideoElement && !storyCardVideoElement.hasAttribute('duration')) { 3543 storyCardVideoElement.setAttribute('duration', video.duration) 3544 } 3545 }) 3546 3547 video.addEventListener('playing', (e) => { 3548 eventData.duration = e.srcElement.duration 3549 if (!progressReached[0]) { 3550 eventData.eventName = this.events.video.start 3551 this.pushVideoEvent(eventData) 3552 progressReached[0] = true 3553 } 3554 this.videoProgressInterval = setInterval(() => { 3555 const currentPercentage = this.getPercentage(video.currentTime, e.srcElement.duration) 3556 eventData.eventName = this.events.video.progress 3557 switch (currentPercentage) { 3558 case 10: 3559 if (!progressReached[10]) { 3560 eventData.progress = 10 3561 this.pushVideoEvent(eventData) 3562 progressReached[10] = true 3563 } 3564 break 3565 case 25: 3566 if (!progressReached[25]) { 3567 eventData.progress = 25 3568 this.pushVideoEvent(eventData) 3569 progressReached[25] = true 3570 } 3571 break 3572 case 50: 3573 if (!progressReached[50]) { 3574 eventData.progress = 50 3575 this.pushVideoEvent(eventData) 3576 progressReached[50] = true 3577 } 3578 break 3579 case 75: 3580 if (!progressReached[75]) { 3581 eventData.progress = 75 3582 this.pushVideoEvent(eventData) 3583 progressReached[75] = true 3584 } 3585 } 3586 }, 1000) 3587 }) 3588 3589 video.addEventListener('play', () => { 3590 if (storyCardVideoElement) { 3591 storyCardVideoElement.dispatchEvent(new Event('videoClick')) 3592 } 3593 }) 3594 3595 video.addEventListener('pause', () => { 3596 clearInterval(this.videoProgressInterval) 3597 }) 3598 3599 video.addEventListener('ended', () => { 3600 eventData.eventName = this.events.video.complete 3601 eventData.progress = 100 3602 3603 this.pushVideoEvent(eventData) 3604 clearInterval(this.videoProgressInterval) 3605 3606 const resetState = () => { 3607 return { 3608 eventTarget: video, 3609 eventName: '', 3610 videoTitle: videoTitle, 3611 videoSrc: videoSrc, 3612 } 3613 } 3614 eventData = resetState() 3615 progressReached = {} 3616 }) 3617 3618 video.addEventListener('error', () => { 3619 const mediaError = video.error 3620 const errorMessageParts = ['Video could not be loaded'] 3621 3622 if (mediaError) { 3623 const errorCodes = { 3624 1: 'MEDIA_ERR_ABORTED', 3625 2: 'MEDIA_ERR_NETWORK', 3626 3: 'MEDIA_ERR_DECODE', 3627 4: 'MEDIA_ERR_SRC_NOT_SUPPORTED' 3628 } 3629 const codeName = errorCodes[mediaError.code] || 'UNKNOWN_ERROR' 3630 errorMessageParts.push(`${codeName} (code ${mediaError.code})`) 3631 if (mediaError.message) { 3632 errorMessageParts.push(mediaError.message) 3633 } 3634 } 3635 3636 const source = video.currentSrc || video.src || videoSrc 3637 if (source) { 3638 errorMessageParts.push(`source: ${source}`) 3639 } 3640 3641 eventData.eventName = this.events.video.error 3642 eventData.error = errorMessageParts.join(' | ') 3643 this.pushVideoEvent(eventData) 3644 clearInterval(this.videoProgressInterval) 3645 }) 3646 } 3647 }, false) 3648 3649 const S7_LOAD_TIMEOUT_MS = 5000 3650 const loadedContainers = new Set() 3651 3652 window.addEventListener('notfUserEvent', (e) => { 3653 if (!e || !e.s7event || !e.target || !e.target.dataset) { 3654 return 3655 } 3656 if (e.s7event.trackEvent === 'LOAD' && e.target.dataset.component === 'VideoPlayer') { 3657 const container = 3658 e.target.closest('.s7dm-dynamic-media') || 3659 e.target.closest('[data-component="video-player"]') 3660 if (container) { 3661 loadedContainers.add(container) 3662 } 3663 } 3664 }, false) 3665 3666 const reportUnloadedContainer = (container) => { 3667 if (loadedContainers.has(container)) { 3668 return 3669 } 3670 3671 // EHE-13959: skip videos hidden from the current visitor - e.g. protected content 3672 // that leaks into the public/anonymous DOM inside a .segment-hidden block. The
3673 // Scene7 viewer never initializes these, so reporting them produced phantom 3674 // videoError events (largely bot/SEA traffic landing on semi-open pages). 3675 if (container.closest('.segment-hidden, [hidden], [aria-hidden="true"]')) { 3676 return 3677 } 3678 3679 const videoSrc = container.dataset.assetPath || container.dataset.videoPath || '' 3680 const videoTitle = container.dataset.assetName || container.dataset.title || 'No video title provided' 3681 const viewerPath = container.dataset.viewerPath || container.dataset.viewerScript || 'https://s7g10.scene7.com/s7viewers/html5/js/VideoViewer.js' 3682 3683 const errorMessageParts = [] 3684 if (!window.s7viewers) { 3685 errorMessageParts.push('S7_SDK_LOAD_FAILURE: Scene7 viewer SDK failed to load') 3686 errorMessageParts.push(`source: ${viewerPath}`) 3687 } else { 3688 errorMessageParts.push('S7_INIT_FAILURE: Scene7 viewer failed to initialize') 3689 errorMessageParts.push(`video: ${videoTitle}`) 3690 } 3691 3692 this.pushVideoEvent({ 3693 eventTarget: container, 3694 eventName: this.events.video.error, 3695 videoTitle: videoTitle, 3696 videoSrc: videoSrc, 3697 error: errorMessageParts.join(' | '), 3698 }) 3699 } 3700 3701 // EHE-13959: only report a load/init failure for videos the user could actually see. 3702 // Videos below the fold that are never scrolled into view (pages can embed many) must 3703 // not be counted as errors - reporting every unloaded container produced phantom 3704 // videoError events that did not reflect a real user-facing playback problem. 3705 const watchVideoContainers = () => { 3706 const allVideoContainers = [ 3707 ...this.body.querySelectorAll('.s7dm-dynamic-media[data-viewer-type="VideoViewer"]'), 3708 ...this.body.querySelectorAll('[data-component="video-player"]'), 3709 ] 3710 3711 if (typeof IntersectionObserver === 'undefined') { 3712 // Fallback where IntersectionObserver is unavailable: keep the previous 3713 // page-load timeout so genuine failures are still tracked. 3714 setTimeout(() => allVideoContainers.forEach(reportUnloadedContainer), S7_LOAD_TIMEOUT_MS) 3715 return 3716 } 3717 3718 const scheduledContainers = new Set() 3719 const observer = new IntersectionObserver((entries) => { 3720 entries.forEach((entry) => { 3721 if (!entry.isIntersecting || scheduledContainers.has(entry.target)) { 3722 return 3723 } 3724 const container = entry.target 3725 scheduledContainers.add(container) 3726 observer.unobserve(container) 3727 // Give the viewer time to load after it becomes visible, then report 3728 // only if it still never loaded. 3729 setTimeout(() => reportUnloadedContainer(container), S7_LOAD_TIMEOUT_MS) 3730 }) 3731 3732 // Once every container has been scheduled there is nothing left to watch. 3733 if (scheduledContainers.size === allVideoContainers.length) { 3734 observer.disconnect() 3735 } 3736 }) 3737 3738 allVideoContainers.forEach((container) => observer.observe(container)) 3739 } 3740 3741 if (document.readyState === 'complete') { 3742 watchVideoContainers() 3743 } else { 3744 window.addEventListener('load', watchVideoContainers, { once: true }) 3745 } 3746 } 3747 3748 handleSiteSearch({ term, results, filter, searchComponent }) { 3749 const json = { 3750 event: this.events && this.events.onsiteSearch, 3751 timestamp: this.timestamp, 3752 content: { 3753 onsiteSearch: { 3754 term, 3755 results, 3756 filter, 3757 searchComponent 3758 } 3759 } 3760 } 3761 3762 this.addToMainDataObject(json) 3763 } 3764 3765 handleLinkClick({ target, url, innertext, name, type, position, format }) { 3766 const { hostname: domain, pathname: path } = url && new URL(url, window.location.origin) 3767 3768 const defaultLinkClickData = { 3769 link: { 3770 url, 3771 domain, 3772 path, 3773 innertext, 3774 name, 3775 type, 3776 ...(position && { position }), 3777 }, 3778 } 3779 3780 const downloadLinkClickData = { name, url, format } 3781 3782 const isDownloadLink = type === "downloadLink" 3783 3784 // TODO for links that live in the light DOM this duplicates the ODL Launch external script, which 3785 // scrapes the DOM after load and tracks the download itself - the click is then counted twice. 3786 // Do not simply drop the branch though: Launch cannot see links inside a shadow root, so EWC 3787 // downloads would stop being tracked. Scope a fix to light-DOM links only, and for those push 3788 // neither downloadLinkClick nor defaultLinkClick (pushing a link click for a download is an 3789 // explicit spec violation). 3790 window.pushOneDataLayerEvent( 3791 isDownloadLink ? this.events.downloadLinkClick : this.events.defaultLinkClick, 3792 isDownloadLink ? downloadLinkClickData : defaultLinkClickData, 3793 target, 3794 ) 3795 } 3796 3797 handleFormEvents({ event, name, type, step, error }) { 3798 const json = { 3799 event, 3800 timestamp: this.timestamp, 3801 content: { 3802 form: { 3803 name, 3804 type, 3805 step, 3806 error 3807 } 3808 } 3809 } 3810 3811 clearSourceObject(json.content.form) 3812 this.addToMainDataObject(json) 3813 } 3814 3815 getPageData() { 3816 if (!this.oneDataLayerDataJson?.page) return {} 3817 3818 const { previousPage, ...pageData } = this.oneDataLayerDataJson.page 3819 3820 if (previousPage) { 3821 pageData.previousPage = previousPage 3822 } else { 3823 pageData.previousPage = this.previousPageData ? JSON.parse(this.previousPageData) : {} 3824 } 3825 3826 if (document.referrer) { 3827 pageData.referrer = document.referrer; 3828 } 3829 3830 return pageData 3831 } 3832 3833 handlePageScroll() { 3834 const scrollTop = window.innerHeight + window.scrollY 3835 const height = document.body.scrollHeight 3836 const breakpoints = [25, 50, 75, 90, 100] 3837
3838 breakpoints.forEach((breakpoint) => { 3839 if (scrollTop >= height * (breakpoint / 100) && !this.scrollPositionReached[breakpoint.toString()]) { 3840 const pageData = this.getPageData() 3841 3842 delete pageData.currency 3843 3844 this.addToMainDataObject({ 3845 event: this.events && this.events.scroll, 3846 timestamp: this.timestamp, 3847 page: { 3848 ...pageData, 3849 scrolled: breakpoint 3850 } 3851 }) 3852 this.scrollPositionReached[breakpoint.toString()] = true 3853 } 3854 }) 3855 } 3856 3857 handlePageLoad() { 3858 this.parsedOneDataLayerJSON = this.oneDataLayerDataJson 3859 3860 const consent = this.prepareConsent() 3861 3862 const components = this.prepareComponents() 3863 3864 this.parsedOneDataLayerJSON.components = { 3865 ...this.parsedOneDataLayerJSON.components, 3866 ...components 3867 } 3868 3869 this.parsedOneDataLayerJSON.page.previousPage = this.previousPageData 3870 ? JSON.parse(this.previousPageData) 3871 : {}; 3872 3873 this.setPagePath(this.parsedOneDataLayerJSON) 3874 this.setPageUrl(this.parsedOneDataLayerJSON) 3875 this.setNewPageData(this.parsedOneDataLayerJSON, OneDataLayer.CURRENT_PAGE_DATA) 3876 this.setUserData(this.parsedOneDataLayerJSON) 3877 3878 if (consent) { 3879 this.parsedOneDataLayerJSON.user.consent = consent 3880 } 3881 3882 if (document.referrer) { 3883 this.parsedOneDataLayerJSON.page.referrer = document.referrer; 3884 } 3885 3886 if (window.EHEBookmarkManager) { 3887 const currentUrl = `${window.location.origin}${window.location.pathname}`; 3888 const isCurrentPageBookmarked = window.EHEBookmarkManager.isBookmarked(currentUrl); 3889 3890 if (isCurrentPageBookmarked) { 3891 const allBookmarks = window.EHEBookmarkManager.getBookmarks(); 3892 const currentPageBookmark = allBookmarks.find(bookmark => bookmark.url === currentUrl); 3893 3894 if (currentPageBookmark) { 3895 const bookmarkObj = { 3896 bookmark: { 3897 bookmarked: true, 3898 items: [{ 3899 id: currentPageBookmark.id || "", 3900 title: currentPageBookmark.title || "" 3901 }] 3902 } 3903 } 3904 3905 this.parsedOneDataLayerJSON.components = { 3906 ...this.parsedOneDataLayerJSON.components, 3907 ...bookmarkObj 3908 } 3909 } 3910 } else { 3911 const bookmarkObj = { bookmark: { bookmarked: false } } 3912 this.parsedOneDataLayerJSON.components = { 3913 ...this.parsedOneDataLayerJSON.components, 3914 ...bookmarkObj 3915 } 3916 } 3917 } 3918 3919 this.addToMainDataObject(this.parsedOneDataLayerJSON) 3920 } 3921 3922 setNewPageData({ page }, destination = OneDataLayer.PREVIOUS_PAGE_DATA) { 3923 const { url, path, pageTitle, pageName, mainCategory, categoryTree } = page 3924 const newPageData = { url, path, pageTitle, pageName, mainCategory, categoryTree } 3925 3926 this.setSessionStorageData( 3927 destination, 3928 JSON.stringify(newPageData) 3929 ) 3930 } 3931 3932 getUser() { 3933 const profileItem = (name) => this.contextHubProfile.getItem(name) || '' 3934 const userIpMeta = document.querySelector('meta[name="user_ip"]') 3935 const userIp = userIpMeta ? userIpMeta.content : '' 3936 const userAccount = { intEm: this.isInternalEmail(profileItem('email')) } 3937 3938 const user = { 3939 isLoggedIn: !!profileItem('uci') || !!profileItem('email'), 3940 uci: profileItem('uci'), 3941 mdm: profileItem('mdm'), 3942 mdmId: profileItem('mdmId'), 3943 sId: profileItem('salesforceId'), 3944 profession: profileItem('g_profession'), 3945 position: profileItem('position'), 3946 specialty: profileItem('g_specialty'), 3947 center: profileItem('center'), 3948 account: userAccount 3949 } 3950 3951 if (userIp) {
3952 user.account.ipAddress = userIp 3953 } 3954 3955 return user; 3956 } 3957 3958 isInternalEmail(email) { 3959 return email.endsWith('@roche.com') || email.endsWith('.roche.com') 3960 } 3961 3962 setPageUrl(oneDataLayerDataJson) { 3963 oneDataLayerDataJson.page.url = window.location.href; 3964 } 3965 3966 setPagePath(oneDataLayerDataJson) { 3967 oneDataLayerDataJson.page.path = window.location.pathname; 3968 } 3969 3970 setUserData(oneDataLayerDataJson) { 3971 oneDataLayerDataJson.user = this.getUser() 3972 } 3973 3974 addToMainDataObject(json) { 3975 json.timestamp = json.timestamp || this.timestamp 3976 this.mainDataObject.push(json) 3977 } 3978 3979 setSessionStorageData(name, value) { 3980 sessionStorage.setItem(name, value) 3981 } 3982 3983 get mainDataObject() { 3984 // @ts-ignore 3985 return oneDataLayer 3986 } 3987 3988 get previousPageData() { 3989 return sessionStorage.getItem(OneDataLayer.PREVIOUS_PAGE_DATA) 3990 } 3991 3992 get currentPageData() { 3993 return sessionStorage.getItem(OneDataLayer.CURRENT_PAGE_DATA) 3994 } 3995 3996 get timestamp() { 3997 return getTimestamp() 3998 } 3999 4000 get oneDataLayerDataJson() { 4001 const oneDataLayerData = this.oneDataLayerData 4002 try { 4003 return JSON.parse(oneDataLayerData) 4004 } catch (e) { 4005 return {} 4006 } 4007 } 4008 } 4009 4010 OneDataLayer.PREVIOUS_PAGE_DATA = 'previousPageData' 4011 OneDataLayer.CURRENT_PAGE_DATA = 'currentPageData' 4012 4013 const init = () => { 4014 // @ts-ignore 4015 window.oneDataLayer = window.oneDataLayer || [] 4016 window.pushOneDataLayerEvent = window.pushOneDataLayerEvent || ((type, data, _element) => { 4017 window.oneDataLayer.push({ 4018 event: type, 4019 timestamp: getTimestamp(), 4020 ...data 4021 }); 4022 }); 4023 4024 const body = document.body 4025 new OneDataLayer(body) 4026 } 4027 4028 if (!inEditor) { 4029 init() 4030 } 4031})(); 4032 4033(function () { 4034 'use strict'; 4035 4036 function onDocumentReady() { 4037 const elementsToEmbed = document.querySelectorAll('.embed-fetch-html'); 4038 elementsToEmbed.forEach((element) => { 4039 appendExternalHtml(element); 4040 }); 4041 4042 const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; 4043 const body = document.body; 4044 const observer = new MutationObserver(function (mutations) { 4045 mutations.forEach(function (mutation) { 4046 const nodesArray = Array.prototype.slice.call(mutation.addedNodes); 4047 if (nodesArray.length > 0) { 4048 nodesArray.forEach(function (addedNode) { 4049 if (addedNode.querySelectorAll) { 4050 const fetchHtmlArray = Array.prototype.slice.call(addedNode.querySelectorAll('.embed-fetch-html')); 4051 fetchHtmlArray.forEach(function (element) { 4052 appendExternalHtml(element); 4053 }); 4054 } 4055 }); 4056 } 4057 }); 4058 }); 4059 4060 function appendExternalHtml(element) { 4061 const url = element.dataset.fetchUrl; 4062 if (url) { 4063 fetch(url) 4064 .then(res => res.text()) 4065 .then(data => { 4066 const childElement = document.createElement('div'); 4067 childElement.innerHTML = data; 4068 const text = element.querySelector('.embeded-text'); 4069 text.appendChild(childElement); 4070 }); 4071 } 4072 } 4073 4074 observer.observe(body, { 4075 subtree: true, 4076 childList: true, 4077 characterData: true 4078 }); 4079 } 4080 4081 if (document.readyState !== 'loading') { 4082 onDocumentReady(); 4083 } else { 4084 document.addEventListener('DOMContentLoaded', onDocumentReady); 4085 } 4086})(); 4087 4088;{ 4089 let oneTrustLoaded = false 4090 const OptanonWrapper = () => { 4091 let accept = false 4092 const { OnetrustActiveGroups, OneTrust } = window 4093 4094 if (!oneTrustLoaded) { 4095 window.dispatchEvent(new CustomEvent('OneTrustReady')) 4096 oneTrustLoaded = true 4097 } 4098 4099 if (!OneTrust.IsAlertBoxClosedAndValid()) { 4100 const event = new CustomEvent('onetrustAlertBoxDisplayed') 4101 window.dispatchEvent(event) 4102 } 4103 4104 if ( 4105 typeof OnetrustActiveGroups === 'undefined' || 4106 OnetrustActiveGroups === undefined || 4107 OnetrustActiveGroups.includes('C0004') 4108 ) { 4109 accept = true 4110 } 4111 4112 const event = new CustomEvent('onetrustChanged', { detail: { accept } }) 4113 4114 window.dispatchEvent(event) 4115 } 4116 4117 window.OptanonWrapper = OptanonWrapper 4118} 4119 4120;{ 4121 4122 const setCompliance = (e) => { 4123 const { ppms } = window 4124 if (typeof ppms === 'undefined') { 4125 return 4126 } 4127 4128 if (ppms && ppms.cm && ppms.cm.api) { 4129 const acceptedCookie = e.detail.accept 4130 const status = acceptedCookie ? 1 : -1;
4131 ppms.cm.api( 4132 "setComplianceSettings", 4133 { 4134 consents: { 4135 analytics: { 4136 status: status, 4137 }, 4138 }, 4139 }, 4140 function onSuccess() { 4141 }, 4142 function onError() { 4143 } 4144 ) 4145 } 4146 } 4147 4148 const initCompliance = () => { 4149 const { ppms } = window 4150 if (typeof ppms === 'undefined') { 4151 return 4152 } 4153 4154 if (ppms && ppms.cm && ppms.cm.api) { 4155 ppms.cm.api( 4156 "setInitialComplianceSettings", 4157 { 4158 consents: ["analytics"], 4159 }, 4160 function onSuccess() { 4161 }, 4162 function onError() { 4163 } 4164 ) 4165 } 4166 } 4167 4168 const initPiwikPro = () => { 4169 window.addEventListener('onetrustAlertBoxDisplayed', initCompliance) 4170 window.addEventListener('onetrustChanged', setCompliance) 4171 } 4172 4173 if (document.readyState !== 'loading') { 4174 initPiwikPro() 4175 } else { 4176 document.addEventListener('DOMContentLoaded', initPiwikPro) 4177 } 4178} 4179
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.