gdpr-cookie-consent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.js ADDED
@@ -0,0 +1,1321 @@
1
+ /**
2
+ * GDPR Cookie Consent Library
3
+ * A headless, data-attribute-driven cookie consent system
4
+ *
5
+ * @version 1.0.0
6
+ * @author Your Name
7
+ * @license MIT
8
+ */
9
+ (function (window) {
10
+ "use strict";
11
+
12
+ class GDPRCookies {
13
+ constructor() {
14
+ this.config = {
15
+ cookiePrefix: "gdpr_",
16
+ cookieDuration: 365,
17
+ categories: [],
18
+ categoryConfig: {},
19
+ onAccept: null,
20
+ onDecline: null,
21
+ onSave: null,
22
+ autoShow: true,
23
+ showDelay: 1000,
24
+ };
25
+
26
+ this.elements = {};
27
+ this.currentPreferences = {};
28
+ this.isInitialized = false;
29
+ this.errorLog = [];
30
+ }
31
+
32
+ /**
33
+ * Safe error logging that doesn't expose sensitive details
34
+ */
35
+ logError(message, context = {}) {
36
+ const errorEntry = {
37
+ timestamp: new Date().toISOString(),
38
+ message: message,
39
+ context: this.sanitizeContext(context)
40
+ };
41
+
42
+ this.errorLog.push(errorEntry);
43
+
44
+ // Keep only last 10 errors to prevent memory issues
45
+ if (this.errorLog.length > 10) {
46
+ this.errorLog.shift();
47
+ }
48
+
49
+ // Log to console in development (when not in production)
50
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
51
+ console.warn(`GDPR: ${message}`, context);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Sanitize context to prevent logging sensitive information
57
+ */
58
+ sanitizeContext(context) {
59
+ const sanitized = {};
60
+ for (const [key, value] of Object.entries(context)) {
61
+ if (typeof value === 'string' && value.length > 100) {
62
+ sanitized[key] = '[TRUNCATED]';
63
+ } else if (typeof value === 'object' && value !== null) {
64
+ sanitized[key] = '[OBJECT]';
65
+ } else {
66
+ sanitized[key] = value;
67
+ }
68
+ }
69
+ return sanitized;
70
+ }
71
+
72
+ /**
73
+ * Initialize the GDPR system
74
+ * @param {Object} options - Configuration options
75
+ */
76
+ init(options = {}) {
77
+ try {
78
+ if (this.isInitialized) {
79
+ this.logError('GDPR system already initialized');
80
+ return false;
81
+ }
82
+
83
+ // Validate input
84
+ if (options !== null && typeof options !== 'object') {
85
+ this.logError('Invalid options provided to init method', { optionsType: typeof options });
86
+ return false;
87
+ }
88
+
89
+ // Merge configuration safely
90
+ try {
91
+ this.config = { ...this.config, ...options };
92
+ } catch (mergeError) {
93
+ this.logError('Failed to merge configuration options');
94
+ return false;
95
+ }
96
+
97
+ // Set default category configurations
98
+ this.setupDefaultCategories();
99
+
100
+ // Find and cache DOM elements
101
+ this.cacheElements();
102
+
103
+ // Setup event listeners
104
+ this.bindEvents();
105
+
106
+ // Initialize categories UI
107
+ this.renderCategories();
108
+
109
+ // Check existing consent
110
+ this.loadExistingConsent();
111
+
112
+ this.isInitialized = true;
113
+ this.updateStatusDisplay();
114
+ return true;
115
+ } catch (error) {
116
+ this.logError('Critical error during initialization');
117
+ this.isInitialized = false;
118
+ return false;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Setup default category configurations
124
+ */
125
+ setupDefaultCategories() {
126
+ const defaults = {
127
+ analytics: {
128
+ title: "📊 Analytics Cookies",
129
+ description:
130
+ "Help us understand how visitors interact with our website",
131
+ scripts: [],
132
+ onAccept: null,
133
+ onDecline: null,
134
+ },
135
+ marketing: {
136
+ title: "🎯 Marketing Cookies",
137
+ description:
138
+ "Used to deliver personalized advertisements and measure their effectiveness",
139
+ scripts: [],
140
+ onAccept: null,
141
+ onDecline: null,
142
+ },
143
+ functional: {
144
+ title: "⚙️ Functional Cookies",
145
+ description:
146
+ "Enable enhanced functionality and personalization features",
147
+ scripts: [],
148
+ onAccept: null,
149
+ onDecline: null,
150
+ },
151
+ };
152
+
153
+ // Merge user config with defaults
154
+ this.config.categories.forEach((category) => {
155
+ if (!this.config.categoryConfig[category]) {
156
+ this.config.categoryConfig[category] = defaults[category] || {
157
+ title: `${
158
+ category.charAt(0).toUpperCase() + category.slice(1)
159
+ } Cookies`,
160
+ description: `Cookies for ${category} purposes`,
161
+ scripts: [],
162
+ };
163
+ }
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Cache DOM elements using data attributes
169
+ */
170
+ cacheElements() {
171
+ try {
172
+ this.elements = {
173
+ banner: this.safeQuerySelector("[data-gdpr-banner]"),
174
+ modal: this.safeQuerySelector("[data-gdpr-modal]"),
175
+ categoriesContainer: this.safeQuerySelector("[data-gdpr-categories]"),
176
+ categoryTemplate: this.safeQuerySelector("[data-gdpr-category-template]"),
177
+ statusDisplay: this.safeQuerySelector("[data-gdpr-status]"),
178
+ };
179
+
180
+ // Validate required elements are available
181
+ this.validateRequiredElements();
182
+ } catch (error) {
183
+ this.logError('Failed to cache DOM elements');
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Safe DOM query selector with error handling
189
+ */
190
+ safeQuerySelector(selector) {
191
+ try {
192
+ if (!selector || typeof selector !== 'string') {
193
+ this.logError('Invalid selector provided', { selector });
194
+ return null;
195
+ }
196
+ return document.querySelector(selector);
197
+ } catch (error) {
198
+ this.logError('DOM query failed', { selector });
199
+ return null;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Validate that required DOM elements are present
205
+ */
206
+ validateRequiredElements() {
207
+ if (!this.elements.banner) {
208
+ this.logError('Banner element with [data-gdpr-banner] not found');
209
+ }
210
+ if (!this.elements.modal) {
211
+ this.logError('Modal element with [data-gdpr-modal] not found');
212
+ }
213
+ if (!this.elements.categoriesContainer) {
214
+ this.logError('Categories container with [data-gdpr-categories] not found');
215
+ }
216
+ if (!this.elements.categoryTemplate) {
217
+ this.logError('Category template with [data-gdpr-category-template] not found');
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Bind event listeners using event delegation
223
+ */
224
+ bindEvents() {
225
+ try {
226
+ // Use event delegation for all GDPR actions
227
+ document.addEventListener("click", (e) => {
228
+ try {
229
+ if (!e.target) return;
230
+
231
+ const action = e.target.getAttribute("data-gdpr-action");
232
+ if (action) {
233
+ this.handleAction(action, e);
234
+ }
235
+ } catch (error) {
236
+ this.logError('Error handling click event');
237
+ }
238
+ });
239
+
240
+ // Handle category toggle changes
241
+ document.addEventListener("change", (e) => {
242
+ try {
243
+ if (!e.target) return;
244
+
245
+ const toggle = e.target.getAttribute("data-category-toggle");
246
+ if (toggle !== null) {
247
+ const categoryElement = e.target.closest("[data-category]");
248
+ if (categoryElement) {
249
+ const category = categoryElement.getAttribute("data-category");
250
+ if (category) {
251
+ this.currentPreferences[category] = e.target.checked;
252
+ }
253
+ }
254
+ }
255
+ } catch (error) {
256
+ this.logError('Error handling change event');
257
+ }
258
+ });
259
+
260
+ // Close modal on overlay click
261
+ if (this.elements.modal) {
262
+ this.elements.modal.addEventListener("click", (e) => {
263
+ try {
264
+ if (e.target === this.elements.modal) {
265
+ this.hideModal();
266
+ }
267
+ } catch (error) {
268
+ this.logError('Error handling modal click');
269
+ }
270
+ });
271
+ }
272
+
273
+ // Keyboard accessibility
274
+ document.addEventListener("keydown", (e) => {
275
+ try {
276
+ if (e.key === "Escape") {
277
+ this.hideModal();
278
+ }
279
+ } catch (error) {
280
+ this.logError('Error handling keydown event');
281
+ }
282
+ });
283
+ } catch (error) {
284
+ this.logError('Critical error binding events');
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Handle action button clicks
290
+ */
291
+ handleAction(action, event) {
292
+ event.preventDefault();
293
+
294
+ switch (action) {
295
+ case "show-banner":
296
+ this.showBanner();
297
+ break;
298
+ case "show-preferences":
299
+ this.showModal();
300
+ break;
301
+ case "close-modal":
302
+ this.hideModal();
303
+ break;
304
+ case "accept-all":
305
+ this.acceptAll();
306
+ break;
307
+ case "accept-essential":
308
+ this.acceptEssential();
309
+ break;
310
+ case "save-preferences":
311
+ this.savePreferences();
312
+ break;
313
+ case "clear-all":
314
+ this.clearAll();
315
+ break;
316
+ default:
317
+ break;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Render category toggles dynamically
323
+ */
324
+ renderCategories() {
325
+ try {
326
+ if (
327
+ !this.elements.categoriesContainer ||
328
+ !this.elements.categoryTemplate
329
+ ) {
330
+ this.logError('Required elements missing for category rendering');
331
+ return;
332
+ }
333
+
334
+ // Clear existing categories safely
335
+ try {
336
+ this.elements.categoriesContainer.replaceChildren();
337
+ } catch (clearError) {
338
+ // Fallback for older browsers
339
+ this.elements.categoriesContainer.innerHTML = '';
340
+ }
341
+
342
+ // Validate categories array
343
+ if (!Array.isArray(this.config.categories)) {
344
+ this.logError('Categories configuration is not an array');
345
+ return;
346
+ }
347
+
348
+ // Create category elements from template
349
+ this.config.categories.forEach((categoryKey) => {
350
+ try {
351
+ if (!categoryKey || typeof categoryKey !== 'string') {
352
+ this.logError('Invalid category key', { categoryKey });
353
+ return;
354
+ }
355
+
356
+ const config = this.config.categoryConfig[categoryKey];
357
+ if (!config) {
358
+ this.logError('No configuration found for category', { categoryKey });
359
+ return;
360
+ }
361
+
362
+ const template = this.elements.categoryTemplate.content.cloneNode(true);
363
+ if (!template) {
364
+ this.logError('Failed to clone category template');
365
+ return;
366
+ }
367
+
368
+ // Set category data safely
369
+ const container = template.querySelector(".cookie-category");
370
+ if (container) {
371
+ container.setAttribute("data-category", categoryKey);
372
+ } else {
373
+ this.logError('Cookie category container not found in template');
374
+ }
375
+
376
+ const title = template.querySelector("[data-category-title]");
377
+ const description = template.querySelector("[data-category-description]");
378
+ const toggle = template.querySelector("[data-category-toggle]");
379
+
380
+ if (title && config.title) {
381
+ title.textContent = config.title;
382
+ }
383
+ if (description && config.description) {
384
+ description.textContent = config.description;
385
+ }
386
+ if (toggle) {
387
+ toggle.checked = this.currentPreferences[categoryKey] || false;
388
+ }
389
+
390
+ this.elements.categoriesContainer.appendChild(template);
391
+ } catch (categoryError) {
392
+ this.logError('Failed to render category', { categoryKey });
393
+ }
394
+ });
395
+ } catch (error) {
396
+ this.logError('Critical error in category rendering');
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Cookie management methods
402
+ */
403
+ setCookie(name, value, days = this.config.cookieDuration) {
404
+ try {
405
+ if (!name || typeof name !== 'string') {
406
+ this.logError('Invalid cookie name provided', { name });
407
+ return false;
408
+ }
409
+
410
+ if (value === undefined) {
411
+ this.logError('Cookie value is undefined', { cookieName: name });
412
+ return false;
413
+ }
414
+
415
+ const expires = new Date(Date.now() + days * 864e5).toUTCString();
416
+ const secure = location.protocol === "https:" ? "; Secure" : "";
417
+
418
+ // Use Strict SameSite for better security
419
+ const sameSite = "; SameSite=Strict";
420
+
421
+ // Add domain for subdomain support
422
+ const domain = this.getDomainForCookie();
423
+
424
+ let serializedValue;
425
+ try {
426
+ serializedValue = encodeURIComponent(JSON.stringify(value));
427
+ } catch (serializeError) {
428
+ this.logError('Failed to serialize cookie value', { cookieName: name });
429
+ return false;
430
+ }
431
+
432
+ const cookieString = `${
433
+ this.config.cookiePrefix
434
+ }${name}=${serializedValue}; expires=${expires}; path=/${domain}${sameSite}${secure}`;
435
+
436
+ // Validate cookie string length (browsers have limits)
437
+ if (cookieString.length > 4096) {
438
+ this.logError('Cookie string too long', { cookieName: name, length: cookieString.length });
439
+ return false;
440
+ }
441
+
442
+ document.cookie = cookieString;
443
+
444
+ // Verify cookie was set by attempting to read it back
445
+ const verification = this.getCookie(name);
446
+ if (verification === null && value !== null) {
447
+ this.logError('Cookie verification failed', { cookieName: name });
448
+ return false;
449
+ }
450
+
451
+ return true;
452
+ } catch (error) {
453
+ this.logError('Cookie setting failed', { cookieName: name });
454
+ return false;
455
+ }
456
+ }
457
+
458
+ getCookie(name) {
459
+ try {
460
+ if (!name || typeof name !== 'string') {
461
+ this.logError('Invalid cookie name provided', { name });
462
+ return null;
463
+ }
464
+
465
+ const cookieValue = document.cookie
466
+ .split("; ")
467
+ .find((row) => row.startsWith(`${this.config.cookiePrefix}${name}=`))
468
+ ?.split("=")[1];
469
+
470
+ if (cookieValue) {
471
+ try {
472
+ return JSON.parse(decodeURIComponent(cookieValue));
473
+ } catch (parseError) {
474
+ this.logError('Failed to parse cookie value', { cookieName: name });
475
+ // Try to return the raw value as fallback
476
+ try {
477
+ return decodeURIComponent(cookieValue);
478
+ } catch (decodeError) {
479
+ this.logError('Failed to decode cookie value', { cookieName: name });
480
+ return null;
481
+ }
482
+ }
483
+ }
484
+ return null;
485
+ } catch (error) {
486
+ this.logError('Cookie retrieval failed', { cookieName: name });
487
+ return null;
488
+ }
489
+ }
490
+
491
+ deleteCookie(name) {
492
+ try {
493
+ if (!name || typeof name !== 'string') {
494
+ this.logError('Invalid cookie name provided for deletion', { name });
495
+ return false;
496
+ }
497
+
498
+ const domain = this.getDomainForCookie();
499
+ document.cookie = `${this.config.cookiePrefix}${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`;
500
+
501
+ // Also try deleting without domain for broader compatibility
502
+ document.cookie = `${this.config.cookiePrefix}${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
503
+
504
+ return true;
505
+ } catch (error) {
506
+ this.logError('Cookie deletion failed', { cookieName: name });
507
+ return false;
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Get domain string for cookie setting
513
+ */
514
+ getDomainForCookie() {
515
+ // Only set domain for non-localhost environments to support subdomains
516
+ const hostname = location.hostname;
517
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes(':')) {
518
+ return '';
519
+ }
520
+ // Use the current domain, allowing cookies to be accessible to subdomains
521
+ const parts = hostname.split('.');
522
+ if (parts.length > 2) {
523
+ // For subdomains like 'app.example.com', use '.example.com'
524
+ return `; domain=.${parts.slice(-2).join('.')}`;
525
+ }
526
+ return `; domain=${hostname}`;
527
+ }
528
+
529
+ /**
530
+ * Load existing consent from cookies
531
+ */
532
+ loadExistingConsent() {
533
+ const consent = this.getCookie("consent");
534
+ const preferences = this.getCookie("preferences");
535
+
536
+ if (consent && preferences) {
537
+ this.currentPreferences = preferences;
538
+ this.applyConsent(preferences);
539
+ this.updateCategoryToggles();
540
+ } else if (this.config.autoShow) {
541
+ // Show banner after delay for new users
542
+ setTimeout(() => this.showBanner(), this.config.showDelay);
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Update category toggle states
548
+ */
549
+ updateCategoryToggles() {
550
+ try {
551
+ if (!Array.isArray(this.config.categories)) {
552
+ this.logError('Categories configuration is not an array');
553
+ return;
554
+ }
555
+
556
+ this.config.categories.forEach((category) => {
557
+ try {
558
+ if (!category || typeof category !== 'string') {
559
+ this.logError('Invalid category in updateCategoryToggles', { category });
560
+ return;
561
+ }
562
+
563
+ const toggle = this.safeQuerySelector(
564
+ `[data-category="${category}"] [data-category-toggle]`
565
+ );
566
+ if (toggle) {
567
+ toggle.checked = this.currentPreferences[category] || false;
568
+ }
569
+ } catch (toggleError) {
570
+ this.logError('Failed to update category toggle', { category });
571
+ }
572
+ });
573
+ } catch (updateError) {
574
+ this.logError('Critical error updating category toggles');
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Apply consent decisions (load/unload scripts)
580
+ */
581
+ applyConsent(preferences) {
582
+ this.config.categories.forEach((category) => {
583
+ const config = this.config.categoryConfig[category];
584
+ const accepted = preferences[category];
585
+
586
+ if (accepted) {
587
+ // Load scripts for this category
588
+ this.loadCategoryScripts(category);
589
+
590
+ // Call onAccept callback
591
+ if (config.onAccept && typeof config.onAccept === "function") {
592
+ config.onAccept(category);
593
+ }
594
+ } else {
595
+ // Unload scripts for this category
596
+ this.unloadCategoryScripts(category);
597
+
598
+ // Call onDecline callback
599
+ if (config.onDecline && typeof config.onDecline === "function") {
600
+ config.onDecline(category);
601
+ }
602
+ }
603
+ });
604
+ }
605
+
606
+ /**
607
+ * Load scripts for a specific category
608
+ */
609
+ async loadCategoryScripts(category) {
610
+ try {
611
+ if (!category || typeof category !== 'string') {
612
+ this.logError('Invalid category provided for script loading', { category });
613
+ return;
614
+ }
615
+
616
+ const config = this.config.categoryConfig[category];
617
+ if (!config) {
618
+ this.logError('No configuration found for category', { category });
619
+ return;
620
+ }
621
+
622
+ if (config.scripts && Array.isArray(config.scripts) && config.scripts.length > 0) {
623
+ // Load scripts sequentially to avoid conflicts
624
+ for (const scriptConfig of config.scripts) {
625
+ try {
626
+ await this.loadScript(scriptConfig, category);
627
+ } catch (scriptError) {
628
+ this.logError('Failed to load script in category', { category, script: scriptConfig });
629
+ // Continue loading other scripts even if one fails
630
+ }
631
+ }
632
+ }
633
+ } catch (categoryError) {
634
+ this.logError('Critical error loading category scripts', { category });
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Unload scripts for a specific category
640
+ */
641
+ unloadCategoryScripts(category) {
642
+ // Remove scripts with data-gdpr-category attribute
643
+ const scripts = document.querySelectorAll(
644
+ `script[data-gdpr-category="${category}"]`
645
+ );
646
+ scripts.forEach((script) => script.remove());
647
+
648
+ // Clean up category-specific cookies
649
+ this.cleanupCategoryCookies(category);
650
+
651
+ }
652
+
653
+ /**
654
+ * Validate URL to prevent script injection attacks
655
+ */
656
+ isValidScriptUrl(url) {
657
+ try {
658
+ const parsedUrl = new URL(url, location.origin);
659
+ // Only allow HTTP and HTTPS protocols
660
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
661
+ return false;
662
+ }
663
+ // Prevent javascript: and data: URLs
664
+ if (parsedUrl.protocol === 'javascript:' || parsedUrl.protocol === 'data:') {
665
+ return false;
666
+ }
667
+ return true;
668
+ } catch (e) {
669
+ return false;
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Load a script dynamically with error handling and timeout
675
+ */
676
+ loadScript(scriptConfig, category) {
677
+ return new Promise((resolve, reject) => {
678
+ try {
679
+ if (!scriptConfig) {
680
+ this.logError('No script configuration provided');
681
+ reject(new Error('No script configuration'));
682
+ return;
683
+ }
684
+
685
+ if (!category || typeof category !== 'string') {
686
+ this.logError('Invalid category provided for script loading', { category });
687
+ reject(new Error('Invalid category'));
688
+ return;
689
+ }
690
+
691
+ const script = document.createElement("script");
692
+ script.setAttribute("data-gdpr-category", category);
693
+
694
+ let scriptSrc;
695
+ let timeout = 10000; // Default 10 second timeout
696
+
697
+ if (typeof scriptConfig === "string") {
698
+ scriptSrc = scriptConfig;
699
+ } else if (typeof scriptConfig === 'object' && scriptConfig !== null) {
700
+ scriptSrc = scriptConfig.src;
701
+ if (scriptConfig.async !== undefined) script.async = scriptConfig.async;
702
+ if (scriptConfig.defer !== undefined) script.defer = scriptConfig.defer;
703
+ if (scriptConfig.type) script.type = scriptConfig.type;
704
+ if (scriptConfig.timeout && typeof scriptConfig.timeout === 'number') {
705
+ timeout = scriptConfig.timeout;
706
+ }
707
+ } else {
708
+ this.logError('Invalid script configuration format', { scriptConfig });
709
+ reject(new Error('Invalid script configuration'));
710
+ return;
711
+ }
712
+
713
+ if (!scriptSrc || typeof scriptSrc !== 'string') {
714
+ this.logError('No script source URL provided');
715
+ reject(new Error('No script source'));
716
+ return;
717
+ }
718
+
719
+ // Validate the script URL before loading
720
+ if (!this.isValidScriptUrl(scriptSrc)) {
721
+ this.logError('Invalid script URL blocked for security', { url: scriptSrc });
722
+ reject(new Error('Invalid script URL'));
723
+ return;
724
+ }
725
+
726
+ // Set up timeout handler
727
+ const timeoutId = setTimeout(() => {
728
+ this.logError('Script loading timeout', { url: scriptSrc, category, timeout });
729
+ script.remove();
730
+ reject(new Error('Script loading timeout'));
731
+ }, timeout);
732
+
733
+ // Handle successful loading
734
+ script.onload = () => {
735
+ clearTimeout(timeoutId);
736
+ resolve();
737
+ };
738
+
739
+ // Handle loading errors
740
+ script.onerror = (error) => {
741
+ clearTimeout(timeoutId);
742
+ this.logError('Script loading failed', { url: scriptSrc, category });
743
+ script.remove();
744
+ reject(new Error('Script loading failed'));
745
+ };
746
+
747
+ script.src = scriptSrc;
748
+ document.head.appendChild(script);
749
+
750
+ } catch (error) {
751
+ this.logError('Critical error in script loading', { category });
752
+ reject(error);
753
+ }
754
+ });
755
+ }
756
+
757
+ /**
758
+ * Clean up cookies for a specific category
759
+ */
760
+ cleanupCategoryCookies(category) {
761
+ const cookiesToClean = {
762
+ analytics: [
763
+ "_ga",
764
+ "_ga_*",
765
+ "_gid",
766
+ "_gat",
767
+ "__utma",
768
+ "__utmb",
769
+ "__utmc",
770
+ "__utmt",
771
+ "__utmz",
772
+ ],
773
+ marketing: [
774
+ "_fbp",
775
+ "_fbc",
776
+ "fr",
777
+ "__Secure-FRCMP",
778
+ "DSID",
779
+ "IDE",
780
+ "test_cookie",
781
+ ],
782
+ functional: [],
783
+ };
784
+
785
+ const cookies = cookiesToClean[category] || [];
786
+ cookies.forEach((cookieName) => {
787
+ if (cookieName.includes("*")) {
788
+ // Handle wildcard cookies
789
+ const prefix = cookieName.replace("*", "");
790
+ document.cookie.split(";").forEach((cookie) => {
791
+ const name = cookie.split("=")[0].trim();
792
+ if (name.startsWith(prefix)) {
793
+ const domain = this.getDomainForCookie();
794
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`;
795
+ }
796
+ });
797
+ } else {
798
+ const domain = this.getDomainForCookie();
799
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`;
800
+ }
801
+ });
802
+ }
803
+
804
+ /**
805
+ * UI control methods
806
+ */
807
+ showBanner() {
808
+ try {
809
+ if (this.elements.banner) {
810
+ this.elements.banner.style.display = "block";
811
+ // Trigger reflow for animation
812
+ this.elements.banner.offsetHeight;
813
+ this.elements.banner.classList.add("show");
814
+ } else {
815
+ this.logError('Banner element not available for display');
816
+ }
817
+ } catch (error) {
818
+ this.logError('Failed to show banner');
819
+ }
820
+ }
821
+
822
+ hideBanner() {
823
+ try {
824
+ if (this.elements.banner) {
825
+ this.elements.banner.classList.remove("show");
826
+ setTimeout(() => {
827
+ try {
828
+ if (this.elements.banner) {
829
+ this.elements.banner.style.display = "none";
830
+ }
831
+ } catch (hideError) {
832
+ this.logError('Failed to hide banner after timeout');
833
+ }
834
+ }, 300);
835
+ }
836
+ } catch (error) {
837
+ this.logError('Failed to initiate banner hiding');
838
+ }
839
+ }
840
+
841
+ showModal() {
842
+ try {
843
+ if (this.elements.modal) {
844
+ this.updateCategoryToggles();
845
+ this.elements.modal.style.display = "flex";
846
+ // Trigger reflow for animation
847
+ this.elements.modal.offsetHeight;
848
+ this.elements.modal.classList.add("show");
849
+ try {
850
+ document.body.style.overflow = "hidden";
851
+ } catch (bodyError) {
852
+ this.logError('Failed to set body overflow');
853
+ }
854
+ } else {
855
+ this.logError('Modal element not available for display');
856
+ }
857
+ } catch (error) {
858
+ this.logError('Failed to show modal');
859
+ }
860
+ }
861
+
862
+ hideModal() {
863
+ try {
864
+ if (this.elements.modal) {
865
+ this.elements.modal.classList.remove("show");
866
+ try {
867
+ document.body.style.overflow = "";
868
+ } catch (bodyError) {
869
+ this.logError('Failed to reset body overflow');
870
+ }
871
+ setTimeout(() => {
872
+ try {
873
+ if (this.elements.modal) {
874
+ this.elements.modal.style.display = "none";
875
+ }
876
+ } catch (hideError) {
877
+ this.logError('Failed to hide modal after timeout');
878
+ }
879
+ }, 300);
880
+ }
881
+ } catch (error) {
882
+ this.logError('Failed to initiate modal hiding');
883
+ }
884
+ }
885
+
886
+ /**
887
+ * Consent action methods
888
+ */
889
+ acceptAll() {
890
+ const preferences = { essential: true };
891
+ this.config.categories.forEach((category) => {
892
+ preferences[category] = true;
893
+ });
894
+
895
+ this.saveConsent("all", preferences);
896
+ this.hideBanner();
897
+ this.hideModal();
898
+
899
+ if (this.config.onAccept) {
900
+ this.config.onAccept(preferences);
901
+ }
902
+ }
903
+
904
+ acceptEssential() {
905
+ const preferences = { essential: true };
906
+ this.config.categories.forEach((category) => {
907
+ preferences[category] = false;
908
+ });
909
+
910
+ this.saveConsent("essential", preferences);
911
+ this.hideBanner();
912
+ this.hideModal();
913
+
914
+ if (this.config.onDecline) {
915
+ this.config.onDecline(preferences);
916
+ }
917
+ }
918
+
919
+ savePreferences() {
920
+ const preferences = { essential: true, ...this.currentPreferences };
921
+
922
+ this.saveConsent("custom", preferences);
923
+ this.hideModal();
924
+ this.hideBanner();
925
+
926
+ if (this.config.onSave) {
927
+ this.config.onSave(preferences);
928
+ }
929
+ }
930
+
931
+ saveConsent(type, preferences) {
932
+ this.setCookie("consent", {
933
+ type: type,
934
+ timestamp: new Date().toISOString(),
935
+ preferences: preferences,
936
+ });
937
+
938
+ this.setCookie("preferences", preferences);
939
+ this.currentPreferences = preferences;
940
+ this.applyConsent(preferences);
941
+ this.updateStatusDisplay();
942
+ }
943
+
944
+ clearAll() {
945
+ // Clear consent cookies
946
+ this.deleteCookie("consent");
947
+ this.deleteCookie("preferences");
948
+
949
+ // Unload all category scripts
950
+ this.config.categories.forEach((category) => {
951
+ this.unloadCategoryScripts(category);
952
+ });
953
+
954
+ // Reset preferences
955
+ this.currentPreferences = {};
956
+ this.updateCategoryToggles();
957
+ this.updateStatusDisplay();
958
+
959
+ // Show banner again
960
+ if (this.config.autoShow) {
961
+ this.showBanner();
962
+ }
963
+ }
964
+
965
+ /**
966
+ * Update status display for demo purposes
967
+ */
968
+ updateStatusDisplay() {
969
+ if (!this.elements.statusDisplay) return;
970
+
971
+ const consent = this.getCookie("consent");
972
+ const preferences = this.getCookie("preferences") || {};
973
+
974
+ // Clear existing content safely
975
+ this.elements.statusDisplay.replaceChildren();
976
+
977
+ // Create status elements using safe DOM manipulation
978
+ const statusTitle = document.createElement('strong');
979
+ statusTitle.textContent = 'Consent Status:';
980
+ this.elements.statusDisplay.appendChild(statusTitle);
981
+ this.elements.statusDisplay.appendChild(document.createElement('br'));
982
+
983
+ const typeText = document.createTextNode(`Type: ${consent ? consent.type : 'Not set'}`);
984
+ this.elements.statusDisplay.appendChild(typeText);
985
+ this.elements.statusDisplay.appendChild(document.createElement('br'));
986
+
987
+ const timestampText = document.createTextNode(`Timestamp: ${
988
+ consent ? new Date(consent.timestamp).toLocaleString() : 'N/A'
989
+ }`);
990
+ this.elements.statusDisplay.appendChild(timestampText);
991
+ this.elements.statusDisplay.appendChild(document.createElement('br'));
992
+ this.elements.statusDisplay.appendChild(document.createElement('br'));
993
+
994
+ const categoriesTitle = document.createElement('strong');
995
+ categoriesTitle.textContent = 'Categories:';
996
+ this.elements.statusDisplay.appendChild(categoriesTitle);
997
+ this.elements.statusDisplay.appendChild(document.createElement('br'));
998
+
999
+ const essentialText = document.createTextNode(`Essential: ${preferences.essential ? '✅' : '❌'}`);
1000
+ this.elements.statusDisplay.appendChild(essentialText);
1001
+ this.elements.statusDisplay.appendChild(document.createElement('br'));
1002
+
1003
+ this.config.categories.forEach((category) => {
1004
+ const categoryText = document.createTextNode(`${category}: ${preferences[category] ? '✅' : '❌'}`);
1005
+ this.elements.statusDisplay.appendChild(categoryText);
1006
+ this.elements.statusDisplay.appendChild(document.createElement('br'));
1007
+ });
1008
+ }
1009
+
1010
+ /**
1011
+ * Public API methods
1012
+ */
1013
+ getConsent() {
1014
+ try {
1015
+ if (!this.isInitialized) {
1016
+ this.logError('GDPR system not initialized - cannot get consent');
1017
+ return null;
1018
+ }
1019
+ return this.getCookie("consent");
1020
+ } catch (error) {
1021
+ this.logError('Failed to get consent');
1022
+ return null;
1023
+ }
1024
+ }
1025
+
1026
+ getPreferences() {
1027
+ try {
1028
+ if (!this.isInitialized) {
1029
+ this.logError('GDPR system not initialized - cannot get preferences');
1030
+ return {};
1031
+ }
1032
+ return this.getCookie("preferences") || {};
1033
+ } catch (error) {
1034
+ this.logError('Failed to get preferences');
1035
+ return {};
1036
+ }
1037
+ }
1038
+
1039
+ hasConsent(category = null) {
1040
+ try {
1041
+ if (!this.isInitialized) {
1042
+ this.logError('GDPR system not initialized - cannot check consent');
1043
+ return false;
1044
+ }
1045
+
1046
+ if (category !== null && (typeof category !== 'string' || category.trim() === '')) {
1047
+ this.logError('Invalid category provided to hasConsent', { category });
1048
+ return false;
1049
+ }
1050
+
1051
+ const preferences = this.getPreferences();
1052
+ if (category) {
1053
+ return preferences[category] === true;
1054
+ }
1055
+ return Object.keys(preferences).length > 0;
1056
+ } catch (error) {
1057
+ this.logError('Failed to check consent status', { category });
1058
+ return false;
1059
+ }
1060
+ }
1061
+
1062
+ updateCategory(category, enabled) {
1063
+ try {
1064
+ if (!this.isInitialized) {
1065
+ this.logError('GDPR system not initialized - cannot update category');
1066
+ return false;
1067
+ }
1068
+
1069
+ if (!category || typeof category !== 'string' || category.trim() === '') {
1070
+ this.logError('Invalid category provided to updateCategory', { category });
1071
+ return false;
1072
+ }
1073
+
1074
+ if (typeof enabled !== 'boolean') {
1075
+ this.logError('Invalid enabled value provided to updateCategory', { category, enabled });
1076
+ return false;
1077
+ }
1078
+
1079
+ if (!Array.isArray(this.config.categories) || !this.config.categories.includes(category)) {
1080
+ this.logError('Category not found in configuration', { category });
1081
+ return false;
1082
+ }
1083
+
1084
+ const preferences = this.getPreferences();
1085
+ preferences[category] = enabled;
1086
+ this.saveConsent("custom", preferences);
1087
+ return true;
1088
+ } catch (error) {
1089
+ this.logError('Failed to update category', { category, enabled });
1090
+ return false;
1091
+ }
1092
+ }
1093
+
1094
+ addScript(category, scriptConfig) {
1095
+ try {
1096
+ if (!this.isInitialized) {
1097
+ this.logError('GDPR system not initialized - cannot add script');
1098
+ return false;
1099
+ }
1100
+
1101
+ if (!category || typeof category !== 'string' || category.trim() === '') {
1102
+ this.logError('Invalid category provided to addScript', { category });
1103
+ return false;
1104
+ }
1105
+
1106
+ if (!scriptConfig) {
1107
+ this.logError('No script configuration provided to addScript', { category });
1108
+ return false;
1109
+ }
1110
+
1111
+ // Validate script configuration format
1112
+ if (typeof scriptConfig === 'string') {
1113
+ if (scriptConfig.trim() === '') {
1114
+ this.logError('Empty script URL provided', { category });
1115
+ return false;
1116
+ }
1117
+ } else if (typeof scriptConfig === 'object' && scriptConfig !== null) {
1118
+ if (!scriptConfig.src || typeof scriptConfig.src !== 'string' || scriptConfig.src.trim() === '') {
1119
+ this.logError('Invalid script source in configuration', { category });
1120
+ return false;
1121
+ }
1122
+ } else {
1123
+ this.logError('Invalid script configuration format', { category, configType: typeof scriptConfig });
1124
+ return false;
1125
+ }
1126
+
1127
+ if (!this.config.categoryConfig[category]) {
1128
+ this.logError('Category configuration not found for addScript', { category });
1129
+ return false;
1130
+ }
1131
+
1132
+ this.config.categoryConfig[category].scripts.push(scriptConfig);
1133
+
1134
+ // If category is already accepted, load the script immediately
1135
+ if (this.hasConsent(category)) {
1136
+ this.loadScript(scriptConfig, category).catch(() => {
1137
+ this.logError('Failed to load newly added script', { category });
1138
+ });
1139
+ }
1140
+
1141
+ return true;
1142
+ } catch (error) {
1143
+ this.logError('Failed to add script', { category });
1144
+ return false;
1145
+ }
1146
+ }
1147
+
1148
+ onCategoryChange(category, callback) {
1149
+ try {
1150
+ if (!this.isInitialized) {
1151
+ this.logError('GDPR system not initialized - cannot set category change handler');
1152
+ return false;
1153
+ }
1154
+
1155
+ if (!category || typeof category !== 'string' || category.trim() === '') {
1156
+ this.logError('Invalid category provided to onCategoryChange', { category });
1157
+ return false;
1158
+ }
1159
+
1160
+ if (!callback || typeof callback !== 'function') {
1161
+ this.logError('Invalid callback provided to onCategoryChange', { category });
1162
+ return false;
1163
+ }
1164
+
1165
+ if (!this.config.categoryConfig[category]) {
1166
+ this.config.categoryConfig[category] = {
1167
+ title: `${category} Cookies`,
1168
+ description: `Cookies for ${category} functionality`,
1169
+ scripts: [],
1170
+ };
1171
+ }
1172
+
1173
+ const config = this.config.categoryConfig[category];
1174
+ const originalOnAccept = config.onAccept;
1175
+ const originalOnDecline = config.onDecline;
1176
+
1177
+ config.onAccept = (cat) => {
1178
+ try {
1179
+ if (originalOnAccept && typeof originalOnAccept === 'function') {
1180
+ originalOnAccept(cat);
1181
+ }
1182
+ callback(cat, true);
1183
+ } catch (callbackError) {
1184
+ this.logError('Error in category accept callback', { category: cat });
1185
+ }
1186
+ };
1187
+
1188
+ config.onDecline = (cat) => {
1189
+ try {
1190
+ if (originalOnDecline && typeof originalOnDecline === 'function') {
1191
+ originalOnDecline(cat);
1192
+ }
1193
+ callback(cat, false);
1194
+ } catch (callbackError) {
1195
+ this.logError('Error in category decline callback', { category: cat });
1196
+ }
1197
+ };
1198
+
1199
+ return true;
1200
+ } catch (error) {
1201
+ this.logError('Failed to set category change handler', { category });
1202
+ return false;
1203
+ }
1204
+ }
1205
+ }
1206
+
1207
+ // Create global instance
1208
+ const gdprInstance = new GDPRCookies();
1209
+
1210
+ // Expose to global scope with error handling wrappers
1211
+ window.GDPRCookies = {
1212
+ init: (options) => {
1213
+ try {
1214
+ return gdprInstance.init(options);
1215
+ } catch (error) {
1216
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
1217
+ console.error('GDPR: Failed to initialize');
1218
+ }
1219
+ return false;
1220
+ }
1221
+ },
1222
+ getConsent: () => {
1223
+ try {
1224
+ return gdprInstance.getConsent();
1225
+ } catch (error) {
1226
+ return null;
1227
+ }
1228
+ },
1229
+ getPreferences: () => {
1230
+ try {
1231
+ return gdprInstance.getPreferences();
1232
+ } catch (error) {
1233
+ return {};
1234
+ }
1235
+ },
1236
+ hasConsent: (category) => {
1237
+ try {
1238
+ return gdprInstance.hasConsent(category);
1239
+ } catch (error) {
1240
+ return false;
1241
+ }
1242
+ },
1243
+ updateCategory: (category, enabled) => {
1244
+ try {
1245
+ return gdprInstance.updateCategory(category, enabled);
1246
+ } catch (error) {
1247
+ return false;
1248
+ }
1249
+ },
1250
+ addScript: (category, script) => {
1251
+ try {
1252
+ return gdprInstance.addScript(category, script);
1253
+ } catch (error) {
1254
+ return false;
1255
+ }
1256
+ },
1257
+ onCategoryChange: (category, callback) => {
1258
+ try {
1259
+ return gdprInstance.onCategoryChange(category, callback);
1260
+ } catch (error) {
1261
+ return false;
1262
+ }
1263
+ },
1264
+ showBanner: () => {
1265
+ try {
1266
+ return gdprInstance.showBanner();
1267
+ } catch (error) {
1268
+ return false;
1269
+ }
1270
+ },
1271
+ showPreferences: () => {
1272
+ try {
1273
+ return gdprInstance.showModal();
1274
+ } catch (error) {
1275
+ return false;
1276
+ }
1277
+ },
1278
+ clearAll: () => {
1279
+ try {
1280
+ return gdprInstance.clearAll();
1281
+ } catch (error) {
1282
+ return false;
1283
+ }
1284
+ },
1285
+ };
1286
+
1287
+ // Auto-initialize if config is provided via data attribute
1288
+ document.addEventListener("DOMContentLoaded", () => {
1289
+ try {
1290
+ const configScript = document.querySelector("script[data-gdpr-config]");
1291
+ if (configScript) {
1292
+ const configAttr = configScript.getAttribute("data-gdpr-config");
1293
+ if (!configAttr || configAttr.trim() === '') {
1294
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
1295
+ console.warn('GDPR: Empty configuration attribute found');
1296
+ }
1297
+ return;
1298
+ }
1299
+
1300
+ try {
1301
+ const config = JSON.parse(configAttr);
1302
+ if (typeof config !== 'object' || config === null) {
1303
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
1304
+ console.error('GDPR: Invalid configuration format - must be an object');
1305
+ }
1306
+ return;
1307
+ }
1308
+ window.GDPRCookies.init(config);
1309
+ } catch (parseError) {
1310
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
1311
+ console.error('GDPR: Failed to parse configuration - invalid JSON format');
1312
+ }
1313
+ }
1314
+ }
1315
+ } catch (error) {
1316
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
1317
+ console.error('GDPR: Auto-initialization failed');
1318
+ }
1319
+ }
1320
+ });
1321
+ })(window);