UNPKG

ivi

Version:

Lightweight Embeddable Web UI Library.

1,445 lines 49.6 kB
export const EMPTY_ARRAY = []; /** * Globally shared strings are automatically generated by template compiler. */ const __IVI_STRINGS__ = ["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 = _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; // 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 = nodeProto.insertBefore; /** `Node.prototype.removeChild`. */ const nodeRemoveChild = nodeProto.removeChild; /** `Node.prototype.cloneNode`. */ const nodeCloneNode = nodeProto.cloneNode; /** `Element.prototype.moveBefore` */ const elementMoveBefore = elementProto.moveBefore ?? nodeInsertBefore; /** `Element.prototype.setAttribute` */ const elementSetAttribute = elementProto.setAttribute; /** `Element.prototype.removeAttribute` */ const elementRemoveAttribute = elementProto.removeAttribute; /** `EventTarget.prototype.addEventListener` */ const elementAddEventListener = elementProto.addEventListener; /** `EventTarget.prototype.removeEventListener` */ const elementRemoveEventListener = elementProto.removeEventListener; /** `Object.getOwnPropertyDescriptor(o, p)` */ const getDescriptor = (o, p) => _Object.getOwnPropertyDescriptor(o, p); /** `get Node.prototype.firstChild` */ const nodeGetFirstChild = /*@__PURE__*/ getDescriptor(nodeProto, "firstChild").get; /** `get Node.prototype.nextSibling` */ const nodeGetNextSibling = /*@__PURE__*/ getDescriptor(nodeProto, "nextSibling").get; /** `set Node.prototype.textContent` */ const nodeSetTextContent = /*@__PURE__*/ getDescriptor(nodeProto, "textContent").set; /** `set Element.prototype.innerHTML` */ const elementSetInnerHTML = /*@__PURE__*/ getDescriptor(elementProto, "innerHTML").set; /** `set Element.prototype.className` */ const elementSetClassName = /*@__PURE__*/ getDescriptor(elementProto, "className").set; /** `get HTMLElement.prototype.style`. */ const htmlElementGetStyle = /*@__PURE__*/ getDescriptor(HTMLElement.prototype, "style").get; /** `get SVGElement.prototype.style` */ const svgElementGetStyle = /*@__PURE__*/ getDescriptor(SVGElement.prototype, "style").get; // 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 = _Object.seal({ p: null, n: null, si: 0, e: [], }); /** * Creates a Stateful Node instance. * * @param v VNode. * @returns {@link SNode} instance. */ export const createSNode = (f, v, c, p, s1) => ({ f, v, c, p, s1 }); 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, opCodes, data, state, prevProps, nextProps, svg) => { let style; for (let i = 0; i < opCodes.length; i++) { const op = opCodes[i]; const type = op & 7 /* PropOpCode.TypeMask */; const dataIndex = op >> 9 /* PropOpCode.DataShift */; if (type === 0 /* PropOpCode.SetNode */) { currentElement = state[dataIndex]; style = void 0; } else { const propsIndex = (op >> 3 /* PropOpCode.InputShift */) & 63 /* PropOpCode.Mask6 */; const next = nextProps[propsIndex]; if (type === 4 /* PropOpCode.DiffDOMProperty */) { const key = data[dataIndex]; if (prevProps === null) { if (next !== void 0) { currentElement[key] = next; } } else if (currentElement[key] !== next) { currentElement[key] = next; } } else { let prev; if (prevProps !== null) { prev = prevProps[propsIndex]; } if (prev !== next) { if (type === 1 /* PropOpCode.Common */) { if (dataIndex === 0 /* 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 === 1 /* 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 === 7 /* PropOpCode.Directive */) { next(currentElement); } else { const key = data[dataIndex]; if (type === 2 /* PropOpCode.Attribute */) { if (next !== false && next != null) { elementSetAttribute.call(currentElement, key, next); } else if (prev !== false && prev != null) { elementRemoveAttribute.call(currentElement, key); } } else if (type === 3 /* PropOpCode.Property */) { currentElement[key] = next; } else if (type === 5 /* PropOpCode.Style */) { if (next !== false && next != null) { if (style === void 0) { style = (svg === false) ? htmlElementGetStyle.call(currentElement) : svgElementGetStyle.call(currentElement); } style.setProperty(key, next); } else if (prev !== false && prev != null) { if (style === void 0) { style = (svg === false) ? htmlElementGetStyle.call(currentElement) : svgElementGetStyle.call(currentElement); } 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, opCodes, offset, endOffset, state) => { const ctx = RENDER_CONTEXT; while (true) { const op = opCodes[offset++]; if (op & 1 /* StateOpCode.Save */) { state[++ctx.si] = currentNode; } if (op & 2 /* StateOpCode.EnterOrRemove */) { const enterOffset = op >> 2 /* 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; state[++ctx.si] = currentNode = nodeGetNextSibling.call(currentNode); commentNode.remove(); } } if (offset === endOffset) { return; } currentNode = nodeGetNextSibling.call(currentNode); } }; const _mountList = (parentState, flags, children, vNode) => { 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, sNode, next, updateFlags) => { if (!_isArray(next)) { _unmount(sNode, true); return _mount(parentSNode, next); } const prevSChildren = sNode.c; 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, next, updateFlags) => { 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 & 127 /* 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 === 16 /* Flags.Text */) { const ctx = RENDER_CONTEXT; if (typeof next !== "object") { if (prev !== next) { state.nodeValue = next; } if (updateFlags & 1024 /* Flags.DisplaceNode */) { elementMoveBefore.call(ctx.p, state, ctx.n); } ctx.n = state; return sNode; } nodeRemoveChild.call(ctx.p, state); 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 === 8 /* Flags.Array */) { return _updateArray(parentSNode, sNode, next, updateFlags); } const descriptor = next.d; const nextProps = next.p; const prevProps = prev.p; if (prev.d !== descriptor) { _unmount(sNode, true); return _mount(parentSNode, next); } if (type === 2 /* Flags.Component */) { if (((flags | updateFlags) & (128 /* Flags.Dirty */ | 512 /* Flags.ForceUpdate */)) || (descriptor.p2 === void 0) || (descriptor.p2(prevProps, nextProps) !== true)) { sNode.c = _update(sNode, children, state(nextProps), updateFlags); } else if (children !== null) { _dirtyCheck(children, updateFlags); } } else if (type === 1 /* Flags.Template */) { const ctx = RENDER_CONTEXT; const parentElement = ctx.p; const tplData = descriptor.p1; const flags = tplData.f; const data = tplData.d; const propsOpCodes = tplData.p; const childOpCodes = tplData.c; const rootDOMNode = state[0]; if (updateFlags & 1024 /* Flags.DisplaceNode */) { updateFlags ^= 1024 /* Flags.DisplaceNode */; elementMoveBefore.call(parentElement, rootDOMNode, ctx.n); } _updateTemplateProperties(rootDOMNode, propsOpCodes, data, state, prevProps, nextProps, !!(flags & 4096 /* 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 & 3 /* ChildOpCode.Type */; const value = childOpCode >> 2 /* ChildOpCode.ValueShift */; if (type === 0 /* ChildOpCode.Child */) { children[childrenIndex] = _update(sNode, children[childrenIndex++], nextProps[value], updateFlags); } else if (type === 1 /* ChildOpCode.SetNext */) { ctx.n = state[value]; } else { // ChildOpCode.SetParent ctx.p = state[value]; ctx.n = null; } } ctx.p = parentElement; } ctx.n = rootDOMNode; } else if (type === 4 /* Flags.List */) { _updateList(sNode, prevProps, nextProps, updateFlags); } else { // Context if (prevProps.v !== nextProps.v) { updateFlags |= 512 /* Flags.ForceUpdate */; } sNode.c = _update(sNode, children, nextProps.c, updateFlags); } return sNode; }; /** * Mounts Stateless Node. * * @param parentSNode Parent Stateful Node. * @param v Stateless Node. * @returns Mounted Stateful Node. */ const _mount = (parentSNode, v) => { if (v !== false && v != null) { if (typeof v === "object") { if (_isArray(v)) { return _mountList(parentSNode, 8 /* Flags.Array */, v, v); } else { const descriptor = v.d; const props = v.p; const descriptorP1 = descriptor.p1; const type = descriptor.f & (1 /* Flags.Template */ | 2 /* Flags.Component */ | 4 /* Flags.List */); if (type === 1 /* Flags.Template */) { const ctx = RENDER_CONTEXT; const parentDOMElement = ctx.p; const nextDOMNode = ctx.n; const tplData = descriptorP1; const data = tplData.d; const propsOpCodes = tplData.p; const stateOpCodes = tplData.s; const childOpCodes = tplData.c; const flags = tplData.f; const rootDOMNode = descriptor.p2(); const state = _Array(flags & 63 /* 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 & 4096 /* TemplateFlags.Svg */)); const sNode = createSNode(1 /* Flags.Template */, v, null, parentSNode, state); if (childOpCodes.length > 0) { const children = _Array((flags >> 6 /* TemplateFlags.ChildrenSizeShift */) & 63 /* 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 & 3 /* ChildOpCode.Type */; const value = childOpCode >> 2 /* ChildOpCode.ValueShift */; if (type === 0 /* ChildOpCode.Child */) { children[childrenIndex++] = _mount(sNode, props[value]); } else if (type === 1 /* ChildOpCode.SetNext */) { ctx.n = state[value]; } else { // ChildOpCode.SetParent ctx.p = state[value]; ctx.n = null; } } ctx.p = parentDOMElement; } ctx.n = rootDOMNode; nodeInsertBefore.call(parentDOMElement, rootDOMNode, nextDOMNode); return sNode; } else if (type === 2 /* Flags.Component */) { const sNode = { f: 2 /* Flags.Component */, v: v, c: null, p: parentSNode, s1: null, s2: null, }; const renderFn = descriptorP1(sNode); sNode.c = _mount(sNode, renderFn(props)); sNode.s1 = renderFn; return sNode; } else if (type === 4 /* Flags.List */) { return _mountList(parentSNode, 4 /* Flags.List */, props.v, v); } // Context const sNode = createSNode(64 /* Flags.Context */, v, null, parentSNode, null); sNode.c = _mount(sNode, props.c); return sNode; } } else if (v !== "") { // text const ctx = RENDER_CONTEXT; const next = ctx.n; const e = doc.createTextNode(v); ctx.n = e; nodeInsertBefore.call(ctx.p, e, next); return createSNode(16 /* 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, updateFlags) => { 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 & 127 /* Flags.TypeMask */; sNode.f = type; if (type === 1 /* Flags.Template */) { const rootDOMNode = state[0]; if (updateFlags & 1024 /* Flags.DisplaceNode */) { updateFlags ^= 1024 /* Flags.DisplaceNode */; elementMoveBefore.call(ctx.p, rootDOMNode, ctx.n); } if (flags & 256 /* Flags.DirtySubtree */) { ctx.p = rootDOMNode; ctx.n = null; const parentDOMElement = ctx.p; const childOpCodes = v.d.p1.c; let childrenIndex = 0; for (let i = 0; i < childOpCodes.length; i++) { const op = childOpCodes[i]; const type = op & 3 /* ChildOpCode.Type */; const value = op >> 2 /* ChildOpCode.ValueShift */; if (type === 0 /* ChildOpCode.Child */) { const sChild = children[childrenIndex++]; if (sChild !== null) { _dirtyCheck(sChild, updateFlags); } } else if (type === 1 /* ChildOpCode.SetNext */) { ctx.n = state[value]; } else { // ChildOpCode.SetParent ctx.p = state[value]; ctx.n = null; } } ctx.p = parentDOMElement; } ctx.n = rootDOMNode; } else if (type === 16 /* Flags.Text */) { if (updateFlags & 1024 /* Flags.DisplaceNode */) { elementMoveBefore.call(ctx.p, state, ctx.n); } ctx.n = state; } else if (type === 2 /* Flags.Component */) { if ((flags | updateFlags) & (128 /* Flags.Dirty */ | 512 /* Flags.ForceUpdate */)) { sNode.c = _update(sNode, children, state(v.p), updateFlags); } else if (children !== null) { _dirtyCheck(children, updateFlags); } } else if (type === 64 /* Flags.Context */) { if (children !== null) { _dirtyCheck(children, updateFlags); } } else { // Array || List let i = children.length; while (--i >= 0) { const sChild = children[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, detach) => { const flags = sNode.f; // polymorphic call-site const sChildren = sNode.c; if (detach === true && (flags & (1 /* Flags.Template */ | 16 /* Flags.Text */))) { detach = false; nodeRemoveChild.call(RENDER_CONTEXT.p, (flags & 1 /* Flags.Template */) ? sNode.s1[0] : sNode.s1); } if (flags & 2 /* Flags.Component */) { const unmountHooks = sNode.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, detach); } } }; /** * 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, a, b, updateFlags) => { 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; 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(); // 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] = -1 /* 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 : 1073741823 /* 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 & 1024 /* Flags.DisplaceNode */) && nodePosition === 1073741823 /* 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 === 1073741823 /* MagicValues.RearrangeNodes */ && lisValue !== -2 /* MagicValues.LISMark */) ? 1024 /* 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) => { const length = a.length; const parent = new _Int32Array(length); const index = new _Int32Array(length); let indexLength = 0; let i = 0; let j; let k; let lo; let hi; // Skip -1 values at the start of the input array `a`. for (; a[i] === -1 /* MagicValues.NewNodeMark */; i++) { /**/ } index[0] = i++; for (; i < length; i++) { k = a[i]; if (k !== -1 /* 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] = -2 /* MagicValues.LISMark */; j = parent[j]; } }; /** * Creates a HTML Template cloning factory. * * @__NO_SIDE_EFFECTS__ */ export const _hN = (t) => (() => { if (typeof t === "string") { HTM_TEMPLATE.innerHTML = t; t = HTM_TEMPLATE_CONTENT.firstChild; } return nodeCloneNode.call(t, true); }); /** * Creates a HTML Element factory. * * @__NO_SIDE_EFFECTS__ */ export const _hE = (t) => (() => doc.createElement(t)); /** * Creates a SVG Template cloning factory. */ export const _sN = (t) => (() => { if (typeof t === "string") { SVG_TEMPLATE.innerHTML = t; t = SVG_TEMPLATE_CONTENT.firstChild; } return nodeCloneNode.call(t, true); }); /** * Creates a SVG Element factory. * * @__NO_SIDE_EFFECTS__ */ export const _sE = (t) => (() => doc.createElementNS("http://www.w3.org/2000/svg", t)); /** * Creates a template descriptor with globally shared data. * * @__NO_SIDE_EFFECTS__ */ export const _T = (p2, f, p, c, s, d = __IVI_STRINGS__) => ({ f: 1 /* Flags.Template */, p1: { f, p, c, s, d }, p2, }); /** * @__NO_SIDE_EFFECTS__ */ export const _t = (d, p) => ({ d, 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 = (p1, p2) => { const d = { f: 2 /* Flags.Component */, p1, p2 }; return (p) => ({ d, p }); }; /** * Gets current component props. * * @typeparam P Property type. * @param component Component node. * @returns Current component props. */ export const getProps = (component) => (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, hook) => { const hooks = component.s2; component.s2 = (hooks === null) ? hook : (typeof hooks === "function") ? [hooks, hook] : (hooks.push(hook), hooks); }; /** * 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 = (component, hook, areEqual) => { // var usage is intentional, see `docs/internals/perf.md` for an explanation. var reset; var prev; var pending; return (next) => { if (pending !== true && (areEqual === void 0 || prev === void 0 || areEqual(prev, next) === 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 = []; let _idleEffects = []; 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) => (component, hook, areEqual) => { // var usage is intentional, see `docs/internals/perf.md` for an explanation. var reset; var prev; var pending; return (next) => { if (pending !== true && (areEqual === void 0 || prev === void 0 || areEqual(prev, next) === 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) => { if (!(c.f & 128 /* Flags.Dirty */)) { c.f |= 128 /* Flags.Dirty */; let prev = c; let parent = c.p; while (parent !== null) { // Polymorphic call-sites if (parent.f & 256 /* Flags.DirtySubtree */) { return; } prev = parent; parent.f |= 256 /* Flags.DirtySubtree */; parent = parent.p; } prev.v.d.p1(prev, prev.s1); } }; /** * VDescriptor for List nodes. */ export const LIST_DESCRIPTOR = { f: 4 /* 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 = (entries, getKey, render) => ({ d: LIST_DESCRIPTOR, p: { k: entries.map(getKey), v: entries.map(render), }, }); /** * Performs dirty checking in a root subtree. * * When `forceUpdate` option is enabled, all components in a root subtree will * be updated. * * @param root Root Node. * @param forceUpdate Force update components. */ export const dirtyCheck = (root, forceUpdate) => { _dirtyCheckRoot(root, forceUpdate === true ? 512 /* Flags.ForceUpdate */ : 0); }; /** * Performs a Dirty Checking in a root subtree. * * @param root Stateful Root Node. * @param updateFlags Update flags (ForceUpdate and DisplaceNode). */ const _dirtyCheckRoot = (root, updateFlags) => { while ((updateFlags | root.f) & (256 /* Flags.DirtySubtree */ | 512 /* Flags.ForceUpdate */)) { const ctx = RENDER_CONTEXT; const { p, n } = ctx; root.f = 32 /* Flags.Root */; if (root.c !== null) { const domSlot = root.v.p; RENDER_CONTEXT.p = domSlot.p; RENDER_CONTEXT.n = domSlot.n; _dirtyCheck(root.c, updateFlags); updateFlags = 0; _flushDOMEffects(); } ctx.p = p; ctx.n = n; } }; /** * Updates a root subtree. * * @param root Stateful Root Node. * @param next New Stateless Node. * @param updateFlags Update flags (ForceUpdate and DisplaceNode). */ const _updateRoot = (root, next, updateFlags) => { const ctx = RENDER_CONTEXT; const { p, n } = ctx; const domSlot = root.v.p; ctx.p = domSlot.p; ctx.n = domSlot.n; root.f = 32 /* Flags.Root */; root.c = _update(root, root.c, next, updateFlags); _flushDOMEffects(); ctx.p = p; ctx.n = n; _dirtyCheckRoot(root, 0); }; /** * Unmounts a root subtree. * * When `detach` option is enabled, root DOM nodes will be detached from the * DOM. * * @param root Root Node. * @param detach Detach root DOM nodes from the DOM. */ export const unmount = (root, detach) => { if (root.c !== null) { const ctx = RENDER_CONTEXT; const { p, n } = ctx; ctx.p = root.v.p.p; root.f = 32 /* Flags.Root */; _unmount(root.c, detach); ctx.p = p; ctx.n = n; } }; /** * Defines a root node with a custom invalidation hook. * * @param onInvalidate Invalidated Hook. * @returns Root Node factory. * @__NO_SIDE_EFFECTS__ */ export const defineRoot = (p1) => { var d = { f: 32 /* Flags.Root */, p1, p2: null }; return (p, n = null, s) => createSNode(32 /* Flags.Root */, // VNode object. { // Root Descriptor. d, // VNode props object contains the location in the DOM tree where subtree // should be rendered. p: { // Parent DOM Element. p, // Next DOM Node. n, }, }, // Children. null, // Parent SNode. null, // Root state. s); }; /** * Creates a root node that uses microtask queue for scheduling updates. * * @param parentElement Parent DOM Element. * @param nextNode Next DOM Node. * @returns Root Node. */ export const createRoot = /*@__PURE__*/ defineRoot( // OnRootInvalidated hook (root) => { // Schedules a microtask for dirty checking. _queueMicrotask(() => { _dirtyCheckRoot(root, 0); }); }); /** * Updates a root subtree. * * When `forceUpdate` option is enabled, all components in a root subtree will * be updated. * * @param root Root Node. * @param v Stateless View Node. * @param forceUpdate Force update components. */ export const update = (root, v, forceUpdate) => { _updateRoot(root, v, forceUpdate === true ? 512 /* Flags.ForceUpdate */ : 0); }; /** * Context. * * @returns Context getter and context provider. * @__NO_SIDE_EFFECTS__ */ export const context = () => { const d = { f: 64 /* Flags.Context */, p1: null, p2: null }; return [ (c) => _getContextValue(c, d), (v, c) => ({ d, p: { v, c } }), ]; }; const _getContextValue = (c, d) => { let node = c.p; while (node !== null) { if (node.f & 64 /* Flags.Context */ && node.v.d === d) { return node.v.p.v; } node = node.p; } }; //# sourceMappingURL=core.js.map