stylescape
Version:
Stylescape is a visual identity framework developed by Scape Agency.
516 lines (466 loc) • 17.2 kB
text/typescript
// ============================================================================
// 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
* <div data-ss="cookie-consent"
* data-ss-cookie-position="bottom"
* data-ss-cookie-privacy-url="/privacy">
* </div>
*
* <!-- Or use in scripts -->
* <script data-ss-cookie-category="analytics" src="analytics.js"></script>
* ```
*/
export class CookieConsentManager {
private options: Required<
Omit<CookieConsentOptions, "onAccept" | "onChange">
> &
Pick<CookieConsentOptions, "onAccept" | "onChange">;
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<ConsentState>): 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<HTMLElement>(
'[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
? `<a href="${this.options.privacyPolicyUrl}" class="${this.options.cssClass}__link">Privacy Policy</a>`
: "";
banner.innerHTML = `
<div class="${this.options.cssClass}__content">
<p id="${this.options.cssClass}-message" class="${this.options.cssClass}__message">
${this.options.message}
${privacyLink}
</p>
<div class="${this.options.cssClass}__actions">
${
this.options.showSettings
? `<button type="button" class="${this.options.cssClass}__button ${this.options.cssClass}__button--settings">
${this.options.settingsText}
</button>`
: ""
}
<button type="button" class="${this.options.cssClass}__button ${this.options.cssClass}__button--necessary">
${this.options.acceptNecessaryText}
</button>
<button type="button" class="${this.options.cssClass}__button ${this.options.cssClass}__button--accept">
${this.options.acceptAllText}
</button>
</div>
</div>
`;
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 `
<div class="${this.options.cssClass}-settings__category">
<label class="${this.options.cssClass}-settings__label">
<input type="checkbox"
name="${cat}"
${isChecked ? "checked" : ""}
${isNecessary ? "disabled" : ""}>
<span class="${this.options.cssClass}-settings__title">${info.title}</span>
</label>
<p class="${this.options.cssClass}-settings__description">${info.description}</p>
</div>
`;
})
.join("");
panel.innerHTML = `
<div class="${this.options.cssClass}-settings__overlay"></div>
<div class="${this.options.cssClass}-settings__panel">
<h2 class="${this.options.cssClass}-settings__heading">Cookie Settings</h2>
<div class="${this.options.cssClass}-settings__categories">
${categoriesHtml}
</div>
<div class="${this.options.cssClass}-settings__actions">
<button type="button" class="${this.options.cssClass}-settings__button--cancel">Cancel</button>
<button type="button" class="${this.options.cssClass}-settings__button--save">Save Preferences</button>
</div>
</div>
`;
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<HTMLInputElement>(
"input[type='checkbox']",
);
const consent: Partial<ConsentState> = { 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<HTMLScriptElement>(
"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;