Created
September 20, 2018 21:33
-
-
Save StephanHoyer/fd52b768ff780401fce7a9be26adb249 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;(function() { | |
"use strict" | |
function Vnode(tag, key, attrs0, children, text, dom) { | |
return {tag: tag, key: key, attrs: attrs0, children: children, text: text, dom: dom, domSize: undefined, state: undefined, events: undefined, instance: undefined} | |
} | |
Vnode.normalize = function(node) { | |
if (Array.isArray(node)) return Vnode("[", undefined, undefined, Vnode.normalizeChildren(node), undefined, undefined) | |
if (node != null && typeof node !== "object") return Vnode("#", undefined, undefined, node === false ? "" : node, undefined, undefined) | |
return node | |
} | |
Vnode.normalizeChildren = function normalizeChildren(input) { | |
var children = [] | |
for (var i = 0; i < input.length; i++) { | |
children[i] = Vnode.normalize(input[i]) | |
} | |
return children | |
} | |
var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g | |
var selectorCache = {} | |
var hasOwn = {}.hasOwnProperty | |
function isEmpty(object) { | |
for (var key in object) if (hasOwn.call(object, key)) return false | |
return true | |
} | |
function compileSelector(selector) { | |
var match, tag = "div", classes = [], attrs = {} | |
while (match = selectorParser.exec(selector)) { | |
var type = match[1], value = match[2] | |
if (type === "" && value !== "") tag = value | |
else if (type === "#") attrs.id = value | |
else if (type === ".") classes.push(value) | |
else if (match[3][0] === "[") { | |
var attrValue = match[6] | |
if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\") | |
if (match[4] === "class") classes.push(attrValue) | |
else attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true | |
} | |
} | |
if (classes.length > 0) attrs.className = classes.join(" ") | |
return selectorCache[selector] = {tag: tag, attrs: attrs} | |
} | |
function execSelector(state, attrs, children) { | |
var hasAttrs = false, childList, text | |
var classAttr = hasOwn.call(attrs, "class") ? "class" : "className" | |
var className = attrs[classAttr] | |
if (!isEmpty(state.attrs) && !isEmpty(attrs)) { | |
var newAttrs = {} | |
for(var key in attrs) { | |
if (hasOwn.call(attrs, key)) { | |
newAttrs[key] = attrs[key] | |
} | |
} | |
attrs = newAttrs | |
} | |
for (var key in state.attrs) { | |
if (hasOwn.call(state.attrs, key) && key !== "className" && !hasOwn.call(attrs, key)){ | |
attrs[key] = state.attrs[key] | |
} | |
} | |
if (className != null || state.attrs.className != null) attrs.className = | |
className != null | |
? state.attrs.className != null | |
? state.attrs.className + " " + className | |
: className | |
: state.attrs.className != null | |
? state.attrs.className | |
: null | |
if (classAttr === "class") attrs.class = null | |
for (var key in attrs) { | |
if (hasOwn.call(attrs, key) && key !== "key") { | |
hasAttrs = true | |
break | |
} | |
} | |
if (Array.isArray(children) && children.length === 1 && children[0] != null && children[0].tag === "#") { | |
text = children[0].children | |
} else { | |
childList = children | |
} | |
return Vnode(state.tag, attrs.key, hasAttrs ? attrs : null, childList, text) | |
} | |
function hyperscript(selector) { | |
if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") { | |
throw Error("The selector must be either a string or a component."); | |
} | |
var attrs = arguments[1], start = 2, children | |
if (attrs == null) { | |
attrs = {} | |
} else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) { | |
attrs = {} | |
start = 1 | |
} | |
if (arguments.length === start + 1) { | |
children = arguments[start] | |
if (!Array.isArray(children)) children = [children] | |
} else { | |
children = [] | |
while (start < arguments.length) children.push(arguments[start++]) | |
} | |
if (typeof selector === "string") { | |
return execSelector(selectorCache[selector] || compileSelector(selector), attrs, Vnode.normalizeChildren(children)) | |
} else { | |
return Vnode(selector, attrs.key, attrs, children) | |
} | |
} | |
hyperscript.trust = function(html) { | |
if (html == null) html = "" | |
return Vnode("<", undefined, undefined, html, undefined, undefined) | |
} | |
hyperscript.fragment = function(attrs1, children0) { | |
return Vnode("[", attrs1.key, attrs1, Vnode.normalizeChildren(children0), undefined, undefined) | |
} | |
var m = hyperscript | |
/** @constructor */ | |
var PromisePolyfill = function(executor) { | |
if (!(this instanceof PromisePolyfill)) throw new Error("Promise must be called with `new`") | |
if (typeof executor !== "function") throw new TypeError("executor must be a function") | |
var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false) | |
var instance = self._instance = {resolvers: resolvers, rejectors: rejectors} | |
var callAsync = typeof setImmediate === "function" ? setImmediate : setTimeout | |
function handler(list, shouldAbsorb) { | |
return function execute(value) { | |
var then | |
try { | |
if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") { | |
if (value === self) throw new TypeError("Promise can't be resolved w/ itself") | |
executeOnce(then.bind(value)) | |
} | |
else { | |
callAsync(function() { | |
if (!shouldAbsorb && list.length === 0) console.error("Possible unhandled promise rejection:", value) | |
for (var i = 0; i < list.length; i++) list[i](value) | |
resolvers.length = 0, rejectors.length = 0 | |
instance.state = shouldAbsorb | |
instance.retry = function() {execute(value)} | |
}) | |
} | |
} | |
catch (e) { | |
rejectCurrent(e) | |
} | |
} | |
} | |
function executeOnce(then) { | |
var runs = 0 | |
function run(fn) { | |
return function(value) { | |
if (runs++ > 0) return | |
fn(value) | |
} | |
} | |
var onerror = run(rejectCurrent) | |
try {then(run(resolveCurrent), onerror)} catch (e) {onerror(e)} | |
} | |
executeOnce(executor) | |
} | |
PromisePolyfill.prototype.then = function(onFulfilled, onRejection) { | |
var self = this, instance = self._instance | |
function handle(callback, list, next, state) { | |
list.push(function(value) { | |
if (typeof callback !== "function") next(value) | |
else try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)} | |
}) | |
if (typeof instance.retry === "function" && state === instance.state) instance.retry() | |
} | |
var resolveNext, rejectNext | |
var promise = new PromisePolyfill(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) | |
handle(onFulfilled, instance.resolvers, resolveNext, true), handle(onRejection, instance.rejectors, rejectNext, false) | |
return promise | |
} | |
PromisePolyfill.prototype.catch = function(onRejection) { | |
return this.then(null, onRejection) | |
} | |
PromisePolyfill.prototype.finally = function(callback) { | |
return this.then( | |
function(value) { | |
return PromisePolyfill.resolve(callback()).then(function() { | |
return value | |
}) | |
}, | |
function(reason) { | |
return PromisePolyfill.resolve(callback()).then(function() { | |
return PromisePolyfill.reject(reason); | |
}) | |
} | |
) | |
} | |
PromisePolyfill.resolve = function(value) { | |
if (value instanceof PromisePolyfill) return value | |
return new PromisePolyfill(function(resolve) {resolve(value)}) | |
} | |
PromisePolyfill.reject = function(value) { | |
return new PromisePolyfill(function(resolve, reject) {reject(value)}) | |
} | |
PromisePolyfill.all = function(list) { | |
return new PromisePolyfill(function(resolve, reject) { | |
var total = list.length, count = 0, values = [] | |
if (list.length === 0) resolve([]) | |
else for (var i = 0; i < list.length; i++) { | |
(function(i) { | |
function consume(value) { | |
count++ | |
values[i] = value | |
if (count === total) resolve(values) | |
} | |
if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") { | |
list[i].then(consume, reject) | |
} | |
else consume(list[i]) | |
})(i) | |
} | |
}) | |
} | |
PromisePolyfill.race = function(list) { | |
return new PromisePolyfill(function(resolve, reject) { | |
for (var i = 0; i < list.length; i++) { | |
list[i].then(resolve, reject) | |
} | |
}) | |
} | |
if (typeof window !== "undefined") { | |
if (typeof window.Promise === "undefined") { | |
window.Promise = PromisePolyfill | |
} else if (!window.Promise.prototype.finally) { | |
window.Promise.prototype.finally = PromisePolyfill.prototype.finally | |
} | |
var PromisePolyfill = window.Promise | |
} else if (typeof global !== "undefined") { | |
if (typeof global.Promise === "undefined") { | |
global.Promise = PromisePolyfill | |
} else if (!global.Promise.prototype.finally) { | |
global.Promise.prototype.finally = PromisePolyfill.prototype.finally | |
} | |
var PromisePolyfill = global.Promise | |
} else { | |
} | |
var buildQueryString = function(object) { | |
if (Object.prototype.toString.call(object) !== "[object Object]") return "" | |
var args = [] | |
for (var key0 in object) { | |
destructure(key0, object[key0]) | |
} | |
return args.join("&") | |
function destructure(key0, value) { | |
if (Array.isArray(value)) { | |
for (var i = 0; i < value.length; i++) { | |
destructure(key0 + "[" + i + "]", value[i]) | |
} | |
} | |
else if (Object.prototype.toString.call(value) === "[object Object]") { | |
for (var i in value) { | |
destructure(key0 + "[" + i + "]", value[i]) | |
} | |
} | |
else args.push(encodeURIComponent(key0) + (value != null && value !== "" ? "=" + encodeURIComponent(value) : "")) | |
} | |
} | |
var FILE_PROTOCOL_REGEX = new RegExp("^file://", "i") | |
var _9 = function($window, Promise) { | |
var callbackCount = 0 | |
var oncompletion | |
function setCompletionCallback(callback) {oncompletion = callback} | |
function finalizer() { | |
var count = 0 | |
function complete() {if (--count === 0 && typeof oncompletion === "function") oncompletion()} | |
return function finalize(promise0) { | |
var then0 = promise0.then | |
promise0.then = function() { | |
count++ | |
var next = then0.apply(promise0, arguments) | |
next.then(complete, function(e) { | |
complete() | |
if (count === 0) throw e | |
}) | |
return finalize(next) | |
} | |
return promise0 | |
} | |
} | |
function normalize(args, extra) { | |
if (typeof args === "string") { | |
var url = args | |
args = extra || {} | |
if (args.url == null) args.url = url | |
} | |
return args | |
} | |
function request(args, extra) { | |
var finalize = finalizer() | |
args = normalize(args, extra) | |
var promise0 = new Promise(function(resolve, reject) { | |
if (args.method == null) args.method = "GET" | |
args.method = args.method.toUpperCase() | |
var useBody = (args.method === "GET" || args.method === "TRACE") ? false : (typeof args.useBody === "boolean" ? args.useBody : true) | |
if (typeof args.serialize !== "function") args.serialize = typeof FormData !== "undefined" && args.data instanceof FormData ? function(value) {return value} : JSON.stringify | |
if (typeof args.deserialize !== "function") args.deserialize = deserialize | |
if (typeof args.extract !== "function") args.extract = extract | |
args.url = interpolate(args.url, args.data) | |
if (useBody) args.data = args.serialize(args.data) | |
else args.url = assemble(args.url, args.data) | |
var xhr = new $window.XMLHttpRequest(), | |
aborted = false, | |
_abort = xhr.abort | |
xhr.abort = function abort() { | |
aborted = true | |
_abort.call(xhr) | |
} | |
xhr.open(args.method, args.url, typeof args.async === "boolean" ? args.async : true, typeof args.user === "string" ? args.user : undefined, typeof args.password === "string" ? args.password : undefined) | |
if (args.serialize === JSON.stringify && useBody && !(args.headers && args.headers.hasOwnProperty("Content-Type"))) { | |
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8") | |
} | |
if (args.deserialize === deserialize && !(args.headers && args.headers.hasOwnProperty("Accept"))) { | |
xhr.setRequestHeader("Accept", "application/json, text/*") | |
} | |
if (args.withCredentials) xhr.withCredentials = args.withCredentials | |
if (args.timeout) xhr.timeout = args.timeout | |
if (args.responseType) xhr.responseType = args.responseType | |
for (var key in args.headers) if ({}.hasOwnProperty.call(args.headers, key)) { | |
xhr.setRequestHeader(key, args.headers[key]) | |
} | |
if (typeof args.config === "function") xhr = args.config(xhr, args) || xhr | |
xhr.onreadystatechange = function() { | |
// Don't throw errors on xhr.abort(). | |
if(aborted) return | |
if (xhr.readyState === 4) { | |
try { | |
var response = (args.extract !== extract) ? args.extract(xhr, args) : args.deserialize(args.extract(xhr, args)) | |
if (args.extract !== extract || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304 || FILE_PROTOCOL_REGEX.test(args.url)) { | |
resolve(cast(args.type, response)) | |
} | |
else { | |
var error = new Error(xhr.responseText) | |
error.code = xhr.status | |
error.response = response | |
reject(error) | |
} | |
} | |
catch (e) { | |
reject(e) | |
} | |
} | |
} | |
if (useBody && (args.data != null)) xhr.send(args.data) | |
else xhr.send() | |
}) | |
return args.background === true ? promise0 : finalize(promise0) | |
} | |
function jsonp(args, extra) { | |
var finalize = finalizer() | |
args = normalize(args, extra) | |
var promise0 = new Promise(function(resolve, reject) { | |
var callbackName = args.callbackName || "_mithril_" + Math.round(Math.random() * 1e16) + "_" + callbackCount++ | |
var script = $window.document.createElement("script") | |
$window[callbackName] = function(data) { | |
script.parentNode.removeChild(script) | |
resolve(cast(args.type, data)) | |
delete $window[callbackName] | |
} | |
script.onerror = function() { | |
script.parentNode.removeChild(script) | |
reject(new Error("JSONP request failed")) | |
delete $window[callbackName] | |
} | |
if (args.data == null) args.data = {} | |
args.url = interpolate(args.url, args.data) | |
args.data[args.callbackKey || "callback"] = callbackName | |
script.src = assemble(args.url, args.data) | |
$window.document.documentElement.appendChild(script) | |
}) | |
return args.background === true? promise0 : finalize(promise0) | |
} | |
function interpolate(url, data) { | |
if (data == null) return url | |
var tokens = url.match(/:[^\/]+/gi) || [] | |
for (var i = 0; i < tokens.length; i++) { | |
var key = tokens[i].slice(1) | |
if (data[key] != null) { | |
url = url.replace(tokens[i], data[key]) | |
} | |
} | |
return url | |
} | |
function assemble(url, data) { | |
var querystring = buildQueryString(data) | |
if (querystring !== "") { | |
var prefix = url.indexOf("?") < 0 ? "?" : "&" | |
url += prefix + querystring | |
} | |
return url | |
} | |
function deserialize(data) { | |
try {return data !== "" ? JSON.parse(data) : null} | |
catch (e) {throw new Error("Invalid JSON: " + data)} | |
} | |
function extract(xhr) {return xhr.responseText} | |
function cast(type0, data) { | |
if (typeof type0 === "function") { | |
if (Array.isArray(data)) { | |
for (var i = 0; i < data.length; i++) { | |
data[i] = new type0(data[i]) | |
} | |
} | |
else return new type0(data) | |
} | |
return data | |
} | |
return {request: request, jsonp: jsonp, setCompletionCallback: setCompletionCallback} | |
} | |
var requestService = _9(window, PromisePolyfill) | |
var coreRenderer = function($window) { | |
var $doc = $window.document | |
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 "#": 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 match1 = 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[match1[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 children1 = vnode.children | |
createNodes(fragment, children1, 0, children1.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 attrs2 = vnode.attrs | |
var is = attrs2 && attrs2.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 (attrs2 != null) { | |
setAttrs(vnode, attrs2, ns) | |
} | |
insertNode(parent, element, nextSibling) | |
if (attrs2 != null && attrs2.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 children1 = vnode.children | |
createNodes(element, children1, 0, children1.length, hooks, null, ns) | |
if (vnode.tag === "select" && attrs2 != null) setLateSelectAttrs(vnode, attrs2) | |
} | |
} | |
} | |
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) | |
} | |
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) { | |
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 last0 `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 next0 DOM node if we're dealing with a | |
* fragment that is not the last0 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) match1 nodes in both lists, per key2, 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 => index0 (for the old list), then1 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 | |
// match1 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 `key2 => 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 last0 node if the list is fully reversed). | |
// | |
// ## Finding the next0 sibling. | |
// | |
// `updateNode()` and `createNode()` expect a nextSibling parameter to perform DOM operations. | |
// When the list is being traversed top-down, at any index0, 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 next0 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 next0 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 runs0 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 start = 0, oldStart = 0, isOldKeyed = null, isKeyed = null | |
for (; oldStart < old.length; oldStart++) { | |
if (old[oldStart] != null) { | |
isOldKeyed = old[oldStart].key != null | |
break | |
} | |
} | |
for (; start < vnodes.length; start++) { | |
if (vnodes[start] != null) { | |
isKeyed = vnodes[start].key != null | |
break | |
} | |
} | |
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 index0 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 index0 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 == null) oldEnd-- | |
else if (ve == null) end-- | |
else if (oe.key === ve.key) { | |
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) | |
if (ve.dom != null) nextSibling = ve.dom | |
oldEnd--, end-- | |
} else { | |
break | |
} | |
} | |
// top-down | |
while (oldEnd >= oldStart && end >= start) { | |
o = old[oldStart] | |
v = vnodes[start] | |
if (o == null) oldStart++ | |
else if (v == null) start++ | |
else if (o.key === v.key) { | |
oldStart++, start++ | |
if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns) | |
} else { | |
break | |
} | |
} | |
// swaps and list reversals | |
while (oldEnd >= oldStart && end >= start) { | |
if (o == null) oldStart++ | |
else if (v == null) start++ | |
else if (oe == null) oldEnd-- | |
else if (ve == null) end-- | |
else if (start === end) break | |
else { | |
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 == null) oldEnd-- | |
else if (ve == null) end-- | |
else if (oe.key === ve.key) { | |
if (oe !== ve) updateNode(parent, oe, ve, hooks, nextSibling, ns) | |
if (ve.dom != null) nextSibling = ve.dom | |
oldEnd--, end-- | |
} else { | |
break | |
} | |
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] | |
if (ve != null) { | |
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, children1 = vnode.children | |
vnode.dom = null | |
if (children1 != null) { | |
for (var i = 0; i < children1.length; i++) { | |
var child = children1[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 handle0 multiple children1 | |
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, 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") | |
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, 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 = {} | |
for (; start < end; start++) { | |
var vnode = vnodes[start] | |
if (vnode != null) { | |
var key2 = vnode.key | |
if (key2 != null) map[key2] = 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 | |
function makeLisIndices(a) { | |
var p = a.slice() | |
var result = [] | |
result.push(0) | |
var u | |
var v | |
for (var i = 0, il = a.length; i < il; ++i) { | |
if (a[i] === -1) { | |
continue | |
} | |
var j = result[result.length - 1] | |
if (a[j] < a[i]) { | |
p[i] = j | |
result.push(i) | |
continue | |
} | |
u = 0 | |
v = result.length - 1 | |
while (u < v) { | |
var c = ((u + v) / 2) | 0 // eslint-disable-line no-bitwise | |
if (a[result[c]] < a[i]) { | |
u = c + 1 | |
} | |
else { | |
v = c | |
} | |
} | |
if (a[i] < a[result[u]]) { | |
if (u > 0) { | |
p[i] = result[u - 1] | |
} | |
result[u] = i | |
} | |
} | |
u = result.length | |
v = result[u - 1] | |
while (u-- > 0) { | |
result[u] = v | |
v = p[v] | |
} | |
return result | |
} | |
function toFragment(vnode) { | |
var count0 = vnode.domSize | |
if (count0 != null || vnode.dom == null) { | |
var fragment = $doc.createDocumentFragment() | |
if (count0 > 0) { | |
var dom = vnode.dom | |
while (--count0) 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 setContentEditable(vnode) { | |
var children1 = vnode.children | |
if (children1 != null && children1.length === 1 && children1[0].tag === "<") { | |
var content = children1[0].children | |
if (vnode.dom.innerHTML !== content) vnode.dom.innerHTML = content | |
} | |
else if (vnode.text != null || children1 != null && children1.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 (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 count0 = vnode.domSize || 1 | |
if (count0 > 1) { | |
var dom = vnode.dom | |
while (--count0) { | |
removeNodeFromDOM(dom.nextSibling) | |
} | |
} | |
removeNodeFromDOM(vnode.dom) | |
} | |
} | |
} | |
} | |
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 children1 = vnode.children | |
if (Array.isArray(children1)) { | |
for (var i = 0; i < children1.length; i++) { | |
var child = children1[i] | |
if (child != null) onremove(child) | |
} | |
} | |
} | |
} | |
//attrs2 | |
function setAttrs(vnode, attrs2, ns) { | |
for (var key2 in attrs2) { | |
setAttr(vnode, key2, null, attrs2[key2], ns) | |
} | |
} | |
function setAttr(vnode, key2, old, value, ns) { | |
if (key2 === "key" || key2 === "is" || value == null || isLifecycleMethod(key2) || (old === value && !isFormAttribute(vnode, key2)) && typeof value !== "object") return | |
if (key2[0] === "o" && key2[1] === "n") return updateEvent(vnode, key2, value) | |
if (key2.slice(0, 6) === "xlink:") vnode.dom.setAttributeNS("http://www.w3.org/1999/xlink", key2.slice(6), value) | |
else if (key2 === "style") updateStyle(vnode.dom, old, value) | |
else if (hasPropertyKey(vnode, key2, ns)) { | |
if (key2 === "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 === $doc.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 type1 that is not supported by IE 11 with an assignment expression, an error1 will occur. | |
if (vnode.tag === "input" && key2 === "type") vnode.dom.setAttribute(key2, value) | |
else vnode.dom[key2] = value | |
} else { | |
if (typeof value === "boolean") { | |
if (value) vnode.dom.setAttribute(key2, "") | |
else vnode.dom.removeAttribute(key2) | |
} | |
else vnode.dom.setAttribute(key2 === "className" ? "class" : key2, value) | |
} | |
} | |
function removeAttr(vnode, key2, old, ns) { | |
if (key2 === "key" || key2 === "is" || old == null || isLifecycleMethod(key2)) return | |
if (key2[0] === "o" && key2[1] === "n" && !isLifecycleMethod(key2)) updateEvent(vnode, key2, undefined) | |
else if (key2 === "style") updateStyle(vnode.dom, old, null) | |
else if ( | |
hasPropertyKey(vnode, key2, ns) | |
&& key2 !== "className" | |
&& !(vnode.tag === "option" && key2 === "value") | |
&& !(vnode.tag === "input" && key2 === "type") | |
) { | |
vnode.dom[key2] = null | |
} else { | |
var nsLastIndex = key2.indexOf(":") | |
if (nsLastIndex !== -1) key2 = key2.slice(nsLastIndex + 1) | |
if (old !== false) vnode.dom.removeAttribute(key2 === "className" ? "class" : key2) | |
} | |
} | |
function setLateSelectAttrs(vnode, attrs2) { | |
if ("value" in attrs2) { | |
if(attrs2.value === null) { | |
if (vnode.dom.selectedIndex !== -1) vnode.dom.value = null | |
} else { | |
var normalized = "" + attrs2.value // eslint-disable-line no-implicit-coercion | |
if (vnode.dom.value !== normalized || vnode.dom.selectedIndex === -1) { | |
vnode.dom.value = normalized | |
} | |
} | |
} | |
if ("selectedIndex" in attrs2) setAttr(vnode, "selectedIndex", null, attrs2.selectedIndex, undefined) | |
} | |
function updateAttrs(vnode, old, attrs2, ns) { | |
if (attrs2 != null) { | |
for (var key2 in attrs2) { | |
setAttr(vnode, key2, old && old[key2], attrs2[key2], ns) | |
} | |
} | |
var val | |
if (old != null) { | |
for (var key2 in old) { | |
if (((val = old[key2]) != null) && (attrs2 == null || attrs2[key2] == null)) { | |
removeAttr(vnode, key2, val, ns) | |
} | |
} | |
} | |
} | |
function isFormAttribute(vnode, attr) { | |
return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === $doc.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, key2, 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" && key2 !== "list" && key2 !== "form" && key2 !== "width" && key2 !== "height"// && key2 !== "type" | |
// Defer the property check until *after* we check everything. | |
) && key2 in vnode.dom | |
} | |
//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 key2 in style) { | |
if (style[key2] !== old[key2]) element.style[key2] = style[key2] | |
} | |
// Remove style properties that no longer exist | |
for (var key2 in old) { | |
if (!(key2 in style)) element.style[key2] = "" | |
} | |
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 key2 in style) { | |
element.style[key2] = style[key2] | |
} | |
} | |
} | |
// 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 handler0 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() {} | |
EventDict.prototype = Object.create(null) | |
EventDict.prototype.handleEvent = function (ev) { | |
var handler0 = this["on" + ev.type] | |
var result | |
if (typeof handler0 === "function") result = handler0.call(ev.target, ev) | |
else if (typeof handler0.handleEvent === "function") handler0.handleEvent(ev) | |
if (typeof onevent === "function") onevent.call(ev.target, ev) | |
if (result === false) { | |
ev.preventDefault() | |
ev.stopPropagation() | |
} | |
} | |
//event | |
function updateEvent(vnode, key2, value) { | |
if (vnode.events != null) { | |
if (vnode.events[key2] === value) return | |
if (value != null && (typeof value === "function" || typeof value === "object")) { | |
if (vnode.events[key2] == null) vnode.dom.addEventListener(key2.slice(2), vnode.events, false) | |
vnode.events[key2] = value | |
} else { | |
if (vnode.events[key2] != null) vnode.dom.removeEventListener(key2.slice(2), vnode.events, false) | |
vnode.events[key2] = undefined | |
} | |
} else if (value != null && (typeof value === "function" || typeof value === "object")) { | |
vnode.events = new EventDict() | |
vnode.dom.addEventListener(key2.slice(2), vnode.events, false) | |
vnode.events[key2] = 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 rendering0 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), hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) | |
dom.vnodes = vnodes | |
// document.activeElement can return null in IE https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement | |
if (active != null && $doc.activeElement !== active && typeof active.focus === "function") active.focus() | |
for (var i = 0; i < hooks.length; i++) hooks[i]() | |
} | |
return {render: render, setEventCallback: setEventCallback} | |
} | |
function throttle(callback) { | |
//60fps translates to 16.6ms, round it down since setTimeout requires int | |
var delay = 16 | |
var last = 0, pending = null | |
var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout | |
return function() { | |
var elapsed = Date.now() - last | |
if (pending === null) { | |
pending = timeout(function() { | |
pending = null | |
callback() | |
last = Date.now() | |
}, delay - elapsed) | |
} | |
} | |
} | |
var _12 = function($window, throttleMock) { | |
var renderService = coreRenderer($window) | |
renderService.setEventCallback(function(e) { | |
if (e.redraw === false) e.redraw = undefined | |
else redraw() | |
}) | |
var callbacks = [] | |
var rendering = false | |
function subscribe(key1, callback) { | |
unsubscribe(key1) | |
callbacks.push(key1, callback) | |
} | |
function unsubscribe(key1) { | |
var index = callbacks.indexOf(key1) | |
if (index > -1) callbacks.splice(index, 2) | |
} | |
function sync() { | |
if (rendering) throw new Error("Nested m.redraw.sync() call") | |
rendering = true | |
for (var i = 1; i < callbacks.length; i+=2) try {callbacks[i]()} catch (e) {if (typeof console !== "undefined") console.error(e)} | |
rendering = false | |
} | |
var redraw = (throttleMock || throttle)(sync) | |
redraw.sync = sync | |
return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} | |
} | |
var redrawService = _12(window) | |
requestService.setCompletionCallback(redrawService.redraw) | |
var _17 = function(redrawService0) { | |
return function(root, component) { | |
if (component === null) { | |
redrawService0.render(root, []) | |
redrawService0.unsubscribe(root) | |
return | |
} | |
if (component.view == null && typeof component !== "function") throw new Error("m.mount(element, component) expects a component, not a vnode") | |
var run0 = function() { | |
redrawService0.render(root, Vnode(component)) | |
} | |
redrawService0.subscribe(root, run0) | |
run0() | |
} | |
} | |
m.mount = _17(redrawService) | |
var Promise = PromisePolyfill | |
var parseQueryString = function(string) { | |
if (string === "" || string == null) return {} | |
if (string.charAt(0) === "?") string = string.slice(1) | |
var entries = string.split("&"), data0 = {}, counters = {} | |
for (var i = 0; i < entries.length; i++) { | |
var entry = entries[i].split("=") | |
var key5 = decodeURIComponent(entry[0]) | |
var value = entry.length === 2 ? decodeURIComponent(entry[1]) : "" | |
if (value === "true") value = true | |
else if (value === "false") value = false | |
var levels = key5.split(/\]\[?|\[/) | |
var cursor = data0 | |
if (key5.indexOf("[") > -1) levels.pop() | |
for (var j0 = 0; j0 < levels.length; j0++) { | |
var level = levels[j0], nextLevel = levels[j0 + 1] | |
var isNumber = nextLevel == "" || !isNaN(parseInt(nextLevel, 10)) | |
var isValue = j0 === levels.length - 1 | |
if (level === "") { | |
var key5 = levels.slice(0, j0).join() | |
if (counters[key5] == null) counters[key5] = 0 | |
level = counters[key5]++ | |
} | |
if (cursor[level] == null) { | |
cursor[level] = isValue ? value : isNumber ? [] : {} | |
} | |
cursor = cursor[level] | |
} | |
} | |
return data0 | |
} | |
var coreRouter = function($window) { | |
var supportsPushState = typeof $window.history.pushState === "function" | |
var callAsync0 = typeof setImmediate === "function" ? setImmediate : setTimeout | |
function normalize1(fragment0) { | |
var data = $window.location[fragment0].replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent) | |
if (fragment0 === "pathname" && data[0] !== "/") data = "/" + data | |
return data | |
} | |
var asyncId | |
function debounceAsync(callback) { | |
return function() { | |
if (asyncId != null) return | |
asyncId = callAsync0(function() { | |
asyncId = null | |
callback() | |
}) | |
} | |
} | |
function parsePath(path, queryData, hashData) { | |
var queryIndex = path.indexOf("?") | |
var hashIndex = path.indexOf("#") | |
var pathEnd = queryIndex > -1 ? queryIndex : hashIndex > -1 ? hashIndex : path.length | |
if (queryIndex > -1) { | |
var queryEnd = hashIndex > -1 ? hashIndex : path.length | |
var queryParams = parseQueryString(path.slice(queryIndex + 1, queryEnd)) | |
for (var key4 in queryParams) queryData[key4] = queryParams[key4] | |
} | |
if (hashIndex > -1) { | |
var hashParams = parseQueryString(path.slice(hashIndex + 1)) | |
for (var key4 in hashParams) hashData[key4] = hashParams[key4] | |
} | |
return path.slice(0, pathEnd) | |
} | |
var router = {prefix: "#!"} | |
router.getPath = function() { | |
var type2 = router.prefix.charAt(0) | |
switch (type2) { | |
case "#": return normalize1("hash").slice(router.prefix.length) | |
case "?": return normalize1("search").slice(router.prefix.length) + normalize1("hash") | |
default: return normalize1("pathname").slice(router.prefix.length) + normalize1("search") + normalize1("hash") | |
} | |
} | |
router.setPath = function(path, data, options) { | |
var queryData = {}, hashData = {} | |
path = parsePath(path, queryData, hashData) | |
if (data != null) { | |
for (var key4 in data) queryData[key4] = data[key4] | |
path = path.replace(/:([^\/]+)/g, function(match2, token) { | |
delete queryData[token] | |
return data[token] | |
}) | |
} | |
var query = buildQueryString(queryData) | |
if (query) path += "?" + query | |
var hash = buildQueryString(hashData) | |
if (hash) path += "#" + hash | |
if (supportsPushState) { | |
var state = options ? options.state : null | |
var title = options ? options.title : null | |
$window.onpopstate() | |
if (options && options.replace) $window.history.replaceState(state, title, router.prefix + path) | |
else $window.history.pushState(state, title, router.prefix + path) | |
} | |
else $window.location.href = router.prefix + path | |
} | |
router.defineRoutes = function(routes, resolve, reject) { | |
function resolveRoute() { | |
var path = router.getPath() | |
var params = {} | |
var pathname = parsePath(path, params, params) | |
var state = $window.history.state | |
if (state != null) { | |
for (var k in state) params[k] = state[k] | |
} | |
for (var route0 in routes) { | |
var matcher = new RegExp("^" + route0.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") | |
if (matcher.test(pathname)) { | |
pathname.replace(matcher, function() { | |
var keys = route0.match(/:[^\/]+/g) || [] | |
var values = [].slice.call(arguments, 1, -2) | |
for (var i = 0; i < keys.length; i++) { | |
params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) | |
} | |
resolve(routes[route0], params, path, route0) | |
}) | |
return | |
} | |
} | |
reject(path, params) | |
} | |
if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) | |
else if (router.prefix.charAt(0) === "#") $window.onhashchange = resolveRoute | |
resolveRoute() | |
} | |
return router | |
} | |
var _21 = function($window, redrawService0) { | |
var routeService = coreRouter($window) | |
var identity = function(v0) {return v0} | |
var render1, component, attrs3, currentPath, lastUpdate | |
var route = function(root, defaultRoute, routes) { | |
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined") | |
function run1() { | |
if (render1 != null) redrawService0.render(root, render1(Vnode(component, attrs3.key, attrs3))) | |
} | |
var redraw2 = function() { | |
run1() | |
redraw2 = redrawService0.redraw | |
} | |
redrawService0.subscribe(root, run1) | |
var bail = function(path) { | |
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true}) | |
else throw new Error("Could not resolve default route " + defaultRoute) | |
} | |
routeService.defineRoutes(routes, function(payload, params, path) { | |
var update = lastUpdate = function(routeResolver, comp) { | |
if (update !== lastUpdate) return | |
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div" | |
attrs3 = params, currentPath = path, lastUpdate = null | |
render1 = (routeResolver.render || identity).bind(routeResolver) | |
redraw2() | |
} | |
if (payload.view || typeof payload === "function") update({}, payload) | |
else { | |
if (payload.onmatch) { | |
Promise.resolve(payload.onmatch(params, path)).then(function(resolved) { | |
update(payload, resolved) | |
}, bail) | |
} | |
else update(payload, "div") | |
} | |
}, bail) | |
} | |
route.set = function(path, data, options) { | |
if (lastUpdate != null) { | |
options = options || {} | |
options.replace = true | |
} | |
lastUpdate = null | |
routeService.setPath(path, data, options) | |
} | |
route.get = function() {return currentPath} | |
route.prefix = function(prefix0) {routeService.prefix = prefix0} | |
var link = function(options, vnode1) { | |
vnode1.dom.setAttribute("href", routeService.prefix + vnode1.attrs.href) | |
vnode1.dom.onclick = function(e) { | |
if (e.ctrlKey || e.metaKey || e.shiftKey || e.which === 2) return | |
e.preventDefault() | |
e.redraw = false | |
var href = this.getAttribute("href") | |
if (href.indexOf(routeService.prefix) === 0) href = href.slice(routeService.prefix.length) | |
route.set(href, undefined, options) | |
} | |
} | |
route.link = function(args0) { | |
if (args0.tag == null) return link.bind(link, args0) | |
return link({}, args0) | |
} | |
route.param = function(key3) { | |
if(typeof attrs3 !== "undefined" && typeof key3 !== "undefined") return attrs3[key3] | |
return attrs3 | |
} | |
return route | |
} | |
m.route = _21(window, redrawService) | |
m.withAttr = function(attrName, callback, context) { | |
return function(e) { | |
callback.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) | |
} | |
} | |
var _29 = coreRenderer(window) | |
m.render = _29.render | |
m.redraw = redrawService.redraw | |
m.request = requestService.request | |
m.jsonp = requestService.jsonp | |
m.parseQueryString = parseQueryString | |
m.buildQueryString = buildQueryString | |
m.version = "1.1.3" | |
m.vnode = Vnode | |
m.PromisePolyfill = PromisePolyfill | |
if (typeof module !== "undefined") module["exports"] = m | |
else window.m = m | |
}()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment