Files
js_theme_manager/js/felexdev-theme-manager.js
T
2026-06-23 03:25:47 +05:00

200 lines
7.7 KiB
JavaScript

/**
* @file felexdev-theme-manager.js
* @description Минимальная версия
* @version 0.0.1
* @author felex67 (admin@felexdev.ru)
* @license Apache2.0
* @warning Системная версия('auto') должна регистрироваться первой!!!
*/
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: '<svg width="100%" height="100%" viewBox="0 0 135.46666 135.46667"version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><path id="path1" style="opacity:100%;fill:currentColor;fill-opacity:1;stroke:#171717;stroke-width:0" d="M 67.777258,12.777515 A 55,55 0 0 0 12.777515,67.777258 55,55 0 0 0 67.777258,122.77752 55,55 0 0 0 122.77752,67.777258 55,55 0 0 0 67.777258,12.777515 Z m -0.128157,4.95577 V 117.73338 A 50,50 0 0 1 17.733285,67.733333 50,50 0 0 1 67.649101,17.733285 Z"/></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.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.#_);
}
};