PageSourceSearch

https://www.dichtbijdebijbel.nl/lib/Cupella/Modules/Cookie/Resources/js/cookie.js?ts=1770908628

js dichtbijdebijbel.nl collected 2026-09-27 14:38:15 UTC 29,066 bytes, 1,024 lines download raw bytes

1/**
2 * MX Cookie Consent Manager
3 * GDPR v4 & Google Consent Mode v2 Compatible
4 *
5 * @version 2.1.0
6 * @license MIT
7 * @author MaxiCMS
8 *
9 * NOTE: Google Consent Mode v2 defaults are set inline in the template
10 * to ensure they load before gtag.js. This script handles the UI and
11 * consent updates.
12 *
13 * Features:
14 * - Google Consent Mode v2 (GCM v2) compliant
15 * - GDPR v4 compliant with granular consent
16 * - Cloudflare Zaraz integration
17 * - Conditional script/iframe loading
18 * - Cookie + localStorage fallback
19 * - Accessible (ARIA, keyboard navigation)
20 * - No dependencies (vanilla JS)
21 */
22
23'use strict';
24
25// ============================================================================
26// Cookie Consent Manager
27// ============================================================================
28
29/**
30 * @typedef {Object} ConsentState
31 * @property {boolean} necessary - Always true, required for site functionality
32 * @property {boolean} functional - Functional cookies (preferences, language)
33 * @property {boolean} statistics - Analytics cookies (Google Analytics, etc.)
34 * @property {boolean} marketing - Marketing cookies (ads, remarketing)
35 */
36
37/**
38 * @typedef {Object} ManagerConfig
39 * @property {boolean} forceModal - Force modal display instead of banner
40 * @property {string|null} cfStatistics - Cloudflare Zaraz statistics event name
41 * @property {string|null} cfMarketing - Cloudflare Zaraz marketing event name
42 */
43
44/**
45 * Cookie Consent Manager Class
46 * Handles GDPR-compliant cookie consent with Google Consent Mode v2 integration
47 */
48class CookieConsentManager {
49  // ===========================================================================
50  // Static Configuration
51  // ===========================================================================
52
53  /**
54   * Default configuration values
55   * @type {Object}
56   */
57  static CONFIG = Object.freeze({
58    STORAGE_KEY: 'mx_cookie_consent_v2',
59    STORAGE_DAYS: 365,
60    ANIMATION_DELAY_MS: 50,
61    TRANSITION_DURATION_MS: 300,
62    VERSION: '2.1.0'
63  });
64
65  /**
66   * Default consent state (GDPR compliant - deny all optional)
67   * @type {ConsentState}
68   */
69  static DEFAULT_CONSENT = Object.freeze({
70    necessary: true,
71    functional: false,
72    statistics: false,
73    marketing: false
74  });
75
76  /**
77   * Full consent state (accept all)
78   * @type {ConsentState}
79   */
80  static FULL_CONSENT = Object.freeze({
81    necessary: true,
82    functional: true,
83    statistics: true,
84    marketing: true
85  });
86
87  /**
88   * DOM element IDs
89   * @type {Object}
90   */
91  static ELEMENT_IDS = Object.freeze({
92    CONTAINER: 'mx_cookie_container',
93    BANNER: 'mx_cookie_banner',
94    MODAL: 'mx_cookie_modal',
95    RESET: 'mx_cookie_reset',
96    ACCEPT: 'mx_cookie_accept',
97    REJECT: 'mx_cookie_reject',
98    SETTINGS: 'mx_cookie_settings',
99    CLOSE: 'mx_cookie_close',
100    ACCEPT_ALL: 'mx_cookie_accept_all',
101    REJECT_ALL: 'mx_cookie_reject_all',
102    SAVE: 'mx_cookie_save',
103    FUNCTIONAL: 'mx_cookie_functional',
104    STATISTICS: 'mx_cookie_statistics',
105    MARKETING: 'mx_cookie_marketing'
106  });
107
108  /**
109   * Legacy DOM element IDs (for backwards compatibility with old template)
110   * @type {Object}
111   */
112  static LEGACY_ELEMENT_IDS = Object.freeze({
113    CONTAINER: 'mx_cookie',
114    ALLOW: 'mx_cookie_allow',
115    DISALLOW: 'mx_cookie_disallow'
116  });
117
118  /**
119   * CSS classes used by the manager
120   * @type {Object}
121   */
122  static CSS_CLASSES = Object.freeze({
123    SHOW: 'mx_show',
124    WARNING: 'mx_no_cookies'
125  });
126
127  // ===========================================================================
128  // Instance Properties
129  // ===========================================================================
130
131  /** @type {Object<string, HTMLElement|null>} */
132  #elements = {};
133
134  /** @type {ManagerConfig} */
135  #config = {};
136
137  /** @type {boolean} */
138  #useCookies = true;
139
140  /** @type {boolean} */
141  #initialized = false;
142
143  // ===========================================================================
144  // Constructor & Initialization
145  // ===========================================================================
146
147  /**
148   * Create a new CookieConsentManager instance
149   */
150  constructor() {
151    this.#useCookies = this.#detectCookieSupport();
152    this.#initialize();
153  }
154
155  /**
156   * Initialize the consent manager
157   * @private
158   */
159  #initialize() {
160    const container = document.getElementById(CookieConsentManager.ELEMENT_IDS.CONTAINER);
161
162    if (!container) {
163      // Try legacy mode for backwards compatibility with old template
164      if (this.#initializeLegacy()) {
165        return; // Legacy mode active
166      }
167      console.warn('CookieConsent: Container element not found');
168      return;
169    }
170
171    try {
172      // Migrate old consent format if needed (before applying stored consent)
173      this.#migrateOldConsent();
174      this.#removeICheck();
175      this.#cacheElements();
176      this.#loadConfiguration(container);
177      this.#bindEventListeners();
178      this.#applyStoredConsent();
179      this.#updateUIState();
180      this.#exposePublicAPI();
181      this.#initialized = true;
182    } catch (error) {
183      console.error('CookieConsent: Initialization failed', error);
184    }
185  }
186
187  /**
188   * Initialize legacy mode for backwards compatibility with old Cookie module template
189   * Supports old element IDs: #mx_cookie, #mx_cookie_allow, #mx_cookie_disallow
190   * @returns {boolean} True if legacy mode was activated, false otherwise
191   * @private
192   */
193  #initializeLegacy() {
194    const legacyContainer = document.getElementById(CookieConsentManager.LEGACY_ELEMENT_IDS.CONTAINER);
195    const allowBtn = document.getElementById(CookieConsentManager.LEGACY_ELEMENT_IDS.ALLOW);
196    const disallowBtn = document.getElementById(CookieConsentManager.LEGACY_ELEMENT_IDS.DISALLOW);
197
198    if (!legacyContainer) {
199      return false;
200    }
201
202    // Migrate old consent if needed
203    this.#migrateOldConsent();
204
205    // Check if consent already given
206    const consent = this.getConsent();
207    if (consent) {
208      legacyContainer.style.display = 'none';
209      document.documentElement.classList.remove('mx_cookie');
210      this.#exposePublicAPI();
211      this.#initialized = true;
212      return true;
213    }
214
215    // Bind legacy button handlers
216    if (allowBtn) {
217      allowBtn.addEventListener('click', (e) => {
218        e.preventDefault();
219        this.#setStorage({ ...CookieConsentManager.FULL_CONSENT });
220        legacyContainer.style.display = 'none';
221        document.documentElement.classList.remove('mx_cookie');
222        window.location.reload();
223      });
224    }
225
226    if (disallowBtn) {
227      disallowBtn.addEventListener('click', (e) => {
228        e.preventDefault();
229        this.#setStorage({ ...CookieConsentManager.DEFAULT_CONSENT });
230        legacyContainer.style.display = 'none';
231        document.documentElement.classList.remove('mx_cookie');
232        window.location.reload();
233      });
234    }
235
236    this.#exposePublicAPI();
237    this.#initialized = true;
238    return true;
239  }
240
241  /**
242   * Remove iCheck styling from cookie toggle checkboxes
243   * iCheck library wraps checkboxes with custom elements that interfere with our toggle switches
244   * @private
245   */
246  #removeICheck() {
247    const container = document.getElementById(CookieConsentManager.ELEMENT_IDS.CONTAINER);
248    if (!container) return;
249
250    // Find all checkboxes in the cookie container
251    const checkboxes = container.querySelectorAll('input[type="checkbox"]');
252    
253    checkboxes.forEach(checkbox => {
254      // Remove iCheck helper elements
255      const parent = checkbox.parentElement;
256      if (parent) {
257        const iCheckHelper = parent.querySelector('.iCheck-helper');
258        if (iCheckHelper) {
259          iCheckHelper.remove();
260        }
261      }
262      
263      // Reset inline styles that iCheck may have added
264      checkbox.style.position = '';
265      checkbox.style.opacity = '';
266      
267      // If iCheck jQuery plugin is available, destroy it
268      if (typeof jQuery !== 'undefined' && jQuery.fn.iCheck) {
269        try {
270          jQuery(checkbox).iCheck('destroy');
271        } catch (e) {
272          // iCheck destroy may fail silently
273        }
274      }
275    });
276  }
277
278  /**
279   * Cache DOM elements for performance
280   * @private
281   */
282  #cacheElements() {
283    const ids = CookieConsentManager.ELEMENT_IDS;
284
285    Object.entries(ids).forEach(([key, id]) => {
286      this.#elements[key.toLowerCase()] = document.getElementById(id);
287    });
288
289    // Cache warning placeholder elements
290    this.#elements.warnings = document.querySelectorAll(
291      `.${CookieConsentManager.CSS_CLASSES.WARNING}`
292    );
293  }
294
295  /**
296   * Load configuration from container data attributes
297   * @param {HTMLElement} container - Container element
298   * @private
299   */
300  #loadConfiguration(container) {
301    this.#config = {
302      forceModal: container.dataset.modal === 'true',
303      cfStatistics: container.dataset.cfStatistics || null,
304      cfMarketing: container.dataset.cfMarketing || null
305    };
306  }
307
308  // ===========================================================================
309  // Storage Management
310  // ===========================================================================
311
312  /**
313   * Detect if cookies are supported in the browser
314   * @returns {boolean} True if cookies are supported
315   * @private
316   */
317  #detectCookieSupport() {
318    try {
319      const testKey = '__mx_cookie_test__';
320      document.cookie = `${testKey}=1;SameSite=Lax`;
321      const supported = document.cookie.includes(`${testKey}=1`);
322      document.cookie = `${testKey}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;SameSite=Lax`;
323      return supported;
324    } catch {
325      return false;
326    }
327  }
328
329  /**
330   * Legacy cookie name used by PHP/Twig for cookie.allowed
331   * @type {string}
332   */
333  static LEGACY_ALLOWED_KEY = 'mx_cookie_allowed';
334
335  /**
336   * Store consent data in cookie or localStorage
337   * @param {ConsentState} value - Consent state to store
338   * @private
339   */
340  #setStorage(value) {
341    const key = CookieConsentManager.CONFIG.STORAGE_KEY;
342    const data = {
343      ...value,
344      version: CookieConsentManager.CONFIG.VERSION,
345      timestamp: Date.now()
346    };
347
348    try {
349      const encoded = encodeURIComponent(JSON.stringify(data));
350
351      if (this.#useCookies) {
352        const days = CookieConsentManager.CONFIG.STORAGE_DAYS;
353        const expires = new Date(Date.now() + days * 86400000).toUTCString();
354        const secure = location.protocol === 'https:' ? ';Secure' : '';
355        document.cookie = `${key}=${encoded};path=/;expires=${expires};SameSite=Lax${secure}`;
356        // Sync legacy cookie so PHP/Twig cookie.allowed is true on next page load
357        document.cookie = `${CookieConsentManager.LEGACY_ALLOWED_KEY}=1;path=/;expires=${expires};SameSite=Lax${secure}`;
358      } else {
359        localStorage.setItem(key, encoded);
360        localStorage.setItem(CookieConsentManager.LEGACY_ALLOWED_KEY, '1');
361      }
362    } catch (error) {
363      console.error('CookieConsent: Failed to store consent', error);
364    }
365  }
366
367  /**
368   * Retrieve consent data from storage
369   * @returns {ConsentState|null} Stored consent state or null
370   * @private
371   */
372  #getStorage() {
373    const key = CookieConsentManager.CONFIG.STORAGE_KEY;
374
375    try {
376      let value = null;
377
378      if (this.#useCookies) {
379        const match = document.cookie.match(new RegExp(`(^| )${key}=([^;]+)`));
380        value = match ? decodeURIComponent(match[2]) : null;
381      } else {
382        const stored = localStorage.getItem(key);
383        value = stored ? decodeURIComponent(stored) : null;
384      }
385
386      if (!value) return null;
387
388      const parsed = JSON.parse(value);
389
390      // Return normalized consent object (strip metadata)
391      return {
392        necessary: true,
393        functional: Boolean(parsed.functional),
394        statistics: Boolean(parsed.statistics),
395        marketing: Boolean(parsed.marketing)
396      };
397    } catch {
398      return null;
399    }
400  }
401
402  /**
403   * Clear consent data from storage
404   * @private
405   */
406  #clearStorage() {
407    const key = CookieConsentManager.CONFIG.STORAGE_KEY;
408
409    try {
410      if (this.#useCookies) {
411        document.cookie = `${key}=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;SameSite=Lax`;
412        document.cookie = `${CookieConsentManager.LEGACY_ALLOWED_KEY}=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;SameSite=Lax`;
413      }
414      localStorage.removeItem(key);
415      localStorage.removeItem(CookieConsentManager.LEGACY_ALLOWED_KEY);
416    } catch (error) {
417      console.error('CookieConsent: Failed to clear consent', error);
418    }
419  }
420
421  /**
422   * Migrate old consent format (mx_cookie_allowed) to new format (mx_cookie_consent_v2)
423   * This ensures backwards compatibility with the old Cookie module
424   * @private
425   */
426  #migrateOldConsent() {
427    // Check if old consent exists without new consent
428    const hasOldCookie = document.cookie.includes('mx_cookie_allowed=1');
429    const hasNewCookie = document.cookie.includes(CookieConsentManager.CONFIG.STORAGE_KEY);
430
431    if (hasOldCookie && !hasNewCookie) {
432      // Migrate: old "allowed" = accept all in new format
433      this.#setStorage({ ...CookieConsentManager.FULL_CONSENT });
434    }
435  }
436
437  // ===========================================================================
438  // Consent Management (Public API)
439  // ===========================================================================
440
441  /**
442   * Get current consent state
443   * @returns {ConsentState|null} Current consent state or null if not set
444   */
445  getConsent() {
446    return this.#getStorage();
447  }
448
449  /**
450   * Check if a specific consent type is granted
451   * @param {string} type - Consent type (necessary|functional|statistics|marketing)
452   * @returns {boolean} True if consent is granted
453   */
454  hasConsent(type) {
455    const consent = this.getConsent();
456    return consent ? consent[type] === true : type === 'necessary';
457  }
458
459  /**
460   * Set consent state and apply changes
461   * @param {ConsentState} consent - New consent state
462   */
463  setConsent(consent) {
464    const validatedConsent = this.#validateConsent(consent);
465
466    this.#setStorage(validatedConsent);
467    this.#applyGoogleConsentMode(validatedConsent);
468    this.#applyCloudflareZaraz(validatedConsent);
469    this.#processConsentElements(validatedConsent);
470
471    // Reload to apply consent changes fully
472    window.location.reload();
473  }
474
475  /**
476   * Reset consent and reload page
477   */
478  resetConsent() {
479    this.#clearStorage();
480    window.location.reload();
481  }
482
483  /**
484   * Accept all cookies
485   */
486  acceptAll() {
487    this.setConsent({ ...this.#getAcceptAllConsent() });
488  }
489
490  /**
491   * Reject all optional cookies (keep only necessary)
492   */
493  rejectAll() {
494    this.setConsent({ ...CookieConsentManager.DEFAULT_CONSENT });
495  }
496
497  /**
498   * Save current toggle selections from the modal
499   */
500  savePreferences() {
501    const consent = {
502      necessary: true,
503      functional: this.#elements.functional?.checked ?? false,
504      statistics: this.#elements.statistics?.checked ?? false,
505      marketing: this.#elements.marketing?.checked ?? false
506    };
507
508    this.setConsent(consent);
509  }
510
511  /**
512   * Show the settings modal
513   */
514  showModal() {
515    const consent = this.getConsent() || CookieConsentManager.DEFAULT_CONSENT;
516
517    this.#updateToggles(consent);
518    this.#hideBanner();
519
520    if (this.#elements.reset) {
521      this.#elements.reset.style.display = 'none';
522    }
523
524    const modal = this.#elements.modal;
525    if (modal) {
526      modal.style.display = 'flex';
527      modal.setAttribute('aria-hidden', 'false');
528
529      // Focus management for accessibility
530      const firstFocusable = modal.querySelector('button, [tabindex="0"]');
531      firstFocusable?.focus();
532
533      setTimeout(() => {
534        modal.classList.add(CookieConsentManager.CSS_CLASSES.SHOW);
535      }, CookieConsentManager.CONFIG.ANIMATION_DELAY_MS);
536    }
537
538    // Hide close button if modal is forced and no consent yet
539    if (this.#elements.close) {
540      const hideClose = this.#config.forceModal && !this.getConsent();
541      this.#elements.close.style.display = hideClose ? 'none' : '';
542    }
543  }
544
545  // ===========================================================================
546  // Consent Validation & Application
547  // ===========================================================================
548
549  /**
550   * Validate and normalize consent object
551   * @param {Object} consent - Raw consent object
552   * @returns {ConsentState} Validated consent state
553   * @private
554   */
555  #validateConsent(consent) {
556    if (!consent || typeof consent !== 'object') {
557      return { ...CookieConsentManager.DEFAULT_CONSENT };
558    }
559
560    return {
561      necessary: true, // Always required
562      functional: Boolean(consent.functional),
563      statistics: Boolean(consent.statistics),
564      marketing: Boolean(consent.marketing)
565    };
566  }
567
568  /**
569   * Apply stored consent on page load
570   * @private
571   */
572  #applyStoredConsent() {
573    const consent = this.getConsent();
574
575    if (consent) {
576      this.#applyGoogleConsentMode(consent);
577      this.#applyCloudflareZaraz(consent);
578      this.#processConsentElements(consent);
579    }
580  }
581
582  // ===========================================================================
583  // Google Consent Mode v2
584  // ===========================================================================
585
586  /**
587   * Update Google Consent Mode with current consent state
588   * @param {ConsentState} consent - Current consent state
589   * @private
590   */
591  #applyGoogleConsentMode(consent) {
592    if (typeof window.gtag !== 'function') return;
593
594    try {
595      window.gtag('consent', 'update', {
596        functionality_storage: consent.functional ? 'granted' : 'denied',
597        security_storage: 'granted',
598        ad_storage: consent.marketing ? 'granted' : 'denied',
599        ad_user_data: consent.marketing ? 'granted' : 'denied',
600        ad_personalization: consent.marketing ? 'granted' : 'denied',
601        analytics_storage: consent.statistics ? 'granted' : 'denied'
602      });
603    } catch (error) {
604      console.warn('CookieConsent: Failed to update Google Consent Mode', error);
605    }
606  }
607
608  // ===========================================================================
609  // Cloudflare Zaraz Integration
610  // ===========================================================================
611
612  /**
613   * Trigger Cloudflare Zaraz events based on consent
614   * @param {ConsentState} consent - Current consent state
615   * @private
616   */
617  #applyCloudflareZaraz(consent) {
618    if (typeof window.zaraz === 'undefined') return;
619
620    try {
621      if (consent.statistics && this.#config.cfStatistics) {
622        window.zaraz.track(this.#config.cfStatistics);
623      }
624
625      if (consent.marketing && this.#config.cfMarketing) {
626        window.zaraz.track(this.#config.cfMarketing);
627      }
628    } catch (error) {
629      console.warn('CookieConsent: Failed to trigger Zaraz events', error);
630    }
631  }
632
633  // ===========================================================================
634  // Conditional Script/Element Loading
635  // ===========================================================================
636
637  /**
638   * Process elements with data-consent attribute
639   * @param {ConsentState} consent - Current consent state
640   * @private
641   */
642  #processConsentElements(consent) {
643    const elements = document.querySelectorAll('[data-consent]');
644
645    elements.forEach(element => {
646      const type = element.dataset.consent;
647      const isAllowed = consent[type] === true;
648
649      if (isAllowed) {
650        this.#activateElement(element);
651      } else if (element.tagName === 'SCRIPT') {
652        // Remove blocked scripts
653        element.remove();
654      }
655    });
656  }
657
658  /**
659   * Activate a consent-gated element
660   * @param {HTMLElement} element - Element to activate
661   * @private
662   */
663  #activateElement(element) {
664    const { src: dataSrc } = element.dataset;
665
666    // Handle iframe src
667    if (dataSrc && element.tagName === 'IFRAME' && !element.src) {
668      element.src = dataSrc;
669      return;
670    }
671
672    // Handle script injection
673    if (element.tagName === 'SCRIPT') {
674      this.#injectScript(element);
675    }
676  }
677
678  /**
679   * Inject a script element into the DOM
680   * @param {HTMLScriptElement} original - Original script element
681   * @private
682   */
683  #injectScript(original) {
684    const src = original.dataset.src;
685
686    // Prevent duplicate injection
687    if (src && document.querySelector(`script[src="${src}"]`)) {
688      return;
689    }
690
691    const script = document.createElement('script');
692
693    if (src) {
694      script.src = src;
695      if (original.hasAttribute('defer')) script.defer = true;
696      if (original.hasAttribute('async')) script.async = true;
697    } else {
698      script.textContent = original.textContent;
699    }
700
701    script.type = original.type || 'text/javascript';
702    document.head.appendChild(script);
703  }
704
705  // ===========================================================================
706  // UI Management
707  // ===========================================================================
708
709  /**
710   * Update UI based on current consent state
711   * @private
712   */
713  #updateUIState() {
714    const consent = this.getConsent();
715
716    // No consent given yet - show banner or modal
717    if (!consent) {
718      if (this.#config.forceModal) {
719        this.showModal();
720      } else {
721        this.#showBanner();
722      }
723      return;
724    }
725
726    // Consent already given - show reset button
727    this.#hideBanner();
728    this.#hideModal();
729    this.#showReset();
730
731    if (this.#elements.close) {
732      this.#elements.close.style.display = '';
733    }
734  }
735
736  /**
737   * Get consent state for "Accept all" based on which categories are enabled (have a toggle in the DOM).
738   * Only grants consent for categories that are present; disabled categories stay false.
739   * @returns {ConsentState} Consent state for accept-all
740   * @private
741   */
742  #getAcceptAllConsent() {
743    const { functional, statistics, marketing } = this.#elements;
744    return {
745      necessary: true,
746      functional: Boolean(functional),
747      statistics: Boolean(statistics),
748      marketing: Boolean(marketing)
749    };
750  }
751
752  /**
753   * Update toggle checkboxes to reflect consent state
754   * @param {ConsentState} consent - Consent state
755   * @private
756   */
757  #updateToggles(consent) {
758    const { functional, statistics, marketing } = this.#elements;
759
760    if (functional) functional.checked = consent.functional;
761    if (statistics) statistics.checked = consent.statistics;
762    if (marketing) marketing.checked = consent.marketing;
763  }
764
765  /**
766   * Show the cookie banner
767   * @private
768   */
769  #showBanner() {
770    const { banner, modal, reset } = this.#elements;
771
772    if (modal) modal.style.display = 'none';
773    if (reset) reset.style.display = 'none';
774
775    if (banner) {
776      banner.style.display = 'flex';
777      banner.setAttribute('aria-hidden', 'false');
778
779      setTimeout(() => {
780        banner.classList.add(CookieConsentManager.CSS_CLASSES.SHOW);
781      }, CookieConsentManager.CONFIG.ANIMATION_DELAY_MS);
782    }
783  }
784
785  /**
786   * Hide the cookie banner
787   * @private
788   */
789  #hideBanner() {
790    const { banner } = this.#elements;
791
792    if (!banner) return;
793
794    banner.classList.remove(CookieConsentManager.CSS_CLASSES.SHOW);
795    banner.setAttribute('aria-hidden', 'true');
796
797    setTimeout(() => {
798      banner.style.display = 'none';
799    }, CookieConsentManager.CONFIG.TRANSITION_DURATION_MS);
800  }
801
802  /**
803   * Hide the settings modal
804   * @private
805   */
806  #hideModal() {
807    const { modal } = this.#elements;
808
809    if (!modal) return;
810
811    modal.classList.remove(CookieConsentManager.CSS_CLASSES.SHOW);
812    modal.setAttribute('aria-hidden', 'true');
813
814    setTimeout(() => {
815      modal.style.display = 'none';
816    }, CookieConsentManager.CONFIG.TRANSITION_DURATION_MS);
817  }
818
819  /**
820   * Show the reset/settings button
821   * @private
822   */
823  #showReset() {
824    const { reset } = this.#elements;
825    if (reset) reset.style.display = 'flex';
826  }
827
828  // ===========================================================================
829  // Event Handling
830  // ===========================================================================
831
832  /**
833   * Bind all event listeners
834   * @private
835   */
836  #bindEventListeners() {
837    const els = this.#elements;
838
839    // Banner buttons
840    els.accept?.addEventListener('click', this.#handleAccept.bind(this));
841    els.reject?.addEventListener('click', this.#handleReject.bind(this));
842    els.settings?.addEventListener('click', this.showModal.bind(this));
843
844    // Modal buttons
845    els.accept_all?.addEventListener('click', this.#handleAccept.bind(this));
846    els.reject_all?.addEventListener('click', this.#handleReject.bind(this));
847    els.save?.addEventListener('click', this.#handleSave.bind(this));
848    els.close?.addEventListener('click', this.#handleClose.bind(this));
849
850    // Reset button
851    els.reset?.addEventListener('click', this.showModal.bind(this));
852
853    // Warning placeholders (e.g., YouTube embeds)
854    els.warnings?.forEach(warnEl => {
855      warnEl.addEventListener('click', (event) => {
856        event.preventDefault();
857        event.stopPropagation();
858        if (typeof event.stopImmediatePropagation === 'function') {
859          event.stopImmediatePropagation();
860        }
861        this.showModal();
862      });
863    });
864
865    // Keyboard navigation
866    document.addEventListener('keydown', this.#handleKeyboard.bind(this));
867  }
868
869  /**
870   * Handle accept button click
871   * @private
872   */
873  #handleAccept() {
874    const consent = this.#getAcceptAllConsent();
875    this.#updateToggles(consent);
876
877    setTimeout(() => {
878      this.setConsent({ ...consent });
879    }, CookieConsentManager.CONFIG.TRANSITION_DURATION_MS);
880  }
881
882  /**
883   * Handle reject button click
884   * @private
885   */
886  #handleReject() {
887    this.#updateToggles(CookieConsentManager.DEFAULT_CONSENT);
888
889    setTimeout(() => {
890      this.rejectAll();
891    }, CookieConsentManager.CONFIG.TRANSITION_DURATION_MS);
892  }
893
894  /**
895   * Handle save preferences button click
896   * @private
897   */
898  #handleSave() {
899    this.savePreferences();
900  }
901
902  /**
903   * Handle close button click
904   * @private
905   */
906  #handleClose() {
907    this.#hideModal();
908    this.#updateUIState();
909  }
910
911  /**
912   * Handle keyboard events (accessibility)
913   * @param {KeyboardEvent} event - Keyboard event
914   * @private
915   */
916  #handleKeyboard(event) {
917    const { modal } = this.#elements;
918    const isModalVisible = modal?.classList.contains(CookieConsentManager.CSS_CLASSES.SHOW);
919
920    // Escape key closes modal (unless forced)
921    if (event.key === 'Escape' && isModalVisible && !this.#config.forceModal) {
922      event.preventDefault();
923      this.#handleClose();
924    }
925
926    // Tab trap within modal for accessibility
927    if (event.key === 'Tab' && isModalVisible) {
928      this.#handleTabTrap(event, modal);
929    }
930  }
931
932  /**
933   * Trap focus within modal for accessibility
934   * @param {KeyboardEvent} event - Keyboard event
935   * @param {HTMLElement} modal - Modal element
936   * @private
937   */
938  #handleTabTrap(event, modal) {
939    const focusableSelector = 'button, [href], input, select, textarea, [tabindex]:not
939([tabindex="-1"])';
940    const focusable = modal.querySelectorAll(focusableSelector);
941    const first = focusable[0];
942    const last = focusable[focusable.length - 1];
943
944    if (event.shiftKey && document.activeElement === first) {
945      event.preventDefault();
946      last?.focus();
947    } else if (!event.shiftKey && document.activeElement === last) {
948      event.preventDefault();
949      first?.focus();
950    }
951  }
952
953  // ===========================================================================
954  // Public API Exposure
955  // ===========================================================================
956
957  /**
958   * Expose public API on window.CookieConsent
959   * @private
960   */
961  #exposePublicAPI() {
962    /**
963     * Public API for external access
964     * @namespace CookieConsent
965     */
966    window.CookieConsent = Object.freeze({
967      /**
968       * Get current consent state
969       * @returns {ConsentState|null}
970       */
971      get: () => this.getConsent(),
972
973      /**
974       * Check if specific consent type is granted
975       * @param {string} type - Consent type
976       * @returns {boolean}
977       */
978      has: (type) => this.hasConsent(type),
979
980      /**
981       * Set consent state
982       * @param {ConsentState} consent
983       */
984      set: (consent) => this.setConsent(consent),
985
986      /**
987       * Reset consent
988       */
989      reset: () => this.resetConsent(),
990
991      /**
992       * Accept all cookies
993       */
994      acceptAll: () => this.acceptAll(),
995
996      /**
997       * Reject all optional cookies
998       */
999      rejectAll: () => this.rejectAll(),
1000
1001      /**
1002       * Show settings modal
1003       */
1004      showModal: () => this.showModal(),
1005
1006      /**
1007       * Version number
1008       */
1009      version: CookieConsentManager.CONFIG.VERSION
1010    });
1011  }
1012}
1013
1014// ============================================================================
1015// Initialize on DOM Ready
1016// ============================================================================
1017if (document.readyState === 'loading') {
1018  document.addEventListener('DOMContentLoaded', () => {
1019    new CookieConsentManager();
1020  });
1021} else {
1022  // DOM already loaded
1023  new CookieConsentManager();
1024}

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.