incremental-dom
Version:
An in-place virtual DOM library
1,325 lines • 63.2 kB
JavaScript
/**
* @preserve
* Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.IncrementalDOM = {}));
}(this, function (exports) {
'use strict';
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* The name of the HTML attribute that holds the element key
* (e.g. `<div key="foo">`). The attribute value, if it exists, is then used
* as the default key when importing an element.
* If null, no attribute value is used as the default key.
*/
var keyAttributeName = "key";
function getKeyAttributeName() {
return keyAttributeName;
}
function setKeyAttributeName(name) {
keyAttributeName = name;
}
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Keeps track whether or not we are in an attributes declaration (after
* elementOpenStart, but before elementOpenEnd).
*/
var inAttributes = false;
/**
* Keeps track whether or not we are in an element that should not have its
* children cleared.
*/
var inSkip = false;
/**
* Keeps track of whether or not we are in a patch.
*/
var inPatch = false;
/**
* Asserts that a value exists and is not null or undefined. goog.asserts
* is not used in order to avoid dependencies on external code.
* @param val The value to assert is truthy.
* @returns The value.
*/
function assert(val) {
if (!val) {
throw new Error("Expected value to be defined");
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return val;
}
/**
* Makes sure that there is a current patch context.
* @param functionName The name of the caller, for the error message.
*/
function assertInPatch(functionName) {
if (!inPatch) {
throw new Error("Cannot call " + functionName + "() unless in patch.");
}
}
/**
* Makes sure that a patch closes every node that it opened.
* @param openElement
* @param root
*/
function assertNoUnclosedTags(openElement, root) {
if (openElement === root) {
return;
}
var currentElement = openElement;
var openTags = [];
while (currentElement && currentElement !== root) {
openTags.push(currentElement.nodeName.toLowerCase());
currentElement = currentElement.parentNode;
}
throw new Error("One or more tags were not closed:\n" + openTags.join("\n"));
}
/**
* Makes sure that node being outer patched has a parent node.
* @param parent
*/
function assertPatchOuterHasParentNode(parent) {
if (!parent) {
console.warn("patchOuter requires the node have a parent if there is a key.");
}
}
/**
* Makes sure that the caller is not where attributes are expected.
* @param functionName The name of the caller, for the error message.
*/
function assertNotInAttributes(functionName) {
if (inAttributes) {
throw new Error(functionName +
"() can not be called between " +
"elementOpenStart() and elementOpenEnd().");
}
}
/**
* Makes sure that the caller is not inside an element that has declared skip.
* @param functionName The name of the caller, for the error message.
*/
function assertNotInSkip(functionName) {
if (inSkip) {
throw new Error(functionName +
"() may not be called inside an element " +
"that has called skip().");
}
}
/**
* Makes sure that the caller is where attributes are expected.
* @param functionName The name of the caller, for the error message.
*/
function assertInAttributes(functionName) {
if (!inAttributes) {
throw new Error(functionName +
"() can only be called after calling " +
"elementOpenStart().");
}
}
/**
* Makes sure the patch closes virtual attributes call
*/
function assertVirtualAttributesClosed() {
if (inAttributes) {
throw new Error("elementOpenEnd() must be called after calling " + "elementOpenStart().");
}
}
/**
* Makes sure that tags are correctly nested.
* @param currentNameOrCtor
* @param nameOrCtor
*/
function assertCloseMatchesOpenTag(currentNameOrCtor, nameOrCtor) {
if (currentNameOrCtor !== nameOrCtor) {
throw new Error('Received a call to close "' +
nameOrCtor +
'" but "' +
currentNameOrCtor +
'" was open.');
}
}
/**
* Makes sure that no children elements have been declared yet in the current
* element.
* @param functionName The name of the caller, for the error message.
* @param previousNode
*/
function assertNoChildrenDeclaredYet(functionName, previousNode) {
if (previousNode !== null) {
throw new Error(functionName +
"() must come before any child " +
"declarations inside the current element.");
}
}
/**
* Checks that a call to patchOuter actually patched the element.
* @param maybeStartNode The value for the currentNode when the patch
* started.
* @param maybeCurrentNode The currentNode when the patch finished.
* @param expectedNextNode The Node that is expected to follow the
* currentNode after the patch;
* @param expectedPrevNode The Node that is expected to preceed the
* currentNode after the patch.
*/
function assertPatchElementNoExtras(maybeStartNode, maybeCurrentNode, expectedNextNode, expectedPrevNode) {
var startNode = assert(maybeStartNode);
var currentNode = assert(maybeCurrentNode);
var wasUpdated = currentNode.nextSibling === expectedNextNode &&
currentNode.previousSibling === expectedPrevNode;
var wasChanged = currentNode.nextSibling === startNode.nextSibling &&
currentNode.previousSibling === expectedPrevNode;
var wasRemoved = currentNode === startNode;
if (!wasUpdated && !wasChanged && !wasRemoved) {
throw new Error("There must be exactly one top level call corresponding " +
"to the patched element.");
}
}
/**
* @param newContext The current patch context.
*/
function updatePatchContext(newContext) {
inPatch = newContext != null;
}
/**
* Updates the state of being in an attribute declaration.
* @param value Whether or not the patch is in an attribute declaration.
* @return the previous value.
*/
function setInAttributes(value) {
var previous = inAttributes;
inAttributes = value;
return previous;
}
/**
* Updates the state of being in a skip element.
* @param value Whether or not the patch is skipping the children of a
* parent node.
* @return the previous value.
*/
function setInSkip(value) {
var previous = inSkip;
inSkip = value;
return previous;
}
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A cached reference to the hasOwnProperty function.
*/
var hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* A constructor function that will create blank objects.
*/
function Blank() { }
Blank.prototype = Object.create(null);
/**
* Used to prevent property collisions between our "map" and its prototype.
* @param map The map to check.
* @param property The property to check.
* @return Whether map has property.
*/
function has(map, property) {
return hasOwnProperty.call(map, property);
}
/**
* Creates an map object without a prototype.
* @returns An Object that can be used as a map.
*/
function createMap() {
return new Blank();
}
/**
* Truncates an array, removing items up until length.
* @param arr The array to truncate.
* @param length The new length of the array.
*/
function truncateArray(arr, length) {
while (arr.length > length) {
arr.pop();
}
}
/**
* Creates an array for a desired initial size. Note that the array will still
* be empty.
* @param initialAllocationSize The initial size to allocate.
* @returns An empty array, with an initial allocation for the desired size.
*/
function createArray(initialAllocationSize) {
var arr = new Array(initialAllocationSize);
truncateArray(arr, 0);
return arr;
}
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var symbols = {
default: "__default"
};
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @param name The name of the attribute. For example "tabindex" or
* "xlink:href".
* @returns The namespace to use for the attribute, or null if there is
* no namespace.
*/
function getNamespace(name) {
if (name.lastIndexOf("xml:", 0) === 0) {
return "http://www.w3.org/XML/1998/namespace";
}
if (name.lastIndexOf("xlink:", 0) === 0) {
return "http://www.w3.org/1999/xlink";
}
return null;
}
/**
* Applies an attribute or property to a given Element. If the value is null
* or undefined, it is removed from the Element. Otherwise, the value is set
* as an attribute.
* @param el The element to apply the attribute to.
* @param name The attribute's name.
* @param value The attribute's value.
*/
function applyAttr(el, name, value) {
if (value == null) {
el.removeAttribute(name);
}
else {
var attrNS = getNamespace(name);
if (attrNS) {
el.setAttributeNS(attrNS, name, String(value));
}
else {
el.setAttribute(name, String(value));
}
}
}
/**
* Applies a property to a given Element.
* @param el The element to apply the property to.
* @param name The property's name.
* @param value The property's value.
*/
function applyProp(el, name, value) {
el[name] = value;
}
/**
* Applies a value to a style declaration. Supports CSS custom properties by
* setting properties containing a dash using CSSStyleDeclaration.setProperty.
* @param style A style declaration.
* @param prop The property to apply. This can be either camelcase or dash
* separated. For example: "backgroundColor" and "background-color" are both
* supported.
* @param value The value of the property.
*/
function setStyleValue(style, prop, value) {
if (prop.indexOf("-") >= 0) {
style.setProperty(prop, value);
}
else {
style[prop] = value;
}
}
/**
* Applies a style to an Element. No vendor prefix expansion is done for
* property names/values.
* @param el The Element to apply the style for.
* @param name The attribute's name.
* @param style The style to set. Either a string of css or an object
* containing property-value pairs.
*/
function applyStyle(el, name, style) {
// MathML elements inherit from Element, which does not have style. We cannot
// do `instanceof HTMLElement` / `instanceof SVGElement`, since el can belong
// to a different document, so just check that it has a style.
assert("style" in el);
var elStyle = el.style;
if (typeof style === "string") {
elStyle.cssText = style;
}
else {
elStyle.cssText = "";
for (var prop in style) {
if (has(style, prop)) {
setStyleValue(elStyle, prop, style[prop]);
}
}
}
}
/**
* Updates a single attribute on an Element.
* @param el The Element to apply the attribute to.
* @param name The attribute's name.
* @param value The attribute's value. If the value is an object or
* function it is set on the Element, otherwise, it is set as an HTML
* attribute.
*/
function applyAttributeTyped(el, name, value) {
var type = typeof value;
if (type === "object" || type === "function") {
applyProp(el, name, value);
}
else {
applyAttr(el, name, value);
}
}
/**
* A publicly mutable object to provide custom mutators for attributes.
* NB: The result of createMap() has to be recast since closure compiler
* will just assume attributes is "any" otherwise and throws away
* the type annotation set by tsickle.
*/
var attributes = createMap();
// Special generic mutator that's called for any attribute that does not
// have a specific mutator.
attributes[symbols.default] = applyAttributeTyped;
attributes["style"] = applyStyle;
/**
* Calls the appropriate attribute mutator for this attribute.
* @param el The Element to apply the attribute to.
* @param name The attribute's name.
* @param value The attribute's value. If the value is an object or
* function it is set on the Element, otherwise, it is set as an HTML
* attribute.
*/
function updateAttribute(el, name, value) {
var mutator = attributes[name] || attributes[symbols.default];
mutator(el, name, value);
}
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var notifications = {
nodesCreated: null,
nodesDeleted: null
};
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A context object keeps track of the state of a patch.
*/
var Context = /** @class */ (function () {
function Context() {
this.created = [];
this.deleted = [];
}
Context.prototype.markCreated = function (node) {
this.created.push(node);
};
Context.prototype.markDeleted = function (node) {
this.deleted.push(node);
};
/**
* Notifies about nodes that were created during the patch operation.
*/
Context.prototype.notifyChanges = function () {
if (notifications.nodesCreated && this.created.length > 0) {
notifications.nodesCreated(this.created);
}
if (notifications.nodesDeleted && this.deleted.length > 0) {
notifications.nodesDeleted(this.deleted);
}
};
return Context;
}());
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Checks if the node is the root of a document. This is either a Document
* or ShadowRoot. DocumentFragments are included for simplicity of the
* implementation, though we only want to consider Documents or ShadowRoots.
* @param node The node to check.
* @return True if the node the root of a document, false otherwise.
*/
function isDocumentRoot(node) {
return node.nodeType === 11 || node.nodeType === 9;
}
/**
* Checks if the node is an Element. This is faster than an instanceof check.
* @param node The node to check.
* @return Whether or not the node is an Element.
*/
function isElement(node) {
return node.nodeType === 1;
}
/**
* @param node The node to start at, inclusive.
* @param root The root ancestor to get until, exclusive.
* @return The ancestry of DOM nodes.
*/
function getAncestry(node, root) {
var ancestry = [];
var cur = node;
while (cur !== root) {
var n = assert(cur);
ancestry.push(n);
cur = n.parentNode;
}
return ancestry;
}
/**
* @param this
* @returns The root node of the DOM tree that contains this node.
*/
var getRootNode = (typeof Node !== "undefined" && Node.prototype.getRootNode) ||
function () {
var cur = this;
var prev = cur;
while (cur) {
prev = cur;
cur = cur.parentNode;
}
return prev;
};
/**
* @param node The node to get the activeElement for.
* @returns The activeElement in the Document or ShadowRoot
* corresponding to node, if present.
*/
function getActiveElement(node) {
var root = getRootNode.call(node);
return isDocumentRoot(root) ? root.activeElement : null;
}
/**
* Gets the path of nodes that contain the focused node in the same document as
* a reference node, up until the root.
* @param node The reference node to get the activeElement for.
* @param root The root to get the focused path until.
* @returns The path of focused parents, if any exist.
*/
function getFocusedPath(node, root) {
var activeElement = getActiveElement(node);
if (!activeElement || !node.contains(activeElement)) {
return [];
}
return getAncestry(activeElement, root);
}
/**
* Like insertBefore, but instead instead of moving the desired node, instead
* moves all the other nodes after.
* @param parentNode
* @param node
* @param referenceNode
*/
function moveBefore(parentNode, node, referenceNode) {
var insertReferenceNode = node.nextSibling;
var cur = referenceNode;
while (cur !== null && cur !== node) {
var next = cur.nextSibling;
parentNode.insertBefore(cur, insertReferenceNode);
cur = next;
}
}
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Keeps track of information needed to perform diffs for a given DOM node.
*/
var NodeData = /** @class */ (function () {
function NodeData(nameOrCtor, key, text) {
/**
* An array of attribute name/value pairs, used for quickly diffing the
* incomming attributes to see if the DOM node's attributes need to be
* updated.
*/
this._attrsArr = null;
/**
* Whether or not the statics have been applied for the node yet.
*/
this.staticsApplied = false;
this.nameOrCtor = nameOrCtor;
this.key = key;
this.text = text;
}
NodeData.prototype.hasEmptyAttrsArr = function () {
var attrs = this._attrsArr;
return !attrs || !attrs.length;
};
NodeData.prototype.getAttrsArr = function (length) {
return this._attrsArr || (this._attrsArr = createArray(length));
};
return NodeData;
}());
/**
* Initializes a NodeData object for a Node.
* @param node The Node to initialized data for.
* @param nameOrCtor The NameOrCtorDef to use when diffing.
* @param key The Key for the Node.
* @param text The data of a Text node, if importing a Text node.
* @returns A NodeData object with the existing attributes initialized.
*/
function initData(node, nameOrCtor, key, text) {
var data = new NodeData(nameOrCtor, key, text);
node["__incrementalDOMData"] = data;
return data;
}
/**
* @param node The node to check.
* @returns True if the NodeData already exists, false otherwise.
*/
function isDataInitialized(node) {
return Boolean(node["__incrementalDOMData"]);
}
/**
* Records the element's attributes.
* @param node The Element that may have attributes
* @param data The Element's data
*/
function recordAttributes(node, data) {
var attributes = node.attributes;
var length = attributes.length;
if (!length) {
return;
}
var attrsArr = data.getAttrsArr(length);
// Use a cached length. The attributes array is really a live NamedNodeMap,
// which exists as a DOM "Host Object" (probably as C++ code). This makes the
// usual constant length iteration very difficult to optimize in JITs.
for (var i = 0, j = 0; i < length; i += 1, j += 2) {
var attr_1 = attributes[i];
var name_1 = attr_1.name;
var value = attr_1.value;
attrsArr[j] = name_1;
attrsArr[j + 1] = value;
}
}
/**
* Imports single node and its subtree, initializing caches, if it has not
* already been imported.
* @param node The node to import.
* @param fallbackKey A key to use if importing and no key was specified.
* Useful when not transmitting keys from serverside render and doing an
* immediate no-op diff.
* @returns The NodeData for the node.
*/
function importSingleNode(node, fallbackKey) {
if (node["__incrementalDOMData"]) {
return node["__incrementalDOMData"];
}
var nodeName = isElement(node) ? node.localName : node.nodeName;
var keyAttrName = getKeyAttributeName();
var keyAttr = isElement(node) && keyAttrName != null
? node.getAttribute(keyAttrName)
: null;
var key = isElement(node) ? keyAttr || fallbackKey : null;
var data = initData(node, nodeName, key);
if (isElement(node)) {
recordAttributes(node, data);
}
return data;
}
/**
* Imports node and its subtree, initializing caches.
* @param node The Node to import.
*/
function importNode(node) {
importSingleNode(node);
for (var child = node.firstChild; child; child = child.nextSibling) {
importNode(child);
}
}
/**
* Retrieves the NodeData object for a Node, creating it if necessary.
* @param node The node to get data for.
* @param fallbackKey A key to use if importing and no key was specified.
* Useful when not transmitting keys from serverside render and doing an
* immediate no-op diff.
* @returns The NodeData for the node.
*/
function getData(node, fallbackKey) {
return importSingleNode(node, fallbackKey);
}
/**
* Gets the key for a Node. note that the Node should have been imported
* by now.
* @param node The node to check.
* @returns The key used to create the node.
*/
function getKey(node) {
assert(node["__incrementalDOMData"]);
return getData(node).key;
}
/**
* Clears all caches from a node and all of its children.
* @param node The Node to clear the cache for.
*/
function clearCache(node) {
node["__incrementalDOMData"] = null;
for (var child = node.firstChild; child; child = child.nextSibling) {
clearCache(child);
}
}
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Gets the namespace to create an element (of a given tag) in.
* @param tag The tag to get the namespace for.
* @param parent The current parent Node, if any.
* @returns The namespace to use,
*/
function getNamespaceForTag(tag, parent) {
if (tag === "svg") {
return "http://www.w3.org/2000/svg";
}
if (tag === "math") {
return "http://www.w3.org/1998/Math/MathML";
}
if (parent == null) {
return null;
}
if (getData(parent).nameOrCtor === "foreignObject") {
return null;
}
return parent.namespaceURI;
}
/**
* Creates an Element and initializes the NodeData.
* @param doc The document with which to create the Element.
* @param parent The parent of new Element.
* @param nameOrCtor The tag or constructor for the Element.
* @param key A key to identify the Element.
* @returns The newly created Element.
*/
function createElement(doc, parent, nameOrCtor, key) {
var el;
if (typeof nameOrCtor === "function") {
el = new nameOrCtor();
}
else {
var namespace = getNamespaceForTag(nameOrCtor, parent);
if (namespace) {
el = doc.createElementNS(namespace, nameOrCtor);
}
else {
el = doc.createElement(nameOrCtor);
}
}
initData(el, nameOrCtor, key);
return el;
}
/**
* Creates a Text Node.
* @param doc The document with which to create the Element.
* @returns The newly created Text.
*/
function createText(doc) {
var node = doc.createTextNode("");
initData(node, "#text", null);
return node;
}
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* The default match function to use, if one was not specified when creating
* the patcher.
* @param matchNode The node to match against, unused.
* @param nameOrCtor The name or constructor as declared.
* @param expectedNameOrCtor The name or constructor of the existing node.
* @param key The key as declared.
* @param expectedKey The key of the existing node.
* @returns True if the node matches, false otherwise.
*/
function defaultMatchFn(matchNode, nameOrCtor, expectedNameOrCtor, key, expectedKey) {
// Key check is done using double equals as we want to treat a null key the
// same as undefined. This should be okay as the only values allowed are
// strings, null and undefined so the == semantics are not too weird.
return nameOrCtor == expectedNameOrCtor && key == expectedKey;
}
var context = null;
var currentNode = null;
var currentParent = null;
var doc = null;
var focusPath = [];
var matchFn = defaultMatchFn;
/**
* Used to build up call arguments. Each patch call gets a separate copy, so
* this works with nested calls to patch.
*/
var argsBuilder = [];
/**
* Used to build up attrs for the an element.
*/
var attrsBuilder = [];
/**
* TODO(sparhami) We should just export argsBuilder directly when Closure
* Compiler supports ES6 directly.
* @returns The Array used for building arguments.
*/
function getArgsBuilder() {
return argsBuilder;
}
/**
* TODO(sparhami) We should just export attrsBuilder directly when Closure
* Compiler supports ES6 directly.
* @returns The Array used for building arguments.
*/
function getAttrsBuilder() {
return attrsBuilder;
}
/**
* Checks whether or not the current node matches the specified nameOrCtor and
* key. This uses the specified match function when creating the patcher.
* @param matchNode A node to match the data to.
* @param nameOrCtor The name or constructor to check for.
* @param key The key used to identify the Node.
* @return True if the node matches, false otherwise.
*/
function matches(matchNode, nameOrCtor, key) {
var data = getData(matchNode, key);
return matchFn(matchNode, nameOrCtor, data.nameOrCtor, key, data.key);
}
/**
* Finds the matching node, starting at `node` and looking at the subsequent
* siblings if a key is used.
* @param matchNode The node to start looking at.
* @param nameOrCtor The name or constructor for the Node.
* @param key The key used to identify the Node.
* @returns The matching Node, if any exists.
*/
function getMatchingNode(matchNode, nameOrCtor, key) {
if (!matchNode) {
return null;
}
var cur = matchNode;
do {
if (matches(cur, nameOrCtor, key)) {
return cur;
}
} while (key && (cur = cur.nextSibling));
return null;
}
/**
* Clears out any unvisited Nodes in a given range.
* @param maybeParentNode
* @param startNode The node to start clearing from, inclusive.
* @param endNode The node to clear until, exclusive.
*/
function clearUnvisitedDOM(maybeParentNode, startNode, endNode) {
var parentNode = maybeParentNode;
var child = startNode;
while (child !== endNode) {
var next = child.nextSibling;
parentNode.removeChild(child);
context.markDeleted(child);
child = next;
}
}
/**
* @return The next Node to be patched.
*/
function getNextNode() {
if (currentNode) {
return currentNode.nextSibling;
}
else {
return currentParent.firstChild;
}
}
/**
* Changes to the first child of the current node.
*/
function enterNode() {
currentParent = currentNode;
currentNode = null;
}
/**
* Changes to the parent of the current node, removing any unvisited children.
*/
function exitNode() {
clearUnvisitedDOM(currentParent, getNextNode(), null);
currentNode = currentParent;
currentParent = currentParent.parentNode;
}
/**
* Changes to the next sibling of the current node.
*/
function nextNode() {
currentNode = getNextNode();
}
/**
* Creates a Node and marking it as created.
* @param nameOrCtor The name or constructor for the Node.
* @param key The key used to identify the Node.
* @return The newly created node.
*/
function createNode(nameOrCtor, key) {
var node;
if (nameOrCtor === "#text") {
node = createText(doc);
}
else {
node = createElement(doc, currentParent, nameOrCtor, key);
}
context.markCreated(node);
return node;
}
/**
* Aligns the virtual Node definition with the actual DOM, moving the
* corresponding DOM node to the correct location or creating it if necessary.
* @param nameOrCtor The name or constructor for the Node.
* @param key The key used to identify the Node.
*/
function alignWithDOM(nameOrCtor, key) {
nextNode();
var existingNode = getMatchingNode(currentNode, nameOrCtor, key);
var node = existingNode || createNode(nameOrCtor, key);
// If we are at the matching node, then we are done.
if (node === currentNode) {
return;
}
// Re-order the node into the right position, preserving focus if either
// node or currentNode are focused by making sure that they are not detached
// from the DOM.
if (focusPath.indexOf(node) >= 0) {
// Move everything else before the node.
moveBefore(currentParent, node, currentNode);
}
else {
currentParent.insertBefore(node, currentNode);
}
currentNode = node;
}
/**
* Makes sure that the current node is an Element with a matching nameOrCtor and
* key.
*
* @param nameOrCtor The tag or constructor for the Element.
* @param key The key used to identify this element. This can be an
* empty string, but performance may be better if a unique value is used
* when iterating over an array of items.
* @return The corresponding Element.
*/
function open(nameOrCtor, key) {
alignWithDOM(nameOrCtor, key);
enterNode();
return currentParent;
}
/**
* Closes the currently open Element, removing any unvisited children if
* necessary.
* @returns The Element that was just closed.
*/
function close() {
{
setInSkip(false);
}
exitNode();
return currentNode;
}
/**
* Makes sure the current node is a Text node and creates a Text node if it is
* not.
* @returns The Text node that was aligned or created.
*/
function text() {
alignWithDOM("#text", null);
return currentNode;
}
/**
* @returns The current Element being patched.
*/
function currentElement() {
{
assertInPatch("currentElement");
assertNotInAttributes("currentElement");
}
return currentParent;
}
/**
* @return The Node that will be evaluated for the next instruction.
*/
function currentPointer() {
{
assertInPatch("currentPointer");
assertNotInAttributes("currentPointer");
}
// TODO(tomnguyen): assert that this is not null
return getNextNode();
}
/**
* Skips the children in a subtree, allowing an Element to be closed without
* clearing out the children.
*/
function skip() {
{
assertNoChildrenDeclaredYet("skip", currentNode);
setInSkip(true);
}
currentNode = currentParent.lastChild;
}
/**
* Returns a patcher function that sets up and restores a patch context,
* running the run function with the provided data.
* @param run The function that will run the patch.
* @param patchConfig The configuration to use for the patch.
* @returns The created patch function.
*/
function createPatcher(run, patchConfig) {
if (patchConfig === void 0) { patchConfig = {}; }
var _a = patchConfig.matches, matches = _a === void 0 ? defaultMatchFn : _a;
var f = function (node, fn, data) {
var prevContext = context;
var prevDoc = doc;
var prevFocusPath = focusPath;
var prevArgsBuilder = argsBuilder;
var prevAttrsBuilder = attrsBuilder;
var prevCurrentNode = currentNode;
var prevCurrentParent = currentParent;
var prevMatchFn = matchFn;
var previousInAttributes = false;
var previousInSkip = false;
doc = node.ownerDocument;
context = new Context();
matchFn = matches;
argsBuilder = [];
attrsBuilder = [];
currentNode = null;
currentParent = node.parentNode;
focusPath = getFocusedPath(node, currentParent);
{
previousInAttributes = setInAttributes(false);
previousInSkip = setInSkip(false);
updatePatchContext(context);
}
try {
var retVal = run(node, fn, data);
{
assertVirtualAttributesClosed();
}
return retVal;
}
finally {
context.notifyChanges();
doc = prevDoc;
context = prevContext;
matchFn = prevMatchFn;
argsBuilder = prevArgsBuilder;
attrsBuilder = prevAttrsBuilder;
currentNode = prevCurrentNode;
currentParent = prevCurrentParent;
focusPath = prevFocusPath;
// Needs to be done after assertions because assertions rely on state
// from these methods.
{
setInAttributes(previousInAttributes);
setInSkip(previousInSkip);
updatePatchContext(context);
}
}
};
return f;
}
/**
* Creates a patcher that patches the document starting at node with a
* provided function. This function may be called during an existing patch operation.
* @param patchConfig The config to use for the patch.
* @returns The created function for patching an Element's children.
*/
function createPatchInner(patchConfig) {
return createPatcher(function (node, fn, data) {
currentNode = node;
enterNode();
fn(data);
exitNode();
{
assertNoUnclosedTags(currentNode, node);
}
return node;
}, patchConfig);
}
/**
* Creates a patcher that patches an Element with the the provided function.
* Exactly one top level element call should be made corresponding to `node`.
* @param patchConfig The config to use for the patch.
* @returns The created function for patching an Element.
*/
function createPatchOuter(patchConfig) {
return createPatcher(function (node, fn, data) {
var startNode = { nextSibling: node };
var expectedNextNode = null;
var expectedPrevNode = null;
{
expectedNextNode = node.nextSibling;
expectedPrevNode = node.previousSibling;
}
currentNode = startNode;
fn(data);
{
assertPatchOuterHasParentNode(currentParent);
assertPatchElementNoExtras(startNode, currentNode, expectedNextNode, expectedPrevNode);
}
if (currentParent) {
clearUnvisitedDOM(currentParent, getNextNode(), node.nextSibling);
}
return startNode === currentNode ? null : currentNode;
}, patchConfig);
}
var patchInner = createPatchInner();
var patchOuter = createPatchOuter();
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var buffer = [];
var bufferStart = 0;
/**
* TODO(tomnguyen): This is a bit silly and really needs to be better typed.
* @param fn A function to call.
* @param a The first argument to the function.
* @param b The second argument to the function.
* @param c The third argument to the function.
*/
function queueChange(fn, a, b, c) {
buffer.push(fn);
buffer.push(a);
buffer.push(b);
buffer.push(c);
}
/**
* Flushes the changes buffer, calling the functions for each change.
*/
function flush() {
// A change may cause this function to be called re-entrantly. Keep track of
// the portion of the buffer we are consuming. Updates the start pointer so
// that the next call knows where to start from.
var start = bufferStart;
var end = buffer.length;
bufferStart = end;
for (var i = start; i < end; i += 4) {
var fn = buffer[i];
fn(buffer[i + 1], buffer[i + 2], buffer[i + 3]);
}
bufferStart = start;
truncateArray(buffer, start);
}
/**
* Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Used to keep track of the previous values when a 2-way diff is necessary.
* This object is cleared out and reused.
*/
var prevValuesMap = createMap();
/**
* Calculates the diff between previous and next values, calling the update
* function when an item has changed value. If an item from the previous values
* is not present in the the next values, the update function is called with a
* value of `undefined`.
* @param prev The previous values, alternating name, value pairs.
* @param next The next values, alternating name, value pairs.
* @param updateCtx The context for the updateFn.
* @param updateFn A function to call when a value has changed.
*/
function calculateDiff(prev, next, updateCtx, updateFn) {
var isNew = !prev.length;
var i = 0;
for (; i < next.length; i += 2) {
var name_2 = next[i];
if (isNew) {
prev[i] = name_2;
}
else if (prev[i] !== name_2) {
break;
}
var value = next[i + 1];
if (isNew || prev[i + 1] !== value) {
prev[i + 1] = value;
queueChange(updateFn, updateCtx, name_2, value);
}
}
// Items did not line up exactly as before, need to make sure old items are
// removed. This should be a rare case.
if (i < next.length || i < prev.length) {
var startIndex = i;
for (i = startIndex; i < prev.length; i += 2) {
prevValuesMap[prev[i]] = prev[i + 1];
}
for (i = startIndex; i < next.length; i += 2) {
var name_3 = next[i];
var value = next[i + 1];
if (prevValuesMap[name_3] !== value) {
queueChange(updateFn, updateCtx, name_3, value