1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | import {
|
18 | assertInPatch,
|
19 | assertNoChildrenDeclaredYet,
|
20 | assertNotInAttributes,
|
21 | assertNoUnclosedTags,
|
22 | assertPatchElementNoExtras,
|
23 | assertPatchOuterHasParentNode,
|
24 | assertVirtualAttributesClosed,
|
25 | setInAttributes,
|
26 | setInSkip,
|
27 | updatePatchContext
|
28 | } from "./assertions";
|
29 | import { Context } from "./context";
|
30 | import { getFocusedPath, moveBefore } from "./dom_util";
|
31 | import { DEBUG } from "./global";
|
32 | import { getData } from "./node_data";
|
33 | import { createElement, createText } from "./nodes";
|
34 | import {
|
35 | Key,
|
36 | MatchFnDef,
|
37 | NameOrCtorDef,
|
38 | PatchConfig,
|
39 | PatchFunction
|
40 | } from "./types";
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | function defaultMatchFn(
|
53 | matchNode: Node,
|
54 | nameOrCtor: NameOrCtorDef,
|
55 | expectedNameOrCtor: NameOrCtorDef,
|
56 | key: Key,
|
57 | expectedKey: Key
|
58 | ): boolean {
|
59 |
|
60 |
|
61 |
|
62 | return nameOrCtor == expectedNameOrCtor && key == expectedKey;
|
63 | }
|
64 |
|
65 | let context: Context | null = null;
|
66 |
|
67 | let currentNode: Node | null = null;
|
68 |
|
69 | let currentParent: Node | null = null;
|
70 |
|
71 | let doc: Document | null = null;
|
72 |
|
73 | let focusPath: Array<Node> = [];
|
74 |
|
75 | let matchFn: MatchFnDef = defaultMatchFn;
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | let argsBuilder: Array<{} | null | undefined> = [];
|
82 |
|
83 |
|
84 |
|
85 |
|
86 | let attrsBuilder: Array<any> = [];
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 | function getArgsBuilder(): Array<any> {
|
94 | return argsBuilder;
|
95 | }
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 | function getAttrsBuilder(): Array<any> {
|
103 | return attrsBuilder;
|
104 | }
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | function matches(
|
115 | matchNode: Node,
|
116 | nameOrCtor: NameOrCtorDef,
|
117 | key: Key
|
118 | ): boolean {
|
119 | const data = getData(matchNode, key);
|
120 |
|
121 | return matchFn(matchNode, nameOrCtor, data.nameOrCtor, key, data.key);
|
122 | }
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 | function getMatchingNode(
|
133 | matchNode: Node | null,
|
134 | nameOrCtor: NameOrCtorDef,
|
135 | key: Key
|
136 | ): Node | null {
|
137 | if (!matchNode) {
|
138 | return null;
|
139 | }
|
140 |
|
141 | let cur: Node | null = matchNode;
|
142 |
|
143 | do {
|
144 | if (matches(cur, nameOrCtor, key)) {
|
145 | return cur;
|
146 | }
|
147 | } while (key && (cur = cur.nextSibling));
|
148 |
|
149 | return null;
|
150 | }
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 | function clearUnvisitedDOM(
|
158 | maybeParentNode: Node | null,
|
159 | startNode: Node | null,
|
160 | endNode: Node | null
|
161 | ) {
|
162 | const parentNode = maybeParentNode!;
|
163 | let child = startNode;
|
164 |
|
165 | while (child !== endNode) {
|
166 | const next = child!.nextSibling;
|
167 | parentNode.removeChild(child!);
|
168 | context!.markDeleted(child!);
|
169 | child = next;
|
170 | }
|
171 | }
|
172 |
|
173 |
|
174 |
|
175 |
|
176 | function getNextNode(): Node | null {
|
177 | if (currentNode) {
|
178 | return currentNode.nextSibling;
|
179 | } else {
|
180 | return currentParent!.firstChild;
|
181 | }
|
182 | }
|
183 |
|
184 |
|
185 |
|
186 |
|
187 | function enterNode() {
|
188 | currentParent = currentNode;
|
189 | currentNode = null;
|
190 | }
|
191 |
|
192 |
|
193 |
|
194 |
|
195 | function exitNode() {
|
196 | clearUnvisitedDOM(currentParent, getNextNode(), null);
|
197 |
|
198 | currentNode = currentParent;
|
199 | currentParent = currentParent!.parentNode;
|
200 | }
|
201 |
|
202 |
|
203 |
|
204 |
|
205 | function nextNode() {
|
206 | currentNode = getNextNode();
|
207 | }
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 | function createNode(nameOrCtor: NameOrCtorDef, key: Key): Node {
|
216 | let node;
|
217 |
|
218 | if (nameOrCtor === "#text") {
|
219 | node = createText(doc!);
|
220 | } else {
|
221 | node = createElement(doc!, currentParent!, nameOrCtor, key);
|
222 | }
|
223 |
|
224 | context!.markCreated(node);
|
225 |
|
226 | return node;
|
227 | }
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 | function alignWithDOM(nameOrCtor: NameOrCtorDef, key: Key) {
|
236 | nextNode();
|
237 | const existingNode = getMatchingNode(currentNode, nameOrCtor, key);
|
238 | const node = existingNode || createNode(nameOrCtor, key);
|
239 |
|
240 |
|
241 | if (node === currentNode) {
|
242 | return;
|
243 | }
|
244 |
|
245 |
|
246 |
|
247 |
|
248 | if (focusPath.indexOf(node) >= 0) {
|
249 |
|
250 | moveBefore(currentParent!, node, currentNode);
|
251 | } else {
|
252 | currentParent!.insertBefore(node, currentNode);
|
253 | }
|
254 |
|
255 | currentNode = node;
|
256 | }
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 | function open(nameOrCtor: NameOrCtorDef, key?: Key): HTMLElement {
|
269 | alignWithDOM(nameOrCtor, key);
|
270 | enterNode();
|
271 | return currentParent as HTMLElement;
|
272 | }
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 | function close(): Element {
|
280 | if (DEBUG) {
|
281 | setInSkip(false);
|
282 | }
|
283 |
|
284 | exitNode();
|
285 | return currentNode as Element;
|
286 | }
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 | function text(): Text {
|
294 | alignWithDOM("#text", null);
|
295 | return currentNode as Text;
|
296 | }
|
297 |
|
298 |
|
299 |
|
300 |
|
301 | function currentElement(): Element {
|
302 | if (DEBUG) {
|
303 | assertInPatch("currentElement");
|
304 | assertNotInAttributes("currentElement");
|
305 | }
|
306 | return currentParent as Element;
|
307 | }
|
308 |
|
309 |
|
310 |
|
311 |
|
312 | function currentPointer(): Node {
|
313 | if (DEBUG) {
|
314 | assertInPatch("currentPointer");
|
315 | assertNotInAttributes("currentPointer");
|
316 | }
|
317 |
|
318 | return getNextNode()!;
|
319 | }
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 | function skip() {
|
326 | if (DEBUG) {
|
327 | assertNoChildrenDeclaredYet("skip", currentNode);
|
328 | setInSkip(true);
|
329 | }
|
330 | currentNode = currentParent!.lastChild;
|
331 | }
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 | function createPatcher<T, R>(
|
341 | run: PatchFunction<T, R>,
|
342 | patchConfig: PatchConfig = {}
|
343 | ): PatchFunction<T, R> {
|
344 | const { matches = defaultMatchFn } = patchConfig;
|
345 |
|
346 | const f: PatchFunction<T, R> = (node, fn, data) => {
|
347 | const prevContext = context;
|
348 | const prevDoc = doc;
|
349 | const prevFocusPath = focusPath;
|
350 | const prevArgsBuilder = argsBuilder;
|
351 | const prevAttrsBuilder = attrsBuilder;
|
352 | const prevCurrentNode = currentNode;
|
353 | const prevCurrentParent = currentParent;
|
354 | const prevMatchFn = matchFn;
|
355 | let previousInAttributes = false;
|
356 | let previousInSkip = false;
|
357 |
|
358 | doc = node.ownerDocument;
|
359 | context = new Context();
|
360 | matchFn = matches;
|
361 | argsBuilder = [];
|
362 | attrsBuilder = [];
|
363 | currentNode = null;
|
364 | currentParent = node.parentNode;
|
365 | focusPath = getFocusedPath(node, currentParent);
|
366 |
|
367 | if (DEBUG) {
|
368 | previousInAttributes = setInAttributes(false);
|
369 | previousInSkip = setInSkip(false);
|
370 | updatePatchContext(context);
|
371 | }
|
372 |
|
373 | try {
|
374 | const retVal = run(node, fn, data);
|
375 | if (DEBUG) {
|
376 | assertVirtualAttributesClosed();
|
377 | }
|
378 |
|
379 | return retVal;
|
380 | } finally {
|
381 | context.notifyChanges();
|
382 |
|
383 | doc = prevDoc;
|
384 | context = prevContext;
|
385 | matchFn = prevMatchFn;
|
386 | argsBuilder = prevArgsBuilder;
|
387 | attrsBuilder = prevAttrsBuilder;
|
388 | currentNode = prevCurrentNode;
|
389 | currentParent = prevCurrentParent;
|
390 | focusPath = prevFocusPath;
|
391 |
|
392 |
|
393 |
|
394 | if (DEBUG) {
|
395 | setInAttributes(previousInAttributes);
|
396 | setInSkip(previousInSkip);
|
397 | updatePatchContext(context);
|
398 | }
|
399 | }
|
400 | };
|
401 | return f;
|
402 | }
|
403 |
|
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 | function createPatchInner<T>(
|
411 | patchConfig?: PatchConfig
|
412 | ): PatchFunction<T, Node> {
|
413 | return createPatcher((node, fn, data) => {
|
414 | currentNode = node;
|
415 |
|
416 | enterNode();
|
417 | fn(data);
|
418 | exitNode();
|
419 |
|
420 | if (DEBUG) {
|
421 | assertNoUnclosedTags(currentNode, node);
|
422 | }
|
423 |
|
424 | return node;
|
425 | }, patchConfig);
|
426 | }
|
427 |
|
428 |
|
429 |
|
430 |
|
431 |
|
432 |
|
433 |
|
434 | function createPatchOuter<T>(
|
435 | patchConfig?: PatchConfig
|
436 | ): PatchFunction<T, Node | null> {
|
437 | return createPatcher((node, fn, data) => {
|
438 | const startNode = ({ nextSibling: node } as any) as Element;
|
439 | let expectedNextNode: Node | null = null;
|
440 | let expectedPrevNode: Node | null = null;
|
441 |
|
442 | if (DEBUG) {
|
443 | expectedNextNode = node.nextSibling;
|
444 | expectedPrevNode = node.previousSibling;
|
445 | }
|
446 |
|
447 | currentNode = startNode;
|
448 | fn(data);
|
449 |
|
450 | if (DEBUG) {
|
451 | assertPatchOuterHasParentNode(currentParent);
|
452 | assertPatchElementNoExtras(
|
453 | startNode,
|
454 | currentNode,
|
455 | expectedNextNode,
|
456 | expectedPrevNode
|
457 | );
|
458 | }
|
459 |
|
460 | if (currentParent) {
|
461 | clearUnvisitedDOM(currentParent, getNextNode(), node.nextSibling);
|
462 | }
|
463 |
|
464 | return startNode === currentNode ? null : currentNode;
|
465 | }, patchConfig);
|
466 | }
|
467 |
|
468 | const patchInner: <T>(
|
469 | node: Element | DocumentFragment,
|
470 | template: (a: T | undefined) => void,
|
471 | data?: T | undefined
|
472 | ) => Node = createPatchInner();
|
473 | const patchOuter: <T>(
|
474 | node: Element | DocumentFragment,
|
475 | template: (a: T | undefined) => void,
|
476 | data?: T | undefined
|
477 | ) => Node | null = createPatchOuter();
|
478 |
|
479 | export {
|
480 | alignWithDOM,
|
481 | getArgsBuilder,
|
482 | getAttrsBuilder,
|
483 | text,
|
484 | createPatchInner,
|
485 | createPatchOuter,
|
486 | patchInner,
|
487 | patchOuter,
|
488 | open,
|
489 | close,
|
490 | currentElement,
|
491 | currentPointer,
|
492 | skip,
|
493 | nextNode as skipNode
|
494 | };
|