PageSourceSearch

https://www.elegant-und-excellent.de/evercdn/assets/privacyControl/v3/scripts/main.js

js elegant-und-excellent.de collected 2026-10-06 22:02:32 UTC 50,873 bytes, 1,407 lines download raw bytes

1/*!
2 * @author Dominik Kressler
3 * @version 3.11.0
4 * @description control scripts and iframes that cause in gdpr related problems
5 * @dependencies [email protected], [email protected]
6 **/
7class PrivacyControl {
8    options = {};
9    #deps = {};
10    #elms = {};
11    #interacted = false;
12    #lang = "en";
13    #loaded = {};
14    #localStorage = undefined;
15    initializing = true;
16    eventQueue = [];
17    version = "3.11.0";
18    isDialogVisible = false;
19    static defaultOptions = {
20        badge: true,
21        badgePos: "vert-left",
22        force: false,
23        hideDialog: false,
24        id: "privacy-control",
25        noteDelay: 300,
26        safeSurfer: true,
27        sessionName: "privacy-control",
28        placeholder: "/securewebapps/evercdn/assets/privacyControl/v3/placeholder/index.html",
29    };
30    static notification = window?.notification;
31
32    constructor(options = {}) {
33        /* prevent multiple instances */
34        if (window?.pCinit !== undefined) return false;
35        else window.pCinit = true;
36
37        this.options = {
38            ...PrivacyControl.defaultOptions,
39            ...options,
40        };
41
42        this.#setUpLocalStorage();
43
44        /** get doc language and set instance language */
45        this.#lang =
46            this.options?.lang ||
47            document.documentElement.lang.split("-")[0].toLowerCase();
48        /** unknown languages keep their code but alias en translations */
49        if (pCl10n[this.#lang] === undefined) {
50            pCl10n[this.#lang] = pCl10n.en;
51            if (typeof pCServiceTemplates === "object") {
52                for (const tplKey in pCServiceTemplates) {
53                    const tpl = pCServiceTemplates[tplKey];
54                    if (
55                        tpl?.l10n &&
56                        tpl.l10n[this.#lang] === undefined &&
57                        tpl.l10n.en
58                    ) {
59                        tpl.l10n[this.#lang] = tpl.l10n.en;
60                    }
61                }
62            }
63        }
64
65        /** did the user already interact? **/
66        this.#interacted =
67            this.#localStorage?.getItem(this.options.sessionName) !== null;
68
69        /** create safeSurfer if enabled */
70        if (this.options.safeSurfer)
71            this.#elms.safeSurferBadge = this.#createSafeSurferBadge();
72
73        this.#elms.dialog = this.#createDialog();
74
75        PrivacyControl.notification.add({
76            sessionName: this.options.sessionName,
77            id: this.options.id,
78            message: this.#elms.dialog,
79            saveState: !this.options.force,
80            remove: false,
81            delay: 0,
82            focus: true,
83            role: "dialog",
84            label: this.#getText("badge"),
85            discard: true,
86        });
87
88        /** event to open dialog from everywhere */
89        for (let link of document?.querySelectorAll(
90            'a[href$="#privacy-controller"]'
91        )) {
92            ["click", "keydown"].forEach((eventType) => {
93                link.addEventListener(
94                    eventType,
95                    this.#handleOpenDialogLinks.bind(this)
96                );
97            });
98        }
99
100        /* closeSettings is deprecated, so we keep it as alias */
101        this.closeSettings = this.closeDialog;
102
103        window.dispatchEvent(new CustomEvent("PCInit", { detail: this }));
104        window.privacyControl = this;
105
106        /** complete initialization after current execution context */
107        queueMicrotask(() => {
108            this.initializing = false;
109            this.#flushEventQueue();
110        });
111    }
112
113    #handleOpenDialogLinks(event) {
114        if (PrivacyControl.isEventClickEnterSpace(event)) this.openDialog();
115    }
116    #handleOpenSettingsLinks(event) {
117        if (PrivacyControl.isEventClickEnterSpace(event)) this.openSettings();
118    }
119    #handleDirectSetterLink(event) {
120        if (PrivacyControl.isEventClickEnterSpace(event)) {
121            event.preventDefault();
122            event.stopPropagation();
123            const key = event.target.dataset.privacyControl;
124            this.set(key, true);
125            /** a link may address a sub consent, forceReload sits on the service */
126            if (this.#deps[this.#resolveKey(key).parent]?.forceReload === true)
127                window.location.reload();
128        }
129    }
130    #handleResetButton(event) {
131        if (PrivacyControl.isEventClickEnterSpace(event)) {
132            this.#localStorage.setItem(this.options.sessionName, "");
133            window.location.reload();
134        }
135    }
136    #handleSetterInIframe(event) {
137        if (PrivacyControl.isEventClickEnterSpace(event)) {
138            let key =
139                document.activeElement.dataset.iframe ||
140                document.activeElement.dataset.pcService;
141            this?.set(key, true);
142            if (this.#deps[key]?.forceReload === true) window.location.reload();
143        }
144    }
145
146    #setUpLocalStorage() {
147        /** check if localStorage exists and create fallback */
148        try {
149            window.localStorage.setItem("_pC_test", "1");
150            window.localStorage.removeItem("_pC_test");
151            window._storageDisabled = !1;
152            this.#localStorage = window.localStorage;
153        } catch (e) {
154            this.#localStorage = {
155                _items: new Map(),
156                setItem(e, t) {
157                    this._items.set(e, t);
158                },
159                getItem(e) {
160                    return this._items.has(e) ? this._items.get(e) : null;
161                },
162            };
163            window._storageDisabled = true;
164
165            console.log(
166                "sessionStorage and/or localStorage not available, falling back to in-memory storage: ",
167                e
168            );
169        }
170    }
171
172    #dispatchOrQueue(eventName, detail = {}) {
173        /** dispatch event immediately or queue if still initializing */
174        const event = new CustomEvent(eventName, { detail });
175
176        if (this.initializing) {
177            this.eventQueue.push(event);
178        } else {
179            window.dispatchEvent(event);
180        }
181    }
182
183    #flushEventQueue() {
184        /** dispatch all queued events after initialization */
185        while (this.eventQueue.length > 0) {
186            const event = this.eventQueue.shift();
187            window.dispatchEvent(event);
188        }
189    }
190
191    static isEventClickEnterSpace(e) {
192        return (
193            e.type === "click" ||
194            (e.type === "keydown" && (e.key === "Enter" || e.code === "Space"))
195        );
196    }
197
198    #createSafeSurferBadge() {
199        const badge = this.#customTag({
200            name: "badge",
201            target: document.body,
202            attrs: {
203                "aria-expanded": "false",
204                "tabindex": "0",
205                "role": "button",
206                "aria-label": this.#getText("safeSurferLabel"),
207            },
208            innerHTML: `${pCsafeSurfer.icon} <span class="safe-surfer-msg">${this.#getText("safeSurfer")}</span>`,
209            classList: ["safe-surfer-badge"],
210        });
211
212        ["click", "keydown", "focusout"].forEach((eventType) => {
213            badge.addEventListener(eventType, function (e) {
214                if (PrivacyControl.isEventClickEnterSpace(e)) {
215                    e.preventDefault();
216                    e.stopPropagation();
217                    this.classList.toggle("expand");
218                    this.ariaExpanded =
219                        this.ariaExpanded === "false" ? "true" : "false";
220                }
221                if (e.type === "focusout") {
222                    e.target.classList.remove("expand");
223                    e.target.ariaExpanded = "false";
224                }
225            });
226        });
227
228        return badge;
229    }
230
231    #getText(key = "", service) {
232        /** get text from language object */
233        let string = "";
234        try {
235            if (
236                service !== undefined &&
237                this.options.deps[service].l10n[this.#lang][key] !== undefined
238            ) {
239                string = this.options.deps[service].l10n[this.#lang][key];
240            } else if (
241                typeof this.options[key] === "object" &&
242                this.options[key][this.#lang] !== undefined
243            ) {
244                string = this.options[key][this.#lang];
245            } else if (pCl10n[this.#lang][key] !== undefined) {
246                string = pCl10n[this.#lang][key];
247            }
248            return string;
249        } catch (error) {
250            console.log("Translation string missing for: " + key);
251            return false;
252        }
253    }
254
255    #serviceStackNeedsReload() {
256        let reloadNeeded = false;
257
258        for (let key in this.#deps) {
259            if (this.#deps[key].forceReload === true) reloadNeeded = true;
260        }
261
262        if (reloadNeeded) window.location.reload();
263
264        return reloadNeeded;
265    }
266
267    #createDialog() {
268        const instance = this;
269        /** create the main pc dialog */
270        const dialog = this.#customTag({ name: "privacy-controller" });
271        dialog.role = "form";
272        dialog.id = "privacy-controller";
273
274        const header = this.#customTag({
275            name: "privacy-header",
276            target: dialog,
277        });
278        header.id = "privacy-header";
279        header.innerHTML = `<privacy-head role="heading" aria-level="1">${this.#getText("intro")}</privacy-head>`;
280        const buttons = this.#customTag({
281            name: "privacy-buttons",
282            target: header,
283        });
284        const buttonOpen = this.#customTag({
285            name: "privacy-btn",
286            target: buttons,
287            attrs: {
288                tabindex: "0",
289                role: "button",
290                id: "privacy-open",
291                href: "#privacy-controller",
292            },
293            innerHTML: this.#getText("settings"),
294        });
295        ["click", "keydown"].forEach((eventType) => {
296            buttonOpen.addEventListener(eventType, function (e) {
297                if (PrivacyControl.isEventClickEnterSpace(e))
298                    instance.openSettings();
299            });
300        });
301        this.#elms.openSettings = buttonOpen;
302
303        const fields = this.#customTag({
304            name: "privacy-fields",
305            target: dialog,
306            attrs: {
307                "role": "table",
308                "aria-label": this.#getText("edit_settings"),
309            },
310        });
311        this.#elms.serviceContainer = fields;
312        const submitButtons = this.#customTag({
313            name: "privacy-submits",
314            target: dialog,
315        });
316        const buttonSave = this.#customTag({
317            name: "privacy-btn",
318            target: submitButtons,
319            attrs: {
320                tabindex: "0",
321                role: "button",
322                id: "privacy-save",
323            },
324            innerHTML: this.#getText("save"),
325        });
326        ["click", "keydown"].forEach((eventType) => {
327            buttonSave.addEventListener(eventType, function (e) {
328                if (PrivacyControl.isEventClickEnterSpace(e)) {
329                    let services =
330                        instance.#elms.serviceContainer.querySelectorAll(
331                            'input[type="checkbox"]'
332                        );
333                    let forceAReload = false;
334                    for (const service of services) {
335                        /** a parent with sub consents follows from its sub toggles */
336                        if (service.dataset.pcHasConsents !== undefined)
337                            continue;
338
339                        const parentKey =
340                            service.dataset.pcParent || service.name;
341                        if (instance.#deps[parentKey].forceReload === true)
342                            forceAReload = true;
343                        instance.set(service.name, service.checked);
344                    }
345                    if (forceAReload) window.location.reload();
346                    instance.closeDialog(e);
347                }
348            });
349        });
350        const buttonConfirmAll = this.#customTag({
351            name: "privacy-btn",
352            target: submitButtons,
353            attrs: {
354                tabindex: "0",
355                role: "button",
356                id: "privacy-confirm",
357                href: "#privacy-controller",
358            },
359            innerHTML: this.#getText("confirm"),
360        });
361        ["click", "keydown"].forEach((eventType) => {
362            buttonConfirmAll.addEventListener(eventType, function (e) {
363                if (PrivacyControl.isEventClickEnterSpace(e)) {
364                    for (let service in instance.#deps) {
365                        instance.set(service, true);
366                    }
367                    instance.#serviceStackNeedsReload();
368                    instance.closeDialog(e);
369                }
370            });
371        });
372
373        const footer = this.#customTag({
374            name: "privacy-footer",
375            target: dialog,
376        });
377        const reset = this.#customTag({
378            name: "privacy-link",
379            target: footer,
380            attrs: { tabindex: "0", role: "link", id: "privacy-reset" },
381            innerHTML: this.#getText("reset"),
382            classList: ["privacy-reset"],
383        });
384        ["click", "keydown"].forEach((eventType) => {
385            reset.addEventListener(
386                eventType,
387                this.#handleResetButton.bind(this)
388            );
389        });
390
391        const legal_page = this.#getText("legal_page");
392        if (legal_page && legal_page != "") {
393            const imprint = this.#customTag({
394                name: "privacy-link",
395                target: footer,
396                attrs: {
397                    tabindex: "0",
398                    role: "link",
399                    href: legal_page,
400                },
401                innerHTML: this.#getText("legal_notice"),
402            });
403            ["click", "keydown"].forEach((eventType) => {
404                imprint.addEventListener(eventType, function (e) {
405                    if (PrivacyControl.isEventClickEnterSpace(e))
406                        window.location = this.getAttribute("href");
407                });
408            });
409        }
410        const privacy_page = this.#getText("privacy_page");
411        if (privacy_page && privacy_page != "") {
412            const policy = this.#customTag({
413                name: "privacy-link",
414                target: footer,
415                attrs: {
416                    tabindex: "0",
417                    role: "link",
418                    href: privacy_page,
419                },
420                innerHTML: this.#getText("policy"),
421            });
422            ["click", "keydown"].forEach((eventType) => {
423                policy.addEventListener(eventType, function (e) {
424                    if (PrivacyControl.isEventClickEnterSpace(e))
425                        window.location = this.getAttribute("href");
426                });
427            });
428        }
429
430        return dialog;
431    }
432
433    #customTag(
434        props = {
435            name: "",
436            target: null,
437            prepend: false,
438            attrs: {},
439            innerHTML: null,
440        }
441    ) {
442        const elm = document.createElement(props?.name);
443        elm.classList.add(props?.name);
444
445        if (props?.target) {
446            if (!props?.prepend) props?.target?.appendChild(elm);
447            else props?.target?.prepend(elm);
448        }
449
450        if (props?.attrs && Object.keys(props?.attrs)?.length) {
451            for (let attr in props?.attrs) {
452                elm.setAttribute(attr, props?.attrs[attr]);
453            }
454        }
455
456        if (props?.innerHTML) elm.innerHTML = props?.innerHTML;
457
458        if (props?.classList && props.classList instanceof Array) {
459            for (let className of props?.classList) {
460                elm.classList.add(className);
461            }
462        }
463
464        return elm;
465    }
466
467    /** create the classic pc badge */
468    #createPCBadge() {
469        const badge = this.#customTag({
470            name: "badge",
471            target: document.body,
472            attrs: {
473                "role": "button",
474                "tabindex": "0",
475                "aria-controls": this.options.id,
476            },
477            innerHTML: `<span>${this.#getText("badge")}</span>`,
478            classList: ["privacy-control-badge", this.options.badgePos],
479        });
480
481        ["click", "keydown"].forEach((eventType) => {
482            badge.addEventListener(
483                eventType,
484                this.#handleOpenSettingsLinks.bind(this)
485            );
486        });
487
488        return badge;
489    }
490
491    static #mergeDeps(obj1 = {}, obj2 = {}) {
492        for (let dep in obj2) {
493            if (obj2[dep] === false) delete obj1[dep];
494        }
495        return obj1;
496    }
497
498    static serviceCallback(callback) {
499        if (typeof callback == "function") {
500            return callback();
501        }
502
503        return false;
504    }
505
506    #isLegalPage() {
507        let found = 0;
508        const legal = pCl10n[this.#lang].legal_page;
509        const priv = pCl10n[this.#lang].privacy_page;
510
511        if (
512            legal == window.location.pathname ||
513            priv == window.location.pathname
514        )
515            found++;
516
517        return found;
518    }
519
520    #getStorage() {
521        let storage = this.#localStorage.getItem(this.options.sessionName);
522        return storage === null || storage === "" || storage === false
523            ? {}
524            : JSON.parse(storage);
525    }
526
527    #resolveKey(key = "") {
528        /** split "parent.sub" into its parts, plain service keys stay untouched */
529        const plain = { key: key, parent: key, sub: null };
530
531        if (pCServiceTemplates[key]) return plain;
532
533        const separator = key.lastIndexOf(".");
534        if (separator < 1) return plain;
535
536        const parent = key.slice(0, separator);
537        const sub = key.slice(separator + 1);
538        const consents = pCServiceTemplates[parent]?.consents;
539
540        if (
541            Array.isArray(consents) &&
542            consents.some((consent) => consent.key === sub)
543        )
544            return { key: key, parent: parent, sub: sub };
545
546        return plain;
547    }
548
549    #consentKeys(parentKey = "") {
550        /** full storage keys of every sub consent of a service */
551        const consents = pCServiceTemplates[parentKey]?.consents;
552        if (!Array.isArray(consents)) return [];
553
554        return consents.map((consent) => `${parentKey}.${consent.key}`);
555    }
556
557    #reflectCheckbox(key, status) {
558        /** mirror a consent state onto its checkbox and status label */
559        const checkbox = this.#elms.serviceContainer.querySelector(
560            `input[name="${key}"]`
561        );
562
563        checkbox.checked = status;
564        checkbox.indeterminate = false;
565        checkbox.nextSibling.innerText = status
566            ? this.#getText("on")
567            : this.#getText("off");
568
569        return checkbox;
570    }
571
572    #syncConsentGroup(parentKey) {
573        /** the parent toggle mirrors OR() over its sub consents */
574        const consentKeys = this.#consentKeys(parentKey);
575        if (!consentKeys.length) return false;
576
577        const granted = consentKeys.filter(
578            (consentKey) =>
579                this.#elms.serviceContainer.querySelector(
580                    `input[name="${consentKey}"]`
581                ).checked
582        );
583        const checkbox = this.#elms.serviceContainer.querySelector(
584            `input[name="${parentKey}"]`
585        );
586
587        checkbox.checked = granted.length > 0;
588        checkbox.indeterminate =
589            granted.length > 0 && granted.length < consentKeys.length;
590
591        if (checkbox.indeterminate)
592            checkbox.nextSibling.innerText = this.#getText("on_partial");
593        else
594            checkbox.nextSibling.innerText = checkbox.checked
595                ? this.#getText("on")
596                : this.#getText("off");
597
598        return true;
599    }
600
601    #customCells(cell1, cell2, target) {
602        return [
603            this.#customTag({
604                name: "privacy-cell",
605                target: target,
606                innerHTML: cell1,
607            }),
608            this.#customTag({
609                name: "privacy-cell",
610                target: target,
611                innerHTML: cell2,
612            }),
613        ];
614    }
615
616    #customRow(label = "", target = null) {
617        return this.#customTag({
618            name: "privacy-row",
619            target: target,
620            attrs: { "aria-label": label, "role": "row" },
621            classList: ["privacy-row"],
622        });
623    }
624
625    #setRequiredMode() {
626        this.openDialog();
627
628        if (this.#isLegalPage() === 0) {
629            const html = document.querySelector("html");
630            if (html.dataset.privacyControl.indexOf("center") == -1)
631                html.dataset.privacyControl += " center";
632            if (html.dataset.privacyControl.indexOf("backdrop") == -1)
633                html.dataset.privacyControl += " backdrop";
634        }
635    }
636
637    #createServiceMarkup(service) {
638        try {
639            const isRequired =
640                typeof service.required != "undefined" &&
641                service.required == true;
642            const group = this.#customTag({
643                name: "privacy-group",
644                target: this.#elms.serviceContainer,
645                prepend: isRequired,
646                attrs: {
647                    "role": "rowgroup",
648                    "aria-label": this.#getText("service") + " " + service.name,
649                },
650            });
651            const id = this.options.sessionName + ":" + service.key;
652
653            if (typeof service.name !== "undefined") {
654                this.#customCells(
655                    this.#getText("name"),
656                    service.name,
657                    this.#customRow(
658                        `${this.#getText("service")} ${this.#getText("name")}: ${service.name}`,
659                        group
660                    )
661                );
662            }
663
664            /* this is the first row with the checkbox */
665            const row = this.#customTag({
666                name: "privacy-row",
667                target: group,
668                attrs: { "aria-label": this.#getText("settings") },
669            });
670            const cell1 = this.#customTag({
671                name: "privacy-cell",
672                target: row,
673                innerHTML: this.#getText("accept"),
674            });
675            const cell2 = this.#customTag({
676                name: "privacy-cell",
677                target: row,
678                attrs: { "aria-label": this.#getText("policy") },
679            });
680
681            const preChecked =
682                typeof this.get(service.key) === "undefined" &&
683                service.softRequired === true
684                    ? "checked"
685                    : "";
686            const status =
687                typeof this.get(service.key) === "undefined" &&
688                service.softRequired === true
689                    ? this.#getText("on")
690                    : this.#getText("off");
691
692            cell2.innerHTML = `<label for="${id}" id="${id}-label" aria-label="${service.name}: ${this.#getText("accept")}"><input ${preChecked} aria-label="${service.name}: ${this.#getText("accept")}" id="${id}" name="${service.key}" type="checkbox" tabindex="0" /><service-status class="service-status" aria-hidden="true">${status}</service-status></label>`;
693
694            /* adjust markup for required services */
695            if (isRequired) {
696                if (!this.#interacted) this.#setRequiredMode();
697
698                cell2.innerHTML = `<label for="${id}" id="${id}-label"><input data-disabled disabled tabindex="0" checked type="checkbox" aria-label="${service.name}: ${this.#getText("accept")}" id="${id}" name="${service.key}" /><service-status class="service-status" aria-hidden="true">${this.#getText("on")}</service-status></label>`;
699
700                cell2.querySelector("service-status").innerText =
701                    this.#getText("on_forced");
702            }
703
704            /* sub consents get their own toggles right below the main one */
705            const consents = Array.isArray(service.consents)
706                ? service.consents
707                : [];
708            if (consents.length) {
709                cell2
710                    .querySelector(`[name="${service.key}"]`)
711                    .setAttribute("data-pc-has-consents", "");
712
713                const consentGroup = this.#customTag({
714                    name: "privacy-consents",
715                    target: group,
716                    attrs: {
717                        "role": "rowgroup",
718                        "aria-label": `${service.name}: ${this.#getText("purpose")}`,
719                    },
720                });
721                /* sub toggles of a required service stay locked like their parent */
722                const consentChecked = isRequired
723                    ? "data-disabled disabled checked"
724                    : preChecked;
725                const consentStatus = isRequired
726                    ? this.#getText("on_forced")
727                    : status;
728
729                for (const consent of consents) {
730                    const consentKey = `${service.key}.${consent.key}`;
731                    const consentId = `${id}.${consent.key}`;
732                    const consentL10n =
733                        consent.l10n?.[this.#lang] || consent.l10n?.en || {};
734                    const consentName = consentL10n.name || consent.key;
735                    const cells = this.#customCells(
736                        consentName,
737                        "",
738                        this.#customRow(
739                            `${service.name}: ${consentName}`,
740                            consentGroup
741                        )
742                    );
743
744                    if (consentL10n.description !== undefined)
745                        this.#customTag({
746                            name: "privacy-consent-hint",
747                            target: cells[0],
748                            innerHTML: consentL10n.description,
749                        });
750
751                    cells[1].innerHTML = `<label for="${consentId}" id="${consentId}-label" aria-label="${consentName}: ${this.#getText("accept")}"><input ${consentChecked} aria-label="${consentName}: ${this.#getText("accept")}" id="${consentId}" name="${consentKey}" data-pc-parent="${service.key}" type="checkbox" tabindex="0" /><service-status class="service-status" aria-hidden="true">${consentStatus}</service-status></label>`;
752                }
753            }
754
755            /* the other rows */
756
757            if (typeof service.provider !== "undefined") {
758                this.#customCells(
759                    this.#getText("provider"),
760                    service.provider,
761                    this.#customRow(
762                        `${this.#getText("service")} ${this.#getText("provider")}: ${service.provider}`,
763                        group
764                    )
765                );
766            }
767            if (typeof service.l10n[this.#lang].description !== "undefined") {
768                this.#customCells(
769                    this.#getText("purpose"),
770                    service.l10n[this.#lang].description,
771                    this.#customRow(
772                        `${this.#getText("service")} ${this.#getText("purpose")}: ${service.l10n[this.#lang].description}`,
773                        group
774                    )
775                );
776            }
777            if (
778                typeof service.l10n[this.#lang].requirement !== "undefined" &&
779                service.required === true
780            ) {
781                this.#customCells(
782                    this.#getText("required"),
783                    service.l10n[this.#lang].requirement,
784                    this.#customRow(
785                        `${this.#getText("service")} ${this.#getText("required")}: ${service.l10n[this.#lang].requirement}`,
786                        group
787                    )
788                );
789            }
790            if (typeof service.l10n[this.#lang].policy !== "undefined") {
791                this.#customCells(
792                    this.#getText("policy"),
793                    `<a href="${service.l10n[this.#lang].policy}" role="link" target="_blank" title="${this.#getText("policy")}" aria-label="${this.#getText("policy")} ${service.name}">${service.name}</a>`,
794                    this.#customRow(
795                        `${this.#getText("service")} ${this.#getText("policy")}: Link`,
796                        group
797                    )
798                );
799            }
800
801            /* listen to checkbox to change label */
802            group
803                .querySelector(`[name="${service.key}"]`)
804                .addEventListener("change", (e) => {
805                    e.target.indeterminate = false;
806                    e.target.nextSibling.innerText = e.target.checked
807                        ? this.#getText("on")
808                        : this.#getText("off");
809
810                    /* the parent toggle carries all of its sub consents along */
811                    for (const consent of consents) {
812                        const consentBox = group.querySelector(
813                            `[name="${service.key}.${consent.key}"]`
814                        );
815                        consentBox.checked = e.target.checked;
816                        consentBox.nextSibling.innerText = e.target.checked
817                            ? this.#getText("on")
818                            : this.#getText("off");
819                    }
820                });
821
822            for (const consent of consents) {
823                group
824                    .querySelector(`[name="${service.key}.${consent.key}"]`)
825                    .addEventListener("change", (e) => {
826                        e.target.nextSibling.innerText = e.target.checked
827                            ? this.#getText("on")
828                            : this.#getText("off");
829
830                        this.#syncConsentGroup(service.key);
831                    });
832            }
833
834            return group;
835        } catch (error) {
836            console.error(
837                "addService failed, because your service object is missing some values!",
838                error
839            );
840            return false;
841        }
842    }
843
844    #bindIframeEvents(doc) {
845        const instance = this;
846        const infoOpen = doc.querySelector(".infoOpen");
847        if (infoOpen) {
848            ["click", "keydown"].forEach((eventType) => {
849                infoOpen.addEventListener(eventType, function (e) {
850                    if (PrivacyControl.isEventClickEnterSpace(e)) {
851                        doc.querySelector(".infoBox").classList.add("open");
852                        doc.querySelector(".infoOpen").classList.add("hide");
853                    }
854                });
855            });
856        }
857
858        const infoClose = doc.querySelector(".infoClose");
859        ["click", "keydown"].forEach((eventType) => {
860            infoClose.addEventListener(eventType, function (e) {
861                if (PrivacyControl.isEventClickEnterSpace(e)) {
862                    doc.querySelector(".infoBox").classList.remove("open");
863                    doc.querySelector(".infoOpen").classList.remove("hide");
864                }
865            });
866        });
867
868        const accept = doc.querySelector(".accept");
869        ["click", "keydown"].forEach((eventType) => {
870            accept.addEventListener(
871                eventType,
872                this.#handleSetterInIframe.bind(this),
873                false
874            );
875        });
876    }
877
878    #loadDep(key, type) {
879        /** this loads the resources of the service */
880        if (this.#loaded[key] === true) return false;
881
882        /** assets alias for src */
883        if (!pCServiceTemplates[key].src && pCServiceTemplates[key].assets)
884            pCServiceTemplates[key].src = pCServiceTemplates[key].assets;
885
886        let scriptLoadIsAsync = false;
887        switch (type) {
888            case "script":
889                try {
890                    if (pCServiceTemplates[key].src !== undefined) {
891                        let srcs = pCServiceTemplates[key].src;
892                        if (typeof srcs != "object") {
893                            srcs = [srcs];
894                        }
895
896                        scriptLoadIsAsync = true;
897                        rIt({
898                            "*": {
899                                assets: srcs,
900                                function: () => {
901                                    PrivacyControl.serviceCallback(
902                                        pCServiceTemplates[key].callback
903                                    );
904                                    this.#dispatchOrQueue("PCDepLoad", {
905                                        key: key,
906                                    });
907                                },
908                            },
909                        });
910                    } else {
911                        const scripts = document.querySelectorAll(
912                            `script[data-script="${key}"], script[data-pc-service="${key}"]`
913                        );
914
915                        if (scripts.length > 0) {
916                            scriptLoadIsAsync = true;
917                            for (let i = 0, l = scripts.length; i < l; i++) {
918                                if (i == l - 1) {
919                                    scripts[i].addEventListener("load", () => {
920                                        PrivacyControl.serviceCallback(
921                                            pCServiceTemplates[key].callback
922                                        );
923                                        this.#dispatchOrQueue("PCDepLoad", {
924                                            key: key,
925                                        });
926                                    });
927                                }
928                                scripts[i].src = scripts[i].dataset.pcAsset;
929                            }
930                        }
931                    }
932
933                    /** check if placeholder exists and hide */
934                    let pCServicePlaceholder = document.querySelectorAll(
935                        'iframe[data-pc-service="' + key + '"]'
936                    );
937                    for (let elm of pCServicePlaceholder) {
938                        elm.classList.add("enabled", "hide");
939                    }
940                } catch (error) {
941                    console.log(
942                        `Could not load script for ${key}, no src defined.`
943                    );
944                }
945
946                break;
947            case "iframe":
948                const iframes = document.querySelectorAll(
949                    `[data-iframe="${key}"], iframe[data-pc-service="${key}"]`
950                );
951
952                for (let iframe of iframes) {
953                    if (iframe.dataset) {
954                        let src = iframe.dataset.pcAsset || iframe.dataset.src;
955
956                        iframe.src = src;
957                        iframe.classList.add("enabled", "pcLoaded");
958                        iframe.classList.remove("pcUnloaded");
959                        iframe.dataset.frameId = Date.now();
960                    }
961                }
962
963                PrivacyControl.serviceCallback(
964                    pCServiceTemplates[key].callback
965                );
966
967                break;
968            case "image":
969                let images = document.querySelectorAll(
970                    `img[data-pc-service="${key}"]`
971                );
972                for (let img of images) {
973                    img.src = img.dataset.pcAsset;
974                    img.classList.add("pcLoaded");
975                    img.classList.remove("pcUnloaded");
976                }
977
978                /** check if placeholder exists and hide */
979                let pCServicePlaceholder = document.querySelectorAll(
980                    `iframe[data-pc-service="${key}"]`
981                );
982                for (let elm of pCServicePlaceholder) {
983                    elm.classList.add("enabled", "hide");
984                }
985
986                PrivacyControl.serviceCallback(
987                    pCServiceTemplates[key].callback
988                );
989
990                break;
991        }
992
993        this.#loaded[key] = true;
994
995        if (type !== "script" || !scriptLoadIsAsync) {
996            this.#dispatchOrQueue("PCDepLoad", { key: key });
997        }
998
999        return true;
1000    }
1001
1002    #unloadDep(key, type) {
1003        if (this.#loaded[key] === false) return false;
1004
1005        let pCServicePlaceholder = document.querySelectorAll(
1006            `iframe[data-pc-service="${key}"]`
1007        );
1008
1009        switch (type) {
1010            case "iframe":
1011                let iframes = document.querySelectorAll(
1012                    `[data-iframe="${key}"], iframe[data-pc-service="${key}"]`
1013                );
1014                for (let iframe of iframes) {
1015                    if (iframe.dataset) {
1016                        iframe.classList.remove("enabled", "pcLoaded");
1017                        iframe.classList.add("pcUnloaded");
1018                    }
1019                }
1020                break;
1021            case "image":
1022                let images = document.querySelectorAll(
1023                    `img[data-pc-service="${key}"]`
1024                );
1025                for (let img of images) {
1026                    img.src = "";
1027                    img.classList.remove("pcLoaded");
1028                    img.classList.add("pcUnloaded");
1029                }
1030
1031                /** check if placeholder exists and show */
1032                for (let elm of pCServicePlaceholder) {
1033                    elm.classList.remove("enabled", "hide");
1034                }
1035                break;
1036            case "script":
1037                /** check if placeholder exists and show */
1038                for (let elm of pCServicePlaceholder) {
1039                    elm.classList.remove("enabled", "hide");
1040                }
1041                break;
1042        }
1043        this.iframePlaceholder(key);
1044        this.#loaded[key] = false;
1045
1046        //callback on disable
1047        if (
1048            pCServiceTemplates[key].callbackOnDisable !== undefined &&
1049            typeof pCServiceTemplates[key].callbackOnDisable == "function"
1050        ) {
1051            pCServiceTemplates[key].callbackOnDisable();
1052        } else if (
1053            pCServiceTemplates[key].callbackOnDisable !== undefined &&
1054            typeof pCServiceTemplates[key].callbackOnDisable == "string"
1055        ) {
1056            window[pCServiceTemplates[key].callbackOnDisable]();
1057        }
1058
1059        this.#dispatchOrQueue("PCDepUnload", { key: key });
1060
1061        return true;
1062    }
1063
1064    #handleFrameLoad(event) {
1065        const frame = event.target;
1066        if (frame.classList.contains("enabled")) return false;
1067
1068        const key = frame?.dataset?.pcService || frame?.dataset?.iframe;
1069        const doc = frame?.contentDocument || frame?.contentWindow?.document;
1070        const serviceObj = pCServiceTemplates[key];
1071
1072        let policy = serviceObj.l10n[this.#lang].policy
1073            ? `<a href="${serviceObj.l10n[this.#lang].policy}" title="${this.#getText("policy")} ${serviceObj.name}" target="_blank">${serviceObj.l10n[this.#lang].policy}</a>`
1074            : "";
1075        let bodyHTML = doc.body.innerHTML;
1076        bodyHTML = bodyHTML.replaceAll("{{policy}}", policy);
1077        bodyHTML = bodyHTML.replaceAll("{{name}}", serviceObj.name);
1078        bodyHTML = bodyHTML.replaceAll(
1079            "{{activate}}",
1080            this.#getText("activate")
1081        );
1082        bodyHTML = bodyHTML.replaceAll(
1083            "{{msg}}",
1084            serviceObj.l10n[this.#lang].description
1085        );
1086        bodyHTML = bodyHTML.replaceAll(
1087            "{{provider}}",
1088            this.#getText("provider")
1089        );
1090        bodyHTML = bodyHTML.replaceAll("{{providerName}}", serviceObj.provider);
1091        bodyHTML = bodyHTML.replaceAll("{{key}}", key);
1092        bodyHTML = bodyHTML.replaceAll(
1093            "{{open_infobox}}",
1094            this.#getText("open_infobox")
1095        );
1096        bodyHTML = bodyHTML.replaceAll(
1097            "{{close_infobox}}",
1098            this.#getText("close_infobox")
1099        );
1100        doc.body.innerHTML = bodyHTML;
1101
1102        doc.head.innerHTML = doc.head.innerHTML.replaceAll(
1103            "{{iframe_title}}",
1104            `${this.#getText("settings")}: ${serviceObj.name}`
1105        );
1106        doc.body.id = key;
1107        doc.body.classList.add("enabled");
1108
1109        doc.querySelector("html").lang = this.#lang;
1110
1111        if (serviceObj.css)
1112            doc.head.innerHTML += `<link rel="stylesheet" href="${serviceObj.css}"></link>`;
1113        this.#bindIframeEvents(doc);
1114    }
1115
1116    iframePlaceholder(key) {
1117        const elms = document.querySelectorAll(
1118            `iframe[data-pc-service="${key}"]:not(.enabled):not(.src), iframe[data-iframe="${key}"]:not(.enabled):not(.src)`
1119        );
1120
1121        for (let i = 0; i < elms.length; i++) {
1122            let frame = elms[i];
1123            if (!frame.dataset.pcListenerBound) {
1124                frame.addEventListener(
1125                    "load",
1126                    this.#handleFrameLoad.bind(this)
1127                );
1128                frame.dataset.pcListenerBound = "true";
1129            }
1130            frame.dataset.lang = document.querySelector("html").lang;
1131            frame.src = `${this.options.placeholder}?key=${key}`;
1132        }
1133        return true;
1134    }
1135
1136    get(key = "") {
1137        /** get status of service from storage */
1138        let storage = this.#getStorage();
1139        if (key == "all") return storage;
1140        return storage[key];
1141    }
1142
1143    set(key = "", status = false) {
1144        /** set a single service or one of its sub consents */
1145        let storage = this.#getStorage();
1146        try {
1147            const resolved = this.#resolveKey(key);
1148            const service = pCServiceTemplates[resolved.parent];
1149
1150            if (!service)
1151                throw new Error(
1152                    `Key "${key}" is missing in the PrivacyControl deps configuration object.`
1153                );
1154
1155            const consentKeys = this.#consentKeys(resolved.parent);
1156            const status_before = storage[resolved.parent];
1157
1158            storage[key] = status;
1159            this.#reflectCheckbox(key, status);
1160
1161            /** a parent carries its sub consents, a sub consent derives its parent */
1162            if (resolved.sub === null) {
1163                for (const consentKey of consentKeys) {
1164                    storage[consentKey] = status;
1165                    this.#reflectCheckbox(consentKey, status);
1166                }
1167            } else {
1168                storage[resolved.parent] = consentKeys.some(
1169                    (consentKey) => storage[consentKey] === true
1170                );
1171                this.#syncConsentGroup(resolved.parent);
1172            }
1173
1174            const parentStatus = storage[resolved.parent];
1175            let type = service.type;
1176
1177            if (parentStatus) this.#loadDep(resolved.parent, type);
1178            else this.#unloadDep(resolved.parent, type);
1179
1180            this.#localStorage.setItem(
1181                this.options.sessionName,
1182                JSON.stringify(storage)
1183            );
1184
1185            //reload only on disable, and only if it was granted before
1186            if (
1187                parentStatus == false &&
1188                status_before === true &&
1189                service.forceReloadOnDisable !== undefined &&
1190                service.forceReloadOnDisable === true
1191            ) {
1192                window.location.reload();
1193            }
1194
1195            /* hide consent buttons on page, sub consents carry their own links */
1196            const grantedKeys = [];
1197            if (parentStatus) grantedKeys.push(resolved.parent);
1198            if (status && resolved.sub === null)
1199                grantedKeys.push(...consentKeys);
1200            else if (status) grantedKeys.push(key);
1201
1202            for (const grantedKey of grantedKeys) {
1203                let buttons = document.querySelectorAll(
1204                    `a[data-privacy-control="${grantedKey}"]`
1205                );
1206                for (let button of buttons)
1207                    button.classList.add("activated", "granted");
1208            }
1209
1210            this.#interacted = true;
1211
1212            const detail = { key: key, status: status };
1213            if (resolved.sub !== null) detail.parent = resolved.parent;
1214            this.#dispatchOrQueue("PCOnSet", detail);
1215
1216            /** consumers listen per key, so derived changes are announced as well */
1217            if (resolved.sub === null) {
1218                for (const consentKey of consentKeys)
1219                    this.#dispatchOrQueue("PCOnSet", {
1220                        key: consentKey,
1221                        status: status,
1222                        parent: resolved.parent,
1223                    });
1224            } else if (parentStatus !== status_before) {
1225                this.#dispatchOrQueue("PCOnSet", {
1226                    key: resolved.parent,
1227                    status: parentStatus,
1228                });
1229            }
1230        } catch (error) {
1231            console.error(error);
1232        }
1233
1234        return storage;
1235    }
1236
1237    addService(service) {
1238        /** if safesurfer badge is there remove it */
1239        if (this.#elms?.safeSurferBadge instanceof HTMLElement) {
1240            this.#elms.safeSurferBadge.remove();
1241            delete this.#elms.safeSurferBadge;
1242        }
1243        /** if not already done and if badge is wanted create it */
1244        if (
1245            this.#elms.pCBadge instanceof HTMLElement == false &&
1246            this.options.badge === true
1247        ) {
1248            this.#elms.pCBadge = this.#createPCBadge();
1249        }
1250
1251        /** show the dialog if not suppressed and if user didn't interact */
1252        if (!this.isDialogVisible) {
1253            const instance = this;
1254            if (this.options.hideDialog === false && !this.#interacted) {
1255                setTimeout(function () {
1256                    instance.openDialog();
1257                }, this.options.noteDelay);
1258            }
1259        }
1260
1261        /** create markup for service */
1262        let serviceObj = false;
1263        /** if service is passed by user */
1264        if (
1265            typeof service === "object" &&
1266            !Array.isArray(service) &&
1267            service !== null
1268        ) {
1269            serviceObj = service;
1270        }
1271        /** if service option is string look for service template */
1272        if (
1273            typeof service === "string" &&
1274            typeof pCServiceTemplates[service] === "object" &&
1275            !Array.isArray(pCServiceTemplates[service])
1276        ) {
1277            serviceObj = pCServiceTemplates[service];
1278        }
1279        /** alias en translations onto the detected lang for services added at runtime */
1280        if (
1281            serviceObj?.l10n &&
1282            serviceObj.l10n[this.#lang] === undefined &&
1283            serviceObj.l10n.en
1284        ) {
1285            serviceObj.l10n[this.#lang] = serviceObj.l10n.en;
1286        }
1287        this.#createServiceMarkup(serviceObj);
1288
1289        /** add current service to service stack if unknown, else merge with users' passed data */
1290        if (typeof pCServiceTemplates[service] !== "object") {
1291            pCServiceTemplates[service.key] = serviceObj;
1292        } else {
1293            pCServiceTemplates = PrivacyControl.#mergeDeps(
1294                pCServiceTemplates,
1295                serviceObj
1296            );
1297        }
1298
1299        /** prepare the rest of the doc, e.g. iframes */
1300        if (serviceObj && serviceObj.type) {
1301            switch (serviceObj.type) {
1302                case "iframe":
1303                case "image":
1304                case "script":
1305                    this.iframePlaceholder(serviceObj.key);
1306                    break;
1307            }
1308        }
1309
1310        this.#deps[serviceObj.key] = serviceObj;
1311
1312        /** load service if consent already given */
1313        const consentKeys = this.#consentKeys(serviceObj.key);
1314        if (consentKeys.length === 0) {
1315            if (this.get(serviceObj.key)) this.set(serviceObj.key, true);
1316        } else if (
1317            this.get(serviceObj.key) &&
1318            consentKeys.every(
1319                (consentKey) => this.get(consentKey) === undefined
1320            )
1321        ) {
1322            /** stored consent predates the sub consents, carry it over to all */
1323            this.set(serviceObj.key, true);
1324        } else {
1325            /** restore each sub consent on its own, the parent follows from them */
1326            for (const consentKey of consentKeys) {
1327                const stored = this.get(consentKey);
1328                if (stored === true) this.set(consentKey, true);
1329                else if (stored === false)
1330                    this.#reflectCheckbox(consentKey, false);
1331            }
1332            this.#syncConsentGroup(serviceObj.key);
1333        }
1334
1335        /** link button event to directly enable a service or one of its purposes */
1336
1337        let links = document.querySelectorAll(
1338            [serviceObj.key, ...consentKeys]
1339                .map((key) =>
1339 `a[data-privacy-control="${key}"]`)
1340                .join(", ")
1341        );
1342        if (links) {
1343            for (let link of links) {
1344                ["click", "keydown"].forEach((eventType) => {
1345                    link.addEventListener(
1346                        eventType,
1347                        this.#handleDirectSetterLink.bind(this)
1348                    );
1349                });
1350            }
1351        }
1352
1353        this.#dispatchOrQueue("PCAddService", serviceObj);
1354    }
1355
1356    openDialog() {
1357        this.isDialogVisible = true;
1358        if (this.#elms.pCBadge) {
1359            this.#elms.pCBadge.tabIndex = "-1";
1360            this.#elms.pCBadge.classList.add("hide");
1361        }
1362        PrivacyControl.notification.open(this.options.id);
1363        document.body.classList.add("privacy-control-is-open");
1364
1365        window.dispatchEvent(new CustomEvent("PCDialogOpen", { detail: this }));
1366    }
1367
1368    openSettings(e, focus = true) {
1369        if (e) {
1370            e.preventDefault();
1371            e.stopPropagation();
1372        }
1373        this.isDialogVisible = true;
1374        this.openDialog();
1375        this.#elms.dialog.classList.add("open");
1376        this.#elms.openSettings.classList.add("hide");
1377        this.#elms.serviceContainer.setAttribute("tabindex", "0");
1378
1379        window.dispatchEvent(
1380            new CustomEvent("PCSettingsOpen", { detail: this })
1381        );
1382    }
1383
1384    closeDialog(e) {
1385        if (e) {
1386            e.preventDefault();
1387            e.stopPropagation();
1388        }
1389        this.isDialogVisible = false;
1390        PrivacyControl.notification.close(this.options.id);
1391        document.body.classList.remove("privacy-control-is-open");
1392        if (this.#elms.pCBadge) {
1393            this.#elms.pCBadge.classList.remove("hide");
1394            this.#elms.pCBadge.tabIndex = "0";
1395        }
1396
1397        window.dispatchEvent(
1398            new CustomEvent("PCDialogClose", { detail: this })
1399        );
1400    }
1401
1402    please(status = true) {
1403        Object.keys(this.#deps).forEach((dep) => {
1404            this.set(dep, status);
1405        });
1406    }
1407}

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.