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.
- package/LICENSE +21 -0
- package/README.md +418 -0
- package/dist/cookiecraft.css +1 -0
- package/dist/cookiecraft.esm.js +1508 -0
- package/dist/cookiecraft.esm.js.map +1 -0
- package/dist/cookiecraft.js +1516 -0
- package/dist/cookiecraft.js.map +1 -0
- package/dist/cookiecraft.min.js +2 -0
- package/dist/cookiecraft.min.js.map +1 -0
- package/dist/stats.html +4949 -0
- package/dist/types/blocking/CategoryManager.d.ts +24 -0
- package/dist/types/blocking/ScriptBlocker.d.ts +54 -0
- package/dist/types/core/ConsentManager.d.ts +33 -0
- package/dist/types/core/CookieConsent.d.ts +76 -0
- package/dist/types/core/EventEmitter.d.ts +26 -0
- package/dist/types/core/StorageManager.d.ts +33 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/integrations/DataLayerManager.d.ts +21 -0
- package/dist/types/integrations/GTMConsentMode.d.ts +29 -0
- package/dist/types/types/index.d.ts +90 -0
- package/dist/types/ui/Banner.d.ts +47 -0
- package/dist/types/ui/FloatingWidget.d.ts +41 -0
- package/dist/types/ui/PreferenceCenter.d.ts +52 -0
- package/dist/types/utils/cookies.d.ts +11 -0
- package/dist/types/utils/sanitize.d.ts +19 -0
- package/package.json +86 -0
|
@@ -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
|
+
'&': '&',
|
|
469
|
+
'<': '<',
|
|
470
|
+
'>': '>',
|
|
471
|
+
'"': '"',
|
|
472
|
+
"'": ''',
|
|
473
|
+
'`': '`',
|
|
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
|