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();