1const OtsukaPCM = Object.create({ 2 /** 3 * Cookie consent management platform. 4 */ 5 cmpPlatform: "onetrust", 6 7 /** 8 * Enumeration of service providers. 9 */ 10 providers: { 11 VIMEO: "vimeo", 12 YOUTUBE: "youtube", 13 BUZZSPROUT: "buzzsprout", 14 ORBITA: "orbita", 15 }, 16 17 /** 18 * The module settings contained within drupalSettings. 19 */ 20 settings: drupalSettings.privacy_compliant_media_settings, 21 22 /** 23 * Get the module settings for the supplied provider. 24 * @param {string} provider The provider to get the settings for. 25 * @returns {object|null} The settings for the supplied provider, if found, 26 * otherwise null. 27 */ 28 getProviderSettings: function (provider) { 29 switch (provider) { 30 case this.providers.VIMEO: 31 return this.settings.vimeo; 32 case this.providers.YOUTUBE: 33 return this.settings.youtube; 34 case this.providers.BUZZSPROUT: 35 return this.settings.buzzsprout_podcasts; 36 case this.providers.ORBITA: 37 return this.settings.orbita_live_chat; 38 default: 39 return null; 40 } 41 }, 42 overlayImageClassname: "otsuka-pcm-img-overlay", 43 videoThumbnailClassname: "otsuka-pcm-video-thumbnail", 44 thumbnailOverlayClassname: "otsuka-pcm-img-overlay__transparent", 45 thumbnailOverlayDefaultClassname: "with-default-video-overlay", 46 thumbnailOverlayCustomClassname: "with-custom-video-overlay", 47 buzzsproutPlaceholderDefaultClassname: "with-default-buzzsprout-placeholder", 48 buzzsproutPlaceholderCustomClassname: "with-custom-buzzsprout-placeholder", 49 overlayClassname: "otsuka-pcm-consent-overlay", 50 modalClassname: "otsuka-pcm-consent-modal", 51 52 /** 53 * Handle error messages by printing to console. 54 * @param {string} message The error message text. 55 */ 56 printError: function (message) { 57 console.error(this.formatErrorMessage(message)); 58 }, 59 60 /** 61 * Handle error messages by printing to console. 62 * @param {string} message The error message text. 63 */ 64 throwError: function (message) { 65 throw new Error(this.formatErrorMessage(message)); 66 }, 67 68 /** 69 * Format error messages. 70 * @param {string} message The error message text. 71 * @returns {string} The formatted error message. 72 */ 73 formatErrorMessage: function (message) { 74 return `OtsukaPCM: ${message}`; 75 }, 76 77 /** 78 * Creates a DOM element from the supplied text. 79 * @param {string} text The text to add to the generated Element. 80 * @param {string} tagName The tag name to wrap the supplied text in. 81 * @param {boolean} strip Whether or not to strip the outer Element from the 82 * returned value. 83 * @returns {Element} The generated DOM Element. 84 */ 85 createElementFromText: function (text, tagName = "div", strip = true) { 86 let element = document.createElement(tagName); 87 element.innerHTML = text ?? ""; 88 return strip ? element.firstChild : element; 89 }, 90 91 /** 92 * Wait for the OneTrust object to be found. 93 * @returns {Promise} A Promise that resolves when OneTrust is found, or 94 * rejects after a preset amount of time. 95 */ 96 awaitOneTrust: function () { 97 return new Promise(function (resolve, reject) { 98 if (typeof OneTrust === "object") { 99 resolve(OneTrust); 100 } 101 102 let attempt = 0; 103 const maxAttempts = 50; 104 const interval = setInterval(() => { 105 if (typeof OneTrust === "object") { 106 clearInterval(interval); 107 resolve(OneTrust); 108 } 109 attempt++; 110 if (attempt >= maxAttempts) { 111 clearInterval(interval); 112 reject( 113 OtsukaPCM.formatErrorMessage( 114 "awaitOneTrust: OneTrust not found within allotted time" 115 ) 116 ); 117 } 118 }, 100); 119 }); 120 }, 121 122 /** 123 * Wait for the consent management platform to be found. 124 * @returns {Promise} A Promise that resolves when the CMP is found, or 125 * rejects after a preset amount of time. 126 */ 127 awaitCMP: function () { 128 switch (this.cmpPlatform) { 129 case "onetrust": 130 return this.awaitOneTrust(); 131 default: 132 return Promise.reject(); 133 } 134 }, 135 136 /** 137 * Wait for the Orbita chatbot object to be found and for the chatbot to be 138 * loaded. 139 * @returns {Promise} A Promise that resolves when the chatbot is found, or 140 * rejects after a preset amount of time. 141 */ 142 awaitOrbitaChatbot: function () { 143 return new Promise(function (resolve, reject) { 144 if ( 145 typeof OrbitaChatBotV3 === "object" && 146 Object.hasOwn(OrbitaChatBotV3, "botLoaded") && 147 OrbitaChatBotV3.botLoaded() 148 ) { 149 resolve(OrbitaChatBotV3); 150 } 151 152 let attempt = 0; 153 const maxAttempts = 50; 154 const interval = setInterval(() => { 155 if ( 156 typeof OrbitaChatBotV3 === "object" && 157 Object.hasOwn(OrbitaChatBotV3, "botLoaded") && 158 OrbitaChatBotV3.botLoaded() 159 ) { 160 clearInterval(interval); 161 resolve(OrbitaChatBotV3); 162 } 163 attempt++; 164 if (attempt >= maxAttempts) { 165 clearInterval(interval); 166 this.throwError( 167 "awaitOrbitaChatbot: Orbita Chatbot not found within allotted time" 168 ); 169 reject(); 170 } 171 }, 100); 172 }); 173 }, 174 175 /** 176 * Wait for the Orbita chat button to be found in the DOM. 177 * @returns {Promise} A Promise that resolves when the chat button is found, 178 * or rejects after a preset amount of time. 179 */ 180 awaitOrbitaChatButton: function (selector = 'button[aria-label="Show Chatbot"]') { 181 return new Promise(function (resolve, reject) { 182 const orbitaButton = document.querySelector(selector); 183 if (orbitaButton) { 184 resolve(orbitaButton); 185 } 186 187 let attempt = 0; 188 const maxAttempts = 50; 189 const interval = setInterval(() => { 190 const orbitaButton = document.querySelector(selector); 191 if (orbitaButton) { 192 clearInterval(interval); 193 resolve(orbitaButton); 194 } 195 attempt++; 196 if (attempt >= maxAttempts) { 197 clearInterval(interval); 198 this.throwError( 199 "awaitOrbitaChatButton: Orbita chat button not found within allotted time" 200 ); 201 reject(); 202 } 203 }, 100); 204 }); 205 }, 206 207 /** 208 * OneTrust: Checks if the visitor is consented to the specified cookie 209 * category. 210 * @param {string}
210 category_id The cookie category to check for consent. 211 * @returns {boolean} Whether the visitor is consented to the specified 212 * cookie category. 213 */ 214 isConsentedToCategoryOneTrust: async function (category_id) { 215 // Wait for CMP object. 216 try { 217 await this.awaitOneTrust(); 218 } catch (e) { 219 throw e; 220 } 221 222 return OnetrustActiveGroups?.split(",").includes(category_id); 223 }, 224 225 /** 226 * Checks if the visitor is consented to the specified cookie category. 227 * @param {string} category_id The cookie category to check for consent. 228 * @returns {boolean} Whether the visitor is consented to the specified 229 * cookie category. 230 */ 231 isConsentedToCategory: async function (category_id) { 232 switch (this.cmpPlatform) { 233 case "onetrust": 234 try { 235 const isConsented = await this.isConsentedToCategoryOneTrust( 236 category_id 237 ); 238 return isConsented; 239 } catch (e) { 240 throw e; 241 } 242 default: 243 return false; 244 } 245 }, 246 247 /** 248 * OneTrust: Consents/opts-in the visitor to the specified cookie category. 249 * @param {string} category_id The category to consent/opt-in to. 250 * @returns {boolean} Whether the action succeeded or not. 251 */ 252 consentToCategoryOneTrust: async function (category_id) { 253 // Wait for CMP object. 254 try { 255 await this.awaitOneTrust(); 256 } catch (e) { 257 this.throwError( 258 "consentToCategoryOneTrust: Error consenting to OneTrust category" 259 ); 260 return false; 261 } 262 263 OneTrust.UpdateConsent("Category", `${category_id}:1`); 264 return true; 265 }, 266 267 /** 268 * Consents/opts-in the visitor to the specified cookie category. 269 * @param {string} category_id The category to consent/opt-in to. 270 */ 271 consentToCategory: async function (category_id) { 272 let result = false;
273 switch (this.cmpPlatform) { 274 case "onetrust": 275 result = await this.consentToCategoryOneTrust(category_id); 276 break; 277 } 278 return result; 279 }, 280 281 /** 282 * Loads the supplied script by setting its `src` attribute to its `data-src` 283 * attribute. 284 * @param {Element} script The script Element to operate on. 285 */ 286 insertScript: function (script) { 287 script.src = script.getAttribute("data-src"); 288 }, 289 290 /** 291 * UNUSED. Uses OneTrust's InsertScript function to insert a script tag into 292 * the DOM if a specified cookie category is opted-in to. 293 * @param {object} param0 The set of parameters to pass to the InsertScript 294 * function. 295 */ 296 insertScriptIfConsented: async function ({ 297 url, 298 selector, 299 callback = null, 300 options = null, 301 categoryId, 302 async = false, 303 }) { 304 // Wait for CMP object. 305 try { 306 await this.awaitOneTrust(); 307 } catch (e) { 308 this.throwError("insertScriptIfConsented: Error inserting script"); 309 return false; 310 } 311 312 // @see https://my.onetrust.com/s/article/UUID-d8291f61-aa31-813a-ef16-3f6dec73d643?language=en_US#UUID-d8291f61-aa31-813a-ef16-3f6dec73d643_section-idm231900142186795 313 OneTrust.InsertScript(url, selector, callback, options, categoryId, async); 314 }, 315 316 /** 317 * Returns the cookie consent category ID, specified in the module backend 318 * settings, for the supplied provider. 319 * @param {string} provider The provider to get the consent category for. 320 * @returns {string|null} The category ID for the specified provider, if 321 * found, otherwise null. 322 */ 323 getConsentCategoryForProvider: function (provider) { 324 return this.getProviderSettings(provider)?.cat_id; 325 }, 326 327 /** 328 * Generates a unique DOM Element ID. 329 * @param {string} p A prefix to prepend to the generated ID. 330 * @returns {string} The generated ID. 331 */ 332 genUID: function (p) { 333 var c = 0, 334 i; 335 p = typeof p === "string" ? p : ""; 336 do { 337 i = p + c++; 338 } while (document.getElementById(i) !== null); 339 return i; 340 }, 341 342 /** 343 * Checks if the specified URL contains a query string. 344 * @param {string} url The URL to check. 345 * @returns {boolean} Whether or not the specified URL contains a query 346 * string. 347 */ 348 hasQueryString: function (url) { 349 return url.indexOf("?") != -1; 350 }, 351 352 /** 353 * Creates a script Element in the DOM with the specified ID attribute. 354 * @param {string} src The URL of the script to load. 355 * @param {string} id The ID attribute to add to the generated script 356 * Element. 357 * @returns {Promise} A Promise that resolves if the specified Element ID 358 * already exists, or when the new script Element is loaded by the browser. 359 */ 360 loadScript: function (src, id) { 361 return new Promise(function (resolve, reject) { 362 if (document.getElementById(id)) { 363 resolve(); 364 } else { 365 const script = document.createElement("script"); 366 script.onload = resolve; 367 script.id = id; 368 script.src = src; 369 const firstScriptTag = document.getElementsByTagName("script")[0]; 370 firstScriptTag.parentNode.insertBefore(script, firstScriptTag); 371 } 372 }); 373 }, 374 375 /** 376 * Loads the YouTube Player API for iframe embeds. 377 * @returns {Promise} A Promise that resolves when the YoutTube API is found, 378 * or rejects after a preset amount of time. 379 */ 380 loadYouTubeAPI: function () { 381 function apiExists() { 382 return "undefined" !== typeof YT && YT.loaded; 383 } 384 if (apiExists()) { 385 return; 386 } 387 const promises = []; 388 promises.push( 389 this.loadScript("https://www.youtube.com/iframe_api", "youtube-api") 390 ); 391 promises.push( 392 new Promise(function (resolve, reject) { 393 // Set an interval to keep checking. 394 const intervalId = setInterval(function () { 395 if (apiExists()) { 396 // API now exists, clear interval and resolve. 397 clearInterval(intervalId); 398 resolve(); 399 } 400 }, 100); 401 }) 402 ); 403 return Promise.all(promises); 404 }, 405 406 /** 407 * Loads the Vimeo Player SDK. 408 * @returns {Promise} A Promise that resolves when the Vimeo SDK is found, or 409 * rejects after a preset amount of time. 410 */ 411 loadVimeoAPI: function () { 412 function apiExists() { 413 return "undefined" !== typeof Vimeo; 414 } 415 if (apiExists()) { 416 return; 417 } 418 const promises = []; 419 promises.push( 420 this.loadScript("https://player.vimeo.com/api/player.js", "vimeo-api") 421 ); 422 promises.push( 423 new Promise(function (resolve, reject) { 424 // Set an interval to keep checking. 425 const intervalId = setInterval(function () { 426 if (apiExists()) { 427 // API now exists, clear interval and resolve. 428 clearInterval(intervalId); 429 resolve(); 430 } 431 }, 100); 432 }) 433 ); 434 return Promise.all(promises); 435 }, 436 437 /** 438 * Loads a player API for the specified provider. 439 * @param {string} provider The provider to load an API for. 440 * @returns {Promise} A Promise that resolves when the specified provider's 441 * API is found, or rejects after a preset amount of time. 442 */ 443 loadPlayerAPI: function (provider) { 444 switch (provider) { 445 case this.providers.VIMEO: 446 return this.loadVimeoAPI(); 447 case this.providers.YOUTUBE: 448 return this.loadYouTubeAPI(); 449 default: 450 return Promise.resolve(); 451 } 452 }, 453 454 /** 455 * Begins playback of the specified iframe video player. 456 * 457 * The iframe must not already be loaded by the browser and must have a 458 * `data-src` attribute. Also checks the module settings for the video 459 * provider of the iframe and alters the video src URL for "privacy-enhanced 460 * mode" when applicable. 461 * @param {Element} iframe The iframe element to load and begin playback. 462 * @returns {void} 463 */ 464 playIframeVideo: function (iframe) { 465 const dataSrc = iframe.getAttribute("data-src"); 466 const { provider, video_id } = this.isManagedSrc(dataSrc); 467 const matcher = /(\/media\/oembed\?url=)([^&]+)(.+)/; 468 const isOembed = dataSrc.match(matcher); 469 470 iframe.style.display = "block"; 471 472 const dependent_cat_id = 473 this.getProviderSettings(provider).dependent_cat_id; 474 const privacy_enhanced = 475 dependent_cat_id && !this.isConsentedToCategory(dependent_cat_id); 476 477 let iframeSrc = dataSrc; 478 479 // oEmbed 480 if (isOembed) { 481 if (privacy_enhanced) { 482 iframeSrc += 483 (this.hasQueryString(iframeSrc) ? "&" : "?") + "privacy_enhanced=1"; 484 } 485 iframe.src = iframeSrc; 486 // @todo Fix for Safari autoplay. 487 return; 488 } 489 490 // iframe embed 491 switch (provider) { 492 case this.providers.VIMEO: 493 if (privacy_enhanced) { 494 iframeSrc += (this.hasQueryString(iframeSrc) ? "&" : "?") + "dnt=1"; 495 } 496 iframe.src = iframeSrc; 497 const player = new Vimeo.Player(iframe.id); 498 player 499 .play() 500 .then(function () {}) 501 .catch(function (e) {
502 switch (e.name) { 503 case "NotAllowedError": 504 // Reload player so initial frame is shown, otherwise it can appear blank. 505 player.unload().then(function () { 506 loadVideo(video_id); 507 }); 508 break; 509 } 510 }); 511 break; 512 case this.providers.YOUTUBE: 513 if (privacy_enhanced) { 514 iframeSrc = iframeSrc.replace( 515 /(youtu\.be|youtube\.com)\//, 516 "youtube-nocookie.com/" 517 ); 518 } 519 iframeSrc += 520 (this.hasQueryString(dataSrc) ? "&" : "?") + "enablejsapi=1"; 521 iframe.src = iframeSrc; 522 new YT.Player(iframe.id, { 523 events: { 524 onReady: function (event) { 525 event.target.playVideo(); 526 }, 527 }, 528 }); 529 break; 530 } 531 532 // Fix for Safari. 533 iframe.querySelector(`video`)?.play(); 534 }, 535 536 /** 537 * Checks if the supplied URL is for a media type or script that the module 538 * manages consent for. 539 * @param {string} src The URL of the video or script to check. 540 * @returns {object|false} If the supplied URL is consent-managed by the 541 * module, returns an object with information about the supplied URL, 542 * otherwise returns false. 543 */ 544 isManagedSrc: function (src) { 545 let matcher = 546 /(youtu\.be|youtube\.com|youtube-nocookie\.com)\/(embed\/|watch\?v=)?([^?&]+)/; 547 let match = src?.match(matcher); 548 if (match && match[3]) { 549 return { 550 domain: match[1], 551 video_id: match[3], 552 provider: this.getProviderByDomain(match[1]), 553 }; 554 } 555 matcher = /(vimeo\.com)\/(video\/)?([0-9]+)/; 556 match = src?.match(matcher); 557 if (match && match[3]) { 558 return { 559 domain: match[1], 560 video_id: match[3], 561 provider: this.getProviderByDomain(match[1]), 562 }; 563 } 564 matcher = /(buzzsprout\.com)\/([0-9]+)\.js/; 565 match = src?.match(matcher); 566 if (match && match[2]) { 567 return { 568 domain: match[1], 569 subscription_id: match[2], 570 provider: this.getProviderByDomain(match[1]), 571 }; 572 } 573 matcher = /(orbita\.(cloud|com))(.*)\/chatbot\//; 574 match = src?.match(matcher); 575 if (match && match[2]) { 576 return { 577 domain: match[1], 578 provider: this.getProviderByDomain(match[1]), 579 }; 580 } 581 return false; 582 }, 583 584 /** 585 * Checks if the supplied iframe Element is for a media type that the module 586 * manages consent for. The iframe must have a `data-src` attribute. 587 * @param {Element} iframe The iframe Element to check. 588 * @returns {object|false} The result of calling `isManagedSrc()` on the 589 * iframe's `data-src` attribute, otherwise false. 590 */ 591 isManagedIframe: function (iframe) { 592 return iframe && this.isManagedSrc(iframe.getAttribute("data-src")); 593 }, 594 595 /** 596 * Returns the provider name for the supplied iframe Element, as returned by 597 * calling `isManagedIframe()`. 598 * @param {Element} iframe The iframe to get the provider for. 599 * @returns {string|false} The provider name, if found, otherwise false. 600 */ 601 getProviderByIframe: function (iframe) { 602 const { provider } = this.isManagedIframe(iframe); 603 return provider; 604 }, 605 606 /** 607 * Returns the provider name for the supplied domain name. 608 * @param {string} domain The domain name to check. 609 * @returns {string|false} The provider name for the supplied domain name, if 610 * found, otherwise false. 611 */ 612 getProviderByDomain: function (domain) { 613 switch (domain) { 614 case "vimeo.com": 615 return this.providers.VIMEO; 616 case "youtube.com": 617 case "youtu.be": 618 case "youtube-nocookie.com": 619 return this.providers.YOUTUBE; 620 case "buzzsprout.com": 621 return this.providers.BUZZSPROUT; 622 case "orbita.com": 623 case "orbita.cloud": 624 return this.providers.ORBITA; 625 } 626 return false; 627 }, 628 629 /** 630 * Checks whether the specified provider is enabled for consent-management in 631 * the module settings. 632 * @param {string} provider The provider name to check. 633 * @returns {boolean} Returns true if the specified provider is found and 634 * enabled, otherwise false. 635 */ 636 isProviderEnabled: function (provider) { 637 return this.getProviderSettings(provider)?.enabled || false; 638 }, 639 640 /** 641 * Gets the consent overlay Element, if present, from inside the specified 642 * target Element. 643 * @param {Element} target The DOM Element to search inside of. 644 * @returns {Element|null} The consent overlay DOM Element, if found, 645 * otherwise null. 646 */ 647 getConsentOverlay: function (target) { 648 const id = target.getAttribute("id"); 649 return id 650 ? document.querySelector(`#${id} > .${this.overlayClassname}`) 651 : null; 652 }, 653 654 /** 655 * Removes the consent overlay Element, if present, from inside the specified 656 * target Element. 657 * @param {Element} target The DOM Element to search inside of. 658 */ 659 removeConsentOverlay: function (target) { 660 this.getConsentOverlay(target)?.remove(); 661 }, 662 663 /** 664 * Observe the target element width and add class at threshold. 665 * 666 * Used with CSS to change the display of the consent modal. 667 */ 668 consentModalResizeObserver: new ResizeObserver((entries) => {
669 for (const entry of entries) { 670 if (entry.borderBoxSize[0].inlineSize > 600) { 671 // Ensure our effect occurs after ResizeObserver completes. 672 setTimeout(() => entry.target.classList.add("wide"), 0); 673 } else { 674 // Ensure our effect occurs after ResizeObserver completes. 675 setTimeout(() => entry.target.classList.remove("wide"), 0); 676 } 677 } 678 }), 679 680 /** 681 * Observe the target element width and add class at threshold. 682 * 683 * Used with CSS to change which overlay (play button) image is used. 684 */ 685 videoOverlayResizeObserver: new ResizeObserver((entries) => { 686 for (const entry of entries) { 687 if (entry.borderBoxSize[0].inlineSize > 600) { 688 entry.target.classList.add("wide"); 689 } else { 690 entry.target.classList.remove("wide"); 691 } 692 } 693 }), 694 695 /** 696 * Creates a consent modal using the specified options. 697 * @param {object} param0 The options for the generated consent modal. 698 * @returns {Element} The generated consent modal. 699 */ 700 getConsentModal: function ({ 701 target, 702 provider, 703 consentCallback = null, 704 cancelCallback = null, 705 withCloseButton = true, 706 }) { 707 const consent_category = this.getConsentCategoryForProvider(provider); 708 const providerSettings = this.getProviderSettings(provider); 709 const dialogTitleId = this.genUID("otsuka-pcm-dialogTitle-id-"); 710 const dialogDescId = this.genUID("otsuka-pcm-dialogDesc-id-"); 711 712 const modal = document.createElement("div"); 713 modal.setAttribute("role", "dialog"); 714 modal.setAttribute("aria-labelledby", dialogTitleId); 715 modal.setAttribute("aria-describedby", dialogDescId); 716 modal.classList.add(this.modalClassname); 717 modal.classList.add(provider); 718 719 modal.addEventListener("click", function (e) { 720 e.stopPropagation(); 721 }); 722 723 let closeButton; 724 725 if (withCloseButton) { 726 closeButton = document.createElement("div"); 727 closeButton.className = "otsuka-pcm-consent-close"; 728 closeButton.innerHTML = "✕"; 729 closeButton.setAttribute("role", "button"); 730 closeButton.setAttribute("aria-label", "Close"); 731 closeButton.setAttribute("tabindex", "0"); 732 closeButton.addEventListener("click", () => { 733 this.removeConsentOverlay(target); 734 cancelCallback && cancelCallback(); 735 }); 736 // Listen for keyboard events. 737 closeButton.addEventListener("keydown", (e) => { 738 if ("Enter" === e.key) { 739 e.target.click(); 740 } 741 }); 742 } 743 744 const dialogTitle = document.createElement("h2"); 745 dialogTitle.setAttribute("id", dialogTitleId); 746 dialogTitle.setAttribute("hidden", "hidden"); 747 dialogTitle.setAttribute("aria-hidden", true); 748 dialogTitle.innerHTML = "Consent request"; 749 750 const content = document.createElement("div"); 751 content.setAttribute("id", dialogDescId); 752 content.className = "otsuka-pcm-consent-content"; 753 content.innerHTML = providerSettings.modal.description; 754 755 // Add an "I agree" button, which when clicked removes the overlay 756 // and executes the consentCallback. 757 const consentButton = document.createElement("button"); 758 consentButton.className = "otsuka-pcm-consent-button"; 759 consentButton.innerHTML = providerSettings.modal.consent_label; 760 consentButton.addEventListener("click", async () => { 761 consentButton.setAttribute("disabled", ""); 762 //Enables consent for the current consent category 763 if (await this.consentToCategory(consent_category)) { 764 // Remove the overlay and load the video player. 765 modal.parentNode.remove(); 766 consentCallback && consentCallback(); 767 } 768 consentButton.removeAttribute("disabled"); 769 }); 770 771 let moreLink; 772 const moreLinkLabel = providerSettings.modal.morelink_label; 773 const moreLinkUrl = providerSettings.modal.morelink_url; 774 775 if (moreLinkLabel && moreLinkUrl) { 776 moreLink = document.createElement("a"); 777 moreLink.className = "otsuka-pcm-consent-link"; 778 moreLink.href = moreLinkUrl; 779 moreLink.innerHTML = moreLinkLabel; 780 moreLink.setAttribute("role", "button"); 781 moreLink.setAttribute("aria-label", moreLinkLabel); 782 } 783 784 // Add the elements to the modal. 785 if (closeButton) { 786 modal.append(closeButton); 787 } 788 modal.append(dialogTitle, content, consentButton); 789 if (moreLink) { 790 modal.append(moreLink); 791 } 792 793 // Observe the modal for resizing. 794 this.consentModalResizeObserver.observe(modal); 795 796 return modal; 797 }, 798 799 /** 800 * Creates an error modal using the specified options. 801 * @param {object} param0 The options for the generated error modal. 802 * @returns {Element} The generated error modal. 803 */ 804 getErrorModal: function ({ 805 target, 806 cancelCallback = null, 807 withCloseButton = true, 808 }) { 809 const dialogTitleId = this.genUID("otsuka-pcm-dialogTitle-id-"); 810 const dialogDescId = this.genUID("otsuka-pcm-dialogDesc-id-"); 811 812 const modal = document.createElement("div"); 813 modal.setAttribute("role", "dialog"); 814 modal.setAttribute("aria-labelledby", dialogTitleId);
815 modal.setAttribute("aria-describedby", dialogDescId); 816 modal.classList.add(this.modalClassname); 817 818 modal.addEventListener("click", function (e) { 819 e.stopPropagation(); 820 }); 821 822 let closeButton; 823 824 if (withCloseButton) { 825 closeButton = document.createElement("div"); 826 closeButton.className = "otsuka-pcm-consent-close"; 827 closeButton.innerHTML = "✕"; 828 closeButton.setAttribute("role", "button"); 829 closeButton.setAttribute("aria-label", "Close"); 830 closeButton.addEventListener("click", () => { 831 this.removeConsentOverlay(target); 832 cancelCallback && cancelCallback(); 833 }); 834 } 835 836 const dialogTitle = document.createElement("h2"); 837 dialogTitle.setAttribute("id", dialogTitleId); 838 dialogTitle.setAttribute("hidden", "hidden"); 839 dialogTitle.setAttribute("aria-hidden", true); 840 dialogTitle.innerHTML = "Error message"; 841 842 const content = document.createElement("div"); 843 content.setAttribute("id", dialogDescId); 844 content.className = "otsuka-pcm-error-content"; 845 content.innerHTML = this.settings.general.error_cmp_not_found; 846 847 // Add the elements to the modal. 848 if (closeButton) { 849 modal.append(closeButton); 850 } 851 modal.append(dialogTitle, content); 852 853 // Observe the modal for resizing. 854 this.consentModalResizeObserver.observe(modal); 855 856 return modal; 857 }, 858 859 /** 860 * Creates a consent overlay, containing a consent modal, using the specified 861 * options. 862 * @param {object} param0 The options for the generated consent modal. 863 * @returns {Element} The generated consent overlay Element. 864 */ 865 createConsentOverlay: function ({ 866 target, 867 provider, 868 consentCallback = null, 869 cancelCallback = null, 870 withCloseButton = true, 871 }) { 872 const overlay = document.createElement("div"); 873 overlay.classList.add(this.overlayClassname); 874 overlay.addEventListener("click", function (e) { 875 e.stopPropagation(); 876 }); 877 878 const providerSettings = this.getProviderSettings(provider) 879 if (providerSettings.modal.description !== '') { 880 const modal = this.getConsentModal({ 881 target: target, 882 provider: provider, 883 consentCallback: consentCallback, 884 cancelCallback: cancelCallback, 885 withCloseButton: withCloseButton, 886 }); 887 overlay.append(modal); 888 } 889 return overlay; 890 }, 891 892 /** 893 * Creates an error overlay, containing an error modal, using the specified 894 * options. 895 * @param {object} param0 The options for the generated error modal. 896 * @returns {Element} The generated error overlay Element. 897 */ 898 createErrorOverlay: function ({ 899 target, 900 cancelCallback = null, 901 withCloseButton = true, 902 }) { 903 const overlay = document.createElement("div"); 904 overlay.classList.add(this.overlayClassname); 905 overlay.addEventListener("click", function (e) { 906 e.stopPropagation(); 907 }); 908 909 const modal = this.getErrorModal({ 910 target: target, 911 cancelCallback: cancelCallback, 912 withCloseButton: withCloseButton, 913 }); 914 overlay.append(modal); 915 return overlay; 916 }, 917 918 /** 919 * Fetches the thumbnail image URL for the supplied Vimeo video ID. 920 * @param {string|number} video_id The ID of the video to get the thumbnail 921 * for. 922 * @returns {string} The URL of the thumbnail image, if found, otherwise 923 * false. 924 */ 925 getVimeoThumbnailUrl: async function (video_id, size={width: 295, height: 166}) { 926 try { 927 const url = `https://vimeo.com/api/oembed.json?url=https://vimeo.com/${video_id}`; 928 const response = await fetch(url); 929 const data = await response.json(); 930 const {width, height} = size; 931 const updatedUrl = data.thumbnail_url.replace(/d_(\d+)x(\d+)/, `d_${width}x${height}`); 932 return updatedUrl; 933 } catch (error) { 934 this.throwError( 935 "getVimeoThumbnailUrl: Error fetching Vimeo thumbnail: ", 936 error 937 ); 938 } 939 return false; 940 }, 941 942 /** 943 * Returns the thumbnail image URL for the supplied YouTube video ID. 944 * @param {string} video_id The ID of the video to get the thumbnail for. 945 * @returns {string} The URL of the thumbnail image. 946 */
947 getYoutubeThumbnailUrl: function (video_id) { 948 return `https://img.youtube.com/vi/${video_id}/maxresdefault.jpg`; 949 }, 950 951 /** 952 * Returns the thumbnail image URL for the supplied provider and video ID. 953 * @param {string} provider The video service provider name. 954 * @param {string|number} video_id The ID of the video to get the thumbnail 955 * for. 956 * @returns {string|false} The URL of the thumbnail image, if found, 957 * otherwise false. 958 */ 959 getThumbnailUrl: async function (provider, video_id, size={width: 295, height: 166}) { 960 switch (provider) { 961 case this.providers.VIMEO: 962 return await this.getVimeoThumbnailUrl(video_id, size); 963 case this.providers.YOUTUBE: 964 return this.getYoutubeThumbnailUrl(video_id); 965 } 966 return false; 967 }, 968 969 /** 970 * Returns an `img` Element for the thumbnail image of the supplied provider 971 * and video ID. 972 * @param {string} provider The video service provider name. 973 * @param {string|number} video_id The ID of the video to get the thumbnail 974 * for. 975 * @returns {Element} The generated `img` Element for the thumbnail. 976 */ 977 getThumbnailImgForVideo: async function (provider, video_id, size={width: 295, height: 166}) { 978 const img = document.createElement("img"); 979 img.classList.add(this.videoThumbnailClassname); 980 img.src = await this.getThumbnailUrl(provider, video_id, size); 981 return img; 982 }, 983 984 /** 985 * Returns an `img` Element for the thumbnail image of the media contained in 986 * the supplied iframe. 987 * @param {Element} iframe The iframe Element to get the thumbnail image for. 988 * @returns The result of calling `getThumbnailImgForVideo()` for the 989 */ 990 getThumbnailImgForIframe: async function (iframe) { 991 const width = iframe.clientWidth; 992 const height = iframe.clientHeight; 993 const { video_id, provider } = this.isManagedIframe(iframe); 994 return this.getThumbnailImgForVideo(provider, video_id, {width, height}); 995 }, 996 997 /** 998 * Gets the custom overlay image URL for the specified provider. 999 * @param {string} provider The provider to get the custom overlay image URL 1000 * for. 1001 * @returns {string} The URL of the custom overlay image, if found, otherwise 1002 * false. 1003 */ 1004 getOverlayImageSrc: function (provider) { 1005 return this.getProviderSettings(provider)?.overlay_image || false; 1006 }, 1007 1008 /** 1009 * Gets the custom overlay image Element for the specified provider. 1010 * @param {string} provider The provider to get the custom overlay image for. 1011 * @returns {Element|false} The custom overlay image Element, if found, 1012 * otherwise false. 1013 */ 1014 getOverlayImage: function (provider) { 1015 const imgOverlaySrc = this.getOverlayImageSrc(provider); 1016 1017 if (!imgOverlaySrc) { 1018 return false; 1019 } 1020 1021 const imgOverlay = document.createElement("img"); 1022 imgOverlay.src = imgOverlaySrc; 1023 imgOverlay.setAttribute("class", this.overlayImageClassname); 1024 return imgOverlay; 1025 }, 1026 1027 /** 1028 * Sets styles on the supplied iframe Element and its parent node to make it 1029 * 16:9 responsive aspect ratio. 1030 * @param {Element} iframe The iframe Element to operate on. 1031 */ 1032 makeResponsiveIframe: function (iframe) { 1033 // Make the video frame responsive, based on 16:9 ratio. 1034 iframe.parentNode.setAttribute( 1035 "style", 1036 "width:100%; height:0; position:relative; padding-bottom:56.25%" 1037 ); 1038 iframe.setAttribute( 1039 "style", 1040 "width:100%; height:100%; position:absolute; left:0; top:0" 1041 ); 1042 }, 1043 1044 /** 1045 * Creates an error modal in the specified target DOM element. 1046 * @param {object} param0 An object containing additional options. 1047 * @returns {void} 1048 */ 1049 doErrorModal: function ({ 1050 target, 1051 cancelCallback = null, 1052 withCloseButton = true, 1053 }) { 1054 const hasOverlay = this.getConsentOverlay(target); 1055 // If the overlay is already open, do nothing. 1056 if (hasOverlay) { 1057 return; 1058 } 1059 1060 if (!target.getAttribute("id")) { 1061 // Add an id attribute for use by `getConsentOverlay()`. 1062 const id = this.genUID("otsuka-pcm-overlay-id-"); 1063 target.setAttribute("id", id); 1064 } 1065
1066 const overlay = this.createErrorOverlay({ 1067 target: target, 1068 cancelCallback: cancelCallback, 1069 withCloseButton: withCloseButton, 1070 }); 1071 1072 target.append(overlay); 1073 }, 1074 1075 /** 1076 * Creates a consent modal in the specified target DOM element. If the 1077 * specified provider is not enabled in the module settings, does nothing 1078 * except to call consentCallback, if provided. 1079 * @param {object} param0 An object containing additional options. 1080 * @returns {void} 1081 */ 1082 doConsentModal: async function ({ 1083 target, 1084 provider, 1085 consentCallback = null, 1086 cancelCallback = null, 1087 withCloseButton = true, 1088 }) { 1089 if (!this.isProviderEnabled(provider)) { 1090 try { 1091 this.doConsentCallback(consentCallback, provider, target); 1092 return; 1093 } catch (e) { 1094 throw e; 1095 } 1096 } 1097 1098 const consent_category = this.getConsentCategoryForProvider(provider); 1099 const hasOverlay = this.getConsentOverlay(target); 1100 const providerSettings = this.getProviderSettings(provider) 1101 let isConsented = false; 1102 1103 try { 1104 isConsented = await this.isConsentedToCategory(consent_category); 1105 } catch (e) { 1106 throw e; 1107 } 1108 1109 if (isConsented) { 1110 try { 1111 this.doConsentCallback(consentCallback, provider, target); 1112 return; 1113 } catch (e) { 1114 throw e; 1115 } 1116 } else { 1117 // If the overlay is already open, do nothing. 1118 if (hasOverlay) { 1119 return; 1120 } 1121 1122 if (!target.getAttribute("id")) { 1123 // Add an id attribute for use by `getConsentOverlay()`. 1124 const id = this.genUID("otsuka-pcm-overlay-id-"); 1125 target.setAttribute("id", id); 1126 } 1127 1128 const overlayConsentCallback = () => { 1129 this.doConsentCallback(consentCallback, provider, target); 1130 }; 1131 1132 const overlay = this.createConsentOverlay({ 1133 target: target, 1134 provider: provider, 1135 consentCallback: overlayConsentCallback, 1136 cancelCallback: cancelCallback, 1137 withCloseButton: withCloseButton, 1138 }); 1139 1140 // Remove all previously open modal before open new. 1141 const previousOverlay = document.querySelector(".otsuka-pcm-consent-overlay") 1142 if (previousOverlay) { 1143 previousOverlay.remove(); 1144 } 1145 if (providerSettings.modal.description !== '') { 1146 target.append(overlay); 1147 } 1148 1149 // Load content if user accept it from OneTrust banner. 1150 const overlayClass = this.overlayClassname; 1151 // Copy scope of the current execution. 1152 let _this = this; 1153 _this.callback = consentCallback; 1154 _this.provider = provider; 1155 _this.target = target; 1156 OneTrust.OnConsentChanged(function () { 1157 let isConsentedPromise = OtsukaPCM.isConsentedToCategory(consent_category); 1158 isConsentedPromise.then(isConsentedResult => { 1159 const hasOverlay = OtsukaPCM.getConsentOverlay(_this.target); 1160 if (isConsentedResult && hasOverlay) { 1161 try { 1162 OtsukaPCM.doConsentCallback(_this.callback, _this.provider, _this.target); 1163 return; 1164 } catch (e) { 1165 throw e; 1166 } 1167 } 1168 }); 1169 }); 1170 // Set up external link dialog. 1171 if (typeof Drupal.behaviors.externalLinkPopup !== undefined) { 1172 document.querySelectorAll('.otsuka-pcm-consent-overlay a').forEach((link) => { 1173 if (link.classList.contains('external-link-popup-disabled')) return; 1174 if (!Drupal.behaviors.externalLinkPopup) return; 1175 const domain = Drupal.behaviors.externalLinkPopup.getDomain(link.getAttribute('href')); 1176 if (!domain) return; 1177 link.addEventListener('click', function (e) { 1178 for (var i = 0; i < drupalSettings.external_link_popup.popups.length; i++) { 1179 popup = drupalSettings.external_link_popup.popups[i]; 1180 if (popup.domains !== '*' && !Drupal.behaviors.externalLinkPopup.inDomain(domain, popup.domains.split(/\s*,\s*|\s+/))) { 1181 continue; 1182 } 1183 e.preventDefault(); 1184 return Drupal.behaviors.externalLinkPopup.openDialog( 1185 link, 1186 popup, 1187 domain 1188 ); 1189 } 1190 }); 1191 }); 1192 } 1193 // Announce modal description 1194 Drupal.announce(providerSettings.modal.description); 1195 } 1196 }, 1197 1198 /** 1199 * Execute a consentCallback from doConsentModal(). 1200 * @param {function} consentCallback 1201 * @param {string} provider 1202 * @param {Element} target 1203 */ 1204 doConsentCallback: async function (consentCallback, provider, target) { 1205 try { 1206 // Ensure the player API is available. 1207 await this.loadPlayerAPI(provider); 1208 } catch (e) { 1209 throw e; 1210 } 1211 // In case the overlay was already shown in this modal, hide it. 1212 this.removeConsentOverlay(target); 1213 consentCallback && consentCallback(); 1214 }, 1215 1216 /** 1217 * Initializes the target iframe for use with consent management. 1218 * @param {Element} iframe The iframe Element of the media item that will be 1219 * consent-managed. The consent overlay will be placed at the location of 1220 * this element. 1221 * @param {object}
1221 passedOptions An object containing additional options. 1222 * @returns {void} 1223 */ 1224 initIframe: async function (iframe, passedOptions = {}) { 1225 const options = { 1226 showModal: false, 1227 withCloseButton: true, 1228 withDefaultOverlay: true, 1229 displayErrorModal: true, 1230 ...passedOptions, 1231 }; 1232 const { domain, video_id, provider } = this.isManagedIframe(iframe); 1233 if (!domain || !video_id) { 1234 return; 1235 } 1236 1237 if (!this.isProviderEnabled(provider)) { 1238 return; 1239 } 1240 1241 if (!iframe.id) { 1242 iframe.id = this.genUID("otsk-player-"); 1243 } 1244 1245 // Hide the iframe. 1246 iframe.style.display = "none"; 1247 1248 // Make the video frame responsive, based on 16:9 ratio. 1249 this.makeResponsiveIframe(iframe); 1250 1251 // Load the video thumbnail. 1252 const img = await this.getThumbnailImgForIframe(iframe); 1253 // Append img as a sibling of the iframe. 1254 iframe.parentNode.append(img); 1255 1256 // Get custom thumbnail overlay image set in the module settings. 1257 const imgOverlay = this.getOverlayImage(provider); 1258 if (imgOverlay) { 1259 // Display custom thumbnail overlay image. 1260 imgOverlay.classList.add(this.thumbnailOverlayClassname); 1261 iframe.parentNode.classList.add(this.thumbnailOverlayCustomClassname); 1262 iframe.parentNode.append(imgOverlay); 1263 } else if (options.withDefaultOverlay) { 1264 // Display default thumbnail overlay image. 1265 iframe.parentNode.classList.add(this.thumbnailOverlayDefaultClassname); 1266 this.videoOverlayResizeObserver.observe(iframe.parentNode); 1267 } 1268 1269 const imgClickListener = async () => { 1270 const target = iframe.parentNode; 1271 try { 1272 const consentCallback = async () => { 1273 target.classList.remove( 1274 this.thumbnailOverlayCustomClassname, 1275 this.thumbnailOverlayDefaultClassname 1276 ); 1277 imgOverlay && imgOverlay.remove(); 1278 img.remove(); 1279 this.playIframeVideo(iframe); 1280 // Dispatch loadPlayer event for react_player. 1281 const e = new Event("loadPlayer"); 1282 e.id = target.getAttribute("id"); 1283 document.dispatchEvent(e); 1284 }; 1285 await this.doConsentModal({ 1286 target: target, 1287 provider: provider, 1288 consentCallback: consentCallback, 1289 ...options, 1290 }); 1291 } catch (e) { 1292 this.printError(`initIframe: caught error: ${e}`); 1293 if (options.displayErrorModal) { 1294 this.doErrorModal({ 1295 target: target, 1296 ...options, 1297 }); 1298 } 1299 } 1300 }; 1301 1302 // Listen for keyboard events. 1303 const imgKeyupListener = async (e) => { 1304 if ("Enter" === e.key) { 1305 e.target.click(); 1306 } 1307 }; 1308 1309 img.addEventListener("click", imgClickListener); 1310 img.addEventListener("keydown", imgKeyupListener); 1311 img.setAttribute("role", "button"); 1312 img.setAttribute("tabindex", "0"); 1313 1314 if (options.showModal) { 1315 img.click(); 1316 } 1317 }, 1318 1319 /** 1320 * Initializes the target React Player wrapper Element for use with consent 1321 * management. 1322 * @param {Element} reactPlayerWrapper The target DOM Element of the react 1323 * player wrapper that is being consent-managed. The consent overlay will 1324 * be placed inside this element. 1325 * @param {object} passedOptions An object containing additional options. 1326 * @returns {void} 1327 */ 1328 initReactPlayer: async function (reactPlayerWrapper, passedOptions = {}) { 1329 const options = { 1330 showModal: false, 1331 withCloseButton: true, 1332 withDefaultOverlay: true, 1333 displayErrorModal: true, 1334 ...passedOptions, 1335 }; 1336 1337 const playerId = reactPlayerWrapper.parentNode.id; 1338 const video = drupalSettings.react_videos?.videos_list[playerId]; 1339 1340 // If a player already exists, do nothing. 1341 const player = reactPlayerWrapper.querySelector(".react-player"); 1342 if (player) { 1343 return; 1344 } 1345 1346 // If the playerId doesn't exist in drupalSettings, do nothing. 1347 if (!video) { 1348 return; 1349 } 1350 1351 // If it's not a managed video src, do nothing. 1352 const { domain, video_id, provider } = this.isManagedSrc(video.url); 1353 if (!domain || !video_id) { 1354 return; 1355 } 1356 1357 const preview = reactPlayerWrapper.querySelector(".react-player
1357-preview"); 1358 1359 // Get custom thumbnail overlay image set in the module settings. 1360 const imgOverlay = this.getOverlayImage(provider); 1361 if (imgOverlay) { 1362 // Display custom thumbnail overlay image. 1363 imgOverlay.classList.add(this.thumbnailOverlayClassname); 1364 reactPlayerWrapper.classList.add(this.thumbnailOverlayCustomClassname); 1365 reactPlayerWrapper.append(imgOverlay); 1366 } else if (options.withDefaultOverlay) { 1367 // Display default thumbnail overlay image. 1368 reactPlayerWrapper.classList.add(this.thumbnailOverlayDefaultClassname); 1369 this.videoOverlayResizeObserver.observe(reactPlayerWrapper); 1370 } 1371 1372 const previewClickListener = async () => { 1373 const target = reactPlayerWrapper; 1374 try { 1375 const consentCallback = () => { 1376 target.classList.remove( 1377 this.thumbnailOverlayCustomClassname, 1378 this.thumbnailOverlayDefaultClassname 1379 ); 1380 imgOverlay && imgOverlay.remove(); 1381 1382 // Dispatch loadPlayer event for react_player. 1383 const e = new Event("loadPlayer"); 1384 e.id = target.getAttribute("id"); 1385 document.dispatchEvent(e); 1386 }; 1387 await this.doConsentModal({ 1388 target: target, 1389 provider: provider, 1390 consentCallback: consentCallback, 1391 ...options, 1392 }); 1393 } catch (e) { 1394 this.printError(`initReactPlayer: caught error: ${e}`); 1395 if (options.displayErrorModal) { 1396 this.doErrorModal({ 1397 target: target, 1398 ...options, 1399 }); 1400 } 1401 } 1402 }; 1403 1404 // Listen for keyboard events. 1405 const previewKeyupListener = async (e) => { 1406 if ("Enter" === e.key) { 1407 e.target.click(); 1408 } 1409 }; 1410 1411 preview.addEventListener("click", previewClickListener); 1412 preview.addEventListener("keydown", previewKeyupListener); 1413 preview.setAttribute("role", "button"); 1414 preview.setAttribute("tabindex", "0"); 1415 1416 if (options.showModal) { 1417 preview.click(); 1418 } 1419 }, 1420 1421 /** 1422 * Initializes the target script Element for use with consent management. 1423 * Example: <script 1424 * src="https://www.buzzsprout.com/1324951.js?container_id=buzzsprout-large-player&player=large" 1425 * type="text/javascript" charset="utf-8"></script> 1426 * @param {Element} script The target script Element of the podcast player 1427 * that is being consent-managed. The consent overlay will be placed inside 1428 * the Element referred to by this script's `container_id` query parameter. 1429 * @param {object} passedOptions An object containing additional options. 1430 */ 1431 initBuzzsproutScript: async function (script, passedOptions = {}) { 1432 const options = { 1433 showModal: true, 1434 withCloseButton: true, 1435 withDefaultOverlay: true, 1436 displayErrorModal: true, 1437 ...passedOptions, 1438 }; 1439 const dataSrc = script.getAttribute("data-src"); 1440 const { provider } = this.isManagedSrc(dataSrc); 1441 1442 // If it's not a Buzzsprout script, do nothing. 1443 if (provider !== this.providers.BUZZSPROUT) { 1444 this.throwError("initBuzzsproutScript: invalid script element"); 1445 return; 1446 } 1447 1448 const consentCategoryId = this.getConsentCategoryForProvider(provider); 1449 let isConsented = false; 1450 1451 try { 1452 isConsented = await this.isConsentedToCategory(consentCategoryId); 1453 } catch (e) { 1454 // We'll display an error below in `doConsentModal` so don't worry about it here. 1455 } 1456 1457 // Get target element (container_id) from script params. 1458 // e.g. container_id=buzzsprout-large-player 1459 const url = new URL(dataSrc); 1460 const container_id = url.searchParams.get("container_id"); 1461 const buzzsproutWrapper = document.getElementById(container_id); 1462 1463 // If the user is already consented, insert the script and return. 1464 if (isConsented) { 1465 this.insertScript(script); 1466 1467 // Dispatch loadPlayerBP event for Buzzsprout. 1468 const target = buzzsproutWrapper; 1469 const e = new Event("loadPlayerBP"); 1470 e.id = target.getAttribute("id"); 1471 document.dispatchEvent(e); 1472 1473 return; 1474 } 1475 1476 // Get custom thumbnail overlay image set in the module settings. 1477 const imgOverlay = this.getOverlayImage(provider); 1478 if (imgOverlay) { 1479 // Display custom thumbnail overlay image. 1480 buzzsproutWrapper.classList.add( 1481 this.buzzsproutPlaceholderCustomClassname 1482 ); 1483 buzzsproutWrapper.append(imgOverlay); 1484 } else if (options.withDefaultOverlay) { 1485 // Display default thumbnail overlay image. 1486 // buzzsproutWrapper.style.position = "relative"; 1487 buzzsproutWrapper.classList.add( 1488 this.buzzsproutPlaceholderDefaultClassname 1489 ); 1490 } 1491 1492 const wrapperClickListener = async () => { 1493 const target = buzzsproutWrapper;
1494 try { 1495 const consentCallback = () => { 1496 target.classList.remove( 1497 this.buzzsproutPlaceholderCustomClassname, 1498 this.buzzsproutPlaceholderDefaultClassname 1499 ); 1500 imgOverlay && imgOverlay.remove(); 1501 this.insertScript(script); 1502 1503 // Dispatch loadPlayerBP event for Buzzsprout. 1504 const e = new Event("loadPlayerBP"); 1505 e.id = target.getAttribute("id"); 1506 document.dispatchEvent(e); 1507 }; 1508 await this.doConsentModal({ 1509 target: target, 1510 provider: provider, 1511 consentCallback: consentCallback, 1512 ...options, 1513 }); 1514 } catch (e) { 1515 this.printError(`initBuzzsproutScript: caught error: ${e}`); 1516 if (options.displayErrorModal) { 1517 this.doErrorModal({ 1518 target: target, 1519 ...options, 1520 }); 1521 } 1522 } 1523 }; 1524 1525 // Listen for keyboard events. 1526 const wrapperKeyupListener = async (e) => { 1527 if ("Enter" === e.key) { 1528 e.target.click(); 1529 } 1530 }; 1531 1532 // Open consent modal in target of script 1533 buzzsproutWrapper.addEventListener("click", wrapperClickListener); 1534 buzzsproutWrapper.addEventListener("keydown", wrapperKeyupListener); 1535 buzzsproutWrapper.setAttribute("role", "button"); 1536 buzzsproutWrapper.setAttribute("tabindex", "0"); 1537 1538 if (options.showModal) { 1539 buzzsproutWrapper.click(); 1540 } 1541 }, 1542 1543 /** 1544 * âDeinitializesâ the target script Element that was previously prepared for 1545 * consent management with `initBuzzsproutScript()`. 1546 * @param {Element} script The target script Element of the podcast player 1547 * that is being consent-managed. The consent overlay will be removed from 1548 * the Element referred to by this script's `container_id` query parameter. 1549 */ 1550 deinitBuzzsproutScript: function (script) { 1551 const dataSrc = script.getAttribute("data-src"); 1552 const { provider } = this.isManagedSrc(dataSrc); 1553 1554 // If it's not a Buzzsprout script, do nothing. 1555 if (provider !== this.providers.BUZZSPROUT) { 1556 this.throwError("deinitBuzzsproutScript: invalid script element"); 1557 return; 1558 } 1559 1560 const url = new URL(dataSrc); 1561 const container_id = url.searchParams.get("container_id"); 1562 const buzzsproutWrapper = document.getElementById(container_id); 1563 const imgOverlay = buzzsproutWrapper.querySelector( 1564 ".otsuka-pcm-img-overlay" 1565 ); 1566 imgOverlay && imgOverlay.remove(); 1567 buzzsproutWrapper.classList.remove(this.buzzsproutOverlayDefaultClassname); 1568 this.removeConsentOverlay(buzzsproutWrapper); 1569 }, 1570 1571 /** 1572 * Initializes the target script Element for use with consent management. 1573 * Example: <script type="text/javascript" defer="" 1574 * src="https://otsuka-jynarque-stage.orbita.cloud:8443/chatbot/v3/chat.js" 1575 * data-once="cmpInitScript"></script> 1576 * @param {Element} script The target script Element of the podcast player 1577 * that is being consent-managed. The consent overlay will be placed inside 1578 * this elementâs parent element. 1579 * @param {object} passedOptions An object containing additional options. 1580 */ 1581 initOrbitaScript: async function (script, passedOptions = {}) { 1582 const options = { 1583 displayErrorModal: false, 1584 ...passedOptions, 1585 withCloseButton: true, 1586 }; 1587 const dataSrc = script.getAttribute("data-src"); 1588 const { provider } = this.isManagedSrc(dataSrc); 1589 1590 // If it's not an Orbita script, do nothing. 1591 if (provider !== this.providers.ORBITA) { 1592 this.throwError("initOrbitaScript: invalid script element"); 1593 return; 1594 } 1595 1596 const orbitaSettings = this.getProviderSettings(this.providers.ORBITA); 1597 const consentCategoryId = this.getConsentCategoryForProvider(provider); 1598 let isConsented = false; 1599 1600 try { 1601 isConsented = await this.isConsentedToCategory(consentCategoryId); 1602 } catch (e) { 1603 if (!options.displayErrorModal) { 1604 throw e; 1605 } 1606 // else: 1607 // We'll display an error below in `doConsentModal` so don't worry about it here. 1608 } 1609 1610 // If the user is already consented, insert the script and return. 1611 if (isConsented) { 1612 this.insertScript(script); 1613 return; 1614 } 1615 1616 const widget = this.createElementFromText(orbitaSettings.widget.html); 1617 const styles = this.createElementFromText( 1618 orbitaSettings.widget.css, 1619 "style", 1620 false 1621 ); 1622 const chatbotWrapper = document.getElementById( 1623 orbitaSettings.widget.wrapper_id 1624 ); 1625 chatbotWrapper.appendChild(widget); 1626 chatbotWrapper.appendChild(styles); 1627 1628 const widgetClickListener = async () => { 1629 const target = document.body;
1630 try { 1631 // Open consent modal in target of script 1632 const consentCallback = async () => { 1633 widget && widget.remove(); 1634 this.insertScript(script); 1635 await this.awaitOrbitaChatbot(); 1636 const orbitaButton = await this.awaitOrbitaChatButton(); 1637 orbitaButton.click(); 1638 }; 1639 await this.doConsentModal({ 1640 target: target, 1641 provider: provider, 1642 consentCallback: consentCallback, 1643 ...options, 1644 }); 1645 } catch (e) { 1646 this.printError(`initOrbitaScript: caught error: ${e}`); 1647 if (options.displayErrorModal) { 1648 this.doErrorModal({ 1649 target: target, 1650 ...options, 1651 }); 1652 } 1653 } 1654 }; 1655 1656 // Listen for keyboard events. 1657 const widgetKeyupListener = async (e) => { 1658 if ("Enter" === e.key) { 1659 e.target.click(); 1660 } 1661 }; 1662 1663 widget.addEventListener("click", widgetClickListener); 1664 widget.addEventListener("keydown", widgetKeyupListener); 1665 widget.setAttribute("role", "button"); 1666 widget.setAttribute("tabindex", "0"); 1667 }, 1668 1669 /** 1670 * Initializes the target script Element for use with consent management. 1671 * @param {Element} script The target script Element that is being 1672 * consent-managed. 1673 * @param {object} passedOptions An object containing additional options. 1674 * @returns {void} 1675 */ 1676 initScript: async function (script, passedOptions = {}) { 1677 const options = { 1678 showModal: true, 1679 withCloseButton: true, 1680 ...passedOptions, 1681 }; 1682 const { provider } = this.isManagedSrc(script.getAttribute("data-src")); 1683 1684 if (!provider) { 1685 // Do nothing. Don't throw or print errors since this function may be 1686 // called en masse on all/many scripts on a page. 1687 return; 1688 } 1689 1690 try { 1691 switch (provider) { 1692 case this.providers.BUZZSPROUT: 1693 await this.initBuzzsproutScript(script, options); 1694 break; 1695 case this.providers.ORBITA: 1696 await this.initOrbitaScript(script, options); 1697 break; 1698 } 1699 } catch (e) { 1700 throw e; 1701 } 1702 }, 1703});
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.