mithril-vndb/render/render.js
Isiah Meadows b98ab29efd
Make errors and their messages more accurate and helpful (#2536)
Also, I normalized them to all be sentences for consistency, and I moved
the reentrancy check from `m.mount` to `m.render` to be a little more
helpful. The router change during mounting is inconsequential and only
to avoid the new modified error, and the change to the update loop is to
send the original error if an error occurred while initializing the
default route. (This is all around more useful anyways.)

And while I was at it, I fixed an obscure bug with sync redraws.
2019-09-30 16:08:04 -04:00

980 lines
37 KiB
JavaScript

"use strict"
var Vnode = require("../render/vnode")
module.exports = function($window) {
var $doc = $window && $window.document
var currentRedraw
var nameSpace = {
svg: "http://www.w3.org/2000/svg",
math: "http://www.w3.org/1998/Math/MathML"
}
function getNameSpace(vnode) {
return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag]
}
//sanity check to discourage people from doing `vnode.state = ...`
function checkState(vnode, original) {
if (vnode.state !== original) throw new Error("'vnode.state' must not be modified.")
}
//Note: the hook is passed as the `this` argument to allow proxying the
//arguments without requiring a full array allocation to do so. It also
//takes advantage of the fact the current `vnode` is the first argument in
//all lifecycle methods.
function callHook(vnode) {
var original = vnode.state
try {
return this.apply(original, arguments)
} finally {
checkState(vnode, original)
}
}
// IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when
// inside an iframe. Catch and swallow this error, and heavy-handidly return null.
function activeElement() {
try {
return $doc.activeElement
} catch (e) {
return null
}
}
//create
function createNodes(parent, vnodes, start, end, hooks, nextSibling, ns) {
for (var i = start; i < end; i++) {
var vnode = vnodes[i]
if (vnode != null) {
createNode(parent, vnode, hooks, ns, nextSibling)
}
}
}
function createNode(parent, vnode, hooks, ns, nextSibling) {
var tag = vnode.tag
if (typeof tag === "string") {
vnode.state = {}
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
switch (tag) {
case "#": createText(parent, vnode, nextSibling); break
case "<": createHTML(parent, vnode, ns, nextSibling); break
case "[": createFragment(parent, vnode, hooks, ns, nextSibling); break
default: createElement(parent, vnode, hooks, ns, nextSibling)
}
}
else createComponent(parent, vnode, hooks, ns, nextSibling)
}
function createText(parent, vnode, nextSibling) {
vnode.dom = $doc.createTextNode(vnode.children)
insertNode(parent, vnode.dom, nextSibling)
}
var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}
function createHTML(parent, vnode, ns, nextSibling) {
var match = vnode.children.match(/^\s*?<(\w+)/im) || []
// not using the proper parent makes the child element(s) vanish.
// var div = document.createElement("div")
// div.innerHTML = "<td>i</td><td>j</td>"
// console.log(div.innerHTML)
// --> "ij", no <td> in sight.
var temp = $doc.createElement(possibleParents[match[1]] || "div")
if (ns === "http://www.w3.org/2000/svg") {
temp.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\">" + vnode.children + "</svg>"
temp = temp.firstChild
} else {
temp.innerHTML = vnode.children
}
vnode.dom = temp.firstChild
vnode.domSize = temp.childNodes.length
// Capture nodes to remove, so we don't confuse them.
vnode.instance = []
var fragment = $doc.createDocumentFragment()
var child
while (child = temp.firstChild) {
vnode.instance.push(child)
fragment.appendChild(child)
}
insertNode(parent, fragment, nextSibling)
}
function createFragment(parent, vnode, hooks, ns, nextSibling) {
var fragment = $doc.createDocumentFragment()
if (vnode.children != null) {
var children = vnode.children
createNodes(fragment, children, 0, children.length, hooks, null, ns)
}
vnode.dom = fragment.firstChild
vnode.domSize = fragment.childNodes.length
insertNode(parent, fragment, nextSibling)
}
function createElement(parent, vnode, hooks, ns, nextSibling) {
var tag = vnode.tag
var attrs = vnode.attrs
var is = attrs && attrs.is
ns = getNameSpace(vnode) || ns
var element = ns ?
is ? $doc.createElementNS(ns, tag, {is: is}) : $doc.createElementNS(ns, tag) :
is ? $doc.createElement(tag, {is: is}) : $doc.createElement(tag)
vnode.dom = element
if (attrs != null) {
setAttrs(vnode, attrs, ns)
}
insertNode(parent, element, nextSibling)
if (!maybeSetContentEditable(vnode)) {
if (vnode.text != null) {
if (vnode.text !== "") element.textContent = vnode.text
else vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)]
}
if (vnode.children != null) {
var children = vnode.children
createNodes(element, children, 0, children.length, hooks, null, ns)
if (vnode.tag === "select" && attrs != null) setLateSelectAttrs(vnode, attrs)
}
}
}
function initComponent(vnode, hooks) {
var sentinel
if (typeof vnode.tag.view === "function") {
vnode.state = Object.create(vnode.tag)
sentinel = vnode.state.view
if (sentinel.$$reentrantLock$$ != null) return
sentinel.$$reentrantLock$$ = true
} else {
vnode.state = void 0
sentinel = vnode.tag
if (sentinel.$$reentrantLock$$ != null) return
sentinel.$$reentrantLock$$ = true
vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode)
}
initLifecycle(vnode.state, vnode, hooks)
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
sentinel.$$reentrantLock$$ = null
}
function createComponent(parent, vnode, hooks, ns, nextSibling) {
initComponent(vnode, hooks)
if (vnode.instance != null) {
createNode(parent, vnode.instance, hooks, ns, nextSibling)
vnode.dom = vnode.instance.dom
vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0
}
else {
vnode.domSize = 0
}
}
//update
/**
* @param {Element|Fragment} parent - the parent element
* @param {Vnode[] | null} old - the list of vnodes of the last `render()` call for
* this part of the tree
* @param {Vnode[] | null} vnodes - as above, but for the current `render()` call.
* @param {Function[]} hooks - an accumulator of post-render hooks (oncreate/onupdate)
* @param {Element | null} nextSibling - the next DOM node if we're dealing with a
* fragment that is not the last item in its
* parent
* @param {'svg' | 'math' | String | null} ns) - the current XML namespace, if any
* @returns void
*/
// This function diffs and patches lists of vnodes, both keyed and unkeyed.
//
// We will:
//
// 1. describe its general structure
// 2. focus on the diff algorithm optimizations
// 3. discuss DOM node operations.
// ## Overview:
//
// The updateNodes() function:
// - deals with trivial cases
// - determines whether the lists are keyed or unkeyed based on the first non-null node
// of each list.
// - diffs them and patches the DOM if needed (that's the brunt of the code)
// - manages the leftovers: after diffing, are there:
// - old nodes left to remove?
// - new nodes to insert?
// deal with them!
//
// The lists are only iterated over once, with an exception for the nodes in `old` that
// are visited in the fourth part of the diff and in the `removeNodes` loop.
// ## Diffing
//
// Reading https://github.com/localvoid/ivi/blob/ddc09d06abaef45248e6133f7040d00d3c6be853/packages/ivi/src/vdom/implementation.ts#L617-L837
// may be good for context on longest increasing subsequence-based logic for moving nodes.
//
// In order to diff keyed lists, one has to
//
// 1) match nodes in both lists, per key, and update them accordingly
// 2) create the nodes present in the new list, but absent in the old one
// 3) remove the nodes present in the old list, but absent in the new one
// 4) figure out what nodes in 1) to move in order to minimize the DOM operations.
//
// To achieve 1) one can create a dictionary of keys => index (for the old list), then iterate
// over the new list and for each new vnode, find the corresponding vnode in the old list using
// the map.
// 2) is achieved in the same step: if a new node has no corresponding entry in the map, it is new
// and must be created.
// For the removals, we actually remove the nodes that have been updated from the old list.
// The nodes that remain in that list after 1) and 2) have been performed can be safely removed.
// The fourth step is a bit more complex and relies on the longest increasing subsequence (LIS)
// algorithm.
//
// the longest increasing subsequence is the list of nodes that can remain in place. Imagine going
// from `1,2,3,4,5` to `4,5,1,2,3` where the numbers are not necessarily the keys, but the indices
// corresponding to the keyed nodes in the old list (keyed nodes `e,d,c,b,a` => `b,a,e,d,c` would
// match the above lists, for example).
//
// In there are two increasing subsequences: `4,5` and `1,2,3`, the latter being the longest. We
// can update those nodes without moving them, and only call `insertNode` on `4` and `5`.
//
// @localvoid adapted the algo to also support node deletions and insertions (the `lis` is actually
// the longest increasing subsequence *of old nodes still present in the new list*).
//
// It is a general algorithm that is fireproof in all circumstances, but it requires the allocation
// and the construction of a `key => oldIndex` map, and three arrays (one with `newIndex => oldIndex`,
// the `LIS` and a temporary one to create the LIS).
//
// So we cheat where we can: if the tails of the lists are identical, they are guaranteed to be part of
// the LIS and can be updated without moving them.
//
// If two nodes are swapped, they are guaranteed not to be part of the LIS, and must be moved (with
// the exception of the last node if the list is fully reversed).
//
// ## Finding the next sibling.
//
// `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations.
// When the list is being traversed top-down, at any index, the DOM nodes up to the previous
// vnode reflect the content of the new list, whereas the rest of the DOM nodes reflect the old
// list. The next sibling must be looked for in the old list using `getNextSibling(... oldStart + 1 ...)`.
//
// In the other scenarios (swaps, upwards traversal, map-based diff),
// the new vnodes list is traversed upwards. The DOM nodes at the bottom of the list reflect the
// bottom part of the new vnodes list, and we can use the `v.dom` value of the previous node
// as the next sibling (cached in the `nextSibling` variable).
// ## DOM node moves
//
// In most scenarios `updateNode()` and `createNode()` perform the DOM operations. However,
// this is not the case if the node moved (second and fourth part of the diff algo). We move
// the old DOM nodes before updateNode runs because it enables us to use the cached `nextSibling`
// variable rather than fetching it using `getNextSibling()`.
//
// The fourth part of the diff currently inserts nodes unconditionally, leading to issues
// like #1791 and #1999. We need to be smarter about those situations where adjascent old
// nodes remain together in the new list in a way that isn't covered by parts one and
// three of the diff algo.
function updateNodes(parent, old, vnodes, hooks, nextSibling, ns) {
if (old === vnodes || old == null && vnodes == null) return
else if (old == null || old.length === 0) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length)
else {
var isOldKeyed = old[0] != null && old[0].key != null
var isKeyed = vnodes[0] != null && vnodes[0].key != null
var start = 0, oldStart = 0
if (!isOldKeyed) while (oldStart < old.length && old[oldStart] == null) oldStart++
if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++
if (isKeyed === null && isOldKeyed == null) return // both lists are full of nulls
if (isOldKeyed !== isKeyed) {
removeNodes(parent, old, oldStart, old.length)
createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
} else if (!isKeyed) {
// Don't index past the end of either list (causes deopts).
var commonLength = old.length < vnodes.length ? old.length : vnodes.length
// Rewind if necessary to the first non-null index on either side.
// We could alternatively either explicitly create or remove nodes when `start !== oldStart`
// but that would be optimizing for sparse lists which are more rare than dense ones.
start = start < oldStart ? start : oldStart
for (; start < commonLength; start++) {
o = old[start]
v = vnodes[start]
if (o === v || o == null && v == null) continue
else if (o == null) createNode(parent, v, hooks, ns, getNextSibling(old, start + 1, nextSibling))
else if (v == null) removeNode(parent, o)
else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns)
}
if (old.length > commonLength) removeNodes(parent, old, start, old.length)
if (vnodes.length > commonLength) createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns)
} else {
// keyed diff
var oldEnd = old.length - 1, end = vnodes.length - 1, map, o, v, oe, ve, topSibling
// bottom-up
while (oldEnd >= oldStart && end >= start) {
oe = old[oldEnd]
ve = vnodes[end]
if (oe.key !== ve.key) break
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
if (ve.dom != null) nextSibling = ve.dom
oldEnd--, end--
}
// top-down
while (oldEnd >= oldStart && end >= start) {
o = old[oldStart]
v = vnodes[start]
if (o.key !== v.key) break
oldStart++, start++
if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns)
}
// swaps and list reversals
while (oldEnd >= oldStart && end >= start) {
if (start === end) break
if (o.key !== ve.key || oe.key !== v.key) break
topSibling = getNextSibling(old, oldStart, nextSibling)
moveNodes(parent, oe, topSibling)
if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns)
if (++start <= --end) moveNodes(parent, o, nextSibling)
if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns)
if (ve.dom != null) nextSibling = ve.dom
oldStart++; oldEnd--
oe = old[oldEnd]
ve = vnodes[end]
o = old[oldStart]
v = vnodes[start]
}
// bottom up once again
while (oldEnd >= oldStart && end >= start) {
if (oe.key !== ve.key) break
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
if (ve.dom != null) nextSibling = ve.dom
oldEnd--, end--
oe = old[oldEnd]
ve = vnodes[end]
}
if (start > end) removeNodes(parent, old, oldStart, oldEnd + 1)
else if (oldStart > oldEnd) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
else {
// inspired by ivi https://github.com/ivijs/ivi/ by Boris Kaul
var originalNextSibling = nextSibling, vnodesLength = end - start + 1, oldIndices = new Array(vnodesLength), li=0, i=0, pos = 2147483647, matched = 0, map, lisIndices
for (i = 0; i < vnodesLength; i++) oldIndices[i] = -1
for (i = end; i >= start; i--) {
if (map == null) map = getKeyMap(old, oldStart, oldEnd + 1)
ve = vnodes[i]
var oldIndex = map[ve.key]
if (oldIndex != null) {
pos = (oldIndex < pos) ? oldIndex : -1 // becomes -1 if nodes were re-ordered
oldIndices[i-start] = oldIndex
oe = old[oldIndex]
old[oldIndex] = null
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns)
if (ve.dom != null) nextSibling = ve.dom
matched++
}
}
nextSibling = originalNextSibling
if (matched !== oldEnd - oldStart + 1) removeNodes(parent, old, oldStart, oldEnd + 1)
if (matched === 0) createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
else {
if (pos === -1) {
// the indices of the indices of the items that are part of the
// longest increasing subsequence in the oldIndices list
lisIndices = makeLisIndices(oldIndices)
li = lisIndices.length - 1
for (i = end; i >= start; i--) {
v = vnodes[i]
if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling)
else {
if (lisIndices[li] === i - start) li--
else moveNodes(parent, v, nextSibling)
}
if (v.dom != null) nextSibling = vnodes[i].dom
}
} else {
for (i = end; i >= start; i--) {
v = vnodes[i]
if (oldIndices[i-start] === -1) createNode(parent, v, hooks, ns, nextSibling)
if (v.dom != null) nextSibling = vnodes[i].dom
}
}
}
}
}
}
}
function updateNode(parent, old, vnode, hooks, nextSibling, ns) {
var oldTag = old.tag, tag = vnode.tag
if (oldTag === tag) {
vnode.state = old.state
vnode.events = old.events
if (shouldNotUpdate(vnode, old)) return
if (typeof oldTag === "string") {
if (vnode.attrs != null) {
updateLifecycle(vnode.attrs, vnode, hooks)
}
switch (oldTag) {
case "#": updateText(old, vnode); break
case "<": updateHTML(parent, old, vnode, ns, nextSibling); break
case "[": updateFragment(parent, old, vnode, hooks, nextSibling, ns); break
default: updateElement(old, vnode, hooks, ns)
}
}
else updateComponent(parent, old, vnode, hooks, nextSibling, ns)
}
else {
removeNode(parent, old)
createNode(parent, vnode, hooks, ns, nextSibling)
}
}
function updateText(old, vnode) {
if (old.children.toString() !== vnode.children.toString()) {
old.dom.nodeValue = vnode.children
}
vnode.dom = old.dom
}
function updateHTML(parent, old, vnode, ns, nextSibling) {
if (old.children !== vnode.children) {
removeHTML(parent, old)
createHTML(parent, vnode, ns, nextSibling)
}
else {
vnode.dom = old.dom
vnode.domSize = old.domSize
vnode.instance = old.instance
}
}
function updateFragment(parent, old, vnode, hooks, nextSibling, ns) {
updateNodes(parent, old.children, vnode.children, hooks, nextSibling, ns)
var domSize = 0, children = vnode.children
vnode.dom = null
if (children != null) {
for (var i = 0; i < children.length; i++) {
var child = children[i]
if (child != null && child.dom != null) {
if (vnode.dom == null) vnode.dom = child.dom
domSize += child.domSize || 1
}
}
if (domSize !== 1) vnode.domSize = domSize
}
}
function updateElement(old, vnode, hooks, ns) {
var element = vnode.dom = old.dom
ns = getNameSpace(vnode) || ns
if (vnode.tag === "textarea") {
if (vnode.attrs == null) vnode.attrs = {}
if (vnode.text != null) {
vnode.attrs.value = vnode.text //FIXME handle multiple children
vnode.text = undefined
}
}
updateAttrs(vnode, old.attrs, vnode.attrs, ns)
if (!maybeSetContentEditable(vnode)) {
if (old.text != null && vnode.text != null && vnode.text !== "") {
if (old.text.toString() !== vnode.text.toString()) old.dom.firstChild.nodeValue = vnode.text
}
else {
if (old.text != null) old.children = [Vnode("#", undefined, undefined, old.text, undefined, old.dom.firstChild)]
if (vnode.text != null) vnode.children = [Vnode("#", undefined, undefined, vnode.text, undefined, undefined)]
updateNodes(element, old.children, vnode.children, hooks, null, ns)
}
}
}
function updateComponent(parent, old, vnode, hooks, nextSibling, ns) {
vnode.instance = Vnode.normalize(callHook.call(vnode.state.view, vnode))
if (vnode.instance === vnode) throw Error("A view cannot return the vnode it received as argument")
updateLifecycle(vnode.state, vnode, hooks)
if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
if (vnode.instance != null) {
if (old.instance == null) createNode(parent, vnode.instance, hooks, ns, nextSibling)
else updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, ns)
vnode.dom = vnode.instance.dom
vnode.domSize = vnode.instance.domSize
}
else if (old.instance != null) {
removeNode(parent, old.instance)
vnode.dom = undefined
vnode.domSize = 0
}
else {
vnode.dom = old.dom
vnode.domSize = old.domSize
}
}
function getKeyMap(vnodes, start, end) {
var map = Object.create(null)
for (; start < end; start++) {
var vnode = vnodes[start]
if (vnode != null) {
var key = vnode.key
if (key != null) map[key] = start
}
}
return map
}
// Lifted from ivi https://github.com/ivijs/ivi/
// takes a list of unique numbers (-1 is special and can
// occur multiple times) and returns an array with the indices
// of the items that are part of the longest increasing
// subsequece
var lisTemp = []
function makeLisIndices(a) {
var result = [0]
var u = 0, v = 0, i = 0
var il = lisTemp.length = a.length
for (var i = 0; i < il; i++) lisTemp[i] = a[i]
for (var i = 0; i < il; ++i) {
if (a[i] === -1) continue
var j = result[result.length - 1]
if (a[j] < a[i]) {
lisTemp[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
while (u < v) {
// Fast integer average without overflow.
// eslint-disable-next-line no-bitwise
var c = (u >>> 1) + (v >>> 1) + (u & v & 1)
if (a[result[c]] < a[i]) {
u = c + 1
}
else {
v = c
}
}
if (a[i] < a[result[u]]) {
if (u > 0) lisTemp[i] = result[u - 1]
result[u] = i
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = lisTemp[v]
}
lisTemp.length = 0
return result
}
function getNextSibling(vnodes, i, nextSibling) {
for (; i < vnodes.length; i++) {
if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom
}
return nextSibling
}
// This covers a really specific edge case:
// - Parent node is keyed and contains child
// - Child is removed, returns unresolved promise in `onbeforeremove`
// - Parent node is moved in keyed diff
// - Remaining children still need moved appropriately
//
// Ideally, I'd track removed nodes as well, but that introduces a lot more
// complexity and I'm not exactly interested in doing that.
function moveNodes(parent, vnode, nextSibling) {
var frag = $doc.createDocumentFragment()
moveChildToFrag(parent, frag, vnode)
insertNode(parent, frag, nextSibling)
}
function moveChildToFrag(parent, frag, vnode) {
// Dodge the recursion overhead in a few of the most common cases.
while (vnode.dom != null && vnode.dom.parentNode === parent) {
if (typeof vnode.tag !== "string") {
vnode = vnode.instance
if (vnode != null) continue
} else if (vnode.tag === "<") {
for (var i = 0; i < vnode.instance.length; i++) {
frag.appendChild(vnode.instance[i])
}
} else if (vnode.tag !== "[") {
// Don't recurse for text nodes *or* elements, just fragments
frag.appendChild(vnode.dom)
} else if (vnode.children.length === 1) {
vnode = vnode.children[0]
if (vnode != null) continue
} else {
for (var i = 0; i < vnode.children.length; i++) {
var child = vnode.children[i]
if (child != null) moveChildToFrag(parent, frag, child)
}
}
break
}
}
function insertNode(parent, dom, nextSibling) {
if (nextSibling != null) parent.insertBefore(dom, nextSibling)
else parent.appendChild(dom)
}
function maybeSetContentEditable(vnode) {
if (vnode.attrs == null || (
vnode.attrs.contenteditable == null && // attribute
vnode.attrs.contentEditable == null // property
)) return false
var children = vnode.children
if (children != null && children.length === 1 && children[0].tag === "<") {
var content = children[0].children
if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content
}
else if (vnode.text != null || children != null && children.length !== 0) throw new Error("Child node of a contenteditable must be trusted.")
return true
}
//remove
function removeNodes(parent, vnodes, start, end) {
for (var i = start; i < end; i++) {
var vnode = vnodes[i]
if (vnode != null) removeNode(parent, vnode)
}
}
function removeNode(parent, vnode) {
var mask = 0
var original = vnode.state
var stateResult, attrsResult
if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeremove === "function") {
var result = callHook.call(vnode.state.onbeforeremove, vnode)
if (result != null && typeof result.then === "function") {
mask = 1
stateResult = result
}
}
if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") {
var result = callHook.call(vnode.attrs.onbeforeremove, vnode)
if (result != null && typeof result.then === "function") {
// eslint-disable-next-line no-bitwise
mask |= 2
attrsResult = result
}
}
checkState(vnode, original)
// If we can, try to fast-path it and avoid all the overhead of awaiting
if (!mask) {
onremove(vnode)
removeChild(parent, vnode)
} else {
if (stateResult != null) {
var next = function () {
// eslint-disable-next-line no-bitwise
if (mask & 1) { mask &= 2; if (!mask) reallyRemove() }
}
stateResult.then(next, next)
}
if (attrsResult != null) {
var next = function () {
// eslint-disable-next-line no-bitwise
if (mask & 2) { mask &= 1; if (!mask) reallyRemove() }
}
attrsResult.then(next, next)
}
}
function reallyRemove() {
checkState(vnode, original)
onremove(vnode)
removeChild(parent, vnode)
}
}
function removeHTML(parent, vnode) {
for (var i = 0; i < vnode.instance.length; i++) {
parent.removeChild(vnode.instance[i])
}
}
function removeChild(parent, vnode) {
// Dodge the recursion overhead in a few of the most common cases.
while (vnode.dom != null && vnode.dom.parentNode === parent) {
if (typeof vnode.tag !== "string") {
vnode = vnode.instance
if (vnode != null) continue
} else if (vnode.tag === "<") {
removeHTML(parent, vnode)
} else {
if (vnode.tag !== "[") {
parent.removeChild(vnode.dom)
if (!Array.isArray(vnode.children)) break
}
if (vnode.children.length === 1) {
vnode = vnode.children[0]
if (vnode != null) continue
} else {
for (var i = 0; i < vnode.children.length; i++) {
var child = vnode.children[i]
if (child != null) removeChild(parent, child)
}
}
}
break
}
}
function onremove(vnode) {
if (typeof vnode.tag !== "string" && typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode)
if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode)
if (typeof vnode.tag !== "string") {
if (vnode.instance != null) onremove(vnode.instance)
} else {
var children = vnode.children
if (Array.isArray(children)) {
for (var i = 0; i < children.length; i++) {
var child = children[i]
if (child != null) onremove(child)
}
}
}
}
//attrs
function setAttrs(vnode, attrs, ns) {
for (var key in attrs) {
setAttr(vnode, key, null, attrs[key], ns)
}
}
function setAttr(vnode, key, old, value, ns) {
if (key === "key" || key === "is" || value == null || isLifecycleMethod(key) || (old === value && !isFormAttribute(vnode, key)) && typeof value !== "object") return
if (key[0] === "o" && key[1] === "n") return updateEvent(vnode, key, value)
if (key.slice(0, 6) === "xlink:") vnode.dom.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value)
else if (key === "style") updateStyle(vnode.dom, old, value)
else if (hasPropertyKey(vnode, key, ns)) {
if (key === "value") {
// Only do the coercion if we're actually going to check the value.
/* eslint-disable no-implicit-coercion */
//setting input[value] to same value by typing on focused element moves cursor to end in Chrome
if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value && vnode.dom === activeElement()) return
//setting select[value] to same value while having select open blinks select dropdown in Chrome
if (vnode.tag === "select" && old !== null && vnode.dom.value === "" + value) return
//setting option[value] to same value while having select open blinks select dropdown in Chrome
if (vnode.tag === "option" && old !== null && vnode.dom.value === "" + value) return
/* eslint-enable no-implicit-coercion */
}
// If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur.
if (vnode.tag === "input" && key === "type") vnode.dom.setAttribute(key, value)
else vnode.dom[key] = value
} else {
if (typeof value === "boolean") {
if (value) vnode.dom.setAttribute(key, "")
else vnode.dom.removeAttribute(key)
}
else vnode.dom.setAttribute(key === "className" ? "class" : key, value)
}
}
function removeAttr(vnode, key, old, ns) {
if (key === "key" || key === "is" || old == null || isLifecycleMethod(key)) return
if (key[0] === "o" && key[1] === "n" && !isLifecycleMethod(key)) updateEvent(vnode, key, undefined)
else if (key === "style") updateStyle(vnode.dom, old, null)
else if (
hasPropertyKey(vnode, key, ns)
&& key !== "className"
&& !(key === "value" && (
vnode.tag === "option"
|| vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement()
))
&& !(vnode.tag === "input" && key === "type")
) {
vnode.dom[key] = null
} else {
var nsLastIndex = key.indexOf(":")
if (nsLastIndex !== -1) key = key.slice(nsLastIndex + 1)
if (old !== false) vnode.dom.removeAttribute(key === "className" ? "class" : key)
}
}
function setLateSelectAttrs(vnode, attrs) {
if ("value" in attrs) {
if(attrs.value === null) {
if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null
} else {
var normalized = "" + attrs.value // eslint-disable-line no-implicit-coercion
if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) {
vnode.dom.value = normalized
}
}
}
if ("selectedIndex" in attrs) setAttr(vnode, "selectedIndex", null, attrs.selectedIndex, undefined)
}
function updateAttrs(vnode, old, attrs, ns) {
if (attrs != null) {
for (var key in attrs) {
setAttr(vnode, key, old && old[key], attrs[key], ns)
}
}
var val
if (old != null) {
for (var key in old) {
if (((val = old[key]) != null) && (attrs == null || attrs[key] == null)) {
removeAttr(vnode, key, val, ns)
}
}
}
}
function isFormAttribute(vnode, attr) {
return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === activeElement() || vnode.tag === "option" && vnode.dom.parentNode === $doc.activeElement
}
function isLifecycleMethod(attr) {
return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate"
}
function hasPropertyKey(vnode, key, ns) {
// Filter out namespaced keys
return ns === undefined && (
// If it's a custom element, just keep it.
vnode.tag.indexOf("-") > -1 || vnode.attrs != null && vnode.attrs.is ||
// If it's a normal element, let's try to avoid a few browser bugs.
key !== "href" && key !== "list" && key !== "form" && key !== "width" && key !== "height"// && key !== "type"
// Defer the property check until *after* we check everything.
) && key in vnode.dom
}
//style
var uppercaseRegex = /[A-Z]/g
function toLowerCase(capital) { return "-" + capital.toLowerCase() }
function normalizeKey(key) {
return key[0] === "-" && key[1] === "-" ? key :
key === "cssFloat" ? "float" :
key.replace(uppercaseRegex, toLowerCase)
}
function updateStyle(element, old, style) {
if (old === style) {
// Styles are equivalent, do nothing.
} else if (style == null) {
// New style is missing, just clear it.
element.style.cssText = ""
} else if (typeof style !== "object") {
// New style is a string, let engine deal with patching.
element.style.cssText = style
} else if (old == null || typeof old !== "object") {
// `old` is missing or a string, `style` is an object.
element.style.cssText = ""
// Add new style properties
for (var key in style) {
var value = style[key]
if (value != null) element.style.setProperty(normalizeKey(key), String(value))
}
} else {
// Both old & new are (different) objects.
// Update style properties that have changed
for (var key in style) {
var value = style[key]
if (value != null && (value = String(value)) !== String(old[key])) {
element.style.setProperty(normalizeKey(key), value)
}
}
// Remove style properties that no longer exist
for (var key in old) {
if (old[key] != null && style[key] == null) {
element.style.removeProperty(normalizeKey(key))
}
}
}
}
// Here's an explanation of how this works:
// 1. The event names are always (by design) prefixed by `on`.
// 2. The EventListener interface accepts either a function or an object
// with a `handleEvent` method.
// 3. The object does not inherit from `Object.prototype`, to avoid
// any potential interference with that (e.g. setters).
// 4. The event name is remapped to the handler before calling it.
// 5. In function-based event handlers, `ev.target === this`. We replicate
// that below.
// 6. In function-based event handlers, `return false` prevents the default
// action and stops event propagation. We replicate that below.
function EventDict() {
// Save this, so the current redraw is correctly tracked.
this._ = currentRedraw
}
EventDict.prototype = Object.create(null)
EventDict.prototype.handleEvent = function (ev) {
var handler = this["on" + ev.type]
var result
if (typeof handler === "function") result = handler.call(ev.currentTarget, ev)
else if (typeof handler.handleEvent === "function") handler.handleEvent(ev)
if (this._ && ev.redraw !== false) (0, this._)()
if (result === false) {
ev.preventDefault()
ev.stopPropagation()
}
}
//event
function updateEvent(vnode, key, value) {
if (vnode.events != null) {
if (vnode.events[key] === value) return
if (value != null && (typeof value === "function" || typeof value === "object")) {
if (vnode.events[key] == null) vnode.dom.addEventListener(key.slice(2), vnode.events, false)
vnode.events[key] = value
} else {
if (vnode.events[key] != null) vnode.dom.removeEventListener(key.slice(2), vnode.events, false)
vnode.events[key] = undefined
}
} else if (value != null && (typeof value === "function" || typeof value === "object")) {
vnode.events = new EventDict()
vnode.dom.addEventListener(key.slice(2), vnode.events, false)
vnode.events[key] = value
}
}
//lifecycle
function initLifecycle(source, vnode, hooks) {
if (typeof source.oninit === "function") callHook.call(source.oninit, vnode)
if (typeof source.oncreate === "function") hooks.push(callHook.bind(source.oncreate, vnode))
}
function updateLifecycle(source, vnode, hooks) {
if (typeof source.onupdate === "function") hooks.push(callHook.bind(source.onupdate, vnode))
}
function shouldNotUpdate(vnode, old) {
do {
if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") {
var force = callHook.call(vnode.attrs.onbeforeupdate, vnode, old)
if (force !== undefined && !force) break
}
if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") {
var force = callHook.call(vnode.state.onbeforeupdate, vnode, old)
if (force !== undefined && !force) break
}
return false
} while (false); // eslint-disable-line no-constant-condition
vnode.dom = old.dom
vnode.domSize = old.domSize
vnode.instance = old.instance
// One would think having the actual latest attributes would be ideal,
// but it doesn't let us properly diff based on our current internal
// representation. We have to save not only the old DOM info, but also
// the attributes used to create it, as we diff *that*, not against the
// DOM directly (with a few exceptions in `setAttr`). And, of course, we
// need to save the children and text as they are conceptually not
// unlike special "attributes" internally.
vnode.attrs = old.attrs
vnode.children = old.children
vnode.text = old.text
return true
}
var currentDOM
return function(dom, vnodes, redraw) {
if (!dom) throw new TypeError("DOM element being rendered to does not exist.")
if (currentDOM != null && dom.contains(currentDOM)) {
throw new TypeError("Node is currently being rendered to and thus is locked.")
}
var prevRedraw = currentRedraw
var prevDOM = currentDOM
var hooks = []
var active = activeElement()
var namespace = dom.namespaceURI
currentDOM = dom
currentRedraw = typeof redraw === "function" ? redraw : undefined
try {
// First time rendering into a node clears it out
if (dom.vnodes == null) dom.textContent = ""
vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])
updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
dom.vnodes = vnodes
// `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement
if (active != null && activeElement() !== active && typeof active.focus === "function") active.focus()
for (var i = 0; i < hooks.length; i++) hooks[i]()
} finally {
currentRedraw = prevRedraw
currentDOM = prevDOM
}
}
}