/** * @file felexdev-theme-manager.js * @description Минимальная версия * @version 0.0.1 * @author felex67 (admin@felexdev.ru) * @license Apache2.0 */ class ThemeManager { /** * @typedef {Object} Theme * * @property {string} name - Внутреннее название темы. * @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-иконка темы. */ 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: '', mediaRequestDark: window.matchMedia('(prefers-color-scheme: dark)'), requestDarkHandle: null, }; /** * Список классов корневого элемента * * @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 #btnClick = (e) => { if (e.type === 'click' || e.key === 'Enter' || e.key === ' ') this.#set((this.#index + 1) % 2) }; /** * Обработчик события изменения системной темы * * @param {Event} e */ static #sysThemeChange = (e) => { if (e.matches != this.#index) this.#set(e.matches + 0); }; static { /* Инициализация конфигурации */ const config = this.#config; const button = this.#button; /* Настраиваем префиксы */ config.globalPrefix = config.globalPrefix + '-'; config.themePrefix = `${config.globalPrefix}${config.themePrefix}-`; /* Настраиваем кнопку */ button.id = config.btnId = config.globalPrefix + config.btnId; button.tabIndex = config.btnTabIndex; button.innerHTML = config.svg; /* Настраиваем обработчики */ button.setAttribute('aria-label', 'toggle color scheme'); button.addEventListener('click', this.#btnClick); button.addEventListener('keydown', this.#btnClick); /* Устанавливаем обработчик смены системной темы */ config.requestDarkHandle = config.mediaRequestDark.addEventListener('change', this.#sysThemeChange); }; /** * Добавить новую тему * @param {string} name - Название темы используемое в селекторе CSS. 'light' -> 'flxd-tm-color-scheme-light' */ static #addTheme = (name) => { const theme = { name: name, selector: this.#config.themePrefix + name, }; this.#themes.push(theme); }; /** * Устанавливаем тему по индексу * @param {number} index */ static #set(index) { if (index == this.#index) return; this.#classList.replace(this.#themes[this.#index].selector, this.#themes[index].selector); this.#save(this.#index = index); this.#button.setAttribute('aria-current', this.#themes[index].name); }; /** * Сохраняет индекс текущей темы * @type {function() => void} * @returns void */ static #save = (() => { let save = () => { console.error(this.#config.globalPrefix + '#save: Невозможно сохранить тему, хранилище недоступно.'); }; try { this.#index = parseInt(localStorage.getItem(this.#config.themePrefix)) || 0; save = () => { try { localStorage.setItem(this.#config.themePrefix, this.#index); } catch(e) { console.error(e); } }; } catch(e) { console.error(this.#config.globalPrefix + '#sv-IIFE: localStorage не доступен.'); } return save; })(); static { /* Инициализация тем */ let index = this.#index; let themes = this.#themes; /* Добавляем темы(светлая/темная) */ this.#addTheme('light'); this.#addTheme('dark'); /* Проверяем индекс */ if (index >= themes.length || index < 0) { console.error(`${this.#config.globalPrefix}bottom-static: В хранилище сохранён не валидный индекс '${index}'. Сброс на системную тему`); this.#index = this.#config.mediaRequestDark.matches + 0; } /* Применяем тему */ this.#classList.add(themes[index].selector); this.#button.setAttribute('aria-current', themes[index].name); }; /** * * @param {string} name Название темы * @throws Error если `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); }; /** Назначаем обработчик события создания DOM */ static { this.#set(this.#index); if (!document.body)window.addEventListener('DOMContentLoaded', this.#_); } };