/** * @file felexdev-theme-manager.js * @description Модуль переключения тем. Рекомендации по применению: статические/генерируемые сайты * @description Singleton theme switcher module * @version 0.0.1 * @author felex67 (admin@felexdev.ru) * @license Apache2.0 * @warning Системная версия('auto') должна регистрироваться первой!!! * @warning The system theme must be addet first!!! */ class ThemeManager { /** * @typedef {Object} Theme * * @property {string} name - Внутреннее название темы. * @property {string} icon - PlainSVG-иконка темы. * @property {string} selector - CSS-селектор темы. Без точки. */ /** * Конфигурация модулю * @typedef {Object} Config * * @property {string} btnId - CSS-id-селектор кнопки. * @property {number} btnTabIndex - Таб-индекс кнопки. * @property {string} globalPrefix - Глобальный префикс. * @property {string} themePrefix - Префикс CSS-class-селектора темы. * @property {string} svg - PlainSVG-иконка темы. * @type {{ btnId: string; btnTabIndex: number; globalPrefix: string; themePrefix: string; svg: string; }} */ static #config = { btnId: 'btn-toggle', // Идентификатор кнопки переключения: `gpfx + btnid == 'flxd-tm-btn-toggle'` btnTabIndex: 0, // Индекс табуляции кнопки переключения тем globalPrefix: 'flxd-tm', // Глобальный префикс: `gpfx + '-' == 'flxd-tm-'` themePrefix: 'color-scheme', // Селектор CSS: `gpfx + stdid == 'flxd-tm-color-scheme'` svg: '', }; /** * Список классов корневого элемента * * @static * @type {HTMLTokenList} */ static #classList = document.documentElement.classList; /** * Кнопка переключения тем * * @static * @type {HTMLDivElement} */ static #button = document.createElement('div'); /** * Идентификатор текущей темы * * @static * @type {number} */ static #index = 0; /** * Массив тем * * @static * @type {[Theme]} */ static #themes = []; static toggle = () => { this.#set((this.#index + 1) % this.#themes.length); }; static /* Инициализация конфигурации */ { const config = this.#config; config.globalPrefix = config.globalPrefix + '-'; config.themePrefix = `${config.globalPrefix}${config.themePrefix}-`; this.#button.id = config.btnId = config.globalPrefix + config.btnId; this.#button.tabIndex = config.btnTabIndex; this.#button.setAttribute('aria-label', 'toggle color scheme'); this.#button.addEventListener('click', this.toggle); this.#button.addEventListener('keydown', (e) => { if (e.key == ' ' || e.key == 'Enter') this.toggle();}); }; /** * Добавить новую тему * @param {string} themeName - Название темы используемое в селекторе CSS. 'auto' -> 'flxd-tm-color-scheme-auto' * @param {string?} themeIcon - Текстовое векторное изображение PlainSVG, если равно нулю используется иконка по умолчанию: 2-я фаза луны. */ static #addTheme = (name, icon = null) => { const theme = { name: name, icon: icon || this.#config.svg, selector: this.#config.themePrefix + name, }; this.#themes.push(theme); }; static #set(index) { if (index == this.#index) return; if (index && this.#index) this.#classList.replace(this.#themes[this.#index].selector, this.#themes[index].selector); else if (this.#index) this.#classList.remove(this.#themes[this.#index].selector); else this.#classList.add(this.#themes[index].selector); this.#save(this.#index = index); this.#button.innerHTML = this.#themes[index].icon; this.#button.setAttribute('aria-current', this.#themes[index].name); }; static #save = (() => { let save = (index) => { console.error(this.#config.globalPrefix + '#save: Невозможно сохранить тему, хранилища не доступны.'); }; try { this.#index = parseInt(localStorage.getItem(this.#config.themePrefix)) || 0; save = (index) => { try { localStorage.setItem(this.#config.themePrefix, index); } catch(e) { console.error(e); } }; } catch(e) { console.error(this.#config.globalPrefix + '#sv-IIFE: localStorage не доступен, пробую cookie.'); try { let cookie = document.cookie.match(`${this.#config.themePrefix}\\s*=\\s*(\\d+)`); this.#index = cookie ? parseInt(cookie[1]) || 0 : 0; save = (i) => { try { document.cookie = `${this.#config.themePrefix}=${i};max-age=${31536000};path=/`; } catch(e) { console.error(e); } }; } catch(e) { console.error(this.#config.globalPrefix + `#s-IIFE: Хранилища не доступны. Настройки темы не сохраняются!`); } } return save; })(); static { /* Инициализация тем */ let index = this.#index; let themes = this.#themes; let addTheme = this.#addTheme; /** @warning Системная версия('auto') должна регистрироваться первой!!! */ addTheme('auto',''); addTheme('light',''); addTheme('dark',''); /* Новые темы добавлять здесь */ addTheme('old-terminal',''); /* */ if (index >= themes.length || index < 0) { console.error(`${this.#config.globalPrefix}bottom-static: В хранилище сохранён не валидный индекс '${index}'. Сброс на системную тему`); this.#index = index = 0; } if(index) this.#classList.add(themes[index].selector); this.#button.innerHTML = themes[index].icon; this.#button.setAttribute('aria-current', themes[index].name); }; static set = (name) => { let i = -1; for(let j = 0; j < this.#themes.length; j++) { if(this.#themes[j].name === name) { i = j; break; } } if(i == -1) { throw new Error(`Темы '${name}' не существует!.`); } this.#set(i); }; static #_ = () => { window.removeEventListener('DOMContentLoaded',this.#_); document.body.appendChild(this.#button); }; static { let m = this; this.#set(this.#index); if (!document.body)window.addEventListener('DOMContentLoaded', this.#_); } };