// ============================================================================ // Stylescape | Cookie Consent Manager // ============================================================================ // GDPR-compliant cookie consent management with granular category control. // Supports data-ss-cookie-consent attributes for declarative configuration. // ============================================================================ /** * Cookie categories for granular consent */ export type CookieCategory = | "necessary" | "analytics" | "marketing" | "preferences"; /** * Consent state for all categories */ export interface ConsentState { necessary: boolean; analytics: boolean; marketing: boolean; preferences: boolean; timestamp?: number; } /** * Configuration options for CookieConsentManager */ export interface CookieConsentOptions { /** Main message text */ message?: string; /** Accept all button text */ acceptAllText?: string; /** Accept necessary only button text */ acceptNecessaryText?: string; /** Settings button text */ settingsText?: string; /** CSS class prefix */ cssClass?: string; /** Storage key */ storageKey?: string; /** Position on screen */ position?: "top" | "bottom" | "center"; /** Cookie categories to show */ categories?: CookieCategory[]; /** Link to privacy policy */ privacyPolicyUrl?: string; /** Days until consent expires */ expirationDays?: number; /** Callback when consent is given */ onAccept?: (consent: ConsentState) => void; /** Callback when consent changes */ onChange?: (consent: ConsentState) => void; /** Auto-show banner if no consent */ autoShow?: boolean; /** Show detailed settings panel */ showSettings?: boolean; } /** * GDPR-compliant cookie consent manager with category support. * * @example JavaScript * ```typescript * const consent = new CookieConsentManager({ * categories: ["necessary", "analytics", "marketing"], * onAccept: (state) => { * if (state.analytics) initAnalytics() * if (state.marketing) initMarketing() * } * }) * ``` * * @example HTML with data-ss * ```html *
*
* * * * ``` */ export class CookieConsentManager { private options: Required< Omit > & Pick; private bannerElement: HTMLElement | null = null; private settingsPanel: HTMLElement | null = null; private consentState: ConsentState; constructor(options: CookieConsentOptions = {}) { this.options = { message: options.message ?? "We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.", acceptAllText: options.acceptAllText ?? "Accept All", acceptNecessaryText: options.acceptNecessaryText ?? "Necessary Only", settingsText: options.settingsText ?? "Cookie Settings", cssClass: options.cssClass ?? "ss-cookie-consent", storageKey: options.storageKey ?? "ss-cookie-consent", position: options.position ?? "bottom", categories: options.categories ?? [ "necessary", "analytics", "marketing", "preferences", ], privacyPolicyUrl: options.privacyPolicyUrl ?? "", expirationDays: options.expirationDays ?? 365, onAccept: options.onAccept, onChange: options.onChange, autoShow: options.autoShow ?? true, showSettings: options.showSettings ?? true, }; // Load existing consent or set defaults this.consentState = this.loadConsent() ?? { necessary: true, // Always required analytics: false, marketing: false, preferences: false, }; if (this.options.autoShow && !this.hasConsent()) { this.show(); } } // ======================================================================== // Public Methods // ======================================================================== /** * Check if user has given consent */ public hasConsent(): boolean { return this.loadConsent() !== null; } /** * Get current consent state */ public getConsent(): ConsentState { return { ...this.consentState }; } /** * Check if a specific category is allowed */ public isAllowed(category: CookieCategory): boolean { return this.consentState[category] ?? false; } /** * Accept all cookies */ public acceptAll(): void { this.consentState = { necessary: true, analytics: true, marketing: true, preferences: true, timestamp: Date.now(), }; this.saveConsent(); this.hide(); this.options.onAccept?.(this.consentState); this.activateCategoryScripts(); } /** * Accept only necessary cookies */ public acceptNecessary(): void { this.consentState = { necessary: true, analytics: false, marketing: false, preferences: false, timestamp: Date.now(), }; this.saveConsent(); this.hide(); this.options.onAccept?.(this.consentState); } /** * Save custom consent selection */ public saveCustomConsent(consent: Partial): void { this.consentState = { necessary: true, // Always required analytics: consent.analytics ?? false, marketing: consent.marketing ?? false, preferences: consent.preferences ?? false, timestamp: Date.now(), }; this.saveConsent(); this.hide(); this.options.onAccept?.(this.consentState); this.options.onChange?.(this.consentState); this.activateCategoryScripts(); } /** * Revoke consent and show banner again */ public revokeConsent(): void { localStorage.removeItem(this.options.storageKey); this.consentState = { necessary: true, analytics: false, marketing: false, preferences: false, }; this.show(); } /** * Show the consent banner */ public show(): void { if (this.bannerElement) { this.bannerElement.style.display = "block"; return; } this.createBanner(); } /** * Hide the consent banner */ public hide(): void { if (this.bannerElement) { this.bannerElement.style.display = "none"; } this.hideSettings(); } /** * Show settings panel */ public showSettings(): void { if (!this.settingsPanel) { this.createSettingsPanel(); } if (this.settingsPanel) { this.settingsPanel.style.display = "block"; } } /** * Hide settings panel */ public hideSettings(): void { if (this.settingsPanel) { this.settingsPanel.style.display = "none"; } } /** * Destroy the manager and remove elements */ public destroy(): void { this.bannerElement?.remove(); this.settingsPanel?.remove(); this.bannerElement = null; this.settingsPanel = null; } // ======================================================================== // Static Factory // ======================================================================== /** * Initialize from data-ss="cookie-consent" element */ public static init(): CookieConsentManager | null { const element = document.querySelector( '[data-ss="cookie-consent"]', ); if (!element) { return new CookieConsentManager(); } return new CookieConsentManager({ message: element.dataset.ssCookieMessage, position: element.dataset.ssCookiePosition as | "top" | "bottom" | "center", privacyPolicyUrl: element.dataset.ssCookiePrivacyUrl, cssClass: element.dataset.ssCookieClass, showSettings: element.dataset.ssCookieShowSettings !== "false", }); } // ======================================================================== // Private Methods // ======================================================================== private loadConsent(): ConsentState | null { try { const stored = localStorage.getItem(this.options.storageKey); if (!stored) return null; const consent = JSON.parse(stored) as ConsentState; // Check expiration if (consent.timestamp) { const expirationMs = this.options.expirationDays * 24 * 60 * 60 * 1000; if (Date.now() - consent.timestamp > expirationMs) { localStorage.removeItem(this.options.storageKey); return null; } } return consent; } catch { return null; } } private saveConsent(): void { localStorage.setItem( this.options.storageKey, JSON.stringify(this.consentState), ); } private createBanner(): void { const banner = document.createElement("div"); banner.className = `${this.options.cssClass} ${this.options.cssClass}--${this.options.position}`; banner.setAttribute("role", "dialog"); banner.setAttribute("aria-label", "Cookie Consent"); banner.setAttribute( "aria-describedby", `${this.options.cssClass}-message`, ); const privacyLink = this.options.privacyPolicyUrl ? `Privacy Policy` : ""; banner.innerHTML = `

