// tree-selector.ts import { LiveTree } from "../livetree.js"; type LiveTreeStyle = LiveTree["style"]; type LiveTreeCss = LiveTree["css"]; type LiveTreeListen = LiveTree["listen"]; export interface TreeSelectorType { array(): LiveTree[]; length: number; first(): LiveTree | undefined; last(): LiveTree | undefined; at(ix: number): LiveTree | undefined; each(fn: (tree: LiveTree, index: number) => void): void; map(fn: (tree: LiveTree, index: number) => T): T[]; filter(fn: (tree: LiveTree, index: number) => boolean): TreeSelector; removeAt(ix: number): boolean; removeAll(): number; readonly listen: LiveTreeListen; readonly style: LiveTreeStyle; readonly css: LiveTreeCss; readonly data: LiveTree["data"]; } /** * Broadcast helper: * Creates a proxy that forwards any method call to *each* selected LiveTree’s manager. * * Example: selector.style.setMany({ ... }) calls tree.style.setMany(...) for every tree. */ function makeBroadcastProxy( items: readonly LiveTree[], pick: (t: LiveTree) => T, ): T { const base: T | undefined = items[0] ? pick(items[0]) : undefined; // empty selection: return a no-op proxy that safely absorbs calls. if (!base) { const noop = new Proxy( {}, { get() { return () => undefined; }, }, ); // TS can’t infer Proxy shape; this is one contained cast. return noop as unknown as T; } const proxy = new Proxy(base, { get(target, prop, receiver) { const v = Reflect.get(target, prop, receiver); // Pass through non-functions (properties, getters, etc.) if (typeof v !== "function") return v; // Broadcast function calls across all items return (...args: unknown[]) => { let last: unknown = undefined; for (let i = 0; i < items.length; i += 1) { const mgr = pick(items[i]); const fn = (mgr as unknown as Record)[prop]; if (typeof fn === "function") { last = (fn as (...xs: unknown[]) => unknown).apply(mgr, args); } } // Return the last manager’s return value (usually ignored, but keeps behavior predictable) return last; }; }, }); return proxy as unknown as T; // isolated cast } export class TreeSelector implements TreeSelectorType { private readonly contents: LiveTree[]; private listenInternal: LiveTreeListen | undefined; private styleInternal: LiveTreeStyle | undefined; private cssInternal: LiveTreeCss | undefined; private dataInternal: LiveTree["data"] | undefined; public constructor(trees: LiveTree[]) { // Defensive copy to avoid external mutation. this.contents = [...trees]; } public get listen(): LiveTreeListen { return this.listenInternal ??= makeBroadcastProxy(this.contents, (tree) => tree.listen); } public get style(): LiveTreeStyle { return this.styleInternal ??= makeBroadcastProxy(this.contents, (tree) => tree.style); } public get css(): LiveTreeCss { return this.cssInternal ??= makeBroadcastProxy(this.contents, (tree) => tree.css); } public get data(): LiveTree["data"] { return this.dataInternal ??= makeBroadcastProxy(this.contents, (tree) => tree.data); } public get length(): number { return this.contents.length; } public array(): LiveTree[] { return [...this.contents]; } public at(ix: number): LiveTree | undefined { return this.contents[ix]; } public last(): LiveTree | undefined { if (this.contents.length === 0) return undefined; return this.contents[this.contents.length - 1]; } public first(): LiveTree | undefined { return this.contents[0]; } public each(fn: (tree: LiveTree, index: number) => void): void { for (let i = 0; i < this.contents.length; i += 1) fn(this.contents[i], i); } public map(fn: (tree: LiveTree, index: number) => T): T[] { const out: T[] = []; for (let i = 0; i < this.contents.length; i += 1) out.push(fn(this.contents[i], i)); return out; } public filter(fn: (tree: LiveTree, index: number) => boolean): TreeSelector { const next: LiveTree[] = []; for (let i = 0; i < this.contents.length; i += 1) { if (fn(this.contents[i], i)) next.push(this.contents[i]); } return new TreeSelector(next); } public removeAt(ix: number): boolean { const hit = this.contents[ix]; if (!hit) return false; hit.remove(); return true; } public removeAll(): number { let n = 0; for (const t of this.contents) { t.remove(); n += 1; } return n; } }