UNPKG

vlens

Version:

Data Centric Routing & Rendering Mini-Framework

323 lines (284 loc) 8.06 kB
import * as core from "./core"; import * as refs from "./refs"; import * as cache from "./cache"; /** @deprecated */ export function readNumberAttr( el: HTMLElement | EventTarget | null, attr: string, ): number { if (el instanceof HTMLElement) { return parseInt(el.getAttribute(attr) ?? ""); } else { return Number.NaN; } } /** @deprecated */ export function readObjectRefAttr<T>( el: HTMLElement | EventTarget | null, attr: string, ): T | null { return refs.objectById<T>(readNumberAttr(el, attr)); } function onInput(ref: refs.Ref, transform: Transform<any, string> | undefined, event: Event) { let target = event.target as HTMLInputElement; let refValue = refs.get(ref); let value: any = target.value; if (target.type === "checkbox") { value = target.checked; } if (target.type === "radio") { value = target.value; } if (target.type === "number" || typeof refValue === "number") { value = parseInt(value); } if (ref.obj instanceof Storage) { value = JSON.stringify(value); } if (transform) { transform(ref, value) } else { refs.set(ref, value); } core.scheduleRedraw(); } export type RedrawMode = "regular" | "container" | "off"; export type Transform<T, R> = (r: refs.Ref<T>, v?: R) => R export interface InputOptions { redraw?: RedrawMode; transform?: Transform<any, string>; radio?: boolean initial?: any; } export function inputAttrs(ref?: refs.Ref, options: InputOptions = {}): any { if (!ref) { return {}; } let value = refs.get(ref); if (ref.obj instanceof Storage) { value = safeJsonParse(value, options.initial); } if (options?.transform) { value = options.transform(ref) } let isBoolean = typeof value === "boolean" || options.radio let valueField = isBoolean ? "checked" : "value"; return { [valueField]: value, onInput: cache.partial(onInput, ref, options.transform), }; } export function contentEditableAttrs(ref: refs.Ref<string>): any { let value = refs.get(ref); return { onInput: cache.partial(onInputContentEditable, ref), contentEditable: true, dangerouslySetInnerHTML: { __html: value }, }; } // content editable caret type CECaret = { activeElement: Element | null; // -1 for no selection start: number; end: number; }; // given a childNode and childNodeOffset, return an offset relative to the focusNode function offsetWithinFocusNode( focusNode: Node, childNode: Node, childNodeOffset: number, ): number { let currentOffset = 0; for (let i = 0; i < focusNode.childNodes.length; i++) { let n = focusNode.childNodes[i]; if (childNode === n) { return currentOffset + childNodeOffset; } else if (childNode.contains(n)) { return ( currentOffset + offsetWithinFocusNode(n, childNode, childNodeOffset) ); } else { let t = n.textContent; if (t) { currentOffset += t.length; } } } return -1; } function offsetContainerOfCaret( focusNode: Node, offset: number, ): [Node, number] | null { if (focusNode.childNodes.length === 0) { return [focusNode, offset]; } let currentOffset = 0; for (let i = 0; i < focusNode.childNodes.length; i++) { let n = focusNode.childNodes[i]; let t = n.textContent; let nextOffset = currentOffset; if (t) { nextOffset += t.length; } if (n.nodeName === "BR") { nextOffset++; } if (nextOffset >= offset) { // if node has no child nodes, just return the node itself. // if node has child nodes, recurse let relativeOffset = offset - currentOffset; if (n.childNodes.length > 0) { return offsetContainerOfCaret(n, relativeOffset); } else { return [n, relativeOffset]; } } currentOffset = nextOffset; } return null; } function getCaret(): CECaret { let sel = getSelection(); let activeElement = document.activeElement; let start = -1; let end = -1; if (activeElement && sel && sel.rangeCount > 0) { let range = sel.getRangeAt(0); start = offsetWithinFocusNode( activeElement, range.startContainer, range.startOffset, ); if (range.collapsed) { end = start; } else { end = offsetWithinFocusNode( activeElement, range.endContainer, range.endOffset, ); } } return { activeElement, start, end, }; } function setCaret(caret: CECaret) { if (caret.start == -1 || caret.end == -1) { return; } if (!caret.activeElement) { return; } let sel = window.getSelection(); if (!sel) { console.log("can't set caret; no selection"); return; } if (caret.activeElement !== document.activeElement) { // (caret.activeElement as HTMLElement).focus(); return; } let range = document.createRange(); let rangeStart = offsetContainerOfCaret(caret.activeElement, caret.start); let rangeEnd = offsetContainerOfCaret(caret.activeElement, caret.end); if (rangeStart && rangeEnd) { range.setStart(...rangeStart); range.setEnd(...rangeEnd); } else { return; } sel.removeAllRanges(); sel.addRange(range); } function onInputContentEditable(ref: refs.Ref, event: InputEvent) { const caret = getCaret(); // console.log("Caret:", caret) let currentTarget = event.currentTarget as HTMLElement; let content = currentTarget.innerHTML; refs.set(ref, content); core.scheduleRedraw(); requestAnimationFrame(() => { setCaret(caret); }); } // TODO: support this usecase within inputAttrs export function radioAttrs<T extends number | string>( ref: refs.Ref<T>, option: T, ) { return { value: option, checked: refs.get(ref) === option, onInput: cache.partial(onInput, ref, undefined), }; } export function toggleButtonAttrs(ref?: refs.Ref<boolean>) { if (!ref) { return {} } return { onClick: cache.partial(onToggleClick, ref), }; } function onToggleClick(ref: refs.Ref<boolean>, event: Event) { refs.set(ref, !refs.get(ref)); core.scheduleRedraw(); } // for boolean refs whose purpose is to know about click events export function consumeBooleanRef(b: refs.Ref<boolean>): boolean { let result = refs.get(b); if (result) { refs.set(b, false); } return result; } function safeJsonParse( raw: string | undefined | null, fallback?: unknown, ): unknown { if (!raw) { return fallback; } try { return JSON.parse(raw); } catch (e) { return fallback; } } export function elementRefAttrs(ref?: refs.Ref<HTMLElement | null>): any { if (!ref) { return {} } return { "listen-create": true, "listen-mutate": true, oncreate: cache.partial(eventSetRef, ref), onmutate: cache.partial(eventSetRef, ref), }; } export function eventSetRef(ref: refs.Ref<HTMLElement | null>, event: CustomEvent) { refs.set(ref, event.currentTarget); } function onHover(ref: refs.Ref<boolean>, event: Event) { if (event.type === "mouseenter") { refs.set(ref, true); } else if (event.type === "mouseleave") { refs.set(ref, false); } core.scheduleRedraw(); } export function trackHoverAttrs(ref?: refs.Ref<boolean>) { if (!ref) { return {} } return { onMouseEnter: cache.partial(onHover, ref), onMouseLeave: cache.partial(onHover, ref), }; }