chrome.js
10.4 kB · javascript · 360 lines
1const DOCK = '(min-width: 74rem)';2const PREFIX = (typeof document !== 'undefined' && document.documentElement.dataset.prefix) || 'mrly-';3const KEY = { theme: `${PREFIX}theme`, font: `${PREFIX}font`, tint: `${PREFIX}tint`, saver: `${PREFIX}saver`, cart: `${PREFIX}cart` };4const WORDMARK = 'wordmark';5const SCREENS = ['matrix', 'sleep', 'mandelbrot', 'julia'];6const SIDES = ['left', 'right'];78const read = (key) => {9 try {10 return localStorage.getItem(key) ?? '';11 } catch {12 return '';13 }14};1516const write = (key, value) => {17 try {18 if (value) localStorage.setItem(key, value);19 else localStorage.removeItem(key);20 } catch {}21};2223/* SUB */2425const root = () => document.documentElement;26const docked = () => matchMedia(DOCK).matches;27const isOpen = (side) => root().dataset[side] === 'open';28const dock = () => document.querySelector('.dock');29const ready = (fn) => (document.readyState === 'complete' ? fn() : addEventListener('load', fn, { once: true }));3031const spot = () => `${PREFIX}spot:${location.href}`;3233function keep() {34 try {35 sessionStorage.setItem(spot(), String(scrollY));36 } catch {}37}3839function saved() {40 try {41 const at = Number(sessionStorage.getItem(spot())) || 0;42 sessionStorage.removeItem(spot());43 return at;44 } catch {45 return 0;46 }47}4849function resumed() {50 const kind = performance.getEntriesByType('navigation')[0]?.type;51 return kind === 'back_forward' || kind === 'reload';52}5354function pin() {55 const top = dock()?.getBoundingClientRect().top ?? 0;56 if (top) scrollTo({ top: top + scrollY, behavior: 'instant' });57}5859function land() {60 const at = saved();61 if (at > 0 && resumed() && !location.hash) scrollTo({ top: at, behavior: 'instant' });62}6364/* DRAWERS */6566function set(side, open) {67 root().dataset[side] = open ? 'open' : 'shut';68 sync();69}7071function stow() {72 for (const side of SIDES) set(side, false);73}7475function sync() {76 for (const button of document.querySelectorAll('[data-pane]')) button.setAttribute('aria-expanded', String(isOpen(button.dataset.pane)));77}7879function toggle(side) {80 const open = !isOpen(side);81 if (open) pin();82 set(side, open);83 if (!open) return;84 set(side === 'left' ? 'right' : 'left', false);85 document.getElementById(side)?.querySelector('a, button')?.focus();86}8788function shut() {89 if (docked()) return false;90 const open = SIDES.filter(isOpen);91 for (const side of open) set(side, false);92 if (open.length) document.querySelector(`[data-pane="${open[0]}"]`)?.focus();93 return open.length > 0;94}9596/* THEME */9798function theme(next) {99 if (next) root().dataset.theme = next;100 else delete root().dataset.theme;101 write(KEY.theme, next);102 for (const label of document.querySelectorAll('[data-theme-toggle] b')) label.textContent = next || 'auto';103 window.dispatchEvent(new Event('theme'));104}105106/* TINT */107108function tint(next) {109 if (next) root().dataset.tint = next;110 else delete root().dataset.tint;111 write(KEY.tint, next);112 for (const pick of document.querySelectorAll('[data-tint-pick]')) pick.value = next || '';113 window.dispatchEvent(new Event('theme'));114}115116function turn() {117 const now = root().dataset.theme ?? '';118 theme(now === '' ? 'light' : now === 'light' ? 'dark' : '');119}120121/* FONT */122123function face(next) {124 if (next) root().dataset.font = next;125 else delete root().dataset.font;126 write(KEY.font, next);127 for (const pick of document.querySelectorAll('[data-font-pick]')) pick.value = next || '';128}129130/* CART */131132function count() {133 try {134 const items = JSON.parse(read(KEY.cart) || '[]');135 return Array.isArray(items) ? items.reduce((sum, item) => sum + (Number(item.qty) || 1), 0) : 0;136 } catch {137 return 0;138 }139}140141function cart() {142 const n = count();143 for (const link of document.querySelectorAll('[data-cart]')) {144 link.setAttribute('aria-label', n ? `Cart, ${n} item${n === 1 ? '' : 's'}` : 'Cart');145 link.querySelectorAll('.dot').forEach((dot, i) => dot.classList.toggle('on', i < n));146 }147}148149/* CONTENTS */150151function contents(nav) {152 const links = [...nav.querySelectorAll('a[href^="#"]')];153 const targets = links.map((a) => document.getElementById(decodeURIComponent(a.hash.slice(1)))).filter(Boolean);154 if (!targets.length) return;155 const seen = new Map();156 const eye = new IntersectionObserver(157 (entries) => {158 for (const entry of entries) seen.set(entry.target, entry.isIntersecting);159 const hit = targets.find((t) => seen.get(t));160 if (!hit) return;161 for (const a of links) {162 if (a.hash.slice(1) === hit.id) a.setAttribute('aria-current', 'location');163 else a.removeAttribute('aria-current');164 }165 },166 { rootMargin: `-${Math.round(dock()?.getBoundingClientRect().height ?? 0)}px 0px -60% 0px` },167 );168 for (const t of targets) eye.observe(t);169}170171/* MARK */172173const marks = new Map();174175const wanted = () => root().dataset.saver || WORDMARK;176177function unmark() {178 for (const canvas of marks.keys()) {179 if (canvas.isConnected) continue;180 canvas.stop?.();181 marks.delete(canvas);182 }183}184185async function footer(canvas) {186 canvas.stop?.();187 marks.delete(canvas);188 const next = canvas.cloneNode(false);189 const label = next.dataset.label ?? next.getAttribute('aria-label') ?? '';190 if (label) next.dataset.label = label;191 canvas.replaceWith(next);192 const name = wanted();193 marks.set(next, name);194 if (name === WORDMARK) {195 next.removeAttribute('aria-hidden');196 next.setAttribute('role', 'img');197 if (label) next.setAttribute('aria-label', label);198 const { cycle, mark } = await import('./font.js');199 next.stop = mark(next, cycle(next.dataset.text || 'MRLYPROD', 1));200 } else {201 next.setAttribute('aria-hidden', 'true');202 next.removeAttribute('role');203 next.removeAttribute('aria-label');204 const { saver } = await import('./savers/index.js');205 next.stop = saver(next, name);206 }207 if (!next.isConnected) next.stop();208}209210function marked() {211 const name = wanted();212 for (const canvas of document.querySelectorAll('canvas.mark')) if (marks.get(canvas) !== name) footer(canvas);213}214215function screen(next) {216 if (SCREENS.includes(next)) root().dataset.saver = next;217 else delete root().dataset.saver;218 const now = root().dataset.saver ?? '';219 write(KEY.saver, now);220 for (const pick of document.querySelectorAll('[data-saver-pick]')) pick.value = now;221}222223function replay(name) {224 screen(name);225 for (const canvas of [...marks.keys()]) footer(canvas);226}227228/* TREE */229230function reveal() {231 const here = document.querySelector('.tree a[aria-current="page"]');232 const pane = here?.closest('.pane');233 if (!here || !pane) return;234 const top = here.getBoundingClientRect().top - pane.getBoundingClientRect().top + pane.scrollTop;235 pane.scrollTop = Math.max(0, top - pane.clientHeight / 2);236}237238/* EXPLORER */239240let forest = null;241242const fetchTree = (url) => (forest ??= fetch(url).then((reply) => (reply.ok ? reply.json() : null)).catch(() => null));243244const named = (path) => (path.slice(path.lastIndexOf('/') + 1).includes('.') ? path : `${path}.txt`);245246function find(node, path) {247 let at = node;248 for (const part of path ? path.split('/') : []) {249 at = (at.c ?? []).find((kid) => kid.n === part);250 if (!at) return null;251 }252 return at;253}254255function branch(base, kid, path) {256 const li = document.createElement('li');257 const a = document.createElement('a');258 if (kid.k !== 'd') {259 const icon = document.createElement('span');260 icon.className = kid.i ? `si si-${kid.i}` : 'si';261 icon.setAttribute('aria-hidden', 'true');262 const name = document.createElement('span');263 name.className = 'name';264 name.textContent = kid.n;265 a.append(icon, name);266 a.href = `${base}${named(path)}`;267 li.append(a);268 return li;269 }270 a.textContent = kid.n;271 a.href = `${base}${path}/`;272 const details = document.createElement('details');273 details.dataset.lazy = path;274 const summary = document.createElement('summary');275 summary.append(a);276 details.append(summary, document.createElement('ul'));277 li.append(details);278 return li;279}280281function fill(details, data) {282 const list = details.querySelector(':scope > ul');283 if (!list || list.children.length) return;284 const path = details.dataset.lazy;285 const node = find(data, path);286 if (!node) return;287 for (const kid of node.c ?? []) list.append(branch(data.base ?? '/git/', kid, path ? `${path}/${kid.n}` : kid.n));288}289290function expand(event) {291 const details = event.target;292 if (!(details instanceof HTMLDetailsElement) || !details.open || details.dataset.lazy === undefined) return;293 const url = details.closest('.tree')?.dataset.source;294 if (!url) return;295 fetchTree(url).then((data) => data && fill(details, data));296}297298/* WIRE */299300const wired = new WeakSet();301302const once = (selector, fn) => {303 for (const el of document.querySelectorAll(selector)) {304 if (wired.has(el)) continue;305 wired.add(el);306 fn(el);307 }308};309310export function wire() {311 sync();312 unmark();313 theme(root().dataset.theme ?? '');314 face(root().dataset.font ?? '');315 tint(root().dataset.tint ?? '');316 screen(root().dataset.saver ?? '');317 cart();318 once('.contents', contents);319 once('.tree', reveal);320 marked();321}322323function boot() {324 root().classList.add('js');325 if ('scrollRestoration' in history) history.scrollRestoration = 'manual';326 ready(land);327 theme(read(KEY.theme));328 face(read(KEY.font));329 tint(read(KEY.tint));330 screen(read(KEY.saver));331 stow();332 matchMedia(DOCK).addEventListener('change', stow);333 document.addEventListener('click', (e) => {334 const target = e.target instanceof Element ? e.target : null;335 if (!target) return;336 const button = target.closest('[data-pane]');337 if (button) return toggle(button.dataset.pane);338 if (target.closest('[data-theme-toggle]')) return turn();339 if (target.closest('.scrim') || target.closest('.pane a[href]')) shut();340 });341 document.addEventListener('change', (e) => {342 const pick = e.target instanceof Element ? e.target : null;343 if (!pick) return;344 if (pick.matches('[data-font-pick]')) return face(pick.value);345 if (pick.matches('[data-tint-pick]')) return tint(pick.value);346 if (pick.matches('[data-saver-pick]')) return replay(pick.value);347 });348 document.addEventListener('toggle', expand, true);349 document.addEventListener('keydown', (e) => {350 if (e.key === 'Escape' && shut()) e.preventDefault();351 });352 window.addEventListener('cart', cart);353 window.addEventListener('storage', cart);354 window.addEventListener('pageshow', cart);355 window.addEventListener('pagehide', keep);356 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire);357 else wire();358}359360if (typeof document !== 'undefined') boot();