import type { Signal } from "@char/aftercare"; import type { JSX } from "@char/aftercare/jsx-runtime"; /** * run `render` now and (coalesced per microtask) whenever any signal changes, for as long as * `el` stays in the document */ // deno-lint-ignore no-explicit-any export function watch(el: Node, signals: readonly Signal[], render: () => void): void { let mounted = false; let queued = false; const run = () => { queued = false; if (mounted && !el.isConnected) return subs.forEach(s => s.unsubscribe()); mounted ||= el.isConnected; render(); }; const subs = signals.map(s => s.subscribe(() => { if (queued) return; queued = true; queueMicrotask(run); }), ); render(); } /** follow a pointer drag at the window level, so it survives the target being re-rendered */ export function drag( start: PointerEvent, move: (e: PointerEvent) => void, end?: (e: PointerEvent) => void, ): void { const onMove = (e: PointerEvent) => { if (e.pointerId === start.pointerId) move(e); }; const onUp = (e: PointerEvent) => { if (e.pointerId !== start.pointerId) return; removeEventListener("pointermove", onMove); removeEventListener("pointerup", onUp); removeEventListener("pointercancel", onUp); end?.(e); }; addEventListener("pointermove", onMove); addEventListener("pointerup", onUp); addEventListener("pointercancel", onUp); } /** * make `parent`'s children exactly `nodes`, leaving nodes that stay in place untouched: moving a * node out and back in would blur it if focused, or drop a click that's halfway through */ export function setChildren(parent: Node, nodes: readonly Node[]): void { const keep = new Set(nodes); for (const child of [...parent.childNodes]) if (!keep.has(child)) child.remove(); let at = parent.firstChild; for (const node of nodes) { if (node === at) at = at.nextSibling; else parent.insertBefore(node, at); } } export const px = (n: number): string => `${n}px`; /** a toggle for a collapsible panel, with a chevron for which way it is */ export const disclosure = (open: Signal, label: string): JSX.Element => ( ); /** a (?) that explains something when hovered */ export const help = (text: string): JSX.Element => ( ? ); /** ask for a file to open; nothing comes of it if none is chosen */ export function chooseFile(accept: string, then: (file: File) => void): void { const input = () as HTMLInputElement; input.addEventListener("change", () => input.files?.[0] && then(input.files[0])); input.click(); } /** download something as a file, named for `name` (which may be anything a project's called) */ export function saveFile(name: string, extension: string, data: BlobPart, type: string): void { const url = URL.createObjectURL(new Blob([data], { type })); const link = document.createElement("a"); link.href = url; link.download = `${name.replace(/[/\\:*?"<>|]/g, "-") || "untitled"}${extension}`; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }