Back to City Breaker
SOURCE / PINNED RELEASE

Made of little things.

City Breaker

Release
94d2b8664b10…
Author-recorded commit
df23103cbcc1…
License
LICENSE
Author’s source reference
nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-cf8b858653e

Archive hash verified: eeeed725668c1ece…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/ui/tooltip.ts
// Hover help for every control: any element with data-tip shows a description.
// Touch: press and hold for half a second. Canvases can show their own tips.

let tip: HTMLDivElement | null = null;
let current: HTMLElement | null = null;
let pressTimer = 0;

function el(): HTMLDivElement {
  if (!tip) {
    tip = document.createElement('div');
    tip.className = 'tip';
    tip.setAttribute('role', 'tooltip');
    tip.hidden = true;
    document.body.append(tip);
  }
  return tip;
}

/** Show a tooltip near a point (clientX/Y). `html` may contain simple markup. */
export function showTipAt(x: number, y: number, html: string): void {
  const t = el();
  t.innerHTML = html;
  t.hidden = false;
  const r = t.getBoundingClientRect();
  const W = window.innerWidth;
  const H = window.innerHeight;
  let left = x + 14;
  let top = y + 16;
  if (left + r.width > W - 6) left = Math.max(6, x - r.width - 10);
  if (top + r.height > H - 6) top = Math.max(6, y - r.height - 12);
  t.style.left = `${left}px`;
  t.style.top = `${top}px`;
}

export function hideTip(): void {
  if (tip) tip.hidden = true;
  current = null;
}

function tipFor(target: EventTarget | null): HTMLElement | null {
  return target instanceof Element ? target.closest<HTMLElement>('[data-tip]') : null;
}

function show(elm: HTMLElement): void {
  current = elm;
  const r = elm.getBoundingClientRect();
  const title = elm.dataset.tipTitle;
  const body = elm.dataset.tip ?? '';
  showTipAt(r.left + Math.min(r.width, 40), r.bottom - 6, `${title ? `<b>${title}</b>` : ''}${body}`);
}

export function installTooltips(): void {
  document.addEventListener('pointerover', (e) => {
    if (e.pointerType === 'touch') return;
    const t = tipFor(e.target);
    if (t && t !== current) show(t);
    if (!t && current) hideTip();
  });
  document.addEventListener('pointerout', (e) => {
    if (e.pointerType === 'touch') return;
    const to = tipFor(e.relatedTarget);
    if (!to) hideTip();
  });
  document.addEventListener('focusin', (e) => {
    const t = tipFor(e.target);
    if (t && (e.target as HTMLElement).matches(':focus-visible')) show(t);
  });
  document.addEventListener('focusout', hideTip);
  // Long-press on touch.
  document.addEventListener('pointerdown', (e) => {
    hideTip();
    if (e.pointerType !== 'touch') return;
    const t = tipFor(e.target);
    if (!t) return;
    window.clearTimeout(pressTimer);
    pressTimer = window.setTimeout(() => show(t), 500);
  });
  const cancel = () => window.clearTimeout(pressTimer);
  document.addEventListener('pointerup', cancel);
  document.addEventListener('pointercancel', cancel);
  document.addEventListener('scroll', hideTip, true);
  window.addEventListener('blur', hideTip);
}