* Recast the router API to be a lot more intuitive. Fixes #2387 Fixes #2072 Fixes quite a few issues reported on Gitter. For `m.route.Link`: - More intuitive - More accessible - More ergonomic - It can be disabled - It can be cancelled - It can be changed - Oh, and you can use it isomorphically. For `m.route.prefix` - You can *read* it. - You can write to it, of course. - It's literally just setting a property. For the router itself (and the rest of Mithril): - You can now `require("mithril")` and all its submodules without a DOM at all. There is a catch: you can't instantiate any routes, you can't mount anything, and you can't invoke `m.render` in any capacity. You can only use `m.route.Link`, `m.route.prefix`, hyperscript stuff, and `mithril/stream`, and you can use `m.request` with `background: true` if you use a global XHR polyfill. (You can't use `m.request` without `background: true` except with a DOM to redraw with.) The goal here is to try to get out of the way for simple testing and to defer the inevitable `TypeError`s for the relevant DOM methods to runtime. The factory requires no arguments, and in terms of globals, you can just figure out based on what errors are thrown what globals to define. Their values don't matter - they just need to be set to *something*, even if it's just `null` or `undefined`, before Mithril executes. Had to make quite a few other changes throughout the docs and tests to update them accordingly. Oh, and that massive router overhaul enabled me to do all this. Also, slip in a few drive-by fixes to the mocks so they're a little easier to work with and can accept more URLs. This was required for a few of the tests. * Update changelog + numbers, add forgotten bundle option * Add PR numbers to changelog [skip ci] * Allow continuing to the next match by returning `false`. * Update numbers again
891 lines
34 KiB
JavaScript
891 lines
34 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
|
|
var fragment = $doc.createDocumentFragment()
|
|
var child
|
|
while (child = temp.firstChild) {
|
|
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(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(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(o)
|
|
else updateNode(parent, o, v, hooks, getNextSibling(old, start + 1, nextSibling), ns)
|
|
}
|
|
if (old.length > commonLength) removeNodes(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)
|
|
insertNode(parent, toFragment(oe), topSibling)
|
|
if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns)
|
|
if (++start <= --end) insertNode(parent, toFragment(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(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(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 insertNode(parent, toFragment(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(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) {
|
|
toFragment(old)
|
|
createHTML(parent, vnode, ns, nextSibling)
|
|
}
|
|
else vnode.dom = old.dom, vnode.domSize = old.domSize
|
|
}
|
|
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(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 toFragment(vnode) {
|
|
var count = vnode.domSize
|
|
if (count != null || vnode.dom == null) {
|
|
var fragment = $doc.createDocumentFragment()
|
|
if (count > 0) {
|
|
var dom = vnode.dom
|
|
while (--count) fragment.appendChild(dom.nextSibling)
|
|
fragment.insertBefore(dom, fragment.firstChild)
|
|
}
|
|
return fragment
|
|
}
|
|
else return vnode.dom
|
|
}
|
|
function getNextSibling(vnodes, i, nextSibling) {
|
|
for (; i < vnodes.length; i++) {
|
|
if (vnodes[i] != null && vnodes[i].dom != null) return vnodes[i].dom
|
|
}
|
|
return nextSibling
|
|
}
|
|
|
|
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
|
|
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")
|
|
}
|
|
|
|
//remove
|
|
function removeNodes(vnodes, start, end) {
|
|
for (var i = start; i < end; i++) {
|
|
var vnode = vnodes[i]
|
|
if (vnode != null) removeNode(vnode)
|
|
}
|
|
}
|
|
function removeNode(vnode) {
|
|
var expected = 1, called = 0
|
|
var original = vnode.state
|
|
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") {
|
|
expected++
|
|
result.then(continuation, continuation)
|
|
}
|
|
}
|
|
if (vnode.attrs && typeof vnode.attrs.onbeforeremove === "function") {
|
|
var result = callHook.call(vnode.attrs.onbeforeremove, vnode)
|
|
if (result != null && typeof result.then === "function") {
|
|
expected++
|
|
result.then(continuation, continuation)
|
|
}
|
|
}
|
|
continuation()
|
|
function continuation() {
|
|
if (++called === expected) {
|
|
checkState(vnode, original)
|
|
onremove(vnode)
|
|
if (vnode.dom) {
|
|
var parent = vnode.dom.parentNode
|
|
var count = vnode.domSize || 1
|
|
while (--count) parent.removeChild(vnode.dom.nextSibling)
|
|
parent.removeChild(vnode.dom)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
return function(dom, vnodes, redraw) {
|
|
if (!dom) throw new TypeError("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.")
|
|
var hooks = []
|
|
var active = activeElement()
|
|
var namespace = dom.namespaceURI
|
|
|
|
// First time rendering into a node clears it out
|
|
if (dom.vnodes == null) dom.textContent = ""
|
|
|
|
vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes])
|
|
var prevRedraw = currentRedraw
|
|
try {
|
|
currentRedraw = typeof redraw === "function" ? redraw : undefined
|
|
updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
|
|
} finally {
|
|
currentRedraw = prevRedraw
|
|
}
|
|
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]()
|
|
}
|
|
}
|