PageSourceSearch

https://www.jynarque.com/modules/custom/otsuka_privacy_compliant_media/js/otsuka_pcm_api.js

js jynarque.com collected 2026-10-08 00:24:12 UTC 54,786 bytes, 1,703 lines download raw bytes

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&amp;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.