cookie-consent-gdpr 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 +758 -0
- package/dist/cookie-consent-gdpr.cjs.js +2365 -0
- package/dist/cookie-consent-gdpr.esm.js +2363 -0
- package/dist/cookie-consent-gdpr.min.js +2 -0
- package/dist/cookie-consent-gdpr.umd.js +2371 -0
- package/dist/gdpr-cookie-consent.cjs.js +2365 -0
- package/dist/gdpr-cookie-consent.d.ts +182 -0
- package/dist/gdpr-cookie-consent.esm.js +2363 -0
- package/dist/gdpr-cookie-consent.min.js +2 -0
- package/dist/gdpr-cookie-consent.umd.js +2371 -0
- package/package.json +53 -0
|
@@ -0,0 +1,2365 @@
|
|
|
1
|
+
/*! cookie-consent-gdpr v1.0.0 | MIT License | github.com/alexgarbacea/cookie-consent-gdpr */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default configuration for the cookie consent banner.
|
|
6
|
+
* Every value here can be overridden by the user.
|
|
7
|
+
*/
|
|
8
|
+
var DEFAULTS = {
|
|
9
|
+
/**
|
|
10
|
+
* Where to mount the banner. Accepts a CSS selector or DOM element.
|
|
11
|
+
* If null, the banner appends to document.body.
|
|
12
|
+
*/
|
|
13
|
+
container: null,
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Selector or element for the "open preferences" button.
|
|
17
|
+
* Any element matching this selector will open the preferences modal on click.
|
|
18
|
+
* Elements with [data-cc-open-preferences] are also auto-bound.
|
|
19
|
+
*/
|
|
20
|
+
preferencesButton: null,
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Banner layout mode: 'bar' | 'modal' | 'popup'
|
|
24
|
+
* - bar: full-width bar at bottom (or top) of viewport
|
|
25
|
+
* - modal: centered overlay demanding attention
|
|
26
|
+
* - popup: small card at bottom-right (or configurable corner)
|
|
27
|
+
*/
|
|
28
|
+
layout: 'bar',
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Position for the banner.
|
|
32
|
+
* - bar: 'bottom' | 'top'
|
|
33
|
+
* - popup: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
|
34
|
+
* - modal: ignored (always centered)
|
|
35
|
+
*/
|
|
36
|
+
position: 'bottom',
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether clicking the overlay backdrop closes the banner (modal/popup modes).
|
|
40
|
+
* For strict GDPR, this should be false so users must make an explicit choice.
|
|
41
|
+
*/
|
|
42
|
+
closeOnBackdrop: false,
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether to show the banner immediately on page load if no consent exists.
|
|
46
|
+
*/
|
|
47
|
+
autoShow: true,
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether to re-show the banner when config version changes (new cookies added, etc.).
|
|
51
|
+
*/
|
|
52
|
+
reconsentOnChange: true,
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Force a modal/overlay that blocks interaction until consent is given.
|
|
56
|
+
* When true, a semi-transparent overlay covers the page.
|
|
57
|
+
*/
|
|
58
|
+
forceOverlay: false,
|
|
59
|
+
|
|
60
|
+
// ─── Cookie Storage ──────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
cookie: {
|
|
63
|
+
/** Name of the consent cookie. */
|
|
64
|
+
name: 'cc_consent',
|
|
65
|
+
/** Cookie domain. Set to '.example.com' to share across subdomains. */
|
|
66
|
+
domain: '',
|
|
67
|
+
/** Cookie path. */
|
|
68
|
+
path: '/',
|
|
69
|
+
/** Consent expiry in days. GDPR recommends re-consent at least every 12 months. */
|
|
70
|
+
expiryDays: 365,
|
|
71
|
+
/** SameSite attribute: 'Lax' | 'Strict' | 'None' */
|
|
72
|
+
sameSite: 'Lax',
|
|
73
|
+
/** Secure flag. Should be true in production (HTTPS). */
|
|
74
|
+
secure: typeof window !== 'undefined' && window.location.protocol === 'https:',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// ─── Consent Webhook ─────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
webhook: {
|
|
80
|
+
/** URL to POST consent records to. Null disables the webhook. */
|
|
81
|
+
url: null,
|
|
82
|
+
/** Additional headers for the webhook request. */
|
|
83
|
+
headers: {},
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// ─── Cookie Categories ───────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
categories: {
|
|
89
|
+
necessary: {
|
|
90
|
+
enabled: true,
|
|
91
|
+
readOnly: true,
|
|
92
|
+
title: 'Strictly Necessary',
|
|
93
|
+
description:
|
|
94
|
+
'These cookies are essential for the website to function and cannot be switched off. They are usually set in response to actions you take such as setting your privacy preferences, logging in, or filling in forms.',
|
|
95
|
+
cookies: [],
|
|
96
|
+
},
|
|
97
|
+
functional: {
|
|
98
|
+
enabled: false,
|
|
99
|
+
readOnly: false,
|
|
100
|
+
title: 'Functional',
|
|
101
|
+
description:
|
|
102
|
+
'These cookies enable the website to provide enhanced functionality and personalisation. They may be set by us or by third-party providers whose services we have added to our pages.',
|
|
103
|
+
cookies: [],
|
|
104
|
+
},
|
|
105
|
+
analytics: {
|
|
106
|
+
enabled: false,
|
|
107
|
+
readOnly: false,
|
|
108
|
+
title: 'Analytics',
|
|
109
|
+
description:
|
|
110
|
+
'These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site. They help us know which pages are the most and least popular and see how visitors move around the site.',
|
|
111
|
+
cookies: [],
|
|
112
|
+
},
|
|
113
|
+
marketing: {
|
|
114
|
+
enabled: false,
|
|
115
|
+
readOnly: false,
|
|
116
|
+
title: 'Marketing',
|
|
117
|
+
description:
|
|
118
|
+
'These cookies may be set through our site by our advertising partners. They may be used to build a profile of your interests and show you relevant adverts on other sites.',
|
|
119
|
+
cookies: [],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Each cookie entry in a category should have:
|
|
125
|
+
* {
|
|
126
|
+
* name: 'cookie_name', // Cookie name or pattern
|
|
127
|
+
* provider: 'example.com', // Who sets this cookie
|
|
128
|
+
* purpose: 'Stores user...', // What it does
|
|
129
|
+
* expiry: '1 year', // How long it lasts
|
|
130
|
+
* type: 'HTTP Cookie', // HTTP Cookie | localStorage | sessionStorage | Pixel
|
|
131
|
+
* }
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
// ─── Texts (fully customizable for i18n) ─────────────────────────────
|
|
135
|
+
|
|
136
|
+
texts: {
|
|
137
|
+
bannerTitle: 'We use cookies',
|
|
138
|
+
bannerDescription:
|
|
139
|
+
'We use cookies and similar technologies to help personalise content, tailor and measure ads, and provide a better experience. By clicking "Accept All", you consent to the use of ALL cookies. You may visit "Cookie Settings" to provide controlled consent.',
|
|
140
|
+
acceptAll: 'Accept All',
|
|
141
|
+
rejectAll: 'Reject All',
|
|
142
|
+
settings: 'Cookie Settings',
|
|
143
|
+
preferencesTitle: 'Cookie Preferences',
|
|
144
|
+
preferencesDescription:
|
|
145
|
+
'When you visit any website, it may store or retrieve information on your browser, mostly in the form of cookies. This information might be about you, your preferences, or your device and is mostly used to make the site work as you expect it to. The information does not usually directly identify you, but it can give you a more personalised web experience. Because we respect your right to privacy, you can choose not to allow some types of cookies.',
|
|
146
|
+
save: 'Save Preferences',
|
|
147
|
+
acceptAllPreferences: 'Accept All',
|
|
148
|
+
rejectAllPreferences: 'Reject All',
|
|
149
|
+
alwaysActive: 'Always Active',
|
|
150
|
+
cookieNameLabel: 'Name',
|
|
151
|
+
cookieProviderLabel: 'Provider',
|
|
152
|
+
cookiePurposeLabel: 'Purpose',
|
|
153
|
+
cookieExpiryLabel: 'Expiry',
|
|
154
|
+
cookieTypeLabel: 'Type',
|
|
155
|
+
noCookies: 'No cookies to display.',
|
|
156
|
+
privacyPolicyLabel: 'Privacy Policy',
|
|
157
|
+
poweredBy: '',
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Link to the site's privacy policy. Displayed in the banner if set.
|
|
162
|
+
*/
|
|
163
|
+
privacyPolicyUrl: '',
|
|
164
|
+
|
|
165
|
+
// ─── Theme / Appearance ──────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
theme: {
|
|
168
|
+
primary: '#0e6b4e',
|
|
169
|
+
primaryHover: '#0a5a40',
|
|
170
|
+
primaryText: '#ffffff',
|
|
171
|
+
background: '#ffffff',
|
|
172
|
+
text: '#333333',
|
|
173
|
+
textSecondary: '#666666',
|
|
174
|
+
border: '#e0e0e0',
|
|
175
|
+
overlay: 'rgba(0, 0, 0, 0.55)',
|
|
176
|
+
toggleOn: '#0e6b4e',
|
|
177
|
+
toggleOff: '#cccccc',
|
|
178
|
+
toggleKnob: '#ffffff',
|
|
179
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
|
180
|
+
fontSize: '14px',
|
|
181
|
+
borderRadius: '8px',
|
|
182
|
+
zIndex: 2147483645,
|
|
183
|
+
/** Max width for bar and popup layouts */
|
|
184
|
+
maxWidth: '1140px',
|
|
185
|
+
/** Width for popup layout */
|
|
186
|
+
popupWidth: '400px',
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// ─── Callbacks ───────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
/** Called when consent is first given or updated. */
|
|
192
|
+
onConsent: null,
|
|
193
|
+
/** Called when consent is revoked. */
|
|
194
|
+
onRevoke: null,
|
|
195
|
+
/** Called when a specific category is accepted. Receives category key. */
|
|
196
|
+
onAccept: null,
|
|
197
|
+
/** Called when a specific category is rejected. Receives category key. */
|
|
198
|
+
onReject: null,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Cookie detail schema for reference (used in documentation, not enforced at runtime):
|
|
203
|
+
* @typedef {Object} CookieDetail
|
|
204
|
+
* @property {string} name - Cookie name or pattern
|
|
205
|
+
* @property {string} provider - Domain/company that sets this cookie
|
|
206
|
+
* @property {string} purpose - Human-readable purpose
|
|
207
|
+
* @property {string} expiry - Duration string (e.g. '1 year', 'Session')
|
|
208
|
+
* @property {string} type - 'HTTP Cookie' | 'localStorage' | 'sessionStorage' | 'Pixel'
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Lightweight event emitter for consent lifecycle events.
|
|
213
|
+
*/
|
|
214
|
+
function EventEmitter() {
|
|
215
|
+
this._listeners = {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
EventEmitter.prototype.on = function (event, fn) {
|
|
219
|
+
if (!this._listeners[event]) this._listeners[event] = [];
|
|
220
|
+
this._listeners[event].push(fn);
|
|
221
|
+
return this;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
EventEmitter.prototype.off = function (event, fn) {
|
|
225
|
+
if (!this._listeners[event]) return this;
|
|
226
|
+
if (!fn) {
|
|
227
|
+
delete this._listeners[event];
|
|
228
|
+
} else {
|
|
229
|
+
this._listeners[event] = this._listeners[event].filter(function (f) {
|
|
230
|
+
return f !== fn;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return this;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
EventEmitter.prototype.emit = function (event, data) {
|
|
237
|
+
if (!this._listeners[event]) return this;
|
|
238
|
+
var listeners = this._listeners[event].slice();
|
|
239
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
240
|
+
try {
|
|
241
|
+
listeners[i](data);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
console.error('[CookieConsent] Event handler error:', e);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return this;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Low-level cookie read/write/delete operations.
|
|
251
|
+
*/
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Set a cookie with the given options.
|
|
255
|
+
*/
|
|
256
|
+
function setCookie(name, value, options) {
|
|
257
|
+
options = options || {};
|
|
258
|
+
var str = encodeURIComponent(name) + '=' + encodeURIComponent(value);
|
|
259
|
+
|
|
260
|
+
if (options.expiryDays) {
|
|
261
|
+
var date = new Date();
|
|
262
|
+
date.setTime(date.getTime() + options.expiryDays * 86400000);
|
|
263
|
+
str += '; expires=' + date.toUTCString();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
str += '; path=' + (options.path || '/');
|
|
267
|
+
|
|
268
|
+
if (options.domain) {
|
|
269
|
+
str += '; domain=' + options.domain;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (options.sameSite) {
|
|
273
|
+
str += '; SameSite=' + options.sameSite;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (options.secure || options.sameSite === 'None') {
|
|
277
|
+
str += '; Secure';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
document.cookie = str;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Read a cookie by name. Returns null if not found.
|
|
285
|
+
*/
|
|
286
|
+
function getCookie(name) {
|
|
287
|
+
var nameEq = encodeURIComponent(name) + '=';
|
|
288
|
+
var cookies = document.cookie.split(';');
|
|
289
|
+
for (var i = 0; i < cookies.length; i++) {
|
|
290
|
+
var c = cookies[i].trim();
|
|
291
|
+
if (c.indexOf(nameEq) === 0) {
|
|
292
|
+
return decodeURIComponent(c.substring(nameEq.length));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Delete a cookie by name. Domain and path must match the original cookie.
|
|
300
|
+
*/
|
|
301
|
+
function deleteCookie(name, options) {
|
|
302
|
+
options = options || {};
|
|
303
|
+
var str = encodeURIComponent(name) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
304
|
+
str += '; path=' + (options.path || '/');
|
|
305
|
+
if (options.domain) {
|
|
306
|
+
str += '; domain=' + options.domain;
|
|
307
|
+
}
|
|
308
|
+
document.cookie = str;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Parse the consent data stored in the consent cookie.
|
|
313
|
+
* Returns null if the cookie doesn't exist or is malformed.
|
|
314
|
+
*/
|
|
315
|
+
function readConsentCookie(cookieName) {
|
|
316
|
+
var raw = getCookie(cookieName);
|
|
317
|
+
if (!raw) return null;
|
|
318
|
+
try {
|
|
319
|
+
return JSON.parse(raw);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Write consent data to the consent cookie.
|
|
327
|
+
*/
|
|
328
|
+
function writeConsentCookie(cookieName, data, options) {
|
|
329
|
+
setCookie(cookieName, JSON.stringify(data), options);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generate a UUID v4 string for consent record identification.
|
|
334
|
+
*/
|
|
335
|
+
function generateId() {
|
|
336
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
337
|
+
return crypto.randomUUID();
|
|
338
|
+
}
|
|
339
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
340
|
+
var r = (Math.random() * 16) | 0;
|
|
341
|
+
var v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
342
|
+
return v.toString(16);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Generate a simple hash of a string (for config versioning).
|
|
348
|
+
*/
|
|
349
|
+
function hashString(str) {
|
|
350
|
+
var hash = 0;
|
|
351
|
+
for (var i = 0; i < str.length; i++) {
|
|
352
|
+
var char = str.charCodeAt(i);
|
|
353
|
+
hash = ((hash << 5) - hash + char) | 0;
|
|
354
|
+
}
|
|
355
|
+
return Math.abs(hash).toString(36);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Deep merge two objects. Source values overwrite target values.
|
|
360
|
+
* Arrays are replaced, not merged.
|
|
361
|
+
*/
|
|
362
|
+
function deepMerge(target, source) {
|
|
363
|
+
var result = Object.assign({}, target);
|
|
364
|
+
for (var key in source) {
|
|
365
|
+
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
|
|
366
|
+
if (
|
|
367
|
+
source[key] &&
|
|
368
|
+
typeof source[key] === 'object' &&
|
|
369
|
+
!Array.isArray(source[key]) &&
|
|
370
|
+
target[key] &&
|
|
371
|
+
typeof target[key] === 'object' &&
|
|
372
|
+
!Array.isArray(target[key])
|
|
373
|
+
) {
|
|
374
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
375
|
+
} else {
|
|
376
|
+
result[key] = source[key];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Query a DOM element safely.
|
|
384
|
+
*/
|
|
385
|
+
function $(selector, parent) {
|
|
386
|
+
return (document).querySelector(selector);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Query all DOM elements safely.
|
|
391
|
+
*/
|
|
392
|
+
function $$(selector, parent) {
|
|
393
|
+
return Array.prototype.slice.call((document).querySelectorAll(selector));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Create a DOM element with attributes and children.
|
|
398
|
+
*/
|
|
399
|
+
function createElement(tag, attrs, children) {
|
|
400
|
+
var el = document.createElement(tag);
|
|
401
|
+
if (attrs) {
|
|
402
|
+
for (var key in attrs) {
|
|
403
|
+
if (key === 'className') {
|
|
404
|
+
el.className = attrs[key];
|
|
405
|
+
} else if (key === 'innerHTML') {
|
|
406
|
+
el.innerHTML = attrs[key];
|
|
407
|
+
} else if (key === 'textContent') {
|
|
408
|
+
el.textContent = attrs[key];
|
|
409
|
+
} else if (key.indexOf('on') === 0) {
|
|
410
|
+
el.addEventListener(key.substring(2).toLowerCase(), attrs[key]);
|
|
411
|
+
} else {
|
|
412
|
+
el.setAttribute(key, attrs[key]);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (children) {
|
|
417
|
+
if (typeof children === 'string') {
|
|
418
|
+
el.textContent = children;
|
|
419
|
+
} else if (Array.isArray(children)) {
|
|
420
|
+
children.forEach(function (child) {
|
|
421
|
+
if (child) el.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
el.appendChild(children);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return el;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Escape HTML special characters to prevent XSS.
|
|
432
|
+
*/
|
|
433
|
+
function escapeHtml(str) {
|
|
434
|
+
if (!str) return '';
|
|
435
|
+
var div = document.createElement('div');
|
|
436
|
+
div.appendChild(document.createTextNode(str));
|
|
437
|
+
return div.innerHTML;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Trap focus within a container element (for modal accessibility).
|
|
442
|
+
* Returns a cleanup function.
|
|
443
|
+
*/
|
|
444
|
+
function trapFocus(container) {
|
|
445
|
+
var focusable = container.querySelectorAll(
|
|
446
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
447
|
+
);
|
|
448
|
+
var first = focusable[0];
|
|
449
|
+
var last = focusable[focusable.length - 1];
|
|
450
|
+
|
|
451
|
+
function handler(e) {
|
|
452
|
+
if (e.key !== 'Tab') return;
|
|
453
|
+
if (e.shiftKey) {
|
|
454
|
+
if (document.activeElement === first) {
|
|
455
|
+
e.preventDefault();
|
|
456
|
+
last.focus();
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
if (document.activeElement === last) {
|
|
460
|
+
e.preventDefault();
|
|
461
|
+
first.focus();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
container.addEventListener('keydown', handler);
|
|
467
|
+
if (first) first.focus();
|
|
468
|
+
|
|
469
|
+
return function () {
|
|
470
|
+
container.removeEventListener('keydown', handler);
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* ConsentManager handles all consent state: loading, saving, validating,
|
|
476
|
+
* expiry checking, config versioning, and webhook dispatch.
|
|
477
|
+
*/
|
|
478
|
+
function ConsentManager(config, emitter) {
|
|
479
|
+
this.config = config;
|
|
480
|
+
this.emitter = emitter;
|
|
481
|
+
this.consent = null;
|
|
482
|
+
this._configHash = this._computeConfigHash();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Compute a hash of the cookie categories config.
|
|
487
|
+
* Used to detect when the site owner changes their cookie configuration,
|
|
488
|
+
* which requires re-consent under GDPR.
|
|
489
|
+
*/
|
|
490
|
+
ConsentManager.prototype._computeConfigHash = function () {
|
|
491
|
+
var cats = this.config.categories;
|
|
492
|
+
var keys = Object.keys(cats).sort();
|
|
493
|
+
var repr = keys
|
|
494
|
+
.map(function (k) {
|
|
495
|
+
var c = cats[k];
|
|
496
|
+
var cookieNames = (c.cookies || []).map(function (ck) { return ck.name; }).sort().join(',');
|
|
497
|
+
return k + ':' + cookieNames;
|
|
498
|
+
})
|
|
499
|
+
.join('|');
|
|
500
|
+
return hashString(repr);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Load existing consent from the cookie. Returns the consent object or null.
|
|
505
|
+
*/
|
|
506
|
+
ConsentManager.prototype.load = function () {
|
|
507
|
+
this.consent = readConsentCookie(this.config.cookie.name);
|
|
508
|
+
return this.consent;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Check if the user has a valid (non-expired, current-version) consent.
|
|
513
|
+
*/
|
|
514
|
+
ConsentManager.prototype.hasValid = function () {
|
|
515
|
+
if (!this.consent) return false;
|
|
516
|
+
|
|
517
|
+
// Check expiry
|
|
518
|
+
if (this.consent.expires) {
|
|
519
|
+
if (new Date(this.consent.expires) <= new Date()) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Check config version change
|
|
525
|
+
if (this.config.reconsentOnChange && this.consent.configHash !== this._configHash) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return true;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Save consent for the given categories map.
|
|
534
|
+
* @param {Object} categories - e.g. { necessary: true, analytics: false, ... }
|
|
535
|
+
*/
|
|
536
|
+
ConsentManager.prototype.save = function (categories) {
|
|
537
|
+
var now = new Date();
|
|
538
|
+
var expires = new Date(now.getTime() + this.config.cookie.expiryDays * 86400000);
|
|
539
|
+
|
|
540
|
+
// Necessary is always true
|
|
541
|
+
categories.necessary = true;
|
|
542
|
+
|
|
543
|
+
this.consent = {
|
|
544
|
+
id: generateId(),
|
|
545
|
+
timestamp: now.toISOString(),
|
|
546
|
+
categories: categories,
|
|
547
|
+
configHash: this._configHash,
|
|
548
|
+
expires: expires.toISOString(),
|
|
549
|
+
url: window.location.href,
|
|
550
|
+
userAgent: navigator.userAgent,
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
writeConsentCookie(this.config.cookie.name, this.consent, {
|
|
554
|
+
expiryDays: this.config.cookie.expiryDays,
|
|
555
|
+
domain: this.config.cookie.domain,
|
|
556
|
+
path: this.config.cookie.path,
|
|
557
|
+
sameSite: this.config.cookie.sameSite,
|
|
558
|
+
secure: this.config.cookie.secure,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
this.emitter.emit('consent:given', this.consent);
|
|
562
|
+
|
|
563
|
+
// Fire per-category events
|
|
564
|
+
var cfg = this.config.categories;
|
|
565
|
+
for (var key in categories) {
|
|
566
|
+
if (!Object.prototype.hasOwnProperty.call(categories, key)) continue;
|
|
567
|
+
if (cfg[key] && cfg[key].readOnly) continue;
|
|
568
|
+
if (categories[key]) {
|
|
569
|
+
this.emitter.emit('category:accepted', key);
|
|
570
|
+
if (typeof this.config.onAccept === 'function') this.config.onAccept(key);
|
|
571
|
+
} else {
|
|
572
|
+
this.emitter.emit('category:rejected', key);
|
|
573
|
+
if (typeof this.config.onReject === 'function') this.config.onReject(key);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (typeof this.config.onConsent === 'function') {
|
|
578
|
+
this.config.onConsent(this.consent);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Send webhook
|
|
582
|
+
this._sendWebhook(this.consent);
|
|
583
|
+
|
|
584
|
+
return this.consent;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Revoke all consent and delete the consent cookie.
|
|
589
|
+
*/
|
|
590
|
+
ConsentManager.prototype.revoke = function () {
|
|
591
|
+
var prev = this.consent;
|
|
592
|
+
this.consent = null;
|
|
593
|
+
deleteCookie(this.config.cookie.name, {
|
|
594
|
+
domain: this.config.cookie.domain,
|
|
595
|
+
path: this.config.cookie.path,
|
|
596
|
+
});
|
|
597
|
+
this.emitter.emit('consent:revoked', prev);
|
|
598
|
+
if (typeof this.config.onRevoke === 'function') {
|
|
599
|
+
this.config.onRevoke(prev);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Get the current consent state. Returns null if no consent.
|
|
605
|
+
*/
|
|
606
|
+
ConsentManager.prototype.get = function () {
|
|
607
|
+
return this.consent;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Check if a specific category is consented.
|
|
612
|
+
*/
|
|
613
|
+
ConsentManager.prototype.hasCategory = function (category) {
|
|
614
|
+
if (!this.consent || !this.consent.categories) return false;
|
|
615
|
+
return !!this.consent.categories[category];
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Build an "accept all" categories map.
|
|
620
|
+
*/
|
|
621
|
+
ConsentManager.prototype.acceptAllMap = function () {
|
|
622
|
+
var map = {};
|
|
623
|
+
for (var key in this.config.categories) {
|
|
624
|
+
if (Object.prototype.hasOwnProperty.call(this.config.categories, key)) {
|
|
625
|
+
map[key] = true;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return map;
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Build a "reject all" categories map (only necessary = true).
|
|
633
|
+
*/
|
|
634
|
+
ConsentManager.prototype.rejectAllMap = function () {
|
|
635
|
+
var map = {};
|
|
636
|
+
for (var key in this.config.categories) {
|
|
637
|
+
if (Object.prototype.hasOwnProperty.call(this.config.categories, key)) {
|
|
638
|
+
map[key] = !!this.config.categories[key].readOnly;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return map;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Send consent record to the configured webhook endpoint.
|
|
646
|
+
*/
|
|
647
|
+
ConsentManager.prototype._sendWebhook = function (record) {
|
|
648
|
+
if (!this.config.webhook || !this.config.webhook.url) return;
|
|
649
|
+
|
|
650
|
+
var payload = {
|
|
651
|
+
consentId: record.id,
|
|
652
|
+
timestamp: record.timestamp,
|
|
653
|
+
categories: record.categories,
|
|
654
|
+
url: record.url,
|
|
655
|
+
userAgent: record.userAgent,
|
|
656
|
+
configHash: record.configHash,
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
var headers = Object.assign({ 'Content-Type': 'application/json' }, this.config.webhook.headers || {});
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
var xhr = new XMLHttpRequest();
|
|
663
|
+
xhr.open('POST', this.config.webhook.url, true);
|
|
664
|
+
for (var h in headers) {
|
|
665
|
+
if (Object.prototype.hasOwnProperty.call(headers, h)) {
|
|
666
|
+
xhr.setRequestHeader(h, headers[h]);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
xhr.send(JSON.stringify(payload));
|
|
670
|
+
} catch (e) {
|
|
671
|
+
console.warn('[CookieConsent] Webhook failed:', e);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* ScriptBlocker manages the activation and deactivation of page elements
|
|
677
|
+
* (scripts, iframes, images, etc.) based on consent categories.
|
|
678
|
+
*
|
|
679
|
+
* Usage in HTML:
|
|
680
|
+
* <script type="text/plain" data-cookiecategory="analytics" src="..."></script>
|
|
681
|
+
* <iframe data-cookiecategory="marketing" data-src="https://..." src="about:blank"></iframe>
|
|
682
|
+
* <img data-cookiecategory="analytics" data-src="https://pixel.example.com/t.gif" />
|
|
683
|
+
*/
|
|
684
|
+
function ScriptBlocker(emitter) {
|
|
685
|
+
this.emitter = emitter;
|
|
686
|
+
this._activated = {};
|
|
687
|
+
this._observer = null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Activate all elements belonging to the given categories.
|
|
692
|
+
* @param {Object} categories - e.g. { analytics: true, marketing: false }
|
|
693
|
+
*/
|
|
694
|
+
ScriptBlocker.prototype.activate = function (categories) {
|
|
695
|
+
for (var cat in categories) {
|
|
696
|
+
if (!Object.prototype.hasOwnProperty.call(categories, cat)) continue;
|
|
697
|
+
if (categories[cat] && !this._activated[cat]) {
|
|
698
|
+
this._activateCategory(cat);
|
|
699
|
+
this._activated[cat] = true;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Activate all blocked elements for a specific category.
|
|
706
|
+
*/
|
|
707
|
+
ScriptBlocker.prototype._activateCategory = function (category) {
|
|
708
|
+
var selector = '[data-cookiecategory="' + category + '"]';
|
|
709
|
+
var elements = $$(selector);
|
|
710
|
+
|
|
711
|
+
for (var i = 0; i < elements.length; i++) {
|
|
712
|
+
this._activateElement(elements[i]);
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Activate a single blocked element.
|
|
718
|
+
*/
|
|
719
|
+
ScriptBlocker.prototype._activateElement = function (el) {
|
|
720
|
+
var tag = el.tagName.toLowerCase();
|
|
721
|
+
|
|
722
|
+
if (tag === 'script') {
|
|
723
|
+
this._activateScript(el);
|
|
724
|
+
} else if (el.hasAttribute('data-src')) {
|
|
725
|
+
// For iframes, images, and any element with data-src
|
|
726
|
+
el.setAttribute('src', el.getAttribute('data-src'));
|
|
727
|
+
el.removeAttribute('data-src');
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Activate a blocked script element.
|
|
733
|
+
* Scripts with type="text/plain" need to be cloned because browsers
|
|
734
|
+
* won't execute them even if you change the type back.
|
|
735
|
+
*/
|
|
736
|
+
ScriptBlocker.prototype._activateScript = function (el) {
|
|
737
|
+
var clone = document.createElement('script');
|
|
738
|
+
|
|
739
|
+
// Copy all attributes
|
|
740
|
+
for (var i = 0; i < el.attributes.length; i++) {
|
|
741
|
+
var attr = el.attributes[i];
|
|
742
|
+
if (attr.name === 'type') {
|
|
743
|
+
clone.setAttribute('type', 'text/javascript');
|
|
744
|
+
} else {
|
|
745
|
+
clone.setAttribute(attr.name, attr.value);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// If no type was set on original (shouldn't happen in normal flow)
|
|
750
|
+
if (!clone.hasAttribute('type')) {
|
|
751
|
+
clone.setAttribute('type', 'text/javascript');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Copy inline script content
|
|
755
|
+
if (el.textContent) {
|
|
756
|
+
clone.textContent = el.textContent;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Replace original with clone
|
|
760
|
+
if (el.parentNode) {
|
|
761
|
+
el.parentNode.replaceChild(clone, el);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Observe DOM for dynamically added elements with data-cookiecategory.
|
|
767
|
+
* Only activates elements whose categories have already been consented to.
|
|
768
|
+
* @param {Object} categories - current consent categories
|
|
769
|
+
*/
|
|
770
|
+
ScriptBlocker.prototype.observe = function (categories) {
|
|
771
|
+
var self = this;
|
|
772
|
+
|
|
773
|
+
if (this._observer) this._observer.disconnect();
|
|
774
|
+
|
|
775
|
+
if (typeof MutationObserver === 'undefined') return;
|
|
776
|
+
|
|
777
|
+
this._observer = new MutationObserver(function (mutations) {
|
|
778
|
+
for (var i = 0; i < mutations.length; i++) {
|
|
779
|
+
var nodes = mutations[i].addedNodes;
|
|
780
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
781
|
+
var node = nodes[j];
|
|
782
|
+
if (node.nodeType !== 1) continue;
|
|
783
|
+
var cat = node.getAttribute && node.getAttribute('data-cookiecategory');
|
|
784
|
+
if (cat && categories[cat]) {
|
|
785
|
+
self._activateElement(node);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
this._observer.observe(document.documentElement, {
|
|
792
|
+
childList: true,
|
|
793
|
+
subtree: true,
|
|
794
|
+
});
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Stop observing DOM mutations.
|
|
799
|
+
*/
|
|
800
|
+
ScriptBlocker.prototype.destroy = function () {
|
|
801
|
+
if (this._observer) {
|
|
802
|
+
this._observer.disconnect();
|
|
803
|
+
this._observer = null;
|
|
804
|
+
}
|
|
805
|
+
this._activated = {};
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* SVG chevron icon for accessibility-friendly dropdowns.
|
|
810
|
+
*/
|
|
811
|
+
var CHEVRON_SVG =
|
|
812
|
+
'<svg class="cc-category__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Banner handles the initial consent banner UI.
|
|
816
|
+
* Supports three layout modes: bar, modal, popup.
|
|
817
|
+
*/
|
|
818
|
+
function Banner(config, emitter) {
|
|
819
|
+
this.config = config;
|
|
820
|
+
this.emitter = emitter;
|
|
821
|
+
this.el = null;
|
|
822
|
+
this.overlayEl = null;
|
|
823
|
+
this._visible = false;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Build and inject the banner DOM.
|
|
828
|
+
*/
|
|
829
|
+
Banner.prototype.render = function (container) {
|
|
830
|
+
var cfg = this.config;
|
|
831
|
+
var texts = cfg.texts;
|
|
832
|
+
var layout = cfg.layout;
|
|
833
|
+
var position = cfg.position || 'bottom';
|
|
834
|
+
|
|
835
|
+
// Create overlay (for modal/popup or when forceOverlay)
|
|
836
|
+
this.overlayEl = createElement('div', { className: 'cc-overlay', role: 'presentation' });
|
|
837
|
+
|
|
838
|
+
// Build layout classes
|
|
839
|
+
var bannerClass = 'cc-banner';
|
|
840
|
+
if (layout === 'bar') {
|
|
841
|
+
bannerClass += ' cc-banner--bar cc-banner--bar-' + position;
|
|
842
|
+
} else if (layout === 'modal') {
|
|
843
|
+
bannerClass += ' cc-banner--modal';
|
|
844
|
+
} else if (layout === 'popup') {
|
|
845
|
+
bannerClass += ' cc-banner--popup cc-banner--popup-' + position;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Build banner
|
|
849
|
+
this.el = createElement('div', {
|
|
850
|
+
className: bannerClass,
|
|
851
|
+
role: 'dialog',
|
|
852
|
+
'aria-modal': layout === 'modal' ? 'true' : 'false',
|
|
853
|
+
'aria-label': texts.bannerTitle,
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
var inner = createElement('div', { className: 'cc-banner__inner' });
|
|
857
|
+
|
|
858
|
+
// Title
|
|
859
|
+
inner.appendChild(createElement('div', { className: 'cc-banner__title' }, texts.bannerTitle));
|
|
860
|
+
|
|
861
|
+
// Description with optional privacy policy link
|
|
862
|
+
var descText = escapeHtml(texts.bannerDescription);
|
|
863
|
+
if (cfg.privacyPolicyUrl) {
|
|
864
|
+
descText +=
|
|
865
|
+
' <a href="' +
|
|
866
|
+
escapeHtml(cfg.privacyPolicyUrl) +
|
|
867
|
+
'" target="_blank" rel="noopener noreferrer">' +
|
|
868
|
+
escapeHtml(texts.privacyPolicyLabel) +
|
|
869
|
+
'</a>';
|
|
870
|
+
}
|
|
871
|
+
inner.appendChild(createElement('div', { className: 'cc-banner__text', innerHTML: descText }));
|
|
872
|
+
|
|
873
|
+
// Action buttons
|
|
874
|
+
var actions = createElement('div', { className: 'cc-banner__actions' });
|
|
875
|
+
var self = this;
|
|
876
|
+
|
|
877
|
+
// Accept All button
|
|
878
|
+
var acceptBtn = createElement(
|
|
879
|
+
'button',
|
|
880
|
+
{
|
|
881
|
+
className: 'cc-btn cc-btn--primary',
|
|
882
|
+
type: 'button',
|
|
883
|
+
'aria-label': texts.acceptAll,
|
|
884
|
+
onClick: function () {
|
|
885
|
+
self.emitter.emit('banner:acceptAll');
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
texts.acceptAll
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
// Reject All button (GDPR requires equal prominence)
|
|
892
|
+
var rejectBtn = createElement(
|
|
893
|
+
'button',
|
|
894
|
+
{
|
|
895
|
+
className: 'cc-btn cc-btn--secondary',
|
|
896
|
+
type: 'button',
|
|
897
|
+
'aria-label': texts.rejectAll,
|
|
898
|
+
onClick: function () {
|
|
899
|
+
self.emitter.emit('banner:rejectAll');
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
texts.rejectAll
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
// Settings button
|
|
906
|
+
var settingsBtn = createElement(
|
|
907
|
+
'button',
|
|
908
|
+
{
|
|
909
|
+
className: 'cc-btn cc-btn--link',
|
|
910
|
+
type: 'button',
|
|
911
|
+
'aria-label': texts.settings,
|
|
912
|
+
onClick: function () {
|
|
913
|
+
self.emitter.emit('banner:settings');
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
texts.settings
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
actions.appendChild(acceptBtn);
|
|
920
|
+
actions.appendChild(rejectBtn);
|
|
921
|
+
actions.appendChild(settingsBtn);
|
|
922
|
+
inner.appendChild(actions);
|
|
923
|
+
this.el.appendChild(inner);
|
|
924
|
+
|
|
925
|
+
// Powered by (optional)
|
|
926
|
+
if (texts.poweredBy) {
|
|
927
|
+
var powered = createElement('div', {
|
|
928
|
+
className: 'cc-banner__powered',
|
|
929
|
+
innerHTML: escapeHtml(texts.poweredBy),
|
|
930
|
+
});
|
|
931
|
+
powered.style.cssText =
|
|
932
|
+
'text-align:center;font-size:0.75em;padding:0 24px 12px;opacity:0.5;';
|
|
933
|
+
this.el.appendChild(powered);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
container.appendChild(this.overlayEl);
|
|
937
|
+
container.appendChild(this.el);
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Show the banner with animation.
|
|
942
|
+
*/
|
|
943
|
+
Banner.prototype.show = function () {
|
|
944
|
+
if (!this.el || this._visible) return;
|
|
945
|
+
this._visible = true;
|
|
946
|
+
var self = this;
|
|
947
|
+
// Force reflow for animation
|
|
948
|
+
void this.el.offsetHeight;
|
|
949
|
+
requestAnimationFrame(function () {
|
|
950
|
+
self.el.classList.add('cc-banner--visible');
|
|
951
|
+
if (
|
|
952
|
+
self.config.layout === 'modal' ||
|
|
953
|
+
self.config.layout === 'popup' ||
|
|
954
|
+
self.config.forceOverlay
|
|
955
|
+
) {
|
|
956
|
+
self.overlayEl.classList.add('cc-overlay--visible');
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
this.emitter.emit('banner:shown');
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Hide the banner with animation.
|
|
964
|
+
*/
|
|
965
|
+
Banner.prototype.hide = function () {
|
|
966
|
+
if (!this.el || !this._visible) return;
|
|
967
|
+
this._visible = false;
|
|
968
|
+
this.el.classList.remove('cc-banner--visible');
|
|
969
|
+
this.overlayEl.classList.remove('cc-overlay--visible');
|
|
970
|
+
this.emitter.emit('banner:hidden');
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Check if the banner is currently visible.
|
|
975
|
+
*/
|
|
976
|
+
Banner.prototype.isVisible = function () {
|
|
977
|
+
return this._visible;
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Remove banner from DOM and clean up.
|
|
982
|
+
*/
|
|
983
|
+
Banner.prototype.destroy = function () {
|
|
984
|
+
if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
|
|
985
|
+
if (this.overlayEl && this.overlayEl.parentNode)
|
|
986
|
+
this.overlayEl.parentNode.removeChild(this.overlayEl);
|
|
987
|
+
this.el = null;
|
|
988
|
+
this.overlayEl = null;
|
|
989
|
+
this._visible = false;
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Preferences modal with category accordions and per-cookie detail tables.
|
|
994
|
+
*/
|
|
995
|
+
function Preferences(config, emitter) {
|
|
996
|
+
this.config = config;
|
|
997
|
+
this.emitter = emitter;
|
|
998
|
+
this.el = null;
|
|
999
|
+
this._visible = false;
|
|
1000
|
+
this._releaseFocus = null;
|
|
1001
|
+
this._toggles = {};
|
|
1002
|
+
this._previousFocus = null;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Build and inject the preferences modal DOM.
|
|
1007
|
+
*/
|
|
1008
|
+
Preferences.prototype.render = function (container, currentConsent) {
|
|
1009
|
+
var cfg = this.config;
|
|
1010
|
+
var texts = cfg.texts;
|
|
1011
|
+
var self = this;
|
|
1012
|
+
|
|
1013
|
+
// Root wrapper
|
|
1014
|
+
this.el = createElement('div', {
|
|
1015
|
+
className: 'cc-preferences',
|
|
1016
|
+
role: 'dialog',
|
|
1017
|
+
'aria-modal': 'true',
|
|
1018
|
+
'aria-label': texts.preferencesTitle,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
// Overlay backdrop
|
|
1022
|
+
var overlay = createElement('div', { className: 'cc-preferences__overlay' });
|
|
1023
|
+
this.el.appendChild(overlay);
|
|
1024
|
+
|
|
1025
|
+
// Dialog container
|
|
1026
|
+
var dialog = createElement('div', { className: 'cc-preferences__dialog' });
|
|
1027
|
+
|
|
1028
|
+
// Header
|
|
1029
|
+
var header = createElement('div', { className: 'cc-preferences__header' });
|
|
1030
|
+
header.appendChild(createElement('div', { className: 'cc-preferences__title' }, texts.preferencesTitle));
|
|
1031
|
+
header.appendChild(
|
|
1032
|
+
createElement('div', { className: 'cc-preferences__description' }, texts.preferencesDescription)
|
|
1033
|
+
);
|
|
1034
|
+
dialog.appendChild(header);
|
|
1035
|
+
|
|
1036
|
+
// Body (scrollable)
|
|
1037
|
+
var body = createElement('div', { className: 'cc-preferences__body' });
|
|
1038
|
+
this._renderCategories(body, currentConsent);
|
|
1039
|
+
dialog.appendChild(body);
|
|
1040
|
+
|
|
1041
|
+
// Footer with actions
|
|
1042
|
+
var footer = createElement('div', { className: 'cc-preferences__footer' });
|
|
1043
|
+
|
|
1044
|
+
var rejectAllBtn = createElement(
|
|
1045
|
+
'button',
|
|
1046
|
+
{
|
|
1047
|
+
className: 'cc-btn cc-btn--secondary',
|
|
1048
|
+
type: 'button',
|
|
1049
|
+
onClick: function () {
|
|
1050
|
+
self.emitter.emit('preferences:rejectAll');
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
texts.rejectAllPreferences
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
var acceptAllBtn = createElement(
|
|
1057
|
+
'button',
|
|
1058
|
+
{
|
|
1059
|
+
className: 'cc-btn cc-btn--secondary',
|
|
1060
|
+
type: 'button',
|
|
1061
|
+
onClick: function () {
|
|
1062
|
+
self.emitter.emit('preferences:acceptAll');
|
|
1063
|
+
},
|
|
1064
|
+
},
|
|
1065
|
+
texts.acceptAllPreferences
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1068
|
+
var saveBtn = createElement(
|
|
1069
|
+
'button',
|
|
1070
|
+
{
|
|
1071
|
+
className: 'cc-btn cc-btn--primary',
|
|
1072
|
+
type: 'button',
|
|
1073
|
+
onClick: function () {
|
|
1074
|
+
self.emitter.emit('preferences:save', self.getSelected());
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
1077
|
+
texts.save
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
footer.appendChild(rejectAllBtn);
|
|
1081
|
+
footer.appendChild(acceptAllBtn);
|
|
1082
|
+
footer.appendChild(saveBtn);
|
|
1083
|
+
dialog.appendChild(footer);
|
|
1084
|
+
|
|
1085
|
+
this.el.appendChild(dialog);
|
|
1086
|
+
container.appendChild(this.el);
|
|
1087
|
+
|
|
1088
|
+
// Close on Escape
|
|
1089
|
+
this._escHandler = function (e) {
|
|
1090
|
+
if (e.key === 'Escape' && self._visible) {
|
|
1091
|
+
self.emitter.emit('preferences:close');
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
document.addEventListener('keydown', this._escHandler);
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Render each cookie category as an accordion section.
|
|
1099
|
+
*/
|
|
1100
|
+
Preferences.prototype._renderCategories = function (body, currentConsent) {
|
|
1101
|
+
var cats = this.config.categories;
|
|
1102
|
+
var texts = this.config.texts;
|
|
1103
|
+
var consentCats = (currentConsent && currentConsent.categories) || {};
|
|
1104
|
+
|
|
1105
|
+
for (var key in cats) {
|
|
1106
|
+
if (!Object.prototype.hasOwnProperty.call(cats, key)) continue;
|
|
1107
|
+
var cat = cats[key];
|
|
1108
|
+
|
|
1109
|
+
// Determine initial toggle state:
|
|
1110
|
+
// If readOnly → always true
|
|
1111
|
+
// If user has existing consent → use that
|
|
1112
|
+
// Otherwise → use default `enabled` value (which should be false for non-necessary per GDPR)
|
|
1113
|
+
var isChecked = cat.readOnly ? true : consentCats[key] != null ? !!consentCats[key] : !!cat.enabled;
|
|
1114
|
+
|
|
1115
|
+
var section = createElement('div', { className: 'cc-category' });
|
|
1116
|
+
|
|
1117
|
+
// Header (clickable for expand/collapse)
|
|
1118
|
+
var headerBtn = createElement('button', {
|
|
1119
|
+
className: 'cc-category__header',
|
|
1120
|
+
type: 'button',
|
|
1121
|
+
'aria-expanded': 'false',
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
var headerLeft = createElement('div', { className: 'cc-category__header-left' });
|
|
1125
|
+
headerLeft.appendChild(createElement('span', { innerHTML: CHEVRON_SVG }));
|
|
1126
|
+
headerLeft.appendChild(createElement('span', { className: 'cc-category__title' }, escapeHtml(cat.title)));
|
|
1127
|
+
|
|
1128
|
+
var cookieCount = (cat.cookies || []).length;
|
|
1129
|
+
if (cookieCount > 0) {
|
|
1130
|
+
headerLeft.appendChild(
|
|
1131
|
+
createElement('span', { className: 'cc-category__count' }, '(' + cookieCount + ')')
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
headerBtn.appendChild(headerLeft);
|
|
1136
|
+
|
|
1137
|
+
// Toggle or "Always Active" label
|
|
1138
|
+
if (cat.readOnly) {
|
|
1139
|
+
headerBtn.appendChild(
|
|
1140
|
+
createElement('span', { className: 'cc-category__always-active' }, texts.alwaysActive)
|
|
1141
|
+
);
|
|
1142
|
+
} else {
|
|
1143
|
+
var toggle = this._createToggle(key, isChecked);
|
|
1144
|
+
headerBtn.appendChild(toggle);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
section.appendChild(headerBtn);
|
|
1148
|
+
|
|
1149
|
+
// Body (collapsible)
|
|
1150
|
+
var catBody = createElement('div', { className: 'cc-category__body' });
|
|
1151
|
+
catBody.appendChild(
|
|
1152
|
+
createElement('div', { className: 'cc-category__description' }, escapeHtml(cat.description))
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
// Cookie details
|
|
1156
|
+
this._renderCookieTable(catBody, cat.cookies || [], texts);
|
|
1157
|
+
|
|
1158
|
+
section.appendChild(catBody);
|
|
1159
|
+
|
|
1160
|
+
// Wire up accordion toggle
|
|
1161
|
+
(function (sec, btn) {
|
|
1162
|
+
btn.addEventListener('click', function (e) {
|
|
1163
|
+
// Don't toggle accordion if clicking the toggle switch itself
|
|
1164
|
+
if (e.target.closest('.cc-toggle')) return;
|
|
1165
|
+
var expanded = sec.classList.toggle('cc-category--expanded');
|
|
1166
|
+
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|
1167
|
+
});
|
|
1168
|
+
})(section, headerBtn);
|
|
1169
|
+
|
|
1170
|
+
body.appendChild(section);
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Create a toggle switch element.
|
|
1176
|
+
*/
|
|
1177
|
+
Preferences.prototype._createToggle = function (categoryKey, checked) {
|
|
1178
|
+
var toggle = createElement('label', { className: 'cc-toggle' });
|
|
1179
|
+
var input = createElement('input', {
|
|
1180
|
+
className: 'cc-toggle__input',
|
|
1181
|
+
type: 'checkbox',
|
|
1182
|
+
'aria-label': this.config.categories[categoryKey].title,
|
|
1183
|
+
});
|
|
1184
|
+
input.checked = checked;
|
|
1185
|
+
toggle.appendChild(input);
|
|
1186
|
+
toggle.appendChild(createElement('span', { className: 'cc-toggle__slider' }));
|
|
1187
|
+
|
|
1188
|
+
// Prevent click on toggle from bubbling to accordion header
|
|
1189
|
+
toggle.addEventListener('click', function (e) {
|
|
1190
|
+
e.stopPropagation();
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
this._toggles[categoryKey] = input;
|
|
1194
|
+
return toggle;
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Render the cookie detail rows for a category.
|
|
1199
|
+
*/
|
|
1200
|
+
Preferences.prototype._renderCookieTable = function (parent, cookies, texts) {
|
|
1201
|
+
var wrapper = createElement('div', { className: 'cc-cookies' });
|
|
1202
|
+
|
|
1203
|
+
if (!cookies || cookies.length === 0) {
|
|
1204
|
+
wrapper.appendChild(createElement('div', { className: 'cc-cookies__empty' }, texts.noCookies));
|
|
1205
|
+
parent.appendChild(wrapper);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
for (var i = 0; i < cookies.length; i++) {
|
|
1210
|
+
var ck = cookies[i];
|
|
1211
|
+
var item = createElement('div', { className: 'cc-cookie-item' });
|
|
1212
|
+
|
|
1213
|
+
var fields = [
|
|
1214
|
+
{ label: texts.cookieNameLabel, value: ck.name },
|
|
1215
|
+
{ label: texts.cookieProviderLabel, value: ck.provider },
|
|
1216
|
+
{ label: texts.cookiePurposeLabel, value: ck.purpose },
|
|
1217
|
+
{ label: texts.cookieExpiryLabel, value: ck.expiry },
|
|
1218
|
+
{ label: texts.cookieTypeLabel, value: ck.type },
|
|
1219
|
+
];
|
|
1220
|
+
|
|
1221
|
+
for (var j = 0; j < fields.length; j++) {
|
|
1222
|
+
if (!fields[j].value) continue;
|
|
1223
|
+
var row = createElement('div', { className: 'cc-cookie-item__row' });
|
|
1224
|
+
row.appendChild(createElement('div', { className: 'cc-cookie-item__label' }, escapeHtml(fields[j].label)));
|
|
1225
|
+
row.appendChild(
|
|
1226
|
+
createElement('div', { className: 'cc-cookie-item__value' }, escapeHtml(fields[j].value))
|
|
1227
|
+
);
|
|
1228
|
+
item.appendChild(row);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
wrapper.appendChild(item);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
parent.appendChild(wrapper);
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Get the currently selected categories from the toggle switches.
|
|
1239
|
+
*/
|
|
1240
|
+
Preferences.prototype.getSelected = function () {
|
|
1241
|
+
var result = {};
|
|
1242
|
+
var cats = this.config.categories;
|
|
1243
|
+
for (var key in cats) {
|
|
1244
|
+
if (!Object.prototype.hasOwnProperty.call(cats, key)) continue;
|
|
1245
|
+
if (cats[key].readOnly) {
|
|
1246
|
+
result[key] = true;
|
|
1247
|
+
} else if (this._toggles[key]) {
|
|
1248
|
+
result[key] = this._toggles[key].checked;
|
|
1249
|
+
} else {
|
|
1250
|
+
result[key] = false;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return result;
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
/**
|
|
1257
|
+
* Update toggle states (used when acceptAll/rejectAll is clicked from preferences).
|
|
1258
|
+
*/
|
|
1259
|
+
Preferences.prototype.setAll = function (value) {
|
|
1260
|
+
var cats = this.config.categories;
|
|
1261
|
+
for (var key in this._toggles) {
|
|
1262
|
+
if (Object.prototype.hasOwnProperty.call(this._toggles, key)) {
|
|
1263
|
+
if (cats[key] && !cats[key].readOnly) {
|
|
1264
|
+
this._toggles[key].checked = value;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Show the preferences modal.
|
|
1272
|
+
*/
|
|
1273
|
+
Preferences.prototype.show = function () {
|
|
1274
|
+
if (!this.el || this._visible) return;
|
|
1275
|
+
this._visible = true;
|
|
1276
|
+
this._previousFocus = document.activeElement;
|
|
1277
|
+
var self = this;
|
|
1278
|
+
void this.el.offsetHeight;
|
|
1279
|
+
requestAnimationFrame(function () {
|
|
1280
|
+
self.el.classList.add('cc-preferences--visible');
|
|
1281
|
+
// Trap focus for accessibility
|
|
1282
|
+
var dialog = self.el.querySelector('.cc-preferences__dialog');
|
|
1283
|
+
if (dialog) self._releaseFocus = trapFocus(dialog);
|
|
1284
|
+
});
|
|
1285
|
+
this.emitter.emit('preferences:shown');
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Hide the preferences modal.
|
|
1290
|
+
*/
|
|
1291
|
+
Preferences.prototype.hide = function () {
|
|
1292
|
+
if (!this.el || !this._visible) return;
|
|
1293
|
+
this._visible = false;
|
|
1294
|
+
this.el.classList.remove('cc-preferences--visible');
|
|
1295
|
+
if (this._releaseFocus) {
|
|
1296
|
+
this._releaseFocus();
|
|
1297
|
+
this._releaseFocus = null;
|
|
1298
|
+
}
|
|
1299
|
+
// Restore focus to previous element
|
|
1300
|
+
if (this._previousFocus && this._previousFocus.focus) {
|
|
1301
|
+
this._previousFocus.focus();
|
|
1302
|
+
}
|
|
1303
|
+
this.emitter.emit('preferences:hidden');
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Check if visible.
|
|
1308
|
+
*/
|
|
1309
|
+
Preferences.prototype.isVisible = function () {
|
|
1310
|
+
return this._visible;
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Remove from DOM and clean up.
|
|
1315
|
+
*/
|
|
1316
|
+
Preferences.prototype.destroy = function () {
|
|
1317
|
+
if (this._escHandler) document.removeEventListener('keydown', this._escHandler);
|
|
1318
|
+
if (this._releaseFocus) this._releaseFocus();
|
|
1319
|
+
if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
|
|
1320
|
+
this.el = null;
|
|
1321
|
+
this._visible = false;
|
|
1322
|
+
this._toggles = {};
|
|
1323
|
+
};
|
|
1324
|
+
|
|
1325
|
+
var CSS = /* css */ `
|
|
1326
|
+
/* ═══════════════════════════════════════════════════════════════════════════
|
|
1327
|
+
GDPR Cookie Consent — Styles
|
|
1328
|
+
All rules are scoped under .cc-container to avoid global conflicts.
|
|
1329
|
+
═══════════════════════════════════════════════════════════════════════════ */
|
|
1330
|
+
|
|
1331
|
+
.cc-container,
|
|
1332
|
+
.cc-container *,
|
|
1333
|
+
.cc-container *::before,
|
|
1334
|
+
.cc-container *::after {
|
|
1335
|
+
box-sizing: border-box;
|
|
1336
|
+
margin: 0;
|
|
1337
|
+
padding: 0;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
.cc-container {
|
|
1341
|
+
font-family: var(--cc-font);
|
|
1342
|
+
font-size: var(--cc-font-size);
|
|
1343
|
+
line-height: 1.5;
|
|
1344
|
+
color: var(--cc-text);
|
|
1345
|
+
-webkit-font-smoothing: antialiased;
|
|
1346
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/* ─── Overlay ──────────────────────────────────────────────────────────── */
|
|
1350
|
+
|
|
1351
|
+
.cc-overlay {
|
|
1352
|
+
position: fixed;
|
|
1353
|
+
top: 0;
|
|
1354
|
+
left: 0;
|
|
1355
|
+
width: 100%;
|
|
1356
|
+
height: 100%;
|
|
1357
|
+
background: var(--cc-overlay);
|
|
1358
|
+
z-index: var(--cc-z-index);
|
|
1359
|
+
opacity: 0;
|
|
1360
|
+
visibility: hidden;
|
|
1361
|
+
transition: opacity 0.3s ease, visibility 0.3s ease;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
.cc-overlay--visible {
|
|
1365
|
+
opacity: 1;
|
|
1366
|
+
visibility: visible;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/* ─── Banner (shared) ──────────────────────────────────────────────────── */
|
|
1370
|
+
|
|
1371
|
+
.cc-banner {
|
|
1372
|
+
position: fixed;
|
|
1373
|
+
z-index: calc(var(--cc-z-index) + 1);
|
|
1374
|
+
background: var(--cc-bg);
|
|
1375
|
+
color: var(--cc-text);
|
|
1376
|
+
opacity: 0;
|
|
1377
|
+
visibility: hidden;
|
|
1378
|
+
transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
.cc-banner--visible {
|
|
1382
|
+
opacity: 1;
|
|
1383
|
+
visibility: visible;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
.cc-banner__inner {
|
|
1387
|
+
max-width: var(--cc-max-width);
|
|
1388
|
+
margin: 0 auto;
|
|
1389
|
+
padding: 20px 24px;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.cc-banner__title {
|
|
1393
|
+
font-size: 1.1em;
|
|
1394
|
+
font-weight: 700;
|
|
1395
|
+
margin-bottom: 8px;
|
|
1396
|
+
color: var(--cc-text);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
.cc-banner__text {
|
|
1400
|
+
color: var(--cc-text-secondary);
|
|
1401
|
+
margin-bottom: 16px;
|
|
1402
|
+
font-size: 0.93em;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
.cc-banner__text a {
|
|
1406
|
+
color: var(--cc-primary);
|
|
1407
|
+
text-decoration: underline;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.cc-banner__text a:hover {
|
|
1411
|
+
color: var(--cc-primary-hover);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
.cc-banner__actions {
|
|
1415
|
+
display: flex;
|
|
1416
|
+
flex-wrap: wrap;
|
|
1417
|
+
gap: 10px;
|
|
1418
|
+
align-items: center;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/* ─── Banner: Bar layout ───────────────────────────────────────────────── */
|
|
1422
|
+
|
|
1423
|
+
.cc-banner--bar {
|
|
1424
|
+
left: 0;
|
|
1425
|
+
right: 0;
|
|
1426
|
+
box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.1);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.cc-banner--bar-bottom {
|
|
1430
|
+
bottom: 0;
|
|
1431
|
+
transform: translateY(100%);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
.cc-banner--bar-bottom.cc-banner--visible {
|
|
1435
|
+
transform: translateY(0);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
.cc-banner--bar-top {
|
|
1439
|
+
top: 0;
|
|
1440
|
+
transform: translateY(-100%);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
.cc-banner--bar-top.cc-banner--visible {
|
|
1444
|
+
transform: translateY(0);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/* ─── Banner: Modal layout ─────────────────────────────────────────────── */
|
|
1448
|
+
|
|
1449
|
+
.cc-banner--modal {
|
|
1450
|
+
top: 50%;
|
|
1451
|
+
left: 50%;
|
|
1452
|
+
transform: translate(-50%, -50%) scale(0.95);
|
|
1453
|
+
max-width: 560px;
|
|
1454
|
+
width: calc(100% - 32px);
|
|
1455
|
+
border-radius: var(--cc-radius);
|
|
1456
|
+
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
.cc-banner--modal.cc-banner--visible {
|
|
1460
|
+
transform: translate(-50%, -50%) scale(1);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/* ─── Banner: Popup layout ─────────────────────────────────────────────── */
|
|
1464
|
+
|
|
1465
|
+
.cc-banner--popup {
|
|
1466
|
+
border-radius: var(--cc-radius);
|
|
1467
|
+
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.12);
|
|
1468
|
+
width: var(--cc-popup-width);
|
|
1469
|
+
max-width: calc(100% - 24px);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.cc-banner--popup-bottom-right {
|
|
1473
|
+
bottom: 16px;
|
|
1474
|
+
right: 16px;
|
|
1475
|
+
transform: translateY(20px);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
.cc-banner--popup-bottom-right.cc-banner--visible {
|
|
1479
|
+
transform: translateY(0);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.cc-banner--popup-bottom-left {
|
|
1483
|
+
bottom: 16px;
|
|
1484
|
+
left: 16px;
|
|
1485
|
+
transform: translateY(20px);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
.cc-banner--popup-bottom-left.cc-banner--visible {
|
|
1489
|
+
transform: translateY(0);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
.cc-banner--popup-top-right {
|
|
1493
|
+
top: 16px;
|
|
1494
|
+
right: 16px;
|
|
1495
|
+
transform: translateY(-20px);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
.cc-banner--popup-top-right.cc-banner--visible {
|
|
1499
|
+
transform: translateY(0);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.cc-banner--popup-top-left {
|
|
1503
|
+
top: 16px;
|
|
1504
|
+
left: 16px;
|
|
1505
|
+
transform: translateY(-20px);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
.cc-banner--popup-top-left.cc-banner--visible {
|
|
1509
|
+
transform: translateY(0);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/* ─── Buttons ──────────────────────────────────────────────────────────── */
|
|
1513
|
+
|
|
1514
|
+
.cc-btn {
|
|
1515
|
+
display: inline-flex;
|
|
1516
|
+
align-items: center;
|
|
1517
|
+
justify-content: center;
|
|
1518
|
+
padding: 10px 20px;
|
|
1519
|
+
border: none;
|
|
1520
|
+
border-radius: calc(var(--cc-radius) - 2px);
|
|
1521
|
+
font-family: var(--cc-font);
|
|
1522
|
+
font-size: 0.9em;
|
|
1523
|
+
font-weight: 600;
|
|
1524
|
+
cursor: pointer;
|
|
1525
|
+
transition: background-color 0.2s ease, transform 0.1s ease;
|
|
1526
|
+
white-space: nowrap;
|
|
1527
|
+
line-height: 1.4;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
.cc-btn:focus-visible {
|
|
1531
|
+
outline: 2px solid var(--cc-primary);
|
|
1532
|
+
outline-offset: 2px;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
.cc-btn:active {
|
|
1536
|
+
transform: scale(0.98);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
.cc-btn--primary {
|
|
1540
|
+
background: var(--cc-primary);
|
|
1541
|
+
color: var(--cc-primary-text);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
.cc-btn--primary:hover {
|
|
1545
|
+
background: var(--cc-primary-hover);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
.cc-btn--secondary {
|
|
1549
|
+
background: transparent;
|
|
1550
|
+
color: var(--cc-text);
|
|
1551
|
+
border: 1.5px solid var(--cc-border);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
.cc-btn--secondary:hover {
|
|
1555
|
+
background: var(--cc-border);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.cc-btn--link {
|
|
1559
|
+
background: transparent;
|
|
1560
|
+
color: var(--cc-primary);
|
|
1561
|
+
padding: 10px 12px;
|
|
1562
|
+
text-decoration: underline;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
.cc-btn--link:hover {
|
|
1566
|
+
color: var(--cc-primary-hover);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/* ─── Preferences Modal ────────────────────────────────────────────────── */
|
|
1570
|
+
|
|
1571
|
+
.cc-preferences {
|
|
1572
|
+
position: fixed;
|
|
1573
|
+
top: 0;
|
|
1574
|
+
left: 0;
|
|
1575
|
+
width: 100%;
|
|
1576
|
+
height: 100%;
|
|
1577
|
+
z-index: calc(var(--cc-z-index) + 2);
|
|
1578
|
+
display: flex;
|
|
1579
|
+
align-items: center;
|
|
1580
|
+
justify-content: center;
|
|
1581
|
+
opacity: 0;
|
|
1582
|
+
visibility: hidden;
|
|
1583
|
+
transition: opacity 0.3s ease, visibility 0.3s ease;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.cc-preferences--visible {
|
|
1587
|
+
opacity: 1;
|
|
1588
|
+
visibility: visible;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
.cc-preferences__overlay {
|
|
1592
|
+
position: absolute;
|
|
1593
|
+
top: 0;
|
|
1594
|
+
left: 0;
|
|
1595
|
+
width: 100%;
|
|
1596
|
+
height: 100%;
|
|
1597
|
+
background: var(--cc-overlay);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
.cc-preferences__dialog {
|
|
1601
|
+
position: relative;
|
|
1602
|
+
background: var(--cc-bg);
|
|
1603
|
+
border-radius: var(--cc-radius);
|
|
1604
|
+
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.18);
|
|
1605
|
+
max-width: 640px;
|
|
1606
|
+
width: calc(100% - 32px);
|
|
1607
|
+
max-height: calc(100vh - 48px);
|
|
1608
|
+
display: flex;
|
|
1609
|
+
flex-direction: column;
|
|
1610
|
+
transform: scale(0.95);
|
|
1611
|
+
transition: transform 0.3s ease;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
.cc-preferences--visible .cc-preferences__dialog {
|
|
1615
|
+
transform: scale(1);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
.cc-preferences__header {
|
|
1619
|
+
padding: 24px 24px 0;
|
|
1620
|
+
flex-shrink: 0;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.cc-preferences__title {
|
|
1624
|
+
font-size: 1.2em;
|
|
1625
|
+
font-weight: 700;
|
|
1626
|
+
margin-bottom: 8px;
|
|
1627
|
+
color: var(--cc-text);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
.cc-preferences__description {
|
|
1631
|
+
font-size: 0.9em;
|
|
1632
|
+
color: var(--cc-text-secondary);
|
|
1633
|
+
margin-bottom: 16px;
|
|
1634
|
+
line-height: 1.6;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.cc-preferences__body {
|
|
1638
|
+
padding: 8px 24px;
|
|
1639
|
+
overflow-y: auto;
|
|
1640
|
+
flex: 1;
|
|
1641
|
+
-webkit-overflow-scrolling: touch;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
.cc-preferences__footer {
|
|
1645
|
+
padding: 16px 24px;
|
|
1646
|
+
border-top: 1px solid var(--cc-border);
|
|
1647
|
+
display: flex;
|
|
1648
|
+
flex-wrap: wrap;
|
|
1649
|
+
gap: 10px;
|
|
1650
|
+
justify-content: flex-end;
|
|
1651
|
+
flex-shrink: 0;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/* ─── Category Accordion ───────────────────────────────────────────────── */
|
|
1655
|
+
|
|
1656
|
+
.cc-category {
|
|
1657
|
+
border: 1px solid var(--cc-border);
|
|
1658
|
+
border-radius: calc(var(--cc-radius) - 2px);
|
|
1659
|
+
margin-bottom: 10px;
|
|
1660
|
+
overflow: hidden;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
.cc-category__header {
|
|
1664
|
+
display: flex;
|
|
1665
|
+
align-items: center;
|
|
1666
|
+
justify-content: space-between;
|
|
1667
|
+
padding: 14px 16px;
|
|
1668
|
+
cursor: pointer;
|
|
1669
|
+
background: transparent;
|
|
1670
|
+
border: none;
|
|
1671
|
+
width: 100%;
|
|
1672
|
+
font-family: var(--cc-font);
|
|
1673
|
+
font-size: 0.95em;
|
|
1674
|
+
color: var(--cc-text);
|
|
1675
|
+
text-align: left;
|
|
1676
|
+
transition: background-color 0.15s ease;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
.cc-category__header:hover {
|
|
1680
|
+
background: rgba(0, 0, 0, 0.02);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
.cc-category__header:focus-visible {
|
|
1684
|
+
outline: 2px solid var(--cc-primary);
|
|
1685
|
+
outline-offset: -2px;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
.cc-category__header-left {
|
|
1689
|
+
display: flex;
|
|
1690
|
+
align-items: center;
|
|
1691
|
+
gap: 10px;
|
|
1692
|
+
flex: 1;
|
|
1693
|
+
min-width: 0;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
.cc-category__chevron {
|
|
1697
|
+
width: 18px;
|
|
1698
|
+
height: 18px;
|
|
1699
|
+
flex-shrink: 0;
|
|
1700
|
+
transition: transform 0.2s ease;
|
|
1701
|
+
color: var(--cc-text-secondary);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
.cc-category--expanded .cc-category__chevron {
|
|
1705
|
+
transform: rotate(180deg);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
.cc-category__title {
|
|
1709
|
+
font-weight: 600;
|
|
1710
|
+
overflow: hidden;
|
|
1711
|
+
text-overflow: ellipsis;
|
|
1712
|
+
white-space: nowrap;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
.cc-category__count {
|
|
1716
|
+
font-size: 0.8em;
|
|
1717
|
+
color: var(--cc-text-secondary);
|
|
1718
|
+
font-weight: 400;
|
|
1719
|
+
margin-left: 4px;
|
|
1720
|
+
flex-shrink: 0;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
.cc-category__body {
|
|
1724
|
+
max-height: 0;
|
|
1725
|
+
overflow: hidden;
|
|
1726
|
+
transition: max-height 0.3s ease;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
.cc-category--expanded .cc-category__body {
|
|
1730
|
+
max-height: 800px;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
.cc-category__description {
|
|
1734
|
+
padding: 0 16px 12px;
|
|
1735
|
+
font-size: 0.88em;
|
|
1736
|
+
color: var(--cc-text-secondary);
|
|
1737
|
+
line-height: 1.6;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/* ─── Cookie Table ─────────────────────────────────────────────────────── */
|
|
1741
|
+
|
|
1742
|
+
.cc-cookies {
|
|
1743
|
+
padding: 0 16px 14px;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
.cc-cookies__empty {
|
|
1747
|
+
font-size: 0.85em;
|
|
1748
|
+
color: var(--cc-text-secondary);
|
|
1749
|
+
font-style: italic;
|
|
1750
|
+
padding: 4px 0;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
.cc-cookie-item {
|
|
1754
|
+
border: 1px solid var(--cc-border);
|
|
1755
|
+
border-radius: calc(var(--cc-radius) - 4px);
|
|
1756
|
+
margin-bottom: 8px;
|
|
1757
|
+
font-size: 0.85em;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
.cc-cookie-item:last-child {
|
|
1761
|
+
margin-bottom: 0;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
.cc-cookie-item__row {
|
|
1765
|
+
display: flex;
|
|
1766
|
+
border-bottom: 1px solid var(--cc-border);
|
|
1767
|
+
min-height: 32px;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
.cc-cookie-item__row:last-child {
|
|
1771
|
+
border-bottom: none;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
.cc-cookie-item__label {
|
|
1775
|
+
flex: 0 0 90px;
|
|
1776
|
+
padding: 6px 10px;
|
|
1777
|
+
font-weight: 600;
|
|
1778
|
+
color: var(--cc-text);
|
|
1779
|
+
background: rgba(0, 0, 0, 0.015);
|
|
1780
|
+
display: flex;
|
|
1781
|
+
align-items: center;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
.cc-cookie-item__value {
|
|
1785
|
+
flex: 1;
|
|
1786
|
+
padding: 6px 10px;
|
|
1787
|
+
color: var(--cc-text-secondary);
|
|
1788
|
+
word-break: break-word;
|
|
1789
|
+
display: flex;
|
|
1790
|
+
align-items: center;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/* ─── Toggle Switch ────────────────────────────────────────────────────── */
|
|
1794
|
+
|
|
1795
|
+
.cc-toggle {
|
|
1796
|
+
position: relative;
|
|
1797
|
+
display: inline-block;
|
|
1798
|
+
width: 44px;
|
|
1799
|
+
height: 24px;
|
|
1800
|
+
flex-shrink: 0;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
.cc-toggle__input {
|
|
1804
|
+
position: absolute;
|
|
1805
|
+
opacity: 0;
|
|
1806
|
+
width: 0;
|
|
1807
|
+
height: 0;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
.cc-toggle__slider {
|
|
1811
|
+
position: absolute;
|
|
1812
|
+
cursor: pointer;
|
|
1813
|
+
top: 0;
|
|
1814
|
+
left: 0;
|
|
1815
|
+
right: 0;
|
|
1816
|
+
bottom: 0;
|
|
1817
|
+
background-color: var(--cc-toggle-off);
|
|
1818
|
+
border-radius: 24px;
|
|
1819
|
+
transition: background-color 0.2s ease;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
.cc-toggle__slider::before {
|
|
1823
|
+
content: '';
|
|
1824
|
+
position: absolute;
|
|
1825
|
+
height: 18px;
|
|
1826
|
+
width: 18px;
|
|
1827
|
+
left: 3px;
|
|
1828
|
+
bottom: 3px;
|
|
1829
|
+
background-color: var(--cc-toggle-knob);
|
|
1830
|
+
border-radius: 50%;
|
|
1831
|
+
transition: transform 0.2s ease;
|
|
1832
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
.cc-toggle__input:checked + .cc-toggle__slider {
|
|
1836
|
+
background-color: var(--cc-toggle-on);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
.cc-toggle__input:checked + .cc-toggle__slider::before {
|
|
1840
|
+
transform: translateX(20px);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
.cc-toggle__input:focus-visible + .cc-toggle__slider {
|
|
1844
|
+
outline: 2px solid var(--cc-primary);
|
|
1845
|
+
outline-offset: 2px;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
.cc-toggle__input:disabled + .cc-toggle__slider {
|
|
1849
|
+
opacity: 0.7;
|
|
1850
|
+
cursor: default;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
.cc-category__always-active {
|
|
1854
|
+
font-size: 0.8em;
|
|
1855
|
+
font-weight: 600;
|
|
1856
|
+
color: var(--cc-primary);
|
|
1857
|
+
white-space: nowrap;
|
|
1858
|
+
padding: 0 4px;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
/* ─── Responsive ───────────────────────────────────────────────────────── */
|
|
1862
|
+
|
|
1863
|
+
@media (max-width: 480px) {
|
|
1864
|
+
.cc-banner__inner {
|
|
1865
|
+
padding: 16px;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
.cc-banner__actions {
|
|
1869
|
+
flex-direction: column;
|
|
1870
|
+
align-items: stretch;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
.cc-banner__actions .cc-btn {
|
|
1874
|
+
width: 100%;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
.cc-banner--popup {
|
|
1878
|
+
width: calc(100% - 24px);
|
|
1879
|
+
left: 12px !important;
|
|
1880
|
+
right: 12px !important;
|
|
1881
|
+
bottom: 12px !important;
|
|
1882
|
+
top: auto !important;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
.cc-preferences__dialog {
|
|
1886
|
+
max-height: calc(100vh - 24px);
|
|
1887
|
+
width: calc(100% - 16px);
|
|
1888
|
+
border-radius: calc(var(--cc-radius) - 2px);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
.cc-preferences__header,
|
|
1892
|
+
.cc-preferences__body {
|
|
1893
|
+
padding-left: 16px;
|
|
1894
|
+
padding-right: 16px;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
.cc-preferences__footer {
|
|
1898
|
+
padding: 12px 16px;
|
|
1899
|
+
flex-direction: column;
|
|
1900
|
+
align-items: stretch;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
.cc-preferences__footer .cc-btn {
|
|
1904
|
+
width: 100%;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
.cc-cookie-item__label {
|
|
1908
|
+
flex: 0 0 75px;
|
|
1909
|
+
font-size: 0.9em;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/* ─── Accessibility: reduced motion ────────────────────────────────────── */
|
|
1914
|
+
|
|
1915
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1916
|
+
.cc-banner,
|
|
1917
|
+
.cc-overlay,
|
|
1918
|
+
.cc-preferences,
|
|
1919
|
+
.cc-preferences__dialog,
|
|
1920
|
+
.cc-category__body,
|
|
1921
|
+
.cc-category__chevron,
|
|
1922
|
+
.cc-toggle__slider,
|
|
1923
|
+
.cc-toggle__slider::before {
|
|
1924
|
+
transition: none !important;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/* ─── Print: hide everything ───────────────────────────────────────────── */
|
|
1929
|
+
|
|
1930
|
+
@media print {
|
|
1931
|
+
.cc-container {
|
|
1932
|
+
display: none !important;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
`;
|
|
1936
|
+
|
|
1937
|
+
var injected = false;
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* Inject the stylesheet into the document head.
|
|
1941
|
+
* Idempotent — only injects once.
|
|
1942
|
+
*/
|
|
1943
|
+
function injectStyles() {
|
|
1944
|
+
if (injected || typeof document === 'undefined') return;
|
|
1945
|
+
var style = document.createElement('style');
|
|
1946
|
+
style.setAttribute('data-cc-styles', '');
|
|
1947
|
+
style.textContent = CSS;
|
|
1948
|
+
(document.head || document.documentElement).appendChild(style);
|
|
1949
|
+
injected = true;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
/**
|
|
1953
|
+
* Set CSS custom properties on the container element based on theme config.
|
|
1954
|
+
*/
|
|
1955
|
+
function applyTheme(container, theme) {
|
|
1956
|
+
if (!container || !theme) return;
|
|
1957
|
+
var map = {
|
|
1958
|
+
'--cc-primary': theme.primary,
|
|
1959
|
+
'--cc-primary-hover': theme.primaryHover,
|
|
1960
|
+
'--cc-primary-text': theme.primaryText,
|
|
1961
|
+
'--cc-bg': theme.background,
|
|
1962
|
+
'--cc-text': theme.text,
|
|
1963
|
+
'--cc-text-secondary': theme.textSecondary,
|
|
1964
|
+
'--cc-border': theme.border,
|
|
1965
|
+
'--cc-overlay': theme.overlay,
|
|
1966
|
+
'--cc-toggle-on': theme.toggleOn,
|
|
1967
|
+
'--cc-toggle-off': theme.toggleOff,
|
|
1968
|
+
'--cc-toggle-knob': theme.toggleKnob,
|
|
1969
|
+
'--cc-font': theme.fontFamily,
|
|
1970
|
+
'--cc-font-size': theme.fontSize,
|
|
1971
|
+
'--cc-radius': theme.borderRadius,
|
|
1972
|
+
'--cc-z-index': theme.zIndex,
|
|
1973
|
+
'--cc-max-width': theme.maxWidth,
|
|
1974
|
+
'--cc-popup-width': theme.popupWidth,
|
|
1975
|
+
};
|
|
1976
|
+
for (var prop in map) {
|
|
1977
|
+
if (map[prop] != null) {
|
|
1978
|
+
container.style.setProperty(prop, String(map[prop]));
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* CookieConsent — GDPR-compliant cookie consent manager.
|
|
1985
|
+
*
|
|
1986
|
+
* Usage (script tag):
|
|
1987
|
+
* <script src="cookie-consent-gdpr.min.js"></script>
|
|
1988
|
+
* <script>CookieConsent.init({ ... });</script>
|
|
1989
|
+
*
|
|
1990
|
+
* Usage (npm):
|
|
1991
|
+
* import CookieConsent from 'cookie-consent-gdpr';
|
|
1992
|
+
* CookieConsent.init({ ... });
|
|
1993
|
+
*
|
|
1994
|
+
* Usage (zero-config HTML):
|
|
1995
|
+
* <div id="cc-banner"></div>
|
|
1996
|
+
* <script src="cookie-consent-gdpr.min.js" data-cc-auto></script>
|
|
1997
|
+
*/
|
|
1998
|
+
|
|
1999
|
+
var instance = {
|
|
2000
|
+
_initialized: false,
|
|
2001
|
+
_config: null,
|
|
2002
|
+
_emitter: null,
|
|
2003
|
+
_consent: null,
|
|
2004
|
+
_blocker: null,
|
|
2005
|
+
_banner: null,
|
|
2006
|
+
_preferences: null,
|
|
2007
|
+
_container: null,
|
|
2008
|
+
_prefBtnListeners: [],
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
// ─── Public API ─────────────────────────────────────────────────────────
|
|
2012
|
+
|
|
2013
|
+
var CookieConsent = {
|
|
2014
|
+
/**
|
|
2015
|
+
* Initialize the cookie consent system.
|
|
2016
|
+
* @param {Object} userConfig - Configuration overrides.
|
|
2017
|
+
*/
|
|
2018
|
+
init: function (userConfig) {
|
|
2019
|
+
if (instance._initialized) {
|
|
2020
|
+
console.warn('[CookieConsent] Already initialized. Call destroy() first to reinitialize.');
|
|
2021
|
+
return CookieConsent;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
var config = deepMerge(DEFAULTS, userConfig || {});
|
|
2025
|
+
instance._config = config;
|
|
2026
|
+
instance._emitter = new EventEmitter();
|
|
2027
|
+
instance._consent = new ConsentManager(config, instance._emitter);
|
|
2028
|
+
instance._blocker = new ScriptBlocker(instance._emitter);
|
|
2029
|
+
instance._initialized = true;
|
|
2030
|
+
|
|
2031
|
+
// Inject styles
|
|
2032
|
+
injectStyles();
|
|
2033
|
+
|
|
2034
|
+
// Determine container element
|
|
2035
|
+
var containerEl;
|
|
2036
|
+
if (config.container) {
|
|
2037
|
+
containerEl =
|
|
2038
|
+
typeof config.container === 'string' ? $(config.container) : config.container;
|
|
2039
|
+
}
|
|
2040
|
+
if (!containerEl) {
|
|
2041
|
+
containerEl = document.createElement('div');
|
|
2042
|
+
document.body.appendChild(containerEl);
|
|
2043
|
+
}
|
|
2044
|
+
containerEl.classList.add('cc-container');
|
|
2045
|
+
instance._container = containerEl;
|
|
2046
|
+
|
|
2047
|
+
// Apply theme CSS variables
|
|
2048
|
+
applyTheme(containerEl, config.theme);
|
|
2049
|
+
|
|
2050
|
+
// Create UI components
|
|
2051
|
+
instance._banner = new Banner(config, instance._emitter);
|
|
2052
|
+
instance._preferences = new Preferences(config, instance._emitter);
|
|
2053
|
+
|
|
2054
|
+
// Load existing consent
|
|
2055
|
+
var existing = instance._consent.load();
|
|
2056
|
+
|
|
2057
|
+
// Render banner (always, but only show if needed)
|
|
2058
|
+
instance._banner.render(containerEl);
|
|
2059
|
+
|
|
2060
|
+
// Wire up internal event handlers
|
|
2061
|
+
_bindEvents();
|
|
2062
|
+
|
|
2063
|
+
// Bind external preferences buttons
|
|
2064
|
+
_bindPreferencesButtons(config);
|
|
2065
|
+
|
|
2066
|
+
if (existing && instance._consent.hasValid()) {
|
|
2067
|
+
// Valid consent exists — activate scripts and observe
|
|
2068
|
+
instance._blocker.activate(existing.categories);
|
|
2069
|
+
instance._blocker.observe(existing.categories);
|
|
2070
|
+
} else {
|
|
2071
|
+
// No valid consent — show banner
|
|
2072
|
+
if (config.autoShow) {
|
|
2073
|
+
// Small delay to allow page to render first
|
|
2074
|
+
setTimeout(function () {
|
|
2075
|
+
instance._banner.show();
|
|
2076
|
+
}, 100);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
return CookieConsent;
|
|
2081
|
+
},
|
|
2082
|
+
|
|
2083
|
+
/** Show the consent banner. */
|
|
2084
|
+
show: function () {
|
|
2085
|
+
if (instance._banner) instance._banner.show();
|
|
2086
|
+
return CookieConsent;
|
|
2087
|
+
},
|
|
2088
|
+
|
|
2089
|
+
/** Hide the consent banner. */
|
|
2090
|
+
hide: function () {
|
|
2091
|
+
if (instance._banner) instance._banner.hide();
|
|
2092
|
+
return CookieConsent;
|
|
2093
|
+
},
|
|
2094
|
+
|
|
2095
|
+
/** Open the preferences/settings modal. */
|
|
2096
|
+
showPreferences: function () {
|
|
2097
|
+
_ensurePreferencesRendered();
|
|
2098
|
+
if (instance._preferences) instance._preferences.show();
|
|
2099
|
+
return CookieConsent;
|
|
2100
|
+
},
|
|
2101
|
+
|
|
2102
|
+
/** Close the preferences modal. */
|
|
2103
|
+
hidePreferences: function () {
|
|
2104
|
+
if (instance._preferences) instance._preferences.hide();
|
|
2105
|
+
return CookieConsent;
|
|
2106
|
+
},
|
|
2107
|
+
|
|
2108
|
+
/** Accept all cookie categories. */
|
|
2109
|
+
acceptAll: function () {
|
|
2110
|
+
if (!instance._consent) return CookieConsent;
|
|
2111
|
+
var cats = instance._consent.acceptAllMap();
|
|
2112
|
+
_applyConsent(cats);
|
|
2113
|
+
return CookieConsent;
|
|
2114
|
+
},
|
|
2115
|
+
|
|
2116
|
+
/** Reject all non-essential cookies. */
|
|
2117
|
+
rejectAll: function () {
|
|
2118
|
+
if (!instance._consent) return CookieConsent;
|
|
2119
|
+
var cats = instance._consent.rejectAllMap();
|
|
2120
|
+
_applyConsent(cats);
|
|
2121
|
+
return CookieConsent;
|
|
2122
|
+
},
|
|
2123
|
+
|
|
2124
|
+
/** Accept a specific category by key. */
|
|
2125
|
+
acceptCategory: function (category) {
|
|
2126
|
+
if (!instance._consent) return CookieConsent;
|
|
2127
|
+
var current = instance._consent.get();
|
|
2128
|
+
var cats = current ? Object.assign({}, current.categories) : instance._consent.rejectAllMap();
|
|
2129
|
+
cats[category] = true;
|
|
2130
|
+
_applyConsent(cats);
|
|
2131
|
+
return CookieConsent;
|
|
2132
|
+
},
|
|
2133
|
+
|
|
2134
|
+
/** Get the full consent record. Returns null if no consent given. */
|
|
2135
|
+
getConsent: function () {
|
|
2136
|
+
return instance._consent ? instance._consent.get() : null;
|
|
2137
|
+
},
|
|
2138
|
+
|
|
2139
|
+
/** Check if the user has given any consent (made a choice). */
|
|
2140
|
+
hasConsented: function () {
|
|
2141
|
+
return !!(instance._consent && instance._consent.get());
|
|
2142
|
+
},
|
|
2143
|
+
|
|
2144
|
+
/** Check if a specific category has been consented to. */
|
|
2145
|
+
hasCategory: function (category) {
|
|
2146
|
+
return instance._consent ? instance._consent.hasCategory(category) : false;
|
|
2147
|
+
},
|
|
2148
|
+
|
|
2149
|
+
/** Revoke all consent and re-show the banner. */
|
|
2150
|
+
revokeConsent: function () {
|
|
2151
|
+
if (instance._consent) instance._consent.revoke();
|
|
2152
|
+
if (instance._blocker) instance._blocker.destroy();
|
|
2153
|
+
// Destroy preferences so it re-renders with fresh toggle states
|
|
2154
|
+
if (instance._preferences) {
|
|
2155
|
+
instance._preferences.destroy();
|
|
2156
|
+
instance._preferences = new Preferences(instance._config, instance._emitter);
|
|
2157
|
+
}
|
|
2158
|
+
if (instance._banner) instance._banner.show();
|
|
2159
|
+
return CookieConsent;
|
|
2160
|
+
},
|
|
2161
|
+
|
|
2162
|
+
/** Register an event listener. */
|
|
2163
|
+
on: function (event, fn) {
|
|
2164
|
+
if (instance._emitter) instance._emitter.on(event, fn);
|
|
2165
|
+
return CookieConsent;
|
|
2166
|
+
},
|
|
2167
|
+
|
|
2168
|
+
/** Remove an event listener. */
|
|
2169
|
+
off: function (event, fn) {
|
|
2170
|
+
if (instance._emitter) instance._emitter.off(event, fn);
|
|
2171
|
+
return CookieConsent;
|
|
2172
|
+
},
|
|
2173
|
+
|
|
2174
|
+
/** Destroy the entire consent UI and clean up. */
|
|
2175
|
+
destroy: function () {
|
|
2176
|
+
if (instance._banner) instance._banner.destroy();
|
|
2177
|
+
if (instance._preferences) instance._preferences.destroy();
|
|
2178
|
+
if (instance._blocker) instance._blocker.destroy();
|
|
2179
|
+
|
|
2180
|
+
// Clean up preferences button listeners
|
|
2181
|
+
instance._prefBtnListeners.forEach(function (item) {
|
|
2182
|
+
item.el.removeEventListener('click', item.fn);
|
|
2183
|
+
});
|
|
2184
|
+
instance._prefBtnListeners = [];
|
|
2185
|
+
|
|
2186
|
+
// Remove container if we created it
|
|
2187
|
+
if (
|
|
2188
|
+
instance._container &&
|
|
2189
|
+
instance._container.parentNode &&
|
|
2190
|
+
!instance._config.container
|
|
2191
|
+
) {
|
|
2192
|
+
instance._container.parentNode.removeChild(instance._container);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
instance._initialized = false;
|
|
2196
|
+
instance._config = null;
|
|
2197
|
+
instance._emitter = null;
|
|
2198
|
+
instance._consent = null;
|
|
2199
|
+
instance._blocker = null;
|
|
2200
|
+
instance._banner = null;
|
|
2201
|
+
instance._preferences = null;
|
|
2202
|
+
instance._container = null;
|
|
2203
|
+
|
|
2204
|
+
return CookieConsent;
|
|
2205
|
+
},
|
|
2206
|
+
|
|
2207
|
+
/** Get the current configuration (read-only copy). */
|
|
2208
|
+
getConfig: function () {
|
|
2209
|
+
return instance._config ? JSON.parse(JSON.stringify(instance._config)) : null;
|
|
2210
|
+
},
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
// ─── Internal Helpers ───────────────────────────────────────────────────
|
|
2214
|
+
|
|
2215
|
+
/**
|
|
2216
|
+
* Ensure the preferences modal is rendered (lazy render on first open).
|
|
2217
|
+
*/
|
|
2218
|
+
function _ensurePreferencesRendered() {
|
|
2219
|
+
if (instance._preferences && !instance._preferences.el) {
|
|
2220
|
+
var current = instance._consent ? instance._consent.get() : null;
|
|
2221
|
+
instance._preferences.render(instance._container, current);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
/**
|
|
2226
|
+
* Apply a consent decision: save, hide banner, activate scripts.
|
|
2227
|
+
*/
|
|
2228
|
+
function _applyConsent(categories) {
|
|
2229
|
+
instance._consent.save(categories);
|
|
2230
|
+
if (instance._banner) instance._banner.hide();
|
|
2231
|
+
if (instance._preferences) instance._preferences.hide();
|
|
2232
|
+
instance._blocker.activate(categories);
|
|
2233
|
+
instance._blocker.observe(categories);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Bind internal UI event listeners.
|
|
2238
|
+
*/
|
|
2239
|
+
function _bindEvents() {
|
|
2240
|
+
var emitter = instance._emitter;
|
|
2241
|
+
|
|
2242
|
+
// Banner actions
|
|
2243
|
+
emitter.on('banner:acceptAll', function () {
|
|
2244
|
+
_applyConsent(instance._consent.acceptAllMap());
|
|
2245
|
+
});
|
|
2246
|
+
|
|
2247
|
+
emitter.on('banner:rejectAll', function () {
|
|
2248
|
+
_applyConsent(instance._consent.rejectAllMap());
|
|
2249
|
+
});
|
|
2250
|
+
|
|
2251
|
+
emitter.on('banner:settings', function () {
|
|
2252
|
+
if (instance._banner) instance._banner.hide();
|
|
2253
|
+
CookieConsent.showPreferences();
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
// Preferences actions
|
|
2257
|
+
emitter.on('preferences:save', function (categories) {
|
|
2258
|
+
_applyConsent(categories);
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
emitter.on('preferences:acceptAll', function () {
|
|
2262
|
+
if (instance._preferences) instance._preferences.setAll(true);
|
|
2263
|
+
_applyConsent(instance._consent.acceptAllMap());
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
emitter.on('preferences:rejectAll', function () {
|
|
2267
|
+
if (instance._preferences) instance._preferences.setAll(false);
|
|
2268
|
+
_applyConsent(instance._consent.rejectAllMap());
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2271
|
+
emitter.on('preferences:close', function () {
|
|
2272
|
+
CookieConsent.hidePreferences();
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* Bind click handlers to preference button elements.
|
|
2278
|
+
*/
|
|
2279
|
+
function _bindPreferencesButtons(config) {
|
|
2280
|
+
var handler = function (e) {
|
|
2281
|
+
e.preventDefault();
|
|
2282
|
+
CookieConsent.showPreferences();
|
|
2283
|
+
};
|
|
2284
|
+
|
|
2285
|
+
// Bind to configured selector
|
|
2286
|
+
if (config.preferencesButton) {
|
|
2287
|
+
var btns =
|
|
2288
|
+
typeof config.preferencesButton === 'string'
|
|
2289
|
+
? $$(config.preferencesButton)
|
|
2290
|
+
: [config.preferencesButton];
|
|
2291
|
+
btns.forEach(function (btn) {
|
|
2292
|
+
btn.addEventListener('click', handler);
|
|
2293
|
+
instance._prefBtnListeners.push({ el: btn, fn: handler });
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// Auto-bind any element with data-cc-open-preferences
|
|
2298
|
+
var autoButtons = $$('[data-cc-open-preferences]');
|
|
2299
|
+
autoButtons.forEach(function (btn) {
|
|
2300
|
+
btn.addEventListener('click', handler);
|
|
2301
|
+
instance._prefBtnListeners.push({ el: btn, fn: handler });
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// ─── Auto-initialization ────────────────────────────────────────────────
|
|
2306
|
+
|
|
2307
|
+
/**
|
|
2308
|
+
* Auto-init when:
|
|
2309
|
+
* 1. Script tag has `data-cc-auto` attribute, OR
|
|
2310
|
+
* 2. An element with id="cc-banner" exists in the page
|
|
2311
|
+
*
|
|
2312
|
+
* For auto-init, config can be provided via data-cc-config attribute (JSON string).
|
|
2313
|
+
*/
|
|
2314
|
+
function _autoInit() {
|
|
2315
|
+
if (instance._initialized) return;
|
|
2316
|
+
|
|
2317
|
+
// Check for script tag with data-cc-auto
|
|
2318
|
+
var scripts = document.querySelectorAll('script[data-cc-auto]');
|
|
2319
|
+
if (scripts.length > 0) {
|
|
2320
|
+
var script = scripts[scripts.length - 1];
|
|
2321
|
+
var configStr = script.getAttribute('data-cc-config');
|
|
2322
|
+
var autoConfig = {};
|
|
2323
|
+
if (configStr) {
|
|
2324
|
+
try {
|
|
2325
|
+
autoConfig = JSON.parse(configStr);
|
|
2326
|
+
} catch (e) {
|
|
2327
|
+
console.error('[CookieConsent] Invalid data-cc-config JSON:', e);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
// Check for an explicit container element
|
|
2331
|
+
var containerEl = $('#cc-banner');
|
|
2332
|
+
if (containerEl) {
|
|
2333
|
+
autoConfig.container = containerEl;
|
|
2334
|
+
}
|
|
2335
|
+
CookieConsent.init(autoConfig);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// Check for container element with id
|
|
2340
|
+
var container = $('#cc-banner');
|
|
2341
|
+
if (container && container.hasAttribute('data-cc-auto')) {
|
|
2342
|
+
var cfgStr = container.getAttribute('data-cc-config');
|
|
2343
|
+
var cfg = {};
|
|
2344
|
+
if (cfgStr) {
|
|
2345
|
+
try {
|
|
2346
|
+
cfg = JSON.parse(cfgStr);
|
|
2347
|
+
} catch (e) {
|
|
2348
|
+
console.error('[CookieConsent] Invalid data-cc-config JSON:', e);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
cfg.container = container;
|
|
2352
|
+
CookieConsent.init(cfg);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// Run auto-init when DOM is ready
|
|
2357
|
+
if (typeof document !== 'undefined') {
|
|
2358
|
+
if (document.readyState === 'loading') {
|
|
2359
|
+
document.addEventListener('DOMContentLoaded', _autoInit);
|
|
2360
|
+
} else {
|
|
2361
|
+
_autoInit();
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
module.exports = CookieConsent;
|