UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

279 lines (249 loc) 9.19 kB
'use strict'; /** * @typedef {Object} Category * @property {string} label - Category label displayed to the user * @property {boolean} required - If true, cannot be disabled * @property {boolean} default - Default state (true = enabled, false = disabled) */ /** * @typedef {Object} Config * @property {string} message - Consent message * @property {string} acceptText - Text for "Accept All" button * @property {string} rejectText - Text for "Reject All" button * @property {string} settingsText - Text for "Manage Settings" button * @property {Category[]} categories - List of cookie categories * @property {string} storageKey - Key used to store preferences * @property {(preferences: Object) => void} onSave - Callback when preferences are saved * @property {number} [animationDuration=400] - Animation duration in ms * @property {((config: Config) => string)|null} [renderBar] - Optional custom HTML renderer for consent bar * @property {((config: Config) => string)|null} [renderModal] - Optional custom HTML renderer for modal */ /** * CookieConsent * * A flexible and customizable cookie consent manager. * Features: * - Displays a consent bar with customizable text and buttons. * - Allows defining multiple cookie categories (required, analytics, ads, etc.). * - Stores user preferences in localStorage. * - Provides callbacks for when user accepts or denies categories. * - Fully customizable via configuration. */ class TinyCookieConsent { /** @type {Record<string, boolean>} */ #preferences; /** @type {Config} */ #config = { message: 'We use cookies to improve your experience.', acceptText: 'Accept All', rejectText: 'Reject All', settingsText: 'Manage Settings', categories: [], storageKey: 'cookie-consent-preferences', onSave: (prefs) => console.log('Preferences saved:', prefs), animationDuration: 400, renderBar: null, renderModal: null, }; /** @type {Record<string, boolean>} */ get preferences() { const saved = localStorage.getItem(this.#config.storageKey); return saved ? JSON.parse(saved) : {}; } /** @type {Config} */ get config() { return { ...this.#config, categories: [...this.#config.categories] }; } /** @param {Config} value */ set config(value) { this.validateConfig(value); this.#config = Object.assign(this.#config, value); } /** * @param {Config} config */ constructor(config) { if (!config || typeof config !== 'object') throw new TypeError('Config must be an object.'); this.config = config; const prefs = this.loadPreferences(); this.#preferences = prefs ?? {}; if (!prefs) this.showConsentBar(); } /** * Validates config object against jsDoc typedefs * @param {Partial<Config>} config */ validateConfig(config) { if (typeof config.message !== 'undefined' && typeof config.message !== 'string') throw new TypeError('Config.message must be a string.'); ['acceptText', 'rejectText', 'settingsText', 'storageKey'].forEach((key) => { // @ts-ignore if (typeof config[key] !== 'undefined' && typeof config[key] !== 'string') { throw new TypeError(`Config.${key} must be a string.`); } }); if (typeof config.categories !== 'undefined') { if (!Array.isArray(config.categories)) { throw new TypeError('Config.categories must be an array of Category objects.'); } config.categories.forEach((cat, i) => this.validateCategory(cat, i)); } if (typeof config.onSave !== 'undefined' && typeof config.onSave !== 'function') throw new TypeError('Config.onSave must be a function.'); if (typeof config.animationDuration !== 'undefined') { if (typeof config.animationDuration !== 'number' || config.animationDuration < 0) { throw new TypeError('Config.animationDuration must be a positive number.'); } } if ( typeof config.renderBar !== 'undefined' && config.renderBar !== null && typeof config.renderBar !== 'function' ) throw new TypeError('Config.renderBar must be a function or null.'); if ( typeof config.renderModal !== 'undefined' && config.renderModal !== null && typeof config.renderModal !== 'function' ) throw new TypeError('Config.renderModal must be a function or null.'); } /** * Validates a single category object * @param {Category} cat * @param {number} index */ validateCategory(cat, index) { if (!cat || typeof cat !== 'object') throw new TypeError(`Category at index ${index} must be an object.`); if (typeof cat.label !== 'string') throw new TypeError(`Category.label at index ${index} must be a string.`); if (typeof cat.required !== 'boolean') throw new TypeError(`Category.required at index ${index} must be a boolean.`); if (typeof cat.default !== 'boolean') throw new TypeError(`Category.default at index ${index} must be a boolean.`); } /** * Loads saved preferences from localStorage * @returns {Record<string, boolean>|null} */ loadPreferences() { const saved = localStorage.getItem(this.#config.storageKey); return saved ? JSON.parse(saved) : null; } /** * Smoothly removes an element with fade-out/slide-out animation * @param {HTMLElement} el */ removeWithAnimation(el) { if (!(el instanceof HTMLElement)) throw new TypeError('removeWithAnimation expects an HTMLElement.'); el.classList.add('closing'); setTimeout(() => el.remove(), this.#config.animationDuration); } /** * Saves preferences to localStorage * * @param {Record<string, boolean>} prefs */ savePreferences(prefs) { if (!prefs || typeof prefs !== 'object') throw new TypeError('Preferences must be an object.'); localStorage.setItem(this.#config.storageKey, JSON.stringify(prefs)); this.#config.onSave(prefs); } /** Shows the initial consent bar */ showConsentBar() { const bar = document.createElement('div'); bar.className = 'cookie-consent-bar'; bar.innerHTML = this.#config.renderBar ? this.#config.renderBar(this.#config) : ` <div><p>${this.#config.message}</p></div> <div><button class="accept">${this.#config.acceptText}</button></div> <div><button class="reject">${this.#config.rejectText}</button></div> <div><button class="settings">${this.#config.settingsText}</button></div> `; document.body.appendChild(bar); const accept = bar.querySelector('.accept'); if (accept instanceof HTMLElement) { accept.onclick = () => { /** @type {Record<string, boolean>} */ const prefs = {}; this.#config.categories.forEach((cat) => (prefs[cat.label] = true)); this.savePreferences(prefs); this.removeWithAnimation(bar); }; } const reject = bar.querySelector('.reject'); if (reject instanceof HTMLElement) { reject.onclick = () => { /** @type {Record<string, boolean>} */ const prefs = {}; this.#config.categories.forEach((cat) => (prefs[cat.label] = cat.required)); this.savePreferences(prefs); this.removeWithAnimation(bar); }; } const settings = bar.querySelector('.settings'); if (settings instanceof HTMLElement) { settings.onclick = () => this.showSettingsModal(bar); } } /** * Shows settings modal for fine-grained control * * @param {HTMLElement} bar */ showSettingsModal(bar) { const modal = document.createElement('div'); modal.className = 'cookie-consent-modal'; modal.innerHTML = this.#config.renderModal ? this.#config.renderModal(this.#config) : ` <div class="modal-content"> <h2>Cookie Settings</h2> <form class="settings-form"> ${this.#config.categories .map( (cat) => ` <label> <input type="checkbox" name="${cat.label}" ${cat.default ? 'checked' : ''} ${cat.required ? 'disabled' : ''}> ${cat.label} </label> `, ) .join('')} </form> <button class="save">Save Preferences</button> </div> `; document.body.appendChild(modal); const save = modal.querySelector('.save'); if (save instanceof HTMLElement) { save.onclick = () => { /** @type {Record<string, boolean>} */ const prefs = {}; modal.querySelectorAll('input[type=checkbox]').forEach((input) => { if (input instanceof HTMLInputElement) prefs[input.name] = input.checked; }); this.savePreferences(prefs); this.removeWithAnimation(modal); if (bar) this.removeWithAnimation(bar); }; } } /** * Checks if a category is allowed * @param {string} category * @returns {boolean} */ isAllowed(category) { if (typeof category !== 'string') throw new TypeError('isAllowed expects a string as category.'); if (!this.#preferences) return false; return !!this.#preferences[category]; } } module.exports = TinyCookieConsent;