/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2023 Tony Garnock-Jones import { Turn, Facet, Dataflow, Dataspace, Ref } from "@syndicate-lang/core"; import { HtmlFragmentsTemplate, HtmlFragment, template } from "./html"; export * from "./html"; export assertion type LocationHash(hash: string); export type LocationHash = ReturnType; export function boot(ds = Dataspace.global) { spawnLocationHashTracker(ds); } type Wrapped = { wrapped: EventListenerOrEventListenerObject, options?: AddEventListenerOptions | boolean, }; export class Widget implements EventTarget { facet: Facet; node: Dataflow.Field; callbacks = new Map>(); _parent: Dataflow.Field; constructor (private nodeGenerator: (t: HtmlFragmentsTemplate) => HtmlFragment) { this.facet = Turn.activeFacet; on stop { this.node.value?.remove(); } field node: ChildNode | null = null; this.node = node; field parent: ParentNode | null = null; this._parent = parent; dataflow { const oldNode = this.node.value; const fragments = template`${this.nodeGenerator(template)}`; const newNodes = fragments.nodes(); if (newNodes.length > 1) { throw new Error(`@syndicate-lang/html2: Multiple nodes returned from template: ${fragments.toString()}`); } const newNode = newNodes[0]; if (oldNode !== newNode) { if (oldNode !== null) { for (const [type, cbs] of this.callbacks.entries()) { for (const entry of cbs.values()) { oldNode.removeEventListener(type, entry.wrapped, entry.options); } } } if (newNode !== null) { for (const [type, cbs] of this.callbacks.entries()) { for (const entry of cbs.values()) { newNode.addEventListener(type, entry.wrapped, entry.options); } } } const wasFocus = oldNode && document.activeElement === oldNode; oldNode?.parentNode?.replaceChild(newNode, oldNode); if (wasFocus) { if (newNode && newNode instanceof HTMLElement) { newNode.focus(); } } this.node.value = newNode; } } dataflow { if (this.node.value === null) return; const p = this._parent.value; if (this.node.value.parentNode !== p) { if (p === null) { this.node.value.remove(); } else { p.appendChild(this.node.value); } } } } get parent(): ParentNode | null { return this._parent.value; } set parent(p: string | ParentNode | null) { this.setParent(p); } setParent(p: string | ParentNode | null, wrt: ParentNode = document): this { if (typeof p === 'string') { p = wrt.querySelector(p); } this._parent.value = p; return this; } on(type: string, callback: EventListenerOrEventListenerObject): this { this.addEventListener(type, callback); return this; } once(type: string, callback: EventListenerOrEventListenerObject): this { this.addEventListener(type, callback, { once: true }); return this; } off(type: string, callback: EventListenerOrEventListenerObject): this { this.removeEventListener(type, callback); return this; } addEventListener( type: string, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean, ): void { if (callback === null) return; let cbs = this.callbacks.get(type); if (cbs === void 0) { cbs = new Map(); this.callbacks.set(type, cbs); } else { if (cbs.has(callback)) return; } const entry: Wrapped = { wrapped: (typeof callback === 'function') ? evt => this.facet.turn(() => callback(evt)) : evt => this.facet.turn(() => callback.handleEvent(evt)), options, }; cbs.set(callback, entry); this.node.value?.addEventListener(type, entry.wrapped, options); } dispatchEvent(event: Event): boolean { return !this.node.value || this.node.value.dispatchEvent(event); } removeEventListener( type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined, ): void { if (callback === null) return; const cbs = this.callbacks.get(type); if (cbs === void 0) return; const r = cbs.get(callback); if (r === void 0) return; this.node.value?.removeEventListener(type, r.wrapped, options); cbs.delete(callback); if (cbs.size === 0) this.callbacks.delete(type); } } function spawnLocationHashTracker(ds: Ref) { spawn named 'LocationHashTracker' { at ds { field hashValue: string = '/'; assert LocationHash(hashValue.value); const loadHash = () => { var h = window.location.hash; if (h.length && h[0] === '#') { h = h.slice(1); } hashValue.value = h || '/'; }; const facet = Turn.activeFacet; const handlerClosure = () => facet.turn(loadHash); loadHash(); window.addEventListener('hashchange', handlerClosure); on stop window.removeEventListener('hashchange', handlerClosure); on message LocationHash($newHash: string) => { window.location.hash = newHash; } } } }