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.esm.js
CHANGED
|
@@ -43,13 +43,20 @@ class StorageManager {
|
|
|
43
43
|
* Clear consent record from localStorage
|
|
44
44
|
*/
|
|
45
45
|
clear() {
|
|
46
|
-
|
|
46
|
+
try {
|
|
47
|
+
localStorage.removeItem(StorageManager.STORAGE_KEY);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
console.error('Failed to clear consent:', e);
|
|
51
|
+
}
|
|
47
52
|
}
|
|
48
53
|
/**
|
|
49
54
|
* Check if consent record has expired
|
|
50
55
|
*/
|
|
51
56
|
isExpired(consent) {
|
|
52
57
|
const expiry = new Date(consent.expiresAt);
|
|
58
|
+
if (isNaN(expiry.getTime()))
|
|
59
|
+
return true;
|
|
53
60
|
return expiry < new Date();
|
|
54
61
|
}
|
|
55
62
|
/**
|
|
@@ -60,7 +67,6 @@ class StorageManager {
|
|
|
60
67
|
typeof data.version === 'number' &&
|
|
61
68
|
typeof data.timestamp === 'string' &&
|
|
62
69
|
typeof data.categories === 'object' &&
|
|
63
|
-
typeof data.userAgent === 'string' &&
|
|
64
70
|
typeof data.expiresAt === 'string');
|
|
65
71
|
}
|
|
66
72
|
/**
|
|
@@ -76,11 +82,20 @@ class StorageManager {
|
|
|
76
82
|
const now = new Date();
|
|
77
83
|
const expiryDate = new Date(now);
|
|
78
84
|
expiryDate.setMonth(expiryDate.getMonth() + StorageManager.EXPIRY_MONTHS);
|
|
85
|
+
// Coerce category values to booleans
|
|
86
|
+
const rawCategories = record.categories;
|
|
87
|
+
const categories = {
|
|
88
|
+
necessary: rawCategories.necessary === true,
|
|
89
|
+
analytics: rawCategories.analytics === true,
|
|
90
|
+
marketing: rawCategories.marketing === true,
|
|
91
|
+
};
|
|
92
|
+
if ('preferences' in rawCategories) {
|
|
93
|
+
categories.preferences = rawCategories.preferences === true;
|
|
94
|
+
}
|
|
79
95
|
return {
|
|
80
96
|
version: typeof record.version === 'number' ? record.version : 1,
|
|
81
97
|
timestamp: typeof record.timestamp === 'string' ? record.timestamp : now.toISOString(),
|
|
82
|
-
categories
|
|
83
|
-
userAgent: typeof record.userAgent === 'string' ? record.userAgent : navigator.userAgent,
|
|
98
|
+
categories,
|
|
84
99
|
expiresAt: typeof record.expiresAt === 'string' ? record.expiresAt : expiryDate.toISOString(),
|
|
85
100
|
};
|
|
86
101
|
}
|
|
@@ -95,6 +110,7 @@ StorageManager.EXPIRY_MONTHS = 13;
|
|
|
95
110
|
*/
|
|
96
111
|
class ConsentManager {
|
|
97
112
|
constructor(config) {
|
|
113
|
+
this.consent = null;
|
|
98
114
|
this.config = config;
|
|
99
115
|
}
|
|
100
116
|
/**
|
|
@@ -113,6 +129,10 @@ class ConsentManager {
|
|
|
113
129
|
}
|
|
114
130
|
}
|
|
115
131
|
}
|
|
132
|
+
// Coerce all values to booleans
|
|
133
|
+
for (const key of Object.keys(categories)) {
|
|
134
|
+
categories[key] = categories[key] === true;
|
|
135
|
+
}
|
|
116
136
|
return true;
|
|
117
137
|
}
|
|
118
138
|
/**
|
|
@@ -129,13 +149,12 @@ class ConsentManager {
|
|
|
129
149
|
* Check if user needs to give consent
|
|
130
150
|
*/
|
|
131
151
|
needsConsent() {
|
|
132
|
-
return this.consent ===
|
|
152
|
+
return this.consent === null;
|
|
133
153
|
}
|
|
134
154
|
/**
|
|
135
155
|
* Check if stored consent needs update due to policy change
|
|
136
156
|
*/
|
|
137
157
|
needsUpdate(storedConsent) {
|
|
138
|
-
// Check if policy version has changed
|
|
139
158
|
return storedConsent.version < this.config.revision;
|
|
140
159
|
}
|
|
141
160
|
/**
|
|
@@ -155,7 +174,6 @@ class ConsentManager {
|
|
|
155
174
|
version: this.config.revision,
|
|
156
175
|
timestamp: now.toISOString(),
|
|
157
176
|
categories: Object.assign({}, categories),
|
|
158
|
-
userAgent: navigator.userAgent,
|
|
159
177
|
expiresAt: expiryDate.toISOString(),
|
|
160
178
|
};
|
|
161
179
|
}
|
|
@@ -396,7 +414,6 @@ class ScriptBlocker {
|
|
|
396
414
|
class CategoryManager {
|
|
397
415
|
constructor() {
|
|
398
416
|
this.categories = new Map();
|
|
399
|
-
// Initialize with common patterns
|
|
400
417
|
this.initializeDefaultPatterns();
|
|
401
418
|
}
|
|
402
419
|
/**
|
|
@@ -433,11 +450,11 @@ class CategoryManager {
|
|
|
433
450
|
}
|
|
434
451
|
/**
|
|
435
452
|
* Initialize default URL patterns for common tracking services
|
|
453
|
+
* Note: GTM is NOT auto-categorized — it should be managed via GTM Consent Mode v2
|
|
436
454
|
*/
|
|
437
455
|
initializeDefaultPatterns() {
|
|
438
456
|
this.categories.set('analytics', [
|
|
439
457
|
'google-analytics.com',
|
|
440
|
-
'googletagmanager.com',
|
|
441
458
|
'analytics.google.com',
|
|
442
459
|
'plausible.io',
|
|
443
460
|
'matomo.org',
|
|
@@ -447,6 +464,7 @@ class CategoryManager {
|
|
|
447
464
|
'amplitude.com',
|
|
448
465
|
]);
|
|
449
466
|
this.categories.set('marketing', [
|
|
467
|
+
'googletagmanager.com',
|
|
450
468
|
'facebook.net',
|
|
451
469
|
'facebook.com/tr',
|
|
452
470
|
'connect.facebook.net',
|
|
@@ -458,6 +476,7 @@ class CategoryManager {
|
|
|
458
476
|
'adroll.com',
|
|
459
477
|
'taboola.com',
|
|
460
478
|
'outbrain.com',
|
|
479
|
+
'tiktok.com',
|
|
461
480
|
]);
|
|
462
481
|
this.categories.set('necessary', []);
|
|
463
482
|
}
|
|
@@ -508,18 +527,47 @@ function sanitizeColor(color) {
|
|
|
508
527
|
// Allow hsl/hsla
|
|
509
528
|
if (/^hsla?\(\s*[\d\s,./%deg]+\)$/.test(trimmed))
|
|
510
529
|
return trimmed;
|
|
511
|
-
// Allow CSS named colors (basic set)
|
|
512
|
-
|
|
530
|
+
// Allow CSS named colors (basic set) but block CSS keywords that could be abused
|
|
531
|
+
const CSS_KEYWORDS = ['inherit', 'initial', 'unset', 'revert', 'revert-layer'];
|
|
532
|
+
if (/^[a-zA-Z]+$/.test(trimmed) && !CSS_KEYWORDS.includes(trimmed.toLowerCase())) {
|
|
513
533
|
return trimmed;
|
|
534
|
+
}
|
|
514
535
|
return '';
|
|
515
536
|
}
|
|
516
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Normalize any supported color format to 6-digit hex
|
|
540
|
+
* Supports: #RGB, #RRGGBB, #RRGGBBAA, named colors
|
|
541
|
+
* Returns null if conversion fails
|
|
542
|
+
*/
|
|
543
|
+
function normalizeToHex6(color) {
|
|
544
|
+
const trimmed = color.trim();
|
|
545
|
+
// Already 6-digit hex
|
|
546
|
+
if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) {
|
|
547
|
+
return trimmed;
|
|
548
|
+
}
|
|
549
|
+
// 3-digit hex → expand to 6-digit
|
|
550
|
+
if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) {
|
|
551
|
+
const r = trimmed[1];
|
|
552
|
+
const g = trimmed[2];
|
|
553
|
+
const b = trimmed[3];
|
|
554
|
+
return `#${r}${r}${g}${g}${b}${b}`;
|
|
555
|
+
}
|
|
556
|
+
// 8-digit hex (with alpha) → strip alpha
|
|
557
|
+
if (/^#[0-9a-fA-F]{8}$/.test(trimmed)) {
|
|
558
|
+
return trimmed.substring(0, 7);
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
517
562
|
/**
|
|
518
563
|
* Adjust a hex color brightness by a percentage
|
|
519
564
|
* Negative = darker, positive = lighter
|
|
520
565
|
*/
|
|
521
566
|
function adjustColorBrightness(color, percent) {
|
|
522
|
-
const
|
|
567
|
+
const hex6 = normalizeToHex6(color);
|
|
568
|
+
if (!hex6)
|
|
569
|
+
return color;
|
|
570
|
+
const hex = hex6.replace('#', '');
|
|
523
571
|
const r = parseInt(hex.substring(0, 2), 16);
|
|
524
572
|
const g = parseInt(hex.substring(2, 4), 16);
|
|
525
573
|
const b = parseInt(hex.substring(4, 6), 16);
|
|
@@ -539,8 +587,13 @@ function adjustColorBrightness(color, percent) {
|
|
|
539
587
|
function buildColorStyle(safeColor) {
|
|
540
588
|
if (!safeColor)
|
|
541
589
|
return '';
|
|
542
|
-
|
|
543
|
-
|
|
590
|
+
// Only generate hover color for hex colors
|
|
591
|
+
const hex6 = normalizeToHex6(safeColor);
|
|
592
|
+
if (!hex6) {
|
|
593
|
+
return `--cc-primary: ${safeColor};`;
|
|
594
|
+
}
|
|
595
|
+
const hover = adjustColorBrightness(hex6, -15);
|
|
596
|
+
return `--cc-primary: ${hex6}; --cc-primary-hover: ${hover};`;
|
|
544
597
|
}
|
|
545
598
|
|
|
546
599
|
/**
|
|
@@ -549,6 +602,8 @@ function buildColorStyle(safeColor) {
|
|
|
549
602
|
class Banner {
|
|
550
603
|
constructor(config, eventEmitter) {
|
|
551
604
|
this.element = null;
|
|
605
|
+
this.hideTimeout = null;
|
|
606
|
+
this.previousActiveElement = null;
|
|
552
607
|
this.config = config;
|
|
553
608
|
this.eventEmitter = eventEmitter;
|
|
554
609
|
}
|
|
@@ -556,8 +611,14 @@ class Banner {
|
|
|
556
611
|
* Show the banner
|
|
557
612
|
*/
|
|
558
613
|
show() {
|
|
614
|
+
// Clear any pending hide timeout
|
|
615
|
+
if (this.hideTimeout) {
|
|
616
|
+
clearTimeout(this.hideTimeout);
|
|
617
|
+
this.hideTimeout = null;
|
|
618
|
+
}
|
|
559
619
|
const append = () => {
|
|
560
620
|
if (!this.element) {
|
|
621
|
+
this.previousActiveElement = document.activeElement;
|
|
561
622
|
this.element = this.createDOM();
|
|
562
623
|
document.body.appendChild(this.element);
|
|
563
624
|
this.attachListeners();
|
|
@@ -570,9 +631,9 @@ class Banner {
|
|
|
570
631
|
// Disable page interaction if configured
|
|
571
632
|
if (this.config.disablePageInteraction) {
|
|
572
633
|
document.body.style.overflow = 'hidden';
|
|
634
|
+
this.trapFocus();
|
|
573
635
|
}
|
|
574
636
|
};
|
|
575
|
-
// Wait for body if not yet available
|
|
576
637
|
if (!document.body) {
|
|
577
638
|
document.addEventListener('DOMContentLoaded', append);
|
|
578
639
|
return;
|
|
@@ -585,22 +646,30 @@ class Banner {
|
|
|
585
646
|
hide() {
|
|
586
647
|
var _a;
|
|
587
648
|
(_a = this.element) === null || _a === void 0 ? void 0 : _a.classList.remove('is-visible');
|
|
588
|
-
// Re-enable page interaction
|
|
589
649
|
if (this.config.disablePageInteraction) {
|
|
590
650
|
document.body.style.overflow = '';
|
|
591
651
|
}
|
|
592
|
-
setTimeout(() => {
|
|
652
|
+
this.hideTimeout = setTimeout(() => {
|
|
593
653
|
this.destroy();
|
|
594
|
-
}, 300);
|
|
654
|
+
}, 300);
|
|
595
655
|
}
|
|
596
656
|
/**
|
|
597
657
|
* Destroy the banner
|
|
598
658
|
*/
|
|
599
659
|
destroy() {
|
|
660
|
+
if (this.hideTimeout) {
|
|
661
|
+
clearTimeout(this.hideTimeout);
|
|
662
|
+
this.hideTimeout = null;
|
|
663
|
+
}
|
|
600
664
|
if (this.element) {
|
|
601
665
|
this.element.remove();
|
|
602
666
|
this.element = null;
|
|
603
667
|
}
|
|
668
|
+
// Restore focus
|
|
669
|
+
if (this.previousActiveElement && document.contains(this.previousActiveElement)) {
|
|
670
|
+
this.previousActiveElement.focus();
|
|
671
|
+
this.previousActiveElement = null;
|
|
672
|
+
}
|
|
604
673
|
}
|
|
605
674
|
/**
|
|
606
675
|
* Create DOM structure for banner
|
|
@@ -611,12 +680,14 @@ class Banner {
|
|
|
611
680
|
const position = this.config.position || 'bottom';
|
|
612
681
|
const layout = this.config.layout || 'bar';
|
|
613
682
|
const backdropBlur = this.config.backdropBlur !== false;
|
|
683
|
+
const isModal = this.config.disablePageInteraction;
|
|
614
684
|
const safeColor = this.config.primaryColor ? sanitizeColor(this.config.primaryColor) : '';
|
|
615
685
|
const colorStyle = buildColorStyle(safeColor);
|
|
616
686
|
const template = `
|
|
617
687
|
<div
|
|
618
688
|
class="cc-banner cc-banner--${escapeHtml(position)} cc-banner--${escapeHtml(layout)} ${backdropBlur ? 'cc-backdrop-blur' : ''}"
|
|
619
|
-
role="region"
|
|
689
|
+
role="${isModal ? 'dialog' : 'region'}"
|
|
690
|
+
${isModal ? 'aria-modal="true"' : ''}
|
|
620
691
|
aria-label="Cookie consent"
|
|
621
692
|
aria-live="polite"
|
|
622
693
|
data-theme="${escapeHtml(theme)}"
|
|
@@ -625,7 +696,7 @@ class Banner {
|
|
|
625
696
|
<div class="cc-banner__container">
|
|
626
697
|
<div class="cc-banner__content">
|
|
627
698
|
<h2 class="cc-banner__title">
|
|
628
|
-
${escapeHtml(translations.title || '
|
|
699
|
+
${escapeHtml(translations.title || 'We use cookies')}
|
|
629
700
|
</h2>
|
|
630
701
|
<p class="cc-banner__description">
|
|
631
702
|
${this.getDescriptionHTML()}
|
|
@@ -635,23 +706,23 @@ class Banner {
|
|
|
635
706
|
<button
|
|
636
707
|
class="cc-btn cc-btn--ghost"
|
|
637
708
|
data-action="reject"
|
|
638
|
-
aria-label="${escapeHtml(translations.rejectAll || '
|
|
709
|
+
aria-label="${escapeHtml(translations.rejectAll || 'Essentials only')}"
|
|
639
710
|
>
|
|
640
|
-
${escapeHtml(translations.rejectAll || '
|
|
711
|
+
${escapeHtml(translations.rejectAll || 'Essentials only')}
|
|
641
712
|
</button>
|
|
642
713
|
<button
|
|
643
714
|
class="cc-btn cc-btn--tertiary"
|
|
644
715
|
data-action="customize"
|
|
645
|
-
aria-label="${escapeHtml(translations.customize || '
|
|
716
|
+
aria-label="${escapeHtml(translations.customize || 'Customize')}"
|
|
646
717
|
>
|
|
647
|
-
${escapeHtml(translations.customize || '
|
|
718
|
+
${escapeHtml(translations.customize || 'Customize')}
|
|
648
719
|
</button>
|
|
649
720
|
<button
|
|
650
721
|
class="cc-btn cc-btn--accept"
|
|
651
722
|
data-action="accept"
|
|
652
|
-
aria-label="${escapeHtml(translations.acceptAll || '
|
|
723
|
+
aria-label="${escapeHtml(translations.acceptAll || 'Accept all')}"
|
|
653
724
|
>
|
|
654
|
-
${escapeHtml(translations.acceptAll || '
|
|
725
|
+
${escapeHtml(translations.acceptAll || 'Accept all')}
|
|
655
726
|
</button>
|
|
656
727
|
</div>
|
|
657
728
|
</div>
|
|
@@ -683,10 +754,8 @@ class Banner {
|
|
|
683
754
|
break;
|
|
684
755
|
}
|
|
685
756
|
});
|
|
686
|
-
// Keyboard support
|
|
687
757
|
(_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
|
|
688
758
|
if (e.key === 'Escape' && this.config.disablePageInteraction) {
|
|
689
|
-
// Allow ESC to close if page interaction is disabled
|
|
690
759
|
this.handleRejectAll();
|
|
691
760
|
}
|
|
692
761
|
});
|
|
@@ -695,36 +764,24 @@ class Banner {
|
|
|
695
764
|
* Handle accept all action
|
|
696
765
|
*/
|
|
697
766
|
handleAcceptAll() {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
marketing: true,
|
|
703
|
-
};
|
|
704
|
-
// Only add preferences if it's configured
|
|
705
|
-
if ((_a = this.config.categories) === null || _a === void 0 ? void 0 : _a.preferences) {
|
|
706
|
-
allCategories.preferences = true;
|
|
767
|
+
const allCategories = { necessary: true, analytics: true, marketing: true };
|
|
768
|
+
// Add all configured categories
|
|
769
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
770
|
+
allCategories[key] = true;
|
|
707
771
|
}
|
|
708
772
|
this.eventEmitter.emit('consent:accept', allCategories);
|
|
709
|
-
(_c = (_b = this.config).onAccept) === null || _c === void 0 ? void 0 : _c.call(_b, allCategories);
|
|
710
773
|
this.hide();
|
|
711
774
|
}
|
|
712
775
|
/**
|
|
713
776
|
* Handle reject all action
|
|
714
777
|
*/
|
|
715
778
|
handleRejectAll() {
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
necessary
|
|
719
|
-
|
|
720
|
-
marketing: false,
|
|
721
|
-
};
|
|
722
|
-
// Only add preferences if it's configured
|
|
723
|
-
if ((_a = this.config.categories) === null || _a === void 0 ? void 0 : _a.preferences) {
|
|
724
|
-
necessaryOnly.preferences = false;
|
|
779
|
+
const necessaryOnly = { necessary: true, analytics: false, marketing: false };
|
|
780
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
781
|
+
if (key !== 'necessary')
|
|
782
|
+
necessaryOnly[key] = false;
|
|
725
783
|
}
|
|
726
784
|
this.eventEmitter.emit('consent:reject', necessaryOnly);
|
|
727
|
-
(_c = (_b = this.config).onReject) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
728
785
|
this.hide();
|
|
729
786
|
}
|
|
730
787
|
/**
|
|
@@ -734,17 +791,41 @@ class Banner {
|
|
|
734
791
|
this.eventEmitter.emit('preferences:show');
|
|
735
792
|
this.hide();
|
|
736
793
|
}
|
|
794
|
+
/**
|
|
795
|
+
* Trap focus within banner (when disablePageInteraction is true)
|
|
796
|
+
*/
|
|
797
|
+
trapFocus() {
|
|
798
|
+
var _a, _b;
|
|
799
|
+
const focusableElements = (_a = this.element) === null || _a === void 0 ? void 0 : _a.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
800
|
+
if (!focusableElements || focusableElements.length === 0)
|
|
801
|
+
return;
|
|
802
|
+
const firstFocusable = focusableElements[0];
|
|
803
|
+
const lastFocusable = focusableElements[focusableElements.length - 1];
|
|
804
|
+
firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus();
|
|
805
|
+
(_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
|
|
806
|
+
if (e.key === 'Tab') {
|
|
807
|
+
if (e.shiftKey && document.activeElement === firstFocusable) {
|
|
808
|
+
e.preventDefault();
|
|
809
|
+
lastFocusable.focus();
|
|
810
|
+
}
|
|
811
|
+
else if (!e.shiftKey && document.activeElement === lastFocusable) {
|
|
812
|
+
e.preventDefault();
|
|
813
|
+
firstFocusable.focus();
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
737
818
|
/**
|
|
738
819
|
* Generate description HTML with privacy policy link
|
|
739
820
|
*/
|
|
740
821
|
getDescriptionHTML() {
|
|
741
822
|
const translations = this.config.translations || {};
|
|
742
|
-
const defaultDescription = '
|
|
823
|
+
const defaultDescription = 'We use cookies to improve your experience on our site. You can choose which cookies you accept.';
|
|
743
824
|
const description = escapeHtml(translations.description || defaultDescription);
|
|
744
825
|
if (translations.privacyPolicyUrl) {
|
|
745
826
|
const safeUrl = sanitizeUrl(translations.privacyPolicyUrl);
|
|
746
827
|
if (safeUrl) {
|
|
747
|
-
const linkLabel = escapeHtml(translations.privacyPolicyLabel || '
|
|
828
|
+
const linkLabel = escapeHtml(translations.privacyPolicyLabel || 'Privacy Policy');
|
|
748
829
|
return `${description} <a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${linkLabel}</a>`;
|
|
749
830
|
}
|
|
750
831
|
}
|
|
@@ -758,6 +839,7 @@ class Banner {
|
|
|
758
839
|
class PreferenceCenter {
|
|
759
840
|
constructor(config, eventEmitter, currentConsent) {
|
|
760
841
|
this.element = null;
|
|
842
|
+
this.previousActiveElement = null;
|
|
761
843
|
this.config = config;
|
|
762
844
|
this.eventEmitter = eventEmitter;
|
|
763
845
|
this.currentConsent = currentConsent;
|
|
@@ -768,13 +850,13 @@ class PreferenceCenter {
|
|
|
768
850
|
show() {
|
|
769
851
|
const append = () => {
|
|
770
852
|
if (!this.element) {
|
|
853
|
+
this.previousActiveElement = document.activeElement;
|
|
771
854
|
this.element = this.createDOM();
|
|
772
855
|
document.body.appendChild(this.element);
|
|
773
856
|
this.attachListeners();
|
|
774
857
|
}
|
|
775
858
|
this.element.classList.add('is-visible');
|
|
776
859
|
this.trapFocus();
|
|
777
|
-
// Prevent body scroll
|
|
778
860
|
document.body.style.overflow = 'hidden';
|
|
779
861
|
};
|
|
780
862
|
if (!document.body) {
|
|
@@ -790,6 +872,11 @@ class PreferenceCenter {
|
|
|
790
872
|
var _a;
|
|
791
873
|
(_a = this.element) === null || _a === void 0 ? void 0 : _a.classList.remove('is-visible');
|
|
792
874
|
document.body.style.overflow = '';
|
|
875
|
+
// Restore focus to triggering element
|
|
876
|
+
if (this.previousActiveElement && document.contains(this.previousActiveElement)) {
|
|
877
|
+
this.previousActiveElement.focus();
|
|
878
|
+
this.previousActiveElement = null;
|
|
879
|
+
}
|
|
793
880
|
setTimeout(() => {
|
|
794
881
|
this.destroy();
|
|
795
882
|
}, 300);
|
|
@@ -824,7 +911,7 @@ class PreferenceCenter {
|
|
|
824
911
|
<polyline points="15 3 21 3 21 9"/>
|
|
825
912
|
<line x1="10" y1="14" x2="21" y2="3"/>
|
|
826
913
|
</svg>
|
|
827
|
-
${escapeHtml(translations.privacyPolicyLabel || '
|
|
914
|
+
${escapeHtml(translations.privacyPolicyLabel || 'Privacy Policy')}
|
|
828
915
|
</a>
|
|
829
916
|
`;
|
|
830
917
|
})()
|
|
@@ -842,7 +929,7 @@ class PreferenceCenter {
|
|
|
842
929
|
<div class="cc-modal__content">
|
|
843
930
|
<div class="cc-modal__header">
|
|
844
931
|
<h2 id="cc-modal-title">
|
|
845
|
-
${escapeHtml(translations.preferencesTitle || translations.title || '
|
|
932
|
+
${escapeHtml(translations.preferencesTitle || translations.title || 'Cookie Preferences')}
|
|
846
933
|
</h2>
|
|
847
934
|
</div>
|
|
848
935
|
|
|
@@ -859,13 +946,13 @@ class PreferenceCenter {
|
|
|
859
946
|
class="cc-btn cc-btn--secondary"
|
|
860
947
|
data-action="reject"
|
|
861
948
|
>
|
|
862
|
-
${escapeHtml(translations.essentialsOnly || '
|
|
949
|
+
${escapeHtml(translations.essentialsOnly || 'Essentials only')}
|
|
863
950
|
</button>
|
|
864
951
|
<button
|
|
865
952
|
class="cc-btn cc-btn--primary"
|
|
866
953
|
data-action="save"
|
|
867
954
|
>
|
|
868
|
-
${escapeHtml(translations.savePreferences || '
|
|
955
|
+
${escapeHtml(translations.savePreferences || 'Save preferences')}
|
|
869
956
|
</button>
|
|
870
957
|
</div>
|
|
871
958
|
</div>
|
|
@@ -883,7 +970,7 @@ class PreferenceCenter {
|
|
|
883
970
|
const categories = Object.entries(this.config.categories);
|
|
884
971
|
return categories
|
|
885
972
|
.map(([key, config]) => {
|
|
886
|
-
const checked = this.currentConsent[key];
|
|
973
|
+
const checked = this.currentConsent[key] === true;
|
|
887
974
|
const disabled = config.readOnly;
|
|
888
975
|
return `
|
|
889
976
|
<div class="cc-category">
|
|
@@ -930,13 +1017,16 @@ class PreferenceCenter {
|
|
|
930
1017
|
* Handle save preferences
|
|
931
1018
|
*/
|
|
932
1019
|
handleSave() {
|
|
933
|
-
var _a
|
|
1020
|
+
var _a;
|
|
934
1021
|
const checkboxes = (_a = this.element) === null || _a === void 0 ? void 0 : _a.querySelectorAll('input[data-category]');
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1022
|
+
// Initialize all configured categories to false
|
|
1023
|
+
const categories = { necessary: true, analytics: false, marketing: false };
|
|
1024
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
1025
|
+
if (key !== 'necessary') {
|
|
1026
|
+
categories[key] = false;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
// Override with actual checkbox values
|
|
940
1030
|
checkboxes === null || checkboxes === void 0 ? void 0 : checkboxes.forEach((checkbox) => {
|
|
941
1031
|
if (checkbox instanceof HTMLInputElement) {
|
|
942
1032
|
const category = checkbox.getAttribute('data-category');
|
|
@@ -946,22 +1036,16 @@ class PreferenceCenter {
|
|
|
946
1036
|
}
|
|
947
1037
|
});
|
|
948
1038
|
this.eventEmitter.emit('consent:update', categories);
|
|
949
|
-
(_c = (_b = this.config).onChange) === null || _c === void 0 ? void 0 : _c.call(_b, categories);
|
|
950
1039
|
this.hide();
|
|
951
1040
|
}
|
|
952
1041
|
/**
|
|
953
1042
|
* Handle reject all
|
|
954
1043
|
*/
|
|
955
1044
|
handleRejectAll() {
|
|
956
|
-
|
|
957
|
-
const
|
|
958
|
-
necessary
|
|
959
|
-
|
|
960
|
-
marketing: false,
|
|
961
|
-
};
|
|
962
|
-
// Only add preferences if it's configured
|
|
963
|
-
if ((_a = this.config.categories) === null || _a === void 0 ? void 0 : _a.preferences) {
|
|
964
|
-
necessaryOnly.preferences = false;
|
|
1045
|
+
const necessaryOnly = { necessary: true, analytics: false, marketing: false };
|
|
1046
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
1047
|
+
if (key !== 'necessary')
|
|
1048
|
+
necessaryOnly[key] = false;
|
|
965
1049
|
}
|
|
966
1050
|
this.eventEmitter.emit('consent:reject', necessaryOnly);
|
|
967
1051
|
this.hide();
|
|
@@ -976,9 +1060,7 @@ class PreferenceCenter {
|
|
|
976
1060
|
return;
|
|
977
1061
|
const firstFocusable = focusableElements[0];
|
|
978
1062
|
const lastFocusable = focusableElements[focusableElements.length - 1];
|
|
979
|
-
// Focus first element
|
|
980
1063
|
firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus();
|
|
981
|
-
// Trap focus
|
|
982
1064
|
(_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
|
|
983
1065
|
if (e.key === 'Tab') {
|
|
984
1066
|
if (e.shiftKey && document.activeElement === firstFocusable) {
|
|
@@ -1066,7 +1148,7 @@ class FloatingWidget {
|
|
|
1066
1148
|
<div
|
|
1067
1149
|
class="cc-widget cc-widget--${escapeHtml(widgetPosition)} cc-widget--${escapeHtml(widgetStyle)}"
|
|
1068
1150
|
role="button"
|
|
1069
|
-
aria-label="${escapeHtml(translations.cookieSettings || '
|
|
1151
|
+
aria-label="${escapeHtml(translations.cookieSettings || 'Cookie settings')}"
|
|
1070
1152
|
tabindex="0"
|
|
1071
1153
|
data-theme="${escapeHtml(theme)}"
|
|
1072
1154
|
style="${colorStyle}"
|
|
@@ -1117,11 +1199,6 @@ class FloatingWidget {
|
|
|
1117
1199
|
|
|
1118
1200
|
/**
|
|
1119
1201
|
* GTMConsentMode - Full integration with Google Consent Mode v2
|
|
1120
|
-
*
|
|
1121
|
-
* Implements all required signals:
|
|
1122
|
-
* - ad_storage, ad_user_data, ad_personalization, analytics_storage (core GCM v2)
|
|
1123
|
-
* - functionality_storage, personalization_storage, security_storage (non-core)
|
|
1124
|
-
* - wait_for_update, url_passthrough, ads_data_redaction (advanced features)
|
|
1125
1202
|
*/
|
|
1126
1203
|
class GTMConsentMode {
|
|
1127
1204
|
constructor(dataLayerManager, config) {
|
|
@@ -1141,15 +1218,13 @@ class GTMConsentMode {
|
|
|
1141
1218
|
analytics_storage: 'denied',
|
|
1142
1219
|
functionality_storage: 'denied',
|
|
1143
1220
|
personalization_storage: 'denied',
|
|
1144
|
-
security_storage: 'granted',
|
|
1221
|
+
security_storage: 'granted',
|
|
1145
1222
|
};
|
|
1146
|
-
// Add wait_for_update to give CMP time to restore returning visitor consent
|
|
1147
1223
|
const waitForUpdate = (_a = this.config.gtmWaitForUpdate) !== null && _a !== void 0 ? _a : 500;
|
|
1148
1224
|
if (waitForUpdate > 0) {
|
|
1149
1225
|
defaults['wait_for_update'] = waitForUpdate;
|
|
1150
1226
|
}
|
|
1151
1227
|
this.dataLayerManager.pushConsent('default', defaults);
|
|
1152
|
-
// Set advanced features via gtag('set', ...)
|
|
1153
1228
|
if (this.config.gtmUrlPassthrough) {
|
|
1154
1229
|
this.dataLayerManager.pushSet('url_passthrough', true);
|
|
1155
1230
|
}
|
|
@@ -1159,7 +1234,6 @@ class GTMConsentMode {
|
|
|
1159
1234
|
}
|
|
1160
1235
|
/**
|
|
1161
1236
|
* Update consent state based on user choices
|
|
1162
|
-
* Called both on new consent and on page load for returning visitors
|
|
1163
1237
|
*/
|
|
1164
1238
|
updateConsent(categories) {
|
|
1165
1239
|
const gtmConsent = this.mapCategoriesToGTM(categories);
|
|
@@ -1169,14 +1243,19 @@ class GTMConsentMode {
|
|
|
1169
1243
|
* Map consent categories to GTM Consent Mode v2 format
|
|
1170
1244
|
*/
|
|
1171
1245
|
mapCategoriesToGTM(categories) {
|
|
1246
|
+
// When preferences category is not configured, default functionality to granted
|
|
1247
|
+
const hasPreferencesCategory = 'preferences' in this.config.categories;
|
|
1248
|
+
const preferencesGranted = hasPreferencesCategory
|
|
1249
|
+
? categories.preferences === true
|
|
1250
|
+
: true;
|
|
1172
1251
|
return {
|
|
1173
1252
|
ad_storage: categories.marketing ? 'granted' : 'denied',
|
|
1174
1253
|
ad_user_data: categories.marketing ? 'granted' : 'denied',
|
|
1175
1254
|
ad_personalization: categories.marketing ? 'granted' : 'denied',
|
|
1176
1255
|
analytics_storage: categories.analytics ? 'granted' : 'denied',
|
|
1177
|
-
functionality_storage:
|
|
1178
|
-
personalization_storage:
|
|
1179
|
-
security_storage: 'granted',
|
|
1256
|
+
functionality_storage: preferencesGranted ? 'granted' : 'denied',
|
|
1257
|
+
personalization_storage: preferencesGranted ? 'granted' : 'denied',
|
|
1258
|
+
security_storage: 'granted',
|
|
1180
1259
|
};
|
|
1181
1260
|
}
|
|
1182
1261
|
}
|
|
@@ -1321,6 +1400,16 @@ class CookieConsent {
|
|
|
1321
1400
|
this.preferenceCenter = null;
|
|
1322
1401
|
this.floatingWidget = null;
|
|
1323
1402
|
this.gtmIntegration = null;
|
|
1403
|
+
this.hideTimeout = null;
|
|
1404
|
+
// SSR guard
|
|
1405
|
+
if (typeof window === 'undefined') {
|
|
1406
|
+
this.config = config;
|
|
1407
|
+
this.consentManager = null;
|
|
1408
|
+
this.storageManager = null;
|
|
1409
|
+
this.eventEmitter = null;
|
|
1410
|
+
this.scriptBlocker = null;
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1324
1413
|
this.config = this.validateConfig(config);
|
|
1325
1414
|
this.consentManager = new ConsentManager(this.config);
|
|
1326
1415
|
this.storageManager = new StorageManager();
|
|
@@ -1329,25 +1418,32 @@ class CookieConsent {
|
|
|
1329
1418
|
if (this.config.gtmConsentMode) {
|
|
1330
1419
|
this.gtmIntegration = new GTMConsentMode(new DataLayerManager(), this.config);
|
|
1331
1420
|
}
|
|
1332
|
-
// Listen for
|
|
1333
|
-
this.eventEmitter.on('preferences:show', () => {
|
|
1334
|
-
this.showPreferences();
|
|
1335
|
-
});
|
|
1336
|
-
// Listen for consent updates
|
|
1421
|
+
// Listen for consent events — callbacks are fired AFTER consent is persisted
|
|
1337
1422
|
this.eventEmitter.on('consent:accept', (categories) => {
|
|
1423
|
+
var _a, _b;
|
|
1338
1424
|
this.updateConsent(categories);
|
|
1425
|
+
(_b = (_a = this.config).onAccept) === null || _b === void 0 ? void 0 : _b.call(_a, categories);
|
|
1339
1426
|
});
|
|
1340
1427
|
this.eventEmitter.on('consent:reject', (categories) => {
|
|
1428
|
+
var _a, _b;
|
|
1341
1429
|
this.updateConsent(categories);
|
|
1430
|
+
(_b = (_a = this.config).onReject) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1342
1431
|
});
|
|
1343
1432
|
this.eventEmitter.on('consent:update', (categories) => {
|
|
1433
|
+
var _a, _b;
|
|
1344
1434
|
this.updateConsent(categories);
|
|
1435
|
+
(_b = (_a = this.config).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, categories);
|
|
1436
|
+
});
|
|
1437
|
+
this.eventEmitter.on('preferences:show', () => {
|
|
1438
|
+
this.showPreferences();
|
|
1345
1439
|
});
|
|
1346
1440
|
}
|
|
1347
1441
|
/**
|
|
1348
1442
|
* Initialize the cookie consent system
|
|
1349
1443
|
*/
|
|
1350
1444
|
init() {
|
|
1445
|
+
if (typeof window === 'undefined')
|
|
1446
|
+
return;
|
|
1351
1447
|
// 1. Start blocking scripts immediately
|
|
1352
1448
|
this.scriptBlocker.init();
|
|
1353
1449
|
// 2. Set GTM default consent BEFORE checking storage
|
|
@@ -1357,35 +1453,28 @@ class CookieConsent {
|
|
|
1357
1453
|
// 3. Check for existing consent
|
|
1358
1454
|
const storedConsent = this.storageManager.load();
|
|
1359
1455
|
if (storedConsent && !this.storageManager.isExpired(storedConsent)) {
|
|
1360
|
-
// Valid consent exists
|
|
1361
1456
|
if (this.consentManager.needsUpdate(storedConsent)) {
|
|
1362
|
-
// Policy updated, show banner again
|
|
1363
1457
|
if (this.config.autoShow) {
|
|
1364
1458
|
this.showBanner();
|
|
1365
1459
|
}
|
|
1366
1460
|
}
|
|
1367
1461
|
else {
|
|
1368
|
-
// Apply stored consent
|
|
1369
1462
|
this.applyConsent(storedConsent.categories);
|
|
1370
|
-
// Restore GTM consent for returning visitors (within wait_for_update window)
|
|
1371
1463
|
if (this.gtmIntegration) {
|
|
1372
1464
|
this.gtmIntegration.updateConsent(storedConsent.categories);
|
|
1373
1465
|
}
|
|
1374
1466
|
this.eventEmitter.emit('consent:load', storedConsent);
|
|
1375
|
-
// Show floating widget if enabled
|
|
1376
1467
|
if (this.config.showWidget) {
|
|
1377
1468
|
this.showFloatingWidget();
|
|
1378
1469
|
}
|
|
1379
1470
|
}
|
|
1380
1471
|
}
|
|
1381
1472
|
else {
|
|
1382
|
-
// No consent or expired
|
|
1383
1473
|
if (this.config.autoShow) {
|
|
1384
1474
|
this.showBanner();
|
|
1385
1475
|
}
|
|
1386
1476
|
}
|
|
1387
|
-
// Store instance globally
|
|
1388
|
-
// when used without a variable (e.g. new CookieConsent({}).init())
|
|
1477
|
+
// Store instance globally
|
|
1389
1478
|
window.cookieConsent = this;
|
|
1390
1479
|
this.eventEmitter.emit('consent:init');
|
|
1391
1480
|
}
|
|
@@ -1408,14 +1497,18 @@ class CookieConsent {
|
|
|
1408
1497
|
showPreferences() {
|
|
1409
1498
|
var _a;
|
|
1410
1499
|
const stored = (_a = this.storageManager.load()) === null || _a === void 0 ? void 0 : _a.categories;
|
|
1411
|
-
// Default to all ON when no prior consent
|
|
1500
|
+
// Default to all ON when no prior consent
|
|
1412
1501
|
const currentConsent = stored || {
|
|
1413
1502
|
necessary: true,
|
|
1414
1503
|
analytics: true,
|
|
1415
1504
|
marketing: true,
|
|
1416
|
-
preferences: true,
|
|
1417
1505
|
};
|
|
1418
|
-
//
|
|
1506
|
+
// Add any configured categories not in current consent
|
|
1507
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
1508
|
+
if (!(key in currentConsent)) {
|
|
1509
|
+
currentConsent[key] = key === 'necessary';
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1419
1512
|
if (this.preferenceCenter) {
|
|
1420
1513
|
this.preferenceCenter.destroy();
|
|
1421
1514
|
}
|
|
@@ -1432,11 +1525,11 @@ class CookieConsent {
|
|
|
1432
1525
|
if (this.gtmIntegration) {
|
|
1433
1526
|
this.gtmIntegration.updateConsent(categories);
|
|
1434
1527
|
}
|
|
1435
|
-
// Show floating widget after consent is given
|
|
1528
|
+
// Show floating widget after consent is given
|
|
1436
1529
|
if (this.config.showWidget) {
|
|
1437
1530
|
setTimeout(() => {
|
|
1438
1531
|
this.showFloatingWidget();
|
|
1439
|
-
}, 400);
|
|
1532
|
+
}, 400);
|
|
1440
1533
|
}
|
|
1441
1534
|
}
|
|
1442
1535
|
/**
|
|
@@ -1449,14 +1542,19 @@ class CookieConsent {
|
|
|
1449
1542
|
* Reset consent (clear stored data and show banner)
|
|
1450
1543
|
*/
|
|
1451
1544
|
reset() {
|
|
1545
|
+
var _a, _b;
|
|
1452
1546
|
this.storageManager.clear();
|
|
1453
1547
|
this.scriptBlocker.block();
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1548
|
+
const denied = { necessary: true, analytics: false, marketing: false };
|
|
1549
|
+
for (const key of Object.keys(this.config.categories)) {
|
|
1550
|
+
if (key !== 'necessary')
|
|
1551
|
+
denied[key] = false;
|
|
1552
|
+
}
|
|
1553
|
+
clearDeniedCookies(denied);
|
|
1457
1554
|
if (this.gtmIntegration) {
|
|
1458
|
-
this.gtmIntegration.updateConsent(
|
|
1555
|
+
this.gtmIntegration.updateConsent(denied);
|
|
1459
1556
|
}
|
|
1557
|
+
(_b = (_a = this.config).onReject) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1460
1558
|
this.showBanner();
|
|
1461
1559
|
}
|
|
1462
1560
|
/**
|
|
@@ -1476,6 +1574,10 @@ class CookieConsent {
|
|
|
1476
1574
|
*/
|
|
1477
1575
|
destroy() {
|
|
1478
1576
|
var _a, _b, _c, _d;
|
|
1577
|
+
if (this.hideTimeout) {
|
|
1578
|
+
clearTimeout(this.hideTimeout);
|
|
1579
|
+
this.hideTimeout = null;
|
|
1580
|
+
}
|
|
1479
1581
|
(_a = this.banner) === null || _a === void 0 ? void 0 : _a.destroy();
|
|
1480
1582
|
this.banner = null;
|
|
1481
1583
|
(_b = this.preferenceCenter) === null || _b === void 0 ? void 0 : _b.destroy();
|
|
@@ -1483,6 +1585,10 @@ class CookieConsent {
|
|
|
1483
1585
|
(_c = this.floatingWidget) === null || _c === void 0 ? void 0 : _c.destroy();
|
|
1484
1586
|
this.floatingWidget = null;
|
|
1485
1587
|
(_d = this.scriptBlocker) === null || _d === void 0 ? void 0 : _d.destroy();
|
|
1588
|
+
this.eventEmitter.clear();
|
|
1589
|
+
if (window.cookieConsent === this) {
|
|
1590
|
+
window.cookieConsent = undefined;
|
|
1591
|
+
}
|
|
1486
1592
|
}
|
|
1487
1593
|
/**
|
|
1488
1594
|
* Show the banner
|
|
@@ -1508,7 +1614,6 @@ class CookieConsent {
|
|
|
1508
1614
|
*/
|
|
1509
1615
|
applyConsent(categories) {
|
|
1510
1616
|
this.scriptBlocker.unblock(categories);
|
|
1511
|
-
// CNIL/GDPR: actively delete cookies for denied categories
|
|
1512
1617
|
clearDeniedCookies(categories);
|
|
1513
1618
|
}
|
|
1514
1619
|
/**
|
|
@@ -1519,22 +1624,22 @@ class CookieConsent {
|
|
|
1519
1624
|
necessary: {
|
|
1520
1625
|
enabled: true,
|
|
1521
1626
|
readOnly: true,
|
|
1522
|
-
label: '
|
|
1523
|
-
description: '
|
|
1627
|
+
label: 'Essential',
|
|
1628
|
+
description: 'Required for the website to function properly.',
|
|
1524
1629
|
},
|
|
1525
1630
|
analytics: {
|
|
1526
1631
|
enabled: true,
|
|
1527
1632
|
readOnly: false,
|
|
1528
|
-
label: '
|
|
1529
|
-
description: '
|
|
1633
|
+
label: 'Analytics',
|
|
1634
|
+
description: 'Help us understand how you use our site.',
|
|
1530
1635
|
},
|
|
1531
1636
|
marketing: {
|
|
1532
1637
|
enabled: true,
|
|
1533
1638
|
readOnly: false,
|
|
1534
1639
|
label: 'Marketing',
|
|
1535
|
-
description: '
|
|
1640
|
+
description: 'Used to deliver relevant advertisements.',
|
|
1536
1641
|
},
|
|
1537
|
-
}, mode: config.mode || 'opt-in', autoShow: config.autoShow !== undefined ? config.autoShow : true, revision: config.revision || 1, gtmConsentMode: config.gtmConsentMode
|
|
1642
|
+
}, 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' });
|
|
1538
1643
|
}
|
|
1539
1644
|
}
|
|
1540
1645
|
|