UNPKG

gdpr-cookie-consent

Version:

Lightweight, headless cookie consent library providing technical tools for implementing consent mechanisms. Data-attribute driven, no dependencies, framework-agnostic. Legal compliance is user's responsibility - consult legal professionals.

1,322 lines (1,174 loc) 41 kB
/** * GDPR Cookie Consent Library * A headless, data-attribute-driven cookie consent system * * @version 1.0.0 * @author Your Name * @license MIT */ (function (window) { "use strict"; class GDPRCookies { constructor() { this.config = { cookiePrefix: "gdpr_", cookieDuration: 365, categories: [], categoryConfig: {}, onAccept: null, onDecline: null, onSave: null, autoShow: true, showDelay: 1000, }; this.elements = {}; this.currentPreferences = {}; this.isInitialized = false; this.errorLog = []; } /** * Safe error logging that doesn't expose sensitive details */ logError(message, context = {}) { const errorEntry = { timestamp: new Date().toISOString(), message: message, context: this.sanitizeContext(context) }; this.errorLog.push(errorEntry); // Keep only last 10 errors to prevent memory issues if (this.errorLog.length > 10) { this.errorLog.shift(); } // Log to console in development (when not in production) if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { console.warn(`GDPR: ${message}`, context); } } /** * Sanitize context to prevent logging sensitive information */ sanitizeContext(context) { const sanitized = {}; for (const [key, value] of Object.entries(context)) { if (typeof value === 'string' && value.length > 100) { sanitized[key] = '[TRUNCATED]'; } else if (typeof value === 'object' && value !== null) { sanitized[key] = '[OBJECT]'; } else { sanitized[key] = value; } } return sanitized; } /** * Initialize the GDPR system * @param {Object} options - Configuration options */ init(options = {}) { try { if (this.isInitialized) { this.logError('GDPR system already initialized'); return false; } // Validate input if (options !== null && typeof options !== 'object') { this.logError('Invalid options provided to init method', { optionsType: typeof options }); return false; } // Merge configuration safely try { this.config = { ...this.config, ...options }; } catch (mergeError) { this.logError('Failed to merge configuration options'); return false; } // Set default category configurations this.setupDefaultCategories(); // Find and cache DOM elements this.cacheElements(); // Setup event listeners this.bindEvents(); // Initialize categories UI this.renderCategories(); // Check existing consent this.loadExistingConsent(); this.isInitialized = true; this.updateStatusDisplay(); return true; } catch (error) { this.logError('Critical error during initialization'); this.isInitialized = false; return false; } } /** * Setup default category configurations */ setupDefaultCategories() { const defaults = { analytics: { title: "📊 Analytics Cookies", description: "Help us understand how visitors interact with our website", scripts: [], onAccept: null, onDecline: null, }, marketing: { title: "🎯 Marketing Cookies", description: "Used to deliver personalized advertisements and measure their effectiveness", scripts: [], onAccept: null, onDecline: null, }, functional: { title: "⚙️ Functional Cookies", description: "Enable enhanced functionality and personalization features", scripts: [], onAccept: null, onDecline: null, }, }; // Merge user config with defaults this.config.categories.forEach((category) => { if (!this.config.categoryConfig[category]) { this.config.categoryConfig[category] = defaults[category] || { title: `${ category.charAt(0).toUpperCase() + category.slice(1) } Cookies`, description: `Cookies for ${category} purposes`, scripts: [], }; } }); } /** * Cache DOM elements using data attributes */ cacheElements() { try { this.elements = { banner: this.safeQuerySelector("[data-gdpr-banner]"), modal: this.safeQuerySelector("[data-gdpr-modal]"), categoriesContainer: this.safeQuerySelector("[data-gdpr-categories]"), categoryTemplate: this.safeQuerySelector("[data-gdpr-category-template]"), statusDisplay: this.safeQuerySelector("[data-gdpr-status]"), }; // Validate required elements are available this.validateRequiredElements(); } catch (error) { this.logError('Failed to cache DOM elements'); } } /** * Safe DOM query selector with error handling */ safeQuerySelector(selector) { try { if (!selector || typeof selector !== 'string') { this.logError('Invalid selector provided', { selector }); return null; } return document.querySelector(selector); } catch (error) { this.logError('DOM query failed', { selector }); return null; } } /** * Validate that required DOM elements are present */ validateRequiredElements() { if (!this.elements.banner) { this.logError('Banner element with [data-gdpr-banner] not found'); } if (!this.elements.modal) { this.logError('Modal element with [data-gdpr-modal] not found'); } if (!this.elements.categoriesContainer) { this.logError('Categories container with [data-gdpr-categories] not found'); } if (!this.elements.categoryTemplate) { this.logError('Category template with [data-gdpr-category-template] not found'); } } /** * Bind event listeners using event delegation */ bindEvents() { try { // Use event delegation for all GDPR actions document.addEventListener("click", (e) => { try { if (!e.target) return; const action = e.target.getAttribute("data-gdpr-action"); if (action) { this.handleAction(action, e); } } catch (error) { this.logError('Error handling click event'); } }); // Handle category toggle changes document.addEventListener("change", (e) => { try { if (!e.target) return; const toggle = e.target.getAttribute("data-category-toggle"); if (toggle !== null) { const categoryElement = e.target.closest("[data-category]"); if (categoryElement) { const category = categoryElement.getAttribute("data-category"); if (category) { this.currentPreferences[category] = e.target.checked; } } } } catch (error) { this.logError('Error handling change event'); } }); // Close modal on overlay click if (this.elements.modal) { this.elements.modal.addEventListener("click", (e) => { try { if (e.target === this.elements.modal) { this.hideModal(); } } catch (error) { this.logError('Error handling modal click'); } }); } // Keyboard accessibility document.addEventListener("keydown", (e) => { try { if (e.key === "Escape") { this.hideModal(); } } catch (error) { this.logError('Error handling keydown event'); } }); } catch (error) { this.logError('Critical error binding events'); } } /** * Handle action button clicks */ handleAction(action, event) { event.preventDefault(); switch (action) { case "show-banner": this.showBanner(); break; case "show-preferences": this.showModal(); break; case "close-modal": this.hideModal(); break; case "accept-all": this.acceptAll(); break; case "accept-essential": this.acceptEssential(); break; case "save-preferences": this.savePreferences(); break; case "clear-all": this.clearAll(); break; default: break; } } /** * Render category toggles dynamically */ renderCategories() { try { if ( !this.elements.categoriesContainer || !this.elements.categoryTemplate ) { this.logError('Required elements missing for category rendering'); return; } // Clear existing categories safely try { this.elements.categoriesContainer.replaceChildren(); } catch (clearError) { // Fallback for older browsers this.elements.categoriesContainer.innerHTML = ''; } // Validate categories array if (!Array.isArray(this.config.categories)) { this.logError('Categories configuration is not an array'); return; } // Create category elements from template this.config.categories.forEach((categoryKey) => { try { if (!categoryKey || typeof categoryKey !== 'string') { this.logError('Invalid category key', { categoryKey }); return; } const config = this.config.categoryConfig[categoryKey]; if (!config) { this.logError('No configuration found for category', { categoryKey }); return; } const template = this.elements.categoryTemplate.content.cloneNode(true); if (!template) { this.logError('Failed to clone category template'); return; } // Set category data safely const container = template.querySelector(".cookie-category"); if (container) { container.setAttribute("data-category", categoryKey); } else { this.logError('Cookie category container not found in template'); } const title = template.querySelector("[data-category-title]"); const description = template.querySelector("[data-category-description]"); const toggle = template.querySelector("[data-category-toggle]"); if (title && config.title) { title.textContent = config.title; } if (description && config.description) { description.textContent = config.description; } if (toggle) { toggle.checked = this.currentPreferences[categoryKey] || false; } this.elements.categoriesContainer.appendChild(template); } catch (categoryError) { this.logError('Failed to render category', { categoryKey }); } }); } catch (error) { this.logError('Critical error in category rendering'); } } /** * Cookie management methods */ setCookie(name, value, days = this.config.cookieDuration) { try { if (!name || typeof name !== 'string') { this.logError('Invalid cookie name provided', { name }); return false; } if (value === undefined) { this.logError('Cookie value is undefined', { cookieName: name }); return false; } const expires = new Date(Date.now() + days * 864e5).toUTCString(); const secure = location.protocol === "https:" ? "; Secure" : ""; // Use Strict SameSite for better security const sameSite = "; SameSite=Strict"; // Add domain for subdomain support const domain = this.getDomainForCookie(); let serializedValue; try { serializedValue = encodeURIComponent(JSON.stringify(value)); } catch (serializeError) { this.logError('Failed to serialize cookie value', { cookieName: name }); return false; } const cookieString = `${ this.config.cookiePrefix }${name}=${serializedValue}; expires=${expires}; path=/${domain}${sameSite}${secure}`; // Validate cookie string length (browsers have limits) if (cookieString.length > 4096) { this.logError('Cookie string too long', { cookieName: name, length: cookieString.length }); return false; } document.cookie = cookieString; // Verify cookie was set by attempting to read it back const verification = this.getCookie(name); if (verification === null && value !== null) { this.logError('Cookie verification failed', { cookieName: name }); return false; } return true; } catch (error) { this.logError('Cookie setting failed', { cookieName: name }); return false; } } getCookie(name) { try { if (!name || typeof name !== 'string') { this.logError('Invalid cookie name provided', { name }); return null; } const cookieValue = document.cookie .split("; ") .find((row) => row.startsWith(`${this.config.cookiePrefix}${name}=`)) ?.split("=")[1]; if (cookieValue) { try { return JSON.parse(decodeURIComponent(cookieValue)); } catch (parseError) { this.logError('Failed to parse cookie value', { cookieName: name }); // Try to return the raw value as fallback try { return decodeURIComponent(cookieValue); } catch (decodeError) { this.logError('Failed to decode cookie value', { cookieName: name }); return null; } } } return null; } catch (error) { this.logError('Cookie retrieval failed', { cookieName: name }); return null; } } deleteCookie(name) { try { if (!name || typeof name !== 'string') { this.logError('Invalid cookie name provided for deletion', { name }); return false; } const domain = this.getDomainForCookie(); document.cookie = `${this.config.cookiePrefix}${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`; // Also try deleting without domain for broader compatibility document.cookie = `${this.config.cookiePrefix}${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; return true; } catch (error) { this.logError('Cookie deletion failed', { cookieName: name }); return false; } } /** * Get domain string for cookie setting */ getDomainForCookie() { // Only set domain for non-localhost environments to support subdomains const hostname = location.hostname; if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes(':')) { return ''; } // Use the current domain, allowing cookies to be accessible to subdomains const parts = hostname.split('.'); if (parts.length > 2) { // For subdomains like 'app.example.com', use '.example.com' return `; domain=.${parts.slice(-2).join('.')}`; } return `; domain=${hostname}`; } /** * Load existing consent from cookies */ loadExistingConsent() { const consent = this.getCookie("consent"); const preferences = this.getCookie("preferences"); if (consent && preferences) { this.currentPreferences = preferences; this.applyConsent(preferences); this.updateCategoryToggles(); } else if (this.config.autoShow) { // Show banner after delay for new users setTimeout(() => this.showBanner(), this.config.showDelay); } } /** * Update category toggle states */ updateCategoryToggles() { try { if (!Array.isArray(this.config.categories)) { this.logError('Categories configuration is not an array'); return; } this.config.categories.forEach((category) => { try { if (!category || typeof category !== 'string') { this.logError('Invalid category in updateCategoryToggles', { category }); return; } const toggle = this.safeQuerySelector( `[data-category="${category}"] [data-category-toggle]` ); if (toggle) { toggle.checked = this.currentPreferences[category] || false; } } catch (toggleError) { this.logError('Failed to update category toggle', { category }); } }); } catch (updateError) { this.logError('Critical error updating category toggles'); } } /** * Apply consent decisions (load/unload scripts) */ applyConsent(preferences) { this.config.categories.forEach((category) => { const config = this.config.categoryConfig[category]; const accepted = preferences[category]; if (accepted) { // Load scripts for this category this.loadCategoryScripts(category); // Call onAccept callback if (config.onAccept && typeof config.onAccept === "function") { config.onAccept(category); } } else { // Unload scripts for this category this.unloadCategoryScripts(category); // Call onDecline callback if (config.onDecline && typeof config.onDecline === "function") { config.onDecline(category); } } }); } /** * Load scripts for a specific category */ async loadCategoryScripts(category) { try { if (!category || typeof category !== 'string') { this.logError('Invalid category provided for script loading', { category }); return; } const config = this.config.categoryConfig[category]; if (!config) { this.logError('No configuration found for category', { category }); return; } if (config.scripts && Array.isArray(config.scripts) && config.scripts.length > 0) { // Load scripts sequentially to avoid conflicts for (const scriptConfig of config.scripts) { try { await this.loadScript(scriptConfig, category); } catch (scriptError) { this.logError('Failed to load script in category', { category, script: scriptConfig }); // Continue loading other scripts even if one fails } } } } catch (categoryError) { this.logError('Critical error loading category scripts', { category }); } } /** * Unload scripts for a specific category */ unloadCategoryScripts(category) { // Remove scripts with data-gdpr-category attribute const scripts = document.querySelectorAll( `script[data-gdpr-category="${category}"]` ); scripts.forEach((script) => script.remove()); // Clean up category-specific cookies this.cleanupCategoryCookies(category); } /** * Validate URL to prevent script injection attacks */ isValidScriptUrl(url) { try { const parsedUrl = new URL(url, location.origin); // Only allow HTTP and HTTPS protocols if (!['http:', 'https:'].includes(parsedUrl.protocol)) { return false; } // Prevent javascript: and data: URLs if (parsedUrl.protocol === 'javascript:' || parsedUrl.protocol === 'data:') { return false; } return true; } catch (e) { return false; } } /** * Load a script dynamically with error handling and timeout */ loadScript(scriptConfig, category) { return new Promise((resolve, reject) => { try { if (!scriptConfig) { this.logError('No script configuration provided'); reject(new Error('No script configuration')); return; } if (!category || typeof category !== 'string') { this.logError('Invalid category provided for script loading', { category }); reject(new Error('Invalid category')); return; } const script = document.createElement("script"); script.setAttribute("data-gdpr-category", category); let scriptSrc; let timeout = 10000; // Default 10 second timeout if (typeof scriptConfig === "string") { scriptSrc = scriptConfig; } else if (typeof scriptConfig === 'object' && scriptConfig !== null) { scriptSrc = scriptConfig.src; if (scriptConfig.async !== undefined) script.async = scriptConfig.async; if (scriptConfig.defer !== undefined) script.defer = scriptConfig.defer; if (scriptConfig.type) script.type = scriptConfig.type; if (scriptConfig.timeout && typeof scriptConfig.timeout === 'number') { timeout = scriptConfig.timeout; } } else { this.logError('Invalid script configuration format', { scriptConfig }); reject(new Error('Invalid script configuration')); return; } if (!scriptSrc || typeof scriptSrc !== 'string') { this.logError('No script source URL provided'); reject(new Error('No script source')); return; } // Validate the script URL before loading if (!this.isValidScriptUrl(scriptSrc)) { this.logError('Invalid script URL blocked for security', { url: scriptSrc }); reject(new Error('Invalid script URL')); return; } // Set up timeout handler const timeoutId = setTimeout(() => { this.logError('Script loading timeout', { url: scriptSrc, category, timeout }); script.remove(); reject(new Error('Script loading timeout')); }, timeout); // Handle successful loading script.onload = () => { clearTimeout(timeoutId); resolve(); }; // Handle loading errors script.onerror = (error) => { clearTimeout(timeoutId); this.logError('Script loading failed', { url: scriptSrc, category }); script.remove(); reject(new Error('Script loading failed')); }; script.src = scriptSrc; document.head.appendChild(script); } catch (error) { this.logError('Critical error in script loading', { category }); reject(error); } }); } /** * Clean up cookies for a specific category */ cleanupCategoryCookies(category) { const cookiesToClean = { analytics: [ "_ga", "_ga_*", "_gid", "_gat", "__utma", "__utmb", "__utmc", "__utmt", "__utmz", ], marketing: [ "_fbp", "_fbc", "fr", "__Secure-FRCMP", "DSID", "IDE", "test_cookie", ], functional: [], }; const cookies = cookiesToClean[category] || []; cookies.forEach((cookieName) => { if (cookieName.includes("*")) { // Handle wildcard cookies const prefix = cookieName.replace("*", ""); document.cookie.split(";").forEach((cookie) => { const name = cookie.split("=")[0].trim(); if (name.startsWith(prefix)) { const domain = this.getDomainForCookie(); document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`; } }); } else { const domain = this.getDomainForCookie(); document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/${domain}`; } }); } /** * UI control methods */ showBanner() { try { if (this.elements.banner) { this.elements.banner.style.display = "block"; // Trigger reflow for animation this.elements.banner.offsetHeight; this.elements.banner.classList.add("show"); } else { this.logError('Banner element not available for display'); } } catch (error) { this.logError('Failed to show banner'); } } hideBanner() { try { if (this.elements.banner) { this.elements.banner.classList.remove("show"); setTimeout(() => { try { if (this.elements.banner) { this.elements.banner.style.display = "none"; } } catch (hideError) { this.logError('Failed to hide banner after timeout'); } }, 300); } } catch (error) { this.logError('Failed to initiate banner hiding'); } } showModal() { try { if (this.elements.modal) { this.updateCategoryToggles(); this.elements.modal.style.display = "flex"; // Trigger reflow for animation this.elements.modal.offsetHeight; this.elements.modal.classList.add("show"); try { document.body.style.overflow = "hidden"; } catch (bodyError) { this.logError('Failed to set body overflow'); } } else { this.logError('Modal element not available for display'); } } catch (error) { this.logError('Failed to show modal'); } } hideModal() { try { if (this.elements.modal) { this.elements.modal.classList.remove("show"); try { document.body.style.overflow = ""; } catch (bodyError) { this.logError('Failed to reset body overflow'); } setTimeout(() => { try { if (this.elements.modal) { this.elements.modal.style.display = "none"; } } catch (hideError) { this.logError('Failed to hide modal after timeout'); } }, 300); } } catch (error) { this.logError('Failed to initiate modal hiding'); } } /** * Consent action methods */ acceptAll() { const preferences = { essential: true }; this.config.categories.forEach((category) => { preferences[category] = true; }); this.saveConsent("all", preferences); this.hideBanner(); this.hideModal(); if (this.config.onAccept) { this.config.onAccept(preferences); } } acceptEssential() { const preferences = { essential: true }; this.config.categories.forEach((category) => { preferences[category] = false; }); this.saveConsent("essential", preferences); this.hideBanner(); this.hideModal(); if (this.config.onDecline) { this.config.onDecline(preferences); } } savePreferences() { const preferences = { essential: true, ...this.currentPreferences }; this.saveConsent("custom", preferences); this.hideModal(); this.hideBanner(); if (this.config.onSave) { this.config.onSave(preferences); } } saveConsent(type, preferences) { this.setCookie("consent", { type: type, timestamp: new Date().toISOString(), preferences: preferences, }); this.setCookie("preferences", preferences); this.currentPreferences = preferences; this.applyConsent(preferences); this.updateStatusDisplay(); } clearAll() { // Clear consent cookies this.deleteCookie("consent"); this.deleteCookie("preferences"); // Unload all category scripts this.config.categories.forEach((category) => { this.unloadCategoryScripts(category); }); // Reset preferences this.currentPreferences = {}; this.updateCategoryToggles(); this.updateStatusDisplay(); // Show banner again if (this.config.autoShow) { this.showBanner(); } } /** * Update status display for demo purposes */ updateStatusDisplay() { if (!this.elements.statusDisplay) return; const consent = this.getCookie("consent"); const preferences = this.getCookie("preferences") || {}; // Clear existing content safely this.elements.statusDisplay.replaceChildren(); // Create status elements using safe DOM manipulation const statusTitle = document.createElement('strong'); statusTitle.textContent = 'Consent Status:'; this.elements.statusDisplay.appendChild(statusTitle); this.elements.statusDisplay.appendChild(document.createElement('br')); const typeText = document.createTextNode(`Type: ${consent ? consent.type : 'Not set'}`); this.elements.statusDisplay.appendChild(typeText); this.elements.statusDisplay.appendChild(document.createElement('br')); const timestampText = document.createTextNode(`Timestamp: ${ consent ? new Date(consent.timestamp).toLocaleString() : 'N/A' }`); this.elements.statusDisplay.appendChild(timestampText); this.elements.statusDisplay.appendChild(document.createElement('br')); this.elements.statusDisplay.appendChild(document.createElement('br')); const categoriesTitle = document.createElement('strong'); categoriesTitle.textContent = 'Categories:'; this.elements.statusDisplay.appendChild(categoriesTitle); this.elements.statusDisplay.appendChild(document.createElement('br')); const essentialText = document.createTextNode(`Essential: ${preferences.essential ? '✅' : '❌'}`); this.elements.statusDisplay.appendChild(essentialText); this.elements.statusDisplay.appendChild(document.createElement('br')); this.config.categories.forEach((category) => { const categoryText = document.createTextNode(`${category}: ${preferences[category] ? '✅' : '❌'}`); this.elements.statusDisplay.appendChild(categoryText); this.elements.statusDisplay.appendChild(document.createElement('br')); }); } /** * Public API methods */ getConsent() { try { if (!this.isInitialized) { this.logError('GDPR system not initialized - cannot get consent'); return null; } return this.getCookie("consent"); } catch (error) { this.logError('Failed to get consent'); return null; } } getPreferences() { try { if (!this.isInitialized) { this.logError('GDPR system not initialized - cannot get preferences'); return {}; } return this.getCookie("preferences") || {}; } catch (error) { this.logError('Failed to get preferences'); return {}; } } hasConsent(category = null) { try { if (!this.isInitialized) { this.logError('GDPR system not initialized - cannot check consent'); return false; } if (category !== null && (typeof category !== 'string' || category.trim() === '')) { this.logError('Invalid category provided to hasConsent', { category }); return false; } const preferences = this.getPreferences(); if (category) { return preferences[category] === true; } return Object.keys(preferences).length > 0; } catch (error) { this.logError('Failed to check consent status', { category }); return false; } } updateCategory(category, enabled) { try { if (!this.isInitialized) { this.logError('GDPR system not initialized - cannot update category'); return false; } if (!category || typeof category !== 'string' || category.trim() === '') { this.logError('Invalid category provided to updateCategory', { category }); return false; } if (typeof enabled !== 'boolean') { this.logError('Invalid enabled value provided to updateCategory', { category, enabled }); return false; } if (!Array.isArray(this.config.categories) || !this.config.categories.includes(category)) { this.logError('Category not found in configuration', { category }); return false; } const preferences = this.getPreferences(); preferences[category] = enabled; this.saveConsent("custom", preferences); return true; } catch (error) { this.logError('Failed to update category', { category, enabled }); return false; } } addScript(category, scriptConfig) { try { if (!this.isInitialized) { this.logError('GDPR system not initialized - cannot add script'); return false; } if (!category || typeof category !== 'string' || category.trim() === '') { this.logError('Invalid category provided to addScript', { category }); return false; } if (!scriptConfig) { this.logError('No script configuration provided to addScript', { category }); return false; } // Validate script configuration format if (typeof scriptConfig === 'string') { if (scriptConfig.trim() === '') { this.logError('Empty script URL provided', { category }); return false; } } else if (typeof scriptConfig === 'object' && scriptConfig !== null) { if (!scriptConfig.src || typeof scriptConfig.src !== 'string' || scriptConfig.src.trim() === '') { this.logError('Invalid script source in configuration', { category }); return false; } } else { this.logError('Invalid script configuration format', { category, configType: typeof scriptConfig }); return false; } if (!this.config.categoryConfig[category]) { this.logError('Category configuration not found for addScript', { category }); return false; } this.config.categoryConfig[category].scripts.push(scriptConfig); // If category is already accepted, load the script immediately if (this.hasConsent(category)) { this.loadScript(scriptConfig, category).catch(() => { this.logError('Failed to load newly added script', { category }); }); } return true; } catch (error) { this.logError('Failed to add script', { category }); return false; } } onCategoryChange(category, callback) { try { if (!this.isInitialized) { this.logError('GDPR system not initialized - cannot set category change handler'); return false; } if (!category || typeof category !== 'string' || category.trim() === '') { this.logError('Invalid category provided to onCategoryChange', { category }); return false; } if (!callback || typeof callback !== 'function') { this.logError('Invalid callback provided to onCategoryChange', { category }); return false; } if (!this.config.categoryConfig[category]) { this.config.categoryConfig[category] = { title: `${category} Cookies`, description: `Cookies for ${category} functionality`, scripts: [], }; } const config = this.config.categoryConfig[category]; const originalOnAccept = config.onAccept; const originalOnDecline = config.onDecline; config.onAccept = (cat) => { try { if (originalOnAccept && typeof originalOnAccept === 'function') { originalOnAccept(cat); } callback(cat, true); } catch (callbackError) { this.logError('Error in category accept callback', { category: cat }); } }; config.onDecline = (cat) => { try { if (originalOnDecline && typeof originalOnDecline === 'function') { originalOnDecline(cat); } callback(cat, false); } catch (callbackError) { this.logError('Error in category decline callback', { category: cat }); } }; return true; } catch (error) { this.logError('Failed to set category change handler', { category }); return false; } } } // Create global instance const gdprInstance = new GDPRCookies(); // Expose to global scope with error handling wrappers window.GDPRCookies = { init: (options) => { try { return gdprInstance.init(options); } catch (error) { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { console.error('GDPR: Failed to initialize'); } return false; } }, getConsent: () => { try { return gdprInstance.getConsent(); } catch (error) { return null; } }, getPreferences: () => { try { return gdprInstance.getPreferences(); } catch (error) { return {}; } }, hasConsent: (category) => { try { return gdprInstance.hasConsent(category); } catch (error) { return false; } }, updateCategory: (category, enabled) => { try { return gdprInstance.updateCategory(category, enabled); } catch (error) { return false; } }, addScript: (category, script) => { try { return gdprInstance.addScript(category, script); } catch (error) { return false; } }, onCategoryChange: (category, callback) => { try { return gdprInstance.onCategoryChange(category, callback); } catch (error) { return false; } }, showBanner: () => { try { return gdprInstance.showBanner(); } catch (error) { return false; } }, showPreferences: () => { try { return gdprInstance.showModal(); } catch (error) { return false; } }, clearAll: () => { try { return gdprInstance.clearAll(); } catch (error) { return false; } }, }; // Auto-initialize if config is provided via data attribute document.addEventListener("DOMContentLoaded", () => { try { const configScript = document.querySelector("script[data-gdpr-config]"); if (configScript) { const configAttr = configScript.getAttribute("data-gdpr-config"); if (!configAttr || configAttr.trim() === '') { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { console.warn('GDPR: Empty configuration attribute found'); } return; } try { const config = JSON.parse(configAttr); if (typeof config !== 'object' || config === null) { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { console.error('GDPR: Invalid configuration format - must be an object'); } return; } window.GDPRCookies.init(config); } catch (parseError) { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { console.error('GDPR: Failed to parse configuration - invalid JSON format'); } } } } catch (error) { if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { console.error('GDPR: Auto-initialization failed'); } } }); })(window);