levineuwirth.org/static/js/settings.js

166 lines
6.7 KiB
JavaScript

/* settings.js — Settings panel: theme, text size, print.
Must stay in sync with TEXT_SIZES in theme.js. */
(function () {
'use strict';
var TEXT_SIZES = [20, 23, 26];
var TEXT_SIZE_DEFAULT = 1; /* index of 23px */
var TEXT_SIZE_KEY = 'text-size';
/* ------------------------------------------------------------------
Init
------------------------------------------------------------------ */
function init() {
var toggle = document.querySelector('.settings-toggle');
var panel = document.querySelector('.settings-panel');
if (!toggle || !panel) return;
syncThemeButtons();
syncTextSizeButtons();
syncToggleButton('focus-mode', 'data-focus-mode');
syncToggleButton('reduce-motion', 'data-reduce-motion');
toggle.addEventListener('click', function (e) {
e.stopPropagation();
setOpen(toggle.getAttribute('aria-expanded') !== 'true');
});
document.addEventListener('click', function (e) {
if (!panel.contains(e.target) && e.target !== toggle) setOpen(false);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { setOpen(false); return; }
if (e.key !== 'Tab' || !panel.classList.contains('is-open')) return;
var focusable = Array.from(panel.querySelectorAll(
'button:not([disabled]), [tabindex]:not([tabindex="-1"])'
));
if (focusable.length === 0) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
panel.querySelectorAll('[data-action]').forEach(function (btn) {
btn.addEventListener('click', function () {
handle(btn.getAttribute('data-action'));
});
});
}
/* ------------------------------------------------------------------
Panel open / close
------------------------------------------------------------------ */
function setOpen(open) {
var toggle = document.querySelector('.settings-toggle');
var panel = document.querySelector('.settings-panel');
var wasOpen = panel.classList.contains('is-open');
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
panel.setAttribute('aria-hidden', open ? 'false' : 'true');
panel.classList.toggle('is-open', open);
if (open) {
var first = panel.querySelector('button:not([disabled]), [tabindex]:not([tabindex="-1"])');
if (first) first.focus();
} else if (wasOpen && (panel.contains(document.activeElement) || document.activeElement === toggle)) {
/* Only return focus to toggle when the panel was open and focus
was inside the settings area. Clicking outside the panel to
dismiss it should not steal focus from wherever the user clicked. */
toggle.focus();
}
}
/* ------------------------------------------------------------------
Actions
------------------------------------------------------------------ */
function handle(action) {
if (action === 'theme-light') setTheme('light');
else if (action === 'theme-dark') setTheme('dark');
else if (action === 'text-smaller') shiftSize(-1);
else if (action === 'text-larger') shiftSize(+1);
else if (action === 'focus-mode') toggleDataAttr('focus-mode', 'data-focus-mode');
else if (action === 'reduce-motion') toggleDataAttr('reduce-motion', 'data-reduce-motion');
else if (action === 'print') { setOpen(false); window.print(); }
else if (action === 'clear-annotations') { clearAnnotations(); }
}
function clearAnnotations() {
if (!confirm('Remove all highlights and annotations across every page?')) return;
if (window.Annotations) window.Annotations.clearAll();
setOpen(false);
}
/* Theme ----------------------------------------------------------- */
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
syncThemeButtons();
}
function currentTheme() {
var attr = document.documentElement.getAttribute('data-theme');
if (attr) return attr;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function syncThemeButtons() {
var active = currentTheme();
document.querySelectorAll('[data-action^="theme-"]').forEach(function (btn) {
btn.classList.toggle('is-active', btn.getAttribute('data-action') === 'theme-' + active);
});
}
/* Text size ------------------------------------------------------- */
function getSizeIndex() {
var n = parseInt(localStorage.getItem(TEXT_SIZE_KEY), 10);
return (isNaN(n) || n < 0 || n >= TEXT_SIZES.length) ? TEXT_SIZE_DEFAULT : n;
}
function shiftSize(delta) {
var idx = Math.max(0, Math.min(TEXT_SIZES.length - 1, getSizeIndex() + delta));
localStorage.setItem(TEXT_SIZE_KEY, idx);
document.documentElement.style.setProperty('--text-size', TEXT_SIZES[idx] + 'px');
syncTextSizeButtons();
}
/* Boolean toggles (focus-mode, reduce-motion) -------------------- */
function toggleDataAttr(storageKey, attrName) {
var html = document.documentElement;
var on = html.hasAttribute(attrName);
if (on) {
html.removeAttribute(attrName);
localStorage.removeItem(storageKey);
} else {
html.setAttribute(attrName, '');
localStorage.setItem(storageKey, '1');
}
syncToggleButton(storageKey, attrName);
}
function syncToggleButton(storageKey, attrName) {
var btn = document.querySelector('[data-action="' + storageKey + '"]');
if (btn) btn.classList.toggle('is-active', document.documentElement.hasAttribute(attrName));
}
function syncTextSizeButtons() {
var idx = getSizeIndex();
var smaller = document.querySelector('[data-action="text-smaller"]');
var larger = document.querySelector('[data-action="text-larger"]');
if (smaller) smaller.disabled = (idx === 0);
if (larger) larger.disabled = (idx === TEXT_SIZES.length - 1);
}
document.addEventListener('DOMContentLoaded', init);
}());