// livetree.bind.ts import { is_Node } from "../../../core/node-guards.js"; import type { HsonNode } from "../../../core/types.js"; import type { DocumentLiveMap, LiveMapPathHandle } from "../../../types/livemap.types.js"; import { is_livemap_document_location } from "../../livemap/livemap.document.location.js"; import { is_livemap_projected_location } from "../../livemap/livemap.handle.js"; import type { LiveTree } from "../livetree.js"; import { own_disposable_for_subject } from "../managers/lifecycle-registry.js"; import { runtime_for_tree, subject_for_tree } from "../runtime/livetree-runtime.js"; type LiveTreeBindable = Pick; export type LiveTreeBindApi = Readonly<{ path: ( source: TSource, apply: PathApply>>, ) => LiveMapDisposer; paths: ( sources: TSources, apply: PathsApply>, ) => LiveMapDisposer; textPaths: ( sources: TSources, toText: PathsTextMapper>, ) => LiveMapDisposer; text: ( source: TSource, ...formatter: TextFormatterArgument ) => LiveMapDisposer; attr: ( source: TSource, name: string, ...formatter: TextFormatterArgument ) => LiveMapDisposer; attrs: ( source: TSource, toAttrs: AttrMapper>>, ) => LiveMapDisposer; attrsPaths: ( sources: TSources, toAttrs: PathsAttrMapper>, ) => LiveMapDisposer; css: ( source: TSource, toCss: CssMapper>>, ) => LiveMapDisposer; cssPaths: ( sources: TSources, toCss: PathsCssMapper>, ) => LiveMapDisposer; }>; export function make_livetree_bind_api(tree: TTree): LiveTreeBindApi { return Object.freeze({ path: ( source: TSource, apply: PathApply>>, ) => bind_path_for(tree, binding_capability(source), (target, value, previous) => { apply(target, value, previous); }), paths: ( sources: TSources, apply: PathsApply>, ) => bind_paths_for(tree, sources, apply), textPaths: ( sources: TSources, toText: PathsTextMapper>, ) => bind_text_paths_for(tree, sources, toText), text: ( source: TSource, ...formatter: TextFormatterArgument ) => bind_text_for(tree, binding_capability(source), text_formatter(formatter)), attr: ( source: TSource, name: string, ...formatter: TextFormatterArgument ) => bind_attr_for(tree, binding_capability(source), name, text_formatter(formatter)), attrs: ( source: TSource, toAttrs: AttrMapper>>, ) => bind_attrs_for(tree, binding_capability(source), toAttrs), attrsPaths: ( sources: TSources, toAttrs: PathsAttrMapper>, ) => bind_attrs_paths_for(tree, sources, toAttrs), css: ( source: TSource, toCss: CssMapper>>, ) => bind_css_for(tree, binding_capability(source), toCss), cssPaths: ( sources: TSources, toCss: PathsCssMapper>, ) => bind_css_paths_for(tree, sources, toCss), }); } function bind_path_for( tree: TTree, source: BindingCapability, apply: InternalPathApply, ): LiveMapDisposer { let previous: TValue | undefined; const mode = classify_binding_source(source); const sync = (value: TValue): void => { apply(tree, value, previous, mode); previous = value; }; sync(source.snap()); return own_disposable_for_subject( subject_for_tree(tree), source.watch(sync), "binding", runtime_for_tree(tree), ); } function bind_paths_for( tree: TTree, sources: TSources, apply: PathsApply>, ): LiveMapDisposer { let previous: BindingValues | undefined; sources.forEach(classify_binding_source); const sync = (): void => { const values = sources.map((source) => source.snap()) as unknown as BindingValues; apply(tree, values, previous); previous = values; }; sync(); const disposers = sources.map((source) => source.watch(sync)); return own_disposable_for_subject( subject_for_tree(tree), () => dispose_all(disposers), "binding", runtime_for_tree(tree), ); } function bind_text_paths_for( tree: TTree, sources: TSources, toText: PathsTextMapper>, ): LiveMapDisposer { return bind_paths_for(tree, sources, (target, values, previous) => { target.text.set(toText(values, previous)); }); } function bind_attrs_paths_for( tree: TTree, sources: TSources, toAttrs: PathsAttrMapper>, ): LiveMapDisposer { return bind_paths_for(tree, sources, (target, values, previous) => { apply_attrs(target, toAttrs(values, previous)); }); } function bind_css_paths_for( tree: TTree, sources: TSources, toCss: PathsCssMapper>, ): LiveMapDisposer { return bind_paths_for(tree, sources, (target, values, previous) => { apply_css(target, toCss(values, previous)); }); } function bind_text_for( tree: TTree, source: BindingCapability, toText?: TextMapper, ): LiveMapDisposer { return bind_path_for(tree, source, (target, value, previous, mode) => { if (toText === undefined) reject_unmapped_document_node(mode, value); const text = toText ? toText(value, previous) : String(value ?? ""); target.text.set(text); }); } function bind_attr_for( tree: TTree, source: BindingCapability, name: string, toValue?: TextMapper, ): LiveMapDisposer { return bind_path_for(tree, source, (target, value, previous, mode) => { if (toValue === undefined) reject_unmapped_document_node(mode, value); const attrValue = toValue ? toValue(value, previous) : value; apply_attrs(target, { [name]: attrValue as string | number | boolean | null | undefined }); }); } function bind_attrs_for( tree: TTree, source: BindingCapability, toAttrs: AttrMapper, ): LiveMapDisposer { return bind_path_for(tree, source, (target, value, previous) => { apply_attrs(target, toAttrs(value, previous)); }); } function bind_css_for( tree: TTree, source: BindingCapability, toCss: CssMapper, ): LiveMapDisposer { return bind_path_for(tree, source, (target, value, previous) => { apply_css(target, toCss(value, previous)); }); } type LiveMapDisposer = () => void; type BindingCapability = Readonly<{ snap(): TValue; watch(listener: (next: TValue) => void): LiveMapDisposer; }>; type ProjectedBindingSource = Pick, "snap" | "watch" | "feed">; type DocumentBindingSource = ReturnType; type BindingSource = ProjectedBindingSource | DocumentBindingSource; type BindingValue = ReturnType; type BindingValues = { readonly [TIndex in keyof TSources]: BindingValue; }; type BindingSourceMode = "projected" | "document"; type TextFormatterArgument = TSource extends ProjectedBindingSource ? readonly [toText?: TextMapper>>] : [Extract, HsonNode>] extends [never] ? readonly [toText?: TextMapper>>] : readonly [toText: TextMapper>>]; type PathApply = ( tree: TTree, value: TValue, previous: TValue | undefined, ) => void; type InternalPathApply = ( tree: TTree, value: TValue, previous: TValue | undefined, mode: BindingSourceMode, ) => void; type PathsApply = ( tree: TTree, values: TValues, previous: TValues | undefined, ) => void; type CssValueMap = Readonly>; type AttrValueMap = Readonly>; type CssMapper = (value: TValue, previous: TValue | undefined) => CssValueMap; type AttrMapper = (value: TValue, previous: TValue | undefined) => AttrValueMap; type TextMapper = (value: TValue, previous: TValue | undefined) => string; type PathsCssMapper = (values: TValues, previous: TValues | undefined) => CssValueMap; type PathsAttrMapper = (values: TValues, previous: TValues | undefined) => AttrValueMap; type PathsTextMapper = (values: TValues, previous: TValues | undefined) => string; function dispose_all(disposers: readonly LiveMapDisposer[]): void { disposers.forEach((dispose) => dispose()); } function classify_binding_source(source: unknown): BindingSourceMode { if (is_livemap_projected_location(source)) return "projected"; if (is_livemap_document_location(source)) return "document"; throw new TypeError("LiveTree.bind source must be an authentic passive LiveMap or Locus location."); } function binding_capability( source: TSource, ): BindingCapability> { return source as BindingCapability>; } function text_formatter( formatter: TextFormatterArgument, ): TextMapper> | undefined { return formatter[0] as TextMapper> | undefined; } function reject_unmapped_document_node(mode: BindingSourceMode, value: unknown): void { if (mode === "document" && is_Node(value)) { throw new TypeError("LiveTree.bind document Hson values require an explicit mapper for primitive destinations."); } } function apply_css(tree: LiveTreeBindable, values: CssValueMap): void { Object.entries(values).forEach(([key, value]) => { if (value === null || value === undefined) { tree.css.remove(key); return; } tree.css.setProp(key, String(value)); }); } function apply_attrs(tree: LiveTreeBindable, values: AttrValueMap): void { Object.entries(values).forEach(([key, value]) => { if (value === null || value === undefined || value === false) { tree.attrs.drop(key); return; } tree.attrs.set(key, value === true ? key : String(value)); }); } export function bind_path( this: TTree, source: TSource, apply: PathApply>>, ): LiveMapDisposer { return bind_path_for(this, binding_capability(source), (target, value, previous) => { apply(target, value, previous); }); } export function bind_paths( this: TTree, sources: TSources, apply: PathsApply>, ): LiveMapDisposer { return bind_paths_for(this, sources, apply); } export function bind_text_paths( this: TTree, sources: TSources, toText: PathsTextMapper>, ): LiveMapDisposer { return bind_text_paths_for(this, sources, toText); } export function bind_text( this: TTree, source: TSource, ...formatter: TextFormatterArgument ): LiveMapDisposer { return bind_text_for(this, binding_capability(source), text_formatter(formatter)); } export function bind_attr( this: TTree, source: TSource, name: string, ...formatter: TextFormatterArgument ): LiveMapDisposer { return bind_attr_for(this, binding_capability(source), name, text_formatter(formatter)); } export function bind_attrs_paths( this: TTree, sources: TSources, toAttrs: PathsAttrMapper>, ): LiveMapDisposer { return bind_attrs_paths_for(this, sources, toAttrs); } export function bind_attrs( this: TTree, source: TSource, toAttrs: AttrMapper>>, ): LiveMapDisposer { return bind_attrs_for(this, binding_capability(source), toAttrs); } export function bind_css_paths( this: TTree, sources: TSources, toCss: PathsCssMapper>, ): LiveMapDisposer { return bind_css_paths_for(this, sources, toCss); } export function bind_css( this: TTree, source: TSource, toCss: CssMapper>>, ): LiveMapDisposer { return bind_css_for(this, binding_capability(source), toCss); }