UNPKG

ivi

Version:

Lightweight Embeddable Web UI Library.

1,753 lines (1,648 loc) 55 kB
import { type TemplateData, TemplateFlags, ChildOpCode, PropOpCode, StateOpCode, CommonPropType, } from "./template.js"; export const EMPTY_ARRAY: any[] = []; /** * Globally shared strings are automatically generated by template compiler. */ const __IVI_STRINGS__: string[] = ["IVI:fa7327d9-0034-492d-bfdf-576548b2d9cc"]; // Store global variables in a local scope as const variables so that JIT // compiler could easily inline functions and eliminate checks in case global // variables are overriden. const _Object = Object; const _Array = Array; const _isArray: <T = any>(a: any) => a is T[] = _Array.isArray; const _Map = Map; const _Int32Array = Int32Array; const _queueMicrotask = queueMicrotask; const _requestAnimationFrame = requestAnimationFrame; const _requestIdleCallback = requestIdleCallback; const nodeProto = Node.prototype; const elementProto = Element.prototype; const doc = document; // Template containers are used to create static templates from HTML strings // via `innerHTML`. const HTM_TEMPLATE = /**@__PURE__*/doc.createElement("template"); const HTM_TEMPLATE_CONTENT = HTM_TEMPLATE.content; const _SVG_TEMPLATE = /**@__PURE__*/doc.createElement("template"); const SVG_TEMPLATE = /**@__PURE__*/doc.createElementNS("http://www.w3.org/2000/svg", "svg"); _SVG_TEMPLATE.content.appendChild(SVG_TEMPLATE); const SVG_TEMPLATE_CONTENT = _SVG_TEMPLATE.content.firstChild as Element; // Store Node/Element methods to avoid going through a long prototype chain and // avoid megamorphic call-sites when accessing DOM nodes. /** `Node.prototype.insertBefore` */ const nodeInsertBefore: <T extends Node>(this: Node, node: T, child: Node | null) => T = nodeProto.insertBefore; /** `Node.prototype.removeChild`. */ const nodeRemoveChild: <T extends Node>(this: Node, node: T) => T = nodeProto.removeChild; /** `Node.prototype.cloneNode`. */ const nodeCloneNode: (this: Node, deep?: boolean | undefined) => Node = nodeProto.cloneNode; declare global { interface Element { moveBefore<T extends Element | CharacterData>(node: T, child: Node | null): void; } } /** `Element.prototype.moveBefore` */ const elementMoveBefore: <T extends Element | CharacterData>(this: Element, node: T, child: Node | null) => void = elementProto.moveBefore ?? nodeInsertBefore; /** `Element.prototype.setAttribute` */ const elementSetAttribute: (this: Element, qualifiedName: string, value: string) => void = elementProto.setAttribute; /** `Element.prototype.removeAttribute` */ const elementRemoveAttribute: (this: Element, qualifiedName: string) => void = elementProto.removeAttribute; /** `EventTarget.prototype.addEventListener` */ const elementAddEventListener = elementProto.addEventListener; /** `EventTarget.prototype.removeEventListener` */ const elementRemoveEventListener = elementProto.removeEventListener; /** `Object.getOwnPropertyDescriptor(o, p)` */ const getDescriptor = (o: any, p: string | number | symbol) => _Object.getOwnPropertyDescriptor(o, p); /** `get Node.prototype.firstChild` */ const nodeGetFirstChild: (this: Node) => ChildNode | null = /*@__PURE__*/getDescriptor(nodeProto, "firstChild")!.get!; /** `get Node.prototype.nextSibling` */ const nodeGetNextSibling: (this: Node) => ChildNode | null = /*@__PURE__*/getDescriptor(nodeProto, "nextSibling")!.get!; /** `set Node.prototype.textContent` */ const nodeSetTextContent: (this: Node, value: string | number) => void = /*@__PURE__*/getDescriptor(nodeProto, "textContent")!.set!; /** `set Element.prototype.innerHTML` */ const elementSetInnerHTML: (this: Element, value: string) => void = /*@__PURE__*/getDescriptor(elementProto, "innerHTML")!.set!; /** `set Element.prototype.className` */ const elementSetClassName: (this: Element, value: string) => void = /*@__PURE__*/getDescriptor(elementProto, "className")!.set!; /** `get HTMLElement.prototype.style`. */ const htmlElementGetStyle: (this: HTMLElement) => CSSStyleDeclaration = /*@__PURE__*/getDescriptor(HTMLElement.prototype, "style")!.get!; /** `get SVGElement.prototype.style` */ const svgElementGetStyle: (this: SVGElement) => CSSStyleDeclaration = /*@__PURE__*/getDescriptor(SVGElement.prototype, "style")!.get!; /** * Render Context. */ export interface RenderContext { /** Parent DOM Element */ p: Element; /** Next DOM Node. */ n: Node | null; /** Template state index. */ si: number; /** DOM Side Effects */ e: Array<() => void>; } // When object is sealed and stored in a const variable, JIT compiler can // eliminate object map(shape) checks when accessing its properties. /** * Global Render Context. */ export const RENDER_CONTEXT: RenderContext = _Object.seal({ p: null!, n: null, si: 0, e: [], }); // Types are stored as bit flags so that we could perform multiple tests with a // single bitwise operation. E.g. `flags & (List | Array)`. /** * Flags. */ export const enum Flags { // VNode and SNode flags Template = 1, Component = 1 << 1, List = 1 << 2, Array = 1 << 3, Text = 1 << 4, Root = 1 << 5, Context = 1 << 6, TypeMask = (1 << 7) - 1, // Component Dirty Flags Dirty = 1 << 7, DirtySubtree = 1 << 8, // Update Flags ForceUpdate = 1 << 9, DisplaceNode = 1 << 10, } /** * Stateful Node. */ export type SAny = | null // Hole | SRoot // Root | SText // Text | STemplate // Template | SList // Dynamic List | SComponent // Component | SContext // Context ; /** * Stateful Node. */ export type SNode<V = VAny> = SNode1<V> | SNode2<V>; /** * Stateful Node with 1 state slot. * * @typeparam S1 State slot #1. */ export interface SNode1<V = VAny, S1 = any> { /** Stateless Node. */ v: V; /** See {@link Flags} for details. */ f: Flags; /** Children Stateful Nodes. */ c: SNode | (SNode | null)[] | null; /** Parent Stateful Node. */ p: SNode | null, /** State slot #1. */ s1: S1; } /** * Stateful Node with 2 state slots. * * @typeparam S1 State slot #1. * @typeparam S2 State slot #2. */ export interface SNode2<V = VAny, S1 = any, S2 = any> extends SNode1<V, S1> { /** State slot #2. */ s2: S2; } /** Stateful Root Node. */ export type SRoot<S = any> = SNode1<VRoot, S>; /** Stateful Root Node. */ export type Root<S = any> = SRoot<S>; /** Stateful Text Node. */ export type SText = SNode1<string | number, Text>; /** Stateful Template Node. */ export type STemplate = SNode1<VTemplate, Node[]>; /** Stateful List Node. */ export type SList = SNode1<VList, null>; /** Stateful Component Node. */ export type SComponent<P = any> = SNode2< VComponent, /** Render function. */ null | ComponentRenderFn<P>, /** Unmount hooks. */ null | (() => void) | (() => void)[] >; /** Stateful Component Node. */ export type Component<P = any> = SComponent<P>; export type ComponentRenderFn<P = any> = (props: P) => VAny; export type SContext = SNode1<VContext, null>; /** * Creates a Stateful Node instance. * * @param v VNode. * @returns {@link SNode} instance. */ export const createSNode = <V extends VAny, S>( f: Flags, v: V, c: SNode | Array<SNode1 | null> | null, p: SNode | null, s1: S, ): SNode1<V, S> => ({ f, v, c, p, s1 }); /** * Stateless Tree Node. */ export type VAny = | null // Hole | undefined // Hole | false // Hole | string // Text | number // Text | VRoot // Root | VTemplate // Template | VComponent // Component | VContext // Context | VList // Dynamic List with track by key algo | VAny[] // Dynamic List with track by index algo ; /** * Stateless Node Descriptor. */ export interface VDescriptor<P1 = any, P2 = any> { /** See {@link Flags} for details. */ readonly f: Flags; /** First property. */ readonly p1: P1; /** Second property. */ readonly p2: P2; } /** Root Invalidate Hook. */ export type OnRootInvalidated<S> = (root: SRoot<S>, state: S) => void; /** Root Descriptor. */ export type RootDescriptor<S = any> = VDescriptor<OnRootInvalidated<S>, null>; /** Template Descriptor */ export type TemplateDescriptor = VDescriptor<TemplateData, () => Element>; /** Component Descriptor */ export type ComponentDescriptor<P = any> = VDescriptor< // Component factory function. ComponentFactoryFn<P>, // `areEqual()` function. undefined | ((prev: P, next: P) => boolean) >; export type ComponentFactoryFn<P = any> = (component: Component) => (props: P) => VAny; /** Context Descriptor */ export type ContextDescriptor = VDescriptor<null, null>; /** List Descriptor */ export type ListDescriptor = VDescriptor<null, null>; /** * Stateless Node. * * @typeparam D Descriptor. * @typeparam P Property. */ export interface VNode<D extends VDescriptor<any, any> = VDescriptor<any, any>, P = any> { /** Descriptor. */ readonly d: D; /** Property. */ readonly p: P; } /** Stateless Root Node. */ export type VRoot = VNode<RootDescriptor, RootProps>; /** Stateless Template Node. */ export type VTemplate<P = any> = VNode<TemplateDescriptor, P>; /** Stateless Component Node. */ export type VComponent<P = any> = VNode<ComponentDescriptor, P>; /** Stateless Context Node. */ export type VContext<T = any> = VNode<ContextDescriptor, ContextProps<T>>; /** Stateless List Node. */ export type VList<K = any> = VNode<ListDescriptor, ListProps<K>>; /** * Stateless Root Node Props. * * Contains a DOM position where root children should mounted. */ export interface RootProps { /** Parent Element */ p: Element, /** Next Node */ n: Node | null, } /** * Stateless Context Node Props. */ export interface ContextProps<T = any> { /** Context Value. */ v: T; /** Stateless Child Node. */ c: VAny; } /** * Stateless List Node Props. * * Contains unique keys for stateless nodes and stateless nodes. */ export interface ListProps<K = any> { /** Unique Keys. */ k: K[], /** Stateless Nodes. */ v: VAny[], } /** * Element Directive. */ export type ElementDirective = <E extends Element>(element: E) => void; export const _flushDOMEffects = () => { const e = RENDER_CONTEXT.e; if (e.length > 0) { RENDER_CONTEXT.e = []; for (let i = 0; i < e.length; i++) { e[i](); } } }; const _updateTemplateProperties = ( currentElement: Element, opCodes: PropOpCode[], data: string[], state: Node[], prevProps: any[] | null, nextProps: any[], svg: boolean, ) => { let style: CSSStyleDeclaration | undefined; for (let i = 0; i < opCodes.length; i++) { const op = opCodes[i]; const type = op & PropOpCode.TypeMask; const dataIndex = op >> PropOpCode.DataShift; if (type === PropOpCode.SetNode) { currentElement = state[dataIndex] as Element; style = void 0; } else { const propsIndex = (op >> PropOpCode.InputShift) & PropOpCode.Mask6; const next = nextProps[propsIndex]; if (type === PropOpCode.DiffDOMProperty) { const key = data[dataIndex]; if (prevProps === null) { if (next !== void 0) { (currentElement as Record<string, any>)[key] = next; } } else if ((currentElement as Record<string, any>)[key] !== next) { (currentElement as Record<string, any>)[key] = next; } } else { let prev; if (prevProps !== null) { prev = prevProps[propsIndex]; } if (prev !== next) { if (type === PropOpCode.Common) { if (dataIndex === CommonPropType.ClassName) { if (next !== "" && next != null && next !== false) { elementSetClassName.call(currentElement, next); } else if (prev !== "" && prev != null && prev !== false) { elementSetClassName.call(currentElement, ""); } } else if (dataIndex === CommonPropType.TextContent) { if (next !== "" && next != null && next !== false) { if (prev == null || prev === "" || prev === false) { nodeSetTextContent.call(currentElement, next); } else { nodeGetFirstChild.call(currentElement)!.nodeValue = next; } } else if (prev != null && prev !== "" && prev !== false) { nodeSetTextContent.call(currentElement, ""); } } else { // CommonPropType.InnerHTML if (next !== "" && next != null && next !== false) { elementSetInnerHTML.call(currentElement, next); } else if (prev !== "" && prev != null && prev !== false) { nodeSetTextContent.call(currentElement, ""); } } } else if (type === PropOpCode.Directive) { (next as ElementDirective)(currentElement); } else { const key = data[dataIndex]; if (type === PropOpCode.Attribute) { if (next !== false && next != null) { elementSetAttribute.call(currentElement, key, next as string); } else if (prev !== false && prev != null) { elementRemoveAttribute.call(currentElement, key); } } else if (type === PropOpCode.Property) { (currentElement as Record<string, any>)[key] = next; } else if (type === PropOpCode.Style) { if (next !== false && next != null) { if (style === void 0) { style = (svg === false) ? htmlElementGetStyle.call(currentElement as HTMLElement) : svgElementGetStyle.call(currentElement as SVGElement); } style!.setProperty(key, next as string); } else if (prev !== false && prev != null) { if (style === void 0) { style = (svg === false) ? htmlElementGetStyle.call(currentElement as HTMLElement) : svgElementGetStyle.call(currentElement as SVGElement); } style!.removeProperty(key); } } else { // PropOpCode.Event if (prev != null && prev !== false) { elementRemoveEventListener.call(currentElement, key, prev); } if (next != null && next !== false) { elementAddEventListener.call(currentElement, key, next); } } } } } } } }; const _assignTemplateSlots = ( currentNode: Node, opCodes: StateOpCode[], offset: number, endOffset: number, state: Node[], ) => { const ctx = RENDER_CONTEXT; while (true) { const op = opCodes[offset++]; if (op & StateOpCode.Save) { state[++ctx.si] = currentNode; } if (op & StateOpCode.EnterOrRemove) { const enterOffset = op >> StateOpCode.OffsetShift; // Enter offset is used to disambiguate between enter and remove // operations. Remove operations will always have a 0 enterOffset. if (enterOffset) { // Enter _assignTemplateSlots( nodeGetFirstChild.call(currentNode)!, opCodes, offset, offset += enterOffset, state, ); } else { // Remove // Remove operation implies that current node is always a comment node // followed by a text node. const commentNode = currentNode as Comment; state[++ctx.si] = currentNode = nodeGetNextSibling.call(currentNode)!; commentNode.remove(); } } if (offset === endOffset) { return; } currentNode = nodeGetNextSibling.call(currentNode as ChildNode)!; } }; const _mountList = ( parentState: SNode1, flags: Flags, children: VAny[], vNode: VAny, ): SNode1 => { let i = children.length; const sChildren = _Array(i); const sNode = createSNode(flags, vNode, sChildren, parentState, null); while (i > 0) { sChildren[--i] = _mount(sNode, children[i]); } return sNode; }; const _updateArray = ( parentSNode: SNode1, sNode: SNode1, next: VAny, updateFlags: Flags, ): SNode1 | null => { if (!_isArray(next)) { _unmount(sNode, true); return _mount(parentSNode, next); } const prevSChildren = sNode.c as (SNode1 | null)[]; let nextSChildren = prevSChildren; let prevLength = prevSChildren.length; let nextLength = next.length; if (nextLength !== prevLength) { sNode.c = nextSChildren = _Array(nextLength); while (prevLength > nextLength) { const sChild = prevSChildren[--prevLength]; if (sChild !== null) { _unmount(sChild, true); } } while (nextLength > prevLength) { nextSChildren[--nextLength] = _mount(sNode, next[nextLength]); } } while (nextLength > 0) { nextSChildren[--nextLength] = _update( sNode, prevSChildren[nextLength], next[nextLength], updateFlags, ); } return sNode; }; /** * Updates a Stateful Node with a new Stateless Node. * * @param parentSNode Parent Stateul Node. * @param sNode Stateful Node to update. * @param next New Stateless Node. * @param updateFlags Update flags (ForceUpdate and DisplaceNode). * @returns Stateful Node. */ const _update = ( parentSNode: SNode, sNode: SNode | null, next: VAny, updateFlags: number, ): SNode | null => { if (sNode === null) { return _mount(parentSNode, next); } if (next === false || next == null || next === "") { _unmount(sNode, true); return null; } // polymorphic call-site const children = sNode.c; const prev = sNode.v; const state = sNode.s1; const flags = sNode.f; const type = flags & Flags.TypeMask; sNode.f = type; // Reassign to reduce memory consumption even if next value is strictly // equal to the prev value. sNode.v = next; // Text and Array should be checked before Component, Template and List // because their stateless nodes are represented with basic string and array // types. if (type === Flags.Text) { const ctx = RENDER_CONTEXT; if (typeof next !== "object") { if (prev !== next) { (state as Text).nodeValue = next as string; } if (updateFlags & Flags.DisplaceNode) { elementMoveBefore!.call( ctx.p, (state as Text), ctx.n, ); } ctx.n = state; return sNode; } nodeRemoveChild!.call(ctx.p, (state as Text)); return _mount(parentSNode, next)!; } if (prev === next) { _dirtyCheck(sNode, updateFlags); return sNode; } // Dirty flags should be cleared after dirty checking. sNode.f = type; if (type === Flags.Array) { return _updateArray(parentSNode, sNode, next, updateFlags); } const descriptor = (next as VNode).d; const nextProps = (next as VNode).p; const prevProps = (prev as VNode).p; if ((prev as VNode).d !== descriptor) { _unmount(sNode, true); return _mount(parentSNode, next); } if (type === Flags.Component) { if ( ((flags | updateFlags) & (Flags.Dirty | Flags.ForceUpdate)) || (descriptor.p2 === void 0) || (descriptor.p2(prevProps, nextProps) !== true) ) { sNode.c = _update( sNode, children as SNode, (state as ComponentRenderFn)(nextProps), updateFlags, ); } else if (children !== null) { _dirtyCheck(children as SNode, updateFlags); } } else if (type === Flags.Template) { const ctx = RENDER_CONTEXT; const parentElement = ctx.p; const tplData = (descriptor as TemplateDescriptor).p1; const flags = tplData.f; const data = tplData.d; const propsOpCodes = tplData.p; const childOpCodes = tplData.c; const rootDOMNode = state[0] as Element; if (updateFlags & Flags.DisplaceNode) { updateFlags ^= Flags.DisplaceNode; elementMoveBefore!.call(parentElement, rootDOMNode, ctx.n); } _updateTemplateProperties( rootDOMNode, propsOpCodes, data, state as Node[], prevProps, nextProps, !!(flags & TemplateFlags.Svg), ); if (children !== null) { ctx.p = rootDOMNode; ctx.n = null; let childrenIndex = 0; for (let i = 0; i < childOpCodes.length; i++) { const childOpCode = childOpCodes[i]; const type = childOpCode & ChildOpCode.Type; const value = childOpCode >> ChildOpCode.ValueShift; if (type === ChildOpCode.Child) { (children as (SNode | null)[])[childrenIndex] = _update( sNode, (children as (SNode | null)[])[childrenIndex++], nextProps[value], updateFlags, ); } else if (type === ChildOpCode.SetNext) { ctx.n = state[value]; } else { // ChildOpCode.SetParent ctx.p = state[value] as Element; ctx.n = null; } } ctx.p = parentElement; } ctx.n = rootDOMNode; } else if (type === Flags.List) { _updateList( sNode as SList, prevProps, nextProps, updateFlags, ); } else { // Context if (prevProps.v !== nextProps.v) { updateFlags |= Flags.ForceUpdate; } sNode.c = _update( sNode, children as SNode | null, nextProps.c, updateFlags, ); } return sNode; }; /** * Mounts Stateless Node. * * @param parentSNode Parent Stateful Node. * @param v Stateless Node. * @returns Mounted Stateful Node. */ const _mount = (parentSNode: SNode, v: VAny): SNode | null => { if (v !== false && v != null) { if (typeof v === "object") { if (_isArray(v)) { return _mountList(parentSNode, Flags.Array, v, v); } else { const descriptor = v.d; const props = v.p; const descriptorP1 = descriptor.p1; const type = descriptor.f & (Flags.Template | Flags.Component | Flags.List); if (type === Flags.Template) { const ctx = RENDER_CONTEXT; const parentDOMElement = ctx.p; const nextDOMNode = ctx.n; const tplData = descriptorP1 as TemplateData; const data = tplData.d; const propsOpCodes = tplData.p; const stateOpCodes = tplData.s; const childOpCodes = tplData.c; const flags = tplData.f; const rootDOMNode = (descriptor as TemplateDescriptor).p2(); const state = _Array<Node>(flags & TemplateFlags.Mask6); state[0] = rootDOMNode; if (stateOpCodes.length > 0) { ctx.si = 0; _assignTemplateSlots( nodeGetFirstChild.call(rootDOMNode)!, stateOpCodes, 0, stateOpCodes.length, state, ); } _updateTemplateProperties( rootDOMNode, propsOpCodes, data, state, null, props, !!(flags & TemplateFlags.Svg), ); const sNode = createSNode( Flags.Template, v, null, parentSNode, state, ); if (childOpCodes.length > 0) { const children = _Array<SNode | null>( (flags >> TemplateFlags.ChildrenSizeShift) & TemplateFlags.Mask6 ); sNode.c = children; ctx.p = rootDOMNode; ctx.n = null; let childrenIndex = 0; for (let i = 0; i < childOpCodes.length; i++) { const childOpCode = childOpCodes[i]; const type = childOpCode & ChildOpCode.Type; const value = childOpCode >> ChildOpCode.ValueShift; if (type === ChildOpCode.Child) { children[childrenIndex++] = _mount(sNode, props[value]); } else if (type === ChildOpCode.SetNext) { ctx.n = state[value]; } else { // ChildOpCode.SetParent ctx.p = state[value] as Element; ctx.n = null; } } ctx.p = parentDOMElement; } ctx.n = rootDOMNode; nodeInsertBefore!.call(parentDOMElement, rootDOMNode, nextDOMNode); return sNode; } else if (type === Flags.Component) { const sNode: Component = { f: Flags.Component, v: v as VComponent, c: null, p: parentSNode, s1: null!, s2: null, }; const renderFn = (descriptorP1 as ComponentFactoryFn)(sNode); sNode.c = _mount(sNode, renderFn(props)); sNode.s1 = renderFn; return sNode; } else if (type === Flags.List) { return _mountList(parentSNode, Flags.List, (props as ListProps).v, v); } // Context const sNode = createSNode(Flags.Context, v, null, parentSNode, null); sNode.c = _mount(sNode, (props as ContextProps).c); return sNode; } } else if (v !== "") { // text const ctx = RENDER_CONTEXT; const next = ctx.n; const e = doc.createTextNode(v as string); ctx.n = e; nodeInsertBefore.call(ctx.p, e, next); return createSNode(Flags.Text, v, null, parentSNode, e); } } return null; }; /** * Performs a Dirty Checking in a Stateful Node Subtree. * * @param sNode Stateful Node. * @param updateFlags Update flags (ForceUpdate and DisplaceNode). */ const _dirtyCheck = (sNode: SNode, updateFlags: number): void => { const ctx = RENDER_CONTEXT; // polymorphic call-site const state = sNode.s1; const v = sNode.v; const children = sNode.c; const flags = sNode.f; const type = flags & Flags.TypeMask; sNode.f = type; if (type === Flags.Template) { const rootDOMNode = (state as Node[])[0] as Element; if (updateFlags & Flags.DisplaceNode) { updateFlags ^= Flags.DisplaceNode; elementMoveBefore.call(ctx.p, rootDOMNode, ctx.n); } if (flags & Flags.DirtySubtree) { ctx.p = rootDOMNode; ctx.n = null; const parentDOMElement = ctx.p; const childOpCodes = (v as VTemplate).d.p1.c; let childrenIndex = 0; for (let i = 0; i < childOpCodes.length; i++) { const op = childOpCodes[i]; const type = op & ChildOpCode.Type; const value = op >> ChildOpCode.ValueShift; if (type === ChildOpCode.Child) { const sChild = (children as (SNode1 | null)[])[childrenIndex++]; if (sChild !== null) { _dirtyCheck(sChild, updateFlags); } } else if (type === ChildOpCode.SetNext) { ctx.n = (state as Node[])[value]; } else { // ChildOpCode.SetParent ctx.p = state[value] as Element; ctx.n = null; } } ctx.p = parentDOMElement; } ctx.n = rootDOMNode; } else if (type === Flags.Text) { if (updateFlags & Flags.DisplaceNode) { elementMoveBefore.call(ctx.p, state as Text, ctx.n); } ctx.n = state as Text; } else if (type === Flags.Component) { if ((flags | updateFlags) & (Flags.Dirty | Flags.ForceUpdate)) { sNode.c = _update( sNode, children as SNode, (state as ComponentRenderFn)!((v as VComponent).p), updateFlags, ); } else if (children !== null) { _dirtyCheck(children as SNode, updateFlags); } } else if (type === Flags.Context) { if (children !== null) { _dirtyCheck(children as SNode, updateFlags); } } else { // Array || List let i = (children as Array<SNode | null>).length; while (--i >= 0) { const sChild = (children as Array<SNode | null>)[i]; if (sChild !== null) { _dirtyCheck(sChild, updateFlags); } } } }; /** * Unmounts Stateful Node. * * @param sNode Stateful Node. * @param detach Detach root DOM nodes from the DOM. */ const _unmount = (sNode: SNode, detach: boolean): void => { const flags = sNode.f; // polymorphic call-site const sChildren = sNode.c; if (detach === true && (flags & (Flags.Template | Flags.Text))) { detach = false; nodeRemoveChild.call( RENDER_CONTEXT.p, (flags & Flags.Template) ? (sNode as STemplate).s1[0] : (sNode as SText).s1 ); } if (flags & Flags.Component) { const unmountHooks = (sNode as SComponent).s2; if (unmountHooks !== null) { if (typeof unmountHooks === "function") { unmountHooks(); } else { for (let i = 0; i < unmountHooks.length; i++) { unmountHooks[i](); } } } } if (sChildren !== null) { if (_isArray(sChildren)) { for (let i = 0; i < sChildren.length; i++) { const sChild = sChildren[i]; if (sChild !== null) { _unmount(sChild, detach); } } } else { _unmount(sChildren as SNode, detach); } } }; const enum MagicValues { /** * One of the children nodes were moved. */ RearrangeNodes = 1073741823, // Max SMI Value /** * New node marker. */ NewNodeMark = -1, /** * LIS marker. */ LISMark = -2, } /** * Update children list with track by key algorithm. * * High-level overview of the algorithm that is implemented in this function: * * This algorithm finds a minimum number of DOM operations. It works in * several steps: * * 1. Common prefix and suffix optimization. * * Look for nodes with identical keys by simultaneously iterating through nodes * in the old children list `A` and new children list `B` from both sides. * * A: -> [a b c d] <- * B: -> [a b d] <- * * Skip nodes "a" and "b" at the start, and node "d" at the end. * * A: -> [c] <- * B: -> [] <- * * 2. Zero length optimizations. * * Check if the size of one of the list is equal to zero. When length of the * old children list is zero, insert remaining nodes from the new list. When * length of the new children list is zero, remove remaining nodes from the old * list. * * A: -> [a b c g] <- * B: -> [a g] <- * * Skip nodes "a" and "g" (prefix and suffix optimization). * * A: [b c] * B: [] * * Remove nodes "b" and "c". * * 3. Index and unmount removed nodes. * * A: [b c d e f] * B: [c b h f e] * P: [. . . . .] // . == -1 * * Create array `P` (`sources`) with the length of the new children list and * fills it with `NewNodeMark` values. This mark indicates that node at this * position should be mounted. Later we will assign node positions in the old * children list to this array. * * A: [b c d e f] * B: [c b h f e] * P: [. . . . .] // . == -1 * I: { * c: 0, // B[0] == c * b: 1, // B[1] == b * h: 2, * f: 3, * e: 4, * } * last = 0 * * Create reverse index `I` that maps keys to node positions in the new * children list. * * A: [b c d e f] * ^ * B: [c b h f e] * P: [. 0 . . .] // . == -1 * I: { * c: 0, * b: 1, <- * h: 2, * f: 3, * e: 4, * } * last = 1 * * Assign original positions of the nodes from the old children list to the * array `P`. * * Iterate through nodes in the old children list and gets their new positions * from the index `I`. Assign old node position to the array `P`. When index * `I` doesn't have a key for the old node, it means that it should be * unmounted. * * When we assigning positions to the array `P`, we also store position of the * last seen node in the new children list `pos`, if the last seen position is * greater than the current position of the node at the new list, then we are * switching `rearrangeNodes` flag to `true` (`pos === RearrangeNodes`). * * A: [b c d e f] * ^ * B: [c b h f e] * P: [1 0 . . .] // . == -1 * I: { * c: 0, <- * b: 1, * h: 2, * f: 3, * e: 4, * } * last = 1 // last > 0; rearrangeNodes = true * * The last position `1` is greater than the current position of the node at the * new list `0`, switch `rearrangeNodes` flag to `true`. * * A: [b c d e f] * ^ * B: [c b h f e] * P: [1 0 . . .] // . == -1 * I: { * c: 0, * b: 1, * h: 2, * f: 3, * e: 4, * } * rearrangeNodes = true * * Node with key "d" doesn't exist in the index `I`, unmounts node `d`. * * A: [b c d e f] * ^ * B: [c b h f e] * P: [1 0 . . 3] // . == -1 * I: { * c: 0, * b: 1, * h: 2, * f: 3, * e: 4, <- * } * rearrangeNodes = true * * Assign position `3` for `e` node. * * A: [b c d e f] * ^ * B: [c b h f e] * P: [1 0 . 4 3] // . == -1 * I: { * c: 0, * b: 1, * h: 2, * f: 3, <- * e: 4, * } * rearrangeNodes = true * * Assign position `4` for 'f' node. * * 4. Find minimum number of moves when `rearrangeNodes` flag is on and mount * new nodes. * * A: [b c d e f] * B: [c b h f e] * P: [1 * . 4 *] // . == -1 * == -2 * * When `rearrangeNodes` is on, mark all nodes in the array `P` that belong to * the [longest increasing subsequence](http://en.wikipedia.org/wiki/Longest_increasing_subsequence) * and move all nodes that doesn't belong to this subsequence. * * Iterate over the new children list and the `P` array simultaneously. When * value from `P` array is equal to `NewNodeMark`, mount a new node. When it * isn't equal to `LisMark`, move it to a new position. * * A: [b c d e f] * B: [c b h f e] * ^ // new_pos == 4 * P: [1 * . 4 *] // . == NewNodeMark * == LisMark * ^ * * Node "e" has `LisMark` value in the array `P`, nothing changes. * * A: [b c d e f] * B: [c b h f e] * ^ // new_pos == 3 * P: [1 * . 4 *] // . == NewNodeMark * == LisMark * ^ * * Node "f" has `4` value in the array `P`, move it before the next node "e". * * A: [b c d e f] * B: [c b h f e] * ^ // new_pos == 2 * P: [1 * . 4 *] // . == NewNodeMark * == LisMark * ^ * * Node "h" has `NewNodeMark` value in the array `P`, mount new node "h". * * A: [b c d e f] * B: [c b h f e] * ^ // new_pos == 1 * P: [1 * . 4 *] // . == NewNodeMark * == LisMark * ^ * * Node "b" has `LisMark` value in the array `P`, nothing changes. * * A: [b c d e f] * B: [c b h f e] * ^ // new_pos == 0 * P: [1 * . 4 *] // . == NewNodeMark * == LisMark * * Node "c" has `1` value in the array `P`, move it before the next node "b". * * When `rearrangeNodes` flag is off, skip LIS algorithm and mount nodes that * have `NewNodeMark` value in the array `P`. * * NOTE: There are many variations of this algorithm that are used by many UI * libraries and many implementations are still using an old optimization * technique that were removed several years ago from this implementation. This * optimization were used to improve performance of simple moves/swaps. E.g. * * A: -> [a b c] <- * B: -> [c b a] <- * * Move "a" and "c" nodes to the other edge. * * A: -> [b] <- * B: -> [b] <- * * Skip node "b". * * This optimization were removed because it breaks invariant that insert and * remove operations shouldn't trigger a move operation. E.g. * * A: -> [a b] * B: [c a] <- * * Move node "a" to the end. * * A: [b] * B: [c a] * * Remove node "b" and insert node "c". * * In this use case, this optimization performs one unnecessary operation. * Instead of removing node "b" and inserting node "c", it also moves node "a". * * @param sNode {@link SList} node. * @param a Previous {@link ListProps}. * @param b Next {@link ListProps}. * @param updateFlags Update flags. * @noinline * @__NOINLINE__ */ const _updateList = ( sNode: SList, a: ListProps<any>, b: ListProps<any>, updateFlags: Flags, ): void => { const aKeys = a.k; const bKeys = b.k; const bVNodes = b.v; let bLength = bKeys.length; let aLength = aKeys.length; const result = _Array(bLength); if (bLength === 0) { // New children list is empty. if (aLength > 0) { // Unmount nodes from the old children list. _unmount(sNode, true); } } else if (aLength === 0) { // Old children list is empty. while (bLength > 0) { // Mount nodes from the new children list. result[--bLength] = _mount(sNode, bVNodes[bLength]); } } else { const sChildren = sNode.c as Array<SNode | null>; let aEnd = aLength - 1; let bEnd = bLength - 1; let start = 0; // Step 1 outer: while (true) { // Update nodes with the same key at the end. while (aKeys[aEnd] === bKeys[bEnd]) { result[bEnd] = _update( sNode, sChildren[aEnd--], bVNodes[bEnd], updateFlags, ); if (start > --bEnd || start > aEnd) { break outer; } } // Update nodes with the same key at the beginning. while (aKeys[start] === bKeys[start] && ++start <= aEnd && start <= bEnd) { // delayed update (all updates should be performed from right-to-left). } break; } // Step 2 if (start > aEnd) { // All nodes from `a` are updated, insert the rest from `b`. while (bEnd >= start) { result[bEnd] = _mount(sNode, bVNodes[bEnd--]); } } else if (start > bEnd) { // All nodes from `b` are updated, remove the rest from `a`. bLength = start; do { const sChild = sChildren[bLength++]; if (sChild !== null) { _unmount(sChild, true); } } while (bLength <= aEnd); } else { // Step 3 let bLength = bEnd - start + 1; const sources = new _Int32Array(bLength); // Maps positions in the new children list to positions in the old list. const keyIndex = new _Map<any, number>(); // Maps keys to their positions in the new children list. for (let i = 0; i < bLength; i++) { // `NewNodeMark` value indicates that node doesn't exist in the old children list. sources[i] = MagicValues.NewNodeMark; const j = start + i; keyIndex.set(bKeys[j], j); } // When `nodePosition === RearrangeNodes`, it means that one of the nodes is in the wrong position and we should // rearrange nodes with LIS-based algorithm `markLIS()`. let nodePosition = 0; for (let i = start; i <= aEnd; i++) { const sChild = sChildren[i]; const nextPosition = keyIndex.get(aKeys[i]); if (nextPosition !== void 0) { nodePosition = (nodePosition < nextPosition) ? nextPosition : MagicValues.RearrangeNodes; sources[nextPosition - start] = i; result[nextPosition] = sChild; } else if (sChild !== null) { _unmount(sChild, true); } } // Step 4 // Mark LIS nodes only when this node weren't moved `moveNode === false` and we've detected that one of the // children nodes were moved `pos === MagicValues.MovedChildren`. if (!(updateFlags & Flags.DisplaceNode) && nodePosition === MagicValues.RearrangeNodes) { markLIS(sources); } while (bLength-- > 0) { bEnd = bLength + start; const node = bVNodes[bEnd]; const lisValue = sources[bLength]; result[bEnd] = (lisValue === -1) ? _mount(sNode, node) : _update( sNode, result[bEnd], node, updateFlags | ((nodePosition === MagicValues.RearrangeNodes && lisValue !== MagicValues.LISMark) ? Flags.DisplaceNode : 0), ); } } // Delayed update for nodes from Step 1 (prefix only). Reconciliation algorithm always updates nodes from right to // left. while (start > 0) { result[--start] = _update( sNode, sChildren[start], bVNodes[start], updateFlags, ); } } sNode.c = result; }; /** * Modified Longest Increased Subsequence algorithm. * * Mutates input array `a` and replaces all values that are part of LIS with -2 value. * * Constraints: * - Doesn't work with negative numbers. -1 values are ignored. * - Input array `a` should contain at least one value that is greater than -1. * * {@link http://en.wikipedia.org/wiki/Longest_increasing_subsequence} * * @example * * const A = Int32Array.from([-1, 0, 2, 1]); * markLIS(A); * // A => [-1, -2, 2, -2] * * @param a Array of numbers. * @noinline * @__NOINLINE__ */ const markLIS = (a: Int32Array): void => { const length = a.length; const parent = new _Int32Array(length); const index = new _Int32Array(length); let indexLength = 0; let i = 0; let j: number; let k: number; let lo: number; let hi: number; // Skip -1 values at the start of the input array `a`. for (; a[i] === MagicValues.NewNodeMark; i++) { /**/ } index[0] = i++; for (; i < length; i++) { k = a[i]; if (k !== MagicValues.NewNodeMark) { // Ignore -1 values. j = index[indexLength]; if (a[j] < k) { parent[i] = j; index[++indexLength] = i; } else { lo = 0; hi = indexLength; while (lo < hi) { j = (lo + hi) >> 1; if (a[index[j]] < k) { lo = j + 1; } else { hi = j; } } if (k < a[index[lo]]) { if (lo > 0) { parent[i] = index[lo - 1]; } index[lo] = i; } } } }; // Mutate input array `a` and assign -2 value to all nodes that are part of LIS. j = index[indexLength]; while (indexLength-- >= 0) { a[j] = MagicValues.LISMark; j = parent[j]; } }; /** * Creates a HTML Template cloning factory. * * @__NO_SIDE_EFFECTS__ */ export const _hN = (t: string | Node): () => Element => ( () => { if (typeof t === "string") { HTM_TEMPLATE.innerHTML = t; t = HTM_TEMPLATE_CONTENT.firstChild!; } return nodeCloneNode.call(t, true) as Element; } ); /** * Creates a HTML Element factory. * * @__NO_SIDE_EFFECTS__ */ export const _hE = (t: string): () => Element => ( () => doc.createElement(t) ); /** * Creates a SVG Template cloning factory. */ export const _sN = (t: string | Node): () => Element => ( () => { if (typeof t === "string") { SVG_TEMPLATE.innerHTML = t; t = SVG_TEMPLATE_CONTENT.firstChild!; } return nodeCloneNode.call(t, true) as Element; } ); /** * Creates a SVG Element factory. * * @__NO_SIDE_EFFECTS__ */ export const _sE = (t: string): () => Element => ( () => doc.createElementNS("http://www.w3.org/2000/svg", t) ); /** * Creates a template descriptor with globally shared data. * * @__NO_SIDE_EFFECTS__ */ export const _T = ( p2: () => Element, f: number, p: PropOpCode[], c: ChildOpCode[], s: StateOpCode[], d = __IVI_STRINGS__, ): TemplateDescriptor => ({ f: Flags.Template, p1: { f, p, c, s, d }, p2, }); /** * @__NO_SIDE_EFFECTS__ */ export const _t = (d: TemplateDescriptor, p: any[]): VTemplate => ({ d, p }); export type ComponentFactory = { ( factory: (c: Component) => () => VAny, areEqual?: (a?: any, b?: any) => boolean ): () => VComponent<undefined>; <P>( factory: (c: Component<P>) => (props: P) => VAny, areEqual?: (prev: P, next: P) => boolean ): (props: P) => VComponent<P>; }; /** * Creates a factory that produces component nodes. * * @typeparam P Property type. * @param factory Function that produces stateful render functions. * @param areEqyal Function that checks `props` for equality. * @returns Factory that produces component nodes. * @__NO_SIDE_EFFECTS__ */ export const component: ComponentFactory = <P>( p1: (c: Component) => (props?: P) => VAny, p2?: (prev: P, next: P) => boolean, ): (p?: any) => VComponent => { const d: ComponentDescriptor = { f: Flags.Component, p1, p2 }; return (p: P) => ({ d, p }); }; /** * Gets current component props. * * @typeparam P Property type. * @param component Component node. * @returns Current component props. */ export const getProps = <P>(component: Component<P>): P => ( component.v.p ); /** * Adds an unmount hook. * * @example * * const Example = component((c) => { * useUnmount(c, () => { console.log("unmounted"); }); * * return () => null; * }); * * @param component Component instance. * @param hook Unmount hook. */ export const useUnmount = (component: Component, hook: () => void): void => { const hooks = component.s2; component.s2 = (hooks === null) ? hook : (typeof hooks === "function") ? [hooks, hook] : (hooks.push(hook), hooks); }; export type Effect = { ( component: Component, effect: () => (() => void) | void, areEqual?: (prev?: any, next?: any) => boolean ): () => void; <P>( component: Component, effect: (props: P) => (() => void) | void, areEqual?: (prev: P, next: P) => boolean ): (props: P) => void; }; /** * Creates a side effect hook. * * @example * * const Example = component((c) => { * const [count, setCount] = useState(c, 0); * const timer = useEffect(c, ({ interval }) => { * const tid = setInterval(() => { setCount(count() + 1); }, interval); * return () => { clearInterval(tid); }; * }, shallowEq); * * return (interval) => ( * timer({ interval }), * * html`<span>${count()}</span>` * ); * }); * * @typeparam T Hook props type. * @param component Component instance. * @param hook Side effect function. * @param areEqual Function that checks if input value hasn't changed. * @returns Side effect hook. */ export const useEffect: Effect = <P>( component: Component, hook: (props?: P) => (() => void) | void, areEqual?: (prev: P, next: P) => boolean, ): (props?: P) => void => { // var usage is intentional, see `docs/internals/perf.md` for an explanation. var reset: (() => void) | void; var prev: P | undefined; var pending: boolean | undefined; return (next?: P) => { if ( pending !== true && ( areEqual === void 0 || prev === void 0 || areEqual(prev as P, next as P) === false ) ) { if (pending === void 0) { useUnmount(component, () => { pending = false; if (reset !== void 0) { reset(); } }); } pending = true; RENDER_CONTEXT.e.push(() => { if (pending === true) { pending = false; if (reset !== void 0) { reset(); } reset = hook(next!); } }); } prev = next; }; }; let _animationFrameEffects: (() => void)[] = []; let _idleEffects: (() => void)[] = []; const _flushAnimationFrameEffects = () => { while (_animationFrameEffects.length > 0) { const e = _animationFrameEffects; _animationFrameEffects = []; for (let i = 0; i < e.length; i++) { e[i](); } } }; const _flushIdleEffects = () => { while (_idleEffects.length > 0) { const e = _idleEffects; _idleEffects = []; for (let i = 0; i < e.length; i++) { e[i](); } } }; /* @__NO_SIDE_EFFECTS__ */ export const createEffectHandler = (scheduleFlushTask: () => Array<() => void>) => <P>( component: Component, hook: (props?: P) => (() => void) | void, areEqual?: (prev: P, next: P) => boolean, ): (props?: P) => void => { // var usage is intentional, see `docs/internals/perf.md` for an explanation. var reset: (() => void) | void; var prev: P | undefined; var pending: boolean | undefined; return (next?: P) => { if ( pending !== true && ( areEqual === void 0 || prev === void 0 || areEqual(prev as P, next as P) === false ) ) { if (pending === void 0) { useUnmount(component, () => { pending = false; if (reset !== void 0) { reset(); } }); } pending = true; scheduleFlushTask().push(() => { if (pending === true) { pending = false; if (reset !== void 0) { reset(); } reset = hook(next!); } }); } prev = next; }; }; const _scheduleAnimationFrameEffects = () => { const queue = _animationFrameEffects; if (queue.length === 0) { _requestAnimationFrame(_flushAnimationFrameEffects); } return queue; }; export const useAnimationFrameEffect = createEffectHandler(_scheduleAnimationFrameEffects); const _scheduleIdleEffects = () => { const queue = _idleEffects; if (queue.length === 0) { _requestIdleCallback(_flushIdleEffects); } return queue; }; export const useIdleEffect = createEffectHandler(_scheduleIdleEffects); /** * Invalidates a component. * * @param c Component instance. */ export const invalidate = (c: Component): void => { if (!(c.f & Flags.Dirty)) { c.f |= Flags.Dirty; let prev: SNode = c; let parent = c.p; while (parent !== null) { // Polymorphic call-sites if (parent.f & Flags.DirtySubtree) { return; } prev = parent; parent.f |= Flags.DirtySubtree; parent = parent.p; } (prev as SRoot).v.d.p1(prev as SRoot, prev.s1); } }; /** * VDescriptor for List nodes. */ export const LIST_DESCRIPTOR: ListDescriptor = { f: Flags.List, p1: null, p2: null, }; /** * Creates a dynamic list. * * @typeparam E Entry type. * @typeparam K Key type. * @param entries Entries. * @param getKey Get key from entry function. * @param render Render entry function. * @returns Dynamic list. * @__NO_SIDE_EFFECTS__ */ export const List = <E, K>( entries: E[], getKey: (e