UNPKG

13.1 kBPlain TextView Raw
1import { Module } from "./modules/module";
2import { vnode, VNode } from "./vnode";
3import * as is from "./is";
4import { htmlDomApi, DOMAPI } from "./htmldomapi";
5
6type NonUndefined<T> = T extends undefined ? never : T;
7
8function isUndef(s: any): boolean {
9 return s === undefined;
10}
11function isDef<A>(s: A): s is NonUndefined<A> {
12 return s !== undefined;
13}
14
15type VNodeQueue = VNode[];
16
17const emptyNode = vnode("", {}, [], undefined, undefined);
18
19function 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 * @todo Remove this function when the document fragment is considered stable.
29 */
30function documentFragmentIsNotSupported(): never {
31 throw new Error("The document fragment is not supported on this platform.");
32}
33
34function isElement(
35 api: DOMAPI,
36 vnode: Element | DocumentFragment | VNode
37): vnode is Element {
38 return api.isElement(vnode as any);
39}
40
41function isDocumentFragment(
42 api: DOMAPI,
43 vnode: DocumentFragment | VNode
44): vnode is DocumentFragment {
45 return api.isDocumentFragment!(vnode as any);
46}
47
48type KeyToIndexMap = { [key: string]: number };
49
50type ArraysOf<T> = {
51 [K in keyof T]: Array<T[K]>;
52};
53
54type ModuleHooks = ArraysOf<Required<Module>>;
55
56function 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
71const hooks: Array<keyof Module> = [
72 "create",
73 "update",
74 "remove",
75 "destroy",
76 "pre",
77 "post",
78];
79
80// TODO Should `domApi` be put into this in the next major version bump?
81type Options = {
82 experimental?: {
83 fragments?: boolean;
84 };
85};
86
87export 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 // elm.className doesn't return a string when elm is an SVG element inside a shadowRoot.
116 // https://stackoverflow.com/questions/29454340/detecting-classname-of-svganimatedstring
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 // Parse selector
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 // Text node
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]; // Vnode might have been moved left
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 // Vnode moved right
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 // Vnode moved left
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 // New element
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}