- Remove docs for it - Remove tests for it - Add runtime check for unexpected reassignment, since we can't freeze the property (we internally have to be able to modify it)
673 lines
24 KiB
JavaScript
673 lines
24 KiB
JavaScript
"use strict"
|
|
|
|
var Vnode = require("../render/vnode")
|
|
|
|
module.exports = function($window) {
|
|
var $doc = $window.document
|
|
var $emptyFragment = $doc.createDocumentFragment()
|
|
|
|
var nameSpace = {
|
|
svg: "http://www.w3.org/2000/svg",
|
|
math: "http://www.w3.org/1998/Math/MathML"
|
|
}
|
|
|
|
var onevent
|
|
function setEventCallback(callback) {return onevent = callback}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
//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 "#": return createText(parent, vnode, nextSibling)
|
|
case "<": return createHTML(parent, vnode, nextSibling)
|
|
case "[": return createFragment(parent, vnode, hooks, ns, nextSibling)
|
|
default: return createElement(parent, vnode, hooks, ns, nextSibling)
|
|
}
|
|
}
|
|
else return createComponent(parent, vnode, hooks, ns, nextSibling)
|
|
}
|
|
function createText(parent, vnode, nextSibling) {
|
|
vnode.dom = $doc.createTextNode(vnode.children)
|
|
insertNode(parent, vnode.dom, nextSibling)
|
|
return vnode.dom
|
|
}
|
|
function createHTML(parent, vnode, nextSibling) {
|
|
var match = vnode.children.match(/^\s*?<(\w+)/im) || []
|
|
var parent1 = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"}[match[1]] || "div"
|
|
var temp = $doc.createElement(parent1)
|
|
|
|
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)
|
|
return fragment
|
|
}
|
|
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)
|
|
return fragment
|
|
}
|
|
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 (vnode.attrs != null && vnode.attrs.contenteditable != null) {
|
|
setContentEditable(vnode)
|
|
}
|
|
else {
|
|
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)
|
|
setLateAttrs(vnode)
|
|
}
|
|
}
|
|
return element
|
|
}
|
|
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 $emptyFragment
|
|
sentinel.$$reentrantLock$$ = true
|
|
} else {
|
|
vnode.state = void 0
|
|
sentinel = vnode.tag
|
|
if (sentinel.$$reentrantLock$$ != null) return $emptyFragment
|
|
sentinel.$$reentrantLock$$ = true
|
|
vnode.state = (vnode.tag.prototype != null && typeof vnode.tag.prototype.view === "function") ? new vnode.tag(vnode) : vnode.tag(vnode)
|
|
}
|
|
if (vnode.attrs != null) initLifecycle(vnode.attrs, vnode, hooks)
|
|
initLifecycle(vnode.state, 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) {
|
|
var element = createNode(parent, vnode.instance, hooks, ns, nextSibling)
|
|
vnode.dom = vnode.instance.dom
|
|
vnode.domSize = vnode.dom != null ? vnode.instance.domSize : 0
|
|
insertNode(parent, element, nextSibling)
|
|
return element
|
|
}
|
|
else {
|
|
vnode.domSize = 0
|
|
return $emptyFragment
|
|
}
|
|
}
|
|
|
|
//update
|
|
function updateNodes(parent, old, vnodes, recycling, hooks, nextSibling, ns) {
|
|
if (old === vnodes || old == null && vnodes == null) return
|
|
else if (old == null) createNodes(parent, vnodes, 0, vnodes.length, hooks, nextSibling, ns)
|
|
else if (vnodes == null) removeNodes(old, 0, old.length, vnodes)
|
|
else {
|
|
if (old.length === vnodes.length) {
|
|
var isUnkeyed = false
|
|
for (var i = 0; i < vnodes.length; i++) {
|
|
if (vnodes[i] != null && old[i] != null) {
|
|
isUnkeyed = vnodes[i].key == null && old[i].key == null
|
|
break
|
|
}
|
|
}
|
|
if (isUnkeyed) {
|
|
for (var i = 0; i < old.length; i++) {
|
|
if (old[i] === vnodes[i]) continue
|
|
else if (old[i] == null && vnodes[i] != null) createNode(parent, vnodes[i], hooks, ns, getNextSibling(old, i + 1, nextSibling))
|
|
else if (vnodes[i] == null) removeNodes(old, i, i + 1, vnodes)
|
|
else updateNode(parent, old[i], vnodes[i], hooks, getNextSibling(old, i + 1, nextSibling), recycling, ns)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
recycling = recycling || isRecyclable(old, vnodes)
|
|
if (recycling) {
|
|
var pool = old.pool
|
|
old = old.concat(old.pool)
|
|
}
|
|
|
|
var oldStart = 0, start = 0, oldEnd = old.length - 1, end = vnodes.length - 1, map
|
|
while (oldEnd >= oldStart && end >= start) {
|
|
var o = old[oldStart], v = vnodes[start]
|
|
if (o === v && !recycling) oldStart++, start++
|
|
else if (o == null) oldStart++
|
|
else if (v == null) start++
|
|
else if (o.key === v.key) {
|
|
var shouldRecycle = (pool != null && oldStart >= old.length - pool.length) || ((pool == null) && recycling)
|
|
oldStart++, start++
|
|
updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), shouldRecycle, ns)
|
|
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
|
}
|
|
else {
|
|
var o = old[oldEnd]
|
|
if (o === v && !recycling) oldEnd--, start++
|
|
else if (o == null) oldEnd--
|
|
else if (v == null) start++
|
|
else if (o.key === v.key) {
|
|
var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling)
|
|
updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns)
|
|
if (recycling || start < end) insertNode(parent, toFragment(o), getNextSibling(old, oldStart, nextSibling))
|
|
oldEnd--, start++
|
|
}
|
|
else break
|
|
}
|
|
}
|
|
while (oldEnd >= oldStart && end >= start) {
|
|
var o = old[oldEnd], v = vnodes[end]
|
|
if (o === v && !recycling) oldEnd--, end--
|
|
else if (o == null) oldEnd--
|
|
else if (v == null) end--
|
|
else if (o.key === v.key) {
|
|
var shouldRecycle = (pool != null && oldEnd >= old.length - pool.length) || ((pool == null) && recycling)
|
|
updateNode(parent, o, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), shouldRecycle, ns)
|
|
if (recycling && o.tag === v.tag) insertNode(parent, toFragment(o), nextSibling)
|
|
if (o.dom != null) nextSibling = o.dom
|
|
oldEnd--, end--
|
|
}
|
|
else {
|
|
if (!map) map = getKeyMap(old, oldEnd)
|
|
if (v != null) {
|
|
var oldIndex = map[v.key]
|
|
if (oldIndex != null) {
|
|
var movable = old[oldIndex]
|
|
var shouldRecycle = (pool != null && oldIndex >= old.length - pool.length) || ((pool == null) && recycling)
|
|
updateNode(parent, movable, v, hooks, getNextSibling(old, oldEnd + 1, nextSibling), recycling, ns)
|
|
insertNode(parent, toFragment(movable), nextSibling)
|
|
old[oldIndex].skip = true
|
|
if (movable.dom != null) nextSibling = movable.dom
|
|
}
|
|
else {
|
|
var dom = createNode(parent, v, hooks, ns, nextSibling)
|
|
nextSibling = dom
|
|
}
|
|
}
|
|
end--
|
|
}
|
|
if (end < start) break
|
|
}
|
|
createNodes(parent, vnodes, start, end + 1, hooks, nextSibling, ns)
|
|
removeNodes(old, oldStart, oldEnd + 1, vnodes)
|
|
}
|
|
}
|
|
function updateNode(parent, old, vnode, hooks, nextSibling, recycling, ns) {
|
|
var oldTag = old.tag, tag = vnode.tag
|
|
if (oldTag === tag) {
|
|
vnode.state = old.state
|
|
vnode.events = old.events
|
|
if (!recycling && shouldNotUpdate(vnode, old)) return
|
|
if (typeof oldTag === "string") {
|
|
if (vnode.attrs != null) {
|
|
if (recycling) {
|
|
vnode.state = {}
|
|
initLifecycle(vnode.attrs, vnode, hooks)
|
|
}
|
|
else updateLifecycle(vnode.attrs, vnode, hooks)
|
|
}
|
|
switch (oldTag) {
|
|
case "#": updateText(old, vnode); break
|
|
case "<": updateHTML(parent, old, vnode, nextSibling); break
|
|
case "[": updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns); break
|
|
default: updateElement(old, vnode, recycling, hooks, ns)
|
|
}
|
|
}
|
|
else updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns)
|
|
}
|
|
else {
|
|
removeNode(old, null)
|
|
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, nextSibling) {
|
|
if (old.children !== vnode.children) {
|
|
toFragment(old)
|
|
createHTML(parent, vnode, nextSibling)
|
|
}
|
|
else vnode.dom = old.dom, vnode.domSize = old.domSize
|
|
}
|
|
function updateFragment(parent, old, vnode, recycling, hooks, nextSibling, ns) {
|
|
updateNodes(parent, old.children, vnode.children, recycling, 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, recycling, 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 (vnode.attrs != null && vnode.attrs.contenteditable != null) {
|
|
setContentEditable(vnode)
|
|
}
|
|
else 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, recycling, hooks, null, ns)
|
|
}
|
|
}
|
|
function updateComponent(parent, old, vnode, hooks, nextSibling, recycling, ns) {
|
|
if (recycling) {
|
|
initComponent(vnode, hooks)
|
|
} else {
|
|
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")
|
|
if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks)
|
|
updateLifecycle(vnode.state, 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, recycling, ns)
|
|
vnode.dom = vnode.instance.dom
|
|
vnode.domSize = vnode.instance.domSize
|
|
}
|
|
else if (old.instance != null) {
|
|
removeNode(old.instance, null)
|
|
vnode.dom = undefined
|
|
vnode.domSize = 0
|
|
}
|
|
else {
|
|
vnode.dom = old.dom
|
|
vnode.domSize = old.domSize
|
|
}
|
|
}
|
|
function isRecyclable(old, vnodes) {
|
|
if (old.pool != null && Math.abs(old.pool.length - vnodes.length) <= Math.abs(old.length - vnodes.length)) {
|
|
var oldChildrenLength = old[0] && old[0].children && old[0].children.length || 0
|
|
var poolChildrenLength = old.pool[0] && old.pool[0].children && old.pool[0].children.length || 0
|
|
var vnodesChildrenLength = vnodes[0] && vnodes[0].children && vnodes[0].children.length || 0
|
|
if (Math.abs(poolChildrenLength - vnodesChildrenLength) <= Math.abs(oldChildrenLength - vnodesChildrenLength)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
function getKeyMap(vnodes, end) {
|
|
var map = {}, i = 0
|
|
for (var i = 0; i < end; i++) {
|
|
var vnode = vnodes[i]
|
|
if (vnode != null) {
|
|
var key = vnode.key
|
|
if (key != null) map[key] = i
|
|
}
|
|
}
|
|
return map
|
|
}
|
|
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 && nextSibling.parentNode) parent.insertBefore(dom, nextSibling)
|
|
else parent.appendChild(dom)
|
|
}
|
|
|
|
function setContentEditable(vnode) {
|
|
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, context) {
|
|
for (var i = start; i < end; i++) {
|
|
var vnode = vnodes[i]
|
|
if (vnode != null) {
|
|
if (vnode.skip) vnode.skip = false
|
|
else removeNode(vnode, context)
|
|
}
|
|
}
|
|
}
|
|
function removeNode(vnode, context) {
|
|
var expected = 1, called = 0
|
|
var original = vnode.state
|
|
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)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
continuation()
|
|
function continuation() {
|
|
if (++called === expected) {
|
|
checkState(vnode, original)
|
|
onremove(vnode)
|
|
if (vnode.dom) {
|
|
var count = vnode.domSize || 1
|
|
if (count > 1) {
|
|
var dom = vnode.dom
|
|
while (--count) {
|
|
removeNodeFromDOM(dom.nextSibling)
|
|
}
|
|
}
|
|
removeNodeFromDOM(vnode.dom)
|
|
if (context != null && vnode.domSize == null && !hasIntegrationMethods(vnode.attrs) && typeof vnode.tag === "string") { //TODO test custom elements
|
|
if (!context.pool) context.pool = [vnode]
|
|
else context.pool.push(vnode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function removeNodeFromDOM(node) {
|
|
var parent = node.parentNode
|
|
if (parent != null) parent.removeChild(node)
|
|
}
|
|
function onremove(vnode) {
|
|
if (vnode.attrs && typeof vnode.attrs.onremove === "function") callHook.call(vnode.attrs.onremove, vnode)
|
|
if (typeof vnode.tag !== "string") {
|
|
if (typeof vnode.state.onremove === "function") callHook.call(vnode.state.onremove, vnode)
|
|
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" || isLifecycleMethod(key)) return
|
|
if (key[0] === "o" && key[1] === "n") return updateEvent(vnode, key, value)
|
|
if ((old === value && !isFormAttribute(vnode, key)) && typeof value !== "object" || value === undefined) return
|
|
var element = vnode.dom
|
|
if (key.slice(0, 6) === "xlink:") element.setAttributeNS("http://www.w3.org/1999/xlink", key.slice(6), value)
|
|
else if (key === "style") updateStyle(element, old, value)
|
|
else if (key in element && !isAttribute(key) && ns === undefined && !isCustomElement(vnode)) {
|
|
if (key === "value") {
|
|
var normalized = "" + value // eslint-disable-line 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 === normalized && vnode.dom === $doc.activeElement) return
|
|
//setting select[value] to same value while having select open blinks select dropdown in Chrome
|
|
if (vnode.tag === "select") {
|
|
if (value === null) {
|
|
if (vnode.dom.selectedIndex === -1 && vnode.dom === $doc.activeElement) return
|
|
} else {
|
|
if (old !== null && vnode.dom.value === normalized && vnode.dom === $doc.activeElement) 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 === normalized) return
|
|
}
|
|
// 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") {
|
|
element.setAttribute(key, value)
|
|
return
|
|
}
|
|
element[key] = value
|
|
}
|
|
else {
|
|
if (typeof value === "boolean") {
|
|
if (value) element.setAttribute(key, "")
|
|
else element.removeAttribute(key)
|
|
}
|
|
else element.setAttribute(key === "className" ? "class" : key, value)
|
|
}
|
|
}
|
|
function setLateAttrs(vnode) {
|
|
var attrs = vnode.attrs
|
|
if (vnode.tag === "select" && attrs != null) {
|
|
if ("value" in attrs) setAttr(vnode, "value", null, attrs.value, undefined)
|
|
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)
|
|
}
|
|
}
|
|
if (old != null) {
|
|
for (var key in old) {
|
|
if (attrs == null || !(key in attrs)) {
|
|
if (key === "className") key = "class"
|
|
if (key[0] === "o" && key[1] === "n" && !isLifecycleMethod(key)) updateEvent(vnode, key, undefined)
|
|
else if (key !== "key") vnode.dom.removeAttribute(key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function isFormAttribute(vnode, attr) {
|
|
return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.activeElement
|
|
}
|
|
function isLifecycleMethod(attr) {
|
|
return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate"
|
|
}
|
|
function isAttribute(attr) {
|
|
return attr === "href" || attr === "list" || attr === "form" || attr === "width" || attr === "height"// || attr === "type"
|
|
}
|
|
function isCustomElement(vnode){
|
|
return vnode.attrs.is || vnode.tag.indexOf("-") > -1
|
|
}
|
|
function hasIntegrationMethods(source) {
|
|
return source != null && (source.oncreate || source.onupdate || source.onbeforeremove || source.onremove)
|
|
}
|
|
|
|
//style
|
|
function updateStyle(element, old, style) {
|
|
if (old != null && style != null && typeof old === "object" && typeof style === "object" && style !== old) {
|
|
// Both old & new are (different) objects.
|
|
// Update style properties that have changed
|
|
for (var key in style) {
|
|
if (style[key] !== old[key]) element.style[key] = style[key]
|
|
}
|
|
// Remove style properties that no longer exist
|
|
for (var key in old) {
|
|
if (!(key in style)) element.style[key] = ""
|
|
}
|
|
return
|
|
}
|
|
if (old === style) element.style.cssText = "", old = null
|
|
if (style == null) element.style.cssText = ""
|
|
else if (typeof style === "string") element.style.cssText = style
|
|
else {
|
|
if (typeof old === "string") element.style.cssText = ""
|
|
for (var key in style) {
|
|
element.style[key] = style[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.
|
|
function EventDict() {}
|
|
EventDict.prototype = Object.create(null)
|
|
EventDict.prototype.handleEvent = function (ev) {
|
|
var handler = this["on" + ev.type]
|
|
if (typeof handler === "function") handler.call(ev.target, ev)
|
|
else if (typeof handler.handleEvent === "function") handler.handleEvent(ev)
|
|
if (typeof onevent === "function") onevent.call(ev.target, ev)
|
|
}
|
|
|
|
//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) {
|
|
var forceVnodeUpdate, forceComponentUpdate
|
|
if (vnode.attrs != null && typeof vnode.attrs.onbeforeupdate === "function") {
|
|
forceVnodeUpdate = callHook.call(vnode.attrs.onbeforeupdate, vnode, old)
|
|
}
|
|
if (typeof vnode.tag !== "string" && typeof vnode.state.onbeforeupdate === "function") {
|
|
forceComponentUpdate = callHook.call(vnode.state.onbeforeupdate, vnode, old)
|
|
}
|
|
if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) {
|
|
vnode.dom = old.dom
|
|
vnode.domSize = old.domSize
|
|
vnode.instance = old.instance
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function render(dom, vnodes) {
|
|
if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.")
|
|
var hooks = []
|
|
var active = $doc.activeElement
|
|
var namespace = dom.namespaceURI
|
|
|
|
// First time rendering into a node clears it out
|
|
if (dom.vnodes == null) dom.textContent = ""
|
|
|
|
if (!Array.isArray(vnodes)) vnodes = [vnodes]
|
|
updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), false, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace)
|
|
dom.vnodes = vnodes
|
|
for (var i = 0; i < hooks.length; i++) hooks[i]()
|
|
// document.activeElement can return null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
|
|
if (active != null && $doc.activeElement !== active) active.focus()
|
|
}
|
|
|
|
return {render: render, setEventCallback: setEventCallback}
|
|
}
|