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