200 lines
7.7 KiB
JavaScript
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.#_);
|
|
}
|
|
}; |