UNPKG

11.6 kBJavaScriptView Raw
1import {
2 RECYCLED_NODE,
3 TEXT_NODE,
4 XLINK_NS,
5 SVG_NS,
6 merge,
7 getType
8} from './utils'
9
10/**
11 * Event proxy for inline events.
12 * @param {Event} event
13 * @return {any} any
14 */
15function eventProxy(event) {
16 return event.currentTarget['events'][event.type](event)
17}
18
19/**
20 * Get the key value of a virtual node.
21 * @typedef {import('./vnode').VNode} VNode
22 * @param {VNode} node
23 * @return {string | number | null}
24 */
25function getKey(node) {
26 return node == null ? null : node.key
27}
28
29/**
30 * Create a map of keyed nodes.
31 * @typedef {import('./vnode').Children} Children
32 * @param {Children} children
33 * @param {number} start
34 * @param {number} end
35 * @return {Object.<string, any>} Object.<string, any>
36 */
37function createKeyMap(children, start, end) {
38 const out = {}
39 let key
40 let node
41
42 for (; start <= end; start++) {
43 if ((key = (node = children[start]).key) != null) {
44 out[key] = node
45 }
46 }
47
48 return out
49}
50
51/**
52 * Update the properties and attributes of a VNode based on new data.
53 * @param {Element} element
54 * @param {string} prop
55 * @param {any} oldValue
56 * @param {any} newValue
57 * @param {boolean} isSVG
58 * @return {void} undefined
59 */
60function setProp(element, prop, oldValue, newValue, isSVG) {
61 if (oldValue === newValue) return
62 if (prop === 'style' && getType(newValue) === 'Object') {
63 for (let i in merge(oldValue, newValue)) {
64 const style = newValue == null || newValue[i] == null ? '' : newValue[i]
65 if (i[0] === '-') {
66 element[prop].setProperty(i, style)
67 } else {
68 element[prop][i] = style
69 }
70 }
71 } else if (prop !== 'key') {
72 if (prop === 'className') prop = 'class'
73
74 if (prop[0] === 'o' && prop[1] === 'n') {
75 if (!element['events']) element['events'] = {}
76 prop = prop.slice(2).toLowerCase()
77 if (!oldValue) oldValue = element['events'][prop]
78 element['events'][prop] = newValue
79
80 if (newValue == null) {
81 element.removeEventListener(prop, eventProxy)
82 } else if (oldValue == null) {
83 element.addEventListener(prop, eventProxy)
84 }
85 } else {
86 const nullOrFalse =
87 newValue == null ||
88 newValue === false ||
89 newValue === 'no' ||
90 newValue === 'off'
91
92 if (
93 prop in element &&
94 prop !== 'list' &&
95 prop !== 'type' &&
96 prop !== 'draggable' &&
97 prop !== 'spellcheck' &&
98 prop !== 'translate' &&
99 !isSVG
100 ) {
101 element[prop] = newValue == null ? '' : newValue
102 if (nullOrFalse) {
103 element.removeAttribute(prop)
104 }
105 } else {
106 if (prop === 'xlink-href' || prop === 'xlinkHref') {
107 element.setAttributeNS(XLINK_NS, 'href', newValue)
108 element.setAttribute('href', newValue)
109 } else {
110 if (nullOrFalse) {
111 element.removeAttribute(prop)
112 } else {
113 element.setAttribute(prop, newValue)
114 }
115 }
116 }
117 }
118 }
119}
120
121/**
122 * Create an element, either node or text, from a VNode.
123 * @typedef {Function[]} Lifecycle
124 * @param {VNode} vnode
125 * @param {Lifecycle} lifecycle
126 * @param {boolean} [isSVG]
127 * @return {Element}
128 */
129export function createElement(vnode, lifecycle, isSVG) {
130 let element
131 if (vnode.flag === TEXT_NODE) {
132 element = document.createTextNode(/** @type {string} */ (vnode.type))
133 } else {
134 if ((isSVG = isSVG || vnode.type === 'svg')) {
135 element = document.createElementNS(
136 SVG_NS,
137 /** @type {string} */ (vnode.type)
138 )
139 } else {
140 element = document.createElement(/** @type {string} */ (vnode.type))
141 }
142 }
143
144 const props = vnode.props
145 if (props['onmount']) {
146 lifecycle.push(function() {
147 props['onmount'](element)
148 })
149 }
150
151 for (let i = 0, length = vnode.children.length; i < length; i++) {
152 element.appendChild(createElement(vnode.children[i], lifecycle, isSVG))
153 }
154
155 for (let prop in props) {
156 setProp(/** @type {Element} */ (element), prop, null, props[prop], isSVG)
157 }
158
159 return (vnode.element = /** @type {Element} */ (element))
160}
161
162/**
163 * Remove children from a node.
164 * @param {VNode} node
165 * @return {Element}
166 */
167function removeChildren(node) {
168 for (let i = 0, length = node.children.length; i < length; i++) {
169 removeChildren(node.children[i])
170 }
171 return node.element
172}
173
174/**
175 * Remove an element from the DOM.
176 * @param {Element} parent
177 * @param {VNode} vnode
178 * @return {void} undefined
179 */
180function removeElement(parent, vnode) {
181 function done() {
182 if (parent && parent.nodeType) parent.removeChild(removeChildren(vnode))
183 }
184
185 const cb = vnode.props && vnode.props['onunmount']
186 if (cb != null) {
187 cb(done, vnode.element)
188 } else {
189 done()
190 }
191}
192
193/**
194 * Update and element based on new prop values.
195 * @typedef {import('./vnode').Props} Props
196 * @param {Element} element
197 * @param {Props} oldProps
198 * @param {Props} newProps
199 * @param {Lifecycle} lifecycle
200 * @param {boolean} isSVG
201 * @param {boolean} isRecycled
202 * @return {void} undefined
203 */
204function updateElement(
205 element,
206 oldProps,
207 newProps,
208 lifecycle,
209 isSVG,
210 isRecycled
211) {
212 for (let prop in merge(oldProps, newProps)) {
213 if (
214 (prop === 'value' || prop === 'checked'
215 ? element[prop]
216 : oldProps[prop]) !== newProps[prop]
217 ) {
218 setProp(element, prop, oldProps[prop], newProps[prop], isSVG)
219 }
220 }
221
222 const cb = isRecycled ? newProps['onmount'] : newProps['onupdate']
223 if (cb != null) {
224 lifecycle.push(function() {
225 cb(element, oldProps, newProps)
226 })
227 }
228}
229
230/**
231 * Patch an element based on differences between its old VNode and its new one.
232 * @param {Element} parent
233 * @param {Element} element
234 * @param {VNode} oldVNode
235 * @param {VNode} newVNode
236 * @param {Lifecycle} lifecycle
237 * @param {boolean} [isSVG]
238 * @return {VNode}
239 */
240export function patchElement(
241 parent,
242 element,
243 oldVNode,
244 newVNode,
245 lifecycle,
246 isSVG
247) {
248 // Abort if vnodes are identical.
249 if (newVNode === oldVNode) {
250 } else if (
251 oldVNode != null &&
252 oldVNode.flag === TEXT_NODE &&
253 newVNode.flag === TEXT_NODE
254 ) {
255 if (oldVNode.type !== newVNode.type) {
256 element.nodeValue = /** @type {string} */ (newVNode.type)
257 }
258 } else if (oldVNode == null || oldVNode.type !== newVNode.type) {
259 const newElement = parent.insertBefore(
260 createElement(newVNode, lifecycle, isSVG),
261 element
262 )
263
264 if (oldVNode != null) removeElement(parent, oldVNode)
265
266 element = newElement
267 } else {
268 updateElement(
269 element,
270 oldVNode.props,
271 newVNode.props,
272 lifecycle,
273 (isSVG = isSVG || newVNode.type === 'svg'),
274 oldVNode.flag === RECYCLED_NODE
275 )
276
277 let savedNode
278 let childNode
279
280 let lastKey
281 const lastChildren = oldVNode.children
282 let lastChildStart = 0
283 let lastChildEnd = lastChildren.length - 1
284
285 let nextKey
286 const nextChildren = newVNode.children
287 let nextChildStart = 0
288 let nextChildEnd = nextChildren.length - 1
289
290 while (nextChildStart <= nextChildEnd && lastChildStart <= lastChildEnd) {
291 lastKey = getKey(lastChildren[lastChildStart])
292 nextKey = getKey(nextChildren[nextChildStart])
293
294 if (lastKey == null || lastKey !== nextKey) break
295
296 patchElement(
297 element,
298 lastChildren[lastChildStart].element,
299 lastChildren[lastChildStart],
300 nextChildren[nextChildStart],
301 lifecycle,
302 isSVG
303 )
304
305 lastChildStart++
306 nextChildStart++
307 }
308
309 while (nextChildStart <= nextChildEnd && lastChildStart <= lastChildEnd) {
310 lastKey = getKey(lastChildren[lastChildEnd])
311 nextKey = getKey(nextChildren[nextChildEnd])
312
313 if (lastKey == null || lastKey !== nextKey) break
314
315 patchElement(
316 element,
317 lastChildren[lastChildEnd].element,
318 lastChildren[lastChildEnd],
319 nextChildren[nextChildEnd],
320 lifecycle,
321 isSVG
322 )
323
324 lastChildEnd--
325 nextChildEnd--
326 }
327
328 if (lastChildStart > lastChildEnd) {
329 while (nextChildStart <= nextChildEnd) {
330 element.insertBefore(
331 createElement(nextChildren[nextChildStart++], lifecycle, isSVG),
332 (childNode = lastChildren[lastChildStart]) && childNode.element
333 )
334 }
335 } else if (nextChildStart > nextChildEnd) {
336 while (lastChildStart <= lastChildEnd) {
337 removeElement(element, lastChildren[lastChildStart++])
338 }
339 } else {
340 let lastKeyed = createKeyMap(lastChildren, lastChildStart, lastChildEnd)
341 const nextKeyed = {}
342
343 while (nextChildStart <= nextChildEnd) {
344 lastKey = getKey((childNode = lastChildren[lastChildStart]))
345 nextKey = getKey(nextChildren[nextChildStart])
346
347 if (
348 nextKeyed[lastKey] ||
349 (nextKey != null &&
350 nextKey === getKey(lastChildren[lastChildStart + 1]))
351 ) {
352 if (lastKey == null) {
353 removeElement(element, childNode)
354 }
355 lastChildStart++
356 continue
357 }
358
359 if (nextKey == null || oldVNode.flag === RECYCLED_NODE) {
360 if (lastKey == null) {
361 patchElement(
362 element,
363 childNode && childNode.element,
364 childNode,
365 nextChildren[nextChildStart],
366 lifecycle,
367 isSVG
368 )
369 nextChildStart++
370 }
371 lastChildStart++
372 } else {
373 if (lastKey === nextKey) {
374 patchElement(
375 element,
376 childNode.element,
377 childNode,
378 nextChildren[nextChildStart],
379 lifecycle,
380 isSVG
381 )
382 nextKeyed[nextKey] = true
383 lastChildStart++
384 } else {
385 if ((savedNode = lastKeyed[nextKey]) != null) {
386 patchElement(
387 element,
388 element.insertBefore(
389 savedNode.element,
390 childNode && childNode.element
391 ),
392 savedNode,
393 nextChildren[nextChildStart],
394 lifecycle,
395 isSVG
396 )
397 nextKeyed[nextKey] = true
398 } else {
399 patchElement(
400 element,
401 childNode && childNode.element,
402 null,
403 nextChildren[nextChildStart],
404 lifecycle,
405 isSVG
406 )
407 }
408 }
409 nextChildStart++
410 }
411 }
412
413 while (lastChildStart <= lastChildEnd) {
414 if (getKey((childNode = lastChildren[lastChildStart++])) == null) {
415 removeElement(element, childNode)
416 }
417 }
418
419 for (let key in lastKeyed) {
420 if (nextKeyed[key] == null) {
421 removeElement(element, lastKeyed[key])
422 }
423 }
424 }
425 }
426
427 newVNode.element = element
428 return newVNode
429}
430
431/**
432 * Function to either mount an element the first time or patch it in place. This behavior depends on the value of the old VNode. If it is null, a new element will be created, otherwise it compares the new VNode with the old one and patches it.
433 * @param {VNode} oldVNode
434 * @param {VNode} newVNode
435 * @param {Element | string} container
436 * @return {VNode} VNode
437 */
438export function patch(newVNode, oldVNode, container) {
439 if (typeof container === 'string')
440 container = document.querySelector(container)
441 const lifecycle = []
442
443 patchElement(container, container.children[0], oldVNode, newVNode, lifecycle)
444
445 if (newVNode !== oldVNode) {
446 while (lifecycle.length > 0) lifecycle.pop()()
447 }
448
449 return newVNode
450}