@freshjuice/zest 0.1.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 +312 -0
- package/dist/zest.de.js +2621 -0
- package/dist/zest.de.js.map +1 -0
- package/dist/zest.de.min.js +1 -0
- package/dist/zest.en.js +2621 -0
- package/dist/zest.en.js.map +1 -0
- package/dist/zest.en.min.js +1 -0
- package/dist/zest.es.js +2621 -0
- package/dist/zest.es.js.map +1 -0
- package/dist/zest.es.min.js +1 -0
- package/dist/zest.esm.js +3104 -0
- package/dist/zest.esm.js.map +1 -0
- package/dist/zest.esm.min.js +1 -0
- package/dist/zest.fr.js +2621 -0
- package/dist/zest.fr.js.map +1 -0
- package/dist/zest.fr.min.js +1 -0
- package/dist/zest.it.js +2621 -0
- package/dist/zest.it.js.map +1 -0
- package/dist/zest.it.min.js +1 -0
- package/dist/zest.ja.js +2621 -0
- package/dist/zest.ja.js.map +1 -0
- package/dist/zest.ja.min.js +1 -0
- package/dist/zest.js +3109 -0
- package/dist/zest.js.map +1 -0
- package/dist/zest.min.js +1 -0
- package/dist/zest.nl.js +2621 -0
- package/dist/zest.nl.js.map +1 -0
- package/dist/zest.nl.min.js +1 -0
- package/dist/zest.pl.js +2621 -0
- package/dist/zest.pl.js.map +1 -0
- package/dist/zest.pl.min.js +1 -0
- package/dist/zest.pt.js +2621 -0
- package/dist/zest.pt.js.map +1 -0
- package/dist/zest.pt.min.js +1 -0
- package/dist/zest.ru.js +2621 -0
- package/dist/zest.ru.js.map +1 -0
- package/dist/zest.ru.min.js +1 -0
- package/dist/zest.uk.js +2621 -0
- package/dist/zest.uk.js.map +1 -0
- package/dist/zest.uk.min.js +1 -0
- package/dist/zest.zh.js +2621 -0
- package/dist/zest.zh.js.map +1 -0
- package/dist/zest.zh.min.js +1 -0
- package/locales/de.json +40 -0
- package/locales/en.json +40 -0
- package/locales/es.json +40 -0
- package/locales/fr.json +40 -0
- package/locales/it.json +40 -0
- package/locales/ja.json +40 -0
- package/locales/nl.json +40 -0
- package/locales/pl.json +40 -0
- package/locales/pt.json +40 -0
- package/locales/ru.json +40 -0
- package/locales/uk.json +40 -0
- package/locales/zh.json +40 -0
- package/package.json +63 -0
- package/zest.config.schema.json +256 -0
package/dist/zest.de.js
ADDED
|
@@ -0,0 +1,2621 @@
|
|
|
1
|
+
var Zest = (function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pattern Matcher - Categorizes cookies and storage keys by pattern
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default patterns for each category
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_PATTERNS = {
|
|
12
|
+
essential: [
|
|
13
|
+
/^zest_/,
|
|
14
|
+
/^csrf/i,
|
|
15
|
+
/^xsrf/i,
|
|
16
|
+
/^session/i,
|
|
17
|
+
/^__host-/i,
|
|
18
|
+
/^__secure-/i
|
|
19
|
+
],
|
|
20
|
+
functional: [
|
|
21
|
+
/^lang/i,
|
|
22
|
+
/^locale/i,
|
|
23
|
+
/^theme/i,
|
|
24
|
+
/^preferences/i,
|
|
25
|
+
/^ui_/i
|
|
26
|
+
],
|
|
27
|
+
analytics: [
|
|
28
|
+
/^_ga/,
|
|
29
|
+
/^_gid/,
|
|
30
|
+
/^_gat/,
|
|
31
|
+
/^_utm/,
|
|
32
|
+
/^__utm/,
|
|
33
|
+
/^plausible/i,
|
|
34
|
+
/^_pk_/,
|
|
35
|
+
/^matomo/i,
|
|
36
|
+
/^_hj/,
|
|
37
|
+
/^ajs_/
|
|
38
|
+
],
|
|
39
|
+
marketing: [
|
|
40
|
+
/^_fbp/,
|
|
41
|
+
/^_fbc/,
|
|
42
|
+
/^_gcl/,
|
|
43
|
+
/^_ttp/,
|
|
44
|
+
/^ads/i,
|
|
45
|
+
/^doubleclick/i,
|
|
46
|
+
/^__gads/,
|
|
47
|
+
/^__gpi/,
|
|
48
|
+
/^_pin_/,
|
|
49
|
+
/^li_/
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let patterns = { ...DEFAULT_PATTERNS };
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Set custom patterns
|
|
57
|
+
*/
|
|
58
|
+
function setPatterns(customPatterns) {
|
|
59
|
+
patterns = { ...DEFAULT_PATTERNS };
|
|
60
|
+
for (const [category, regexList] of Object.entries(customPatterns)) {
|
|
61
|
+
if (Array.isArray(regexList)) {
|
|
62
|
+
patterns[category] = regexList.map(p =>
|
|
63
|
+
p instanceof RegExp ? p : new RegExp(p)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Determine category for a cookie/storage key name
|
|
71
|
+
* @param {string} name - Cookie or storage key name
|
|
72
|
+
* @returns {string} Category ID (defaults to 'marketing' for unknown)
|
|
73
|
+
*/
|
|
74
|
+
function getCategoryForName(name) {
|
|
75
|
+
for (const [category, regexList] of Object.entries(patterns)) {
|
|
76
|
+
if (regexList.some(regex => regex.test(name))) {
|
|
77
|
+
return category;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Unknown items default to marketing (strictest)
|
|
81
|
+
return 'marketing';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse cookie string to extract name
|
|
86
|
+
* @param {string} cookieString - Full cookie string (e.g., "name=value; path=/")
|
|
87
|
+
* @returns {string|null} Cookie name or null
|
|
88
|
+
*/
|
|
89
|
+
function parseCookieName(cookieString) {
|
|
90
|
+
const match = cookieString.match(/^([^=]+)/);
|
|
91
|
+
return match ? match[1].trim() : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Cookie Interceptor - Intercepts document.cookie operations
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// Store original descriptor
|
|
100
|
+
let originalCookieDescriptor = null;
|
|
101
|
+
|
|
102
|
+
// Queue for blocked cookies
|
|
103
|
+
const cookieQueue = [];
|
|
104
|
+
|
|
105
|
+
// Reference to consent checker function
|
|
106
|
+
let checkConsent$3 = () => false;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Set the consent checker function
|
|
110
|
+
*/
|
|
111
|
+
function setConsentChecker$2(fn) {
|
|
112
|
+
checkConsent$3 = fn;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the original cookie descriptor
|
|
117
|
+
*/
|
|
118
|
+
function getOriginalCookieDescriptor() {
|
|
119
|
+
return originalCookieDescriptor;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Replay queued cookies for allowed categories
|
|
124
|
+
*/
|
|
125
|
+
function replayCookies(allowedCategories) {
|
|
126
|
+
const remaining = [];
|
|
127
|
+
|
|
128
|
+
for (const item of cookieQueue) {
|
|
129
|
+
if (allowedCategories.includes(item.category)) {
|
|
130
|
+
// Set the cookie using original setter
|
|
131
|
+
if (originalCookieDescriptor?.set) {
|
|
132
|
+
originalCookieDescriptor.set.call(document, item.value);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
remaining.push(item);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
cookieQueue.length = 0;
|
|
140
|
+
cookieQueue.push(...remaining);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Start intercepting cookies
|
|
145
|
+
*/
|
|
146
|
+
function interceptCookies() {
|
|
147
|
+
// Store original
|
|
148
|
+
originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
|
|
149
|
+
|
|
150
|
+
if (!originalCookieDescriptor) {
|
|
151
|
+
console.warn('[Zest] Could not get cookie descriptor');
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Object.defineProperty(document, 'cookie', {
|
|
156
|
+
get() {
|
|
157
|
+
// Always allow reading
|
|
158
|
+
return originalCookieDescriptor.get.call(document);
|
|
159
|
+
},
|
|
160
|
+
set(value) {
|
|
161
|
+
const name = parseCookieName(value);
|
|
162
|
+
if (!name) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const category = getCategoryForName(name);
|
|
167
|
+
|
|
168
|
+
if (checkConsent$3(category)) {
|
|
169
|
+
// Consent given - set cookie
|
|
170
|
+
originalCookieDescriptor.set.call(document, value);
|
|
171
|
+
} else {
|
|
172
|
+
// No consent - queue for later
|
|
173
|
+
cookieQueue.push({
|
|
174
|
+
value,
|
|
175
|
+
name,
|
|
176
|
+
category,
|
|
177
|
+
timestamp: Date.now()
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
configurable: true
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Storage Interceptor - Intercepts localStorage and sessionStorage operations
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
// Store originals
|
|
193
|
+
let originalLocalStorage = null;
|
|
194
|
+
let originalSessionStorage = null;
|
|
195
|
+
|
|
196
|
+
// Queues for blocked operations
|
|
197
|
+
const localStorageQueue = [];
|
|
198
|
+
const sessionStorageQueue = [];
|
|
199
|
+
|
|
200
|
+
// Reference to consent checker function
|
|
201
|
+
let checkConsent$2 = () => false;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Set the consent checker function
|
|
205
|
+
*/
|
|
206
|
+
function setConsentChecker$1(fn) {
|
|
207
|
+
checkConsent$2 = fn;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a proxy for storage API
|
|
212
|
+
*/
|
|
213
|
+
function createStorageProxy(storage, queue, storageName) {
|
|
214
|
+
return new Proxy(storage, {
|
|
215
|
+
get(target, prop) {
|
|
216
|
+
if (prop === 'setItem') {
|
|
217
|
+
return (key, value) => {
|
|
218
|
+
const category = getCategoryForName(key);
|
|
219
|
+
|
|
220
|
+
if (checkConsent$2(category)) {
|
|
221
|
+
target.setItem(key, value);
|
|
222
|
+
} else {
|
|
223
|
+
queue.push({
|
|
224
|
+
key,
|
|
225
|
+
value,
|
|
226
|
+
category,
|
|
227
|
+
timestamp: Date.now()
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Allow all other operations
|
|
234
|
+
const val = target[prop];
|
|
235
|
+
return typeof val === 'function' ? val.bind(target) : val;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Replay queued storage operations for allowed categories
|
|
242
|
+
*/
|
|
243
|
+
function replayStorage(allowedCategories) {
|
|
244
|
+
// Replay localStorage
|
|
245
|
+
const remainingLocal = [];
|
|
246
|
+
for (const item of localStorageQueue) {
|
|
247
|
+
if (allowedCategories.includes(item.category)) {
|
|
248
|
+
originalLocalStorage?.setItem(item.key, item.value);
|
|
249
|
+
} else {
|
|
250
|
+
remainingLocal.push(item);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
localStorageQueue.length = 0;
|
|
254
|
+
localStorageQueue.push(...remainingLocal);
|
|
255
|
+
|
|
256
|
+
// Replay sessionStorage
|
|
257
|
+
const remainingSession = [];
|
|
258
|
+
for (const item of sessionStorageQueue) {
|
|
259
|
+
if (allowedCategories.includes(item.category)) {
|
|
260
|
+
originalSessionStorage?.setItem(item.key, item.value);
|
|
261
|
+
} else {
|
|
262
|
+
remainingSession.push(item);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
sessionStorageQueue.length = 0;
|
|
266
|
+
sessionStorageQueue.push(...remainingSession);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Start intercepting storage APIs
|
|
271
|
+
*/
|
|
272
|
+
function interceptStorage() {
|
|
273
|
+
try {
|
|
274
|
+
originalLocalStorage = window.localStorage;
|
|
275
|
+
originalSessionStorage = window.sessionStorage;
|
|
276
|
+
|
|
277
|
+
Object.defineProperty(window, 'localStorage', {
|
|
278
|
+
value: createStorageProxy(originalLocalStorage, localStorageQueue, 'localStorage'),
|
|
279
|
+
configurable: true,
|
|
280
|
+
writable: false
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
Object.defineProperty(window, 'sessionStorage', {
|
|
284
|
+
value: createStorageProxy(originalSessionStorage, sessionStorageQueue, 'sessionStorage'),
|
|
285
|
+
configurable: true,
|
|
286
|
+
writable: false
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return true;
|
|
290
|
+
} catch (e) {
|
|
291
|
+
console.warn('[Zest] Could not intercept storage APIs:', e);
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Known Trackers - Lists of known tracking script domains by category
|
|
298
|
+
*/
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Safe mode - Major, well-known trackers only
|
|
302
|
+
*/
|
|
303
|
+
const SAFE_TRACKERS = {
|
|
304
|
+
analytics: [
|
|
305
|
+
'google-analytics.com',
|
|
306
|
+
'www.google-analytics.com',
|
|
307
|
+
'analytics.google.com',
|
|
308
|
+
'googletagmanager.com',
|
|
309
|
+
'www.googletagmanager.com',
|
|
310
|
+
'plausible.io',
|
|
311
|
+
'cloudflareinsights.com',
|
|
312
|
+
'static.cloudflareinsights.com'
|
|
313
|
+
],
|
|
314
|
+
marketing: [
|
|
315
|
+
'connect.facebook.net',
|
|
316
|
+
'www.facebook.com/tr',
|
|
317
|
+
'ads.google.com',
|
|
318
|
+
'www.googleadservices.com',
|
|
319
|
+
'googleads.g.doubleclick.net',
|
|
320
|
+
'pagead2.googlesyndication.com'
|
|
321
|
+
]
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Strict mode - Extended list including less common trackers
|
|
326
|
+
*/
|
|
327
|
+
const STRICT_TRACKERS = {
|
|
328
|
+
analytics: [
|
|
329
|
+
...SAFE_TRACKERS.analytics,
|
|
330
|
+
'analytics.tiktok.com',
|
|
331
|
+
'matomo.', // partial match
|
|
332
|
+
'hotjar.com',
|
|
333
|
+
'static.hotjar.com',
|
|
334
|
+
'script.hotjar.com',
|
|
335
|
+
'clarity.ms',
|
|
336
|
+
'www.clarity.ms',
|
|
337
|
+
'heapanalytics.com',
|
|
338
|
+
'cdn.heapanalytics.com',
|
|
339
|
+
'mixpanel.com',
|
|
340
|
+
'cdn.mxpnl.com',
|
|
341
|
+
'segment.com',
|
|
342
|
+
'cdn.segment.com',
|
|
343
|
+
'api.segment.io',
|
|
344
|
+
'fullstory.com',
|
|
345
|
+
'rs.fullstory.com',
|
|
346
|
+
'amplitude.com',
|
|
347
|
+
'cdn.amplitude.com',
|
|
348
|
+
'mouseflow.com',
|
|
349
|
+
'cdn.mouseflow.com',
|
|
350
|
+
'luckyorange.com',
|
|
351
|
+
'cdn.luckyorange.net',
|
|
352
|
+
'crazyegg.com',
|
|
353
|
+
'script.crazyegg.com'
|
|
354
|
+
],
|
|
355
|
+
marketing: [
|
|
356
|
+
...SAFE_TRACKERS.marketing,
|
|
357
|
+
'snap.licdn.com',
|
|
358
|
+
'px.ads.linkedin.com',
|
|
359
|
+
'ads.linkedin.com',
|
|
360
|
+
'analytics.twitter.com',
|
|
361
|
+
'static.ads-twitter.com',
|
|
362
|
+
't.co',
|
|
363
|
+
'analytics.tiktok.com',
|
|
364
|
+
'ads.tiktok.com',
|
|
365
|
+
'sc-static.net', // Snapchat
|
|
366
|
+
'tr.snapchat.com',
|
|
367
|
+
'ct.pinterest.com',
|
|
368
|
+
'pintrk.com',
|
|
369
|
+
's.pinimg.com',
|
|
370
|
+
'widgets.pinterest.com',
|
|
371
|
+
'bat.bing.com',
|
|
372
|
+
'ads.yahoo.com',
|
|
373
|
+
'sp.analytics.yahoo.com',
|
|
374
|
+
'amazon-adsystem.com',
|
|
375
|
+
'z-na.amazon-adsystem.com',
|
|
376
|
+
'criteo.com',
|
|
377
|
+
'static.criteo.net',
|
|
378
|
+
'dis.criteo.com',
|
|
379
|
+
'taboola.com',
|
|
380
|
+
'cdn.taboola.com',
|
|
381
|
+
'trc.taboola.com',
|
|
382
|
+
'outbrain.com',
|
|
383
|
+
'widgets.outbrain.com',
|
|
384
|
+
'adroll.com',
|
|
385
|
+
's.adroll.com'
|
|
386
|
+
],
|
|
387
|
+
functional: [
|
|
388
|
+
'cdn.onesignal.com',
|
|
389
|
+
'onesignal.com',
|
|
390
|
+
'pusher.com',
|
|
391
|
+
'js.pusher.com',
|
|
392
|
+
'intercom.io',
|
|
393
|
+
'widget.intercom.io',
|
|
394
|
+
'js.intercomcdn.com',
|
|
395
|
+
'crisp.chat',
|
|
396
|
+
'client.crisp.chat',
|
|
397
|
+
'cdn.livechatinc.com',
|
|
398
|
+
'livechatinc.com',
|
|
399
|
+
'tawk.to',
|
|
400
|
+
'embed.tawk.to',
|
|
401
|
+
'zendesk.com',
|
|
402
|
+
'static.zdassets.com'
|
|
403
|
+
]
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Check if a URL matches any tracker in the list
|
|
408
|
+
*/
|
|
409
|
+
function matchesTrackerList(url, trackerList) {
|
|
410
|
+
try {
|
|
411
|
+
const urlObj = new URL(url);
|
|
412
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
413
|
+
const fullUrl = url.toLowerCase();
|
|
414
|
+
|
|
415
|
+
for (const domain of trackerList) {
|
|
416
|
+
// Support partial matches (e.g., "matomo." matches "analytics.matomo.cloud")
|
|
417
|
+
if (domain.endsWith('.')) {
|
|
418
|
+
if (hostname.includes(domain.slice(0, -1))) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
} else if (hostname === domain || hostname.endsWith('.' + domain)) {
|
|
422
|
+
return true;
|
|
423
|
+
} else if (fullUrl.includes(domain)) {
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch (e) {
|
|
428
|
+
// Invalid URL
|
|
429
|
+
}
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Get category for a script URL based on tracker lists
|
|
435
|
+
*/
|
|
436
|
+
function getCategoryForScript(url, mode = 'safe') {
|
|
437
|
+
const trackers = mode === 'strict' ? STRICT_TRACKERS : SAFE_TRACKERS;
|
|
438
|
+
|
|
439
|
+
for (const [category, domains] of Object.entries(trackers)) {
|
|
440
|
+
if (matchesTrackerList(url, domains)) {
|
|
441
|
+
return category;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Check if URL is third-party (different domain)
|
|
450
|
+
*/
|
|
451
|
+
function isThirdParty(url) {
|
|
452
|
+
try {
|
|
453
|
+
const scriptHost = new URL(url).hostname;
|
|
454
|
+
const pageHost = window.location.hostname;
|
|
455
|
+
|
|
456
|
+
// Remove www. for comparison
|
|
457
|
+
const normalizeHost = (h) => h.replace(/^www\./, '');
|
|
458
|
+
|
|
459
|
+
return normalizeHost(scriptHost) !== normalizeHost(pageHost);
|
|
460
|
+
} catch (e) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Script Blocker - Blocks and manages consent-gated scripts
|
|
467
|
+
*
|
|
468
|
+
* Modes:
|
|
469
|
+
* - manual: Only blocks scripts with data-consent-category attribute
|
|
470
|
+
* - safe: Manual + known major trackers (Google, Facebook, etc.)
|
|
471
|
+
* - strict: Safe + extended tracker list (Hotjar, Mixpanel, etc.)
|
|
472
|
+
* - doomsday: Block ALL third-party scripts
|
|
473
|
+
*/
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
// Queue for blocked scripts
|
|
477
|
+
const scriptQueue = [];
|
|
478
|
+
|
|
479
|
+
// MutationObserver instance
|
|
480
|
+
let observer = null;
|
|
481
|
+
|
|
482
|
+
// Current blocking mode
|
|
483
|
+
let blockingMode = 'safe';
|
|
484
|
+
|
|
485
|
+
// Custom blocked domains (user-defined)
|
|
486
|
+
let customBlockedDomains = [];
|
|
487
|
+
|
|
488
|
+
// Reference to consent checker function
|
|
489
|
+
let checkConsent$1 = () => false;
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Set the consent checker function
|
|
493
|
+
*/
|
|
494
|
+
function setConsentChecker(fn) {
|
|
495
|
+
checkConsent$1 = fn;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Check if script URL matches custom blocked domains
|
|
500
|
+
*/
|
|
501
|
+
function matchesCustomDomains(url) {
|
|
502
|
+
if (!url || customBlockedDomains.length === 0) return null;
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
506
|
+
|
|
507
|
+
for (const entry of customBlockedDomains) {
|
|
508
|
+
const domain = typeof entry === 'string' ? entry : entry.domain;
|
|
509
|
+
const category = typeof entry === 'string' ? 'marketing' : (entry.category || 'marketing');
|
|
510
|
+
|
|
511
|
+
if (hostname === domain || hostname.endsWith('.' + domain)) {
|
|
512
|
+
return category;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} catch (e) {
|
|
516
|
+
// Invalid URL
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Determine if a script should be blocked and get its category
|
|
524
|
+
*/
|
|
525
|
+
function getScriptBlockCategory(script) {
|
|
526
|
+
// 1. Check for explicit data-consent-category attribute (always respected)
|
|
527
|
+
const explicitCategory = script.getAttribute('data-consent-category');
|
|
528
|
+
if (explicitCategory) {
|
|
529
|
+
return explicitCategory;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// 2. Skip if script has data-zest-allow attribute
|
|
533
|
+
if (script.hasAttribute('data-zest-allow')) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const src = script.src;
|
|
538
|
+
|
|
539
|
+
// No src = inline script, only block if explicitly tagged
|
|
540
|
+
if (!src) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 3. Check custom blocked domains
|
|
545
|
+
const customCategory = matchesCustomDomains(src);
|
|
546
|
+
if (customCategory) {
|
|
547
|
+
return customCategory;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 4. Mode-based blocking
|
|
551
|
+
switch (blockingMode) {
|
|
552
|
+
case 'manual':
|
|
553
|
+
// Only explicit tags, already checked above
|
|
554
|
+
return null;
|
|
555
|
+
|
|
556
|
+
case 'safe':
|
|
557
|
+
case 'strict':
|
|
558
|
+
// Check against known tracker lists
|
|
559
|
+
return getCategoryForScript(src, blockingMode);
|
|
560
|
+
|
|
561
|
+
case 'doomsday':
|
|
562
|
+
// Block all third-party scripts
|
|
563
|
+
if (isThirdParty(src)) {
|
|
564
|
+
// Try to categorize, default to marketing
|
|
565
|
+
return getCategoryForScript(src, 'strict') || 'marketing';
|
|
566
|
+
}
|
|
567
|
+
return null;
|
|
568
|
+
|
|
569
|
+
default:
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Block a script element
|
|
576
|
+
*/
|
|
577
|
+
function blockScript(script) {
|
|
578
|
+
// Skip already processed scripts
|
|
579
|
+
if (script.hasAttribute('data-zest-processed')) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const category = getScriptBlockCategory(script);
|
|
584
|
+
|
|
585
|
+
if (!category) {
|
|
586
|
+
script.setAttribute('data-zest-processed', 'allowed');
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (checkConsent$1(category)) {
|
|
591
|
+
// Consent already given - allow script
|
|
592
|
+
script.setAttribute('data-zest-processed', 'allowed');
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Store script info for later execution
|
|
597
|
+
const scriptInfo = {
|
|
598
|
+
category,
|
|
599
|
+
src: script.src,
|
|
600
|
+
inline: script.textContent,
|
|
601
|
+
type: script.type,
|
|
602
|
+
async: script.async,
|
|
603
|
+
defer: script.defer,
|
|
604
|
+
timestamp: Date.now()
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// Mark as processed
|
|
608
|
+
script.setAttribute('data-zest-processed', 'blocked');
|
|
609
|
+
script.setAttribute('data-consent-category', category);
|
|
610
|
+
|
|
611
|
+
// Disable the script
|
|
612
|
+
script.type = 'text/plain';
|
|
613
|
+
|
|
614
|
+
// If it has a src, also remove it to prevent loading
|
|
615
|
+
if (script.src) {
|
|
616
|
+
script.setAttribute('data-blocked-src', script.src);
|
|
617
|
+
script.removeAttribute('src');
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
scriptQueue.push(scriptInfo);
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Execute a queued script
|
|
626
|
+
*/
|
|
627
|
+
function executeScript(scriptInfo) {
|
|
628
|
+
const script = document.createElement('script');
|
|
629
|
+
|
|
630
|
+
if (scriptInfo.src) {
|
|
631
|
+
script.src = scriptInfo.src;
|
|
632
|
+
} else if (scriptInfo.inline) {
|
|
633
|
+
script.textContent = scriptInfo.inline;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (scriptInfo.async) script.async = true;
|
|
637
|
+
if (scriptInfo.defer) script.defer = true;
|
|
638
|
+
|
|
639
|
+
script.setAttribute('data-zest-processed', 'executed');
|
|
640
|
+
script.setAttribute('data-consent-executed', 'true');
|
|
641
|
+
|
|
642
|
+
document.head.appendChild(script);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Replay queued scripts for allowed categories
|
|
647
|
+
*/
|
|
648
|
+
function replayScripts(allowedCategories) {
|
|
649
|
+
const remaining = [];
|
|
650
|
+
|
|
651
|
+
for (const scriptInfo of scriptQueue) {
|
|
652
|
+
if (allowedCategories.includes(scriptInfo.category)) {
|
|
653
|
+
executeScript(scriptInfo);
|
|
654
|
+
} else {
|
|
655
|
+
remaining.push(scriptInfo);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
scriptQueue.length = 0;
|
|
660
|
+
scriptQueue.push(...remaining);
|
|
661
|
+
|
|
662
|
+
// Also re-enable any blocked scripts in the DOM
|
|
663
|
+
const blockedScripts = document.querySelectorAll('script[data-zest-processed="blocked"]');
|
|
664
|
+
blockedScripts.forEach(script => {
|
|
665
|
+
const category = script.getAttribute('data-consent-category');
|
|
666
|
+
if (allowedCategories.includes(category)) {
|
|
667
|
+
// Clone and replace to execute
|
|
668
|
+
const newScript = document.createElement('script');
|
|
669
|
+
|
|
670
|
+
const blockedSrc = script.getAttribute('data-blocked-src');
|
|
671
|
+
if (blockedSrc) {
|
|
672
|
+
newScript.src = blockedSrc;
|
|
673
|
+
} else {
|
|
674
|
+
newScript.textContent = script.textContent;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (script.async) newScript.async = true;
|
|
678
|
+
if (script.defer) newScript.defer = true;
|
|
679
|
+
|
|
680
|
+
newScript.setAttribute('data-zest-processed', 'executed');
|
|
681
|
+
newScript.setAttribute('data-consent-executed', 'true');
|
|
682
|
+
script.parentNode?.replaceChild(newScript, script);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Process existing scripts in the DOM
|
|
689
|
+
*/
|
|
690
|
+
function processExistingScripts() {
|
|
691
|
+
const scripts = document.querySelectorAll('script:not([data-zest-processed])');
|
|
692
|
+
scripts.forEach(blockScript);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Handle mutations (new scripts added to DOM)
|
|
697
|
+
*/
|
|
698
|
+
function handleMutations(mutations) {
|
|
699
|
+
for (const mutation of mutations) {
|
|
700
|
+
for (const node of mutation.addedNodes) {
|
|
701
|
+
if (node.nodeName === 'SCRIPT' && !node.hasAttribute('data-zest-processed')) {
|
|
702
|
+
blockScript(node);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Check child scripts
|
|
706
|
+
if (node.querySelectorAll) {
|
|
707
|
+
const scripts = node.querySelectorAll('script:not([data-zest-processed])');
|
|
708
|
+
scripts.forEach(blockScript);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Start observing for new scripts
|
|
716
|
+
*/
|
|
717
|
+
function startScriptBlocking(mode = 'safe', customDomains = []) {
|
|
718
|
+
blockingMode = mode;
|
|
719
|
+
customBlockedDomains = customDomains;
|
|
720
|
+
|
|
721
|
+
// Process existing scripts
|
|
722
|
+
processExistingScripts();
|
|
723
|
+
|
|
724
|
+
// Watch for new scripts
|
|
725
|
+
observer = new MutationObserver(handleMutations);
|
|
726
|
+
|
|
727
|
+
observer.observe(document.documentElement, {
|
|
728
|
+
childList: true,
|
|
729
|
+
subtree: true
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Default consent categories
|
|
737
|
+
*/
|
|
738
|
+
const DEFAULT_CATEGORIES = {
|
|
739
|
+
essential: {
|
|
740
|
+
id: 'essential',
|
|
741
|
+
label: 'Essential',
|
|
742
|
+
description: 'Required for the website to function properly. Cannot be disabled.',
|
|
743
|
+
required: true,
|
|
744
|
+
default: true
|
|
745
|
+
},
|
|
746
|
+
functional: {
|
|
747
|
+
id: 'functional',
|
|
748
|
+
label: 'Functional',
|
|
749
|
+
description: 'Enable personalized features like language preferences and themes.',
|
|
750
|
+
required: false,
|
|
751
|
+
default: false
|
|
752
|
+
},
|
|
753
|
+
analytics: {
|
|
754
|
+
id: 'analytics',
|
|
755
|
+
label: 'Analytics',
|
|
756
|
+
description: 'Help us understand how visitors interact with our website.',
|
|
757
|
+
required: false,
|
|
758
|
+
default: false
|
|
759
|
+
},
|
|
760
|
+
marketing: {
|
|
761
|
+
id: 'marketing',
|
|
762
|
+
label: 'Marketing',
|
|
763
|
+
description: 'Used to deliver relevant advertisements and track campaign performance.',
|
|
764
|
+
required: false,
|
|
765
|
+
default: false
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Default consent state
|
|
771
|
+
*/
|
|
772
|
+
function getDefaultConsent() {
|
|
773
|
+
return {
|
|
774
|
+
essential: true,
|
|
775
|
+
functional: false,
|
|
776
|
+
analytics: false,
|
|
777
|
+
marketing: false
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Get all category IDs
|
|
783
|
+
*/
|
|
784
|
+
function getCategoryIds() {
|
|
785
|
+
return Object.keys(DEFAULT_CATEGORIES);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Do Not Track (DNT) Detection
|
|
790
|
+
*
|
|
791
|
+
* Detects browser DNT/GPC signals for privacy compliance
|
|
792
|
+
*/
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Check if Do Not Track is enabled
|
|
796
|
+
* Checks both DNT header and Global Privacy Control (GPC)
|
|
797
|
+
*/
|
|
798
|
+
function isDoNotTrackEnabled() {
|
|
799
|
+
if (typeof navigator === 'undefined') {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Check DNT (Do Not Track)
|
|
804
|
+
// Values: "1" = enabled, "0" = disabled, null/undefined = not set
|
|
805
|
+
const dnt = navigator.doNotTrack ||
|
|
806
|
+
window.doNotTrack ||
|
|
807
|
+
navigator.msDoNotTrack;
|
|
808
|
+
|
|
809
|
+
if (dnt === '1' || dnt === 'yes' || dnt === true) {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Check GPC (Global Privacy Control) - newer standard
|
|
814
|
+
// https://globalprivacycontrol.org/
|
|
815
|
+
if (navigator.globalPrivacyControl === true) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Get DNT signal details for logging/debugging
|
|
824
|
+
*/
|
|
825
|
+
function getDNTDetails() {
|
|
826
|
+
if (typeof navigator === 'undefined') {
|
|
827
|
+
return { enabled: false, source: null };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const dnt = navigator.doNotTrack ||
|
|
831
|
+
window.doNotTrack ||
|
|
832
|
+
navigator.msDoNotTrack;
|
|
833
|
+
|
|
834
|
+
if (dnt === '1' || dnt === 'yes' || dnt === true) {
|
|
835
|
+
return { enabled: true, source: 'dnt' };
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (navigator.globalPrivacyControl === true) {
|
|
839
|
+
return { enabled: true, source: 'gpc' };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return { enabled: false, source: null };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* DE only translation - auto-generated
|
|
847
|
+
* Do not edit manually, run: npm run build
|
|
848
|
+
*/
|
|
849
|
+
const translations = {
|
|
850
|
+
de: {
|
|
851
|
+
"labels": {
|
|
852
|
+
"banner": {
|
|
853
|
+
"title": "Wir respektieren Ihre Privatsphäre",
|
|
854
|
+
"description": "Wir verwenden Cookies, um Ihr Surferlebnis zu verbessern, personalisierte Inhalte bereitzustellen und unseren Datenverkehr zu analysieren. Mit einem Klick auf „Alle akzeptieren\" stimmen Sie der Verwendung von Cookies zu.",
|
|
855
|
+
"acceptAll": "Alle akzeptieren",
|
|
856
|
+
"rejectAll": "Alle ablehnen",
|
|
857
|
+
"settings": "Einstellungen"
|
|
858
|
+
},
|
|
859
|
+
"modal": {
|
|
860
|
+
"title": "Datenschutzeinstellungen",
|
|
861
|
+
"description": "Verwalten Sie Ihre Cookie-Einstellungen. Sie können verschiedene Arten von Cookies unten aktivieren oder deaktivieren.",
|
|
862
|
+
"save": "Einstellungen speichern",
|
|
863
|
+
"acceptAll": "Alle akzeptieren",
|
|
864
|
+
"rejectAll": "Alle ablehnen"
|
|
865
|
+
},
|
|
866
|
+
"widget": {
|
|
867
|
+
"label": "Cookie-Einstellungen"
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
"categories": {
|
|
871
|
+
"essential": {
|
|
872
|
+
"label": "Notwendig",
|
|
873
|
+
"description": "Erforderlich für die ordnungsgemäße Funktion der Website. Können nicht deaktiviert werden."
|
|
874
|
+
},
|
|
875
|
+
"functional": {
|
|
876
|
+
"label": "Funktional",
|
|
877
|
+
"description": "Ermöglichen personalisierte Funktionen wie Spracheinstellungen und Designs."
|
|
878
|
+
},
|
|
879
|
+
"analytics": {
|
|
880
|
+
"label": "Analytisch",
|
|
881
|
+
"description": "Helfen uns zu verstehen, wie Besucher mit unserer Website interagieren."
|
|
882
|
+
},
|
|
883
|
+
"marketing": {
|
|
884
|
+
"label": "Marketing",
|
|
885
|
+
"description": "Werden verwendet, um relevante Werbung anzuzeigen und die Kampagnenleistung zu messen."
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
function detectLanguage() {
|
|
892
|
+
return 'de';
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function getTranslation() {
|
|
896
|
+
return translations.de;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Default configuration values
|
|
901
|
+
*/
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
const DEFAULTS = {
|
|
905
|
+
// Language: 'auto' | 'en' | 'de' | 'es' | 'fr' | 'it' | 'pt' | 'nl' | 'pl' | 'uk' | 'ru' | 'ja' | 'zh'
|
|
906
|
+
lang: 'auto',
|
|
907
|
+
|
|
908
|
+
// UI positioning
|
|
909
|
+
position: 'bottom', // 'bottom' | 'bottom-left' | 'bottom-right' | 'top'
|
|
910
|
+
|
|
911
|
+
// Theming
|
|
912
|
+
theme: 'auto', // 'light' | 'dark' | 'auto'
|
|
913
|
+
accentColor: '#0071e3',
|
|
914
|
+
|
|
915
|
+
// Categories
|
|
916
|
+
categories: DEFAULT_CATEGORIES,
|
|
917
|
+
|
|
918
|
+
// UI Labels
|
|
919
|
+
labels: {
|
|
920
|
+
banner: {
|
|
921
|
+
title: 'We value your privacy',
|
|
922
|
+
description: 'We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic. By clicking "Accept All", you consent to our use of cookies.',
|
|
923
|
+
acceptAll: 'Accept All',
|
|
924
|
+
rejectAll: 'Reject All',
|
|
925
|
+
settings: 'Settings'
|
|
926
|
+
},
|
|
927
|
+
modal: {
|
|
928
|
+
title: 'Privacy Settings',
|
|
929
|
+
description: 'Manage your cookie preferences. You can enable or disable different types of cookies below.',
|
|
930
|
+
save: 'Save Preferences',
|
|
931
|
+
acceptAll: 'Accept All',
|
|
932
|
+
rejectAll: 'Reject All'
|
|
933
|
+
},
|
|
934
|
+
widget: {
|
|
935
|
+
label: 'Cookie Settings'
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
|
|
939
|
+
// Behavior
|
|
940
|
+
autoInit: true,
|
|
941
|
+
showWidget: true,
|
|
942
|
+
expiration: 365,
|
|
943
|
+
|
|
944
|
+
// Do Not Track / Global Privacy Control
|
|
945
|
+
// respectDNT: true = respect DNT/GPC signals
|
|
946
|
+
// dntBehavior: 'reject' | 'preselect' | 'ignore'
|
|
947
|
+
// - 'reject': auto-reject non-essential, don't show banner
|
|
948
|
+
// - 'preselect': show banner with non-essential unchecked (same as normal)
|
|
949
|
+
// - 'ignore': ignore DNT completely
|
|
950
|
+
respectDNT: true,
|
|
951
|
+
dntBehavior: 'reject',
|
|
952
|
+
|
|
953
|
+
// Custom styles to inject into Shadow DOM
|
|
954
|
+
customStyles: '',
|
|
955
|
+
|
|
956
|
+
// Blocking mode: 'manual' | 'safe' | 'strict' | 'doomsday'
|
|
957
|
+
mode: 'safe',
|
|
958
|
+
|
|
959
|
+
// Custom domains to block (in addition to mode-based blocking)
|
|
960
|
+
blockedDomains: [], // days
|
|
961
|
+
|
|
962
|
+
// Links
|
|
963
|
+
policyUrl: null,
|
|
964
|
+
imprintUrl: null,
|
|
965
|
+
|
|
966
|
+
// Callbacks
|
|
967
|
+
callbacks: {
|
|
968
|
+
onAccept: null,
|
|
969
|
+
onReject: null,
|
|
970
|
+
onChange: null,
|
|
971
|
+
onReady: null
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Merge user config with defaults (deep merge)
|
|
977
|
+
*/
|
|
978
|
+
function mergeConfig(userConfig) {
|
|
979
|
+
const config = { ...DEFAULTS };
|
|
980
|
+
|
|
981
|
+
if (!userConfig) {
|
|
982
|
+
userConfig = {};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Simple properties
|
|
986
|
+
const simpleKeys = ['lang', 'position', 'theme', 'accentColor', 'autoInit', 'showWidget', 'expiration', 'policyUrl', 'imprintUrl', 'customStyles', 'mode', 'blockedDomains', 'respectDNT', 'dntBehavior'];
|
|
987
|
+
for (const key of simpleKeys) {
|
|
988
|
+
if (userConfig[key] !== undefined) {
|
|
989
|
+
config[key] = userConfig[key];
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Detect language and get translations
|
|
994
|
+
const detectedLang = detectLanguage(config.lang);
|
|
995
|
+
config.lang = detectedLang;
|
|
996
|
+
const translation = getTranslation();
|
|
997
|
+
|
|
998
|
+
// Deep merge labels (translation < user config)
|
|
999
|
+
const translationLabels = translation.labels || {};
|
|
1000
|
+
const userLabels = userConfig.labels || {};
|
|
1001
|
+
config.labels = {
|
|
1002
|
+
banner: {
|
|
1003
|
+
...DEFAULTS.labels.banner,
|
|
1004
|
+
...translationLabels.banner,
|
|
1005
|
+
...userLabels.banner
|
|
1006
|
+
},
|
|
1007
|
+
modal: {
|
|
1008
|
+
...DEFAULTS.labels.modal,
|
|
1009
|
+
...translationLabels.modal,
|
|
1010
|
+
...userLabels.modal
|
|
1011
|
+
},
|
|
1012
|
+
widget: {
|
|
1013
|
+
...DEFAULTS.labels.widget,
|
|
1014
|
+
...translationLabels.widget,
|
|
1015
|
+
...userLabels.widget
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// Deep merge categories (translation < user config)
|
|
1020
|
+
const translationCategories = translation.categories || {};
|
|
1021
|
+
const userCategories = userConfig.categories || {};
|
|
1022
|
+
config.categories = { ...DEFAULTS.categories };
|
|
1023
|
+
for (const key of Object.keys(DEFAULTS.categories)) {
|
|
1024
|
+
config.categories[key] = {
|
|
1025
|
+
...DEFAULTS.categories[key],
|
|
1026
|
+
...translationCategories[key],
|
|
1027
|
+
...userCategories[key]
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Merge callbacks
|
|
1032
|
+
if (userConfig.callbacks) {
|
|
1033
|
+
config.callbacks = { ...DEFAULTS.callbacks, ...userConfig.callbacks };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Patterns (for pattern matcher)
|
|
1037
|
+
if (userConfig.patterns) {
|
|
1038
|
+
config.patterns = userConfig.patterns;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
return config;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Configuration Parser - Reads config from various sources
|
|
1046
|
+
*/
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Parse data attributes from script tag
|
|
1051
|
+
*/
|
|
1052
|
+
function parseDataAttributes() {
|
|
1053
|
+
// Find the Zest script tag
|
|
1054
|
+
const script = document.currentScript ||
|
|
1055
|
+
document.querySelector('script[data-zest]') ||
|
|
1056
|
+
document.querySelector('script[src*="zest"]');
|
|
1057
|
+
|
|
1058
|
+
if (!script) {
|
|
1059
|
+
return {};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const config = {};
|
|
1063
|
+
|
|
1064
|
+
// Position
|
|
1065
|
+
const position = script.getAttribute('data-position');
|
|
1066
|
+
if (position) config.position = position;
|
|
1067
|
+
|
|
1068
|
+
// Theme
|
|
1069
|
+
const theme = script.getAttribute('data-theme');
|
|
1070
|
+
if (theme) config.theme = theme;
|
|
1071
|
+
|
|
1072
|
+
// Accent color
|
|
1073
|
+
const accent = script.getAttribute('data-accent') || script.getAttribute('data-accent-color');
|
|
1074
|
+
if (accent) config.accentColor = accent;
|
|
1075
|
+
|
|
1076
|
+
// Policy URL
|
|
1077
|
+
const policyUrl = script.getAttribute('data-policy-url') || script.getAttribute('data-privacy-url');
|
|
1078
|
+
if (policyUrl) config.policyUrl = policyUrl;
|
|
1079
|
+
|
|
1080
|
+
// Imprint URL
|
|
1081
|
+
const imprintUrl = script.getAttribute('data-imprint-url');
|
|
1082
|
+
if (imprintUrl) config.imprintUrl = imprintUrl;
|
|
1083
|
+
|
|
1084
|
+
// Show widget
|
|
1085
|
+
const showWidget = script.getAttribute('data-show-widget');
|
|
1086
|
+
if (showWidget !== null) config.showWidget = showWidget !== 'false';
|
|
1087
|
+
|
|
1088
|
+
// Auto init
|
|
1089
|
+
const autoInit = script.getAttribute('data-auto-init');
|
|
1090
|
+
if (autoInit !== null) config.autoInit = autoInit !== 'false';
|
|
1091
|
+
|
|
1092
|
+
// Expiration
|
|
1093
|
+
const expiration = script.getAttribute('data-expiration');
|
|
1094
|
+
if (expiration) config.expiration = parseInt(expiration, 10);
|
|
1095
|
+
|
|
1096
|
+
return config;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Parse window.ZestConfig object
|
|
1101
|
+
*/
|
|
1102
|
+
function parseWindowConfig() {
|
|
1103
|
+
if (typeof window !== 'undefined' && window.ZestConfig) {
|
|
1104
|
+
return window.ZestConfig;
|
|
1105
|
+
}
|
|
1106
|
+
return {};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Get final merged configuration
|
|
1111
|
+
* Priority: data attributes > window.ZestConfig > defaults
|
|
1112
|
+
*/
|
|
1113
|
+
function getConfig() {
|
|
1114
|
+
const windowConfig = parseWindowConfig();
|
|
1115
|
+
const dataConfig = parseDataAttributes();
|
|
1116
|
+
|
|
1117
|
+
// Merge: defaults < windowConfig < dataConfig
|
|
1118
|
+
return mergeConfig({
|
|
1119
|
+
...windowConfig,
|
|
1120
|
+
...dataConfig
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Update configuration at runtime
|
|
1126
|
+
*/
|
|
1127
|
+
let currentConfig = null;
|
|
1128
|
+
|
|
1129
|
+
function setConfig(config) {
|
|
1130
|
+
currentConfig = mergeConfig(config);
|
|
1131
|
+
return currentConfig;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function getCurrentConfig() {
|
|
1135
|
+
if (!currentConfig) {
|
|
1136
|
+
currentConfig = getConfig();
|
|
1137
|
+
}
|
|
1138
|
+
return currentConfig;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Consent Store - Manages consent state persistence
|
|
1143
|
+
*/
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
const COOKIE_NAME = 'zest_consent';
|
|
1147
|
+
const CONSENT_VERSION = '1.0';
|
|
1148
|
+
|
|
1149
|
+
// Current consent state
|
|
1150
|
+
let consent = null;
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Get the original cookie setter (bypasses interception)
|
|
1154
|
+
*/
|
|
1155
|
+
function setRawCookie(value) {
|
|
1156
|
+
const descriptor = getOriginalCookieDescriptor();
|
|
1157
|
+
if (descriptor?.set) {
|
|
1158
|
+
descriptor.set.call(document, value);
|
|
1159
|
+
} else {
|
|
1160
|
+
// Fallback if interceptor not initialized yet
|
|
1161
|
+
document.cookie = value;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Get the original cookie getter
|
|
1167
|
+
*/
|
|
1168
|
+
function getRawCookie() {
|
|
1169
|
+
const descriptor = getOriginalCookieDescriptor();
|
|
1170
|
+
if (descriptor?.get) {
|
|
1171
|
+
return descriptor.get.call(document);
|
|
1172
|
+
}
|
|
1173
|
+
return document.cookie;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Load consent from cookie
|
|
1178
|
+
*/
|
|
1179
|
+
function loadConsent() {
|
|
1180
|
+
try {
|
|
1181
|
+
const cookies = getRawCookie();
|
|
1182
|
+
const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
|
|
1183
|
+
|
|
1184
|
+
if (match) {
|
|
1185
|
+
const data = JSON.parse(decodeURIComponent(match[1]));
|
|
1186
|
+
consent = data.categories || getDefaultConsent();
|
|
1187
|
+
return { ...consent };
|
|
1188
|
+
}
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
// Invalid or missing cookie
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
consent = getDefaultConsent();
|
|
1194
|
+
return { ...consent };
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Save consent to cookie
|
|
1199
|
+
*/
|
|
1200
|
+
function saveConsent(expirationDays = 365) {
|
|
1201
|
+
if (!consent) {
|
|
1202
|
+
consent = getDefaultConsent();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const data = {
|
|
1206
|
+
version: CONSENT_VERSION,
|
|
1207
|
+
timestamp: Date.now(),
|
|
1208
|
+
categories: consent
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
const expires = new Date(Date.now() + expirationDays * 24 * 60 * 60 * 1000).toUTCString();
|
|
1212
|
+
const cookieValue = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(data))}; expires=${expires}; path=/; SameSite=Lax`;
|
|
1213
|
+
|
|
1214
|
+
setRawCookie(cookieValue);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Get current consent state
|
|
1219
|
+
*/
|
|
1220
|
+
function getConsent() {
|
|
1221
|
+
if (!consent) {
|
|
1222
|
+
consent = loadConsent();
|
|
1223
|
+
}
|
|
1224
|
+
return { ...consent };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Update consent state
|
|
1229
|
+
*/
|
|
1230
|
+
function updateConsent(newConsent, expirationDays = 365) {
|
|
1231
|
+
const previous = consent ? { ...consent } : getDefaultConsent();
|
|
1232
|
+
|
|
1233
|
+
consent = {
|
|
1234
|
+
essential: true, // Always true
|
|
1235
|
+
functional: !!newConsent.functional,
|
|
1236
|
+
analytics: !!newConsent.analytics,
|
|
1237
|
+
marketing: !!newConsent.marketing
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
saveConsent(expirationDays);
|
|
1241
|
+
|
|
1242
|
+
return { current: { ...consent }, previous };
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Check if specific category is allowed
|
|
1247
|
+
*/
|
|
1248
|
+
function hasConsent(category) {
|
|
1249
|
+
if (!consent) {
|
|
1250
|
+
consent = loadConsent();
|
|
1251
|
+
}
|
|
1252
|
+
return consent[category] === true;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Accept all categories
|
|
1257
|
+
*/
|
|
1258
|
+
function acceptAll(expirationDays = 365) {
|
|
1259
|
+
return updateConsent({
|
|
1260
|
+
functional: true,
|
|
1261
|
+
analytics: true,
|
|
1262
|
+
marketing: true
|
|
1263
|
+
}, expirationDays);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Reject all (except essential)
|
|
1268
|
+
*/
|
|
1269
|
+
function rejectAll(expirationDays = 365) {
|
|
1270
|
+
return updateConsent({
|
|
1271
|
+
functional: false,
|
|
1272
|
+
analytics: false,
|
|
1273
|
+
marketing: false
|
|
1274
|
+
}, expirationDays);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Reset consent (clear cookie)
|
|
1279
|
+
*/
|
|
1280
|
+
function resetConsent() {
|
|
1281
|
+
setRawCookie(`${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`);
|
|
1282
|
+
consent = null;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Check if consent has been given (any decision made)
|
|
1287
|
+
*/
|
|
1288
|
+
function hasConsentDecision() {
|
|
1289
|
+
try {
|
|
1290
|
+
const cookies = getRawCookie();
|
|
1291
|
+
return cookies.includes(COOKIE_NAME);
|
|
1292
|
+
} catch (e) {
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Get consent proof for compliance
|
|
1299
|
+
*/
|
|
1300
|
+
function getConsentProof() {
|
|
1301
|
+
try {
|
|
1302
|
+
const cookies = getRawCookie();
|
|
1303
|
+
const match = cookies.match(new RegExp(`${COOKIE_NAME}=([^;]+)`));
|
|
1304
|
+
|
|
1305
|
+
if (match) {
|
|
1306
|
+
return JSON.parse(decodeURIComponent(match[1]));
|
|
1307
|
+
}
|
|
1308
|
+
} catch (e) {
|
|
1309
|
+
// Invalid cookie
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Events - Custom event dispatching for consent changes
|
|
1317
|
+
*/
|
|
1318
|
+
|
|
1319
|
+
// Event names
|
|
1320
|
+
const EVENTS = {
|
|
1321
|
+
READY: 'zest:ready',
|
|
1322
|
+
CONSENT: 'zest:consent',
|
|
1323
|
+
REJECT: 'zest:reject',
|
|
1324
|
+
CHANGE: 'zest:change',
|
|
1325
|
+
SHOW: 'zest:show',
|
|
1326
|
+
HIDE: 'zest:hide'
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Dispatch a custom event
|
|
1331
|
+
*/
|
|
1332
|
+
function emit(eventName, detail = {}) {
|
|
1333
|
+
const event = new CustomEvent(eventName, {
|
|
1334
|
+
detail,
|
|
1335
|
+
bubbles: true,
|
|
1336
|
+
cancelable: true
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
document.dispatchEvent(event);
|
|
1340
|
+
return event;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Emit ready event
|
|
1345
|
+
*/
|
|
1346
|
+
function emitReady(consent) {
|
|
1347
|
+
return emit(EVENTS.READY, { consent });
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Emit consent event (user accepted)
|
|
1352
|
+
*/
|
|
1353
|
+
function emitConsent(consent, previous) {
|
|
1354
|
+
return emit(EVENTS.CONSENT, { consent, previous });
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Emit reject event (user rejected all)
|
|
1359
|
+
*/
|
|
1360
|
+
function emitReject(consent) {
|
|
1361
|
+
return emit(EVENTS.REJECT, { consent });
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Emit change event (any consent change)
|
|
1366
|
+
*/
|
|
1367
|
+
function emitChange(consent, previous) {
|
|
1368
|
+
return emit(EVENTS.CHANGE, { consent, previous });
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Emit show event (banner/modal shown)
|
|
1373
|
+
*/
|
|
1374
|
+
function emitShow(type = 'banner') {
|
|
1375
|
+
return emit(EVENTS.SHOW, { type });
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Emit hide event (banner/modal hidden)
|
|
1380
|
+
*/
|
|
1381
|
+
function emitHide(type = 'banner') {
|
|
1382
|
+
return emit(EVENTS.HIDE, { type });
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* Styles - Shadow DOM encapsulated CSS with theming
|
|
1387
|
+
*/
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Generate CSS with custom properties
|
|
1391
|
+
*/
|
|
1392
|
+
function generateStyles(config) {
|
|
1393
|
+
const accentColor = config.accentColor || '#4F46E5';
|
|
1394
|
+
|
|
1395
|
+
return `
|
|
1396
|
+
:host {
|
|
1397
|
+
--zest-accent: ${accentColor};
|
|
1398
|
+
--zest-accent-hover: ${adjustColor(accentColor, -15)};
|
|
1399
|
+
--zest-bg: #ffffff;
|
|
1400
|
+
--zest-bg-secondary: #f3f4f6;
|
|
1401
|
+
--zest-text: #1f2937;
|
|
1402
|
+
--zest-text-secondary: #6b7280;
|
|
1403
|
+
--zest-border: #e5e7eb;
|
|
1404
|
+
--zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
1405
|
+
--zest-radius: 12px;
|
|
1406
|
+
--zest-radius-sm: 8px;
|
|
1407
|
+
--zest-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
1408
|
+
|
|
1409
|
+
font-family: var(--zest-font);
|
|
1410
|
+
font-size: 14px;
|
|
1411
|
+
line-height: 1.5;
|
|
1412
|
+
color: var(--zest-text);
|
|
1413
|
+
box-sizing: border-box;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
:host([data-theme="dark"]) {
|
|
1417
|
+
--zest-bg: #1f2937;
|
|
1418
|
+
--zest-bg-secondary: #374151;
|
|
1419
|
+
--zest-text: #f9fafb;
|
|
1420
|
+
--zest-text-secondary: #9ca3af;
|
|
1421
|
+
--zest-border: #4b5563;
|
|
1422
|
+
--zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
@media (prefers-color-scheme: dark) {
|
|
1426
|
+
:host([data-theme="auto"]) {
|
|
1427
|
+
--zest-bg: #1f2937;
|
|
1428
|
+
--zest-bg-secondary: #374151;
|
|
1429
|
+
--zest-text: #f9fafb;
|
|
1430
|
+
--zest-text-secondary: #9ca3af;
|
|
1431
|
+
--zest-border: #4b5563;
|
|
1432
|
+
--zest-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.3);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
*, *::before, *::after {
|
|
1437
|
+
box-sizing: border-box;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/* Banner */
|
|
1441
|
+
.zest-banner {
|
|
1442
|
+
position: fixed;
|
|
1443
|
+
z-index: 999999;
|
|
1444
|
+
max-width: 480px;
|
|
1445
|
+
padding: 20px;
|
|
1446
|
+
background: var(--zest-bg);
|
|
1447
|
+
border-radius: var(--zest-radius);
|
|
1448
|
+
box-shadow: var(--zest-shadow);
|
|
1449
|
+
animation: zest-slide-in 0.3s ease-out;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
.zest-banner--bottom {
|
|
1453
|
+
bottom: 20px;
|
|
1454
|
+
left: 50%;
|
|
1455
|
+
transform: translateX(-50%);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
.zest-banner--bottom-left {
|
|
1459
|
+
bottom: 20px;
|
|
1460
|
+
left: 20px;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
.zest-banner--bottom-right {
|
|
1464
|
+
bottom: 20px;
|
|
1465
|
+
right: 20px;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.zest-banner--top {
|
|
1469
|
+
top: 20px;
|
|
1470
|
+
left: 50%;
|
|
1471
|
+
transform: translateX(-50%);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
@keyframes zest-slide-in {
|
|
1475
|
+
from {
|
|
1476
|
+
opacity: 0;
|
|
1477
|
+
transform: translateX(-50%) translateY(20px);
|
|
1478
|
+
}
|
|
1479
|
+
to {
|
|
1480
|
+
opacity: 1;
|
|
1481
|
+
transform: translateX(-50%) translateY(0);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.zest-banner--bottom-left {
|
|
1486
|
+
animation-name: zest-slide-in-left;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
@keyframes zest-slide-in-left {
|
|
1490
|
+
from {
|
|
1491
|
+
opacity: 0;
|
|
1492
|
+
transform: translateY(20px);
|
|
1493
|
+
}
|
|
1494
|
+
to {
|
|
1495
|
+
opacity: 1;
|
|
1496
|
+
transform: translateY(0);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
.zest-banner--bottom-right {
|
|
1501
|
+
animation-name: zest-slide-in-right;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
@keyframes zest-slide-in-right {
|
|
1505
|
+
from {
|
|
1506
|
+
opacity: 0;
|
|
1507
|
+
transform: translateY(20px);
|
|
1508
|
+
}
|
|
1509
|
+
to {
|
|
1510
|
+
opacity: 1;
|
|
1511
|
+
transform: translateY(0);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1516
|
+
.zest-banner,
|
|
1517
|
+
.zest-modal {
|
|
1518
|
+
animation: none;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
.zest-banner__title {
|
|
1523
|
+
margin: 0 0 8px 0;
|
|
1524
|
+
font-size: 16px;
|
|
1525
|
+
font-weight: 600;
|
|
1526
|
+
color: var(--zest-text);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
.zest-banner__description {
|
|
1530
|
+
margin: 0 0 16px 0;
|
|
1531
|
+
font-size: 14px;
|
|
1532
|
+
color: var(--zest-text-secondary);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
.zest-banner__buttons {
|
|
1536
|
+
display: flex;
|
|
1537
|
+
flex-wrap: wrap;
|
|
1538
|
+
gap: 8px;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/* Buttons */
|
|
1542
|
+
.zest-btn {
|
|
1543
|
+
display: inline-flex;
|
|
1544
|
+
align-items: center;
|
|
1545
|
+
justify-content: center;
|
|
1546
|
+
padding: 10px 16px;
|
|
1547
|
+
font-size: 14px;
|
|
1548
|
+
font-weight: 500;
|
|
1549
|
+
font-family: inherit;
|
|
1550
|
+
border: none;
|
|
1551
|
+
border-radius: var(--zest-radius-sm);
|
|
1552
|
+
cursor: pointer;
|
|
1553
|
+
transition: background-color 0.15s ease, transform 0.1s ease;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
.zest-btn:hover {
|
|
1557
|
+
transform: translateY(-1px);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
.zest-btn:active {
|
|
1561
|
+
transform: translateY(0);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
.zest-btn:focus-visible {
|
|
1565
|
+
outline: 2px solid var(--zest-accent);
|
|
1566
|
+
outline-offset: 2px;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
.zest-btn--primary {
|
|
1570
|
+
background: var(--zest-accent);
|
|
1571
|
+
color: #ffffff;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
.zest-btn--primary:hover {
|
|
1575
|
+
background: var(--zest-accent-hover);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.zest-btn--secondary {
|
|
1579
|
+
background: var(--zest-bg-secondary);
|
|
1580
|
+
color: var(--zest-text);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
.zest-btn--secondary:hover {
|
|
1584
|
+
background: var(--zest-border);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
.zest-btn--ghost {
|
|
1588
|
+
background: transparent;
|
|
1589
|
+
color: var(--zest-text-secondary);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
.zest-btn--ghost:hover {
|
|
1593
|
+
background: var(--zest-bg-secondary);
|
|
1594
|
+
color: var(--zest-text);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/* Modal */
|
|
1598
|
+
.zest-modal-overlay {
|
|
1599
|
+
position: fixed;
|
|
1600
|
+
inset: 0;
|
|
1601
|
+
z-index: 999998;
|
|
1602
|
+
display: flex;
|
|
1603
|
+
align-items: center;
|
|
1604
|
+
justify-content: center;
|
|
1605
|
+
padding: 20px;
|
|
1606
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1607
|
+
animation: zest-fade-in 0.2s ease-out;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
@keyframes zest-fade-in {
|
|
1611
|
+
from { opacity: 0; }
|
|
1612
|
+
to { opacity: 1; }
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
.zest-modal {
|
|
1616
|
+
width: 100%;
|
|
1617
|
+
max-width: 500px;
|
|
1618
|
+
max-height: 90vh;
|
|
1619
|
+
overflow-y: auto;
|
|
1620
|
+
background: var(--zest-bg);
|
|
1621
|
+
border-radius: var(--zest-radius);
|
|
1622
|
+
box-shadow: var(--zest-shadow);
|
|
1623
|
+
animation: zest-modal-in 0.3s ease-out;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
@keyframes zest-modal-in {
|
|
1627
|
+
from {
|
|
1628
|
+
opacity: 0;
|
|
1629
|
+
transform: scale(0.95);
|
|
1630
|
+
}
|
|
1631
|
+
to {
|
|
1632
|
+
opacity: 1;
|
|
1633
|
+
transform: scale(1);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.zest-modal__header {
|
|
1638
|
+
padding: 20px 20px 0;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
.zest-modal__title {
|
|
1642
|
+
margin: 0 0 8px 0;
|
|
1643
|
+
font-size: 18px;
|
|
1644
|
+
font-weight: 600;
|
|
1645
|
+
color: var(--zest-text);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
.zest-modal__description {
|
|
1649
|
+
margin: 0;
|
|
1650
|
+
font-size: 14px;
|
|
1651
|
+
color: var(--zest-text-secondary);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
.zest-modal__body {
|
|
1655
|
+
padding: 20px;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
.zest-modal__footer {
|
|
1659
|
+
display: flex;
|
|
1660
|
+
flex-wrap: wrap;
|
|
1661
|
+
gap: 8px;
|
|
1662
|
+
padding: 0 20px 20px;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/* Categories */
|
|
1666
|
+
.zest-category {
|
|
1667
|
+
padding: 16px;
|
|
1668
|
+
margin-bottom: 12px;
|
|
1669
|
+
background: var(--zest-bg-secondary);
|
|
1670
|
+
border-radius: var(--zest-radius-sm);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
.zest-category:last-child {
|
|
1674
|
+
margin-bottom: 0;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
.zest-category__header {
|
|
1678
|
+
display: flex;
|
|
1679
|
+
align-items: center;
|
|
1680
|
+
justify-content: space-between;
|
|
1681
|
+
gap: 12px;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
.zest-category__info {
|
|
1685
|
+
flex: 1;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.zest-category__label {
|
|
1689
|
+
display: block;
|
|
1690
|
+
font-size: 14px;
|
|
1691
|
+
font-weight: 600;
|
|
1692
|
+
color: var(--zest-text);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
.zest-category__description {
|
|
1696
|
+
margin: 4px 0 0;
|
|
1697
|
+
font-size: 13px;
|
|
1698
|
+
color: var(--zest-text-secondary);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/* Toggle Switch */
|
|
1702
|
+
.zest-toggle {
|
|
1703
|
+
position: relative;
|
|
1704
|
+
width: 44px;
|
|
1705
|
+
height: 24px;
|
|
1706
|
+
flex-shrink: 0;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
.zest-toggle__input {
|
|
1710
|
+
position: absolute;
|
|
1711
|
+
opacity: 0;
|
|
1712
|
+
width: 100%;
|
|
1713
|
+
height: 100%;
|
|
1714
|
+
cursor: pointer;
|
|
1715
|
+
margin: 0;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
.zest-toggle__input:disabled {
|
|
1719
|
+
cursor: not-allowed;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
.zest-toggle__slider {
|
|
1723
|
+
position: absolute;
|
|
1724
|
+
inset: 0;
|
|
1725
|
+
background: var(--zest-border);
|
|
1726
|
+
border-radius: 12px;
|
|
1727
|
+
transition: background-color 0.2s ease;
|
|
1728
|
+
pointer-events: none;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
.zest-toggle__slider::before {
|
|
1732
|
+
content: '';
|
|
1733
|
+
position: absolute;
|
|
1734
|
+
top: 2px;
|
|
1735
|
+
left: 2px;
|
|
1736
|
+
width: 20px;
|
|
1737
|
+
height: 20px;
|
|
1738
|
+
background: #ffffff;
|
|
1739
|
+
border-radius: 50%;
|
|
1740
|
+
transition: transform 0.2s ease;
|
|
1741
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
.zest-toggle__input:checked + .zest-toggle__slider {
|
|
1745
|
+
background: var(--zest-accent);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
.zest-toggle__input:checked + .zest-toggle__slider::before {
|
|
1749
|
+
transform: translateX(20px);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
.zest-toggle__input:focus-visible + .zest-toggle__slider {
|
|
1753
|
+
outline: 2px solid var(--zest-accent);
|
|
1754
|
+
outline-offset: 2px;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
.zest-toggle__input:disabled + .zest-toggle__slider {
|
|
1758
|
+
opacity: 0.6;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/* Widget */
|
|
1762
|
+
.zest-widget {
|
|
1763
|
+
position: fixed;
|
|
1764
|
+
z-index: 999997;
|
|
1765
|
+
bottom: 20px;
|
|
1766
|
+
left: 20px;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
.zest-widget__btn {
|
|
1770
|
+
display: flex;
|
|
1771
|
+
align-items: center;
|
|
1772
|
+
justify-content: center;
|
|
1773
|
+
width: 48px;
|
|
1774
|
+
height: 48px;
|
|
1775
|
+
padding: 0;
|
|
1776
|
+
background: var(--zest-bg);
|
|
1777
|
+
border: 1px solid var(--zest-border);
|
|
1778
|
+
border-radius: 50%;
|
|
1779
|
+
box-shadow: var(--zest-shadow);
|
|
1780
|
+
cursor: pointer;
|
|
1781
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
.zest-widget__btn:hover {
|
|
1785
|
+
transform: scale(1.05);
|
|
1786
|
+
box-shadow: 0 12px 28px -5px rgba(0, 0, 0, 0.15);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
.zest-widget__btn:focus-visible {
|
|
1790
|
+
outline: 2px solid var(--zest-accent);
|
|
1791
|
+
outline-offset: 2px;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
.zest-widget__icon {
|
|
1795
|
+
width: 24px;
|
|
1796
|
+
height: 24px;
|
|
1797
|
+
fill: var(--zest-text);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/* Link */
|
|
1801
|
+
.zest-link {
|
|
1802
|
+
color: var(--zest-accent);
|
|
1803
|
+
text-decoration: none;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
.zest-link:hover {
|
|
1807
|
+
text-decoration: underline;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
/* Mobile */
|
|
1811
|
+
@media (max-width: 480px) {
|
|
1812
|
+
.zest-banner {
|
|
1813
|
+
left: 10px;
|
|
1814
|
+
right: 10px;
|
|
1815
|
+
max-width: none;
|
|
1816
|
+
transform: none;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
.zest-banner--bottom,
|
|
1820
|
+
.zest-banner--bottom-left,
|
|
1821
|
+
.zest-banner--bottom-right {
|
|
1822
|
+
bottom: 10px;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
.zest-banner--top {
|
|
1826
|
+
top: 10px;
|
|
1827
|
+
transform: none;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
@keyframes zest-slide-in {
|
|
1831
|
+
from {
|
|
1832
|
+
opacity: 0;
|
|
1833
|
+
transform: translateY(20px);
|
|
1834
|
+
}
|
|
1835
|
+
to {
|
|
1836
|
+
opacity: 1;
|
|
1837
|
+
transform: translateY(0);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
.zest-banner__buttons {
|
|
1842
|
+
flex-direction: column;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
.zest-btn {
|
|
1846
|
+
width: 100%;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
.zest-modal-overlay {
|
|
1850
|
+
padding: 10px;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
.zest-widget {
|
|
1854
|
+
bottom: 10px;
|
|
1855
|
+
left: 10px;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/* Hidden utility */
|
|
1860
|
+
.zest-hidden {
|
|
1861
|
+
display: none !important;
|
|
1862
|
+
}
|
|
1863
|
+
${config.customStyles || ''}
|
|
1864
|
+
`;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/**
|
|
1868
|
+
* Adjust color brightness
|
|
1869
|
+
*/
|
|
1870
|
+
function adjustColor(hex, percent) {
|
|
1871
|
+
const num = parseInt(hex.replace('#', ''), 16);
|
|
1872
|
+
const amt = Math.round(2.55 * percent);
|
|
1873
|
+
const R = Math.min(255, Math.max(0, (num >> 16) + amt));
|
|
1874
|
+
const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00ff) + amt));
|
|
1875
|
+
const B = Math.min(255, Math.max(0, (num & 0x0000ff) + amt));
|
|
1876
|
+
return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/**
|
|
1880
|
+
* Cookie icon SVG
|
|
1881
|
+
*/
|
|
1882
|
+
const COOKIE_ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10c0-.728-.078-1.437-.225-2.12a1 1 0 0 0-1.482-.63 3 3 0 0 1-4.086-3.72 1 1 0 0 0-.793-1.263A10.05 10.05 0 0 0 12 2zm0 2c.178 0 .354.006.528.017a5 5 0 0 0 5.955 5.955c.011.174.017.35.017.528 0 4.418-3.582 8-8 8s-8-3.582-8-8 3.582-8 8-8zm-4 6a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm5 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm-2 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/></svg>`;
|
|
1883
|
+
|
|
1884
|
+
/**
|
|
1885
|
+
* Banner - Main consent banner component
|
|
1886
|
+
*/
|
|
1887
|
+
|
|
1888
|
+
|
|
1889
|
+
let bannerElement = null;
|
|
1890
|
+
let shadowRoot$2 = null;
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* Create the banner HTML
|
|
1894
|
+
*/
|
|
1895
|
+
function createBannerHTML(config) {
|
|
1896
|
+
const labels = config.labels.banner;
|
|
1897
|
+
const position = config.position || 'bottom';
|
|
1898
|
+
|
|
1899
|
+
return `
|
|
1900
|
+
<div class="zest-banner zest-banner--${position}" role="dialog" aria-modal="false" aria-label="${labels.title}">
|
|
1901
|
+
<h2 class="zest-banner__title">${labels.title}</h2>
|
|
1902
|
+
<p class="zest-banner__description">${labels.description}</p>
|
|
1903
|
+
<div class="zest-banner__buttons">
|
|
1904
|
+
<button type="button" class="zest-btn zest-btn--primary" data-action="accept-all">
|
|
1905
|
+
${labels.acceptAll}
|
|
1906
|
+
</button>
|
|
1907
|
+
<button type="button" class="zest-btn zest-btn--secondary" data-action="reject-all">
|
|
1908
|
+
${labels.rejectAll}
|
|
1909
|
+
</button>
|
|
1910
|
+
<button type="button" class="zest-btn zest-btn--ghost" data-action="settings">
|
|
1911
|
+
${labels.settings}
|
|
1912
|
+
</button>
|
|
1913
|
+
</div>
|
|
1914
|
+
</div>
|
|
1915
|
+
`;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/**
|
|
1919
|
+
* Create and mount the banner
|
|
1920
|
+
*/
|
|
1921
|
+
function createBanner(callbacks = {}) {
|
|
1922
|
+
if (bannerElement) {
|
|
1923
|
+
return bannerElement;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const config = getCurrentConfig();
|
|
1927
|
+
|
|
1928
|
+
// Create host element
|
|
1929
|
+
bannerElement = document.createElement('zest-banner');
|
|
1930
|
+
bannerElement.setAttribute('data-theme', config.theme || 'light');
|
|
1931
|
+
|
|
1932
|
+
// Create shadow root
|
|
1933
|
+
shadowRoot$2 = bannerElement.attachShadow({ mode: 'open' });
|
|
1934
|
+
|
|
1935
|
+
// Add styles
|
|
1936
|
+
const styleEl = document.createElement('style');
|
|
1937
|
+
styleEl.textContent = generateStyles(config);
|
|
1938
|
+
shadowRoot$2.appendChild(styleEl);
|
|
1939
|
+
|
|
1940
|
+
// Add banner HTML
|
|
1941
|
+
const container = document.createElement('div');
|
|
1942
|
+
container.innerHTML = createBannerHTML(config);
|
|
1943
|
+
shadowRoot$2.appendChild(container.firstElementChild);
|
|
1944
|
+
|
|
1945
|
+
// Add event listeners
|
|
1946
|
+
const banner = shadowRoot$2.querySelector('.zest-banner');
|
|
1947
|
+
|
|
1948
|
+
banner.addEventListener('click', (e) => {
|
|
1949
|
+
const action = e.target.dataset.action;
|
|
1950
|
+
if (!action) return;
|
|
1951
|
+
|
|
1952
|
+
switch (action) {
|
|
1953
|
+
case 'accept-all':
|
|
1954
|
+
callbacks.onAcceptAll?.();
|
|
1955
|
+
break;
|
|
1956
|
+
case 'reject-all':
|
|
1957
|
+
callbacks.onRejectAll?.();
|
|
1958
|
+
break;
|
|
1959
|
+
case 'settings':
|
|
1960
|
+
callbacks.onSettings?.();
|
|
1961
|
+
break;
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
// Keyboard handling
|
|
1966
|
+
banner.addEventListener('keydown', (e) => {
|
|
1967
|
+
if (e.key === 'Escape') {
|
|
1968
|
+
callbacks.onSettings?.();
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
// Mount to document
|
|
1973
|
+
document.body.appendChild(bannerElement);
|
|
1974
|
+
|
|
1975
|
+
// Focus first button for accessibility
|
|
1976
|
+
requestAnimationFrame(() => {
|
|
1977
|
+
const firstButton = shadowRoot$2.querySelector('button');
|
|
1978
|
+
firstButton?.focus();
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
return bannerElement;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Show the banner
|
|
1986
|
+
*/
|
|
1987
|
+
function showBanner(callbacks = {}) {
|
|
1988
|
+
if (!bannerElement) {
|
|
1989
|
+
createBanner(callbacks);
|
|
1990
|
+
} else {
|
|
1991
|
+
bannerElement.classList.remove('zest-hidden');
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
/**
|
|
1996
|
+
* Hide the banner
|
|
1997
|
+
*/
|
|
1998
|
+
function hideBanner() {
|
|
1999
|
+
if (bannerElement) {
|
|
2000
|
+
bannerElement.remove();
|
|
2001
|
+
bannerElement = null;
|
|
2002
|
+
shadowRoot$2 = null;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* Modal - Settings modal component for category toggles
|
|
2008
|
+
*/
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
let modalElement = null;
|
|
2012
|
+
let shadowRoot$1 = null;
|
|
2013
|
+
let currentSelections = {};
|
|
2014
|
+
|
|
2015
|
+
/**
|
|
2016
|
+
* Create category toggle HTML
|
|
2017
|
+
*/
|
|
2018
|
+
function createCategoryHTML(category, isChecked, isRequired) {
|
|
2019
|
+
const disabled = isRequired ? 'disabled' : '';
|
|
2020
|
+
const checked = isChecked ? 'checked' : '';
|
|
2021
|
+
|
|
2022
|
+
return `
|
|
2023
|
+
<div class="zest-category">
|
|
2024
|
+
<div class="zest-category__header">
|
|
2025
|
+
<div class="zest-category__info">
|
|
2026
|
+
<span class="zest-category__label">${category.label}</span>
|
|
2027
|
+
<p class="zest-category__description">${category.description}</p>
|
|
2028
|
+
</div>
|
|
2029
|
+
<label class="zest-toggle">
|
|
2030
|
+
<input
|
|
2031
|
+
type="checkbox"
|
|
2032
|
+
class="zest-toggle__input"
|
|
2033
|
+
data-category="${category.id}"
|
|
2034
|
+
${checked}
|
|
2035
|
+
${disabled}
|
|
2036
|
+
aria-label="${category.label}"
|
|
2037
|
+
>
|
|
2038
|
+
<span class="zest-toggle__slider"></span>
|
|
2039
|
+
</label>
|
|
2040
|
+
</div>
|
|
2041
|
+
</div>
|
|
2042
|
+
`;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
/**
|
|
2046
|
+
* Create the modal HTML
|
|
2047
|
+
*/
|
|
2048
|
+
function createModalHTML(config, consent) {
|
|
2049
|
+
const labels = config.labels.modal;
|
|
2050
|
+
const categories = config.categories || DEFAULT_CATEGORIES;
|
|
2051
|
+
|
|
2052
|
+
const categoriesHTML = Object.values(categories)
|
|
2053
|
+
.map(cat => createCategoryHTML(
|
|
2054
|
+
cat,
|
|
2055
|
+
consent[cat.id] ?? cat.default,
|
|
2056
|
+
cat.required
|
|
2057
|
+
))
|
|
2058
|
+
.join('');
|
|
2059
|
+
|
|
2060
|
+
const policyLink = config.policyUrl
|
|
2061
|
+
? `<a href="${config.policyUrl}" class="zest-link" target="_blank" rel="noopener">Privacy Policy</a>`
|
|
2062
|
+
: '';
|
|
2063
|
+
|
|
2064
|
+
return `
|
|
2065
|
+
<div class="zest-modal-overlay" role="dialog" aria-modal="true" aria-label="${labels.title}">
|
|
2066
|
+
<div class="zest-modal">
|
|
2067
|
+
<div class="zest-modal__header">
|
|
2068
|
+
<h2 class="zest-modal__title">${labels.title}</h2>
|
|
2069
|
+
<p class="zest-modal__description">${labels.description} ${policyLink}</p>
|
|
2070
|
+
</div>
|
|
2071
|
+
<div class="zest-modal__body">
|
|
2072
|
+
${categoriesHTML}
|
|
2073
|
+
</div>
|
|
2074
|
+
<div class="zest-modal__footer">
|
|
2075
|
+
<button type="button" class="zest-btn zest-btn--primary" data-action="save">
|
|
2076
|
+
${labels.save}
|
|
2077
|
+
</button>
|
|
2078
|
+
<button type="button" class="zest-btn zest-btn--secondary" data-action="accept-all">
|
|
2079
|
+
${labels.acceptAll}
|
|
2080
|
+
</button>
|
|
2081
|
+
<button type="button" class="zest-btn zest-btn--ghost" data-action="reject-all">
|
|
2082
|
+
${labels.rejectAll}
|
|
2083
|
+
</button>
|
|
2084
|
+
</div>
|
|
2085
|
+
</div>
|
|
2086
|
+
</div>
|
|
2087
|
+
`;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* Get current selections from toggles
|
|
2092
|
+
*/
|
|
2093
|
+
function getSelections() {
|
|
2094
|
+
if (!shadowRoot$1) return currentSelections;
|
|
2095
|
+
|
|
2096
|
+
const toggles = shadowRoot$1.querySelectorAll('.zest-toggle__input');
|
|
2097
|
+
const selections = { essential: true };
|
|
2098
|
+
|
|
2099
|
+
toggles.forEach(toggle => {
|
|
2100
|
+
const category = toggle.dataset.category;
|
|
2101
|
+
if (category && category !== 'essential') {
|
|
2102
|
+
selections[category] = toggle.checked;
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
return selections;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/**
|
|
2110
|
+
* Create and show the modal
|
|
2111
|
+
*/
|
|
2112
|
+
function showModal(consent = {}, callbacks = {}) {
|
|
2113
|
+
if (modalElement) {
|
|
2114
|
+
return modalElement;
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
const config = getCurrentConfig();
|
|
2118
|
+
currentSelections = { ...consent };
|
|
2119
|
+
|
|
2120
|
+
// Create host element
|
|
2121
|
+
modalElement = document.createElement('zest-modal');
|
|
2122
|
+
modalElement.setAttribute('data-theme', config.theme || 'light');
|
|
2123
|
+
|
|
2124
|
+
// Create shadow root
|
|
2125
|
+
shadowRoot$1 = modalElement.attachShadow({ mode: 'open' });
|
|
2126
|
+
|
|
2127
|
+
// Add styles
|
|
2128
|
+
const styleEl = document.createElement('style');
|
|
2129
|
+
styleEl.textContent = generateStyles(config);
|
|
2130
|
+
shadowRoot$1.appendChild(styleEl);
|
|
2131
|
+
|
|
2132
|
+
// Add modal HTML
|
|
2133
|
+
const container = document.createElement('div');
|
|
2134
|
+
container.innerHTML = createModalHTML(config, consent);
|
|
2135
|
+
shadowRoot$1.appendChild(container.firstElementChild);
|
|
2136
|
+
|
|
2137
|
+
// Add event listeners
|
|
2138
|
+
const modal = shadowRoot$1.querySelector('.zest-modal-overlay');
|
|
2139
|
+
|
|
2140
|
+
// Button clicks
|
|
2141
|
+
modal.addEventListener('click', (e) => {
|
|
2142
|
+
const action = e.target.dataset.action;
|
|
2143
|
+
if (!action) {
|
|
2144
|
+
// Click on overlay background to close
|
|
2145
|
+
if (e.target === modal) {
|
|
2146
|
+
callbacks.onClose?.();
|
|
2147
|
+
}
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
switch (action) {
|
|
2152
|
+
case 'save':
|
|
2153
|
+
callbacks.onSave?.(getSelections());
|
|
2154
|
+
break;
|
|
2155
|
+
case 'accept-all':
|
|
2156
|
+
callbacks.onAcceptAll?.();
|
|
2157
|
+
break;
|
|
2158
|
+
case 'reject-all':
|
|
2159
|
+
callbacks.onRejectAll?.();
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
|
|
2164
|
+
// Keyboard handling
|
|
2165
|
+
modal.addEventListener('keydown', (e) => {
|
|
2166
|
+
if (e.key === 'Escape') {
|
|
2167
|
+
callbacks.onClose?.();
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
// Track toggle changes
|
|
2172
|
+
shadowRoot$1.querySelectorAll('.zest-toggle__input').forEach(toggle => {
|
|
2173
|
+
toggle.addEventListener('change', () => {
|
|
2174
|
+
currentSelections = getSelections();
|
|
2175
|
+
});
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
// Mount to document
|
|
2179
|
+
document.body.appendChild(modalElement);
|
|
2180
|
+
|
|
2181
|
+
// Trap focus
|
|
2182
|
+
requestAnimationFrame(() => {
|
|
2183
|
+
const firstButton = shadowRoot$1.querySelector('button');
|
|
2184
|
+
firstButton?.focus();
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
return modalElement;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/**
|
|
2191
|
+
* Hide the modal
|
|
2192
|
+
*/
|
|
2193
|
+
function hideModal() {
|
|
2194
|
+
if (modalElement) {
|
|
2195
|
+
modalElement.remove();
|
|
2196
|
+
modalElement = null;
|
|
2197
|
+
shadowRoot$1 = null;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
/**
|
|
2202
|
+
* Widget - Minimal floating button to reopen settings
|
|
2203
|
+
*/
|
|
2204
|
+
|
|
2205
|
+
|
|
2206
|
+
let widgetElement = null;
|
|
2207
|
+
let shadowRoot = null;
|
|
2208
|
+
|
|
2209
|
+
/**
|
|
2210
|
+
* Create the widget HTML
|
|
2211
|
+
*/
|
|
2212
|
+
function createWidgetHTML(config) {
|
|
2213
|
+
const labels = config.labels.widget;
|
|
2214
|
+
|
|
2215
|
+
return `
|
|
2216
|
+
<div class="zest-widget">
|
|
2217
|
+
<button type="button" class="zest-widget__btn" aria-label="${labels.label}" title="${labels.label}">
|
|
2218
|
+
<span class="zest-widget__icon">${COOKIE_ICON}</span>
|
|
2219
|
+
</button>
|
|
2220
|
+
</div>
|
|
2221
|
+
`;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
/**
|
|
2225
|
+
* Create and mount the widget
|
|
2226
|
+
*/
|
|
2227
|
+
function createWidget(callbacks = {}) {
|
|
2228
|
+
if (widgetElement) {
|
|
2229
|
+
return widgetElement;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
const config = getCurrentConfig();
|
|
2233
|
+
|
|
2234
|
+
// Create host element
|
|
2235
|
+
widgetElement = document.createElement('zest-widget');
|
|
2236
|
+
widgetElement.setAttribute('data-theme', config.theme || 'light');
|
|
2237
|
+
|
|
2238
|
+
// Create shadow root
|
|
2239
|
+
shadowRoot = widgetElement.attachShadow({ mode: 'open' });
|
|
2240
|
+
|
|
2241
|
+
// Add styles
|
|
2242
|
+
const styleEl = document.createElement('style');
|
|
2243
|
+
styleEl.textContent = generateStyles(config);
|
|
2244
|
+
shadowRoot.appendChild(styleEl);
|
|
2245
|
+
|
|
2246
|
+
// Add widget HTML
|
|
2247
|
+
const container = document.createElement('div');
|
|
2248
|
+
container.innerHTML = createWidgetHTML(config);
|
|
2249
|
+
shadowRoot.appendChild(container.firstElementChild);
|
|
2250
|
+
|
|
2251
|
+
// Add event listener
|
|
2252
|
+
const button = shadowRoot.querySelector('.zest-widget__btn');
|
|
2253
|
+
button.addEventListener('click', () => {
|
|
2254
|
+
callbacks.onClick?.();
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
// Mount to document
|
|
2258
|
+
document.body.appendChild(widgetElement);
|
|
2259
|
+
|
|
2260
|
+
return widgetElement;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/**
|
|
2264
|
+
* Show the widget
|
|
2265
|
+
*/
|
|
2266
|
+
function showWidget(callbacks = {}) {
|
|
2267
|
+
if (!widgetElement) {
|
|
2268
|
+
createWidget(callbacks);
|
|
2269
|
+
} else {
|
|
2270
|
+
widgetElement.style.display = '';
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/**
|
|
2275
|
+
* Hide the widget
|
|
2276
|
+
*/
|
|
2277
|
+
function hideWidget() {
|
|
2278
|
+
if (widgetElement) {
|
|
2279
|
+
widgetElement.style.display = 'none';
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/**
|
|
2284
|
+
* Remove the widget completely
|
|
2285
|
+
*/
|
|
2286
|
+
function removeWidget() {
|
|
2287
|
+
if (widgetElement) {
|
|
2288
|
+
widgetElement.remove();
|
|
2289
|
+
widgetElement = null;
|
|
2290
|
+
shadowRoot = null;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
/**
|
|
2295
|
+
* Zest - Lightweight Cookie Consent Toolkit
|
|
2296
|
+
* Main entry point
|
|
2297
|
+
*/
|
|
2298
|
+
|
|
2299
|
+
|
|
2300
|
+
// State
|
|
2301
|
+
let initialized = false;
|
|
2302
|
+
let config = null;
|
|
2303
|
+
|
|
2304
|
+
/**
|
|
2305
|
+
* Consent checker function shared across interceptors
|
|
2306
|
+
*/
|
|
2307
|
+
function checkConsent(category) {
|
|
2308
|
+
return hasConsent(category);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/**
|
|
2312
|
+
* Replay all queued items for newly allowed categories
|
|
2313
|
+
*/
|
|
2314
|
+
function replayAll(allowedCategories) {
|
|
2315
|
+
replayCookies(allowedCategories);
|
|
2316
|
+
replayStorage(allowedCategories);
|
|
2317
|
+
replayScripts(allowedCategories);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* Handle accept all
|
|
2322
|
+
*/
|
|
2323
|
+
function handleAcceptAll() {
|
|
2324
|
+
const result = acceptAll(config.expiration);
|
|
2325
|
+
const categories = getCategoryIds();
|
|
2326
|
+
|
|
2327
|
+
hideBanner();
|
|
2328
|
+
hideModal();
|
|
2329
|
+
|
|
2330
|
+
replayAll(categories);
|
|
2331
|
+
|
|
2332
|
+
if (config.showWidget) {
|
|
2333
|
+
showWidget({ onClick: handleShowSettings });
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
emitConsent(result.current, result.previous);
|
|
2337
|
+
emitChange(result.current, result.previous);
|
|
2338
|
+
config.callbacks?.onAccept?.(result.current);
|
|
2339
|
+
config.callbacks?.onChange?.(result.current);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
/**
|
|
2343
|
+
* Handle reject all
|
|
2344
|
+
*/
|
|
2345
|
+
function handleRejectAll() {
|
|
2346
|
+
const result = rejectAll(config.expiration);
|
|
2347
|
+
|
|
2348
|
+
hideBanner();
|
|
2349
|
+
hideModal();
|
|
2350
|
+
|
|
2351
|
+
if (config.showWidget) {
|
|
2352
|
+
showWidget({ onClick: handleShowSettings });
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
emitReject(result.current);
|
|
2356
|
+
emitChange(result.current, result.previous);
|
|
2357
|
+
config.callbacks?.onReject?.();
|
|
2358
|
+
config.callbacks?.onChange?.(result.current);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/**
|
|
2362
|
+
* Handle save preferences from modal
|
|
2363
|
+
*/
|
|
2364
|
+
function handleSavePreferences(selections) {
|
|
2365
|
+
const result = updateConsent(selections, config.expiration);
|
|
2366
|
+
|
|
2367
|
+
// Find newly allowed categories
|
|
2368
|
+
const newlyAllowed = Object.keys(result.current).filter(
|
|
2369
|
+
cat => result.current[cat] && !result.previous[cat]
|
|
2370
|
+
);
|
|
2371
|
+
|
|
2372
|
+
if (newlyAllowed.length > 0) {
|
|
2373
|
+
replayAll(newlyAllowed);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
hideModal();
|
|
2377
|
+
|
|
2378
|
+
if (config.showWidget) {
|
|
2379
|
+
showWidget({ onClick: handleShowSettings });
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// Determine if this was acceptance or rejection based on selections
|
|
2383
|
+
const hasNonEssential = Object.entries(selections)
|
|
2384
|
+
.some(([cat, val]) => cat !== 'essential' && val);
|
|
2385
|
+
|
|
2386
|
+
if (hasNonEssential) {
|
|
2387
|
+
emitConsent(result.current, result.previous);
|
|
2388
|
+
} else {
|
|
2389
|
+
emitReject(result.current);
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
emitChange(result.current, result.previous);
|
|
2393
|
+
config.callbacks?.onChange?.(result.current);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
/**
|
|
2397
|
+
* Handle show settings
|
|
2398
|
+
*/
|
|
2399
|
+
function handleShowSettings() {
|
|
2400
|
+
hideBanner();
|
|
2401
|
+
hideWidget();
|
|
2402
|
+
|
|
2403
|
+
showModal(getConsent(), {
|
|
2404
|
+
onSave: handleSavePreferences,
|
|
2405
|
+
onAcceptAll: handleAcceptAll,
|
|
2406
|
+
onRejectAll: handleRejectAll,
|
|
2407
|
+
onClose: handleCloseModal
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
emitShow('modal');
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/**
|
|
2414
|
+
* Handle close modal
|
|
2415
|
+
*/
|
|
2416
|
+
function handleCloseModal() {
|
|
2417
|
+
hideModal();
|
|
2418
|
+
emitHide('modal');
|
|
2419
|
+
|
|
2420
|
+
// Show widget if consent was already given
|
|
2421
|
+
if (hasConsentDecision() && config.showWidget) {
|
|
2422
|
+
showWidget({ onClick: handleShowSettings });
|
|
2423
|
+
} else {
|
|
2424
|
+
// Show banner again if no decision made
|
|
2425
|
+
showBanner({
|
|
2426
|
+
onAcceptAll: handleAcceptAll,
|
|
2427
|
+
onRejectAll: handleRejectAll,
|
|
2428
|
+
onSettings: handleShowSettings
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Initialize Zest
|
|
2435
|
+
*/
|
|
2436
|
+
function init(userConfig = {}) {
|
|
2437
|
+
if (initialized) {
|
|
2438
|
+
console.warn('[Zest] Already initialized');
|
|
2439
|
+
return Zest;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// Merge config
|
|
2443
|
+
config = setConfig(userConfig);
|
|
2444
|
+
|
|
2445
|
+
// Set patterns if provided
|
|
2446
|
+
if (config.patterns) {
|
|
2447
|
+
setPatterns(config.patterns);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// Set up consent checkers
|
|
2451
|
+
setConsentChecker$2(checkConsent);
|
|
2452
|
+
setConsentChecker$1(checkConsent);
|
|
2453
|
+
setConsentChecker(checkConsent);
|
|
2454
|
+
|
|
2455
|
+
// Start interception
|
|
2456
|
+
interceptCookies();
|
|
2457
|
+
interceptStorage();
|
|
2458
|
+
startScriptBlocking(config.mode, config.blockedDomains);
|
|
2459
|
+
|
|
2460
|
+
// Load saved consent
|
|
2461
|
+
const consent = loadConsent();
|
|
2462
|
+
|
|
2463
|
+
initialized = true;
|
|
2464
|
+
|
|
2465
|
+
// Check Do Not Track / Global Privacy Control
|
|
2466
|
+
const dntEnabled = isDoNotTrackEnabled();
|
|
2467
|
+
let dntApplied = false;
|
|
2468
|
+
|
|
2469
|
+
if (dntEnabled && config.respectDNT && config.dntBehavior !== 'ignore') {
|
|
2470
|
+
if (config.dntBehavior === 'reject' && !hasConsentDecision()) {
|
|
2471
|
+
// Auto-reject non-essential cookies silently
|
|
2472
|
+
const result = rejectAll(config.expiration);
|
|
2473
|
+
dntApplied = true;
|
|
2474
|
+
|
|
2475
|
+
// Emit events
|
|
2476
|
+
emitReject(result.current);
|
|
2477
|
+
emitChange(result.current, result.previous);
|
|
2478
|
+
config.callbacks?.onReject?.();
|
|
2479
|
+
config.callbacks?.onChange?.(result.current);
|
|
2480
|
+
}
|
|
2481
|
+
// 'preselect' behavior is handled by default (banner shows with defaults off)
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// Emit ready event
|
|
2485
|
+
emitReady(consent);
|
|
2486
|
+
config.callbacks?.onReady?.(consent);
|
|
2487
|
+
|
|
2488
|
+
// Show UI based on consent state
|
|
2489
|
+
if (!hasConsentDecision() && !dntApplied) {
|
|
2490
|
+
// No consent decision yet - show banner
|
|
2491
|
+
showBanner({
|
|
2492
|
+
onAcceptAll: handleAcceptAll,
|
|
2493
|
+
onRejectAll: handleRejectAll,
|
|
2494
|
+
onSettings: handleShowSettings
|
|
2495
|
+
});
|
|
2496
|
+
emitShow('banner');
|
|
2497
|
+
} else {
|
|
2498
|
+
// Consent already given (or DNT auto-rejected) - show widget for reopening settings
|
|
2499
|
+
if (config.showWidget) {
|
|
2500
|
+
showWidget({ onClick: handleShowSettings });
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
return Zest;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
/**
|
|
2508
|
+
* Public API
|
|
2509
|
+
*/
|
|
2510
|
+
const Zest = {
|
|
2511
|
+
// Initialization
|
|
2512
|
+
init,
|
|
2513
|
+
|
|
2514
|
+
// Banner control
|
|
2515
|
+
show() {
|
|
2516
|
+
if (!initialized) {
|
|
2517
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
hideModal();
|
|
2521
|
+
hideWidget();
|
|
2522
|
+
showBanner({
|
|
2523
|
+
onAcceptAll: handleAcceptAll,
|
|
2524
|
+
onRejectAll: handleRejectAll,
|
|
2525
|
+
onSettings: handleShowSettings
|
|
2526
|
+
});
|
|
2527
|
+
emitShow('banner');
|
|
2528
|
+
},
|
|
2529
|
+
|
|
2530
|
+
hide() {
|
|
2531
|
+
hideBanner();
|
|
2532
|
+
emitHide('banner');
|
|
2533
|
+
},
|
|
2534
|
+
|
|
2535
|
+
// Settings modal
|
|
2536
|
+
showSettings() {
|
|
2537
|
+
if (!initialized) {
|
|
2538
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
handleShowSettings();
|
|
2542
|
+
},
|
|
2543
|
+
|
|
2544
|
+
hideSettings() {
|
|
2545
|
+
hideModal();
|
|
2546
|
+
emitHide('modal');
|
|
2547
|
+
},
|
|
2548
|
+
|
|
2549
|
+
// Consent management
|
|
2550
|
+
getConsent,
|
|
2551
|
+
hasConsent,
|
|
2552
|
+
hasConsentDecision,
|
|
2553
|
+
getConsentProof,
|
|
2554
|
+
|
|
2555
|
+
// DNT detection
|
|
2556
|
+
isDoNotTrackEnabled,
|
|
2557
|
+
getDNTDetails,
|
|
2558
|
+
|
|
2559
|
+
// Accept/Reject programmatically
|
|
2560
|
+
acceptAll() {
|
|
2561
|
+
if (!initialized) {
|
|
2562
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
handleAcceptAll();
|
|
2566
|
+
},
|
|
2567
|
+
|
|
2568
|
+
rejectAll() {
|
|
2569
|
+
if (!initialized) {
|
|
2570
|
+
console.warn('[Zest] Not initialized. Call Zest.init() first.');
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
handleRejectAll();
|
|
2574
|
+
},
|
|
2575
|
+
|
|
2576
|
+
// Reset and show banner again
|
|
2577
|
+
reset() {
|
|
2578
|
+
resetConsent();
|
|
2579
|
+
hideModal();
|
|
2580
|
+
removeWidget();
|
|
2581
|
+
|
|
2582
|
+
if (initialized) {
|
|
2583
|
+
showBanner({
|
|
2584
|
+
onAcceptAll: handleAcceptAll,
|
|
2585
|
+
onRejectAll: handleRejectAll,
|
|
2586
|
+
onSettings: handleShowSettings
|
|
2587
|
+
});
|
|
2588
|
+
emitShow('banner');
|
|
2589
|
+
}
|
|
2590
|
+
},
|
|
2591
|
+
|
|
2592
|
+
// Config
|
|
2593
|
+
getConfig: getCurrentConfig,
|
|
2594
|
+
|
|
2595
|
+
// Events
|
|
2596
|
+
EVENTS
|
|
2597
|
+
};
|
|
2598
|
+
|
|
2599
|
+
// Auto-init if config present
|
|
2600
|
+
if (typeof window !== 'undefined') {
|
|
2601
|
+
// Make Zest available globally
|
|
2602
|
+
window.Zest = Zest;
|
|
2603
|
+
|
|
2604
|
+
const autoInit = () => {
|
|
2605
|
+
const cfg = getConfig();
|
|
2606
|
+
if (cfg.autoInit !== false) {
|
|
2607
|
+
init(window.ZestConfig);
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
|
|
2611
|
+
if (document.readyState === 'loading') {
|
|
2612
|
+
document.addEventListener('DOMContentLoaded', autoInit);
|
|
2613
|
+
} else {
|
|
2614
|
+
autoInit();
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
return Zest;
|
|
2619
|
+
|
|
2620
|
+
})();
|
|
2621
|
+
//# sourceMappingURL=zest.de.js.map
|