PageSourceSearch

https://latinprayer.org/wp-content/plugins/dwcookies/assets/js/dwcookies.js?ver=1.26.09.04.02

js latinprayer.org collected 2026-10-02 01:06:58 UTC 38,596 bytes, 911 lines download raw bytes

1/**
2 * DW Cookies — frontend banner, YouTube blocking, consent management
3 *
4 * WHAT:    Shows/hides the consent banner; blocks YouTube iframes until YouTube
5 *          consent is given; saves consent via AJAX; sets the consent cookie.
6 * WHY:     GDPR requires consent before non-essential cookies are set. YouTube
7 *          embeds must not load until the visitor explicitly allows them.
8 * INPUT:   dwcookiesData (wp_localize_script) — ajaxUrl, nonce, categories,
9 *          version, cookieName, cookieDays, necessaryOnly, overlay,
10 *          ytPlaceholderText, ytAllowBtnText.
11 * OUTPUT:  Consent cookie (dwcookies_consent); AJAX POST to server; YouTube
12 *          iframes replaced with/restored from placeholders.
13 * DEPENDS: dwcookies.css, DWCookies_Consent::ajax_save_consent (PHP)
14 */
15
16(function () {
17    'use strict';
18
19    var data     = window.dwcookiesData || {};
20    var COOKIE   = data.cookieName     || 'dwcookies_consent';
21    var DAYS     = data.cookieDays     || 365;
22    var AJAX     = data.ajaxUrl        || '';
23    var NONCE    = data.nonce          || '';
24    var VERSION  = data.version        || '';
25    var NEC_ONLY         = !! data.necessaryOnly;    // informational notice mode
26    var OVERLAY          = !! data.overlay;          // dim page behind banner
27    var POLICY_VERSION   = data.policyVersion || ''; // admin-set policy version string
28    var SHOW_MANAGE_LINK = !! data.showManageLink;   // always-visible manage-cookies button
29    var FORCE_PREVIEW    = !! data.forcePreview;     // admin preview: show banner despite consent
30
31    var YT_PLACEHOLDER_TEXT = data.ytPlaceholderText || 'This video is hosted by YouTube. Playing it will allow YouTube to set cookies on your device.';
32    var YT_ALLOW_BTN_TEXT   = data.ytAllowBtnText    || 'Allow YouTube & Play';
33
34    var X_PLACEHOLDER_TEXT = data.xPlaceholderText || 'This post is hosted by X (Twitter). Showing it will allow X to set cookies on your device.';
35    var X_ALLOW_BTN_TEXT   = data.xAllowBtnText    || 'Allow X & show post';
36    var X_LOADING_TEXT     = data.xLoadingText     || 'Loading post…';
37    var X_WIDGETS_SRC      = data.xWidgetsSrc      || 'https://platform.twitter.com/widgets.js';
38
39    // Settings panel texts
40    var SP_TITLE    = data.spTitle    || 'Your Cookie Settings';
41    var SP_INTRO    = data.spIntro    || 'Manage your cookie preferences below.';
42    var SP_SAVE_BTN = data.spSaveBtn  || 'Save Preferences';
43    var SP_SAVED    = data.spSaved    || 'Your preferences have been saved.';
44    var SP_WITHDRAW = data.spWithdraw || 'Withdraw all consent';
45    var SP_WITHDRAWN= data.spWithdrawn|| 'All optional consent withdrawn.';
46
47    // Category texts
48    var CAT_NEC_LABEL = data.catNecessaryLabel || 'Technically Necessary';
49    var CAT_NEC_DESC  = data.catNecessaryDesc  || 'Required for the website to function. Cannot be disabled.';
50    var CAT_NEC_BADGE = data.catNecessaryBadge || 'Always active';
51    var CAT_YT_LABEL  = data.catYoutubeLabel   || 'YouTube Videos';
52    var CAT_YT_DESC   = data.catYoutubeDesc    || 'Required to play embedded YouTube videos.';
53    var CAT_X_LABEL   = data.catXLabel         || 'X (Twitter) Posts';
54    var CAT_X_DESC    = data.catXDesc          || 'Required to display embedded X (Twitter) posts.';
55
56    // -------------------------------------------------------------------------
57    // Cookie helpers
58    // -------------------------------------------------------------------------
59
60    function setCookie(name, value, days) {
61        var expires = new Date();
62        expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
63        var secure = (location.protocol === 'https:') ? '; Secure' : '';
64        document.cookie = name + '=' + encodeURIComponent(value)
65            + '; expires=' + expires.toUTCString()
66            + '; path=/'
67            + '; SameSite=Lax'
68            + secure;
69    }
70
71    function getCookie(name) {
72        var prefix  = name + '=';
73        var cookies = document.cookie.split(';');
74        for (var i = 0; i < cookies.length; i++) {
75            var c = cookies[i].trim();
76            if (c.indexOf(prefix) === 0) {
77                return decodeURIComponent(c.substring(prefix.length));
78            }
79        }
80        return null;
81    }
82
83    function getConsentData() {
84        var raw = getCookie(COOKIE);
85        if (!raw) return null;
86        try {
87            var parsed = JSON.parse(raw);
88            return (parsed && Array.isArray(parsed.categories)) ? parsed : null;
89        } catch (e) { return null; }
90    }
91
92    function hasValidConsent() {
93        var d = getConsentData();
94        return d !== null && d.categories.length > 0;
95    }
96
97    function hasYouTubeConsent() {
98        var d = getConsentData();
99        return d !== null && d.categories.indexOf('youtube') !== -1;
100    }
101
102    function hasXConsent() {
103        var d = getConsentData();
104        return d !== null && d.categories.indexOf('x') !== -1;
105    }
106
107    // -------------------------------------------------------------------------
108    // Save consent
109    // -------------------------------------------------------------------------
110
111    /**
112     * Save a consent decision — sets the browser cookie and logs to the server.
113     *
114     * @param {string[]} categories  Accepted category slugs (must include 'necessary').
115     * @param {string}   action      One of: granted|withdrawn|updated|acknowledged.
116     *                               Defaults to 'granted' if omitted.
117     */
118    function saveConsent(categories, action) {
119        if (categories.indexOf('necessary') === -1) {
120            categories = ['necessary'].concat(categories);
121        }
122        action = action || 'granted';
123
124        var policyVersion = window.dwcookiesData && window.dwcookiesData.policyVersion
125            ? window.dwcookiesData.policyVersion : '';
126
127        setCookie(COOKIE, JSON.stringify({
128            categories:    categories,
129            version:       VERSION,
130            policyVersion: policyVersion,
131            timestamp:     Math.floor(Date.now() / 1000)
132        }), DAYS);
133
134        // Server log (fire-and-forget — use sendBeacon so it survives page navigation).
135        if (AJAX && NONCE) {
136            var fd = new FormData();
137            fd.append('action',         'dwcookies_save_consent');
138            fd.append('nonce',          NONCE);
139            fd.append('categories',     JSON.stringify(categories));
140            fd.append('consent_action', action);
141            fd.append('policy_version', policyVersion);
142
143            if (navigator.sendBeacon) {
144                var blob = new Blob([new URLSearchParams(fd).toString()], {
145                    type: 'application/x-www-form-urlencoded'
146                });
147                navigator.sendBeacon(AJAX, blob);
148            } else {
149                fetch(AJAX, { method: 'POST', body: fd, keepalive: true })
150                    .catch(function () {});
151            }
152        }
153    }
154
155    // -------------------------------------------------------------------------
156    // Banner / overlay
157    // -------------------------------------------------------------------------
158
159    function showBanner() {
160        var banner = document.getElementById('dwcookies-banner');
161        if (!banner) return;
162        banner.removeAttribute('hidden');
163        if (OVERLAY) showOverlay('banner');
164        // Hide manage link while banner is visible (redundant to show both).
165        hideManageLink();
166    }
167
168    function hideBanner() {
169        var banner = document.getElementById('dwcookies-banner');
170        if (banner) banner.setAttribute('hidden', '');
171        hideOverlay('banner');
172        // Restore manage link after banner is dismissed.
173        if (SHOW_MANAGE_LINK && !NEC_ONLY) showManageLink();
174    }
175
176    /** Shows the persistent manage-cookies button (bottom-right corner). */
177    function showManageLink() {
178        var el = document.getElementById('dwcookies-manage-link');
179        if (el) el.removeAttribute('hidden');
180    }
181
182    /** Hides the persistent manage-cookies button. */
183    function hideManageLink() {
184        var el = document.getElementById('dwcookies-manage-link');
185        if (el) el.setAttribute('hidden', '');
186    }
187
188    function showModal() {
189        var modal = document.getElementById('dwcookies-modal');
190        if (!modal) return;
191        modal.removeAttribute('hidden');
192        showOverlay('modal');
193        var first = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
194        if (first) first.focus();
195    }
196
197    function hideModal() {
198        var modal = document.getElementById('dwcookies-modal');
199        if (modal) modal.setAttribute('hidden', '');
200        hideOverlay('modal');
201    }
202
203    // The overlay element is shared; track who "owns" it so both banner and modal
204    // can request/release it independently.
205    var _overlayOwners = {};
206
207    function showOverlay(owner) {
208        _overlayOwners[owner] = true;
209        var el = document.getElementById('dwcookies-overlay');
210        if (el) el.classList.add('dwcookies-overlay--visible');
211    }
212
213    function hideOverlay(owner) {
214        delete _overlayOwners[owner];
215        // Only hide if nobody else needs it.
216        if (Object.keys(_overlayOwners).length === 0) {
217            var el = document.getElementById('dwcookies-overlay');
218            if (el) el.classList.remove('dwcookies-overlay--visible');
219        }
220    }
221
222    function injectOverlay() {
223        var overlay = document.createElement('div');
224        overlay.id        = 'dwcookies-overlay';
225        overlay.className = 'dwcookies-overlay';
226        // Clicking overlay closes the modal, but does NOT dismiss the banner
227        // (GDPR: user must make an active choice, not click away).
228        overlay.addEventListener('click', function () {
229            hideModal();
230        });
231        document.body.appendChild(overlay);
232    }
233
234    // -------------------------------------------------------------------------
235    // Consent actions
236    // -------------------------------------------------------------------------
237
238    function acceptAll() {
239        var all    = (data.categories || []).slice();
240        var action = hasValidConsent() ? 'updated' : 'granted';
241        saveConsent(all, action);
242        hideModal();
243        hideBanner();
244        restoreYouTubeIframes();
245        restoreXEmbeds();
246    }
247
248    function rejectNonEssential() {
249        var action = hasValidConsent() ? 'updated' : 'granted';
250        saveConsent(['necessary'], action);
251        hideModal();
252        hideBanner();
253    }
254
255    function acknowledge() {
256        // Necessary-only mode: user dismisses the informational notice.
257        saveConsent(['necessary'], 'acknowledged');
258        hideBanner();
259    }
260
261    function savePreferences() {
262        var checks   = document.querySelectorAll('.dwcookies-cat-check:not([disabled]):checked');
263        var selected = ['necessary'];
264        for (var i = 0; i < checks.length; i++) {
265            var val = checks[i].value;
266            if (val && selected.indexOf(val) === -1) {
267                selected.push(val);
268            }
269        }
270        var action = hasValidConsent() ? 'updated' : 'granted';
271        saveConsent(selected, action);
272        hideModal();
273        hideBanner();
274        // Each category takes effect immediately — restore on opt-in, re-block on
275        // withdrawal (GDPR Art. 7(3): withdrawal as easy as consent, applied at once).
276        if (selected.indexOf('youtube') !== -1) {
277            restoreYouTubeIframes();
278        } else {
279            blockYouTubeIframes();
280        }
281        if (selected.indexOf('x') !== -1) {
282            restoreXEmbeds();
283        } else {
284            blockXEmbeds();
285        }
286    }
287
288    // -------------------------------------------------------------------------
289    // YouTube iframe blocking
290    // -------------------------------------------------------------------------
291
292    /**
293     * Replaces all YouTube iframes with placeholder divs.
294     * Each placeholder stores the original iframe's outerHTML as a data attribute
295     * so it can be restored exactly when consent is granted.
296     */
297    function blockYouTubeIframes() {
298        var iframes = document.querySelectorAll('iframe[src*="youtube.com/embed"], iframe[src*="youtube-nocookie.com/embed"]');
299        for (var i = 0; i < iframes.length; i++) {
300            var iframe = iframes[i];
301            // Poster = the page's featured image (passed from PHP). Deliberately
302            // NO fallback to img.youtube.com — fetching the YT thumbnail would
303            // contact Google before consent, defeating the whole point of blocking.
304            var thumb  = data.ytPlaceholderImage || '';
305
306            var ph = document.createElement('div');
307            ph.className            = 'dwcookies-yt-placeholder';
308            ph.dataset.originalHtml = encodeURIComponent(iframe.outerHTML); // URL-encoded; restore decodes
309            ph.style.cssText        = 'position:relative;overflow:hidden;background:#000;'
310                + (iframe.width  ? 'width:'  + iframe.width  + 'px;' : 'width:100%;')
311                + (iframe.height ? 'height:' + iframe.height + 'px;' : 'aspect-ratio:16/9;');
312
313            if (thumb) {
314                var img = document.createElement('img');
315                img.src     = thumb;
316                img.alt     = '';
317                img.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0.4;';
318                ph.appendChild(img);
319            }
320
321            // Notice text
322            var notice = document.createElement('div');
323            notice.className = 'dwcookies-yt-notice';
324            notice.textContent = YT_PLACEHOLDER_TEXT;
325            ph.appendChild(notice);
326
327            // Inline allow button — grants YouTube consent and loads THIS video.
328            var btn = document.createElement('button');
329            btn.type      = 'button';
330            btn.className = 'dwcookies-yt-btn';
331            btn.textContent = YT_ALLOW_BTN_TEXT;
332            btn.addEventListener('click', onAllowClick);
333            btn.dataset.dwcWired = '1';
334            ph.appendChild(btn);
335
336            iframe.parentNode.replaceChild(ph, iframe);
337        }
338    }
339
340    /**
341     * Shared handler for any "Allow YouTube & Play" button — grants YouTube
342     * consent, restores every placeholder to its real iframe, and dismisses the
343     * banner if consent is now complete.
344     */
345    function onAllowClick() {
346        grantYouTubeConsent();
347        restoreYouTubeIframes();
348        if (hasValidConsent()) hideBanner();
349    }
350
351    /**
352     * Wires the "Allow" buttons on SERVER-rendered placeholders (the leak-free
353     * path — PHP emits the placeholder, so there is no live iframe to block).
354     * Idempotent: skips buttons already wired by blockYouTubeIframes().
355     */
356    function wirePlaceholderButtons() {
357        var btns = document.querySelectorAll('.dwcookies-yt-placeholder .dwcookies-yt-btn');
358        for (var i = 0; i < btns.length; i++) {
359            if (btns[i].dataset.dwcWired) continue;
360            btns[i].dataset.dwcWired = '1';
361            btns[i].addEventListener('click', onAllowClick);
362        }
363    }
364
365    /**
366     * Grants YouTube consent (sets the cookie) without dismissing the main banner.
367     * Used when the visitor clicks "Allow YouTube & Play" on a placeholder.
368     */
369    function grantYouTubeConsent() {
370        var existing = getConsentData();
371        var cats     = existing ? existing.categories.slice() : ['necessary'];
372        if (cats.indexOf('youtube') === -1) { cats.push('youtube'); }
373        var action   = existing ? 'updated' : 'granted';
374        saveConsent(cats, action);
375    }
376
377    /**
378     * Restores all YouTube placeholders back to their original iframes.
379     * Called after YouTube consent is granted.
380     */
381    function restoreYouTubeIframes() {
382        var placeholders = document.querySelectorAll('.dwcookies-yt-placeholder');
383        for (var i = 0; i < placeholders.length; i++) {
384            var ph   = placeholders[i];
385            var raw  = ph.dataset.originalHtml;
386            if (!raw) continue;
387            // Stored URL-encoded (by PHP rawurlencode or the JS fallback's
388            // encodeURIComponent) so the iframe markup survives an HTML attribute.
389            var html = decodeURIComponent(raw);
390            var tmp = document.createElement('div');
391            tmp.innerHTML = html;
392            var restored = tmp.firstChild;
393            if (restored) {
394                ph.parentNode.replaceChild(restored, ph);
395            }
396        }
397    }
398
399    // -------------------------------------------------------------------------
400    // X (Twitter) embed blocking
401    // -------------------------------------------------------------------------
402
403    /**
404     * Fallback blocker for any LIVE X embed that bypassed the server filter (e.g.
405     * a manually pasted full embed). Replaces each `blockquote.twitter-tweet` with a
406     * placeholder storing its original HTML, and removes X's widget script so nothing
407     * contacts X. The server filter (filter_x_embeds) already does this for c
407ontent;
408     * this mirrors blockYouTubeIframes() for the widget-HTML path.
409     */
410    function blockXEmbeds() {
411        // Remove any X widget script so it cannot (re)hydrate blockquotes.
412        var scripts = document.querySelectorAll('script[src*="platform.twitter.com/widgets.js"]');
413        for (var s = 0; s < scripts.length; s++) {
414            if (scripts[s].parentNode) scripts[s].parentNode.removeChild(scripts[s]);
415        }
416
417        var quotes = document.querySelectorAll('blockquote.twitter-tweet');
418        for (var i = 0; i < quotes.length; i++) {
419            var quote = quotes[i];
420            // Skip a blockquote already inside a placeholder (defensive).
421            if (quote.closest && quote.closest('.dwcookies-x-placeholder')) continue;
422
423            var ph = document.createElement('div');
424            ph.className            = 'dwcookies-x-placeholder';
425            ph.dataset.originalHtml = encodeURIComponent(quote.outerHTML); // URL-encoded; restore decodes
426
427            var notice = document.createElement('div');
428            notice.className   = 'dwcookies-yt-notice';
429            notice.textContent = X_PLACEHOLDER_TEXT;
430            ph.appendChild(notice);
431
432            var btn = document.createElement('button');
433            btn.type        = 'button';
434            btn.className    = 'dwcookies-yt-btn dwcookies-x-btn';
435            btn.textContent  = X_ALLOW_BTN_TEXT;
436            btn.addEventListener('click', onAllowXClick);
437            btn.dataset.dwcWired = '1';
438            ph.appendChild(btn);
439
440            quote.parentNode.replaceChild(ph, quote);
441        }
442    }
443
444    /**
445     * Wires the "Allow" buttons on SERVER-rendered X placeholders (the leak-free
446     * path — PHP emits the placeholder, so there is no live blockquote to block).
447     * Idempotent: skips buttons already wired by blockXEmbeds().
448     */
449    function wireXPlaceholderButtons() {
450        var btns = document.querySelectorAll('.dwcookies-x-placeholder .dwcookies-x-btn');
451        for (var i = 0; i < btns.length; i++) {
452            if (btns[i].dataset.dwcWired) continue;
453            btns[i].dataset.dwcWired = '1';
454            btns[i].addEventListener('click', onAllowXClick);
455        }
456    }
457
458    /**
459     * Shared handler for any "Allow X & show post" button — grants X consent,
460     * restores every placeholder to its real blockquote, loads the widget script,
461     * and dismisses the banner if consent is now complete.
462     */
463    function onAllowXClick() {
464        grantXConsent();
465        restoreXEmbeds();
466        if (hasValidConsent()) hideBanner();
467    }
468
469    /**
470     * Grants X consent (sets the cookie) without dismissing the main banner.
471     * Used when the visitor clicks "Allow X & show post" on a placeholder.
472     */
473    function grantXConsent() {
474        var existing = getConsentData();
475        var cats     = existing ? existing.categories.slice() : ['necessary'];
476        if (cats.indexOf('x') === -1) { cats.push('x'); }
477        var action   = existing ? 'updated' : 'granted';
478        saveConsent(cats, action);
479    }
480
481    /**
482     * Restores all X placeholders. Instead of dumping the raw (unstyled) blockquote
483     * and letting it pop, each placeholder becomes a spinner card with the real tweet
484     * hydrating INVISIBLY behind it; once X's widget finishes rendering, the spinner
485     * fades out and the tweet fades in. No flash of unstyled blockquote, no jump.
486     */
487    function restoreXEmbeds() {
488        var placeholders = document.querySelectorAll('.dwcookies-x-placeholder');
489        for (var i = 0; i < placeholders.length; i++) {
490            var ph  = placeholders[i];
491            var raw = ph.dataset.originalHtml;
492            if (!raw) continue;
493            var html = decodeURIComponent(raw);
494
495            var wrap = document.createElement('div');
496            wrap.className = 'dwcookies-x-embed';
497
498            var loading = document.createElement('div');
499            loading.className = 'dwcookies-x-loading';
500            loading.setAttribute('role', 'status');
501            var spin = document.createElement('span');
502            spin.className = 'dwcookies-x-spinner';
503            spin.setAttribute('aria-hidden', 'true');
504            var ltext = document.createElement('span');
505            ltext.className   = 'dwcookies-x-loading-text';
506            ltext.textContent = X_LOADING_TEXT;
507            loading.appendChild(spin);
508            loading.appendChild(ltext);
509
510            var host = document.createElement('div');
511            host.className = 'dwcookies-x-host'; // starts opacity:0 (CSS); tweet hydrates here
512            host.innerHTML = html;
513
514            wrap.appendChild(loading);
515            wrap.appendChild(host);
516            ph.parentNode.replaceChild(wrap, ph);
517
518            hydrateXHost(wrap, host, loading);
519        }
520    }
521
522    /**
523     * Loads X's widget into one host, then crossfades from spinner to tweet. Always
524     * reveals eventually — even if the widget script is blocked/offline — so a visitor
525     * never gets stuck on a spinner.
526     */
527    function hydrateXHost(wrap, host, loading) {
528        var revealed = false;
529        function reveal() {
530            if (revealed) return;
531            revealed = true;
532            wrap.classList.add('dwcookies-x-ready'); // CSS: fade host in, min-height→0
533            setTimeout(function () {
534                if (loading && loading.parentNode) loading.parentNode.removeChild(loading);
535            }, 450);
536        }
537        loadTwitterWidgets(function (twttr) {
538            try { twttr.widgets.load(host); } catch (e) {}
539            // Reveal only once the real tweet IFRAME exists in this host — NOT when
540            // widgets.load()'s promise resolves. That promise fires a couple of frames
541            // before the blockquote is swapped for the iframe, briefly showing the raw
542            // blockquote (theme red quote-border). Poll for the iframe instead.
543            var waited = 0;
544            (function check() {
545                if (host.querySelector('iframe')) { reveal(); return; }
546                waited += 60;
547                if (waited > 6000) { reveal(); return; } // safety: reveal anyway
548                setTimeout(check, 60);
549            })();
550        }, reveal); // onFail (script blocked) → reveal so the spinner doesn't hang
551    }
552
553    /**
554     * Ensures X's official widget script is present (injected once — NEVER before X
555     * consent, keeping the block leak-free), then calls onReady(twttr) when the API
556     * is available. onFail() fires if the script can't load within the timeout.
557     */
558    function loadTwitterWidgets(onReady, onFail) {
559        if (!document.getElementById('dwcookies-twitter-wjs')) {
560            var js = document.createElement('script');
561            js.id    = 'dwcookies-twitter-wjs';
562            js.async = true;
563            js.src   = X_WIDGETS_SRC;
564            (document.head || document.body).appendChild(js);
565        }
566        var waited = 0;
567        (function ready() {
568            if (window.twttr && window.twttr.widgets && typeof window.twttr.widgets.load === 'function') {
569                if (typeof onReady === 'function') onReady(window.twttr);
570                return;
571            }
572            waited += 100;
573            if (waited > 8000) { if (typeof onFail === 'function') onFail(); return; }
574            setTimeout(ready, 100);
575        })();
576    }
577
578    // -------------------------------------------------------------------------
579    // Keyboard accessibility (focus trap in modal)
580    // -------------------------------------------------------------------------
581
582    function trapFocus(e) {
583        var modal = document.getElementById('dwcookies-modal');
584        if (!modal || modal.hasAttribute('hidden')) return;
585
586        var focusable = modal.querySelectorAll(
587            'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
588        );
589        var first = focusable[0];
590        var last  = focusable[focusable.length - 1];
591
592        if (e.key === 'Tab') {
593            if (e.shiftKey) {
594                if (document.activeElement === first) { e.preventDefault(); last.focus(); }
595            } else {
596                if (document.activeElement === last) { e.preventDefault(); first.focus(); }
597            }
598        }
599        if (e.key === 'Escape') hideModal();
600    }
601
602    // -------------------------------------------------------------------------
603    // Settings panel (shortcode: [dwcookies_settings])
604    // -------------------------------------------------------------------------
605
606    /**
607     * Builds and activates the settings panel inside #dwcookies-settings-panel.
608     * Shows current consent state; lets the visitor toggle categories and save.
609     */
610    function initSettingsPanel() {
611        var panel = document.getElementById('dwcookies-settings-panel');
612        if (!panel) return;
613
614        var consentData   = getConsentData();
615        var hasYT         = consentData && consentData.categories.indexOf('youtube') !== -1;
616        var hasX          = consentData && consentData.categories.indexOf('x') !== -1;
617        var hasAnyConsent = consentData !== null;
618
619        // Build panel HTML
620        panel.innerHTML =
621            '<h3 class="dwcookies-sp__title">' + esc(SP_TITLE) + '</h3>' +
622            '<p class="dwcookies-sp__intro">' + esc(SP_INTRO) + '</p>' +
623
624            '<ul class="dwcookies-sp__list">' +
625
626              // Necessary: always on, disabled
627              '<li class="dwcookies-sp__item">' +
628                '<label class="dwcookies-sp__label">' +
629                  '<input type="checkbox" checked disabled> ' +
630                  '<span>' + esc(CAT_NEC_LABEL) + '</span>' +
631                  '<span class="dwcookies-sp__badge">' + esc(CAT_NEC_BADGE) + '</span>' +
632                '</label>' +
633                '<p class="dwcookies-sp__desc">' + esc(CAT_NEC_DESC) + '</p>' +
634              '</li>' +
635
636              // YouTube: toggleable
637              '<li class="dwcookies-sp__item">' +
638                '<label class="dwcookies-sp__label">' +
639                  '<input type="checkbox" id="dwcookies-sp-youtube"' + (hasYT ? ' checked' : '') + '> ' +
640                  '<span>' + esc(CAT_YT_LABEL) + '</span>' +
641                '</label>' +
642                '<p class="dwcookies-sp__desc">' + esc(CAT_YT_DESC) + '</p>' +
643              '</li>' +
644
645              // X (Twitter): toggleable
646              '<li class="dwcookies-sp__item">' +
647                '<label class="dwcookies-sp__label">' +
648                  '<input type="checkbox" id="dwcookies-sp-x"' + (hasX ? ' checked' : '') + '> ' +
649                  '<span>' + esc(CAT_X_LABEL) + '</span>' +
650                '</label>' +
651                '<p class="dwcookies-sp__desc">' + esc(CAT_X_DESC) + '</p>' +
652              '</li>' +
653
654            '</ul>' +
655
656            '<div class="dwcookies-sp__actions">' +
657              '<button type="button" id="dwcookies-sp-save" class="dwcookies-btn dwcookies-btn--primary">' + esc(SP_SAVE_BTN) + '</button>' +
658              (hasAnyConsent
659                ? ' <button type="button" id="dwcookies-sp-withdraw" class="dwcookies-btn dwcookies-btn--ghost">' + esc(SP_WITHDRAW) + '</button>'
660                : '') +
661            '</div>' +
662            '<p class="dwcookies-sp__status" id="dwcookies-sp-status" aria-live="polite"></p>';
663
664        // Save button
665        document.getElementById('dwcookies-sp-save').addEventListener('click', function () {
666            var ytCheck  = document.getElementById('dwcookies-sp-youtube');
667            var xCheck   = document.getElementById('dwcookies-sp-x');
668            var selected = ['necessary'];
669            if (ytCheck && ytCheck.checked) selected.push('youtube');
670            if (xCheck  && xCheck.checked)  selected.push('x');
671
672            saveConsent(selected, hasValidConsent() ? 'updated' : 'granted');
673
674            // Update embeds immediately, per category.
675            if (selected.indexOf('youtube') !== -1) {
676                restoreYouTubeIframes();
677            } else {
678                blockYouTubeIframes();
679            }
680            if (selected.indexOf('x') !== -1) {
681                restoreXEmbeds();
682            } else {
683                blockXEmbeds();
684            }
685
686            setStatus(SP_SAVED);
687
688            // Refresh the "Withdraw" button visibility
689            var withdrawBtn = document.getElementById('dwcookies-sp-withdraw');
690            if (!withdrawBtn) {
691                var actions = panel.querySelector('.dwcookies-sp__actions');
692                if (actions) {
693                    var btn = document.createElement('button');
694                    btn.type      = 'button';
695                    btn.id        = 'dwcookies-sp-withdraw';
696                    btn.className = 'dwcookies-btn dwcookies-btn--ghost';
697                    btn.textContent = SP_WITHDRAW;
698                    actions.appendChild(btn);
699                    wireWithdraw(btn);
700                }
701            }
702        });
703
704        // Withdraw button (if present)
705        var withdrawBtn = document.getElementById('dwcookies-sp-withdraw');
706        if (withdrawBtn) wireWithdraw(withdrawBtn);
707    }
708
709    function wireWithdraw(btn) {
710        btn.addEventListener('click', function () {
711            saveConsent(['necessary'], 'withdrawn');
712            blockYouTubeIframes();
713            blockXEmbeds();
714
715            // Uncheck the optional toggles
716            var ytCheck = document.getElementById('dwcookies-sp-youtube');
717            if (ytCheck) ytCheck.checked = false;
718            var xCheck = document.getElementById('dwcookies-sp-x');
719            if (xCheck) xCheck.checked = false;
720
721            setStatus(SP_WITHDRAWN);
722
723            // Remove the withdraw button (no consent left to withdraw)
724            btn.parentNode.removeChild(btn);
725        });
726    }
727
728    function setStatus(msg) {
729        var el = document.getElementById('dwcookies-sp-status');
730        if (!el) return;
731        el.textContent = msg;
732        // Clear after 4 seconds
733        setTimeout(function () { el.textContent = ''; }, 4000);
734    }
735
736    /** Minimal HTML-escape for JS-generated content. */
737    function esc(str) {
738        return String(str)
739            .replace(/&/g, '&amp;')
740            .replace(/</g, '&lt;')
741            .replace(/>/g, '&gt;')
742            .replace(/"/g, '&quot;');
743    }
744
745    // -------------------------------------------------------------------------
746    // Init
747    // -------------------------------------------------------------------------
748
749    /**
750     * Returns true if the visitor's stored consent cookie is still valid for
751     * the current policy version. A non-empty POLICY_VERSION that differs from
752     * what's in the cookie means the policy changed — the visitor must re-consent.
753     */
754    function isPolicyVersionCurrent() {
755        if (!POLICY_VERSION) return true; // no policy version configured — always current
756        var d = getConsentData();
757        if (!d) return false;
758        return (d.policyVersion || '') === POLICY_VERSION;
759    }
760
761    /**
762     * Screenshot bypass: a deliberately low-profile URL param (?dwc_nobanner=1)
763     * suppresses the banner UI so clean screenshots can be taken. PURELY COSMETIC
764     * — it grants and changes NO consent (YouTube stays blocked until a real
765     * Accept). Client-side only, so it never pollutes the page cache.
766     */
767    function isScreenshotBypass() {
768        try { return /[?&]dwc_nobanner=1(?:&|$)/.test(window.location.search); }
769        catch (e) { return false; }
770    }
771
772    function init() {
773        injectOverlay();
774
775        // Settings panel: wire up on any page that has the shortcode.
776        initSettingsPanel();
777
778        // If the policy version changed since the visitor last consented, clear
779        // the stale cookie so the banner shows again (PHP already does this server-
780        // side to suppress the banner HTML, but clear client-side too for consistency).
781        if (hasValidConsent() && !isPolicyVersionCurrent()) {
782            setCookie(COOKIE, '', -1); // expire immediately
783        }
784
785        // YouTube embeds arrive as inert placeholders (PHP the_content filter).
786        // If the visitor already consented, swap them back to live iframes now;
787        // otherwise wire their Allow buttons. blockYouTubeIframes() is a fallback
788        // for any LIVE iframe that bypassed the server filter (e.g. widget HTML).
789        if (!hasYouTubeConsent() || !isPolicyVersionCurrent()) {
790            blockYouTubeIframes();
791            wirePlaceholderButtons();
792        } else {
793            restoreYouTubeIframes();
794        }
795
796        // X (Twitter) embeds arrive as inert placeholders (PHP the_content filter),
797        // handled exactly like YouTube: restore-if-consented, else wire the Allow
798        // buttons. blockXEmbeds() is the fallback for any live blockquote that
799        // bypassed the server filter (e.g. manually pasted widget HTML).
800        if (!hasXConsent() || !isPolicyVersionCurrent()) {
801            blockXEmbeds();
802            wireXPlaceholderButtons();
803        } else {
804            restoreXEmbeds();
805        }
806
807        // Wire modal and manage link regardless of consent state — the modal
808        // is always in the DOM (rendered by render_persistent_elements) and the
809        // manage link must work even after consent has been given.
810        if (!NEC_ONLY) {
811            wireModal();
812            wireManageLink();
813            wireOpenLinks();
814        }
815
816        // If the visitor already has current valid consent, show manage link and stop.
817        // (In admin preview mode we skip this so the banner always shows.)
818        if (!FORCE_PREVIEW && hasValidConsent() && isPolicyVersionCurrent()) {
819            if (SHOW_MANAGE_LINK && !NEC_ONLY) showManageLink();
820            return;
821        }
822
823        // Screenshot bypass (dev tool): suppress the banner UI for a clean shot.
824        if (isScreenshotBypass()) { hideBanner(); return; }
825
826        showBanner(); // showBanner() also hides the manage link
827
828        if (NEC_ONLY) {
829            // Informational notice mode: just a single dismiss button.
830            var btnAck = document.getElementById('dwcookies-acknowledge');
831            if (btnAck) btnAck.addEventListener('click', acknowledge);
832            return; // no modal wiring needed
833        }
834
835        // Full consent mode: wire banner buttons.
836        var btnAcceptAll = document.getElementById('dwcookies-accept-all');
837        if (btnAcceptAll) btnAcceptAll.addEventListener('click', acceptAll);
838
839        var btnReject = document.getElementById('dwcookies-reject');
840        if (btnReject) btnReject.addEventListener('click', rejectNonEssential);
841
842        var btnCustomize = document.getElementById('dwcookies-customize-open');
843        if (btnCustomize) btnCustomize.addEventListener('click', showModal);
844    }
845
846    /**
847     * Wires the preferences modal buttons (save, cancel) and keyboard trap.
848     * Called unconditionally on every page in full-consent mode so the modal
849     * works both when opened from the banner and from the manage link.
850     */
851    function wireModal() {
852        var btnSave = document.getElementById('dwcookies-save-prefs');
853        if (btnSave) btnSave.addEventListener('click', savePreferences);
854
855        var btnCancel = document.getElementById('dwcookies-modal-cancel');
856        if (btnCancel) btnCancel.addEventListener('click', hideModal);
857
858        document.addEventListener('keydown', trapFocus);
859    }
860
861    /**
862     * Wires any opt-in "open cookie preferences" element to show the modal.
863     * Matches (via event delegation, so it also covers WordPress menu items — where
864     * the CSS class lands on the <li> and the click originates on the <a> inside —
865     * and any element added after load):
866     *   - `.dwcookies-open`            — CSS class on any element or its ancestor
867     *   - `a[href="#cookie-settings"]` — soft anchor convention for menu/footer links
868     * Works regardless of the "persistent manage link" admin toggle, so a theme or
869     * menu can host the link even when the floating bottom-right button is off.
870     */
871    function wireOpenLinks() {
872        document.addEventListener('click', function (e) {
873            if (!e.target || !e.target.closest) return;
874            var trigger = e.target.closest('.dwcookies-open, a[href="#cookie-settings"]');
875            if (!trigger) return;
876            e.preventDefault();
877            syncModalChecks();
878            showModal();
879        });
880    }
881
882    /** Syncs the modal's optional-category checkboxes to the visitor's current consent. */
883    function syncModalChecks() {
884        var ytCheck = document.getElementById('dwcookies-cat-youtube');
885        if (ytCheck) ytCheck.checked = hasYouTubeConsent();
886        var xCheck = document.getElementById('dwcookies-cat-x');
887        if (xCheck) xCheck.checked = hasXConsent();
888    }
889
890    /**
891     * Wires the persistent manage-cookies button to open the modal.
892     * Also syncs the YouTube checkbox state when the modal opens via this path.
893     */
894    function wireManageLink() {
895        var btn = document.getElementById('dwcookies-manage-link');
896        if (!btn || !SHOW_MANAGE_LINK) return;
897
898        btn.addEventListener('click', function () {
899            // Sync optional-category checkboxes to current consent before opening.
900            syncModalChecks();
901            showModal();
902        });
903    }
904
905    if (document.readyState === 'loading') {
906        document.addEventListener('DOMContentLoaded', init);
907    } else {
908        init();
909    }
910
911})();

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.