fivo_cookie_consent 0.2.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.
- checksums.yaml +7 -0
- data/.nvmrc +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.windsurfrules +76 -0
- data/CHANGELOG.md +64 -0
- data/LICENSE.txt +21 -0
- data/README.md +524 -0
- data/Rakefile +11 -0
- data/app/assets/javascripts/fivo_cookie_consent.js +924 -0
- data/app/assets/stylesheets/fivo_cookie_consent.scss +654 -0
- data/app/helpers/rails_cookies_gdpr/application_helper.rb +156 -0
- data/app/views/rails_cookies_gdpr/_banner.html.erb +27 -0
- data/app/views/rails_cookies_gdpr/_modal.html.erb +123 -0
- data/config/database.yml +6 -0
- data/db/test.sqlite3 +0 -0
- data/db/test.sqlite3-shm +0 -0
- data/db/test.sqlite3-wal +0 -0
- data/lib/fivo_cookie_consent/configuration.rb +141 -0
- data/lib/fivo_cookie_consent/engine.rb +31 -0
- data/lib/fivo_cookie_consent/railtie.rb +18 -0
- data/lib/fivo_cookie_consent/version.rb +5 -0
- data/lib/fivo_cookie_consent.rb +16 -0
- data/lib/generators/fivo_cookie_consent/install_generator.rb +144 -0
- data/scratchpad.md +315 -0
- data/yarn.lock +4 -0
- metadata +190 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GDPR Cookie Consent Manager
|
|
3
|
+
* Handles cookie consent banner display, user preferences, and localStorage management
|
|
4
|
+
*/
|
|
5
|
+
class GDPRCookieConsent {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.storageKey = 'gdpr_cookie_consent';
|
|
8
|
+
this.cookieLifespan = 365; // days
|
|
9
|
+
this.banner = null;
|
|
10
|
+
this.modal = null;
|
|
11
|
+
this.detectedCookies = {};
|
|
12
|
+
this.expandedCategories = new Set();
|
|
13
|
+
|
|
14
|
+
this.init();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the cookie consent manager
|
|
19
|
+
*/
|
|
20
|
+
init() {
|
|
21
|
+
// Wait for DOM to be ready
|
|
22
|
+
if (document.readyState === 'loading') {
|
|
23
|
+
document.addEventListener('DOMContentLoaded', () => this.setup());
|
|
24
|
+
} else {
|
|
25
|
+
this.setup();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Setup event listeners and check consent status
|
|
31
|
+
*/
|
|
32
|
+
setup() {
|
|
33
|
+
this.banner = document.getElementById('gdpr-cookie-banner');
|
|
34
|
+
this.modal = document.getElementById('gdpr-cookie-modal');
|
|
35
|
+
|
|
36
|
+
if (!this.banner || !this.modal) {
|
|
37
|
+
console.warn('GDPR Cookie Consent: Banner or modal not found');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.setupEventListeners();
|
|
42
|
+
this.detectAndCategorizeInitialCookies();
|
|
43
|
+
this.checkConsentStatus();
|
|
44
|
+
this.setupCookieObserver();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Setup all event listeners for banner and modal interactions
|
|
49
|
+
*/
|
|
50
|
+
setupEventListeners() {
|
|
51
|
+
// Banner buttons
|
|
52
|
+
this.banner.addEventListener('click', (e) => {
|
|
53
|
+
const action = e.target.dataset.action;
|
|
54
|
+
if (action === 'accept') {
|
|
55
|
+
this.acceptAll();
|
|
56
|
+
} else if (action === 'decline') {
|
|
57
|
+
this.declineAll();
|
|
58
|
+
} else if (action === 'settings') {
|
|
59
|
+
this.openModal();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Modal interactions
|
|
64
|
+
this.modal.addEventListener('click', (e) => {
|
|
65
|
+
// Check the clicked element and its parents for data-action
|
|
66
|
+
let element = e.target;
|
|
67
|
+
let action = null;
|
|
68
|
+
|
|
69
|
+
// Traverse up the DOM to find an element with data-action
|
|
70
|
+
while (element && element !== this.modal) {
|
|
71
|
+
if (element.dataset && element.dataset.action) {
|
|
72
|
+
action = element.dataset.action;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
element = element.parentElement;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (action === 'close-modal') {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
this.closeModal();
|
|
81
|
+
} else if (action === 'save-preferences') {
|
|
82
|
+
this.savePreferences();
|
|
83
|
+
} else if (action === 'decline-all') {
|
|
84
|
+
this.declineAll();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Close modal on Escape key
|
|
89
|
+
document.addEventListener('keydown', (e) => {
|
|
90
|
+
if (e.key === 'Escape' && this.isModalOpen()) {
|
|
91
|
+
this.closeModal();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if user has already made consent choices
|
|
98
|
+
*/
|
|
99
|
+
checkConsentStatus() {
|
|
100
|
+
const consent = this.getConsent();
|
|
101
|
+
if (!consent) {
|
|
102
|
+
this.showBanner();
|
|
103
|
+
} else {
|
|
104
|
+
this.loadPreferences();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Show the cookie consent banner
|
|
110
|
+
*/
|
|
111
|
+
showBanner() {
|
|
112
|
+
if (this.banner) {
|
|
113
|
+
this.banner.style.display = 'block';
|
|
114
|
+
// Add animation class for better UX
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
this.banner.classList.add('gdpr-banner--visible');
|
|
117
|
+
}, 100);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Hide the cookie consent banner
|
|
123
|
+
*/
|
|
124
|
+
hideBanner() {
|
|
125
|
+
if (this.banner) {
|
|
126
|
+
this.banner.classList.remove('gdpr-banner--visible');
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
this.banner.style.display = 'none';
|
|
129
|
+
}, 300);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Open the preferences modal
|
|
135
|
+
*/
|
|
136
|
+
openModal() {
|
|
137
|
+
if (this.modal) {
|
|
138
|
+
this.modal.style.display = 'block';
|
|
139
|
+
document.body.style.overflow = 'hidden';
|
|
140
|
+
// Re-detect cookies in case they changed since initial load
|
|
141
|
+
this.detectAndCategorizeInitialCookies();
|
|
142
|
+
|
|
143
|
+
// Load current preferences into modal
|
|
144
|
+
this.loadPreferences();
|
|
145
|
+
|
|
146
|
+
// Setup category expansion functionality
|
|
147
|
+
this.setupCategoryExpansion();
|
|
148
|
+
|
|
149
|
+
// Focus management for accessibility
|
|
150
|
+
const firstInput = this.modal.querySelector('input[type="checkbox"]:not([disabled])');
|
|
151
|
+
if (firstInput) {
|
|
152
|
+
firstInput.focus();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Close the preferences modal
|
|
159
|
+
*/
|
|
160
|
+
closeModal() {
|
|
161
|
+
if (this.modal) {
|
|
162
|
+
this.modal.style.display = 'none';
|
|
163
|
+
document.body.style.overflow = '';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if modal is currently open
|
|
169
|
+
*/
|
|
170
|
+
isModalOpen() {
|
|
171
|
+
return this.modal && this.modal.style.display === 'block';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Accept all cookie categories
|
|
176
|
+
*/
|
|
177
|
+
acceptAll() {
|
|
178
|
+
const consent = {
|
|
179
|
+
necessary: true,
|
|
180
|
+
analytics: true,
|
|
181
|
+
marketing: true,
|
|
182
|
+
functional: true,
|
|
183
|
+
savedAt: new Date().toISOString()
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
this.saveConsent(consent);
|
|
187
|
+
this.hideBanner();
|
|
188
|
+
this.closeModal();
|
|
189
|
+
this.dispatchEvent('gdpr:accept', { categories: ['analytics', 'marketing', 'functional'] });
|
|
190
|
+
this.dispatchEvent('gdpr:change', { consent: consent });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Decline all non-necessary cookies
|
|
195
|
+
*/
|
|
196
|
+
declineAll() {
|
|
197
|
+
const consent = {
|
|
198
|
+
necessary: true,
|
|
199
|
+
analytics: false,
|
|
200
|
+
marketing: false,
|
|
201
|
+
functional: false,
|
|
202
|
+
savedAt: new Date().toISOString()
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
this.saveConsent(consent);
|
|
206
|
+
this.hideBanner();
|
|
207
|
+
this.closeModal();
|
|
208
|
+
this.dispatchEvent('gdpr:decline', { categories: ['analytics', 'marketing', 'functional'] });
|
|
209
|
+
this.dispatchEvent('gdpr:change', { consent: consent });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Save user preferences from modal
|
|
214
|
+
*/
|
|
215
|
+
savePreferences() {
|
|
216
|
+
const consent = {
|
|
217
|
+
necessary: true,
|
|
218
|
+
analytics: this.getCheckboxValue('gdpr-analytics'),
|
|
219
|
+
marketing: this.getCheckboxValue('gdpr-marketing'),
|
|
220
|
+
functional: this.getCheckboxValue('gdpr-functional'),
|
|
221
|
+
savedAt: new Date().toISOString()
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
this.saveConsent(consent);
|
|
225
|
+
this.hideBanner();
|
|
226
|
+
this.closeModal();
|
|
227
|
+
|
|
228
|
+
// Dispatch events for accepted and declined categories
|
|
229
|
+
const accepted = [];
|
|
230
|
+
const declined = [];
|
|
231
|
+
|
|
232
|
+
['analytics', 'marketing', 'functional'].forEach(category => {
|
|
233
|
+
if (consent[category]) {
|
|
234
|
+
accepted.push(category);
|
|
235
|
+
} else {
|
|
236
|
+
declined.push(category);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (accepted.length > 0) {
|
|
241
|
+
this.dispatchEvent('gdpr:accept', { categories: accepted });
|
|
242
|
+
}
|
|
243
|
+
if (declined.length > 0) {
|
|
244
|
+
this.dispatchEvent('gdpr:decline', { categories: declined });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.dispatchEvent('gdpr:change', { consent: consent });
|
|
248
|
+
|
|
249
|
+
// Re-detect cookies after saving preferences
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
this.detectAndCategorizeInitialCookies();
|
|
252
|
+
}, 100);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Load preferences into modal checkboxes
|
|
257
|
+
*/
|
|
258
|
+
loadPreferences() {
|
|
259
|
+
const consent = this.getConsent();
|
|
260
|
+
if (!consent) return;
|
|
261
|
+
|
|
262
|
+
this.setCheckboxValue('gdpr-analytics', consent.analytics);
|
|
263
|
+
this.setCheckboxValue('gdpr-marketing', consent.marketing);
|
|
264
|
+
this.setCheckboxValue('gdpr-functional', consent.functional);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get checkbox value by ID
|
|
269
|
+
*/
|
|
270
|
+
getCheckboxValue(id) {
|
|
271
|
+
const checkbox = document.getElementById(id);
|
|
272
|
+
return checkbox ? checkbox.checked : false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Set checkbox value by ID
|
|
277
|
+
*/
|
|
278
|
+
setCheckboxValue(id, value) {
|
|
279
|
+
const checkbox = document.getElementById(id);
|
|
280
|
+
if (checkbox) {
|
|
281
|
+
checkbox.checked = value;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Save consent to localStorage
|
|
287
|
+
*/
|
|
288
|
+
saveConsent(consent) {
|
|
289
|
+
try {
|
|
290
|
+
localStorage.setItem(this.storageKey, JSON.stringify(consent));
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error('GDPR Cookie Consent: Failed to save consent to localStorage', error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get consent from localStorage
|
|
298
|
+
*/
|
|
299
|
+
getConsent() {
|
|
300
|
+
try {
|
|
301
|
+
const stored = localStorage.getItem(this.storageKey);
|
|
302
|
+
if (!stored) return null;
|
|
303
|
+
|
|
304
|
+
const consent = JSON.parse(stored);
|
|
305
|
+
|
|
306
|
+
// Check if consent is still valid (not expired)
|
|
307
|
+
if (consent.savedAt) {
|
|
308
|
+
const savedDate = new Date(consent.savedAt);
|
|
309
|
+
const expiryDate = new Date(savedDate.getTime() + (this.cookieLifespan * 24 * 60 * 60 * 1000));
|
|
310
|
+
|
|
311
|
+
if (new Date() > expiryDate) {
|
|
312
|
+
this.clearConsent();
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return consent;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error('GDPR Cookie Consent: Failed to read consent from localStorage', error);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Clear consent from localStorage
|
|
326
|
+
*/
|
|
327
|
+
clearConsent() {
|
|
328
|
+
try {
|
|
329
|
+
localStorage.removeItem(this.storageKey);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('GDPR Cookie Consent: Failed to clear consent from localStorage', error);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Dispatch custom events for cookie consent changes
|
|
337
|
+
*/
|
|
338
|
+
dispatchEvent(eventName, detail) {
|
|
339
|
+
const event = new CustomEvent(eventName, {
|
|
340
|
+
detail: detail,
|
|
341
|
+
bubbles: true,
|
|
342
|
+
cancelable: true
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
document.dispatchEvent(event);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Check if a specific category is consented to
|
|
350
|
+
*/
|
|
351
|
+
hasConsent(category) {
|
|
352
|
+
const consent = this.getConsent();
|
|
353
|
+
return consent ? consent[category] === true : false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get all consent categories
|
|
358
|
+
*/
|
|
359
|
+
getAllConsent() {
|
|
360
|
+
return this.getConsent();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Force show banner (useful for testing or settings page)
|
|
365
|
+
*/
|
|
366
|
+
showBannerForced() {
|
|
367
|
+
this.showBanner();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Force show modal (useful for settings page)
|
|
372
|
+
*/
|
|
373
|
+
showModalForced() {
|
|
374
|
+
this.openModal();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Detect and categorize cookies initially
|
|
379
|
+
*/
|
|
380
|
+
detectAndCategorizeInitialCookies() {
|
|
381
|
+
// Get client-side detectable cookies
|
|
382
|
+
this.detectedCookies = this.detectCookies();
|
|
383
|
+
|
|
384
|
+
// Merge with server-side cookies
|
|
385
|
+
this.mergeServerSideCookies();
|
|
386
|
+
|
|
387
|
+
this.updateCookieDisplay();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Merge server-side cookies (HttpOnly) with client-side detected cookies
|
|
392
|
+
*/
|
|
393
|
+
mergeServerSideCookies() {
|
|
394
|
+
if (!this.banner) return;
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const serverCookiesData = this.banner.dataset.serverCookies;
|
|
398
|
+
if (!serverCookiesData) return;
|
|
399
|
+
|
|
400
|
+
const serverCookies = JSON.parse(serverCookiesData);
|
|
401
|
+
|
|
402
|
+
// Merge server-side cookies into detected cookies
|
|
403
|
+
Object.keys(serverCookies).forEach(category => {
|
|
404
|
+
if (this.detectedCookies[category] && serverCookies[category]) {
|
|
405
|
+
// Add server-side cookies to the category, avoiding duplicates
|
|
406
|
+
serverCookies[category].forEach(serverCookie => {
|
|
407
|
+
const exists = this.detectedCookies[category].some(cookie =>
|
|
408
|
+
cookie.name === serverCookie.name
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
if (!exists) {
|
|
412
|
+
this.detectedCookies[category].push(serverCookie);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.warn('GDPR: Failed to parse server-side cookies', error);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Set up cookie observer to detect new cookies
|
|
424
|
+
*/
|
|
425
|
+
setupCookieObserver() {
|
|
426
|
+
// Simple interval-based observer for cookie changes
|
|
427
|
+
setInterval(() => {
|
|
428
|
+
// Detect client-side cookies first
|
|
429
|
+
const newCookies = this.detectCookies();
|
|
430
|
+
|
|
431
|
+
// Clone so we don’t accidentally mutate the comparison object
|
|
432
|
+
this.detectedCookies = JSON.parse(JSON.stringify(newCookies));
|
|
433
|
+
|
|
434
|
+
// Always merge the server-side (HttpOnly) cookies so they persist
|
|
435
|
+
this.mergeServerSideCookies();
|
|
436
|
+
|
|
437
|
+
// If the overall cookie snapshot changed, re-render the UI
|
|
438
|
+
if (JSON.stringify(this.detectedCookies) !== JSON.stringify(newCookies)) {
|
|
439
|
+
this.updateCookieDisplay();
|
|
440
|
+
this.updateCategoryStates();
|
|
441
|
+
}
|
|
442
|
+
}, 2000); // Check every 2 seconds
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Detect cookies and categorize them
|
|
447
|
+
* @returns {Object} Categorized cookies object
|
|
448
|
+
*/
|
|
449
|
+
detectCookies() {
|
|
450
|
+
const cookies = {};
|
|
451
|
+
const categorized = {
|
|
452
|
+
necessary: [],
|
|
453
|
+
analytics: [],
|
|
454
|
+
marketing: [],
|
|
455
|
+
functional: [],
|
|
456
|
+
uncategorised: []
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// Parse document.cookie
|
|
460
|
+
if (document.cookie && document.cookie.trim()) {
|
|
461
|
+
document.cookie.split(';').forEach(cookie => {
|
|
462
|
+
const trimmed = cookie.trim();
|
|
463
|
+
if (trimmed) {
|
|
464
|
+
const [name, ...valueParts] = trimmed.split('=');
|
|
465
|
+
const value = valueParts.join('='); // Handle values with = in them
|
|
466
|
+
|
|
467
|
+
if (name && name.trim()) {
|
|
468
|
+
const cookieName = name.trim();
|
|
469
|
+
cookies[cookieName] = {
|
|
470
|
+
name: cookieName,
|
|
471
|
+
value: value ? value.trim() : '',
|
|
472
|
+
duration: this.getCookieDuration(cookieName),
|
|
473
|
+
description: this.getCookieDescription(cookieName)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Categorize cookies based on patterns
|
|
481
|
+
Object.values(cookies).forEach(cookie => {
|
|
482
|
+
const category = this.categorizeCookie(cookie.name);
|
|
483
|
+
categorized[category].push(cookie);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
return categorized;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Categorize a cookie based on its name
|
|
491
|
+
* @param {string} cookieName - Name of the cookie
|
|
492
|
+
* @returns {string} Category name
|
|
493
|
+
*/
|
|
494
|
+
categorizeCookie(cookieName) {
|
|
495
|
+
const patterns = this.getCookiePatterns();
|
|
496
|
+
|
|
497
|
+
for (const [category, regexList] of Object.entries(patterns)) {
|
|
498
|
+
for (const pattern of regexList) {
|
|
499
|
+
if (cookieName.match(pattern)) {
|
|
500
|
+
return category;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return 'uncategorised';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Get cookie patterns from configuration
|
|
509
|
+
* @returns {Object} Cookie patterns object
|
|
510
|
+
*/
|
|
511
|
+
getCookiePatterns() {
|
|
512
|
+
// Default patterns - in production these would come from Ruby config
|
|
513
|
+
return {
|
|
514
|
+
necessary: [
|
|
515
|
+
/^_session_/,
|
|
516
|
+
/^session_/,
|
|
517
|
+
/^csrf_token/,
|
|
518
|
+
/^authenticity_token/,
|
|
519
|
+
/^_rails_/,
|
|
520
|
+
/^gdpr_cookie_consent$/
|
|
521
|
+
],
|
|
522
|
+
analytics: [
|
|
523
|
+
/^_ga/,
|
|
524
|
+
/^_gid/,
|
|
525
|
+
/^_gat/,
|
|
526
|
+
/^_gtag/,
|
|
527
|
+
/^__utm/,
|
|
528
|
+
/^_dc_gtm_/,
|
|
529
|
+
/^_hjid/,
|
|
530
|
+
/^_hjFirstSeen/,
|
|
531
|
+
/^_hj/
|
|
532
|
+
],
|
|
533
|
+
marketing: [
|
|
534
|
+
/^_fbp/,
|
|
535
|
+
/^_fbc/,
|
|
536
|
+
/^fr$/,
|
|
537
|
+
/^tr$/,
|
|
538
|
+
/^_pinterest_/,
|
|
539
|
+
/^_pin_unauth/,
|
|
540
|
+
/^__Secure-3PAPISID/,
|
|
541
|
+
/^__Secure-3PSID/,
|
|
542
|
+
/^NID$/,
|
|
543
|
+
/^IDE$/
|
|
544
|
+
],
|
|
545
|
+
functional: [
|
|
546
|
+
/^_locale/,
|
|
547
|
+
/^language/,
|
|
548
|
+
/^theme/,
|
|
549
|
+
/^preferences/,
|
|
550
|
+
/^_user_settings/,
|
|
551
|
+
/^_ui_/
|
|
552
|
+
]
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Get cookie duration (simplified)
|
|
558
|
+
* @param {string} cookieName - Name of the cookie
|
|
559
|
+
* @returns {string} Duration description
|
|
560
|
+
*/
|
|
561
|
+
getCookieDuration(cookieName) {
|
|
562
|
+
// Simplified duration detection
|
|
563
|
+
if (cookieName.match(/^_session_|^session_/)) {
|
|
564
|
+
return 'Session';
|
|
565
|
+
}
|
|
566
|
+
if (cookieName.match(/^_ga/)) {
|
|
567
|
+
return '2 Jahre';
|
|
568
|
+
}
|
|
569
|
+
if (cookieName.match(/^_gid/)) {
|
|
570
|
+
return '24 Stunden';
|
|
571
|
+
}
|
|
572
|
+
return 'Unbekannt';
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Get cookie description
|
|
577
|
+
* @param {string} cookieName - Name of the cookie
|
|
578
|
+
* @returns {string} Cookie description
|
|
579
|
+
*/
|
|
580
|
+
getCookieDescription(cookieName) {
|
|
581
|
+
const descriptions = {
|
|
582
|
+
'_ga': 'Google Analytics Hauptcookie',
|
|
583
|
+
'_gid': 'Google Analytics Sitzungs-ID',
|
|
584
|
+
'_gat': 'Google Analytics Drosselung',
|
|
585
|
+
'_fbp': 'Facebook Pixel Browser-ID',
|
|
586
|
+
'_fbc': 'Facebook Click-ID'
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
return descriptions[cookieName] || '';
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Update cookie display in modal
|
|
594
|
+
*/
|
|
595
|
+
updateCookieDisplay() {
|
|
596
|
+
if (!this.modal) return;
|
|
597
|
+
|
|
598
|
+
const categories = ['necessary', 'analytics', 'marketing', 'functional'];
|
|
599
|
+
|
|
600
|
+
categories.forEach(category => {
|
|
601
|
+
this.updateCategoryDisplay(category);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Show uncategorised cookies only in development mode
|
|
605
|
+
if (this.isDevelopmentMode() && this.detectedCookies.uncategorised.length > 0) {
|
|
606
|
+
this.showUncategorisedCookies();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Update individual category display
|
|
612
|
+
* @param {string} category - Category name
|
|
613
|
+
*/
|
|
614
|
+
updateCategoryDisplay(category) {
|
|
615
|
+
const categoryElement = this.modal.querySelector(`[data-category-section="${category}"]`);
|
|
616
|
+
if (!categoryElement) return;
|
|
617
|
+
|
|
618
|
+
const cookies = this.detectedCookies[category] || [];
|
|
619
|
+
const cookieListElement = categoryElement.querySelector('.gdpr-modal__cookie-list');
|
|
620
|
+
const emptyStateElement = categoryElement.querySelector('.gdpr-modal__empty-state');
|
|
621
|
+
const toggleElement = categoryElement.querySelector('input[type="checkbox"]');
|
|
622
|
+
const chevronElement = categoryElement.querySelector('.gdpr-modal__chevron');
|
|
623
|
+
|
|
624
|
+
// Determine expansion state (default collapsed)
|
|
625
|
+
const isExpanded = this.expandedCategories.has(category);
|
|
626
|
+
|
|
627
|
+
// Update empty state
|
|
628
|
+
if (cookies.length === 0) {
|
|
629
|
+
if (emptyStateElement) {
|
|
630
|
+
emptyStateElement.style.display = 'block';
|
|
631
|
+
}
|
|
632
|
+
if (cookieListElement) {
|
|
633
|
+
cookieListElement.style.display = 'none';
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Hide chevron for empty categories
|
|
637
|
+
if (chevronElement) {
|
|
638
|
+
chevronElement.style.display = 'none';
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
if (emptyStateElement) {
|
|
642
|
+
emptyStateElement.style.display = 'none';
|
|
643
|
+
}
|
|
644
|
+
if (cookieListElement) {
|
|
645
|
+
// Always (re)render table contents so data is fresh
|
|
646
|
+
this.renderCookieTable(cookieListElement, cookies);
|
|
647
|
+
|
|
648
|
+
// Show or hide list based on expansion state
|
|
649
|
+
cookieListElement.style.display = isExpanded ? 'block' : 'none';
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Enable toggle for non-empty categories
|
|
653
|
+
if (toggleElement && category !== 'necessary') {
|
|
654
|
+
toggleElement.disabled = false;
|
|
655
|
+
toggleElement.removeAttribute('aria-disabled');
|
|
656
|
+
const toggleContainer = toggleElement.closest('.gdpr-modal__toggle');
|
|
657
|
+
if (toggleContainer) {
|
|
658
|
+
toggleContainer.classList.remove('gdpr-modal__toggle--disabled');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Show chevron for non-empty categories and set orientation
|
|
663
|
+
if (chevronElement) {
|
|
664
|
+
chevronElement.style.display = 'inline';
|
|
665
|
+
chevronElement.textContent = isExpanded ? '▾' : '▶';
|
|
666
|
+
chevronElement.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Render cookie table
|
|
673
|
+
* @param {Element} container - Container element
|
|
674
|
+
* @param {Array} cookies - Array of cookie objects
|
|
675
|
+
*/
|
|
676
|
+
renderCookieTable(container, cookies) {
|
|
677
|
+
const table = document.createElement('table');
|
|
678
|
+
table.className = 'gdpr-modal__cookie-table';
|
|
679
|
+
|
|
680
|
+
// Table header
|
|
681
|
+
const header = document.createElement('thead');
|
|
682
|
+
header.innerHTML = `
|
|
683
|
+
<tr>
|
|
684
|
+
<th>Name</th>
|
|
685
|
+
<th>Dauer</th>
|
|
686
|
+
<th>Beschreibung</th>
|
|
687
|
+
</tr>
|
|
688
|
+
`;
|
|
689
|
+
table.appendChild(header);
|
|
690
|
+
|
|
691
|
+
// Table body
|
|
692
|
+
const body = document.createElement('tbody');
|
|
693
|
+
cookies.forEach(cookie => {
|
|
694
|
+
const row = document.createElement('tr');
|
|
695
|
+
row.innerHTML = `
|
|
696
|
+
<td><code>${this.escapeHtml(cookie.name)}</code></td>
|
|
697
|
+
<td>${this.escapeHtml(cookie.duration)}</td>
|
|
698
|
+
<td>${this.escapeHtml(cookie.description)}</td>
|
|
699
|
+
`;
|
|
700
|
+
body.appendChild(row);
|
|
701
|
+
});
|
|
702
|
+
table.appendChild(body);
|
|
703
|
+
|
|
704
|
+
container.innerHTML = '';
|
|
705
|
+
container.appendChild(table);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Update category states based on detected cookies
|
|
710
|
+
*/
|
|
711
|
+
updateCategoryStates() {
|
|
712
|
+
// Re-enable toggles that now have cookies
|
|
713
|
+
const categories = ['analytics', 'marketing', 'functional'];
|
|
714
|
+
|
|
715
|
+
categories.forEach(category => {
|
|
716
|
+
const cookies = this.detectedCookies[category] || [];
|
|
717
|
+
const toggleElement = this.modal?.querySelector(`#gdpr-${category}`);
|
|
718
|
+
|
|
719
|
+
if (toggleElement && cookies.length > 0) {
|
|
720
|
+
toggleElement.disabled = false;
|
|
721
|
+
toggleElement.removeAttribute('aria-disabled');
|
|
722
|
+
const toggleContainer = toggleElement.closest('.gdpr-modal__toggle');
|
|
723
|
+
if (toggleContainer) {
|
|
724
|
+
toggleContainer.classList.remove('gdpr-modal__toggle--disabled');
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Check if in development mode
|
|
732
|
+
* @returns {boolean} True if in development mode
|
|
733
|
+
*/
|
|
734
|
+
isDevelopmentMode() {
|
|
735
|
+
return window.location.hostname === 'localhost' ||
|
|
736
|
+
window.location.hostname === '127.0.0.1' ||
|
|
737
|
+
window.location.hostname.includes('dev');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Show uncategorised cookies section
|
|
742
|
+
*/
|
|
743
|
+
showUncategorisedCookies() {
|
|
744
|
+
// Implementation would add a special section for uncategorised cookies
|
|
745
|
+
console.log('Uncategorised cookies detected:', this.detectedCookies.uncategorised);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Setup category expansion functionality
|
|
750
|
+
*/
|
|
751
|
+
setupCategoryExpansion() {
|
|
752
|
+
if (!this.modal) return;
|
|
753
|
+
|
|
754
|
+
// Add click handlers for category headers with chevrons
|
|
755
|
+
this.modal.addEventListener('click', (e) => {
|
|
756
|
+
// Allow click either on chevron or entire header row
|
|
757
|
+
const header = e.target.closest('.gdpr-modal__category-header');
|
|
758
|
+
if (header) {
|
|
759
|
+
const category = header.dataset.category;
|
|
760
|
+
if (category) {
|
|
761
|
+
this.toggleCategoryExpansion(category);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Toggle category expansion
|
|
769
|
+
* @param {string} category - Category to toggle
|
|
770
|
+
*/
|
|
771
|
+
toggleCategoryExpansion(category) {
|
|
772
|
+
const categoryElement = this.modal.querySelector(`[data-category-section="${category}"]`);
|
|
773
|
+
const cookieListElement = categoryElement?.querySelector('.gdpr-modal__cookie-list');
|
|
774
|
+
const chevronElement = categoryElement?.querySelector('.gdpr-modal__chevron');
|
|
775
|
+
const cookies = this.detectedCookies[category] || [];
|
|
776
|
+
|
|
777
|
+
// Do not expand categories that currently have no cookies; only show the empty state
|
|
778
|
+
if (!categoryElement || !cookieListElement || cookies.length === 0) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const isExpanded = this.expandedCategories.has(category);
|
|
783
|
+
|
|
784
|
+
if (isExpanded) {
|
|
785
|
+
// Collapse
|
|
786
|
+
cookieListElement.style.display = 'none';
|
|
787
|
+
chevronElement.textContent = '▶';
|
|
788
|
+
chevronElement.setAttribute('aria-expanded', 'false');
|
|
789
|
+
this.expandedCategories.delete(category);
|
|
790
|
+
} else {
|
|
791
|
+
// Expand
|
|
792
|
+
cookieListElement.style.display = 'block';
|
|
793
|
+
chevronElement.textContent = '▾';
|
|
794
|
+
chevronElement.setAttribute('aria-expanded', 'true');
|
|
795
|
+
this.expandedCategories.add(category);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Escape HTML for safe rendering
|
|
801
|
+
* @param {string} text - Text to escape
|
|
802
|
+
* @returns {string} Escaped text
|
|
803
|
+
*/
|
|
804
|
+
escapeHtml(text) {
|
|
805
|
+
const div = document.createElement('div');
|
|
806
|
+
div.textContent = text;
|
|
807
|
+
return div.innerHTML;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Auto-initialize when module is loaded
|
|
812
|
+
let gdprInstance = null;
|
|
813
|
+
|
|
814
|
+
// Initialize GDPR Cookie Consent
|
|
815
|
+
function initGDPRCookieConsent() {
|
|
816
|
+
if (!gdprInstance) {
|
|
817
|
+
gdprInstance = new GDPRCookieConsent();
|
|
818
|
+
}
|
|
819
|
+
return gdprInstance;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function processGdprPlaceholders() {
|
|
823
|
+
const instance = initGDPRCookieConsent();
|
|
824
|
+
const placeholders = document.querySelectorAll(
|
|
825
|
+
'script[type="text/plain"][data-gdpr-category]:not([data-gdpr-loaded="true"])'
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
placeholders.forEach((element) => {
|
|
829
|
+
const category = element.dataset.gdprCategory;
|
|
830
|
+
if (!category || !instance.hasConsent(category)) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const src = element.dataset.gdprSrc;
|
|
835
|
+
const inlineCode = element.textContent && element.textContent.trim();
|
|
836
|
+
|
|
837
|
+
if (!src && !inlineCode) {
|
|
838
|
+
element.dataset.gdprLoaded = 'true';
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const script = document.createElement('script');
|
|
843
|
+
|
|
844
|
+
if (src) {
|
|
845
|
+
script.src = src;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (element.dataset.gdprAsync === 'true') {
|
|
849
|
+
script.async = true;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (element.dataset.gdprDefer === 'true') {
|
|
853
|
+
script.defer = true;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (!src && inlineCode) {
|
|
857
|
+
script.text = inlineCode;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
script.setAttribute('data-gdpr-origin', 'fivo_cookie_consent');
|
|
861
|
+
|
|
862
|
+
if (element.parentNode) {
|
|
863
|
+
element.parentNode.insertBefore(script, element);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
element.dataset.gdprLoaded = 'true';
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function bootstrapGdprScriptLoading() {
|
|
871
|
+
if (document.readyState === 'loading') {
|
|
872
|
+
document.addEventListener('DOMContentLoaded', processGdprPlaceholders);
|
|
873
|
+
} else {
|
|
874
|
+
processGdprPlaceholders();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
document.addEventListener('gdpr:accept', processGdprPlaceholders);
|
|
878
|
+
document.addEventListener('gdpr:change', processGdprPlaceholders);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Export for manual initialization if needed
|
|
882
|
+
const GDPRConsent = {
|
|
883
|
+
getConsent() {
|
|
884
|
+
return initGDPRCookieConsent().getAllConsent();
|
|
885
|
+
},
|
|
886
|
+
hasConsent(category) {
|
|
887
|
+
return initGDPRCookieConsent().hasConsent(category);
|
|
888
|
+
},
|
|
889
|
+
on(eventName, handler) {
|
|
890
|
+
if (!eventName || typeof handler !== 'function') {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
document.addEventListener(eventName, handler);
|
|
894
|
+
},
|
|
895
|
+
off(eventName, handler) {
|
|
896
|
+
if (!eventName || typeof handler !== 'function') {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
document.removeEventListener(eventName, handler);
|
|
900
|
+
},
|
|
901
|
+
reset() {
|
|
902
|
+
const instance = initGDPRCookieConsent();
|
|
903
|
+
instance.clearConsent();
|
|
904
|
+
instance.showBannerForced();
|
|
905
|
+
},
|
|
906
|
+
showBanner() {
|
|
907
|
+
initGDPRCookieConsent().showBannerForced();
|
|
908
|
+
},
|
|
909
|
+
showModal() {
|
|
910
|
+
initGDPRCookieConsent().showModalForced();
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
window.GDPRCookieConsent = GDPRCookieConsent;
|
|
915
|
+
window.initGDPRCookieConsent = initGDPRCookieConsent;
|
|
916
|
+
window.GDPRConsent = GDPRConsent;
|
|
917
|
+
|
|
918
|
+
// Auto-initialize
|
|
919
|
+
initGDPRCookieConsent();
|
|
920
|
+
bootstrapGdprScriptLoading();
|
|
921
|
+
|
|
922
|
+
// Export for ES6 modules
|
|
923
|
+
export default GDPRCookieConsent;
|
|
924
|
+
export { initGDPRCookieConsent, GDPRConsent };
|