UNPKG

9.95 kBJavaScriptView Raw
1// @ts-check
2import { XLINK_NS, SVG_NS, mixin } from './utils'
3
4/**
5 * Event proxy for inline events.
6 * @param {Event} event
7 * @return {any} any
8 */
9function eventProxy(event) {
10 return event.currentTarget['events'][event.type](event)
11}
12
13/**
14 * Function to get a node's key.
15 * @typedef {import('./vnode').VNode} VNode
16 * @param {VNode} node A virtual node.
17 * @return {string | number | null} key.
18 */
19const getKey = node => (node ? node.key : null)
20
21/**
22 * Function to set properties and attributes on element.
23 * @param {Element} element The element to set props on.
24 * @param {string} prop The property/attribute.
25 * @param {any} newValue The new value for the prop.
26 * @param {any} oldValue The original value of the prop.
27 * @param {boolean} isSVG Whether this is SVG or not
28 * @return {void} undefined
29 */
30function setProp(element, prop, newValue, oldValue, isSVG) {
31 // Do not add these as node attributes:
32 if (
33 prop === 'key' ||
34 prop === 'onmount' ||
35 prop === 'onupdate' ||
36 prop === 'onunmount' ||
37 prop === 'onComponentDidMount' ||
38 prop === 'onComponentDidUpdate' ||
39 prop === 'onComponentWillUnmount'
40 ) {
41 return
42 } else if (
43 prop === 'style' &&
44 typeof newValue === 'object' &&
45 !Array.isArray(newValue)
46 ) {
47 for (let i in mixin(oldValue, newValue)) {
48 const style = newValue == null || newValue[i] == null ? '' : newValue[i]
49 if (i[0] === '-') {
50 element[prop].setProperty(i, style)
51 } else {
52 element[prop][i] = style
53 }
54 }
55 } else {
56 // Convert camel case props to lower case:
57 prop = prop.toLowerCase()
58 if (prop[0] === 'o' && prop[1] === 'n') {
59 if (!element['events']) element['events'] = {}
60
61 element['events'][(prop = prop.slice(2))] = newValue
62
63 if (newValue == null) {
64 element.removeEventListener(prop, eventProxy)
65 } else if (oldValue == null) {
66 element.addEventListener(prop, eventProxy)
67 }
68 }
69
70 // Handle cases where 'className' is used:
71 if (prop === 'classname') {
72 prop = 'class'
73 }
74
75 // Allow setting innerHTML:
76 if (prop === 'dangerouslysetinnerhtml') {
77 element.innerHTML = newValue
78 }
79
80 if (prop in element && prop !== 'list' && !isSVG) {
81 element[prop] = newValue == (null || 'no') ? '' : newValue
82 } else if (
83 newValue != null &&
84 newValue !== 'null' &&
85 newValue !== 'false' &&
86 newValue !== 'no' &&
87 newValue !== 'off'
88 ) {
89 // Support SVG 'xlink:href' property:
90 if (prop === 'xlink-href') {
91 element.setAttributeNS(XLINK_NS, 'href', newValue)
92 element.setAttribute('href', newValue)
93 } else {
94 if (newValue === 'true') newValue = ''
95 // Set prop as attribute, except dangerouslySetInnerHTML:
96 if (prop !== 'dangerouslysetinnerhtml') {
97 element.setAttribute(prop, newValue)
98 }
99 }
100 }
101
102 if (
103 newValue == null ||
104 newValue === 'null' ||
105 newValue === 'undefined' ||
106 newValue === 'false' ||
107 newValue === 'no' ||
108 newValue === 'off'
109 ) {
110 element.removeAttribute(prop)
111 }
112 }
113}
114
115/**
116 * Function to convert hyperscript/JSX into DOM nodes.
117 * @param {string | number | Object} node A node to create. This may be a hyperscript function or a JSX tag which gets converted to hyperscript during transpilation.
118 * @param {boolean} [isSVG] Whether the node is SVG or not.
119 * @return {Node} An element created from a virtual dom object.
120 */
121function createElement(node, isSVG) {
122 let element
123 if (typeof node === 'number') node = node.toString()
124 if (typeof node === 'string') {
125 element = document.createTextNode(node)
126 } else if ((isSVG = isSVG || node.type === 'svg')) {
127 element = document.createElementNS(SVG_NS, node.type)
128 } else {
129 element = document.createElement(node.type)
130 }
131 /**
132 * @property {Object.<string, any>} node.props A virtual node stored on the node.
133 */
134 const props = node.props
135 if (props) {
136 for (let i = 0; i < node.children.length; i++) {
137 element.appendChild(createElement(node.children[i], isSVG))
138 }
139
140 for (let prop in props) {
141 setProp(element, prop, props[prop], null, isSVG)
142 }
143 }
144
145 return element
146}
147
148/**
149 * Function to remove element from DOM.
150 * @param {Node} parent The containing element in which the component resides.
151 * @param {Node} element The parent of the element to remove.
152 * @param {Element} node The element to remove.
153 * @return {void} undefined
154 */
155const removeElement = (parent, element, node) => {
156 const remove = function() {
157 parent.removeChild(element)
158 }
159 const callback =
160 (node['props'] && node['props']['onunmount']) ||
161 (node['props'] && node['props']['onComponentWillUnmount'])
162 if (callback != null) {
163 callback(element, remove)
164 } else {
165 remove()
166 }
167}
168
169/**
170 * @description A function to update an element based on a virtual dom node.
171 * @param {Element} element
172 * @param {Object.<string, any>} oldProps The original props used to create the element.
173 * @param {Object.<string, any>} newProps New props generated by the virtual dom.
174 * @param {boolean} isSVG Whether we are dealing with SVG or not.
175 * @function {function(element: Node, oldProps: VNode, props: VNode,isSVG: boolean): void}
176 * @return {void} undefined
177 */
178function updateElement(element, oldProps, newProps, isSVG) {
179 for (let prop in mixin(oldProps, newProps)) {
180 if (
181 newProps[prop] !==
182 (prop === 'value' || prop === 'checked' ? element[prop] : oldProps[prop])
183 ) {
184 setProp(element, prop, newProps[prop], oldProps[prop], isSVG)
185 }
186 }
187
188 // Handle lifecycle hook:
189 if (element['mounted'] && newProps && newProps.onComponentDidUpdate) {
190 newProps.onComponentDidUpdate.call(
191 newProps.onComponentDidUpdate,
192 oldProps,
193 newProps,
194 element
195 )
196 }
197 if (element['mounted'] && newProps && newProps.onupdate) {
198 newProps.onupdate.call(newProps.onupdate, oldProps, newProps, element)
199 }
200}
201
202/**
203 * A function to diff and patch a DOM node with a virtual node.
204 * @param {Node} parent The parent node of the elment being patched.
205 * @param {Element} element The element being patched.
206 * @param {Object} oldVNode A virtual dom node from the previous patch.
207 * @param {Object} newVNode The current virtual dom node.
208 * @param {boolean} [isSVG] Whether we are dealing with an SVG element or not.
209 * @return {Node} element The patched element.
210 */
211export function patchElement(parent, element, oldVNode, newVNode, isSVG) {
212 // Short circuit patch if VNodes are identical
213 if (newVNode === oldVNode) {
214 return
215 } else if (oldVNode == null || oldVNode.type !== newVNode.type) {
216 const newElement = createElement(newVNode, isSVG)
217 if (parent) {
218 parent.insertBefore(newElement, element)
219 if (oldVNode != null) {
220 removeElement(parent, element, oldVNode)
221 }
222 }
223 element = /** @type {Element} */ (newElement)
224 } else if (oldVNode.type == null) {
225 element.nodeValue = newVNode
226 } else {
227 updateElement(
228 element,
229 oldVNode.props,
230 newVNode.props,
231 (isSVG = isSVG || newVNode.type === 'svg')
232 )
233
234 const oldKeyed = {}
235 const newKeyed = {}
236 const oldElements = []
237 const oldChildren = oldVNode.children
238 const children = newVNode.children
239
240 for (let i = 0; i < oldChildren.length; i++) {
241 oldElements[i] = element.childNodes[i]
242
243 const oldKey = getKey(oldChildren[i])
244 if (oldKey != null) {
245 oldKeyed[oldKey] = [oldElements[i], oldChildren[i]]
246 }
247 }
248
249 let i = 0
250 let j = 0
251
252 while (j < children.length) {
253 let oldKey = getKey(oldChildren[i])
254 let newKey = getKey(children[j])
255
256 if (newKeyed[oldKey]) {
257 i++
258 continue
259 }
260
261 if (newKey != null && newKey === getKey(oldChildren[i + 1])) {
262 if (oldKey == null) {
263 removeElement(element, oldElements[i], oldChildren[i])
264 }
265 i++
266 continue
267 }
268
269 if (newKey == null) {
270 if (oldKey == null) {
271 patchElement(
272 element,
273 /** @type {any} */ (oldElements[i]),
274 oldChildren[i],
275 children[j],
276 isSVG
277 )
278 j++
279 }
280 i++
281 } else {
282 const keyedNode = oldKeyed[newKey] || []
283
284 if (oldKey === newKey) {
285 patchElement(element, keyedNode[0], keyedNode[1], children[j], isSVG)
286 i++
287 } else if (keyedNode[0]) {
288 patchElement(
289 element,
290 element.insertBefore(keyedNode[0], oldElements[i]),
291 keyedNode[1],
292 children[j],
293 isSVG
294 )
295 } else {
296 patchElement(
297 element,
298 /** @type {any} */ (oldElements[i]),
299 null,
300 children[j],
301 isSVG
302 )
303 }
304
305 newKeyed[newKey] = children[j]
306 j++
307 }
308 }
309
310 while (i < oldChildren.length) {
311 if (getKey(oldChildren[i]) == null) {
312 removeElement(element, oldElements[i], oldChildren[i])
313 }
314 i++
315 }
316 for (let k in oldKeyed) {
317 if (!newKeyed[k]) {
318 removeElement(element, oldKeyed[k][0], oldKeyed[k][1])
319 }
320 }
321 }
322 return element
323}
324
325/**
326 * A function to patch a virtual node against a DOM element, updating it in the most efficient manner possible.
327 * @param {() => VNode} newVNode A function that returns a virtual node. This may be a JSX tag, which gets converted into a function, or a hyperscript function.
328 * @param {Node} [element] The element to patch.
329 * @return {Node} The updated element.
330 */
331export function patch(newVNode, element) {
332 if (element) {
333 patchElement(
334 element.parentNode,
335 /** @type{Element} */ (element),
336 element && element['vnode'],
337 newVNode
338 )
339 } else {
340 element = patchElement(null, null, null, newVNode)
341 }
342
343 element['vnode'] = newVNode
344
345 return element
346}