cookiecraft 1.0.7 → 1.0.8
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/README.md +171 -304
- package/dist/cookiecraft.css +1 -1
- package/dist/cookiecraft.esm.js +221 -116
- package/dist/cookiecraft.esm.js.map +1 -1
- package/dist/cookiecraft.js +221 -116
- package/dist/cookiecraft.js.map +1 -1
- package/dist/cookiecraft.min.js +1 -1
- package/dist/cookiecraft.min.js.map +1 -1
- package/dist/stats.html +1 -1
- package/dist/types/blocking/CategoryManager.d.ts +1 -0
- package/dist/types/core/ConsentManager.d.ts +1 -1
- package/dist/types/core/CookieConsent.d.ts +5 -4
- package/dist/types/core/EventEmitter.d.ts +3 -2
- package/dist/types/integrations/GTMConsentMode.d.ts +0 -6
- package/dist/types/types/index.d.ts +3 -7
- package/dist/types/ui/Banner.d.ts +6 -0
- package/dist/types/ui/PreferenceCenter.d.ts +1 -0
- package/package.json +1 -1
package/dist/cookiecraft.js
CHANGED
|
@@ -49,13 +49,20 @@
|
|
|
49
49
|
* Clear consent record from localStorage
|
|
50
50
|
*/
|
|
51
51
|
clear() {
|
|
52
|
-
|
|
52
|
+
try {
|
|
53
|
+
localStorage.removeItem(StorageManager.STORAGE_KEY);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
console.error('Failed to clear consent:', e);
|
|
57
|
+
}
|
|
53
58
|
}
|
|
54
59
|
/**
|
|
55
60
|
* Check if consent record has expired
|
|
56
61
|
*/
|
|
57
62
|
isExpired(consent) {
|
|
58
63
|
const expiry = new Date(consent.expiresAt);
|
|
64
|
+
if (isNaN(expiry.getTime()))
|
|
65
|
+
return true;
|
|
59
66
|
return expiry < new Date();
|
|
60
67
|
}
|
|
61
68
|
/**
|
|
@@ -66,7 +73,6 @@
|
|
|
66
73
|
typeof data.version === 'number' &&
|
|
67
74
|
typeof data.timestamp === 'string' &&
|
|
68
75
|
typeof data.categories === 'object' &&
|
|
69
|
-
typeof data.userAgent === 'string' &&
|
|
70
76
|
typeof data.expiresAt === 'string');
|
|
71
77
|
}
|
|
72
78
|
/**
|
|
@@ -82,11 +88,20 @@
|
|
|
82
88
|
const now = new Date();
|
|
83
89
|
const expiryDate = new Date(now);
|
|
84
90
|
expiryDate.setMonth(expiryDate.getMonth() + StorageManager.EXPIRY_MONTHS);
|
|
91
|
+
// Coerce category values to booleans
|
|
92
|
+
const rawCategories = record.categories;
|
|
93
|
+
const categories = {
|
|
94
|
+
necessary: rawCategories.necessary === true,
|
|
95
|
+
analytics: rawCategories.analytics === true,
|
|
96
|
+
marketing: rawCategories.marketing === true,
|
|
97
|
+
};
|
|
98
|
+
if ('preferences' in rawCategories) {
|
|
99
|
+
categories.preferences = rawCategories.preferences === true;
|
|
100
|
+
}
|
|
85
101
|
return {
|
|
86
102
|
version: typeof record.version === 'number' ? record.version : 1,
|
|
87
103
|
timestamp: typeof record.timestamp === 'string' ? record.timestamp : now.toISOString(),
|
|
88
|
-
categories
|
|
89
|
-
userAgent: typeof record.userAgent === 'string' ? record.userAgent : navigator.userAgent,
|
|
104
|
+
categories,
|
|
90
105
|
expiresAt: typeof record.expiresAt === 'string' ? record.expiresAt : expiryDate.toISOString(),
|
|
91
106
|
};
|
|
92
107
|
}
|
|
@@ -101,6 +116,7 @@
|
|
|
101
116
|
*/
|
|
102
117
|
class ConsentManager {
|
|
103
118
|
constructor(config) {
|
|
119
|
+
this.consent = null;
|
|
104
120
|
this.config = config;
|
|
105
121
|
}
|
|
106
122
|
/**
|
|
@@ -119,6 +135,10 @@
|
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
}
|
|
138
|
+
// Coerce all values to booleans
|
|
139
|
+
for (const key of Object.keys(categories)) {
|
|
140
|
+
categories[key] = categories[key] === true;
|
|
141
|
+
}
|
|
122
142
|
return true;
|
|
123
143
|
}
|
|
124
144
|
/**
|
|
@@ -135,13 +155,12 @@
|
|
|
135
155
|
* Check if user needs to give consent
|
|
136
156
|
*/
|
|
137
157
|
needsConsent() {
|
|
138
|
-
return this.consent ===
|
|
158
|
+
return this.consent === null;
|
|
139
159
|
}
|
|
140
160
|
/**
|
|
141
161
|
* Check if stored consent needs update due to policy change
|
|
142
162
|
*/
|
|
143
163
|
needsUpdate(storedConsent) {
|
|
144
|
-
// Check if policy version has changed
|
|
145
164
|
return storedConsent.version < this.config.revision;
|
|
146
165
|
}
|
|
147
166
|
/**
|
|
@@ -161,7 +180,6 @@
|
|
|
161
180
|
version: this.config.revision,
|
|
162
181
|
timestamp: now.toISOString(),
|
|
163
182
|
categories: Object.assign({}, categories),
|
|
164
|
-
userAgent: navigator.userAgent,
|
|
165
183
|
expiresAt: expiryDate.toISOString(),
|
|
166
184
|
};
|
|
167
185
|
}
|
|
@@ -402,7 +420,6 @@
|
|
|
402
420
|
class CategoryManager {
|
|
403
421
|
constructor() {
|
|
404
422
|
this.categories = new Map();
|
|
405
|
-
// Initialize with common patterns
|
|
406
423
|
this.initializeDefaultPatterns();
|
|
407
424
|
}
|
|
408
425
|
/**
|
|
@@ -439,11 +456,11 @@
|
|
|
439
456
|
}
|
|
440
457
|
/**
|
|
441
458
|
* Initialize default URL patterns for common tracking services
|
|
459
|
+
* Note: GTM is NOT auto-categorized — it should be managed via GTM Consent Mode v2
|
|
442
460
|
*/
|
|
443
461
|
initializeDefaultPatterns() {
|
|
444
462
|
this.categories.set('analytics', [
|
|
445
463
|
'google-analytics.com',
|
|
446
|
-
'googletagmanager.com',
|
|
447
464
|
'analytics.google.com',
|
|
448
465
|
'plausible.io',
|
|
449
466
|
'matomo.org',
|
|
@@ -453,6 +470,7 @@
|
|
|
453
470
|
'amplitude.com',
|
|
454
471
|
]);
|
|
455
472
|
this.categories.set('marketing', [
|
|
473
|
+
'googletagmanager.com',
|
|
456
474
|
'facebook.net',
|
|
457
475
|
'facebook.com/tr',
|
|
458
476
|
'connect.facebook.net',
|
|
@@ -464,6 +482,7 @@
|
|
|
464
482
|
'adroll.com',
|
|
465
483
|
'taboola.com',
|
|
466
484
|
'outbrain.com',
|
|
485
|
+
'tiktok.com',
|
|
467
486
|
]);
|
|
468
487
|
this.categories.set('necessary', []);
|
|
469
488
|
}
|
|
@@ -514,18 +533,47 @@
|
|
|
514
533
|
// Allow hsl/hsla
|
|
515
534
|
if (/^hsla?\(\s*[\d\s,./%deg]+\)$/.test(trimmed))
|
|
516
535
|
return trimmed;
|
|
517
|
-
// Allow CSS named colors (basic set)
|
|
518
|
-
|
|
536
|
+
// Allow CSS named colors (basic set) but block CSS keywords that could be abused
|
|
537
|
+
const CSS_KEYWORDS = ['inherit', 'initial', 'unset', 'revert', 'revert-layer'];
|
|
538
|
+
if (/^[a-zA-Z]+$/.test(trimmed) && !CSS_KEYWORDS.includes(trimmed.toLowerCase())) {
|
|
519
539
|
return trimmed;
|
|
540
|
+
}
|
|
520
541
|
return '';
|
|
521
542
|
}
|
|
522
543
|
|
|
544
|
+
/**
|
|
545
|
+
* Normalize any supported color format to 6-digit hex
|
|
546
|
+
* Supports: #RGB, #RRGGBB, #RRGGBBAA, named colors
|
|
547
|
+
* Returns null if conversion fails
|
|
548
|
+
*/
|
|
549
|
+
function normalizeToHex6(color) {
|
|
550
|
+
const trimmed = color.trim();
|
|
551
|
+
// Already 6-digit hex
|
|
552
|
+
if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) {
|
|
553
|
+
return trimmed;
|
|
554
|
+
}
|
|
555
|
+
// 3-digit hex → expand to 6-digit
|
|
556
|
+
if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) {
|
|
557
|
+
const r = trimmed[1];
|
|
558
|
+
const g = trimmed[2];
|
|
559
|
+
const b = trimmed[3];
|
|
560
|
+
return `#${r}${r}${g}${g}${b}${b}`;
|
|
561
|
+
}
|
|
562
|
+
// 8-digit hex (with alpha) → strip alpha
|
|
563
|
+
if (/^#[0-9a-fA-F]{8}$/.test(trimmed)) {
|
|
564
|
+
return trimmed.substring(0, 7);
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
523
568
|
/**
|
|
524
569
|
* Adjust a hex color brightness by a percentage
|
|
525
570
|
* Negative = darker, positive = lighter
|
|
526
571
|
*/
|
|
527
572
|
function adjustColorBrightness(color, percent) {
|
|
528
|
-
const
|
|
573
|
+
const hex6 = normalizeToHex6(color);
|
|
574
|
+
if (!hex6)
|
|
575
|
+
return color;
|
|
576
|
+
const hex = hex6.replace('#', '');
|
|
529
577
|
const r = parseInt(hex.substring(0, 2), 16);
|
|
530
578
|
const g = parseInt(hex.substring(2, 4), 16);
|
|
531
579
|
const b = parseInt(hex.substring(4, 6), 16);
|
|
@@ -545,8 +593,13 @@
|
|
|
545
593
|
function buildColorStyle(safeColor) {
|
|
546
594
|
if (!safeColor)
|
|
547
595
|
return '';
|
|
548
|
-
|
|
549
|
-
|
|
596
|
+
// Only generate hover color for hex colors
|
|
597
|
+
const hex6 = normalizeToHex6(safeColor);
|
|
598
|
+
if (!hex6) {
|
|
599
|
+
return `--cc-primary: ${safeColor};`;
|
|
600
|
+
}
|
|
601
|
+
const hover = adjustColorBrightness(hex6, -15);
|
|
602
|
+
return `--cc-primary: ${hex6}; --cc-primary-hover: ${hover};`;
|
|
550
603
|
}
|
|
551
604
|
|
|
552
605
|
/**
|
|
@@ -555,6 +608,8 @@
|
|
|
555
608
|
class Banner {
|
|
556
609
|
constructor(config, eventEmitter) {
|
|
557
610
|
this.element = null;
|
|
611
|
+
this.hideTimeout = null;
|
|
612
|
+
this.previousActiveElement = null;
|
|
558
613
|
this.config = config;
|
|
559
614
|
this.eventEmitter = eventEmitter;
|
|
560
615
|
}
|
|
@@ -562,8 +617,14 @@
|
|
|
562
617
|
* Show the banner
|
|
563
618
|
*/
|
|
564
619
|
show() {
|
|
620
|
+
// Clear any pending hide timeout
|
|
621
|
+
if (this.hideTimeout) {
|
|
622
|
+
clearTimeout(this.hideTimeout);
|
|
623
|
+
this.hideTimeout = null;
|
|
624
|
+
}
|
|
565
625
|
const append = () => {
|
|
566
626
|
if (!this.element) {
|
|
627
|
+
this.previousActiveElement = document.activeElement;
|
|
567
628
|
this.element = this.createDOM();
|
|
568
629
|
document.body.appendChild(this.element);
|
|
569
630
|
this.attachListeners();
|
|
@@ -576,9 +637,9 @@
|
|
|
576
637
|
// Disable page interaction if configured
|
|
577
638
|
if (this.config.disablePageInteraction) {
|
|
578
639
|
document.body.style.overflow = 'hidden';
|
|
640
|
+
this.trapFocus();
|
|
579
641
|
}
|
|
580
642
|
};
|
|
581
|
-
// Wait for body if not yet available
|
|
582
643
|
if (!document.body) {
|
|
583
644
|
document.addEventListener('DOMContentLoaded', append);
|
|
584
645
|
return;
|
|
@@ -591,22 +652,30 @@
|
|
|
591
652
|
hide() {
|
|
592
653
|
var _a;
|
|
593
654
|
(_a = this.element) === null || _a === void 0 ? void 0 : _a.classList.remove('is-visible');
|
|
594
|
-
// Re-enable page interaction
|
|
595
655
|
if (this.config.disablePageInteraction) {
|
|
596
656
|
document.body.style.overflow = '';
|
|
597
657
|
}
|
|
598
|
-
setTimeout(() => {
|
|
658
|
+
this.hideTimeout = setTimeout(() => {
|
|
599
659
|
this.destroy();
|
|
600
|
-
}, 300);
|
|
660
|
+
}, 300);
|
|
601
661
|
}
|
|
602
662
|
/**
|
|
603
663
|
* Destroy the banner
|
|
604
664
|
*/
|
|
605
665
|
destroy() {
|
|
666
|
+
if (this.hideTimeout) {
|
|
667
|
+
clearTimeout(this.hideTimeout);
|
|
668
|
+
this.hideTimeout = null;
|
|
669
|
+
}
|
|
606
670
|
if (this.element) {
|
|
607
671
|
this.element.remove();
|
|
608
672
|
this.element = null;
|
|
609
673
|
}
|
|
674
|
+
// Restore focus
|
|
675
|
+
if (this.previousActiveElement && document.contains(this.previousActiveElement)) {
|
|
676
|
+
this.previousActiveElement.focus();
|
|
677
|
+
this.previousActiveElement = null;
|
|
678
|
+
}
|
|
610
679
|
}
|
|
611
680
|
/**
|
|
612
681
|
* Create DOM structure for banner
|
|
@@ -617,12 +686,14 @@
|
|
|
617
686
|
const position = this.config.position || 'bottom';
|
|
618
687
|
const layout = this.config.layout || 'bar';
|
|
619
688
|
const backdropBlur = this.config.backdropBlur !== false;
|
|
689
|
+
const isModal = this.config.disablePageInteraction;
|
|
620
690
|
const safeColor = this.config.primaryColor ? sanitizeColor(this.config.primaryColor) : '';
|
|
621
691
|
const colorStyle = buildColorStyle(safeColor);
|
|
622
692
|
const template = `
|
|
623
693
|
<div
|
|
624
694
|
class="cc-banner cc-banner--${escapeHtml(position)} cc-banner--${escapeHtml(layout)} ${backdropBlur ? 'cc-backdrop-blur' : ''}"
|
|
625
|
-
role="region"
|
|
695
|
+
role="${isModal ? 'dialog' : 'region'}"
|
|
696
|
+
${isModal ? 'aria-modal="true"' : ''}
|
|
626
697
|
aria-label="Cookie consent"
|
|
627
698
|
aria-live="polite"
|
|
628
699
|
data-theme="${escapeHtml(theme)}"
|
|
@@ -631,7 +702,7 @@
|
|
|
631
702
|
<div class="cc-banner__container">
|
|
632
703
|
<div class="cc-banner__content">
|
|
633
704
|
<h2 class="cc-banner__title">
|
|
634
|
-
${escapeHtml(translations.title || '
|
|
705
|
+
${escapeHtml(translations.title || 'We use cookies')}
|
|
635
706
|
</h2>
|
|
636
707
|
<p class="cc-banner__description">
|
|
637
708
|
${this.getDescriptionHTML()}
|
|
@@ -641,23 +712,23 @@
|
|
|
641
712
|
<button
|
|
642
713
|
class="cc-btn cc-btn--ghost"
|
|
643
714
|
data-action="reject"
|
|
644
|
-
aria-label="${escapeHtml(translations.rejectAll || '
|
|
715
|
+
aria-label="${escapeHtml(translations.rejectAll || 'Essentials only')}"
|
|
645
716
|
>
|
|
646
|
-
${escapeHtml(translations.rejectAll || '
|
|
717
|
+
${escapeHtml(translations.rejectAll || 'Essentials only')}
|
|
647
718
|
</button>
|
|
648
719
|
<button
|
|
649
720
|
class="cc-btn cc-btn--tertiary"
|
|
650
721
|
data-action="customize"
|
|
651
|
-
aria-label="${escapeHtml(translations.customize || '
|
|
722
|
+
aria-label="${escapeHtml(translations.customize || 'Customize')}"
|
|
652
723
|
>
|
|
653
|
-
${escapeHtml(translations.customize || '
|
|
724
|
+
${escapeHtml(translations.customize || 'Customize')}
|
|
654
725
|
</button>
|
|
655
726
|
<button
|
|
656
727
|
class="cc-btn cc-btn--accept"
|
|
657
728
|
data-action="accept"
|
|
658
|
-
aria-label="${escapeHtml(translations.acceptAll || '
|
|
729
|
+
aria-label="${escapeHtml(translations.acceptAll || 'Accept all')}"
|
|
659
730
|
>
|
|
660
|
-
${escapeHtml(translations.acceptAll || '
|
|
731
|
+
${escapeHtml(translations.acceptAll || 'Accept all')}
|
|
661
732
|
</button>
|
|
662
733
|
</div>
|
|
663
734
|
</div>
|
|
@@ -689,10 +760,8 @@
|
|
|
689
760
|
break;
|
|
690
761
|
}
|
|
691
762
|
});
|
|
692
|
-
// Keyboard support
|
|
693
763
|
(_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
|
|
694
764
|
if (e.key === 'Escape' && this.config.disablePageInteraction) {
|
|
695
|
-
// Allow ESC to close if page interaction is disabled
|
|
696
765
|
this.handleRejectAll();
|
|
697
766
|
}
|
|
698
767
|
});
|
|
@@ -701,36 +770,24 @@
|
|
|
701
770
|
* Handle accept all action
|
|
702
771
|
*/
|
|
703
772
|
handleAcceptAll() {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
marketing: true,
|
|
709
|
-
};
|
|
710
|
-
// Only add preferences if it's configured
|
|
711
|
-
if ((_a = this.config.categories) === null || _a === void 0 ? void 0 : _a.preferences) {
|
|
712
|
-
allCategories.preferences = true;
|
|
773
|
+
const allCategories = { necessary: true, analytics: true, marketing: true };
|
|
774
|
+
// Add all configured categories
|
|
775
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
776
|
+
allCategories[key] = true;
|
|
713
777
|
}
|
|
714
778
|
this.eventEmitter.emit('consent:accept', allCategories);
|
|
715
|
-
(_c = (_b = this.config).onAccept) === null || _c === void 0 ? void 0 : _c.call(_b, allCategories);
|
|
716
779
|
this.hide();
|
|
717
780
|
}
|
|
718
781
|
/**
|
|
719
782
|
* Handle reject all action
|
|
720
783
|
*/
|
|
721
784
|
handleRejectAll() {
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
necessary
|
|
725
|
-
|
|
726
|
-
marketing: false,
|
|
727
|
-
};
|
|
728
|
-
// Only add preferences if it's configured
|
|
729
|
-
if ((_a = this.config.categories) === null || _a === void 0 ? void 0 : _a.preferences) {
|
|
730
|
-
necessaryOnly.preferences = false;
|
|
785
|
+
const necessaryOnly = { necessary: true, analytics: false, marketing: false };
|
|
786
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
787
|
+
if (key !== 'necessary')
|
|
788
|
+
necessaryOnly[key] = false;
|
|
731
789
|
}
|
|
732
790
|
this.eventEmitter.emit('consent:reject', necessaryOnly);
|
|
733
|
-
(_c = (_b = this.config).onReject) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
734
791
|
this.hide();
|
|
735
792
|
}
|
|
736
793
|
/**
|
|
@@ -740,17 +797,41 @@
|
|
|
740
797
|
this.eventEmitter.emit('preferences:show');
|
|
741
798
|
this.hide();
|
|
742
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* Trap focus within banner (when disablePageInteraction is true)
|
|
802
|
+
*/
|
|
803
|
+
trapFocus() {
|
|
804
|
+
var _a, _b;
|
|
805
|
+
const focusableElements = (_a = this.element) === null || _a === void 0 ? void 0 : _a.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
806
|
+
if (!focusableElements || focusableElements.length === 0)
|
|
807
|
+
return;
|
|
808
|
+
const firstFocusable = focusableElements[0];
|
|
809
|
+
const lastFocusable = focusableElements[focusableElements.length - 1];
|
|
810
|
+
firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus();
|
|
811
|
+
(_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
|
|
812
|
+
if (e.key === 'Tab') {
|
|
813
|
+
if (e.shiftKey && document.activeElement === firstFocusable) {
|
|
814
|
+
e.preventDefault();
|
|
815
|
+
lastFocusable.focus();
|
|
816
|
+
}
|
|
817
|
+
else if (!e.shiftKey && document.activeElement === lastFocusable) {
|
|
818
|
+
e.preventDefault();
|
|
819
|
+
firstFocusable.focus();
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
743
824
|
/**
|
|
744
825
|
* Generate description HTML with privacy policy link
|
|
745
826
|
*/
|
|
746
827
|
getDescriptionHTML() {
|
|
747
828
|
const translations = this.config.translations || {};
|
|
748
|
-
const defaultDescription = '
|
|
829
|
+
const defaultDescription = 'We use cookies to improve your experience on our site. You can choose which cookies you accept.';
|
|
749
830
|
const description = escapeHtml(translations.description || defaultDescription);
|
|
750
831
|
if (translations.privacyPolicyUrl) {
|
|
751
832
|
const safeUrl = sanitizeUrl(translations.privacyPolicyUrl);
|
|
752
833
|
if (safeUrl) {
|
|
753
|
-
const linkLabel = escapeHtml(translations.privacyPolicyLabel || '
|
|
834
|
+
const linkLabel = escapeHtml(translations.privacyPolicyLabel || 'Privacy Policy');
|
|
754
835
|
return `${description} <a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${linkLabel}</a>`;
|
|
755
836
|
}
|
|
756
837
|
}
|
|
@@ -764,6 +845,7 @@
|
|
|
764
845
|
class PreferenceCenter {
|
|
765
846
|
constructor(config, eventEmitter, currentConsent) {
|
|
766
847
|
this.element = null;
|
|
848
|
+
this.previousActiveElement = null;
|
|
767
849
|
this.config = config;
|
|
768
850
|
this.eventEmitter = eventEmitter;
|
|
769
851
|
this.currentConsent = currentConsent;
|
|
@@ -774,13 +856,13 @@
|
|
|
774
856
|
show() {
|
|
775
857
|
const append = () => {
|
|
776
858
|
if (!this.element) {
|
|
859
|
+
this.previousActiveElement = document.activeElement;
|
|
777
860
|
this.element = this.createDOM();
|
|
778
861
|
document.body.appendChild(this.element);
|
|
779
862
|
this.attachListeners();
|
|
780
863
|
}
|
|
781
864
|
this.element.classList.add('is-visible');
|
|
782
865
|
this.trapFocus();
|
|
783
|
-
// Prevent body scroll
|
|
784
866
|
document.body.style.overflow = 'hidden';
|
|
785
867
|
};
|
|
786
868
|
if (!document.body) {
|
|
@@ -796,6 +878,11 @@
|
|
|
796
878
|
var _a;
|
|
797
879
|
(_a = this.element) === null || _a === void 0 ? void 0 : _a.classList.remove('is-visible');
|
|
798
880
|
document.body.style.overflow = '';
|
|
881
|
+
// Restore focus to triggering element
|
|
882
|
+
if (this.previousActiveElement && document.contains(this.previousActiveElement)) {
|
|
883
|
+
this.previousActiveElement.focus();
|
|
884
|
+
this.previousActiveElement = null;
|
|
885
|
+
}
|
|
799
886
|
setTimeout(() => {
|
|
800
887
|
this.destroy();
|
|
801
888
|
}, 300);
|
|
@@ -830,7 +917,7 @@
|
|
|
830
917
|
<polyline points="15 3 21 3 21 9"/>
|
|
831
918
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
832
919
|
</svg>
|
|
833
|
-
${escapeHtml(translations.privacyPolicyLabel || '
|
|
920
|
+
${escapeHtml(translations.privacyPolicyLabel || 'Privacy Policy')}
|
|
834
921
|
</a>
|
|
835
922
|
`;
|
|
836
923
|
})()
|
|
@@ -848,7 +935,7 @@
|
|
|
848
935
|
<div class="cc-modal__content">
|
|
849
936
|
<div class="cc-modal__header">
|
|
850
937
|
<h2 id="cc-modal-title">
|
|
851
|
-
${escapeHtml(translations.preferencesTitle || translations.title || '
|
|
938
|
+
${escapeHtml(translations.preferencesTitle || translations.title || 'Cookie Preferences')}
|
|
852
939
|
</h2>
|
|
853
940
|
</div>
|
|
854
941
|
|
|
@@ -865,13 +952,13 @@
|
|
|
865
952
|
class="cc-btn cc-btn--secondary"
|
|
866
953
|
data-action="reject"
|
|
867
954
|
>
|
|
868
|
-
${escapeHtml(translations.essentialsOnly || '
|
|
955
|
+
${escapeHtml(translations.essentialsOnly || 'Essentials only')}
|
|
869
956
|
</button>
|
|
870
957
|
<button
|
|
871
958
|
class="cc-btn cc-btn--primary"
|
|
872
959
|
data-action="save"
|
|
873
960
|
>
|
|
874
|
-
${escapeHtml(translations.savePreferences || '
|
|
961
|
+
${escapeHtml(translations.savePreferences || 'Save preferences')}
|
|
875
962
|
</button>
|
|
876
963
|
</div>
|
|
877
964
|
</div>
|
|
@@ -889,7 +976,7 @@
|
|
|
889
976
|
const categories = Object.entries(this.config.categories);
|
|
890
977
|
return categories
|
|
891
978
|
.map(([key, config]) => {
|
|
892
|
-
const checked = this.currentConsent[key];
|
|
979
|
+
const checked = this.currentConsent[key] === true;
|
|
893
980
|
const disabled = config.readOnly;
|
|
894
981
|
return `
|
|
895
982
|
<div class="cc-category">
|
|
@@ -936,13 +1023,16 @@
|
|
|
936
1023
|
* Handle save preferences
|
|
937
1024
|
*/
|
|
938
1025
|
handleSave() {
|
|
939
|
-
var _a
|
|
1026
|
+
var _a;
|
|
940
1027
|
const checkboxes = (_a = this.element) === null || _a === void 0 ? void 0 : _a.querySelectorAll('input[data-category]');
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1028
|
+
// Initialize all configured categories to false
|
|
1029
|
+
const categories = { necessary: true, analytics: false, marketing: false };
|
|
1030
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
1031
|
+
if (key !== 'necessary') {
|
|
1032
|
+
categories[key] = false;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// Override with actual checkbox values
|
|
946
1036
|
checkboxes === null || checkboxes === void 0 ? void 0 : checkboxes.forEach((checkbox) => {
|
|
947
1037
|
if (checkbox instanceof HTMLInputElement) {
|
|
948
1038
|
const category = checkbox.getAttribute('data-category');
|
|
@@ -952,22 +1042,16 @@
|
|
|
952
1042
|
}
|
|
953
1043
|
});
|
|
954
1044
|
this.eventEmitter.emit('consent:update', categories);
|
|
955
|
-
(_c = (_b = this.config).onChange) === null || _c === void 0 ? void 0 : _c.call(_b, categories);
|
|
956
1045
|
this.hide();
|
|
957
1046
|
}
|
|
958
1047
|
/**
|
|
959
1048
|
* Handle reject all
|
|
960
1049
|
*/
|
|
961
1050
|
handleRejectAll() {
|
|
962
|
-
|
|
963
|
-
const
|
|
964
|
-
necessary
|
|
965
|
-
|
|
966
|
-
marketing: false,
|
|
967
|
-
};
|
|
968
|
-
// Only add preferences if it's configured
|
|
969
|
-
if ((_a = this.config.categories) === null || _a === void 0 ? void 0 : _a.preferences) {
|
|
970
|
-
necessaryOnly.preferences = false;
|
|
1051
|
+
const necessaryOnly = { necessary: true, analytics: false, marketing: false };
|
|
1052
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
1053
|
+
if (key !== 'necessary')
|
|
1054
|
+
necessaryOnly[key] = false;
|
|
971
1055
|
}
|
|
972
1056
|
this.eventEmitter.emit('consent:reject', necessaryOnly);
|
|
973
1057
|
this.hide();
|
|
@@ -982,9 +1066,7 @@
|
|
|
982
1066
|
return;
|
|
983
1067
|
const firstFocusable = focusableElements[0];
|
|
984
1068
|
const lastFocusable = focusableElements[focusableElements.length - 1];
|
|
985
|
-
// Focus first element
|
|
986
1069
|
firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus();
|
|
987
|
-
// Trap focus
|
|
988
1070
|
(_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
|
|
989
1071
|
if (e.key === 'Tab') {
|
|
990
1072
|
if (e.shiftKey && document.activeElement === firstFocusable) {
|
|
@@ -1072,7 +1154,7 @@
|
|
|
1072
1154
|
<div
|
|
1073
1155
|
class="cc-widget cc-widget--${escapeHtml(widgetPosition)} cc-widget--${escapeHtml(widgetStyle)}"
|
|
1074
1156
|
role="button"
|
|
1075
|
-
aria-label="${escapeHtml(translations.cookieSettings || '
|
|
1157
|
+
aria-label="${escapeHtml(translations.cookieSettings || 'Cookie settings')}"
|
|
1076
1158
|
tabindex="0"
|
|
1077
1159
|
data-theme="${escapeHtml(theme)}"
|
|
1078
1160
|
style="${colorStyle}"
|
|
@@ -1123,11 +1205,6 @@
|
|
|
1123
1205
|
|
|
1124
1206
|
/**
|
|
1125
1207
|
* GTMConsentMode - Full integration with Google Consent Mode v2
|
|
1126
|
-
*
|
|
1127
|
-
* Implements all required signals:
|
|
1128
|
-
* - ad_storage, ad_user_data, ad_personalization, analytics_storage (core GCM v2)
|
|
1129
|
-
* - functionality_storage, personalization_storage, security_storage (non-core)
|
|
1130
|
-
* - wait_for_update, url_passthrough, ads_data_redaction (advanced features)
|
|
1131
1208
|
*/
|
|
1132
1209
|
class GTMConsentMode {
|
|
1133
1210
|
constructor(dataLayerManager, config) {
|
|
@@ -1147,15 +1224,13 @@
|
|
|
1147
1224
|
analytics_storage: 'denied',
|
|
1148
1225
|
functionality_storage: 'denied',
|
|
1149
1226
|
personalization_storage: 'denied',
|
|
1150
|
-
security_storage: 'granted',
|
|
1227
|
+
security_storage: 'granted',
|
|
1151
1228
|
};
|
|
1152
|
-
// Add wait_for_update to give CMP time to restore returning visitor consent
|
|
1153
1229
|
const waitForUpdate = (_a = this.config.gtmWaitForUpdate) !== null && _a !== void 0 ? _a : 500;
|
|
1154
1230
|
if (waitForUpdate > 0) {
|
|
1155
1231
|
defaults['wait_for_update'] = waitForUpdate;
|
|
1156
1232
|
}
|
|
1157
1233
|
this.dataLayerManager.pushConsent('default', defaults);
|
|
1158
|
-
// Set advanced features via gtag('set', ...)
|
|
1159
1234
|
if (this.config.gtmUrlPassthrough) {
|
|
1160
1235
|
this.dataLayerManager.pushSet('url_passthrough', true);
|
|
1161
1236
|
}
|
|
@@ -1165,7 +1240,6 @@
|
|
|
1165
1240
|
}
|
|
1166
1241
|
/**
|
|
1167
1242
|
* Update consent state based on user choices
|
|
1168
|
-
* Called both on new consent and on page load for returning visitors
|
|
1169
1243
|
*/
|
|
1170
1244
|
updateConsent(categories) {
|
|
1171
1245
|
const gtmConsent = this.mapCategoriesToGTM(categories);
|
|
@@ -1175,14 +1249,19 @@
|
|
|
1175
1249
|
* Map consent categories to GTM Consent Mode v2 format
|
|
1176
1250
|
*/
|
|
1177
1251
|
mapCategoriesToGTM(categories) {
|
|
1252
|
+
// When preferences category is not configured, default functionality to granted
|
|
1253
|
+
const hasPreferencesCategory = 'preferences' in this.config.categories;
|
|
1254
|
+
const preferencesGranted = hasPreferencesCategory
|
|
1255
|
+
? categories.preferences === true
|
|
1256
|
+
: true;
|
|
1178
1257
|
return {
|
|
1179
1258
|
ad_storage: categories.marketing ? 'granted' : 'denied',
|
|
1180
1259
|
ad_user_data: categories.marketing ? 'granted' : 'denied',
|
|
1181
1260
|
ad_personalization: categories.marketing ? 'granted' : 'denied',
|
|
1182
1261
|
analytics_storage: categories.analytics ? 'granted' : 'denied',
|
|
1183
|
-
functionality_storage:
|
|
1184
|
-
personalization_storage:
|
|
1185
|
-
security_storage: 'granted',
|
|
1262
|
+
functionality_storage: preferencesGranted ? 'granted' : 'denied',
|
|
1263
|
+
personalization_storage: preferencesGranted ? 'granted' : 'denied',
|
|
1264
|
+
security_storage: 'granted',
|
|
1186
1265
|
};
|
|
1187
1266
|
}
|
|
1188
1267
|
}
|
|
@@ -1327,6 +1406,16 @@
|
|
|
1327
1406
|
this.preferenceCenter = null;
|
|
1328
1407
|
this.floatingWidget = null;
|
|
1329
1408
|
this.gtmIntegration = null;
|
|
1409
|
+
this.hideTimeout = null;
|
|
1410
|
+
// SSR guard
|
|
1411
|
+
if (typeof window === 'undefined') {
|
|
1412
|
+
this.config = config;
|
|
1413
|
+
this.consentManager = null;
|
|
1414
|
+
this.storageManager = null;
|
|
1415
|
+
this.eventEmitter = null;
|
|
1416
|
+
this.scriptBlocker = null;
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1330
1419
|
this.config = this.validateConfig(config);
|
|
1331
1420
|
this.consentManager = new ConsentManager(this.config);
|
|
1332
1421
|
this.storageManager = new StorageManager();
|
|
@@ -1335,25 +1424,32 @@
|
|
|
1335
1424
|
if (this.config.gtmConsentMode) {
|
|
1336
1425
|
this.gtmIntegration = new GTMConsentMode(new DataLayerManager(), this.config);
|
|
1337
1426
|
}
|
|
1338
|
-
// Listen for
|
|
1339
|
-
this.eventEmitter.on('preferences:show', () => {
|
|
1340
|
-
this.showPreferences();
|
|
1341
|
-
});
|
|
1342
|
-
// Listen for consent updates
|
|
1427
|
+
// Listen for consent events — callbacks are fired AFTER consent is persisted
|
|
1343
1428
|
this.eventEmitter.on('consent:accept', (categories) => {
|
|
1429
|
+
var _a, _b;
|
|
1344
1430
|
this.updateConsent(categories);
|
|
1431
|
+
(_b = (_a = this.config).onAccept) === null || _b === void 0 ? void 0 : _b.call(_a, categories);
|
|
1345
1432
|
});
|
|
1346
1433
|
this.eventEmitter.on('consent:reject', (categories) => {
|
|
1434
|
+
var _a, _b;
|
|
1347
1435
|
this.updateConsent(categories);
|
|
1436
|
+
(_b = (_a = this.config).onReject) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1348
1437
|
});
|
|
1349
1438
|
this.eventEmitter.on('consent:update', (categories) => {
|
|
1439
|
+
var _a, _b;
|
|
1350
1440
|
this.updateConsent(categories);
|
|
1441
|
+
(_b = (_a = this.config).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, categories);
|
|
1442
|
+
});
|
|
1443
|
+
this.eventEmitter.on('preferences:show', () => {
|
|
1444
|
+
this.showPreferences();
|
|
1351
1445
|
});
|
|
1352
1446
|
}
|
|
1353
1447
|
/**
|
|
1354
1448
|
* Initialize the cookie consent system
|
|
1355
1449
|
*/
|
|
1356
1450
|
init() {
|
|
1451
|
+
if (typeof window === 'undefined')
|
|
1452
|
+
return;
|
|
1357
1453
|
// 1. Start blocking scripts immediately
|
|
1358
1454
|
this.scriptBlocker.init();
|
|
1359
1455
|
// 2. Set GTM default consent BEFORE checking storage
|
|
@@ -1363,35 +1459,28 @@
|
|
|
1363
1459
|
// 3. Check for existing consent
|
|
1364
1460
|
const storedConsent = this.storageManager.load();
|
|
1365
1461
|
if (storedConsent && !this.storageManager.isExpired(storedConsent)) {
|
|
1366
|
-
// Valid consent exists
|
|
1367
1462
|
if (this.consentManager.needsUpdate(storedConsent)) {
|
|
1368
|
-
// Policy updated, show banner again
|
|
1369
1463
|
if (this.config.autoShow) {
|
|
1370
1464
|
this.showBanner();
|
|
1371
1465
|
}
|
|
1372
1466
|
}
|
|
1373
1467
|
else {
|
|
1374
|
-
// Apply stored consent
|
|
1375
1468
|
this.applyConsent(storedConsent.categories);
|
|
1376
|
-
// Restore GTM consent for returning visitors (within wait_for_update window)
|
|
1377
1469
|
if (this.gtmIntegration) {
|
|
1378
1470
|
this.gtmIntegration.updateConsent(storedConsent.categories);
|
|
1379
1471
|
}
|
|
1380
1472
|
this.eventEmitter.emit('consent:load', storedConsent);
|
|
1381
|
-
// Show floating widget if enabled
|
|
1382
1473
|
if (this.config.showWidget) {
|
|
1383
1474
|
this.showFloatingWidget();
|
|
1384
1475
|
}
|
|
1385
1476
|
}
|
|
1386
1477
|
}
|
|
1387
1478
|
else {
|
|
1388
|
-
// No consent or expired
|
|
1389
1479
|
if (this.config.autoShow) {
|
|
1390
1480
|
this.showBanner();
|
|
1391
1481
|
}
|
|
1392
1482
|
}
|
|
1393
|
-
// Store instance globally
|
|
1394
|
-
// when used without a variable (e.g. new CookieConsent({}).init())
|
|
1483
|
+
// Store instance globally
|
|
1395
1484
|
window.cookieConsent = this;
|
|
1396
1485
|
this.eventEmitter.emit('consent:init');
|
|
1397
1486
|
}
|
|
@@ -1414,14 +1503,18 @@
|
|
|
1414
1503
|
showPreferences() {
|
|
1415
1504
|
var _a;
|
|
1416
1505
|
const stored = (_a = this.storageManager.load()) === null || _a === void 0 ? void 0 : _a.categories;
|
|
1417
|
-
// Default to all ON when no prior consent
|
|
1506
|
+
// Default to all ON when no prior consent
|
|
1418
1507
|
const currentConsent = stored || {
|
|
1419
1508
|
necessary: true,
|
|
1420
1509
|
analytics: true,
|
|
1421
1510
|
marketing: true,
|
|
1422
|
-
preferences: true,
|
|
1423
1511
|
};
|
|
1424
|
-
//
|
|
1512
|
+
// Add any configured categories not in current consent
|
|
1513
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
1514
|
+
if (!(key in currentConsent)) {
|
|
1515
|
+
currentConsent[key] = key === 'necessary';
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1425
1518
|
if (this.preferenceCenter) {
|
|
1426
1519
|
this.preferenceCenter.destroy();
|
|
1427
1520
|
}
|
|
@@ -1438,11 +1531,11 @@
|
|
|
1438
1531
|
if (this.gtmIntegration) {
|
|
1439
1532
|
this.gtmIntegration.updateConsent(categories);
|
|
1440
1533
|
}
|
|
1441
|
-
// Show floating widget after consent is given
|
|
1534
|
+
// Show floating widget after consent is given
|
|
1442
1535
|
if (this.config.showWidget) {
|
|
1443
1536
|
setTimeout(() => {
|
|
1444
1537
|
this.showFloatingWidget();
|
|
1445
|
-
}, 400);
|
|
1538
|
+
}, 400);
|
|
1446
1539
|
}
|
|
1447
1540
|
}
|
|
1448
1541
|
/**
|
|
@@ -1455,14 +1548,19 @@
|
|
|
1455
1548
|
* Reset consent (clear stored data and show banner)
|
|
1456
1549
|
*/
|
|
1457
1550
|
reset() {
|
|
1551
|
+
var _a, _b;
|
|
1458
1552
|
this.storageManager.clear();
|
|
1459
1553
|
this.scriptBlocker.block();
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1554
|
+
const denied = { necessary: true, analytics: false, marketing: false };
|
|
1555
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
1556
|
+
if (key !== 'necessary')
|
|
1557
|
+
denied[key] = false;
|
|
1558
|
+
}
|
|
1559
|
+
clearDeniedCookies(denied);
|
|
1463
1560
|
if (this.gtmIntegration) {
|
|
1464
|
-
this.gtmIntegration.updateConsent(
|
|
1561
|
+
this.gtmIntegration.updateConsent(denied);
|
|
1465
1562
|
}
|
|
1563
|
+
(_b = (_a = this.config).onReject) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1466
1564
|
this.showBanner();
|
|
1467
1565
|
}
|
|
1468
1566
|
/**
|
|
@@ -1482,6 +1580,10 @@
|
|
|
1482
1580
|
*/
|
|
1483
1581
|
destroy() {
|
|
1484
1582
|
var _a, _b, _c, _d;
|
|
1583
|
+
if (this.hideTimeout) {
|
|
1584
|
+
clearTimeout(this.hideTimeout);
|
|
1585
|
+
this.hideTimeout = null;
|
|
1586
|
+
}
|
|
1485
1587
|
(_a = this.banner) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
1486
1588
|
this.banner = null;
|
|
1487
1589
|
(_b = this.preferenceCenter) === null || _b === void 0 ? void 0 : _b.destroy();
|
|
@@ -1489,6 +1591,10 @@
|
|
|
1489
1591
|
(_c = this.floatingWidget) === null || _c === void 0 ? void 0 : _c.destroy();
|
|
1490
1592
|
this.floatingWidget = null;
|
|
1491
1593
|
(_d = this.scriptBlocker) === null || _d === void 0 ? void 0 : _d.destroy();
|
|
1594
|
+
this.eventEmitter.clear();
|
|
1595
|
+
if (window.cookieConsent === this) {
|
|
1596
|
+
window.cookieConsent = undefined;
|
|
1597
|
+
}
|
|
1492
1598
|
}
|
|
1493
1599
|
/**
|
|
1494
1600
|
* Show the banner
|
|
@@ -1514,7 +1620,6 @@
|
|
|
1514
1620
|
*/
|
|
1515
1621
|
applyConsent(categories) {
|
|
1516
1622
|
this.scriptBlocker.unblock(categories);
|
|
1517
|
-
// CNIL/GDPR: actively delete cookies for denied categories
|
|
1518
1623
|
clearDeniedCookies(categories);
|
|
1519
1624
|
}
|
|
1520
1625
|
/**
|
|
@@ -1525,22 +1630,22 @@
|
|
|
1525
1630
|
necessary: {
|
|
1526
1631
|
enabled: true,
|
|
1527
1632
|
readOnly: true,
|
|
1528
|
-
label: '
|
|
1529
|
-
description: '
|
|
1633
|
+
label: 'Essential',
|
|
1634
|
+
description: 'Required for the website to function properly.',
|
|
1530
1635
|
},
|
|
1531
1636
|
analytics: {
|
|
1532
1637
|
enabled: true,
|
|
1533
1638
|
readOnly: false,
|
|
1534
|
-
label: '
|
|
1535
|
-
description: '
|
|
1639
|
+
label: 'Analytics',
|
|
1640
|
+
description: 'Help us understand how you use our site.',
|
|
1536
1641
|
},
|
|
1537
1642
|
marketing: {
|
|
1538
1643
|
enabled: true,
|
|
1539
1644
|
readOnly: false,
|
|
1540
1645
|
label: 'Marketing',
|
|
1541
|
-
description: '
|
|
1646
|
+
description: 'Used to deliver relevant advertisements.',
|
|
1542
1647
|
},
|
|
1543
|
-
}, mode: config.mode || 'opt-in', autoShow: config.autoShow !== undefined ? config.autoShow : true, revision: config.revision || 1, gtmConsentMode: config.gtmConsentMode
|
|
1648
|
+
}, mode: config.mode || 'opt-in', autoShow: config.autoShow !== undefined ? config.autoShow : true, revision: config.revision || 1, gtmConsentMode: config.gtmConsentMode || false, disablePageInteraction: config.disablePageInteraction || false, theme: config.theme || 'light', position: config.position || 'bottom-left', layout: config.layout || 'box', backdropBlur: config.backdropBlur !== false, animationStyle: config.animationStyle || 'smooth', preferencesPosition: config.preferencesPosition || 'center', showWidget: config.showWidget !== undefined ? config.showWidget : true, widgetPosition: config.widgetPosition || 'bottom-left', widgetStyle: config.widgetStyle || 'compact' });
|
|
1544
1649
|
}
|
|
1545
1650
|
}
|
|
1546
1651
|
|