1 | import { Module } from "./modules/module";
|
2 | import { vnode, VNode } from "./vnode";
|
3 | import * as is from "./is";
|
4 | import { htmlDomApi, DOMAPI } from "./htmldomapi";
|
5 |
|
6 | type NonUndefined<T> = T extends undefined ? never : T;
|
7 |
|
8 | function isUndef(s: any): boolean {
|
9 | return s === undefined;
|
10 | }
|
11 | function isDef<A>(s: A): s is NonUndefined<A> {
|
12 | return s !== undefined;
|
13 | }
|
14 |
|
15 | type VNodeQueue = VNode[];
|
16 |
|
17 | const emptyNode = vnode("", {}, [], undefined, undefined);
|
18 |
|
19 | function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
|
20 | const isSameKey = vnode1.key === vnode2.key;
|
21 | const isSameIs = vnode1.data?.is === vnode2.data?.is;
|
22 | const isSameSel = vnode1.sel === vnode2.sel;
|
23 |
|
24 | return isSameSel && isSameKey && isSameIs;
|
25 | }
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | function documentFragmentIsNotSupported(): never {
|
31 | throw new Error("The document fragment is not supported on this platform.");
|
32 | }
|
33 |
|
34 | function isElement(
|
35 | api: DOMAPI,
|
36 | vnode: Element | DocumentFragment | VNode
|
37 | ): vnode is Element {
|
38 | return api.isElement(vnode as any);
|
39 | }
|
40 |
|
41 | function isDocumentFragment(
|
42 | api: DOMAPI,
|
43 | vnode: DocumentFragment | VNode
|
44 | ): vnode is DocumentFragment {
|
45 | return api.isDocumentFragment!(vnode as any);
|
46 | }
|
47 |
|
48 | type KeyToIndexMap = { [key: string]: number };
|
49 |
|
50 | type ArraysOf<T> = {
|
51 | [K in keyof T]: Array<T[K]>;
|
52 | };
|
53 |
|
54 | type ModuleHooks = ArraysOf<Required<Module>>;
|
55 |
|
56 | function createKeyToOldIdx(
|
57 | children: VNode[],
|
58 | beginIdx: number,
|
59 | endIdx: number
|
60 | ): KeyToIndexMap {
|
61 | const map: KeyToIndexMap = {};
|
62 | for (let i = beginIdx; i <= endIdx; ++i) {
|
63 | const key = children[i]?.key;
|
64 | if (key !== undefined) {
|
65 | map[key as string] = i;
|
66 | }
|
67 | }
|
68 | return map;
|
69 | }
|
70 |
|
71 | const hooks: Array<keyof Module> = [
|
72 | "create",
|
73 | "update",
|
74 | "remove",
|
75 | "destroy",
|
76 | "pre",
|
77 | "post",
|
78 | ];
|
79 |
|
80 |
|
81 | type Options = {
|
82 | experimental?: {
|
83 | fragments?: boolean;
|
84 | };
|
85 | };
|
86 |
|
87 | export function init(
|
88 | modules: Array<Partial<Module>>,
|
89 | domApi?: DOMAPI,
|
90 | options?: Options
|
91 | ) {
|
92 | const cbs: ModuleHooks = {
|
93 | create: [],
|
94 | update: [],
|
95 | remove: [],
|
96 | destroy: [],
|
97 | pre: [],
|
98 | post: [],
|
99 | };
|
100 |
|
101 | const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
|
102 |
|
103 | for (const hook of hooks) {
|
104 | for (const module of modules) {
|
105 | const currentHook = module[hook];
|
106 | if (currentHook !== undefined) {
|
107 | (cbs[hook] as any[]).push(currentHook);
|
108 | }
|
109 | }
|
110 | }
|
111 |
|
112 | function emptyNodeAt(elm: Element) {
|
113 | const id = elm.id ? "#" + elm.id : "";
|
114 |
|
115 |
|
116 |
|
117 | const classes = elm.getAttribute("class");
|
118 |
|
119 | const c = classes ? "." + classes.split(" ").join(".") : "";
|
120 | return vnode(
|
121 | api.tagName(elm).toLowerCase() + id + c,
|
122 | {},
|
123 | [],
|
124 | undefined,
|
125 | elm
|
126 | );
|
127 | }
|
128 |
|
129 | function emptyDocumentFragmentAt(frag: DocumentFragment) {
|
130 | return vnode(undefined, {}, [], undefined, frag);
|
131 | }
|
132 |
|
133 | function createRmCb(childElm: Node, listeners: number) {
|
134 | return function rmCb() {
|
135 | if (--listeners === 0) {
|
136 | const parent = api.parentNode(childElm) as Node;
|
137 | api.removeChild(parent, childElm);
|
138 | }
|
139 | };
|
140 | }
|
141 |
|
142 | function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
|
143 | let i: any;
|
144 | let data = vnode.data;
|
145 | if (data !== undefined) {
|
146 | const init = data.hook?.init;
|
147 | if (isDef(init)) {
|
148 | init(vnode);
|
149 | data = vnode.data;
|
150 | }
|
151 | }
|
152 | const children = vnode.children;
|
153 | const sel = vnode.sel;
|
154 | if (sel === "!") {
|
155 | if (isUndef(vnode.text)) {
|
156 | vnode.text = "";
|
157 | }
|
158 | vnode.elm = api.createComment(vnode.text!);
|
159 | } else if (sel !== undefined) {
|
160 |
|
161 | const hashIdx = sel.indexOf("#");
|
162 | const dotIdx = sel.indexOf(".", hashIdx);
|
163 | const hash = hashIdx > 0 ? hashIdx : sel.length;
|
164 | const dot = dotIdx > 0 ? dotIdx : sel.length;
|
165 | const tag =
|
166 | hashIdx !== -1 || dotIdx !== -1
|
167 | ? sel.slice(0, Math.min(hash, dot))
|
168 | : sel;
|
169 | const elm = (vnode.elm =
|
170 | isDef(data) && isDef((i = data.ns))
|
171 | ? api.createElementNS(i, tag, data)
|
172 | : api.createElement(tag, data));
|
173 | if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
|
174 | if (dotIdx > 0)
|
175 | elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
|
176 | for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
|
177 | if (is.array(children)) {
|
178 | for (i = 0; i < children.length; ++i) {
|
179 | const ch = children[i];
|
180 | if (ch != null) {
|
181 | api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
|
182 | }
|
183 | }
|
184 | } else if (is.primitive(vnode.text)) {
|
185 | api.appendChild(elm, api.createTextNode(vnode.text));
|
186 | }
|
187 | const hook = vnode.data!.hook;
|
188 | if (isDef(hook)) {
|
189 | hook.create?.(emptyNode, vnode);
|
190 | if (hook.insert) {
|
191 | insertedVnodeQueue.push(vnode);
|
192 | }
|
193 | }
|
194 | } else if (options?.experimental?.fragments && vnode.children) {
|
195 | const children = vnode.children;
|
196 | vnode.elm = (
|
197 | api.createDocumentFragment ?? documentFragmentIsNotSupported
|
198 | )();
|
199 | for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
|
200 | for (i = 0; i < children.length; ++i) {
|
201 | const ch = children[i];
|
202 | if (ch != null) {
|
203 | api.appendChild(
|
204 | vnode.elm,
|
205 | createElm(ch as VNode, insertedVnodeQueue)
|
206 | );
|
207 | }
|
208 | }
|
209 | } else {
|
210 | vnode.elm = api.createTextNode(vnode.text!);
|
211 | }
|
212 | return vnode.elm;
|
213 | }
|
214 |
|
215 | function addVnodes(
|
216 | parentElm: Node,
|
217 | before: Node | null,
|
218 | vnodes: VNode[],
|
219 | startIdx: number,
|
220 | endIdx: number,
|
221 | insertedVnodeQueue: VNodeQueue
|
222 | ) {
|
223 | for (; startIdx <= endIdx; ++startIdx) {
|
224 | const ch = vnodes[startIdx];
|
225 | if (ch != null) {
|
226 | api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
|
227 | }
|
228 | }
|
229 | }
|
230 |
|
231 | function invokeDestroyHook(vnode: VNode) {
|
232 | const data = vnode.data;
|
233 | if (data !== undefined) {
|
234 | data?.hook?.destroy?.(vnode);
|
235 | for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
|
236 | if (vnode.children !== undefined) {
|
237 | for (let j = 0; j < vnode.children.length; ++j) {
|
238 | const child = vnode.children[j];
|
239 | if (child != null && typeof child !== "string") {
|
240 | invokeDestroyHook(child);
|
241 | }
|
242 | }
|
243 | }
|
244 | }
|
245 | }
|
246 |
|
247 | function removeVnodes(
|
248 | parentElm: Node,
|
249 | vnodes: VNode[],
|
250 | startIdx: number,
|
251 | endIdx: number
|
252 | ): void {
|
253 | for (; startIdx <= endIdx; ++startIdx) {
|
254 | let listeners: number;
|
255 | let rm: () => void;
|
256 | const ch = vnodes[startIdx];
|
257 | if (ch != null) {
|
258 | if (isDef(ch.sel)) {
|
259 | invokeDestroyHook(ch);
|
260 | listeners = cbs.remove.length + 1;
|
261 | rm = createRmCb(ch.elm!, listeners);
|
262 | for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
|
263 | const removeHook = ch?.data?.hook?.remove;
|
264 | if (isDef(removeHook)) {
|
265 | removeHook(ch, rm);
|
266 | } else {
|
267 | rm();
|
268 | }
|
269 | } else {
|
270 |
|
271 | api.removeChild(parentElm, ch.elm!);
|
272 | }
|
273 | }
|
274 | }
|
275 | }
|
276 |
|
277 | function updateChildren(
|
278 | parentElm: Node,
|
279 | oldCh: VNode[],
|
280 | newCh: VNode[],
|
281 | insertedVnodeQueue: VNodeQueue
|
282 | ) {
|
283 | let oldStartIdx = 0;
|
284 | let newStartIdx = 0;
|
285 | let oldEndIdx = oldCh.length - 1;
|
286 | let oldStartVnode = oldCh[0];
|
287 | let oldEndVnode = oldCh[oldEndIdx];
|
288 | let newEndIdx = newCh.length - 1;
|
289 | let newStartVnode = newCh[0];
|
290 | let newEndVnode = newCh[newEndIdx];
|
291 | let oldKeyToIdx: KeyToIndexMap | undefined;
|
292 | let idxInOld: number;
|
293 | let elmToMove: VNode;
|
294 | let before: any;
|
295 |
|
296 | while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
|
297 | if (oldStartVnode == null) {
|
298 | oldStartVnode = oldCh[++oldStartIdx];
|
299 | } else if (oldEndVnode == null) {
|
300 | oldEndVnode = oldCh[--oldEndIdx];
|
301 | } else if (newStartVnode == null) {
|
302 | newStartVnode = newCh[++newStartIdx];
|
303 | } else if (newEndVnode == null) {
|
304 | newEndVnode = newCh[--newEndIdx];
|
305 | } else if (sameVnode(oldStartVnode, newStartVnode)) {
|
306 | patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
|
307 | oldStartVnode = oldCh[++oldStartIdx];
|
308 | newStartVnode = newCh[++newStartIdx];
|
309 | } else if (sameVnode(oldEndVnode, newEndVnode)) {
|
310 | patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
|
311 | oldEndVnode = oldCh[--oldEndIdx];
|
312 | newEndVnode = newCh[--newEndIdx];
|
313 | } else if (sameVnode(oldStartVnode, newEndVnode)) {
|
314 |
|
315 | patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
|
316 | api.insertBefore(
|
317 | parentElm,
|
318 | oldStartVnode.elm!,
|
319 | api.nextSibling(oldEndVnode.elm!)
|
320 | );
|
321 | oldStartVnode = oldCh[++oldStartIdx];
|
322 | newEndVnode = newCh[--newEndIdx];
|
323 | } else if (sameVnode(oldEndVnode, newStartVnode)) {
|
324 |
|
325 | patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
|
326 | api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
|
327 | oldEndVnode = oldCh[--oldEndIdx];
|
328 | newStartVnode = newCh[++newStartIdx];
|
329 | } else {
|
330 | if (oldKeyToIdx === undefined) {
|
331 | oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
|
332 | }
|
333 | idxInOld = oldKeyToIdx[newStartVnode.key as string];
|
334 | if (isUndef(idxInOld)) {
|
335 |
|
336 | api.insertBefore(
|
337 | parentElm,
|
338 | createElm(newStartVnode, insertedVnodeQueue),
|
339 | oldStartVnode.elm!
|
340 | );
|
341 | } else {
|
342 | elmToMove = oldCh[idxInOld];
|
343 | if (elmToMove.sel !== newStartVnode.sel) {
|
344 | api.insertBefore(
|
345 | parentElm,
|
346 | createElm(newStartVnode, insertedVnodeQueue),
|
347 | oldStartVnode.elm!
|
348 | );
|
349 | } else {
|
350 | patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
|
351 | oldCh[idxInOld] = undefined as any;
|
352 | api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
|
353 | }
|
354 | }
|
355 | newStartVnode = newCh[++newStartIdx];
|
356 | }
|
357 | }
|
358 |
|
359 | if (newStartIdx <= newEndIdx) {
|
360 | before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
|
361 | addVnodes(
|
362 | parentElm,
|
363 | before,
|
364 | newCh,
|
365 | newStartIdx,
|
366 | newEndIdx,
|
367 | insertedVnodeQueue
|
368 | );
|
369 | }
|
370 | if (oldStartIdx <= oldEndIdx) {
|
371 | removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
|
372 | }
|
373 | }
|
374 |
|
375 | function patchVnode(
|
376 | oldVnode: VNode,
|
377 | vnode: VNode,
|
378 | insertedVnodeQueue: VNodeQueue
|
379 | ) {
|
380 | const hook = vnode.data?.hook;
|
381 | hook?.prepatch?.(oldVnode, vnode);
|
382 | const elm = (vnode.elm = oldVnode.elm)!;
|
383 | const oldCh = oldVnode.children as VNode[];
|
384 | const ch = vnode.children as VNode[];
|
385 | if (oldVnode === vnode) return;
|
386 | if (vnode.data !== undefined) {
|
387 | for (let i = 0; i < cbs.update.length; ++i)
|
388 | cbs.update[i](oldVnode, vnode);
|
389 | vnode.data.hook?.update?.(oldVnode, vnode);
|
390 | }
|
391 | if (isUndef(vnode.text)) {
|
392 | if (isDef(oldCh) && isDef(ch)) {
|
393 | if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
|
394 | } else if (isDef(ch)) {
|
395 | if (isDef(oldVnode.text)) api.setTextContent(elm, "");
|
396 | addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
|
397 | } else if (isDef(oldCh)) {
|
398 | removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
399 | } else if (isDef(oldVnode.text)) {
|
400 | api.setTextContent(elm, "");
|
401 | }
|
402 | } else if (oldVnode.text !== vnode.text) {
|
403 | if (isDef(oldCh)) {
|
404 | removeVnodes(elm, oldCh, 0, oldCh.length - 1);
|
405 | }
|
406 | api.setTextContent(elm, vnode.text!);
|
407 | }
|
408 | hook?.postpatch?.(oldVnode, vnode);
|
409 | }
|
410 |
|
411 | return function patch(
|
412 | oldVnode: VNode | Element | DocumentFragment,
|
413 | vnode: VNode
|
414 | ): VNode {
|
415 | let i: number, elm: Node, parent: Node;
|
416 | const insertedVnodeQueue: VNodeQueue = [];
|
417 | for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
|
418 |
|
419 | if (isElement(api, oldVnode)) {
|
420 | oldVnode = emptyNodeAt(oldVnode);
|
421 | } else if (isDocumentFragment(api, oldVnode)) {
|
422 | oldVnode = emptyDocumentFragmentAt(oldVnode);
|
423 | }
|
424 |
|
425 | if (sameVnode(oldVnode, vnode)) {
|
426 | patchVnode(oldVnode, vnode, insertedVnodeQueue);
|
427 | } else {
|
428 | elm = oldVnode.elm!;
|
429 | parent = api.parentNode(elm) as Node;
|
430 |
|
431 | createElm(vnode, insertedVnodeQueue);
|
432 |
|
433 | if (parent !== null) {
|
434 | api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
|
435 | removeVnodes(parent, [oldVnode], 0, 0);
|
436 | }
|
437 | }
|
438 |
|
439 | for (i = 0; i < insertedVnodeQueue.length; ++i) {
|
440 | insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
|
441 | }
|
442 | for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
|
443 | return vnode;
|
444 | };
|
445 | }
|