ivi
Version:
Lightweight Embeddable Web UI Library.
1,445 lines • 49.6 kB
JavaScript
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