1 | /**
|
2 | * Copyright 2018 The Incremental DOM Authors. All Rights Reserved.
|
3 | *
|
4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
5 | * you may not use this file except in compliance with the License.
|
6 | * You may obtain a copy of the License at
|
7 | *
|
8 | * http://www.apache.org/licenses/LICENSE-2.0
|
9 | *
|
10 | * Unless required by applicable law or agreed to in writing, software
|
11 | * distributed under the License is distributed on an "AS-IS" BASIS,
|
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 | * See the License for the specific language governing permissions and
|
14 | * limitations under the License.
|
15 | */
|
16 |
|
17 | import { Key, NameOrCtorDef } from "./types";
|
18 | import { assert } from "./assertions";
|
19 | import { createArray } from "./util";
|
20 | import { isElement } from "./dom_util";
|
21 | import { getKeyAttributeName } from "./global";
|
22 |
|
23 | declare global {
|
24 | interface Node {
|
25 | __incrementalDOMData: NodeData | null;
|
26 | }
|
27 | }
|
28 |
|
29 | /**
|
30 | * Keeps track of information needed to perform diffs for a given DOM node.
|
31 | */
|
32 | export class NodeData {
|
33 | /**
|
34 | * An array of attribute name/value pairs, used for quickly diffing the
|
35 | * incomming attributes to see if the DOM node's attributes need to be
|
36 | * updated.
|
37 | */
|
38 | private _attrsArr: Array<any> | null = null;
|
39 |
|
40 | /**
|
41 | * Whether or not the statics have been applied for the node yet.
|
42 | */
|
43 | public staticsApplied = false;
|
44 |
|
45 | /**
|
46 | * The key used to identify this node, used to preserve DOM nodes when they
|
47 | * move within their parent.
|
48 | */
|
49 | public readonly key: Key;
|
50 |
|
51 | /**
|
52 | * The previous text value, for Text nodes.
|
53 | */
|
54 | public text: string | undefined;
|
55 |
|
56 | /**
|
57 | * The nodeName or contructor for the Node.
|
58 | */
|
59 | public readonly nameOrCtor: NameOrCtorDef;
|
60 |
|
61 | public constructor(
|
62 | nameOrCtor: NameOrCtorDef,
|
63 | key: Key,
|
64 | text: string | undefined
|
65 | ) {
|
66 | this.nameOrCtor = nameOrCtor;
|
67 | this.key = key;
|
68 | this.text = text;
|
69 | }
|
70 |
|
71 | public hasEmptyAttrsArr(): boolean {
|
72 | const attrs = this._attrsArr;
|
73 | return !attrs || !attrs.length;
|
74 | }
|
75 |
|
76 | public getAttrsArr(length: number): Array<any> {
|
77 | return this._attrsArr || (this._attrsArr = createArray(length));
|
78 | }
|
79 | }
|
80 |
|
81 | /**
|
82 | * Initializes a NodeData object for a Node.
|
83 | * @param node The Node to initialized data for.
|
84 | * @param nameOrCtor The NameOrCtorDef to use when diffing.
|
85 | * @param key The Key for the Node.
|
86 | * @param text The data of a Text node, if importing a Text node.
|
87 | * @returns A NodeData object with the existing attributes initialized.
|
88 | */
|
89 | function initData(
|
90 | node: Node,
|
91 | nameOrCtor: NameOrCtorDef,
|
92 | key: Key,
|
93 | text?: string | undefined
|
94 | ): NodeData {
|
95 | const data = new NodeData(nameOrCtor, key, text);
|
96 | node["__incrementalDOMData"] = data;
|
97 | return data;
|
98 | }
|
99 |
|
100 | /**
|
101 | * @param node The node to check.
|
102 | * @returns True if the NodeData already exists, false otherwise.
|
103 | */
|
104 | function isDataInitialized(node: Node): boolean {
|
105 | return Boolean(node["__incrementalDOMData"]);
|
106 | }
|
107 |
|
108 | /**
|
109 | * Records the element's attributes.
|
110 | * @param node The Element that may have attributes
|
111 | * @param data The Element's data
|
112 | */
|
113 | function recordAttributes(node: Element, data: NodeData) {
|
114 | const attributes = node.attributes;
|
115 | const length = attributes.length;
|
116 | if (!length) {
|
117 | return;
|
118 | }
|
119 |
|
120 | const attrsArr = data.getAttrsArr(length);
|
121 |
|
122 | // Use a cached length. The attributes array is really a live NamedNodeMap,
|
123 | // which exists as a DOM "Host Object" (probably as C++ code). This makes the
|
124 | // usual constant length iteration very difficult to optimize in JITs.
|
125 | for (let i = 0, j = 0; i < length; i += 1, j += 2) {
|
126 | const attr = attributes[i];
|
127 | const name = attr.name;
|
128 | const value = attr.value;
|
129 |
|
130 | attrsArr[j] = name;
|
131 | attrsArr[j + 1] = value;
|
132 | }
|
133 | }
|
134 |
|
135 | /**
|
136 | * Imports single node and its subtree, initializing caches, if it has not
|
137 | * already been imported.
|
138 | * @param node The node to import.
|
139 | * @param fallbackKey A key to use if importing and no key was specified.
|
140 | * Useful when not transmitting keys from serverside render and doing an
|
141 | * immediate no-op diff.
|
142 | * @returns The NodeData for the node.
|
143 | */
|
144 | function importSingleNode(node: Node, fallbackKey?: Key): NodeData {
|
145 | if (node["__incrementalDOMData"]) {
|
146 | return node["__incrementalDOMData"];
|
147 | }
|
148 |
|
149 | const nodeName = isElement(node) ? node.localName : node.nodeName;
|
150 | const keyAttrName = getKeyAttributeName();
|
151 | const keyAttr =
|
152 | isElement(node) && keyAttrName != null
|
153 | ? node.getAttribute(keyAttrName)
|
154 | : null;
|
155 | const key = isElement(node) ? keyAttr || fallbackKey : null;
|
156 | const data = initData(node, nodeName, key);
|
157 |
|
158 | if (isElement(node)) {
|
159 | recordAttributes(node, data);
|
160 | }
|
161 |
|
162 | return data;
|
163 | }
|
164 |
|
165 | /**
|
166 | * Imports node and its subtree, initializing caches.
|
167 | * @param node The Node to import.
|
168 | */
|
169 | function importNode(node: Node) {
|
170 | importSingleNode(node);
|
171 |
|
172 | for (
|
173 | let child: Node | null = node.firstChild;
|
174 | child;
|
175 | child = child.nextSibling
|
176 | ) {
|
177 | importNode(child);
|
178 | }
|
179 | }
|
180 |
|
181 | /**
|
182 | * Retrieves the NodeData object for a Node, creating it if necessary.
|
183 | * @param node The node to get data for.
|
184 | * @param fallbackKey A key to use if importing and no key was specified.
|
185 | * Useful when not transmitting keys from serverside render and doing an
|
186 | * immediate no-op diff.
|
187 | * @returns The NodeData for the node.
|
188 | */
|
189 | function getData(node: Node, fallbackKey?: Key) {
|
190 | return importSingleNode(node, fallbackKey);
|
191 | }
|
192 |
|
193 | /**
|
194 | * Gets the key for a Node. note that the Node should have been imported
|
195 | * by now.
|
196 | * @param node The node to check.
|
197 | * @returns The key used to create the node.
|
198 | */
|
199 | function getKey(node: Node) {
|
200 | assert(node["__incrementalDOMData"]);
|
201 | return getData(node).key;
|
202 | }
|
203 |
|
204 | /**
|
205 | * Clears all caches from a node and all of its children.
|
206 | * @param node The Node to clear the cache for.
|
207 | */
|
208 | function clearCache(node: Node) {
|
209 | node["__incrementalDOMData"] = null;
|
210 |
|
211 | for (
|
212 | let child: Node | null = node.firstChild;
|
213 | child;
|
214 | child = child.nextSibling
|
215 | ) {
|
216 | clearCache(child);
|
217 | }
|
218 | }
|
219 |
|
220 | export { getData, getKey, initData, importNode, isDataInitialized, clearCache };
|