/**
* @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.#_);
}
};