${this.options.message} ${privacyLink}

${ this.options.showSettings ? `` : "" }
`; document.body.appendChild(banner); this.bannerElement = banner; // Add event listeners banner .querySelector(`.${this.options.cssClass}__button--accept`) ?.addEventListener("click", () => this.acceptAll()); banner .querySelector(`.${this.options.cssClass}__button--necessary`) ?.addEventListener("click", () => this.acceptNecessary()); banner .querySelector(`.${this.options.cssClass}__button--settings`) ?.addEventListener("click", () => this.showSettings()); } private createSettingsPanel(): void { const panel = document.createElement("div"); panel.className = `${this.options.cssClass}-settings`; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-label", "Cookie Settings"); const categoryLabels: Record< CookieCategory, { title: string; description: string } > = { necessary: { title: "Necessary Cookies", description: "Required for the website to function properly. Cannot be disabled.", }, analytics: { title: "Analytics Cookies", description: "Help us understand how visitors interact with our website.", }, marketing: { title: "Marketing Cookies", description: "Used to track visitors across websites for advertising purposes.", }, preferences: { title: "Preference Cookies", description: "Allow the website to remember choices you make.", }, }; const categoriesHtml = this.options.categories .map((cat) => { const info = categoryLabels[cat]; const isNecessary = cat === "necessary"; const isChecked = this.consentState[cat]; return `

${info.description}

`; }) .join(""); panel.innerHTML = `

Cookie Settings

${categoriesHtml}
`; document.body.appendChild(panel); this.settingsPanel = panel; // Event listeners panel .querySelector( `.${this.options.cssClass}-settings__button--cancel`, ) ?.addEventListener("click", () => this.hideSettings()); panel .querySelector(`.${this.options.cssClass}-settings__overlay`) ?.addEventListener("click", () => this.hideSettings()); panel .querySelector(`.${this.options.cssClass}-settings__button--save`) ?.addEventListener("click", () => this.saveFromSettings()); } private saveFromSettings(): void { if (!this.settingsPanel) return; const checkboxes = this.settingsPanel.querySelectorAll( "input[type='checkbox']", ); const consent: Partial = { necessary: true }; checkboxes.forEach((checkbox) => { const category = checkbox.name as CookieCategory; consent[category] = checkbox.checked; }); this.saveCustomConsent(consent); } private activateCategoryScripts(): void { // Activate scripts based on consent document .querySelectorAll( "script[data-ss-cookie-category]", ) .forEach((script) => { const category = script.dataset .ssCookieCategory as CookieCategory; if ( this.isAllowed(category) && !script.dataset.ssCookieActivated ) { const newScript = document.createElement("script"); newScript.src = script.src; newScript.dataset.ssCookieActivated = "true"; document.head.appendChild(newScript); } }); } } export default CookieConsentManager;