svarog-ui-core
Version:
Core components of Svarog UI without themes
584 lines (501 loc) • 16.7 kB
JavaScript
// src/components/CookieConsent/CookieConsent.js
import { createElement } from '../../utils/componentFactory.js';
import { createStyleInjector } from '../../utils/styleInjection.js';
import { cookieConsentStyles } from './CookieConsent.styles.js';
import Modal from '../Modal/index.js';
import Checkbox from '../Checkbox/index.js';
const injectCookieConsentStyles = createStyleInjector('CookieConsent');
// Default cookie categories - GDPR compliant with realistic texts
const DEFAULT_COOKIE_CATEGORIES = {
necessary: {
id: 'necessary',
name: 'Technisch notwendige Cookies',
description:
'Diese Cookies sind für den Betrieb der Website unerlässlich. Sie ermöglichen grundlegende Funktionen wie Seitennavigation, Zugriff auf sichere Bereiche und das Speichern von Warenkorb-Inhalten. Diese Cookies sammeln keine Informationen, die Sie identifizieren könnten.',
required: true,
enabled: true,
},
functional: {
id: 'functional',
name: 'Funktionale Cookies',
description:
'Diese Cookies ermöglichen erweiterte Funktionen und Personalisierung. Sie können von uns oder von Drittanbietern gesetzt werden, deren Dienste wir auf unseren Seiten verwenden. Beispiele: Spracheinstellungen, Standortdienste, Live-Chat-Funktionen.',
required: false,
enabled: false,
},
analytics: {
id: 'analytics',
name: 'Analyse-Cookies',
description:
'Diese Cookies helfen uns zu verstehen, wie Besucher mit unserer Website interagieren, indem sie Informationen anonym sammeln und melden. Wir verwenden diese Daten zur Verbesserung unserer Website (z.B. Google Analytics, Hotjar).',
required: false,
enabled: false,
},
marketing: {
id: 'marketing',
name: 'Marketing- und Werbe-Cookies',
description:
'Diese Cookies werden verwendet, um Ihnen relevante Werbeanzeigen zu zeigen und die Wirksamkeit von Werbekampagnen zu messen. Sie können auch verwendet werden, um Ihre Interessen zu identifizieren und Ihnen gezielte Werbung auf anderen Websites zu zeigen.',
required: false,
enabled: false,
},
};
/**
* Cookie storage manager
*/
class CookieManager {
static CONSENT_KEY = 'svarog_cookie_consent';
static VERSION = '1.0';
static EXPIRY_DAYS = 365;
static saveConsent(preferences) {
const consent = {
version: this.VERSION,
timestamp: new Date().toISOString(),
preferences,
expires: new Date(
Date.now() + this.EXPIRY_DAYS * 24 * 60 * 60 * 1000
).toISOString(),
};
try {
localStorage.setItem(this.CONSENT_KEY, JSON.stringify(consent));
this.setCookie(
this.CONSENT_KEY,
JSON.stringify(consent),
this.EXPIRY_DAYS
);
} catch (error) {
console.warn('Could not save cookie consent:', error);
}
}
static getConsent() {
try {
const stored =
localStorage.getItem(this.CONSENT_KEY) ||
this.getCookie(this.CONSENT_KEY);
if (!stored) {
return null;
}
const consent = JSON.parse(stored);
if (new Date(consent.expires) < new Date()) {
this.clearConsent();
return null;
}
return consent;
} catch (error) {
console.warn('Could not read cookie consent:', error);
return null;
}
}
static clearConsent() {
try {
localStorage.removeItem(this.CONSENT_KEY);
this.deleteCookie(this.CONSENT_KEY);
} catch (error) {
console.warn('Could not clear cookie consent:', error);
}
}
static hasValidConsent() {
const consent = this.getConsent();
return consent && consent.version === this.VERSION;
}
static setCookie(name, value, days) {
const expires = new Date(
Date.now() + days * 24 * 60 * 60 * 1000
).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;
}
static getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return decodeURIComponent(parts.pop().split(';').shift());
}
return null;
}
static deleteCookie(name) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
}
}
/**
* Creates a CookieConsent component using Modal and Checkbox components
*/
const createCookieConsent = (props = {}) => {
const state = {
autoShow: props.autoShow !== false,
mode: props.mode || 'simple',
position: props.position || 'bottom',
modal: props.modal || false,
title: props.title || 'Cookie-Einstellungen',
description: props.description,
privacyPolicyUrl: props.privacyPolicyUrl,
imprintUrl: props.imprintUrl,
customCategories: props.customCategories || {},
className: ['cookie-consent', props.className].filter(Boolean).join(' '),
...props,
};
let currentModal = null;
const categoryCheckboxes = new Map(); // Store checkbox component references
// Get merged categories with GDPR compliance
const getCategories = () => {
const categories = {};
Object.keys(DEFAULT_COOKIE_CATEGORIES).forEach((key) => {
categories[key] = { ...DEFAULT_COOKIE_CATEGORIES[key] };
});
Object.keys(state.customCategories).forEach((key) => {
categories[key] = { ...categories[key], ...state.customCategories[key] };
});
// GDPR compliance: non-required categories default to false
Object.values(categories).forEach((category) => {
if (!category.required) {
category.enabled = category.enabled === true ? true : false;
}
});
return categories;
};
/**
* Creates the categories section using Checkbox components
*/
const createCategoriesSection = (categories) => {
// Clear existing checkbox references
categoryCheckboxes.clear();
let html = '<div class="cookie-consent__categories">';
Object.values(categories).forEach((category) => {
const requiredClass = category.required
? ' cookie-consent__category--required'
: '';
html += `
<div class="cookie-consent__category${requiredClass}">
<div class="cookie-consent__category-header">
<div class="cookie-consent__category-checkbox-container" data-category="${category.id}">
<!-- Checkbox will be inserted here -->
</div>
</div>
<div class="cookie-consent__category-description">${category.description}</div>
</div>
`;
});
html += '</div>';
return html;
};
/**
* Setup checkboxes after content is rendered
*/
const setupCategoryCheckboxes = (categories, onCategoryChange) => {
// Clear existing checkbox references
categoryCheckboxes.forEach((checkbox) => {
checkbox.destroy();
});
categoryCheckboxes.clear();
Object.values(categories).forEach((category) => {
const container = document.querySelector(
`[data-category="${category.id}"]`
);
if (!container) {
return;
}
// Create checkbox using Checkbox component
const checkbox = Checkbox({
id: `cookie-category-${category.id}`,
label: category.name,
checked: category.enabled, // Using standard checked prop
disabled: category.required,
showValidation: false, // No validation messages in cookie consent
onChange: (event, checked) => {
// Update category state
category.enabled = checked;
onCategoryChange?.(category.id, checked);
},
});
// Store checkbox reference for cleanup
categoryCheckboxes.set(category.id, checkbox);
// Add required badge if needed
const checkboxElement = checkbox.getElement();
if (category.required) {
const badge = createElement('span', {
className: 'cookie-consent__required-badge',
textContent: 'Erforderlich',
});
checkboxElement.appendChild(badge);
}
// Replace container content with checkbox
container.replaceWith(checkboxElement);
});
};
/**
* Creates the description section
*/
const createDescriptionSection = () => {
const defaultDescription = `
<p><strong>Ihre Privatsphäre ist uns wichtig.</strong></p>
<p>Wir verwenden Cookies und ähnliche Technologien, um Ihnen die bestmögliche Nutzererfahrung zu bieten, den Traffic zu analysieren und Inhalte zu personalisieren.</p>
<p>Sie können selbst entscheiden, welche Cookie-Kategorien Sie zulassen möchten. Bitte beachten Sie, dass aufgrund Ihrer Einstellungen möglicherweise nicht alle Funktionen der Website verfügbar sind.</p>
<p>Weitere Informationen finden Sie in unserer Datenschutzerklärung.</p>
`;
return `<div class="cookie-consent__description">${state.description || defaultDescription}</div>`;
};
/**
* Creates the legal links section
*/
const createLegalLinksSection = () => {
if (!state.privacyPolicyUrl && !state.imprintUrl) {
return '';
}
let html = '<div class="cookie-consent__legal">';
if (state.privacyPolicyUrl) {
html += `<a href="${state.privacyPolicyUrl}" target="_blank" rel="noopener noreferrer" class="cookie-consent__legal-link">Datenschutzerklärung</a>`;
}
if (state.imprintUrl) {
html += `<a href="${state.imprintUrl}" target="_blank" rel="noopener noreferrer" class="cookie-consent__legal-link">Impressum</a>`;
}
html += '</div>';
return html;
};
/**
* Creates the main content as HTML string
*/
const createContentHTML = () => {
// Inject styles on render
injectCookieConsentStyles(cookieConsentStyles);
const categories = getCategories();
let html = '<div class="cookie-consent__content">';
// Add description
html += createDescriptionSection();
// Add categories for detailed mode
if (state.mode === 'detailed') {
html += createCategoriesSection(categories, (categoryId, enabled) => {
props.onCategoryChange?.(categoryId, enabled);
});
}
// Add legal links
html += createLegalLinksSection();
html += '</div>';
return html;
};
const getActions = () => {
const actions = [];
// Reject button (necessary only)
actions.push({
text: 'Nur notwendige',
variant: 'secondary',
action: 'reject',
});
// Details button (only in simple mode)
if (state.mode === 'simple') {
actions.push({
text: 'Einstellungen anpassen',
variant: 'secondary',
action: 'details',
});
}
// Accept selected button (only in detailed mode)
if (state.mode === 'detailed') {
actions.push({
text: 'Auswahl speichern',
variant: 'primary',
action: 'accept-selected',
});
}
// Accept all button
actions.push({
text: 'Alle akzeptieren',
variant: 'primary',
action: 'accept-all',
});
return actions;
};
const handleAction = (action) => {
const categories = getCategories();
switch (action) {
case 'reject': {
const rejectPreferences = {};
Object.keys(categories).forEach((key) => {
rejectPreferences[key] = categories[key].required;
});
handleAccept(rejectPreferences);
break;
}
case 'details':
state.mode = 'detailed';
recreateModal();
props.onShowDetails?.();
break;
case 'accept-selected': {
const selectedPreferences = {};
// Get preferences from checkbox components
categoryCheckboxes.forEach((checkbox, categoryId) => {
selectedPreferences[categoryId] = checkbox.getValue();
});
// Fallback: use categories from state if no checkboxes
if (Object.keys(selectedPreferences).length === 0) {
Object.keys(categories).forEach((key) => {
selectedPreferences[key] = categories[key].enabled;
});
}
handleAccept(selectedPreferences);
break;
}
case 'accept-all': {
const allPreferences = {};
Object.keys(categories).forEach((key) => {
allPreferences[key] = true;
});
handleAccept(allPreferences);
break;
}
}
};
const handleAccept = (preferences) => {
CookieManager.saveConsent(preferences);
cookieConsent.hide();
dispatchConsentEvent(preferences);
props.onAccept?.(preferences);
};
const dispatchConsentEvent = (preferences) => {
if (typeof window !== 'undefined') {
window.dispatchEvent(
new CustomEvent('cookieConsentAccepted', {
detail: { preferences },
})
);
}
};
const recreateModal = () => {
if (!currentModal) {
return;
}
const wasOpen = currentModal.isOpen();
// Clean up existing checkboxes
categoryCheckboxes.forEach((checkbox) => {
checkbox.destroy();
});
categoryCheckboxes.clear();
currentModal.destroy();
currentModal = createModal();
if (wasOpen) {
currentModal.open();
}
};
const createModal = () => {
const categories = getCategories();
const modalConfig = {
title: state.title,
content: createContentHTML(), // Use HTML string instead of DOM element
variant: 'minimal',
size: state.mode === 'detailed' ? 'large' : 'medium',
showCloseButton: state.modal || state.showCloseButton,
closeOnBackdrop: state.closeOnBackdrop || false,
closeOnEscape: state.closeOnEscape !== false,
showBackdrop: state.modal,
lockBodyScroll: state.modal,
className: state.className,
actions: getActions(),
onAction: handleAction,
onClose: () => {
props.onDismiss?.();
},
onOpen: () => {
// Setup checkbox components after modal opens and content is rendered
if (state.mode === 'detailed') {
setTimeout(() => {
setupCategoryCheckboxes(categories, (categoryId, enabled) => {
props.onCategoryChange?.(categoryId, enabled);
});
}, 0);
}
},
};
// Position handling for banner mode
if (!state.modal) {
modalConfig.showBackdrop = false;
modalConfig.lockBodyScroll = false;
modalConfig.closeOnBackdrop = false;
modalConfig.className += ` cookie-consent--banner cookie-consent--${state.position}`;
} else {
modalConfig.className += ' cookie-consent--modal';
}
return Modal(modalConfig);
};
const cookieConsent = {
show() {
if (!currentModal) {
currentModal = createModal();
}
currentModal.open();
return this;
},
hide() {
if (currentModal) {
currentModal.close();
}
return this;
},
showDetails() {
state.mode = 'detailed';
recreateModal();
return this;
},
showSimple() {
state.mode = 'simple';
recreateModal();
return this;
},
update(newProps) {
Object.assign(state, newProps);
recreateModal();
return this;
},
destroy() {
// Clean up checkbox components
categoryCheckboxes.forEach((checkbox) => {
checkbox.destroy();
});
categoryCheckboxes.clear();
if (currentModal) {
currentModal.destroy();
currentModal = null;
}
return this;
},
getElement() {
return currentModal?.getElement() || null;
},
getPreferences() {
return CookieManager.getConsent()?.preferences || null;
},
hasConsent(category = null) {
const consent = CookieManager.getConsent();
if (!consent) {
return false;
}
return category ? consent.preferences[category] === true : true;
},
revokeConsent() {
CookieManager.clearConsent();
if (state.autoShow) {
this.show();
}
return this;
},
};
// Auto-show check
if (typeof document !== 'undefined' && state.autoShow) {
setTimeout(() => {
if (!CookieManager.hasValidConsent()) {
cookieConsent.show();
}
}, 100);
}
return cookieConsent;
};
// Static methods
createCookieConsent.getConsent = () => CookieManager.getConsent();
createCookieConsent.hasConsent = (category) => {
const consent = CookieManager.getConsent();
if (!consent) {
return false;
}
return category ? consent.preferences[category] === true : true;
};
createCookieConsent.revokeConsent = () => CookieManager.clearConsent();
export default createCookieConsent;
export { createCookieConsent, CookieManager };