UNPKG

incremental-dom

Version:
1,325 lines 63.2 kB
/** * @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