Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development. cameron.tngl.io/misaligned
Something went wrong. Try again.
3.0 kB · 56 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657// One tooltip for the whole screen. Hover anything with a data-tip (or a title, which is// taken over so the browser's slow native tip never shows) and it says what that thing is.// The first line of a tip is its name; the rest explains it.type Spot = { left: number; top: number; right: number; bottom: number; width: number };let place: ((text: string, r: Spot) => void) | null = null, drop: (() => void) | null = null;let spotText: string | null = null, spotTimer = 0;/** A tip for something drawn on the canvas, which has no element to hover: show `text` by the * screen box `r` after the usual pause, or hide it with null. Calling again with the same text * keeps the tip that is showing. */export function tipAt(text: string | null, r?: Spot) { if (text === spotText) return; clearTimeout(spotTimer); spotText = text; if (!text || !r) { drop?.(); return; } spotTimer = window.setTimeout(() => { if (spotText === text) place?.(text, r); }, 250);}export function attachTips() { const tip = document.createElement('div'); tip.className = 'tip'; tip.hidden = true; tip.setAttribute('role', 'tooltip'); document.body.append(tip); let at: HTMLElement | null = null, timer = 0;
const target = (el: EventTarget | null) => { const e = el instanceof Element ? el.closest<HTMLElement>('[data-tip], [title]') : null; if (!e) return null; if (e.title) { e.dataset.tip = e.title; e.removeAttribute('title'); } return e.dataset.tip ? e : null; }; const hide = () => { clearTimeout(timer); clearTimeout(spotTimer); spotText = null; tip.hidden = true; at = null; }; const show = (e: HTMLElement) => showAt(e.dataset.tip!, e.getBoundingClientRect()); const showAt = (text: string, r: Spot) => { const [head, ...rest] = text.split('\n'); tip.innerHTML = rest.length ? `<b></b><span></span>` : `<span></span>`; if (rest.length) { tip.querySelector('b')!.textContent = head; tip.querySelector('span')!.textContent = rest.join('\n'); } else tip.querySelector('span')!.textContent = head; tip.hidden = false; // below the thing if there's room, else above; always on screen const w = tip.offsetWidth, h = tip.offsetHeight, pad = 8; const x = Math.max(pad, Math.min(innerWidth - w - pad, r.left + r.width / 2 - w / 2)); const y = r.bottom + 6 + h < innerHeight - pad ? r.bottom + 6 : Math.max(pad, r.top - 6 - h); tip.style.left = `${x}px`; tip.style.top = `${y}px`; };
place = showAt; drop = () => { if (!at) tip.hidden = true; }; document.addEventListener('pointerover', ev => { if (ev.pointerType === 'touch') return; const e = target(ev.target); if (e === at) return; hide(); if (!e) return; at = e; timer = window.setTimeout(() => { if (at === e && e.isConnected) show(e); }, 250); }); document.addEventListener('pointerdown', hide, true); document.addEventListener('keydown', hide, true); document.addEventListener('wheel', hide, { passive: true });}