178 lines
6.1 KiB
JavaScript
178 lines
6.1 KiB
JavaScript
import { syncAriaRadioGroup } from "../shared/dom.js";
|
|
import { clampNum } from "../shared/math.js";
|
|
import { subscribePrefersColorSchemeChange } from "../shared/media.js";
|
|
import {
|
|
PICK,
|
|
S_RANGE,
|
|
L_RANGE,
|
|
RADIUS_IDS,
|
|
THEME_MODE_IDS,
|
|
FONT_IDS,
|
|
MESH_BG_IDS,
|
|
applyThemeState,
|
|
drawSLPlane,
|
|
readThemeState,
|
|
updateKnob,
|
|
writeThemeState,
|
|
} from "./theme-core.js";
|
|
import { ensureThemeFontLoaded } from "./theme-fonts.js";
|
|
|
|
export function initThemeControls() {
|
|
const picker = document.querySelector("[data-theme-picker]");
|
|
const panel = document.querySelector("[data-theme-panel]");
|
|
const panelBtns = document.querySelectorAll("[data-theme-panel-btn]");
|
|
const themeModeBtns = document.querySelectorAll("[data-theme-mode]");
|
|
const canvas = document.querySelector("[data-sl-canvas]");
|
|
const knob = document.querySelector("[data-sl-knob]");
|
|
const hueSlider = document.querySelector("[data-hue-slider]");
|
|
const radiusBtns = document.querySelectorAll("[data-radius-option]");
|
|
const fontBtns = document.querySelectorAll("[data-font-option]");
|
|
const meshBgBtns = document.querySelectorAll("[data-mesh-bg-option]");
|
|
|
|
const themeOk =
|
|
picker && panel && panelBtns.length > 0 && themeModeBtns.length === 3 && canvas && hueSlider;
|
|
if (!themeOk) {
|
|
window.siteThemePanel = { open: () => {}, close: () => {}, toggle: () => {} };
|
|
return;
|
|
}
|
|
|
|
const state = readThemeState();
|
|
applyThemeState(state);
|
|
ensureThemeFontLoaded(state.font);
|
|
|
|
function syncHueSlider() {
|
|
hueSlider.value = String(Math.round(state.h));
|
|
hueSlider.setAttribute("aria-valuenow", String(Math.round(state.h)));
|
|
}
|
|
|
|
function persistAndApply() {
|
|
applyThemeState(state);
|
|
ensureThemeFontLoaded(state.font);
|
|
writeThemeState(state);
|
|
}
|
|
|
|
function bindOptionButtons(btns, attr, ids, stateKey) {
|
|
btns.forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
const id = btn.getAttribute(attr);
|
|
if (!ids.includes(id)) return;
|
|
state[stateKey] = id;
|
|
syncAriaRadioGroup(btns, attr, state[stateKey]);
|
|
persistAndApply();
|
|
});
|
|
});
|
|
}
|
|
|
|
syncHueSlider();
|
|
drawSLPlane(canvas, state.h);
|
|
updateKnob(knob, state.s, state.l);
|
|
syncAriaRadioGroup(radiusBtns, "data-radius-option", state.radius);
|
|
syncAriaRadioGroup(themeModeBtns, "data-theme-mode", state.mode);
|
|
syncAriaRadioGroup(fontBtns, "data-font-option", state.font);
|
|
if (meshBgBtns.length >= 2) syncAriaRadioGroup(meshBgBtns, "data-mesh-bg-option", state.meshBg);
|
|
|
|
bindOptionButtons(radiusBtns, "data-radius-option", RADIUS_IDS, "radius");
|
|
bindOptionButtons(fontBtns, "data-font-option", FONT_IDS, "font");
|
|
bindOptionButtons(themeModeBtns, "data-theme-mode", THEME_MODE_IDS, "mode");
|
|
if (meshBgBtns.length >= 2) bindOptionButtons(meshBgBtns, "data-mesh-bg-option", MESH_BG_IDS, "meshBg");
|
|
|
|
function setPanelOpen(open) {
|
|
panel.hidden = !open;
|
|
panelBtns.forEach((btn) => {
|
|
btn.setAttribute("aria-expanded", open ? "true" : "false");
|
|
btn.setAttribute("aria-label", open ? "Закрыть настройки стиля" : "Настройки стиля");
|
|
});
|
|
if (open) {
|
|
drawSLPlane(canvas, state.h);
|
|
updateKnob(knob, state.s, state.l);
|
|
syncAriaRadioGroup(themeModeBtns, "data-theme-mode", state.mode);
|
|
syncAriaRadioGroup(fontBtns, "data-font-option", state.font);
|
|
if (meshBgBtns.length >= 2) syncAriaRadioGroup(meshBgBtns, "data-mesh-bg-option", state.meshBg);
|
|
}
|
|
}
|
|
|
|
subscribePrefersColorSchemeChange(() => {
|
|
if (state.mode !== "system") return;
|
|
applyThemeState(state);
|
|
});
|
|
|
|
panelBtns.forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setPanelOpen(panel.hidden);
|
|
});
|
|
});
|
|
|
|
picker.addEventListener("click", (e) => e.stopPropagation());
|
|
|
|
document.addEventListener("click", (e) => {
|
|
const t = e.target;
|
|
if (!(t instanceof Node)) return;
|
|
const el = t instanceof Element ? t : t.parentElement;
|
|
if (el?.closest("[data-theme-panel-btn]")) return;
|
|
if (panel.contains(t) || picker.contains(t) || panel.hidden) return;
|
|
setPanelOpen(false);
|
|
});
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape" && !panel.hidden) {
|
|
setPanelOpen(false);
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
|
|
try {
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get("themePanel") === "open") requestAnimationFrame(() => setPanelOpen(true));
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
window.siteThemePanel = {
|
|
open: () => setPanelOpen(true),
|
|
close: () => setPanelOpen(false),
|
|
toggle: () => setPanelOpen(!!panel.hidden),
|
|
};
|
|
|
|
let canvasRect = null;
|
|
|
|
function pickFromClientXY(clientX, clientY) {
|
|
const r = canvasRect || canvas.getBoundingClientRect();
|
|
if (r.width <= 0 || r.height <= 0) return;
|
|
const x = clampNum(((clientX - r.left) / r.width) * 100, 0, 100, 0) / 100;
|
|
const y = clampNum(((clientY - r.top) / r.height) * 100, 0, 100, 0) / 100;
|
|
state.s = Math.round(PICK.sMin + x * S_RANGE);
|
|
state.l = Math.round(PICK.lMin + (1 - y) * L_RANGE);
|
|
updateKnob(knob, state.s, state.l);
|
|
persistAndApply();
|
|
}
|
|
|
|
canvas.addEventListener("pointerdown", (e) => {
|
|
e.preventDefault();
|
|
canvasRect = canvas.getBoundingClientRect();
|
|
canvas.setPointerCapture(e.pointerId);
|
|
pickFromClientXY(e.clientX, e.clientY);
|
|
});
|
|
|
|
canvas.addEventListener("pointermove", (e) => {
|
|
if (!canvas.hasPointerCapture(e.pointerId)) return;
|
|
e.preventDefault();
|
|
pickFromClientXY(e.clientX, e.clientY);
|
|
});
|
|
|
|
canvas.addEventListener("pointerup", (e) => {
|
|
if (canvas.hasPointerCapture(e.pointerId)) canvas.releasePointerCapture(e.pointerId);
|
|
canvasRect = null;
|
|
});
|
|
|
|
hueSlider.addEventListener("input", () => {
|
|
state.h = clampNum(hueSlider.value, 0, 360, state.h);
|
|
syncHueSlider();
|
|
drawSLPlane(canvas, state.h);
|
|
updateKnob(knob, state.s, state.l);
|
|
persistAndApply();
|
|
});
|
|
}
|