0 directories, 9 files

util

Home / tinai / util
/**
 * Theme loading, dynamic stylesheet injection, theme indexing, and page loader transitions.
 */

import { queryEl, queryAll } from './dom-utils.js';

export const DEFAULT_THEME = 'theme_dark';
export const DEFAULT_THEME_FAMILY = 'DARK';

let cachedThemeIndex = null;

/**
 * Fetches and caches the themes index from themes/theme_index.json.
 * @returns {Promise<object>} The theme index mapping families to lists of themes.
 */
export async function fetchThemeIndex() {
	if (cachedThemeIndex) {
		return cachedThemeIndex;
	}
	try {
		const response = await fetch('themes/theme_index.json');
		if (response.ok) {
			cachedThemeIndex = await response.json();
			return cachedThemeIndex;
		}
	} catch (e) {
		console.warn('Failed to fetch theme_index.json, using fallback', e);
	}
	// Fallback structure
	cachedThemeIndex = {
		DARK: {
			name: 'Dark',
			themes: [{ key: 'theme_dark', name: 'Dark' }]
		},
		LIGHT: {
			name: 'Light',
			themes: [{ key: 'theme_light', name: 'Light' }]
		}
	};
	return cachedThemeIndex;
}

/**
 * Returns a flat array of all valid theme keys across all families.
 * @param {object} [themeIndex=null] - Optional theme index object.
 * @returns {string[]} Array of theme keys.
 */
export function getAllThemeKeys(themeIndex = null) {
	const index = themeIndex || cachedThemeIndex;
	if (!index || typeof index !== 'object') return [DEFAULT_THEME];
	const keys = [];
	for (const familyKey in index) {
		if (Object.prototype.hasOwnProperty.call(index, familyKey)) {
			const family = index[familyKey];
			if (Array.isArray(family.themes)) {
				family.themes.forEach(t => {
					if (t && t.key) keys.push(t.key);
				});
			}
		}
	}
	return keys;
}

/**
 * Finds the family key (e.g. 'DARK', 'GRAY', 'LIGHT') that contains the given theme key.
 * @param {string} themeKey - The theme key to search for.
 * @param {object} [themeIndex=null] - Optional theme index.
 * @returns {string} The family key, or DEFAULT_THEME_FAMILY if not found.
 */
export function getThemeFamilyForTheme(themeKey, themeIndex = null) {
	const index = themeIndex || cachedThemeIndex;
	if (!index || typeof index !== 'object') return DEFAULT_THEME_FAMILY;

	for (const familyKey in index) {
		if (Object.prototype.hasOwnProperty.call(index, familyKey)) {
			const family = index[familyKey];
			if (Array.isArray(family.themes)) {
				const match = family.themes.find(t => t.key === themeKey);
				if (match) return familyKey;
			}
		}
	}
	return DEFAULT_THEME_FAMILY;
}

/**
 * Populates a select dropdown with theme families.
 * @param {HTMLSelectElement|null} selectEl - Target select element for families.
 * @param {object} themeIndex - Theme index map.
 * @param {string} [selectedFamily='DARK'] - Selected family key.
 */
export function populateThemeFamilyDropdown(selectEl, themeIndex, selectedFamily = DEFAULT_THEME_FAMILY) {
	if (!selectEl) return;
	selectEl.innerHTML = '';
	if (!themeIndex || typeof themeIndex !== 'object') return;

	for (const key in themeIndex) {
		if (Object.prototype.hasOwnProperty.call(themeIndex, key)) {
			const family = themeIndex[key];
			const option = document.createElement('option');
			option.value = key;
			option.textContent = family.name || key;
			if (key === selectedFamily) {
				option.selected = true;
			}
			selectEl.appendChild(option);
		}
	}
}

/**
 * Populates a select dropdown with themes for a specific theme family.
 * @param {HTMLSelectElement|null} selectEl - Target select element for themes.
 * @param {object} themeIndex - Theme index map.
 * @param {string} familyKey - Family key ('DARK' | 'GRAY' | 'LIGHT').
 * @param {string} [selectedTheme=''] - Selected theme key.
 */
export function populateThemeDropdown(selectEl, themeIndex, familyKey, selectedTheme = '') {
	if (!selectEl) return;
	selectEl.innerHTML = '';
	if (!themeIndex || typeof themeIndex !== 'object') return;

	const family = themeIndex[familyKey];
	if (!family || !Array.isArray(family.themes)) return;

	family.themes.forEach(t => {
		const option = document.createElement('option');
		option.value = t.key;
		option.textContent = t.name || t.key;
		if (t.key === selectedTheme) {
			option.selected = true;
		}
		selectEl.appendChild(option);
	});
}

/**
 * Fades out and hides the page loader element.
 * @param {HTMLElement|null} [loaderEl=null] - Optional loader element reference.
 */
export function fadeOutLoader(loaderEl = null) {
	const loader = loaderEl || queryEl('.page-loader');
	if (loader) {
		loader.style.opacity = '0';
		loader.addEventListener('transitionend', () => {
			loader.style.display = 'none';
		}, { once: true });
	}
}

/**
 * Applies a theme by fetching root variable declarations from dynamic.php,
 * updating the #theme-variables style element, and transitioning smoothly via View Transitions API.
 * @param {string} theme - The theme identifier.
 * @param {boolean} [includeHljs=true] - Whether to also swap the Highlight.js theme stylesheet.
 * @param {Function} [onComplete] - Optional callback once theme stylesheet finishes applying.
 */
export async function applyTheme(theme, includeHljs = true, onComplete = null) {
	const activeTheme = theme || DEFAULT_THEME;
	const loader = queryEl('.page-loader');

	try {
		const response = await fetch(`dynamic.php?t=${encodeURIComponent(activeTheme)}`);
		if (!response.ok) {
			throw new Error(`Failed to fetch theme variables: ${response.status}`);
		}
		const cssText = await response.text();

		const updateDom = () => {
			let styleEl = document.getElementById('theme-variables');
			if (!styleEl) {
				styleEl = document.createElement('style');
				styleEl.id = 'theme-variables';
				document.head.appendChild(styleEl);
			}
			styleEl.textContent = cssText;
			void document.documentElement.offsetHeight;

			if (includeHljs) {
				const hljsTheme = activeTheme.includes('light')
					? 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css'
					: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/dark.min.css';

				const existingHljs = queryEl('link[href*="highlight.js"]');
				if (existingHljs && existingHljs.getAttribute('href') !== hljsTheme) {
					existingHljs.setAttribute('href', hljsTheme);
				}
			}
		};

		const isLoaderVisible = loader && getComputedStyle(loader).display !== 'none' && getComputedStyle(loader).opacity !== '0';

		if (document.startViewTransition && !isLoaderVisible) {
			document.documentElement.classList.add('theme-transitioning');
			const transition = document.startViewTransition(() => {
				updateDom();
			});
			transition.finished.finally(() => {
				document.documentElement.classList.remove('theme-transitioning');
			});
		} else {
			updateDom();
		}

		fadeOutLoader(loader);
		if (typeof onComplete === 'function') onComplete(activeTheme);
	} catch (e) {
		console.warn('Failed to apply theme dynamically', e);
		fadeOutLoader(loader);
		if (typeof onComplete === 'function') onComplete(activeTheme);
	}
}
🌐
theme-utils.js ×
Type: Web, text/x-java
6.78 Kilobytes
Last Modified 2026-09-23 02:24:05
⬇ Download File