PageSourceSearch

https://spinderup.dk/external/cookie-popup/cookie-consent.js

js spinderup.dk collected 2026-09-27 07:06:26 UTC 44,989 bytes, 1,072 lines download raw bytes

1// Shoporama Cookie Consent Interceptor v1.0
2// External script version
3//
4// GDPR Compliance Cookie Interceptor:
5// Dette script overskriver document.cookie på prototype-niveau for at blokere
6// alle cookies indtil brugeren giver eksplicit samtykke. Nødvendige cookies
7// (kurv, login, etc.) tillades altid via whitelist. Andre cookies kategoriseres
8// automatisk som Analytics eller Marketing baseret på navn-patterns og blokeres
9// indtil relevant samtykke gives. Blokerede cookies gemmes i hukommelsen og
10// sættes først når/hvis brugeren accepterer den pågældende kategori.
11// Google Consent Mode v2 opdateres automatisk baseret på brugerens valg.
12//
13(function() {
14    'use strict';
15
16    // Get configuration from global variable set by Shoporama
17    var config = window.shoporamaCookieConfig || {};
18    
19    // Ensure default values
20    config.consentCookieName = config.consentCookieName || 'shoporama_consent';
21    config.consentDuration = config.consentDuration || 365;
22    
23    // Check multiple sources for display mode configuration:
24    // 1. Direct config object (if Shoporama sets it)
25    // 2. Global variable that Shoporama might set
26    // 3. Data attribute on script tag
27    // 4. Default to overlay
28    
29    // Try to get from global variable first
30    if (typeof window.SHOPORAMA_COOKIE_DISPLAY_MODE !== 'undefined') {
31        config.displayMode = window.SHOPORAMA_COOKIE_DISPLAY_MODE;
32    }
33    // Try to get from data attribute on current script tag
34    else if (document.currentScript && document.currentScript.dataset.displayMode) {
35        config.displayMode = document.currentScript.dataset.displayMode;
36    }
37    // Default to overlay
38    else if (!config.displayMode) {
39        config.displayMode = 'overlay';
40    }
41    
42    // Default texts if not provided
43    config.texts = config.texts || {
44        bannerText: 'Vi bruger cookies til at forbedre din oplevelse på webshoppen.',
45        readMore: 'Læs mere',
46        acceptAll: 'Accepter alle',
47        settings: 'Indstillinger',
48        reject: 'Afvis alle',
49        settingsTitle: 'Cookie indstillinger',
50        settingsText: 'Vælg hvilke cookies du vil tillade:',
51        necessary: 'Nødvendige cookies (kan ikke fravælges)',
52        analytics: 'Statistik cookies',
53        analyticsDesc: 'Hjælper os med at forstå hvordan webshoppen bruges',
54        marketing: 'Marketing cookies',
55        marketingDesc: 'Bruges til at vise relevante annoncer',
56        save: 'Gem valg',
57        cancel: 'Tilbage'
58    };
59    
60    // Default policy content if not provided
61    config.policyContent = config.policyContent || '';
62
63    // Gem original cookie descriptor
64    var originalCookie = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
65
66    // Nødvendige cookies der altid tillades
67    var cookieWhitelist = [
68        'basket', 'customer_token', 'user_token', 'language',
69        'unpaid_order_id', 'shoporama_consent', 'currency', 'PHPSESSID',
70        'st', 'ref'
71    ];
72
73    // Prefix whitelist (funktionelle cookies med dynamisk suffix)
74    var prefixWhitelist = ['np_closed_'];
75
76    // Gem blokerede cookies til senere
77    var blockedCookies = {};
78    var blockedLocalStorage = {};
79
80    // Check for existing consent
81    function getConsent() {
82        var cookies = originalCookie.get.call(document).split(';');
83        for (var i = 0; i < cookies.length; i++) {
84            var cookie = cookies[i].trim();
85            if (cookie.indexOf(config.consentCookieName + '=') === 0) {
86                var value = cookie.substring(config.consentCookieName.length + 1);
87                if (value) {
88                    try {
89                        return JSON.parse(decodeURIComponent(value));
90                    } catch(e) {
91                        // Ignore invalid consent cookie
92                    }
93                }
94            }
95        }
96        return null;
97    }
98
99    // Public consent API for apps
100    // Apps can use: shoporamaConsent.onConsent('statistics', load)
101    // or: shoporamaConsent.onConsent(['statistics', 'marketing'], load)
102    var existingConsent = getConsent();
103    window.shoporamaConsent = {
104        categories: {
105            necessary: true,
106            preferences: false,
107            statistics: existingConsent ? existingConsent.analytics === true : false,
108            marketing: existingConsent ? existingConsent.marketing === true : false
109        },
110
111        isAllowed: function(category) {
112            return this.categories[category] === true;
113        },
114
115        grant: function(category) {
116            this.categories[category] = true;
117            dispatchEvent(new CustomEvent('shoporama:consent:' + category));
118        },
119
120        onConsent: function(categories, callback) {
121            categories = Array.isArray(categories) ? categories : [categories];
122            var self = this;
123            var remaining = categories.filter(function(c) { return !self.isAllowed(c); });
124
125            if (remaining.length === 0) {
126                callback();
127            }
128            else {
129                remaining.forEach(function(category) {
130                    addEventListener('shoporama:consent:' + category, function() {
131                        remaining = remaining.filter(function(c) { return c !== category; });
132                        if (remaining.length === 0) callback();
133                    }, { once: true });
134                });
135            }
136        }
137    };
138
139    // Kategoriser cookie baseret på navn
140    function categorizeCookie(cookieName) {
141        // Analytics cookies - Google, Adobe, Hotjar, Mixpanel, etc.
142        if (cookieName.match(/^(_ga|_gid|_gat|_gac_|_gcl_|gtm_|_gtm|_gaq|__utm|_hjid|_hjfirstseen|_hjincludedinsample|_hj|hj|mp_|__mp|_omappvp|_omappvs|s_|sc_|_tq|ajs_|_dc_gtm|_fbq)/i)) {
143            return 'analytics';
144        }
145
146        // Marketing cookies - Facebook, LinkedIn, Pinterest, TikTok, Snapchat, Twitter/X, etc.
147        if (cookieName.match(/^(_fbp|_fbc|fr|tr|sb|c_user|_pin_|_pinterest_|_linkedin_|li_|bcookie|bscookie|lidc|_form_|ac_|vuid|__ar_|mc_|partnerads|pa_|pacid|cFPID|_ttp|_tt_enable_cookie|_uetsid|_uetvid|MUID|_clck|_clsk|__kla_|klaviyo|_gcl_aw|_gcl_dc|_schn|sc_at|personalization_id|guest_id|_twitter_sess)/i)) {
148            return 'marketing';
149        }
150
151        // Cookie consent management platforms
152        if (cookieName.match(/^(cookiebot|CookieConsent|euconsent|consentUUID|_cmpRepromptHash|OptanonConsent|OptanonAlertBoxClosed)/i)) {
153            return 'necessary';
154        }
155
156        // Chat/Support widgets - Intercom, Zendesk, Drift, etc.
157        if (cookieName.match(/^(intercom|_hjSessionUser|_hjSession|drift_|__zlcmid|__zlcstore|zd-store|__hssc|__hssrc|__hstc|hubspotutk)/i)) {
158            return 'marketing';
159        }
160
161        // A/B Testing platforms - Optimizely, VWO, etc.
162        if (cookieName.match(/^(optimizelyEndUserId|optimizelySegments|optimizelyBuckets|_vis_opt_|_vwo_)/i)) {
163            return 'analytics';
164        }
165
166        return 'unknown';
167    }
168
169    // Override document.cookie setter
170    Object.defineProperty(document, 'cookie', {
171        get: function() {
172            return originalCookie.get.call(this);
173        },
174        set: function(value) {
175            // Parse cookie name
176            var cookieName = value.split('=')[0].trim();
177
178            // Check whitelist
179            var isWhitelisted = cookieWhitelist.some(function(allowed) {
180                return cookieName === allowed;
181            });
182
183            // Always allow whitelisted cookies
184            if (isWhitelisted) {
185                return originalCookie.set.call(this, value);
186            }
187
188            // Check prefix whitelist
189            var isPrefixWhitelisted = prefixWhitelist.some(function(prefix) {
190                return cookieName.indexOf(prefix) === 0;
191            });
192            if (isPrefixWhitelisted) {
193                return originalCookie.set.call(this, value);
194            }
195
196            // Get current consent
197            var consent = getConsent();
198
199            // If no consent, block and store
200            if (!consent) {
201                blockedCookies[cookieName] = value;
202                return;
203            }
204
205            // Categorize cookie
206            var category = categorizeCookie(cookieName);
207
208            // Check category consent
209            if (category === 'analytics' && !consent.analytics) {
210                blockedCookies[cookieName] = value;
211                return;
212            }
213
214            // Treat unknown cookies as marketing cookies
215            if ((category === 'marketing' || category === 'unknown') && !consent.marketing) {
216                blockedCookies[cookieName] = value;
217                return;
218            }
219
220            // Cookie is allowed
221            return originalCookie.set.call(this, value);
222        },
223        configurable: true
224    });
225
226    // Override localStorage for tracking keys
227    var originalSetItem = Storage.prototype.setItem;
228    Storage.prototype.setItem = function(key, value) {
229        var consent = getConsent();
230
231        // Expanded list of known tracking localStorage keys
232        var trackingKeys = /^(_ga|_gid|_fbp|_fbc|_pin|linkedin|klaviyo|hj|_hj|ab_|optimizely|gtm|_gtm|amplitude|mixpanel|mp_|intercom|drift|_vwo|_vis_opt|_hjSessionUser|_hjSession|__mp)/i;
233
234        if (!consent && trackingKeys.test(key)) {
235            blockedLocalStorage[key] = value;
236            return;
237        }
238
239        return originalSetItem.call(this, key, value);
240    };
241
242    // Create and inject styles
243    function injectStyles() {
244        // Load external CSS
245        var link = document.createElement('link');
246        link.rel = 'stylesheet';
247        link.href = '/external/cookie-popup/cookie-consent.css';
248        document.head.appendChild(link);
249
250        // Add color customization CSS
251        if (config.colors) {
252            var colorCSS = `
253                #shoporama-cookie-banner .accept,
254                #shoporama-cookie-settings .accept {
255                    background: ${config.colors.acceptColor} !important;
256                    border-color: ${config.colors.acceptColor} !important;
257                }
258                #shoporama-cookie-banner .accept:hover,
259                #shoporama-cookie-settings .accept:hover {
260                    background: ${config.colors.acceptHoverColor} !important;
261                    border-color: ${config.colors.acceptHoverColor} !important;
262                }
263                #shoporama-cookie-banner .reject,
264                #shoporama-cookie-settings .reject {
265                    background: ${config.colors.rejectColor} !important;
266                    border-color: ${config.colors.rejectColor} !important;
267                }
268                #shoporama-cookie-banner .reject:hover,
269                #shoporama-cookie-settings .reject:hover {
270                    background: ${config.colors.rejectHoverColor} !important;
271                    border-color: ${config.colors.rejectHoverColor} !important;
272                }
273            `;
274            var colorStyle = document.createElement('style');
275            colorStyle.textContent = colorCSS;
276            document.head.appendChild(colorStyle);
277        }
278
279        // Add custom CSS if provided
280        if (config.customCSS) {
281            var style = document.createElement('style');
282            style.textContent = config.customCSS;
283            document.head.appendChild(style);
284        }
285    }
286
287    // Create banner HTML
288    function createBanner() {
289        // Remove consent check - we want to show banner when user clicks button
290        // if (getConsent()) return;
291
292        injectStyles();
293
294        var isSticky = config.displayMode === 'sticky';
295        var overlay, bannerContainer, banner;
296
297        if (isSticky) {
298            // Sticky bar mode - no overlay needed
299            banner = document.createElement('div');
300            banner.id = 'shoporama-cookie-banner';
301            banner.className = 'sticky-mode';
302            banner.innerHTML = `
303                <div class="sticky-content">
304                    <div class="sticky-text">
305                        <p>${config.texts.bannerText}</p>
306                        <a href="#" onclick="shoporamaCookies.showPolicy(); return false;">${config.texts.readMore}</a>
307                    </div>
308                    <div class="sticky-buttons">
309                        <button class="accept" onclick="shoporamaCookies.accept()">${config.texts.acceptAll}</button>
310                        <button class="reject" onclick="shoporamaCookies.reject()">${config.texts.reject}</button>
311                        <button class="settings" onclick="shoporamaCookies.showSettings()">${config.texts.settings}</button>
312                    </div>
313                </div>
314            `;
315        } else {
316            // Classic overlay mode
317            overlay = document.createElement('div');
318            overlay.id = 'shoporama-cookie-overlay';
319
320            bannerContainer = document.createElement('div');
321            bannerContainer.id = 'shoporama-cookie-banner-container';
322
323            // Add click handler to shake banner when clicking outside
324            bannerContainer.onclick = function(e) {
325                if (e.target === bannerContainer) {
326                    var banner = document.getElementById('shoporama-cookie-banner');
327                    if (banner) {
328                        banner.classList.add('shake');
329                        setTimeout(function() {
330                            banner.classList.remove('shake');
331                        }, 300);
332                    }
333                }
334            };
335
336            banner = document.createElement('div');
337            banner.id = 'shoporama-cookie-banner';
338            banner.innerHTML = `
339                <div class="banner-text">
340                    <h3>${config.texts.bannerTitle || 'Dette website bruger cookies'}</h3>
341                    <p>${config.texts.bannerText}</p>
342                    <p>${config.texts.bannerSubtext || 'Oplysninger om din brug af vores hjemmeside deles også med vores partnere inden for sociale medier, annoncering, og analyse, som kan kombinere dem med andre data fra dine interaktioner med deres tjenester.'}</p>
343                    <p>
344                        <a href="#" onclick="shoporamaCookies.showPolicy(); return false;">${config.texts.readMore}</a>
345                    </p>
346                </div>
347                <div class="buttons">
348                    <button class="accept" onclick="shoporamaCookies.accept()">${config.texts.acceptAll}</button>
349                    <button class="reject" onclick="shoporamaCookies.reject()">${config.texts.reject}</button>
350                    <button class="settings" onclick="shoporamaCookies.showSettings()">${config.texts.settings}</button>
351                </div>
352            `;
353
354            bannerContainer.appendChild(banner);
355        }
356
357        // Create policy modal
358        var policyModal = document.createElement('div');
359        policyModal.id = 'shoporama-cookie-policy';
360        policyModal.className = 'cookie-modal';
361        policyModal.innerHTML = `
362            <div class="modal-header">
363                <h3>Cookie Politik</h3>
364            </div>
365            <div class="modal-body">
366                ${config.policyContent || '<p>Cookie politik indhold ikke tilgængeligt.</p>'}
367            </div>
368            <div class="modal-footer">
369                <button class="back" onclick="shoporamaCookies.hidePolicy()"><span style="display: inline-flex; align-items: center; gap: 0.5rem;">‹ Tilbage</span></button>
370            </div>
371        `;
372
373        // Create settings modal
374        var settings = document.createElement('div');
375        settings.id = 'shoporama-cookie-settings';
376
377        // Add click handler for settings modal background (only for overlay mode)
378        if (!isSticky && overlay) {
379            overlay.addEventListener('click', function(e) {
380                if (e.target === overlay && settings.style.display !== 'none') {
381                    settings.classList.add('shake');
382                    setTimeout(function() {
383                        settings.classList.remove('shake');
384                    }, 300);
385                }
386            });
387        }
388
389        settings.innerHTML = `
390            <h3>${config.texts.settingsTitle}</h3>
391
392            <div class="cookie-groups">
393                <!-- Necessary Cookies -->
394                <div class="cookie-group">
395                    <div class="cookie-group-header">
396                        <div class="cookie-info">
397                            <h4>${config.texts.necessary}</h4>
398                            <p>Disse cookies er nødvendige for at websitet kan fungere</p>
399                        </div>
400                        <div class="toggle-switch disabled">
401                            <input type="checkbox" checked disabled>
402                            <span class="toggle-slider"></span>
403                        </div>
404                    </div>
405                </div>
406
407                <!-- Analytics Cookies -->
408                <div class="cookie-group">
409                    <div class="cookie-group-header">
410                        <div class="cookie-info">
411                            <h4>${config.texts.analytics}</h4>
412                            <p>${config.texts.analyticsDesc}</p>
413                        </div>
414                        <label class="toggle-switch">
415                            <input type="checkbox" id="consent-analytics">
416                            <span class="toggle-slider"></span>
417                        </label>
418                    </div>
419                </div>
420
421                <!-- Marketing Cookies -->
422                <div class="cookie-group">
423                    <div class="cookie-group-header">
424                        <div class="cookie-info">
425                            <h4>${config.texts.marketing}</h4>
426                            <p>${config.texts.marketingDesc}</p>
427                        </div>
428                        <label class="toggle-switch">
429                            <input type="checkbox" id="consent-marketing">
430                            <span class="toggle-slider"></span>
431                        </label>
432                    </div>
433                </div>
434            </div>
435
436            <div class="button-group">
437                <button class="back" onclick="shoporamaCookies.hideSettings()"><span style="display: inline-flex; align-items: center; gap: 0.5rem;">‹ ${config.texts.cancel || 'Tilbage'}</span></button>
438                <div class="save-buttons">
439                    <button class="reject" onclick="shoporamaCookies.reject()">${config.texts.reject}</button>
440                    <button class="save" onclick="shoporamaCookies.saveSettings()">${config.texts.save || 'Gem indstillinger'}</button>
441                    <button class="accept" onclick="shoporamaCookies.accept()">${config.texts.acceptAll}</button>
442                </div>
443            </div>
444        `;
445
446        // Add to DOM when ready
447        function addToDOM() {
448            if (isSticky) {
449                // Sticky mode - just add banner and modals
450                document.body.appendChild(banner);
451                document.body.appendChild(settings);
452                document.body.appendChild(policyModal);
453                
454                // Create overlay for modals only
455                var modalOverlay = document.createElement('div');
456                modalOverlay.id = 'shoporama-cookie-overlay';
457                modalOverlay.style.display = 'none';
458                document.body.appendChild(modalOverlay);
459                
460                // Show banner immediately
461                setTimeout(function() {
462                    banner.classList.add('show');
463                }, 100);
464            } else {
465                // Overlay mode - add all elements
466                document.body.appendChild(overlay);
467                document.body.appendChild(bannerContainer);
468                document.body.appendChild(settings);
469                document.body.appendChild(policyModal);
470            }
471
472            // Show overlay with fade effect (only for overlay mode)
473            setTimeout(function() {
474                if (!isSticky && overlay) {
475                    overlay.style.display = 'block';
476                }
477                setTimeout(function() {
478                    overlay.classList.add('show');
479                }, 10);
480            }, 10);
481        }
482
483        if (document.body) {
484            addToDOM();
485        } else {
486            document.addEventListener('DOMContentLoaded', addToDOM);
487        }
488    }
489
490    // Process blocked cookies after consent
491    function processBlockedCookies(consent) {
492        // Re-set allowed cookies
493        for (var cookieName in blockedCookies) {
494            var category = categorizeCookie(cookieName);
495            var allowed = false;
496
497            if (category === 'analytics' && consent.analytics) allowed = true;
498            if (category === 'marketing' && consent.marketing) allowed = true;
499            if (category === 'unknown' && (consent.analytics || consent.marketing)) allowed = true;
500
501            if (allowed) {
502                originalCookie.set.call(document, blockedCookies[cookieName]);
503            }
504        }
505
506        // Process blocked localStorage
507        for (var key in blockedLocalStorage) {
508            if (consent.analytics || consent.marketing) {
509                originalSetItem.call(localStorage, key, blockedLocalStorage[key]);
510            }
511        }
512
513        // Clear blocked storage
514        blockedCookies = {};
515        blockedLocalStorage = {};
516    }
517
518    // Initialize default consent states for Google Consent Mode v2
519    function initDefaultConsentMode() {
520        // Ensure dataLayer exists
521        window.dataLayer = window.dataLayer || [];
522        
523        // Define gtag function if it doesn't exist
524        if (typeof window.gtag !== 'function') {
525            window.gtag = function() {
526                window.dataLayer.push(arguments);
527            };
528        }
529        
530        // Set default consent states (denied by default for GDPR compliance)
531        window.gtag('consent', 'default', {
532            'analytics_storage': 'denied',
533            'ad_storage': 'denied',
534            'ad_user_data': 'denied',
535            'ad_personalization': 'denied',
536            'functionality_storage': 'granted',
537            'security_storage': 'granted',
538            'wait_for_update': 2000 // Wait up to 2 seconds for user consent
539        });
540        
541        // Also set region-specific defaults for EU/UK (optional but recommended)
542        window.gtag('consent', 'default', {
543            'analytics_storage': 'denied',
544            'ad_storage': 'denied',
545            'ad_user_data': 'denied',
546            'ad_personalization': 'denied',
547            'region': ['AD', 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IS', 'IE', 'IT', 'LV', 'LI', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', '
547SE', 'GB', 'CH'],
548            'wait_for_update': 2000
549        });
550    }
551
552    // Update Google Consent Mode
553    function updateConsentMode(consent) {
554        // Ensure gtag is available
555        if (typeof window.gtag !== 'function') {
556            window.dataLayer = window.dataLayer || [];
557            window.gtag = function() {
558                window.dataLayer.push(arguments);
559            };
560        }
561        
562        // Update consent states based on user choice
563        window.gtag('consent', 'update', {
564            'analytics_storage': consent.analytics ? 'granted' : 'denied',
565            'ad_storage': consent.marketing ? 'granted' : 'denied',
566            'ad_user_data': consent.marketing ? 'granted' : 'denied',
567            'ad_personalization': consent.marketing ? 'granted' : 'denied'
568        });
569
570        // Trigger dataLayer event
571        if (window.dataLayer) {
572            window.dataLayer.push({
573                'event': 'cookie_consent_update',
574                'consent_analytics': consent.analytics,
575                'consent_marketing': consent.marketing
576            });
577        }
578    }
579
580    // Public API
581    window.shoporamaCookies = {
582        accept: function() {
583            this.setConsent({ analytics: true, marketing: true });
584        },
585
586        reject: function() {
587            this.setConsent({ analytics: false, marketing: false });
588        },
589
590        showSettings: function() {
591            var isSticky = config.displayMode === 'sticky';
592            var banner = document.getElementById('shoporama-cookie-banner');
593            var bannerContainer = document.getElementById('shoporama-cookie-banner-container');
594            var modal = document.getElementById('shoporama-cookie-settings');
595            var overlay = document.getElementById('shoporama-cookie-overlay');
596
597            if (modal) {
598                // Load current preferences
599                var consent = getConsent();
600                if (consent) {
601                    document.getElementById('consent-analytics').checked = consent.analytics;
602                    document.getElementById('consent-marketing').checked = consent.marketing;
603                }
604
605                // Hide banner and show settings
606                if (isSticky && banner) {
607                    banner.style.display = 'none';
608                } else if (bannerContainer) {
609                    bannerContainer.style.display = 'none';
610                }
611                
612                // Show overlay for modal
613                if (overlay) {
614                    overlay.style.display = 'block';
615                }
616                
617                modal.style.display = 'block';
618            }
619        },
620
621        hideSettings: function() {
622            var isSticky = config.displayMode === 'sticky';
623            var banner = document.getElementById('shoporama-cookie-banner');
624            var bannerContainer = document.getElementById('shoporama-cookie-banner-container');
625            var modal = document.getElementById('shoporama-cookie-settings');
626            var overlay = document.getElementById('shoporama-cookie-overlay');
627
628            if (modal) {
629                // Hide settings
630                modal.style.display = 'none';
631                
632                // Show banner again
633                if (isSticky && banner) {
634                    banner.style.display = 'block';
635                    // Hide overlay for sticky mode
636                    if (overlay) {
637                        overlay.style.display = 'none';
638                    }
639                } else if (bannerContainer) {
640                    bannerContainer.style.display = 'flex';
641                    // Keep overlay visible for overlay mode
642                }
643            }
644        },
645
646        saveSettings: function() {
647            var analytics = document.getElementById('consent-analytics').checked;
648            var marketing = document.getElementById('consent-marketing').checked;
649            this.setConsent({ analytics: analytics, marketing: marketing });
650            this.hideSettings();
651        },
652
653        setConsent: function(consent) {
654            // Get previous consent to detect changes
655            var previousConsent = getConsent();
656            
657            // Add timestamp
658            consent.timestamp = new Date().toISOString();
659            consent.version = '1.0';
660
661            // If consent is being withdrawn (was true, now false), delete cookies
662            if (previousConsent) {
663                if (previousConsent.analytics && !consent.analytics) {
664                    this.deleteAnalyticsCookies();
665                }
666                if (previousConsent.marketing && !consent.marketing) {
667                    this.deleteMarketingCookies();
668                }
669            }
670
671            // Save consent cookie
672            var expires = new Date();
673            expires.setTime(expires.getTime() + (config.consentDuration * 24 * 60 * 60 * 1000));
674
675            originalCookie.set.call(document, config.consentCookieName + '=' +
676                encodeURIComponent(JSON.stringify(consent)) +
677                '; expires=' + expires.toUTCString() +
678                '; path=/; SameSite=Lax');
679
680            // Process blocked cookies
681            processBlockedCookies(consent);
682
683            // Update consent mode
684            updateConsentMode(consent);
685
686            // Fire consent events for apps
687            if (consent.analytics) {
688                window.shoporamaConsent.grant('statistics');
689            }
690            if (consent.marketing) {
691                window.shoporamaConsent.grant('marketing');
692            }
693
694            // Hide overlay and containers
695            var isSticky = config.displayMode === 'sticky';
696            var overlay = document.getElementById('shoporama-cookie-overlay');
697            var banner = document.getElementById('shoporama-cookie-banner');
698            var bannerContainer = document.getElementById('shoporama-cookie-banner-container');
699            var settings = document.getElementById('shoporama-cookie-settings');
700
701            if (overlay) overlay.style.display = 'none';
702            if (isSticky && banner) {
703                banner.style.display = 'none';
704            } else if (bannerContainer) {
705                bannerContainer.style.display = 'none';
706            }
707            if (settings) settings.style.display = 'none';
708
709            // Reload page to ensure all tracking scripts initialize properly
710            setTimeout(function() {
711                window.location.reload();
712            }, 100);
713        },
714
715        // Delete analytics cookies
716        deleteAnalyticsCookies: function() {
717            var analyticsCookies = [
718                '_ga', '_gid', '_gat', '_gac_', '_gcl_', 'gtm_', '_gtm', '_gaq', '__utm',
719                '_hjid', '_hjfirstseen', '_hjincludedinsample', '_hj', 'hj',
720                'mp_', '__mp', '_omappvp', '_omappvs', 's_', 'sc_', '_tq', 'ajs_',
721                '_dc_gtm', '_fbq', 'optimizelyEndUserId', 'optimizelySegments', 
722                'optimizelyBuckets', '_vis_opt_', '_vwo_'
723            ];
724            
725            this.deleteCookiesByPattern(analyticsCookies);
726            this.clearLocalStorageByPattern(analyticsCookies);
727        },
728
729        // Delete marketing cookies
730        deleteMarketingCookies: function() {
731            var marketingCookies = [
732                '_fbp', '_fbc', 'fr', 'tr', 'sb', 'c_user', '_pin_', '_pinterest_',
733                '_linkedin_', 'li_', 'bcookie', 'bscookie', 'lidc', '_form_', 'ac_',
734                'vuid', '__ar_', 'mc_', 'partnerads', 'pa_', 'pacid', 'cFPID',
735                '_ttp', '_tt_enable_cookie', '_uetsid', '_uetvid', 'MUID', '_clck',
736                '_clsk', '__kla_', 'klaviyo', '_gcl_aw', '_gcl_dc', '_schn', 'sc_at',
737                'personalization_id', 'guest_id', '_twitter_sess', 'intercom',
738                'drift_', '__zlcmid', '__zlcstore', 'zd-store', '__hssc', '__hssrc',
739                '__hstc', 'hubspotutk'
740            ];
741            
742            this.deleteCookiesByPattern(marketingCookies);
743            this.clearLocalStorageByPattern(marketingCookies);
744        },
745
746        // Helper function to delete cookies by pattern
747        deleteCookiesByPattern: function(patterns) {
748            // Get all cookies
749            var cookies = document.cookie.split(';');
750            
751            for (var i = 0; i < cookies.length; i++) {
752                var cookie = cookies[i].trim();
753                var cookieName = cookie.split('=')[0];
754                
755                // Check if cookie matches any pattern
756                for (var j = 0; j < patterns.length; j++) {
757                    if (cookieName.indexOf(patterns[j]) === 0) {
758                        // Delete cookie for current domain and all parent domains
759                        this.deleteCookie(cookieName);
760                        break;
761                    }
762                }
763            }
764        },
765
766        // Delete a specific cookie
767        deleteCookie: function(name) {
768            // Try to delete for various domain/path combinations
769            var pathBits = location.pathname.split('/');
770            var pathCurrent = '/';
771            
772            // Delete for current path and all parent paths
773            originalCookie.set.call(document, name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/');
774            originalCookie.set.call(document, name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=' + window.location.hostname);
775            originalCookie.set.call(document, name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.' + window.location.hostname);
776            
777            // Try parent domain (e.g., .example.com for sub.example.com)
778            var domainParts = window.location.hostname.split('.');
779            if (domainParts.length >= 2) {
780                var parentDomain = '.' + domainParts.slice(-2).join('.');
781                originalCookie.set.call(document, name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=' + parentDomain);
782            }
783            
784            // Try all path variations
785            for (var i = 0; i < pathBits.length; i++) {
786                if (pathBits[i]) {
787                    pathCurrent += pathBits[i] + '/';
788                    originalCookie.set.call(document, name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=' + pathCurrent);
789                }
790            }
791        },
792
793        // Clear localStorage items by pattern
794        clearLocalStorageByPattern: function(patterns) {
795            if (typeof Storage === 'undefined') return;
796            
797            var keysToRemove = [];
798            
799            // Find all matching keys
800            for (var i = 0; i < localStorage.length; i++) {
801                var key = localStorage.key(i);
802                
803                for (var j = 0; j < patterns.length; j++) {
804                    if (key && key.indexOf(patterns[j]) === 0) {
805                        keysToRemove.push(key);
806                        break;
807                    }
808                }
809            }
810            
811            // Remove the keys
812            for (var k = 0; k < keysToRemove.length; k++) {
813                try {
814                    localStorage.removeItem(keysToRemove[k]);
815                } catch(e) {
816                    // Ignore errors
817                }
818            }
819            
820            // Also try sessionStorage
821            keysToRemove = [];
822            for (var i = 0; i < sessionStorage.length; i++) {
823                var key = sessionStorage.key(i);
824                
825                for (var j = 0; j < patterns.length; j++) {
826                    if (key && key.indexOf(patterns[j]) === 0) {
827                        keysToRemove.push(key);
828                        break;
829                    }
830                }
831            }
832            
833            for (var k = 0; k < keysToRemove.length; k++) {
834                try {
835                    sessionStorage.removeItem(keysToRemove[k]);
836                } catch(e) {
837                    // Ignore errors
838                }
839            }
840        },
841
842        // Get current consent status
843        getConsent: getConsent,
844
845        // Show policy modal
846        showPolicy: function() {
847            var isSticky = config.displayMode === 'sticky';
848            var policyModal = document.getElementById('shoporama-cookie-policy');
849            var overlay = document.getElementById('shoporama-cookie-overlay');
850
851            if (policyModal) {
852                // Show overlay for modal in sticky mode
853                if (isSticky && overlay) {
854                    overlay.style.display = 'block';
855                }
856                
857                policyModal.style.display = 'flex';
858
859                // Create a click handler for the policy modal
860                var clickHandler = function(e) {
861                    // Check if click is on the modal background (not on modal content)
862                    if (e.target === policyModal) {
863                        shoporamaCookies.hidePolicy();
864                    }
865                };
866
867                // Add click handler to policy modal
868                policyModal.addEventListener('click', clickHandler);
869
870                // Store the handler so we can remove it later
871                policyModal._clickHandler = clickHandler;
872
873                // Also add click handler to banner container if visible
874                if (bannerContainer && bannerContainer.style.display !== 'none') {
875                    bannerContainer.addEventListener('click', function(e) {
876                        if (e.target === bannerContainer) {
877                            shoporamaCookies.hidePolicy();
878                        }
879                    });
880                }
881
882                // Add escape key handler
883                var escapeHandler = function(e) {
884                    if (e.key === 'Escape') {
885                        shoporamaCookies.hidePolicy();
886                    }
887                };
888                document.addEventListener('keydown', escapeHandler);
889
890                // Store the handler so we can remove it later
891                policyModal._escapeHandler = escapeHandler;
892            }
893        },
894
895        // Hide policy modal
896        hidePolicy: function() {
897            var isSticky = config.displayMode === 'sticky';
898            var policyModal = document.getElementById('shoporama-cookie-policy');
899            var overlay = document.getElementById('shoporama-cookie-overlay');
900            
901            if (policyModal) {
902                policyModal.style.display = 'none';
903                
904                // Hide overlay in sticky mode when closing policy
905                if (isSticky && overlay) {
906                    overlay.style.display = 'none';
907                }
908
909                // Remove event listeners
910                if (policyModal._clickHandler) {
911                    policyModal.removeEventListener('click', policyModal._clickHandler);
912                    delete policyModal._clickHandler;
913                }
914
915                if (policyModal._escapeHandler) {
916                    document.removeEventListener('keydown', policyModal._escapeHandler);
917                    delete policyModal._escapeHandler;
918                }
919            }
920        },
921
922        // Reset consent (for testing)
923        reset: function() {
924            originalCookie.set.call(document, config.consentCookieName + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/');
925            window.location.reload();
926        },
927
928        // Open banner
929        openBanner: function() {
930            // Remove existing elements if any
931            var existingOverlay = document.getElementById('shoporama-cookie-overlay');
932            var existingContainer = document.getElementById('shoporama-cookie-banner-container');
933            var existingSettings = document.getElementById('shoporama-cookie-settings');
934            var existingPolicy = document.getElementById('shoporama-cookie-policy');
935
936            if (existingOverlay) existingOverlay.remove();
937            if (existingContainer) existingContainer.remove();
938            if (existingSettings) existingSettings.remove();
939            if (existingPolicy) existingPolicy.remove();
940
941            // Create new banner
942            createBanner();
943        }
944    };
945
946    // Create cookie settings button
947    function createSettingsButton() {
948        // Create floating button
949        var settingsBtn = document.createElement('button');
950        settingsBtn.id = 'shoporama-cookie-settings-btn';
951        settingsBtn.innerHTML = '🍪';
952        settingsBtn.title = 'Cookie indstillinger';
953
954        // Check if mobile - multiple detection methods
955        var isMobile = window.innerWidth <= 768 ||
956                      /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
957                      ('ontouchstart' in window);
958
959        settingsBtn.style.cssText = `
960            position: fixed;
961            bottom: ${isMobile ? 'calc(20px + env(safe-area-inset-bottom, 0px))' : '20px'};
962            left: ${isMobile ? '15px' : '20px'};
963            width: ${isMobile ? '44px' : '40px'};
964            height: ${isMobile ? '44px' : '40px'};
965            padding: 0;
966            background: ${isMobile ? '#ffffff' : 'rgba(255, 255, 255, 0.9)'};
967            border: 1px solid ${isMobile ? '#000000' : 'rgba(209, 213, 219, 0.5)'};
968            border-radius: 50%;
969            color: #374151;
970            font-size: ${isMobile ? '20px' : '18px'};
971            cursor: pointer;
972            z-index: 2147483647;
973            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
974            transition: all 0.15s;
975            display: flex !important;
976            align-items: center;
977            justify-content: center;
978            opacity: ${isMobile ? '0.9' : '0.7'};
979            -webkit-tap-highlight-color: transparent;
980            visibility: visible !important;
981        `;
982
983        settingsBtn.onmouseover = function() {
984            this.style.opacity = '1';
985            this.style.transform = 'scale(1.05)';
986        };
987
988        settingsBtn.onmouseout = function() {
989            this.style.opacity = '0.7';
990            this.style.transform = 'scale(1)';
991        };
992
993        var handleClick = function(e) {
994            e.preventDefault();
995            e.stopPropagation();
996
997            // Force create banner without consent check
998            var existingOverlay = document.getElementById('shoporama-cookie-overlay');
999            var existingContainer = document.getElementById('shoporama-cookie-banner-container');
1000
1001            if (existingOverlay) existingOverlay.remove();
1002            if (existingContainer) existingContainer.remove();
1003
1004            // Temporarily clear consent to bypass check
1005            var savedConsent = getConsent();
1006            originalCookie.set.call(document, config.consentCookieName + '=; path=/');
1007
1008            createBanner();
1009
1010            // Restore consent
1011            if (savedConsent) {
1012                originalCookie.set.call(document, config.consentCookieName + '=' +
1013                    encodeURIComponent(JSON.stringify(savedConsent)) + '; path=/');
1014            }
1015        };
1016
1017        // Add both click and touch events for better mobile support
1018        settingsBtn.addEventListener('click', handleClick);
1019        settingsBtn.addEventListener('touchstart', handleClick);
1020
1021        // Add to DOM when ready
1022        function addToDOM() {
1023            document.body.appendChild(settingsBtn);
1024            settingsBtn.style.display = 'flex'; // Ensure it's visible
1025
1026            // Extra check for mobile
1027            if (isMobile) {
1028                // Force re-apply styles in case something overwrote them
1029                setTimeout(function() {
1030                    settingsBtn.style.position = 'fixed';
1031                    settingsBtn.style.bottom = '20px';
1032                    settingsBtn.style.left = '15px';
1033                    settingsBtn.style.display = 'flex';
1034                    settingsBtn.style.zIndex = '2147483647';
1035                }, 100);
1036            }
1037        }
1038
1039        if (document.body) {
1040            addToDOM();
1041        } else {
1042            document.addEventListener('DOMContentLoaded', addToDOM);
1043        }
1044    }
1045
1046    // Initialize consent system
1047    function init() {
1048        // CRITICAL: Set default consent states FIRST (before any tags fire)
1049        // This ensures Google tags start in 'denied' state for GDPR compliance
1050        initDefaultConsentMode();
1051        
1052        // Check for existing consent
1053        var consent = getConsent();
1054
1055        if (!consent) {
1056            // No consent - show banner
1057            createBanner();
1058        } else {
1059            // Apply existing consent (this will update from default 'denied' to user's actual choice)
1060            updateConsentMode(consent);
1061            // Show settings button when consent exists
1062            createSettingsButton();
1063        }
1064    }
1065
1066    // Start when DOM is ready
1067    if (document.readyState === 'loading') {
1068        document.addEventListener('DOMContentLoaded', init);
1069    } else {
1070        init();
1071    }
1072})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.