cookiecraft 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.
@@ -0,0 +1,1508 @@
1
+ /**
2
+ * StorageManager - Manages localStorage persistence for consent records
3
+ */
4
+ class StorageManager {
5
+ /**
6
+ * Save consent record to localStorage
7
+ */
8
+ save(consent) {
9
+ try {
10
+ localStorage.setItem(StorageManager.STORAGE_KEY, JSON.stringify(consent));
11
+ }
12
+ catch (e) {
13
+ console.error('Failed to save consent:', e);
14
+ }
15
+ }
16
+ /**
17
+ * Load consent record from localStorage
18
+ */
19
+ load() {
20
+ try {
21
+ const data = localStorage.getItem(StorageManager.STORAGE_KEY);
22
+ if (!data)
23
+ return null;
24
+ const parsed = JSON.parse(data);
25
+ // Validate schema
26
+ if (!this.validateSchema(parsed)) {
27
+ // Try migration
28
+ const migrated = this.migrate(parsed);
29
+ if (migrated) {
30
+ this.save(migrated);
31
+ return migrated;
32
+ }
33
+ return null;
34
+ }
35
+ return parsed;
36
+ }
37
+ catch (e) {
38
+ console.error('Failed to load consent:', e);
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * Clear consent record from localStorage
44
+ */
45
+ clear() {
46
+ localStorage.removeItem(StorageManager.STORAGE_KEY);
47
+ }
48
+ /**
49
+ * Check if consent record has expired
50
+ */
51
+ isExpired(consent) {
52
+ const expiry = new Date(consent.expiresAt);
53
+ return expiry < new Date();
54
+ }
55
+ /**
56
+ * Validate consent record schema
57
+ */
58
+ validateSchema(data) {
59
+ return (data &&
60
+ typeof data.version === 'number' &&
61
+ typeof data.timestamp === 'string' &&
62
+ typeof data.categories === 'object' &&
63
+ typeof data.userAgent === 'string' &&
64
+ typeof data.expiresAt === 'string');
65
+ }
66
+ /**
67
+ * Migrate old consent format to new format
68
+ * Returns null if migration fails
69
+ */
70
+ migrate(data) {
71
+ if (!data || typeof data !== 'object')
72
+ return null;
73
+ const record = data;
74
+ // Attempt to reconstruct a valid record from partial data
75
+ if (record.categories && typeof record.categories === 'object') {
76
+ const now = new Date();
77
+ const expiryDate = new Date(now);
78
+ expiryDate.setMonth(expiryDate.getMonth() + StorageManager.EXPIRY_MONTHS);
79
+ return {
80
+ version: typeof record.version === 'number' ? record.version : 1,
81
+ timestamp: typeof record.timestamp === 'string' ? record.timestamp : now.toISOString(),
82
+ categories: record.categories,
83
+ userAgent: typeof record.userAgent === 'string' ? record.userAgent : navigator.userAgent,
84
+ expiresAt: typeof record.expiresAt === 'string' ? record.expiresAt : expiryDate.toISOString(),
85
+ };
86
+ }
87
+ return null;
88
+ }
89
+ }
90
+ StorageManager.STORAGE_KEY = 'cookiecraft_consent';
91
+ StorageManager.EXPIRY_MONTHS = 13;
92
+
93
+ /**
94
+ * ConsentManager - Handles consent logic and validation
95
+ */
96
+ class ConsentManager {
97
+ constructor(config) {
98
+ this.config = config;
99
+ }
100
+ /**
101
+ * Validate consent categories
102
+ */
103
+ validateConsent(categories) {
104
+ // Necessary cookies must always be enabled
105
+ if (!categories.necessary) {
106
+ return false;
107
+ }
108
+ // Validate against config
109
+ for (const key of Object.keys(categories)) {
110
+ if (!(key in this.config.categories)) {
111
+ return false;
112
+ }
113
+ }
114
+ return true;
115
+ }
116
+ /**
117
+ * Update consent with new categories
118
+ */
119
+ updateConsent(categories) {
120
+ if (!this.validateConsent(categories)) {
121
+ throw new Error('Invalid consent categories');
122
+ }
123
+ this.consent = this.createConsentRecord(categories);
124
+ return this.consent;
125
+ }
126
+ /**
127
+ * Check if user needs to give consent
128
+ */
129
+ needsConsent() {
130
+ return this.consent === undefined;
131
+ }
132
+ /**
133
+ * Check if stored consent needs update due to policy change
134
+ */
135
+ needsUpdate(storedConsent) {
136
+ // Check if policy version has changed
137
+ return storedConsent.version < this.config.revision;
138
+ }
139
+ /**
140
+ * Get current consent record
141
+ */
142
+ getCurrentConsent() {
143
+ return this.consent;
144
+ }
145
+ /**
146
+ * Create a new consent record
147
+ */
148
+ createConsentRecord(categories) {
149
+ const now = new Date();
150
+ const expiryDate = new Date(now);
151
+ expiryDate.setMonth(expiryDate.getMonth() + StorageManager.EXPIRY_MONTHS);
152
+ return {
153
+ version: this.config.revision,
154
+ timestamp: now.toISOString(),
155
+ categories: Object.assign({}, categories),
156
+ userAgent: navigator.userAgent,
157
+ expiresAt: expiryDate.toISOString(),
158
+ };
159
+ }
160
+ }
161
+
162
+ /**
163
+ * EventEmitter - Simple pub/sub pattern for internal and external events
164
+ */
165
+ class EventEmitter {
166
+ constructor() {
167
+ this.events = new Map();
168
+ }
169
+ /**
170
+ * Register an event handler
171
+ */
172
+ on(event, callback) {
173
+ if (!this.events.has(event)) {
174
+ this.events.set(event, new Set());
175
+ }
176
+ this.events.get(event).add(callback);
177
+ }
178
+ /**
179
+ * Unregister an event handler
180
+ */
181
+ off(event, callback) {
182
+ if (this.events.has(event)) {
183
+ this.events.get(event).delete(callback);
184
+ }
185
+ }
186
+ /**
187
+ * Emit an event with optional data
188
+ */
189
+ emit(event, data) {
190
+ if (this.events.has(event)) {
191
+ this.events.get(event).forEach((callback) => {
192
+ try {
193
+ callback(data);
194
+ }
195
+ catch (e) {
196
+ console.error(`Error in event handler for ${event}:`, e);
197
+ }
198
+ });
199
+ }
200
+ }
201
+ /**
202
+ * Clear all event handlers
203
+ */
204
+ clear() {
205
+ this.events.clear();
206
+ }
207
+ /**
208
+ * Clear handlers for a specific event
209
+ */
210
+ clearEvent(event) {
211
+ this.events.delete(event);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * ScriptBlocker - Prevents scripts from executing before consent using MutationObserver
217
+ */
218
+ class ScriptBlocker {
219
+ constructor(categoryManager, eventEmitter) {
220
+ this.observer = null;
221
+ this.blockedScripts = new Map();
222
+ this.currentConsent = null;
223
+ this.categoryManager = categoryManager;
224
+ this.eventEmitter = eventEmitter;
225
+ }
226
+ /**
227
+ * Initialize script blocking
228
+ */
229
+ init() {
230
+ // Block all existing scripts
231
+ this.blockExistingScripts();
232
+ // Watch for dynamically added scripts (GTM, etc.)
233
+ this.observeDOM();
234
+ }
235
+ /**
236
+ * Block all scripts (reset consent)
237
+ */
238
+ block() {
239
+ this.currentConsent = null;
240
+ this.blockedScripts.clear();
241
+ this.blockExistingScripts();
242
+ }
243
+ /**
244
+ * Unblock scripts based on consent categories
245
+ */
246
+ unblock(categories) {
247
+ this.currentConsent = categories;
248
+ // Reactivate blocked scripts based on consent
249
+ const toRemove = [];
250
+ this.blockedScripts.forEach((script, id) => {
251
+ const category = this.categoryManager.getCategoryForScript(script);
252
+ if (category && this.categoryManager.isAllowed(category, categories)) {
253
+ this.reactivateScript(script);
254
+ toRemove.push(id);
255
+ }
256
+ });
257
+ // Remove after iteration to avoid modifying map during forEach
258
+ toRemove.forEach((id) => this.blockedScripts.delete(id));
259
+ }
260
+ /**
261
+ * Destroy the blocker and stop observing
262
+ */
263
+ destroy() {
264
+ if (this.observer) {
265
+ this.observer.disconnect();
266
+ this.observer = null;
267
+ }
268
+ }
269
+ /**
270
+ * Block all existing scripts with data-cookieconsent attribute
271
+ */
272
+ blockExistingScripts() {
273
+ const scripts = document.querySelectorAll('script[data-cookieconsent]');
274
+ scripts.forEach((script) => {
275
+ if (script instanceof HTMLScriptElement) {
276
+ this.processScript(script);
277
+ }
278
+ });
279
+ }
280
+ /**
281
+ * Observe DOM for dynamically added scripts
282
+ */
283
+ observeDOM() {
284
+ this.observer = new MutationObserver((mutations) => {
285
+ mutations.forEach((mutation) => {
286
+ mutation.addedNodes.forEach((node) => {
287
+ if (node instanceof HTMLScriptElement &&
288
+ node.hasAttribute('data-cookieconsent')) {
289
+ this.processScript(node);
290
+ }
291
+ });
292
+ });
293
+ });
294
+ this.observer.observe(document.documentElement, {
295
+ childList: true,
296
+ subtree: true,
297
+ });
298
+ }
299
+ /**
300
+ * Process a script element - block or allow based on consent
301
+ */
302
+ processScript(script) {
303
+ const category = this.categoryManager.getCategoryForScript(script);
304
+ if (!category)
305
+ return;
306
+ // Check if consent allows this category
307
+ if (this.currentConsent &&
308
+ this.categoryManager.isAllowed(category, this.currentConsent)) {
309
+ // Already have consent, don't block
310
+ return;
311
+ }
312
+ // Block the script by changing its type
313
+ if (script.type !== 'text/plain') {
314
+ const id = this.generateScriptId(script);
315
+ // Don't re-block scripts we already know about
316
+ if (this.blockedScripts.has(id))
317
+ return;
318
+ // Store original type if it exists
319
+ const originalType = script.type || 'text/javascript';
320
+ script.setAttribute('data-original-type', originalType);
321
+ script.type = 'text/plain';
322
+ this.blockedScripts.set(id, script);
323
+ }
324
+ }
325
+ /**
326
+ * Reactivate a blocked script by creating a new one with correct type
327
+ */
328
+ reactivateScript(script) {
329
+ // Clone script and change type to execute it
330
+ const newScript = document.createElement('script');
331
+ // Copy attributes
332
+ Array.from(script.attributes).forEach((attr) => {
333
+ if (attr.name !== 'type' && attr.name !== 'data-original-type') {
334
+ newScript.setAttribute(attr.name, attr.value);
335
+ }
336
+ });
337
+ // Set correct type
338
+ const originalType = script.getAttribute('data-original-type') || 'text/javascript';
339
+ newScript.type = originalType;
340
+ // Copy content
341
+ if (script.src) {
342
+ newScript.src = script.src;
343
+ }
344
+ else {
345
+ newScript.textContent = script.textContent;
346
+ }
347
+ // Replace old script
348
+ if (script.parentNode) {
349
+ script.parentNode.insertBefore(newScript, script);
350
+ script.parentNode.removeChild(script);
351
+ }
352
+ this.eventEmitter.emit('script:activated', {
353
+ category: script.getAttribute('data-cookieconsent'),
354
+ src: script.src || 'inline',
355
+ });
356
+ }
357
+ /**
358
+ * Generate a stable, deterministic ID for a script element
359
+ */
360
+ generateScriptId(script) {
361
+ // Use src for external scripts (stable across calls)
362
+ if (script.src) {
363
+ return `src:${script.src}`;
364
+ }
365
+ // For inline scripts, use a hash of the content for stability
366
+ const content = script.textContent || '';
367
+ const category = script.getAttribute('data-cookieconsent') || '';
368
+ // Use existing data-cc-id if present (allows reset/re-block)
369
+ const existingId = script.getAttribute('data-cc-id');
370
+ if (existingId)
371
+ return existingId;
372
+ // Generate deterministic ID from content + category
373
+ const id = `inline:${category}:${this.simpleHash(content)}`;
374
+ script.setAttribute('data-cc-id', id);
375
+ return id;
376
+ }
377
+ /**
378
+ * Simple hash function for content-based script identification
379
+ */
380
+ simpleHash(str) {
381
+ let hash = 0;
382
+ for (let i = 0; i < str.length; i++) {
383
+ const char = str.charCodeAt(i);
384
+ hash = ((hash << 5) - hash) + char;
385
+ hash |= 0; // Convert to 32bit integer
386
+ }
387
+ return hash.toString(36);
388
+ }
389
+ }
390
+
391
+ /**
392
+ * CategoryManager - Maps scripts to consent categories and manages patterns
393
+ */
394
+ class CategoryManager {
395
+ constructor() {
396
+ this.categories = new Map();
397
+ // Initialize with common patterns
398
+ this.initializeDefaultPatterns();
399
+ }
400
+ /**
401
+ * Register a category with URL patterns
402
+ */
403
+ registerCategory(name, patterns) {
404
+ this.categories.set(name, patterns);
405
+ }
406
+ /**
407
+ * Get category for a script element
408
+ */
409
+ getCategoryForScript(script) {
410
+ // Explicit category attribute takes precedence
411
+ const explicitCategory = script.getAttribute('data-cookieconsent');
412
+ if (explicitCategory) {
413
+ return explicitCategory;
414
+ }
415
+ // Try to match by src pattern
416
+ const src = script.src;
417
+ if (!src)
418
+ return null;
419
+ for (const [category, patterns] of this.categories) {
420
+ if (patterns.some((pattern) => src.includes(pattern))) {
421
+ return category;
422
+ }
423
+ }
424
+ return null;
425
+ }
426
+ /**
427
+ * Check if a category is allowed based on consent
428
+ */
429
+ isAllowed(category, consent) {
430
+ return consent[category] === true;
431
+ }
432
+ /**
433
+ * Initialize default URL patterns for common tracking services
434
+ */
435
+ initializeDefaultPatterns() {
436
+ this.categories.set('analytics', [
437
+ 'google-analytics.com',
438
+ 'googletagmanager.com',
439
+ 'analytics.google.com',
440
+ 'plausible.io',
441
+ 'matomo.org',
442
+ 'hotjar.com',
443
+ 'mixpanel.com',
444
+ 'segment.com',
445
+ 'amplitude.com',
446
+ ]);
447
+ this.categories.set('marketing', [
448
+ 'facebook.net',
449
+ 'facebook.com/tr',
450
+ 'connect.facebook.net',
451
+ 'doubleclick.net',
452
+ 'ads.google.com',
453
+ 'linkedin.com/analytics',
454
+ 'twitter.com/i/adsct',
455
+ 'pinterest.com/ct',
456
+ 'adroll.com',
457
+ 'taboola.com',
458
+ 'outbrain.com',
459
+ ]);
460
+ this.categories.set('necessary', []);
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Sanitization utilities to prevent XSS in HTML templates
466
+ */
467
+ const HTML_ESCAPE_MAP = {
468
+ '&': '&amp;',
469
+ '<': '&lt;',
470
+ '>': '&gt;',
471
+ '"': '&quot;',
472
+ "'": '&#x27;',
473
+ '`': '&#x60;',
474
+ };
475
+ const HTML_ESCAPE_RE = /[&<>"'`]/g;
476
+ /**
477
+ * Escape HTML entities in a string to prevent XSS
478
+ */
479
+ function escapeHtml(str) {
480
+ return str.replace(HTML_ESCAPE_RE, (char) => HTML_ESCAPE_MAP[char] || char);
481
+ }
482
+ /**
483
+ * Sanitize a URL - only allow http(s) and relative URLs
484
+ */
485
+ function sanitizeUrl(url) {
486
+ const trimmed = url.trim();
487
+ if (trimmed.startsWith('https://') ||
488
+ trimmed.startsWith('http://') ||
489
+ trimmed.startsWith('/') ||
490
+ trimmed.startsWith('./')) {
491
+ return escapeHtml(trimmed);
492
+ }
493
+ return '';
494
+ }
495
+ /**
496
+ * Sanitize a CSS color value - only allow valid hex, rgb, hsl, named colors
497
+ */
498
+ function sanitizeColor(color) {
499
+ const trimmed = color.trim();
500
+ // Allow hex colors
501
+ if (/^#[0-9a-fA-F]{3,8}$/.test(trimmed))
502
+ return trimmed;
503
+ // Allow rgb/rgba
504
+ if (/^rgba?\(\s*[\d\s,./%]+\)$/.test(trimmed))
505
+ return trimmed;
506
+ // Allow hsl/hsla
507
+ if (/^hsla?\(\s*[\d\s,./%deg]+\)$/.test(trimmed))
508
+ return trimmed;
509
+ // Allow CSS named colors (basic set)
510
+ if (/^[a-zA-Z]+$/.test(trimmed))
511
+ return trimmed;
512
+ return '';
513
+ }
514
+
515
+ /**
516
+ * Banner - Cookie consent banner component
517
+ */
518
+ class Banner {
519
+ constructor(config, eventEmitter) {
520
+ this.element = null;
521
+ this.config = config;
522
+ this.eventEmitter = eventEmitter;
523
+ }
524
+ /**
525
+ * Show the banner
526
+ */
527
+ show() {
528
+ var _a;
529
+ if (!this.element) {
530
+ this.element = this.createDOM();
531
+ document.body.appendChild(this.element);
532
+ this.attachListeners();
533
+ }
534
+ // Trigger animation
535
+ requestAnimationFrame(() => {
536
+ var _a;
537
+ (_a = this.element) === null || _a === void 0 ? void 0 : _a.classList.add('is-visible');
538
+ });
539
+ // Focus first button for accessibility
540
+ const firstButton = (_a = this.element) === null || _a === void 0 ? void 0 : _a.querySelector('button');
541
+ firstButton === null || firstButton === void 0 ? void 0 : firstButton.focus();
542
+ // Disable page interaction if configured
543
+ if (this.config.disablePageInteraction) {
544
+ document.body.style.overflow = 'hidden';
545
+ }
546
+ }
547
+ /**
548
+ * Hide the banner
549
+ */
550
+ hide() {
551
+ var _a;
552
+ (_a = this.element) === null || _a === void 0 ? void 0 : _a.classList.remove('is-visible');
553
+ // Re-enable page interaction
554
+ if (this.config.disablePageInteraction) {
555
+ document.body.style.overflow = '';
556
+ }
557
+ setTimeout(() => {
558
+ this.destroy();
559
+ }, 300); // Match CSS transition
560
+ }
561
+ /**
562
+ * Destroy the banner
563
+ */
564
+ destroy() {
565
+ if (this.element) {
566
+ this.element.remove();
567
+ this.element = null;
568
+ }
569
+ }
570
+ /**
571
+ * Create DOM structure for banner
572
+ */
573
+ createDOM() {
574
+ const translations = this.config.translations || {};
575
+ const theme = this.config.theme || 'light';
576
+ const position = this.config.position || 'bottom';
577
+ const layout = this.config.layout || 'bar';
578
+ const backdropBlur = this.config.backdropBlur !== false;
579
+ const safeColor = this.config.primaryColor ? sanitizeColor(this.config.primaryColor) : '';
580
+ const colorStyle = safeColor ? `--cc-primary: ${safeColor};` : '';
581
+ const template = `
582
+ <div
583
+ class="cc-banner cc-banner--${escapeHtml(position)} cc-banner--${escapeHtml(layout)} ${backdropBlur ? 'cc-backdrop-blur' : ''}"
584
+ role="region"
585
+ aria-label="Cookie consent"
586
+ aria-live="polite"
587
+ data-theme="${escapeHtml(theme)}"
588
+ style="${colorStyle}"
589
+ >
590
+ <div class="cc-banner__container">
591
+ <div class="cc-banner__content">
592
+ <h2 class="cc-banner__title">
593
+ ${escapeHtml(translations.title || '🍪 Nous utilisons des cookies')}
594
+ </h2>
595
+ <p class="cc-banner__description">
596
+ ${this.getDescriptionHTML()}
597
+ </p>
598
+ </div>
599
+ <div class="cc-banner__actions">
600
+ <button
601
+ class="cc-btn cc-btn--tertiary"
602
+ data-action="customize"
603
+ aria-label="${escapeHtml(translations.customize || 'Personnaliser')}"
604
+ >
605
+ ${escapeHtml(translations.customize || 'Personnaliser')}
606
+ </button>
607
+ <button
608
+ class="cc-btn cc-btn--reject"
609
+ data-action="reject"
610
+ aria-label="${escapeHtml(translations.rejectAll || 'Tout refuser')}"
611
+ >
612
+ ${escapeHtml(translations.rejectAll || 'Tout refuser')}
613
+ </button>
614
+ <button
615
+ class="cc-btn cc-btn--accept"
616
+ data-action="accept"
617
+ aria-label="${escapeHtml(translations.acceptAll || 'Tout accepter')}"
618
+ >
619
+ ${escapeHtml(translations.acceptAll || 'Tout accepter')}
620
+ </button>
621
+ </div>
622
+ </div>
623
+ </div>
624
+ `;
625
+ const wrapper = document.createElement('div');
626
+ wrapper.innerHTML = template.trim();
627
+ return wrapper.firstChild;
628
+ }
629
+ /**
630
+ * Attach event listeners
631
+ */
632
+ attachListeners() {
633
+ var _a, _b;
634
+ (_a = this.element) === null || _a === void 0 ? void 0 : _a.addEventListener('click', (e) => {
635
+ const target = e.target.closest('[data-action]');
636
+ if (!target)
637
+ return;
638
+ const action = target.getAttribute('data-action');
639
+ switch (action) {
640
+ case 'accept':
641
+ this.handleAcceptAll();
642
+ break;
643
+ case 'reject':
644
+ this.handleRejectAll();
645
+ break;
646
+ case 'customize':
647
+ this.handleCustomize();
648
+ break;
649
+ }
650
+ });
651
+ // Keyboard support
652
+ (_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
653
+ if (e.key === 'Escape' && this.config.disablePageInteraction) {
654
+ // Allow ESC to close if page interaction is disabled
655
+ this.handleRejectAll();
656
+ }
657
+ });
658
+ }
659
+ /**
660
+ * Handle accept all action
661
+ */
662
+ handleAcceptAll() {
663
+ var _a, _b;
664
+ const allCategories = {
665
+ necessary: true,
666
+ analytics: true,
667
+ marketing: true,
668
+ };
669
+ // Only add preferences if it's configured
670
+ if (this.config.categories.preferences) {
671
+ allCategories.preferences = true;
672
+ }
673
+ this.eventEmitter.emit('consent:accept', allCategories);
674
+ (_b = (_a = this.config).onAccept) === null || _b === void 0 ? void 0 : _b.call(_a, allCategories);
675
+ this.hide();
676
+ }
677
+ /**
678
+ * Handle reject all action
679
+ */
680
+ handleRejectAll() {
681
+ var _a, _b;
682
+ const necessaryOnly = {
683
+ necessary: true,
684
+ analytics: false,
685
+ marketing: false,
686
+ };
687
+ // Only add preferences if it's configured
688
+ if (this.config.categories.preferences) {
689
+ necessaryOnly.preferences = false;
690
+ }
691
+ this.eventEmitter.emit('consent:reject', necessaryOnly);
692
+ (_b = (_a = this.config).onReject) === null || _b === void 0 ? void 0 : _b.call(_a);
693
+ this.hide();
694
+ }
695
+ /**
696
+ * Handle customize action
697
+ */
698
+ handleCustomize() {
699
+ this.eventEmitter.emit('preferences:show');
700
+ this.hide();
701
+ }
702
+ /**
703
+ * Generate description HTML with privacy policy link
704
+ */
705
+ getDescriptionHTML() {
706
+ const translations = this.config.translations || {};
707
+ const defaultDescription = 'Pour améliorer votre expérience sur notre site, nous utilisons des cookies. Vous pouvez choisir les cookies que vous acceptez.';
708
+ const description = escapeHtml(translations.description || defaultDescription);
709
+ if (translations.privacyPolicyUrl) {
710
+ const safeUrl = sanitizeUrl(translations.privacyPolicyUrl);
711
+ if (safeUrl) {
712
+ const linkLabel = escapeHtml(translations.privacyPolicyLabel || 'Politique de confidentialité');
713
+ return `${description} <a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${linkLabel}</a>`;
714
+ }
715
+ }
716
+ return description;
717
+ }
718
+ }
719
+
720
+ /**
721
+ * PreferenceCenter - Modal for granular cookie preferences
722
+ */
723
+ class PreferenceCenter {
724
+ constructor(config, eventEmitter, currentConsent) {
725
+ this.element = null;
726
+ this.config = config;
727
+ this.eventEmitter = eventEmitter;
728
+ this.currentConsent = currentConsent;
729
+ }
730
+ /**
731
+ * Show the preference center
732
+ */
733
+ show() {
734
+ if (!this.element) {
735
+ this.element = this.createDOM();
736
+ document.body.appendChild(this.element);
737
+ this.attachListeners();
738
+ }
739
+ this.element.classList.add('is-visible');
740
+ this.trapFocus();
741
+ // Prevent body scroll
742
+ document.body.style.overflow = 'hidden';
743
+ }
744
+ /**
745
+ * Hide the preference center
746
+ */
747
+ hide() {
748
+ var _a;
749
+ (_a = this.element) === null || _a === void 0 ? void 0 : _a.classList.remove('is-visible');
750
+ document.body.style.overflow = '';
751
+ setTimeout(() => {
752
+ this.destroy();
753
+ }, 300);
754
+ }
755
+ /**
756
+ * Destroy the preference center
757
+ */
758
+ destroy() {
759
+ if (this.element) {
760
+ this.element.remove();
761
+ this.element = null;
762
+ }
763
+ }
764
+ /**
765
+ * Create DOM structure for preference center
766
+ */
767
+ createDOM() {
768
+ const translations = this.config.translations || {};
769
+ const theme = this.config.theme || 'light';
770
+ const position = this.config.preferencesPosition || 'center';
771
+ const safeColor = this.config.primaryColor ? sanitizeColor(this.config.primaryColor) : '';
772
+ const colorStyle = safeColor
773
+ ? `--cc-primary: ${safeColor}; --cc-primary-hover: ${this.adjustColorBrightness(safeColor, -15)};`
774
+ : '';
775
+ const privacyLinkHtml = translations.privacyPolicyUrl
776
+ ? (() => {
777
+ const safeUrl = sanitizeUrl(translations.privacyPolicyUrl);
778
+ if (!safeUrl)
779
+ return '';
780
+ return `
781
+ <a href="${safeUrl}" target="_blank" rel="noopener noreferrer" class="cc-privacy-link">
782
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
783
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
784
+ <polyline points="15 3 21 3 21 9"/>
785
+ <line x1="10" y1="14" x2="21" y2="3"/>
786
+ </svg>
787
+ ${escapeHtml(translations.privacyPolicyLabel || 'Politique de confidentialité')}
788
+ </a>
789
+ `;
790
+ })()
791
+ : '';
792
+ const template = `
793
+ <div
794
+ class="cc-modal cc-modal--${escapeHtml(position)}"
795
+ role="dialog"
796
+ aria-modal="true"
797
+ aria-labelledby="cc-modal-title"
798
+ data-theme="${escapeHtml(theme)}"
799
+ style="${colorStyle}"
800
+ >
801
+ <div class="cc-modal__overlay" data-action="close"></div>
802
+ <div class="cc-modal__content">
803
+ <div class="cc-modal__header">
804
+ <h2 id="cc-modal-title">
805
+ ${escapeHtml(translations.preferencesTitle || translations.title || 'Préférences de cookies')}
806
+ </h2>
807
+ <button
808
+ class="cc-modal__close"
809
+ aria-label="Fermer"
810
+ data-action="close"
811
+ >
812
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
813
+ <path d="M18 6L6 18M6 6l12 12" stroke-width="2" stroke-linecap="round"/>
814
+ </svg>
815
+ </button>
816
+ </div>
817
+
818
+ <div class="cc-modal__body">
819
+ ${this.renderCategories()}
820
+ </div>
821
+
822
+ <div class="cc-modal__footer">
823
+ <div class="cc-modal__footer-links">
824
+ ${privacyLinkHtml}
825
+ </div>
826
+ <div class="cc-modal__footer-actions">
827
+ <button
828
+ class="cc-btn cc-btn--secondary"
829
+ data-action="reject"
830
+ >
831
+ ${escapeHtml(translations.essentialsOnly || 'Uniquement les essentiels')}
832
+ </button>
833
+ <button
834
+ class="cc-btn cc-btn--primary"
835
+ data-action="save"
836
+ >
837
+ ${escapeHtml(translations.savePreferences || 'Enregistrer mes choix')}
838
+ </button>
839
+ </div>
840
+ </div>
841
+ </div>
842
+ </div>
843
+ `;
844
+ const wrapper = document.createElement('div');
845
+ wrapper.innerHTML = template.trim();
846
+ return wrapper.firstChild;
847
+ }
848
+ /**
849
+ * Render category toggles
850
+ */
851
+ renderCategories() {
852
+ const categories = Object.entries(this.config.categories);
853
+ return categories
854
+ .map(([key, config]) => {
855
+ const checked = this.currentConsent[key];
856
+ const disabled = config.readOnly;
857
+ return `
858
+ <div class="cc-category">
859
+ <div class="cc-category__header">
860
+ <label class="cc-toggle ${disabled ? 'cc-toggle--disabled' : ''}">
861
+ <input
862
+ type="checkbox"
863
+ data-category="${escapeHtml(key)}"
864
+ ${checked ? 'checked' : ''}
865
+ ${disabled ? 'disabled' : ''}
866
+ aria-label="${escapeHtml(config.label)} cookies"
867
+ >
868
+ <span class="cc-toggle__slider"></span>
869
+ </label>
870
+ <div class="cc-category__info">
871
+ <h3>${escapeHtml(config.label)}</h3>
872
+ <p>${escapeHtml(config.description)}</p>
873
+ </div>
874
+ </div>
875
+ </div>
876
+ `;
877
+ })
878
+ .join('');
879
+ }
880
+ /**
881
+ * Attach event listeners
882
+ */
883
+ attachListeners() {
884
+ var _a, _b;
885
+ (_a = this.element) === null || _a === void 0 ? void 0 : _a.addEventListener('click', (e) => {
886
+ const target = e.target.closest('[data-action]');
887
+ if (!target)
888
+ return;
889
+ const action = target.getAttribute('data-action');
890
+ if (action === 'close') {
891
+ this.hide();
892
+ }
893
+ else if (action === 'save') {
894
+ this.handleSave();
895
+ }
896
+ else if (action === 'reject') {
897
+ this.handleRejectAll();
898
+ }
899
+ });
900
+ // Keyboard shortcuts
901
+ (_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
902
+ if (e.key === 'Escape') {
903
+ this.hide();
904
+ }
905
+ });
906
+ }
907
+ /**
908
+ * Handle save preferences
909
+ */
910
+ handleSave() {
911
+ var _a, _b, _c;
912
+ const checkboxes = (_a = this.element) === null || _a === void 0 ? void 0 : _a.querySelectorAll('input[data-category]');
913
+ const categories = {
914
+ necessary: true,
915
+ analytics: false,
916
+ marketing: false,
917
+ };
918
+ checkboxes === null || checkboxes === void 0 ? void 0 : checkboxes.forEach((checkbox) => {
919
+ if (checkbox instanceof HTMLInputElement) {
920
+ const category = checkbox.getAttribute('data-category');
921
+ if (category) {
922
+ categories[category] = checkbox.checked;
923
+ }
924
+ }
925
+ });
926
+ this.eventEmitter.emit('consent:update', categories);
927
+ (_c = (_b = this.config).onChange) === null || _c === void 0 ? void 0 : _c.call(_b, categories);
928
+ this.hide();
929
+ }
930
+ /**
931
+ * Handle reject all
932
+ */
933
+ handleRejectAll() {
934
+ const necessaryOnly = {
935
+ necessary: true,
936
+ analytics: false,
937
+ marketing: false,
938
+ };
939
+ // Only add preferences if it's configured
940
+ if (this.config.categories.preferences) {
941
+ necessaryOnly.preferences = false;
942
+ }
943
+ this.eventEmitter.emit('consent:reject', necessaryOnly);
944
+ this.hide();
945
+ }
946
+ /**
947
+ * Trap focus within modal
948
+ */
949
+ trapFocus() {
950
+ var _a, _b;
951
+ const focusableElements = (_a = this.element) === null || _a === void 0 ? void 0 : _a.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
952
+ if (!focusableElements || focusableElements.length === 0)
953
+ return;
954
+ const firstFocusable = focusableElements[0];
955
+ const lastFocusable = focusableElements[focusableElements.length - 1];
956
+ // Focus first element
957
+ firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus();
958
+ // Trap focus
959
+ (_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
960
+ if (e.key === 'Tab') {
961
+ if (e.shiftKey && document.activeElement === firstFocusable) {
962
+ e.preventDefault();
963
+ lastFocusable.focus();
964
+ }
965
+ else if (!e.shiftKey && document.activeElement === lastFocusable) {
966
+ e.preventDefault();
967
+ firstFocusable.focus();
968
+ }
969
+ }
970
+ });
971
+ }
972
+ /**
973
+ * Adjust color brightness for hover effect
974
+ */
975
+ adjustColorBrightness(color, percent) {
976
+ // Remove # if present
977
+ const hex = color.replace('#', '');
978
+ // Convert to RGB
979
+ const r = parseInt(hex.substring(0, 2), 16);
980
+ const g = parseInt(hex.substring(2, 4), 16);
981
+ const b = parseInt(hex.substring(4, 6), 16);
982
+ // Adjust brightness
983
+ const adjust = (value) => {
984
+ const adjusted = value + (value * percent / 100);
985
+ return Math.max(0, Math.min(255, Math.round(adjusted)));
986
+ };
987
+ // Convert back to hex
988
+ const toHex = (value) => {
989
+ const hex = value.toString(16);
990
+ return hex.length === 1 ? '0' + hex : hex;
991
+ };
992
+ return `#${toHex(adjust(r))}${toHex(adjust(g))}${toHex(adjust(b))}`;
993
+ }
994
+ }
995
+
996
+ /**
997
+ * FloatingWidget - Permanent cookie settings button
998
+ * Stays visible at all times for easy access to preferences
999
+ */
1000
+ class FloatingWidget {
1001
+ constructor(config, eventEmitter) {
1002
+ this.element = null;
1003
+ this.isVisible = false;
1004
+ this.config = config;
1005
+ this.eventEmitter = eventEmitter;
1006
+ }
1007
+ /**
1008
+ * Show the floating widget
1009
+ */
1010
+ show() {
1011
+ if (!this.element) {
1012
+ this.element = this.createDOM();
1013
+ document.body.appendChild(this.element);
1014
+ this.attachListeners();
1015
+ }
1016
+ // Delay to allow for transition
1017
+ requestAnimationFrame(() => {
1018
+ var _a;
1019
+ (_a = this.element) === null || _a === void 0 ? void 0 : _a.classList.add('is-visible');
1020
+ this.isVisible = true;
1021
+ });
1022
+ }
1023
+ /**
1024
+ * Hide the floating widget
1025
+ */
1026
+ hide() {
1027
+ var _a;
1028
+ (_a = this.element) === null || _a === void 0 ? void 0 : _a.classList.remove('is-visible');
1029
+ this.isVisible = false;
1030
+ }
1031
+ /**
1032
+ * Destroy the widget
1033
+ */
1034
+ destroy() {
1035
+ if (this.element) {
1036
+ this.element.remove();
1037
+ this.element = null;
1038
+ }
1039
+ this.isVisible = false;
1040
+ }
1041
+ /**
1042
+ * Check if widget is visible
1043
+ */
1044
+ getIsVisible() {
1045
+ return this.isVisible;
1046
+ }
1047
+ /**
1048
+ * Create DOM structure for floating widget
1049
+ */
1050
+ createDOM() {
1051
+ const translations = this.config.translations || {};
1052
+ const theme = this.config.theme || 'light';
1053
+ const widgetPosition = this.config.widgetPosition || 'bottom-right';
1054
+ const widgetStyle = this.config.widgetStyle || 'full';
1055
+ const safeColor = this.config.primaryColor ? sanitizeColor(this.config.primaryColor) : '';
1056
+ const colorStyle = safeColor ? `--cc-primary: ${safeColor};` : '';
1057
+ const template = `
1058
+ <div
1059
+ class="cc-widget cc-widget--${escapeHtml(widgetPosition)} cc-widget--${escapeHtml(widgetStyle)}"
1060
+ role="button"
1061
+ aria-label="${escapeHtml(translations.cookieSettings || 'Paramètres des cookies')}"
1062
+ tabindex="0"
1063
+ data-theme="${escapeHtml(theme)}"
1064
+ style="${colorStyle}"
1065
+ >
1066
+ <svg class="cc-widget__icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
1067
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
1068
+ <circle cx="12" cy="12" r="2"/>
1069
+ <circle cx="7" cy="7" r="1.5"/>
1070
+ <circle cx="17" cy="7" r="1.5"/>
1071
+ <circle cx="7" cy="17" r="1.5"/>
1072
+ <circle cx="17" cy="17" r="1.5"/>
1073
+ </svg>
1074
+ <span class="cc-widget__text">
1075
+ ${escapeHtml(translations.cookies || 'Cookies')}
1076
+ </span>
1077
+ </div>
1078
+ `;
1079
+ const wrapper = document.createElement('div');
1080
+ wrapper.innerHTML = template.trim();
1081
+ return wrapper.firstChild;
1082
+ }
1083
+ /**
1084
+ * Attach event listeners
1085
+ */
1086
+ attachListeners() {
1087
+ var _a, _b;
1088
+ (_a = this.element) === null || _a === void 0 ? void 0 : _a.addEventListener('click', () => {
1089
+ this.handleClick();
1090
+ });
1091
+ (_b = this.element) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', (e) => {
1092
+ if (e.key === 'Enter' || e.key === ' ') {
1093
+ e.preventDefault();
1094
+ this.handleClick();
1095
+ }
1096
+ });
1097
+ }
1098
+ /**
1099
+ * Handle widget click
1100
+ */
1101
+ handleClick() {
1102
+ this.eventEmitter.emit('preferences:show');
1103
+ }
1104
+ }
1105
+
1106
+ /**
1107
+ * GTMConsentMode - Full integration with Google Consent Mode v2
1108
+ *
1109
+ * Implements all required signals:
1110
+ * - ad_storage, ad_user_data, ad_personalization, analytics_storage (core GCM v2)
1111
+ * - functionality_storage, personalization_storage, security_storage (non-core)
1112
+ * - wait_for_update, url_passthrough, ads_data_redaction (advanced features)
1113
+ */
1114
+ class GTMConsentMode {
1115
+ constructor(dataLayerManager, config) {
1116
+ this.dataLayerManager = dataLayerManager;
1117
+ this.config = config;
1118
+ }
1119
+ /**
1120
+ * Set default consent state (MUST be called BEFORE GTM loads)
1121
+ * All non-essential consent types default to 'denied' per GDPR
1122
+ */
1123
+ setDefaultConsent() {
1124
+ var _a;
1125
+ const defaults = {
1126
+ ad_storage: 'denied',
1127
+ ad_user_data: 'denied',
1128
+ ad_personalization: 'denied',
1129
+ analytics_storage: 'denied',
1130
+ functionality_storage: 'denied',
1131
+ personalization_storage: 'denied',
1132
+ security_storage: 'granted', // Always granted
1133
+ };
1134
+ // Add wait_for_update to give CMP time to restore returning visitor consent
1135
+ const waitForUpdate = (_a = this.config.gtmWaitForUpdate) !== null && _a !== void 0 ? _a : 500;
1136
+ if (waitForUpdate > 0) {
1137
+ defaults['wait_for_update'] = waitForUpdate;
1138
+ }
1139
+ this.dataLayerManager.pushConsent('default', defaults);
1140
+ // Set advanced features via gtag('set', ...)
1141
+ if (this.config.gtmUrlPassthrough) {
1142
+ this.dataLayerManager.pushSet('url_passthrough', true);
1143
+ }
1144
+ if (this.config.gtmAdsDataRedaction) {
1145
+ this.dataLayerManager.pushSet('ads_data_redaction', true);
1146
+ }
1147
+ }
1148
+ /**
1149
+ * Update consent state based on user choices
1150
+ * Called both on new consent and on page load for returning visitors
1151
+ */
1152
+ updateConsent(categories) {
1153
+ const gtmConsent = this.mapCategoriesToGTM(categories);
1154
+ this.dataLayerManager.pushConsent('update', gtmConsent);
1155
+ }
1156
+ /**
1157
+ * Map consent categories to GTM Consent Mode v2 format
1158
+ */
1159
+ mapCategoriesToGTM(categories) {
1160
+ return {
1161
+ ad_storage: categories.marketing ? 'granted' : 'denied',
1162
+ ad_user_data: categories.marketing ? 'granted' : 'denied',
1163
+ ad_personalization: categories.marketing ? 'granted' : 'denied',
1164
+ analytics_storage: categories.analytics ? 'granted' : 'denied',
1165
+ functionality_storage: categories.preferences ? 'granted' : 'denied',
1166
+ personalization_storage: categories.preferences ? 'granted' : 'denied',
1167
+ security_storage: 'granted', // Always granted
1168
+ };
1169
+ }
1170
+ }
1171
+
1172
+ /**
1173
+ * DataLayerManager - Manages Google Tag Manager dataLayer communication
1174
+ * Implements Google Consent Mode v2 correctly via gtag() API
1175
+ */
1176
+ class DataLayerManager {
1177
+ /**
1178
+ * Initialize gtag function if not already present
1179
+ * This must be called before GTM loads for consent defaults to work
1180
+ */
1181
+ ensureGtag() {
1182
+ window.dataLayer = window.dataLayer || [];
1183
+ if (typeof window.gtag !== 'function') {
1184
+ window.gtag = function () {
1185
+ window.dataLayer.push(arguments);
1186
+ };
1187
+ }
1188
+ }
1189
+ /**
1190
+ * Push consent command via gtag (correct format for Google Consent Mode v2)
1191
+ * Usage: pushConsent('default', {...}) or pushConsent('update', {...})
1192
+ */
1193
+ pushConsent(action, params) {
1194
+ this.ensureGtag();
1195
+ window.gtag('consent', action, params);
1196
+ }
1197
+ /**
1198
+ * Push a 'set' command via gtag for advanced features
1199
+ * Usage: pushSet('url_passthrough', true) or pushSet('ads_data_redaction', true)
1200
+ */
1201
+ pushSet(key, value) {
1202
+ this.ensureGtag();
1203
+ window.gtag('set', key, value);
1204
+ }
1205
+ }
1206
+
1207
+ /**
1208
+ * Cookie utilities for clearing non-essential cookies on rejection/withdrawal
1209
+ */
1210
+ /** Known analytics cookie name patterns */
1211
+ const ANALYTICS_COOKIE_PATTERNS = [
1212
+ /^_ga/, // Google Analytics
1213
+ /^_gid/, // Google Analytics
1214
+ /^_gat/, // Google Analytics
1215
+ /^_gcl/, // Google Ads conversion linker
1216
+ /^_hj/, // Hotjar
1217
+ /^_pk_/, // Matomo/Piwik
1218
+ /^mp_/, // Mixpanel
1219
+ /^ajs_/, // Segment
1220
+ /^amplitude/, // Amplitude
1221
+ /^plausible/, // Plausible
1222
+ ];
1223
+ /** Known marketing cookie name patterns */
1224
+ const MARKETING_COOKIE_PATTERNS = [
1225
+ /^_fbp/, // Facebook Pixel
1226
+ /^_fbc/, // Facebook click
1227
+ /^fr$/, // Facebook
1228
+ /^_pin_/, // Pinterest
1229
+ /^_tt_/, // TikTok
1230
+ /^li_/, // LinkedIn
1231
+ /^_uet/, // Microsoft/Bing Ads
1232
+ /^IDE$/, // DoubleClick
1233
+ /^test_cookie/, // DoubleClick
1234
+ /^MUID$/, // Microsoft
1235
+ /^NID$/, // Google Ads
1236
+ ];
1237
+ /** Known preferences cookie name patterns */
1238
+ const PREFERENCES_COOKIE_PATTERNS = [
1239
+ /^lang$/,
1240
+ /^locale$/,
1241
+ /^i18n/,
1242
+ ];
1243
+ const CATEGORY_PATTERNS = {
1244
+ analytics: ANALYTICS_COOKIE_PATTERNS,
1245
+ marketing: MARKETING_COOKIE_PATTERNS,
1246
+ preferences: PREFERENCES_COOKIE_PATTERNS,
1247
+ };
1248
+ /**
1249
+ * Get all cookies as name/value pairs
1250
+ */
1251
+ function getAllCookies() {
1252
+ return document.cookie.split(';').map(c => c.trim().split('=')[0]).filter(Boolean);
1253
+ }
1254
+ /**
1255
+ * Delete a cookie by name, trying all common path/domain combinations
1256
+ */
1257
+ function deleteCookie(name) {
1258
+ const hostname = window.location.hostname;
1259
+ const paths = ['/', window.location.pathname];
1260
+ // Build domain variants: current domain + parent domains
1261
+ const domains = ['', hostname];
1262
+ const parts = hostname.split('.');
1263
+ if (parts.length > 2) {
1264
+ // Add parent domain (e.g., .example.com for sub.example.com)
1265
+ domains.push('.' + parts.slice(-2).join('.'));
1266
+ }
1267
+ domains.push('.' + hostname);
1268
+ for (const domain of domains) {
1269
+ for (const path of paths) {
1270
+ const domainPart = domain ? `; domain=${domain}` : '';
1271
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}${domainPart}`;
1272
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}${domainPart}; secure`;
1273
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}${domainPart}; samesite=lax`;
1274
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}${domainPart}; samesite=none; secure`;
1275
+ }
1276
+ }
1277
+ }
1278
+ /**
1279
+ * Clear cookies matching patterns for denied categories
1280
+ */
1281
+ function clearCookiesForCategory(category) {
1282
+ const patterns = CATEGORY_PATTERNS[category];
1283
+ if (!patterns)
1284
+ return;
1285
+ const cookies = getAllCookies();
1286
+ for (const name of cookies) {
1287
+ if (patterns.some(pattern => pattern.test(name))) {
1288
+ deleteCookie(name);
1289
+ }
1290
+ }
1291
+ }
1292
+ /**
1293
+ * Clear all non-essential cookies based on consent categories
1294
+ */
1295
+ function clearDeniedCookies(categories) {
1296
+ for (const [category, allowed] of Object.entries(categories)) {
1297
+ if (!allowed && category !== 'necessary') {
1298
+ clearCookiesForCategory(category);
1299
+ }
1300
+ }
1301
+ }
1302
+
1303
+ /**
1304
+ * CookieConsent - Main orchestrator class
1305
+ */
1306
+ class CookieConsent {
1307
+ constructor(config) {
1308
+ this.banner = null;
1309
+ this.preferenceCenter = null;
1310
+ this.floatingWidget = null;
1311
+ this.gtmIntegration = null;
1312
+ this.config = this.validateConfig(config);
1313
+ this.consentManager = new ConsentManager(this.config);
1314
+ this.storageManager = new StorageManager();
1315
+ this.eventEmitter = new EventEmitter();
1316
+ this.scriptBlocker = new ScriptBlocker(new CategoryManager(), this.eventEmitter);
1317
+ if (this.config.gtmConsentMode) {
1318
+ this.gtmIntegration = new GTMConsentMode(new DataLayerManager(), this.config);
1319
+ }
1320
+ // Listen for preference center requests
1321
+ this.eventEmitter.on('preferences:show', () => {
1322
+ this.showPreferences();
1323
+ });
1324
+ // Listen for consent updates
1325
+ this.eventEmitter.on('consent:accept', (categories) => {
1326
+ this.updateConsent(categories);
1327
+ });
1328
+ this.eventEmitter.on('consent:reject', (categories) => {
1329
+ this.updateConsent(categories);
1330
+ });
1331
+ this.eventEmitter.on('consent:update', (categories) => {
1332
+ this.updateConsent(categories);
1333
+ });
1334
+ }
1335
+ /**
1336
+ * Initialize the cookie consent system
1337
+ */
1338
+ init() {
1339
+ // 1. Start blocking scripts immediately
1340
+ this.scriptBlocker.init();
1341
+ // 2. Set GTM default consent BEFORE checking storage
1342
+ if (this.gtmIntegration) {
1343
+ this.gtmIntegration.setDefaultConsent();
1344
+ }
1345
+ // 3. Check for existing consent
1346
+ const storedConsent = this.storageManager.load();
1347
+ if (storedConsent && !this.storageManager.isExpired(storedConsent)) {
1348
+ // Valid consent exists
1349
+ if (this.consentManager.needsUpdate(storedConsent)) {
1350
+ // Policy updated, show banner again
1351
+ if (this.config.autoShow) {
1352
+ this.showBanner();
1353
+ }
1354
+ }
1355
+ else {
1356
+ // Apply stored consent
1357
+ this.applyConsent(storedConsent.categories);
1358
+ // Restore GTM consent for returning visitors (within wait_for_update window)
1359
+ if (this.gtmIntegration) {
1360
+ this.gtmIntegration.updateConsent(storedConsent.categories);
1361
+ }
1362
+ this.eventEmitter.emit('consent:load', storedConsent);
1363
+ // Show floating widget if enabled
1364
+ if (this.config.showWidget) {
1365
+ this.showFloatingWidget();
1366
+ }
1367
+ }
1368
+ }
1369
+ else {
1370
+ // No consent or expired
1371
+ if (this.config.autoShow) {
1372
+ this.showBanner();
1373
+ }
1374
+ }
1375
+ this.eventEmitter.emit('consent:init');
1376
+ }
1377
+ /**
1378
+ * Show the banner
1379
+ */
1380
+ show() {
1381
+ this.showBanner();
1382
+ }
1383
+ /**
1384
+ * Hide the banner
1385
+ */
1386
+ hide() {
1387
+ var _a;
1388
+ (_a = this.banner) === null || _a === void 0 ? void 0 : _a.hide();
1389
+ }
1390
+ /**
1391
+ * Show preferences modal
1392
+ */
1393
+ showPreferences() {
1394
+ var _a;
1395
+ const stored = (_a = this.storageManager.load()) === null || _a === void 0 ? void 0 : _a.categories;
1396
+ // Default to all ON when no prior consent (user chose to customize)
1397
+ const currentConsent = stored || {
1398
+ necessary: true,
1399
+ analytics: true,
1400
+ marketing: true,
1401
+ preferences: true,
1402
+ };
1403
+ // Always recreate to get fresh state from storage
1404
+ if (this.preferenceCenter) {
1405
+ this.preferenceCenter.destroy();
1406
+ }
1407
+ this.preferenceCenter = new PreferenceCenter(this.config, this.eventEmitter, currentConsent);
1408
+ this.preferenceCenter.show();
1409
+ }
1410
+ /**
1411
+ * Update consent with new categories
1412
+ */
1413
+ updateConsent(categories) {
1414
+ const consentRecord = this.consentManager.updateConsent(categories);
1415
+ this.storageManager.save(consentRecord);
1416
+ this.applyConsent(categories);
1417
+ if (this.gtmIntegration) {
1418
+ this.gtmIntegration.updateConsent(categories);
1419
+ }
1420
+ // Show floating widget after consent is given (delay to let banner hide)
1421
+ if (this.config.showWidget) {
1422
+ setTimeout(() => {
1423
+ this.showFloatingWidget();
1424
+ }, 400); // Wait for banner hide animation
1425
+ }
1426
+ }
1427
+ /**
1428
+ * Get current consent
1429
+ */
1430
+ getConsent() {
1431
+ return this.storageManager.load();
1432
+ }
1433
+ /**
1434
+ * Reset consent (clear stored data and show banner)
1435
+ */
1436
+ reset() {
1437
+ this.storageManager.clear();
1438
+ this.scriptBlocker.block();
1439
+ // Clear all non-essential cookies on reset
1440
+ clearDeniedCookies({ necessary: true, analytics: false, marketing: false, preferences: false });
1441
+ // Update GTM to denied state
1442
+ if (this.gtmIntegration) {
1443
+ this.gtmIntegration.updateConsent({ necessary: true, analytics: false, marketing: false });
1444
+ }
1445
+ this.showBanner();
1446
+ }
1447
+ /**
1448
+ * Register event handler
1449
+ */
1450
+ on(event, callback) {
1451
+ this.eventEmitter.on(event, callback);
1452
+ }
1453
+ /**
1454
+ * Unregister event handler
1455
+ */
1456
+ off(event, callback) {
1457
+ this.eventEmitter.off(event, callback);
1458
+ }
1459
+ /**
1460
+ * Destroy and cleanup all UI elements
1461
+ */
1462
+ destroy() {
1463
+ var _a, _b, _c, _d;
1464
+ (_a = this.banner) === null || _a === void 0 ? void 0 : _a.destroy();
1465
+ this.banner = null;
1466
+ (_b = this.preferenceCenter) === null || _b === void 0 ? void 0 : _b.destroy();
1467
+ this.preferenceCenter = null;
1468
+ (_c = this.floatingWidget) === null || _c === void 0 ? void 0 : _c.destroy();
1469
+ this.floatingWidget = null;
1470
+ (_d = this.scriptBlocker) === null || _d === void 0 ? void 0 : _d.destroy();
1471
+ }
1472
+ /**
1473
+ * Show the banner
1474
+ */
1475
+ showBanner() {
1476
+ if (!this.banner) {
1477
+ this.banner = new Banner(this.config, this.eventEmitter);
1478
+ }
1479
+ this.banner.show();
1480
+ this.eventEmitter.emit('consent:show');
1481
+ }
1482
+ /**
1483
+ * Show the floating widget
1484
+ */
1485
+ showFloatingWidget() {
1486
+ if (!this.floatingWidget) {
1487
+ this.floatingWidget = new FloatingWidget(this.config, this.eventEmitter);
1488
+ }
1489
+ this.floatingWidget.show();
1490
+ }
1491
+ /**
1492
+ * Apply consent by unblocking allowed scripts and clearing denied cookies
1493
+ */
1494
+ applyConsent(categories) {
1495
+ this.scriptBlocker.unblock(categories);
1496
+ // CNIL/GDPR: actively delete cookies for denied categories
1497
+ clearDeniedCookies(categories);
1498
+ }
1499
+ /**
1500
+ * Validate and set default config values
1501
+ */
1502
+ validateConfig(config) {
1503
+ return Object.assign(Object.assign({}, config), { 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', layout: config.layout || 'bar', backdropBlur: config.backdropBlur !== false, animationStyle: config.animationStyle || 'smooth', preferencesPosition: config.preferencesPosition || 'center', showWidget: config.showWidget !== undefined ? config.showWidget : true, widgetPosition: config.widgetPosition || 'bottom-right', widgetStyle: config.widgetStyle || 'full' });
1504
+ }
1505
+ }
1506
+
1507
+ export { CookieConsent };
1508
+ //# sourceMappingURL=cookiecraft.esm.js.map