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 {
|
18 | assert,
|
19 | assertCloseMatchesOpenTag,
|
20 | assertInAttributes,
|
21 | assertInPatch,
|
22 | assertNotInAttributes,
|
23 | assertNotInSkip,
|
24 | setInAttributes
|
25 | } from "./assertions";
|
26 | import { updateAttribute } from "./attributes";
|
27 | import {
|
28 | getArgsBuilder,
|
29 | getAttrsBuilder,
|
30 | close,
|
31 | open,
|
32 | text as coreText,
|
33 | currentElement
|
34 | } from "./core";
|
35 | import { DEBUG } from "./global";
|
36 | import { getData, NodeData } from "./node_data";
|
37 | import { Key, NameOrCtorDef, Statics } from "./types";
|
38 | import { createMap, truncateArray } from "./util";
|
39 | import { calculateDiff } from "./diff";
|
40 |
|
41 | /**
|
42 | * The offset in the virtual element declaration where the attributes are
|
43 | * specified.
|
44 | */
|
45 | const ATTRIBUTES_OFFSET = 3;
|
46 |
|
47 | /**
|
48 | * Used to keep track of the previous values when a 2-way diff is necessary.
|
49 | * This object is reused.
|
50 | * TODO(sparhamI) Scope this to a patch so you can call patch from an attribute
|
51 | * update.
|
52 | */
|
53 | const prevAttrsMap = createMap();
|
54 |
|
55 | /**
|
56 | * @param element The Element to diff the attrs for.
|
57 | * @param data The NodeData associated with the Element.
|
58 | */
|
59 | function diffAttrs(element: Element, data: NodeData) {
|
60 | const attrsBuilder = getAttrsBuilder();
|
61 | const prevAttrsArr = data.getAttrsArr(attrsBuilder.length);
|
62 |
|
63 | calculateDiff(prevAttrsArr, attrsBuilder, element, updateAttribute);
|
64 | truncateArray(attrsBuilder, 0);
|
65 | }
|
66 |
|
67 | /**
|
68 | * Applies the statics. When importing an Element, any existing attributes that
|
69 | * match a static are converted into a static attribute.
|
70 | * @param node The Element to apply statics for.
|
71 | * @param data The NodeData associated with the Element.
|
72 | * @param statics The statics array.
|
73 | */
|
74 | function diffStatics(node: Element, data: NodeData, statics: Statics) {
|
75 | if (data.staticsApplied) {
|
76 | return;
|
77 | }
|
78 |
|
79 | data.staticsApplied = true;
|
80 |
|
81 | if (!statics || !statics.length) {
|
82 | return;
|
83 | }
|
84 |
|
85 | if (data.hasEmptyAttrsArr()) {
|
86 | for (let i = 0; i < statics.length; i += 2) {
|
87 | updateAttribute(node, statics[i] as string, statics[i + 1]);
|
88 | }
|
89 | return;
|
90 | }
|
91 |
|
92 | for (let i = 0; i < statics.length; i += 2) {
|
93 | prevAttrsMap[statics[i] as string] = i + 1;
|
94 | }
|
95 |
|
96 | const attrsArr = data.getAttrsArr(0);
|
97 | let j = 0;
|
98 | for (let i = 0; i < attrsArr.length; i += 2) {
|
99 | const name = attrsArr[i];
|
100 | const value = attrsArr[i + 1];
|
101 | const staticsIndex = prevAttrsMap[name];
|
102 |
|
103 | if (staticsIndex) {
|
104 | // For any attrs that are static and have the same value, make sure we do
|
105 | // not set them again.
|
106 | if (statics[staticsIndex] === value) {
|
107 | delete prevAttrsMap[name];
|
108 | }
|
109 |
|
110 | continue;
|
111 | }
|
112 |
|
113 | // For any attrs that are dynamic, move them up to the right place.
|
114 | attrsArr[j] = name;
|
115 | attrsArr[j + 1] = value;
|
116 | j += 2;
|
117 | }
|
118 | // Anything after `j` was either moved up already or static.
|
119 | truncateArray(attrsArr, j);
|
120 |
|
121 | for (const name in prevAttrsMap) {
|
122 | updateAttribute(node, name, statics[prevAttrsMap[name]]);
|
123 | delete prevAttrsMap[name];
|
124 | }
|
125 | }
|
126 |
|
127 | /**
|
128 | * Declares a virtual Element at the current location in the document. This
|
129 | * corresponds to an opening tag and a elementClose tag is required. This is
|
130 | * like elementOpen, but the attributes are defined using the attr function
|
131 | * rather than being passed as arguments. Must be folllowed by 0 or more calls
|
132 | * to attr, then a call to elementOpenEnd.
|
133 | * @param nameOrCtor The Element's tag or constructor.
|
134 | * @param key The key used to identify this element. This can be an
|
135 | * empty string, but performance may be better if a unique value is used
|
136 | * when iterating over an array of items.
|
137 | * @param statics An array of attribute name/value pairs of the static
|
138 | * attributes for the Element. Attributes will only be set once when the
|
139 | * Element is created.
|
140 | */
|
141 | function elementOpenStart(
|
142 | nameOrCtor: NameOrCtorDef,
|
143 | key?: Key,
|
144 | statics?: Statics
|
145 | ) {
|
146 | const argsBuilder = getArgsBuilder();
|
147 |
|
148 | if (DEBUG) {
|
149 | assertNotInAttributes("elementOpenStart");
|
150 | setInAttributes(true);
|
151 | }
|
152 |
|
153 | argsBuilder[0] = nameOrCtor;
|
154 | argsBuilder[1] = key;
|
155 | argsBuilder[2] = statics;
|
156 | }
|
157 |
|
158 | /**
|
159 | * Allows you to define a key after an elementOpenStart. This is useful in
|
160 | * templates that define key after an element has been opened ie
|
161 | * `<div key('foo')></div>`.
|
162 | * @param key The key to use for the next call.
|
163 | */
|
164 | function key(key: string) {
|
165 | const argsBuilder = getArgsBuilder();
|
166 |
|
167 | if (DEBUG) {
|
168 | assertInAttributes("key");
|
169 | assert(argsBuilder);
|
170 | }
|
171 | argsBuilder[1] = key;
|
172 | }
|
173 |
|
174 | /**
|
175 | * Buffers an attribute, which will get applied during the next call to
|
176 | * `elementOpen`, `elementOpenEnd` or `applyAttrs`.
|
177 | * @param name The of the attribute to buffer.
|
178 | * @param value The value of the attribute to buffer.
|
179 | */
|
180 | function attr(name: string, value: any) {
|
181 | const attrsBuilder = getAttrsBuilder();
|
182 |
|
183 | if (DEBUG) {
|
184 | assertInPatch("attr");
|
185 | }
|
186 |
|
187 | attrsBuilder.push(name);
|
188 | attrsBuilder.push(value);
|
189 | }
|
190 |
|
191 | /**
|
192 | * Closes an open tag started with elementOpenStart.
|
193 | * @return The corresponding Element.
|
194 | */
|
195 | function elementOpenEnd(): HTMLElement {
|
196 | const argsBuilder = getArgsBuilder();
|
197 |
|
198 | if (DEBUG) {
|
199 | assertInAttributes("elementOpenEnd");
|
200 | setInAttributes(false);
|
201 | }
|
202 |
|
203 | const node = open(<NameOrCtorDef>argsBuilder[0], <Key>argsBuilder[1]);
|
204 | const data = getData(node);
|
205 |
|
206 | diffStatics(node, data, <Statics>argsBuilder[2]);
|
207 | diffAttrs(node, data);
|
208 | truncateArray(argsBuilder, 0);
|
209 |
|
210 | return node;
|
211 | }
|
212 |
|
213 | /**
|
214 | * @param nameOrCtor The Element's tag or constructor.
|
215 | * @param key The key used to identify this element. This can be an
|
216 | * empty string, but performance may be better if a unique value is used
|
217 | * when iterating over an array of items.
|
218 | * @param statics An array of attribute name/value pairs of the static
|
219 | * attributes for the Element. Attributes will only be set once when the
|
220 | * Element is created.
|
221 | * @param varArgs, Attribute name/value pairs of the dynamic attributes
|
222 | * for the Element.
|
223 | * @return The corresponding Element.
|
224 | */
|
225 | function elementOpen(
|
226 | nameOrCtor: NameOrCtorDef,
|
227 | key?: Key,
|
228 | // Ideally we could tag statics and varArgs as an array where every odd
|
229 | // element is a string and every even element is any, but this is hard.
|
230 | statics?: Statics,
|
231 | ...varArgs: Array<any>
|
232 | ) {
|
233 | if (DEBUG) {
|
234 | assertNotInAttributes("elementOpen");
|
235 | assertNotInSkip("elementOpen");
|
236 | }
|
237 |
|
238 | elementOpenStart(nameOrCtor, key, statics);
|
239 |
|
240 | for (let i = ATTRIBUTES_OFFSET; i < arguments.length; i += 2) {
|
241 | attr(arguments[i], arguments[i + 1]);
|
242 | }
|
243 |
|
244 | return elementOpenEnd();
|
245 | }
|
246 |
|
247 | /**
|
248 | * Applies the currently buffered attrs to the currently open element. This
|
249 | * clears the buffered attributes.
|
250 | */
|
251 | function applyAttrs() {
|
252 | const node = currentElement();
|
253 | const data = getData(node);
|
254 |
|
255 | diffAttrs(node, data);
|
256 | }
|
257 |
|
258 | /**
|
259 | * Applies the current static attributes to the currently open element. Note:
|
260 | * statics should be applied before calling `applyAtrs`.
|
261 | * @param statics The statics to apply to the current element.
|
262 | */
|
263 | function applyStatics(statics: Statics) {
|
264 | const node = currentElement();
|
265 | const data = getData(node);
|
266 |
|
267 | diffStatics(node, data, statics);
|
268 | }
|
269 |
|
270 | /**
|
271 | * Closes an open virtual Element.
|
272 | *
|
273 | * @param nameOrCtor The Element's tag or constructor.
|
274 | * @return The corresponding Element.
|
275 | */
|
276 | function elementClose(nameOrCtor: NameOrCtorDef): Element {
|
277 | if (DEBUG) {
|
278 | assertNotInAttributes("elementClose");
|
279 | }
|
280 |
|
281 | const node = close();
|
282 |
|
283 | if (DEBUG) {
|
284 | assertCloseMatchesOpenTag(getData(node).nameOrCtor, nameOrCtor);
|
285 | }
|
286 |
|
287 | return node;
|
288 | }
|
289 |
|
290 | /**
|
291 | * Declares a virtual Element at the current location in the document that has
|
292 | * no children.
|
293 | * @param nameOrCtor The Element's tag or constructor.
|
294 | * @param key The key used to identify this element. This can be an
|
295 | * empty string, but performance may be better if a unique value is used
|
296 | * when iterating over an array of items.
|
297 | * @param statics An array of attribute name/value pairs of the static
|
298 | * attributes for the Element. Attributes will only be set once when the
|
299 | * Element is created.
|
300 | * @param varArgs Attribute name/value pairs of the dynamic attributes
|
301 | * for the Element.
|
302 | * @return The corresponding Element.
|
303 | */
|
304 | function elementVoid(
|
305 | nameOrCtor: NameOrCtorDef,
|
306 | key?: Key,
|
307 | // Ideally we could tag statics and varArgs as an array where every odd
|
308 | // element is a string and every even element is any, but this is hard.
|
309 | statics?: Statics,
|
310 | ...varArgs: Array<any>
|
311 | ) {
|
312 | elementOpen.apply(null, arguments as any);
|
313 | return elementClose(nameOrCtor);
|
314 | }
|
315 |
|
316 | /**
|
317 | * Declares a virtual Text at this point in the document.
|
318 | *
|
319 | * @param value The value of the Text.
|
320 | * @param varArgs
|
321 | * Functions to format the value which are called only when the value has
|
322 | * changed.
|
323 | * @return The corresponding text node.
|
324 | */
|
325 | function text(
|
326 | value: string | number | boolean,
|
327 | ...varArgs: Array<(a: {}) => string>
|
328 | ) {
|
329 | if (DEBUG) {
|
330 | assertNotInAttributes("text");
|
331 | assertNotInSkip("text");
|
332 | }
|
333 |
|
334 | const node = coreText();
|
335 | const data = getData(node);
|
336 |
|
337 | if (data.text !== value) {
|
338 | data.text = value as string;
|
339 |
|
340 | let formatted = value;
|
341 | for (let i = 1; i < arguments.length; i += 1) {
|
342 | /*
|
343 | * Call the formatter function directly to prevent leaking arguments.
|
344 | * https://github.com/google/incremental-dom/pull/204#issuecomment-178223574
|
345 | */
|
346 | const fn = arguments[i];
|
347 | formatted = fn(formatted);
|
348 | }
|
349 |
|
350 | // Setting node.data resets the cursor in IE/Edge.
|
351 | if (node.data !== formatted) {
|
352 | node.data = formatted as string;
|
353 | }
|
354 | }
|
355 |
|
356 | return node;
|
357 | }
|
358 |
|
359 | /** */
|
360 | export {
|
361 | applyAttrs,
|
362 | applyStatics,
|
363 | elementOpenStart,
|
364 | elementOpenEnd,
|
365 | elementOpen,
|
366 | elementVoid,
|
367 | elementClose,
|
368 | text,
|
369 | attr,
|
370 | key
|
371 | };
|