diff --git a/webui-src/app/mithril.js b/webui-src/app/mithril.js index b60c1b2..eca571d 100644 --- a/webui-src/app/mithril.js +++ b/webui-src/app/mithril.js @@ -1,13 +1,14 @@ (function () { 'use strict'; - function Vnode(tag, key, attrs0, children0, text, dom) { + function Vnode(tag, key, attrs0, children, text, dom) { return { tag: tag, key: key, attrs: attrs0, - children: children0, + children: children, text: text, dom: dom, + is: undefined, domSize: undefined, state: undefined, events: undefined, @@ -22,83 +23,76 @@ return Vnode('#', undefined, undefined, String(node), undefined, undefined); }; Vnode.normalizeChildren = function (input) { - var children0 = []; - if (input.length) { - var isKeyed = input[0] != null && input[0].key != null; - // Note: this is a *very* perf-sensitive check. - // Fun fact: merging the loop like this is somehow faster than splitting - // it, noticeably so. - for (var i = 1; i < input.length; i++) { - if ((input[i] != null && input[i].key != null) !== isKeyed) { - throw new TypeError('Vnodes must either always have keys or never have keys!'); - } - } - for (var i = 0; i < input.length; i++) { - children0[i] = Vnode.normalize(input[i]); - } + // Preallocate the array length (initially holey) and fill every index immediately in order. + // Benchmarking shows better performance on V8. + var children = new Array(input.length); + // Count the number of keyed normalized vnodes for consistency check. + // Note: this is a perf-sensitive check. + // Fun fact: merging the loop like this is somehow faster than splitting + // the check within updateNodes(), noticeably so. + var numKeyed = 0; + for (var i = 0; i < input.length; i++) { + children[i] = Vnode.normalize(input[i]); + if (children[i] !== null && children[i].key != null) numKeyed++; } - return children0; + if (numKeyed !== 0 && numKeyed !== input.length) { + throw new TypeError( + children.includes(null) + ? 'In fragments, vnodes must either all have keys or none have keys. You may wish to consider using an explicit keyed empty fragment, m.fragment({key: ...}), instead of a hole.' + : 'In fragments, vnodes must either all have keys or none have keys.' + ); + } + return children; }; - // Call via `hyperscriptVnode0.apply(startOffset, arguments)` + // Note: the processing of variadic parameters is perf-sensitive. // - // The reason I do it this way, forwarding the arguments and passing the start - // offset in `this`, is so I don't have to create a temporary array in a - // performance-critical path. + // In native ES6, it might be preferable to define hyperscript and fragment + // factories with a final ...args parameter and call hyperscriptVnode(...args), + // since modern engines can optimize spread calls. // - // In native ES6, I'd instead add a final `...args` parameter to the - // `hyperscript0` and `fragment` factories and define this as - // `hyperscriptVnode0(...args)`, since modern engines do optimize that away. But - // ES5 (what Mithril requires thanks to IE support) doesn't give me that luxury, - // and engines aren't nearly intelligent enough to do either of these: - // - // 1. Elide the allocation for `[].slice.call(arguments, 1)` when it's passed to - // another function only to be indexed. - // 2. Elide an `arguments` allocation when it's passed to any function other - // than `Function.prototype.apply` or `Reflect.apply`. - // - // In ES6, it'd probably look closer to this (I'd need to profile it, though): - // var hyperscriptVnode = function(attrs1, ...children1) { - // if (attrs1 == null || typeof attrs1 === "object" && attrs1.tag == null && !Array.isArray(attrs1)) { - // if (children1.length === 1 && Array.isArray(children1[0])) children1 = children1[0] - // } else { - // children1 = children1.length === 0 && Array.isArray(attrs1) ? attrs1 : [attrs1, ...children1] - // attrs1 = undefined - // } - // - // if (attrs1 == null) attrs1 = {} - // return Vnode("", attrs1.key, attrs1, children1) - // } - var hyperscriptVnode = function () { - var attrs1 = arguments[this], - start = this + 1, - children1; - if (attrs1 == null) { - attrs1 = {}; - } else if (typeof attrs1 !== 'object' || attrs1.tag != null || Array.isArray(attrs1)) { - attrs1 = {}; - start = this; - } - if (arguments.length === start + 1) { - children1 = arguments[start]; - if (!Array.isArray(children1)) children1 = [children1]; + // However, benchmarks showed this was not faster. As a result, spread is used + // only in the parameter lists of hyperscript and fragment, while an array is + // passed to hyperscriptVnode. + var hyperscriptVnode = function (attrs1, children0) { + if ( + attrs1 == null || + (typeof attrs1 === 'object' && attrs1.tag == null && !Array.isArray(attrs1)) + ) { + if (children0.length === 1 && Array.isArray(children0[0])) children0 = children0[0]; } else { - children1 = []; - while (start < arguments.length) children1.push(arguments[start++]); + children0 = children0.length === 0 && Array.isArray(attrs1) ? attrs1 : [attrs1, ...children0]; + attrs1 = undefined; } - return Vnode('', attrs1.key, attrs1, children1); + return Vnode('', attrs1 && attrs1.key, attrs1, children0); }; - var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g; - var selectorCache = {}; + // This exists so I'm only saving it once. var hasOwn = {}.hasOwnProperty; + // This is an attrs object that is used by default when attrs is undefined or null. + var emptyAttrs = {}; + // This Map manages the following: + // - Whether an attrs is cached attrs generated by compileSelector(). + // - Whether the cached attrs is "static", i.e., does not contain any form attributes. + // These information will be useful to skip updating attrs in render(). + // + // Since the attrs used as keys in this map are not released from the selectorCache object, + // there is no risk of memory leaks. Therefore, Map is used here instead of WeakMap. + var cachedAttrsIsStaticMap = new Map([[emptyAttrs, true]]); + var selectorParser = + /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g; + var selectorCache = Object.create(null); function isEmpty(object) { for (var key in object) if (hasOwn.call(object, key)) return false; return true; } + function isFormAttributeKey(key) { + return key === 'value' || key === 'checked' || key === 'selectedIndex' || key === 'selected'; + } function compileSelector(selector) { var match, tag = 'div', classes = [], - attrs = {}; + attrs = {}, + isStatic = true; while ((match = selectorParser.exec(selector))) { var type = match[1], value = match[2]; @@ -109,61 +103,50 @@ 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; + else { + attrs[match[4]] = attrValue === '' ? attrValue : attrValue || true; + if (isFormAttributeKey(match[4])) isStatic = false; + } } } if (classes.length > 0) attrs.className = classes.join(' '); - return (selectorCache[selector] = { tag: tag, attrs: attrs }); + if (isEmpty(attrs)) attrs = emptyAttrs; + else cachedAttrsIsStaticMap.set(attrs, isStatic); + return (selectorCache[selector] = { tag: tag, attrs: attrs, is: attrs.is }); } function execSelector(state, vnode) { - var attrs = vnode.attrs; - var children = Vnode.normalizeChildren(vnode.children); - var hasClass = hasOwn.call(attrs, 'class'); - var className = hasClass ? attrs.class : attrs.className; vnode.tag = state.tag; - vnode.attrs = null; - vnode.children = undefined; - if (!isEmpty(state.attrs) && !isEmpty(attrs)) { - var newAttrs = {}; - for (var key in attrs) { - if (hasOwn.call(attrs, key)) newAttrs[key] = attrs[key]; - } - attrs = newAttrs; + var attrs = vnode.attrs; + if (attrs == null) { + vnode.attrs = state.attrs; + vnode.is = state.is; + return vnode; } - for (var key in state.attrs) { - if (hasOwn.call(state.attrs, key) && key !== 'className' && !hasOwn.call(attrs, key)) { - attrs[key] = state.attrs[key]; - } + if (hasOwn.call(attrs, 'class')) { + if (attrs.class != null) attrs.className = attrs.class; + attrs.class = null; } - if (className != null || state.attrs.className != null) - attrs.className = - className != null - ? state.attrs.className != null + if (state.attrs !== emptyAttrs) { + var className = attrs.className; + attrs = Object.assign({}, state.attrs, attrs); + if (state.attrs.className != null) + attrs.className = + className != null ? String(state.attrs.className) + ' ' + String(className) - : className - : state.attrs.className != null - ? state.attrs.className - : null; - if (hasClass) attrs.class = null; - for (var key in attrs) { - if (hasOwn.call(attrs, key) && key !== 'key') { - vnode.attrs = attrs; - break; - } + : state.attrs.className; } - if ( - Array.isArray(children) && - children.length === 1 && - children[0] != null && - children[0].tag === '#' - ) { - vnode.text = children[0].children; - } else { - vnode.children = children; + // workaround for #2622 (reorder keys in attrs to set "type" first) + // The DOM does things to inputs based on the "type", so it needs set first. + // See: https://github.com/MithrilJS/mithril.js/issues/2622 + if (state.tag === 'input' && hasOwn.call(attrs, 'type')) { + attrs = Object.assign({ type: attrs.type }, attrs); } + // This reduces the complexity of the evaluation of "is" within the render function. + vnode.is = attrs.is; + vnode.attrs = attrs; return vnode; } - function hyperscript(selector) { + function hyperscript(selector, attrs, ...children) { if ( selector == null || (typeof selector !== 'string' && @@ -172,12 +155,13 @@ ) { throw Error('The selector must be either a string or a component.'); } - var vnode = hyperscriptVnode.apply(1, arguments); + var vnode = hyperscriptVnode(attrs, children); if (typeof selector === 'string') { vnode.children = Vnode.normalizeChildren(vnode.children); if (selector !== '[') return execSelector(selectorCache[selector] || compileSelector(selector), vnode); } + if (vnode.attrs == null) vnode.attrs = {}; vnode.tag = selector; return vnode; } @@ -185,185 +169,50 @@ if (html == null) html = ''; return Vnode('<', undefined, undefined, html, undefined, undefined); }; - hyperscript.fragment = function () { - var vnode2 = hyperscriptVnode.apply(0, arguments); + hyperscript.fragment = function (attrs4, ...children1) { + var vnode2 = hyperscriptVnode(attrs4, children1); + if (vnode2.attrs == null) vnode2.attrs = {}; vnode2.tag = '['; vnode2.children = Vnode.normalizeChildren(vnode2.children); return vnode2; }; - /** @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); + var delayedRemoval = new WeakMap(); + function* domFor(vnode4) { + // To avoid unintended mangling of the internal bundler, + // parameter destructuring is not used here. + var dom = vnode4.dom; + var domSize0 = vnode4.domSize; + var generation0 = delayedRemoval.get(dom); + if (dom != null) + do { + var nextSibling = dom.nextSibling; + if (delayedRemoval.get(dom) === generation0) { + yield dom; + domSize0--; } - }; - } - 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 { + dom = nextSibling; + } while (domSize0); } - var _12 = function ($window) { - var $doc = $window && $window.document; - var currentRedraw; + var _14 = function () { var nameSpace = { svg: 'http://www.w3.org/2000/svg', math: 'http://www.w3.org/1998/Math/MathML', }; + var currentRedraw; + var currentRender; + function getDocument(dom) { + return dom.ownerDocument; + } function getNameSpace(vnode3) { return (vnode3.attrs && vnode3.attrs.xmlns) || nameSpace[vnode3.tag]; } - //sanity check to discourage people from doing `vnode3.state = ...` + //sanity check to discourage people from doing `vnode.state = ...` function checkState(vnode3, original) { - if (vnode3.state !== original) throw new Error('`vnode.state` must not be modified'); + if (vnode3.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 `vnode3` is the first argument in + //takes advantage of the fact the current `vnode` is the first argument in //all lifecycle methods. function callHook(vnode3) { var original = vnode3.state; @@ -375,9 +224,9 @@ } // 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() { + function activeElement(dom) { try { - return $doc.activeElement; + return getDocument(dom).activeElement; } catch (e) { return null; } @@ -412,8 +261,8 @@ } else createComponent(parent, vnode3, hooks, ns, nextSibling); } function createText(parent, vnode3, nextSibling) { - vnode3.dom = $doc.createTextNode(vnode3.children); - insertNode(parent, vnode3.dom, nextSibling); + vnode3.dom = getDocument(parent).createTextNode(vnode3.children); + insertDOM(parent, vnode3.dom, nextSibling); } var possibleParents = { caption: 'table', @@ -433,7 +282,7 @@ // div.innerHTML = "ij" // console.log(div.innerHTML) // --> "ij", no in sight. - var temp = $doc.createElement(possibleParents[match0[1]] || 'div'); + var temp = getDocument(parent).createElement(possibleParents[match0[1]] || 'div'); if (ns === 'http://www.w3.org/2000/svg') { temp.innerHTML = '' + vnode3.children + ''; temp = temp.firstChild; @@ -442,53 +291,45 @@ } vnode3.dom = temp.firstChild; vnode3.domSize = temp.childNodes.length; - // Capture nodes to remove, so we don't confuse them. - vnode3.instance = []; - var fragment = $doc.createDocumentFragment(); + var fragment = getDocument(parent).createDocumentFragment(); var child; while ((child = temp.firstChild)) { - vnode3.instance.push(child); fragment.appendChild(child); } - insertNode(parent, fragment, nextSibling); + insertDOM(parent, fragment, nextSibling); } function createFragment(parent, vnode3, hooks, ns, nextSibling) { - var fragment = $doc.createDocumentFragment(); + var fragment = getDocument(parent).createDocumentFragment(); if (vnode3.children != null) { - var children3 = vnode3.children; - createNodes(fragment, children3, 0, children3.length, hooks, null, ns); + var children2 = vnode3.children; + createNodes(fragment, children2, 0, children2.length, hooks, null, ns); } vnode3.dom = fragment.firstChild; vnode3.domSize = fragment.childNodes.length; - insertNode(parent, fragment, nextSibling); + insertDOM(parent, fragment, nextSibling); } function createElement(parent, vnode3, hooks, ns, nextSibling) { var tag = vnode3.tag; - var attrs2 = vnode3.attrs; - var is = attrs2 && attrs2.is; + var attrs5 = vnode3.attrs; + var is = vnode3.is; ns = getNameSpace(vnode3) || ns; var element = ns ? is - ? $doc.createElementNS(ns, tag, { is: is }) - : $doc.createElementNS(ns, tag) + ? getDocument(parent).createElementNS(ns, tag, { is: is }) + : getDocument(parent).createElementNS(ns, tag) : is - ? $doc.createElement(tag, { is: is }) - : $doc.createElement(tag); + ? getDocument(parent).createElement(tag, { is: is }) + : getDocument(parent).createElement(tag); vnode3.dom = element; - if (attrs2 != null) { - setAttrs(vnode3, attrs2, ns); + if (attrs5 != null) { + setAttrs(vnode3, attrs5, ns); } - insertNode(parent, element, nextSibling); + insertDOM(parent, element, nextSibling); if (!maybeSetContentEditable(vnode3)) { - if (vnode3.text != null) { - if (vnode3.text !== '') element.textContent = vnode3.text; - else - vnode3.children = [Vnode('#', undefined, undefined, vnode3.text, undefined, undefined)]; - } if (vnode3.children != null) { - var children3 = vnode3.children; - createNodes(element, children3, 0, children3.length, hooks, null, ns); - if (vnode3.tag === 'select' && attrs2 != null) setLateSelectAttrs(vnode3, attrs2); + var children2 = vnode3.children; + createNodes(element, children2, 0, children2.length, hooks, null, ns); + if (vnode3.tag === 'select' && attrs5 != null) setLateSelectAttrs(vnode3, attrs5); } } } @@ -521,7 +362,7 @@ if (vnode3.instance != null) { createNode(parent, vnode3.instance, hooks, ns, nextSibling); vnode3.dom = vnode3.instance.dom; - vnode3.domSize = vnode3.dom != null ? vnode3.instance.domSize : 0; + vnode3.domSize = vnode3.instance.domSize; } else { vnode3.domSize = 0; } @@ -529,10 +370,10 @@ //update /** * @param {Element|Fragment} parent - the parent element - * @param {Vnode[] | null} old - the list of vnodes of the last `render0()` call for + * @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 `render0()` call. - * @param {Function[]} hooks - an accumulator of post-render0 hooks (oncreate/onupdate) + * @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 @@ -567,13 +408,13 @@ // // In order to diff keyed lists, one has to // - // 1) match0 nodes in both lists, per key, and update them accordingly + // 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), then0 iterate - // over the new list and for each new vnode3, find the corresponding vnode3 in the old list using + // 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. @@ -585,7 +426,7 @@ // 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 - // match0 the above lists, for example). + // 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`. @@ -607,7 +448,7 @@ // // `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 - // vnode3 reflect the content of the new list, whereas the rest of the DOM nodes reflect the old + // 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), @@ -618,13 +459,8 @@ // // 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` + // 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) @@ -632,16 +468,15 @@ else if (vnodes == null || vnodes.length === 0) removeNodes(parent, old, 0, old.length); else { var isOldKeyed = old[0] != null && old[0].key != null; - var isKeyed0 = vnodes[0] != null && vnodes[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 (!isKeyed0) while (start < vnodes.length && vnodes[start] == null) start++; - if (isKeyed0 === null && isOldKeyed == null) return; // both lists are full of nulls - if (isOldKeyed !== isKeyed0) { + if (!isKeyed) while (start < vnodes.length && vnodes[start] == null) start++; + if (isOldKeyed !== isKeyed) { removeNodes(parent, old, oldStart, old.length); createNodes(parent, vnodes, start, vnodes.length, hooks, nextSibling, ns); - } else if (!isKeyed0) { + } 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. @@ -677,14 +512,14 @@ 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--; + (oldEnd--, end--); } // top-down while (oldEnd >= oldStart && end >= start) { o = old[oldStart]; v = vnodes[start]; if (o.key !== v.key) break; - oldStart++, start++; + (oldStart++, start++); if (o !== v) updateNode(parent, o, v, hooks, getNextSibling(old, oldStart, nextSibling), ns); } @@ -693,9 +528,9 @@ if (start === end) break; if (o.key !== ve.key || oe.key !== v.key) break; topSibling = getNextSibling(old, oldStart, nextSibling); - moveNodes(parent, oe, topSibling); + moveDOM(parent, oe, topSibling); if (oe !== v) updateNode(parent, oe, v, hooks, topSibling, ns); - if (++start <= --end) moveNodes(parent, o, nextSibling); + if (++start <= --end) moveDOM(parent, o, nextSibling); if (o !== ve) updateNode(parent, o, ve, hooks, nextSibling, ns); if (ve.dom != null) nextSibling = ve.dom; oldStart++; @@ -710,7 +545,7 @@ 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--; + (oldEnd--, end--); oe = old[oldEnd]; ve = vnodes[end]; } @@ -757,7 +592,7 @@ if (oldIndices[i - start] === -1) createNode(parent, v, hooks, ns, nextSibling); else { if (lisIndices[li] === i - start) li--; - else moveNodes(parent, v, nextSibling); + else moveDOM(parent, v, nextSibling); } if (v.dom != null) nextSibling = vnodes[i].dom; } @@ -776,7 +611,7 @@ function updateNode(parent, old, vnode3, hooks, nextSibling, ns) { var oldTag = old.tag, tag = vnode3.tag; - if (oldTag === tag) { + if (oldTag === tag && old.is === vnode3.is) { vnode3.state = old.state; vnode3.events = old.events; if (shouldNotUpdate(vnode3, old)) return; @@ -811,54 +646,40 @@ } function updateHTML(parent, old, vnode3, ns, nextSibling) { if (old.children !== vnode3.children) { - removeHTML(parent, old); + removeDOM(parent, old); createHTML(parent, vnode3, ns, nextSibling); } else { vnode3.dom = old.dom; vnode3.domSize = old.domSize; - vnode3.instance = old.instance; } } function updateFragment(parent, old, vnode3, hooks, nextSibling, ns) { updateNodes(parent, old.children, vnode3.children, hooks, nextSibling, ns); var domSize = 0, - children3 = vnode3.children; + children2 = vnode3.children; vnode3.dom = null; - if (children3 != null) { - for (var i = 0; i < children3.length; i++) { - var child = children3[i]; + if (children2 != null) { + for (var i = 0; i < children2.length; i++) { + var child = children2[i]; if (child != null && child.dom != null) { if (vnode3.dom == null) vnode3.dom = child.dom; domSize += child.domSize || 1; } } - if (domSize !== 1) vnode3.domSize = domSize; } + vnode3.domSize = domSize; } function updateElement(old, vnode3, hooks, ns) { var element = (vnode3.dom = old.dom); ns = getNameSpace(vnode3) || ns; - if (vnode3.tag === 'textarea') { - if (vnode3.attrs == null) vnode3.attrs = {}; - if (vnode3.text != null) { - vnode3.attrs.value = vnode3.text; //FIXME handle0 multiple children3 - vnode3.text = undefined; - } + if ( + old.attrs != vnode3.attrs || + (vnode3.attrs != null && !cachedAttrsIsStaticMap.get(vnode3.attrs)) + ) { + updateAttrs(vnode3, old.attrs, vnode3.attrs, ns); } - updateAttrs(vnode3, old.attrs, vnode3.attrs, ns); if (!maybeSetContentEditable(vnode3)) { - if (old.text != null && vnode3.text != null && vnode3.text !== '') { - if (old.text.toString() !== vnode3.text.toString()) - old.dom.firstChild.nodeValue = vnode3.text; - } else { - if (old.text != null) - old.children = [ - Vnode('#', undefined, undefined, old.text, undefined, old.dom.firstChild), - ]; - if (vnode3.text != null) - vnode3.children = [Vnode('#', undefined, undefined, vnode3.text, undefined, undefined)]; - updateNodes(element, old.children, vnode3.children, hooks, null, ns); - } + updateNodes(element, old.children, vnode3.children, hooks, null, ns); } } function updateComponent(parent, old, vnode3, hooks, nextSibling, ns) { @@ -872,13 +693,9 @@ else updateNode(parent, old.instance, vnode3.instance, hooks, nextSibling, ns); vnode3.dom = vnode3.instance.dom; vnode3.domSize = vnode3.instance.domSize; - } else if (old.instance != null) { - removeNode(parent, old.instance); - vnode3.dom = undefined; - vnode3.domSize = 0; } else { - vnode3.dom = old.dom; - vnode3.domSize = old.domSize; + if (old.instance != null) removeNode(parent, old.instance); + vnode3.domSize = 0; } } function getKeyMap(vnodes, start, end) { @@ -896,7 +713,7 @@ // 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 + // subsequence var lisTemp = []; function makeLisIndices(a) { var result = [0]; @@ -945,45 +762,21 @@ } return nextSibling; } - // This covers a really specific edge case: - // - Parent node is keyed and contains child - // - Child is removed, returns unresolved promise0 in `onbeforeremove` - // - Parent node is moved in keyed diff - // - Remaining children3 still need moved appropriately - // - // Ideally, I'd track removed nodes as well, but that introduces a lot more - // complexity and I'm0 not exactly interested in doing that. - function moveNodes(parent, vnode3, nextSibling) { - var frag = $doc.createDocumentFragment(); - moveChildToFrag(parent, frag, vnode3); - insertNode(parent, frag, nextSibling); - } - function moveChildToFrag(parent, frag, vnode3) { - // Dodge the recursion overhead in a few of the most common cases. - while (vnode3.dom != null && vnode3.dom.parentNode === parent) { - if (typeof vnode3.tag !== 'string') { - vnode3 = vnode3.instance; - if (vnode3 != null) continue; - } else if (vnode3.tag === '<') { - for (var i = 0; i < vnode3.instance.length; i++) { - frag.appendChild(vnode3.instance[i]); - } - } else if (vnode3.tag !== '[') { - // Don't recurse for text nodes *or* elements, just fragments - frag.appendChild(vnode3.dom); - } else if (vnode3.children.length === 1) { - vnode3 = vnode3.children[0]; - if (vnode3 != null) continue; + // This handles fragments with zombie children (removed from vdom, but persisted in DOM through onbeforeremove) + function moveDOM(parent, vnode3, nextSibling) { + if (vnode3.dom != null) { + var target; + if (vnode3.domSize == null || vnode3.domSize === 1) { + // don't allocate for the common case + target = vnode3.dom; } else { - for (var i = 0; i < vnode3.children.length; i++) { - var child = vnode3.children[i]; - if (child != null) moveChildToFrag(parent, frag, child); - } + target = getDocument(parent).createDocumentFragment(); + for (var dom of domFor(vnode3)) target.appendChild(dom); } - break; + insertDOM(parent, target, nextSibling); } } - function insertNode(parent, dom, nextSibling) { + function insertDOM(parent, dom, nextSibling) { if (nextSibling != null) parent.insertBefore(dom, nextSibling); else parent.appendChild(dom); } @@ -994,12 +787,12 @@ vnode3.attrs.contentEditable == null) // property ) return false; - var children3 = vnode3.children; - if (children3 != null && children3.length === 1 && children3[0].tag === '<') { - var content = children3[0].children; + var children2 = vnode3.children; + if (children2 != null && children2.length === 1 && children2[0].tag === '<') { + var content = children2[0].children; if (vnode3.dom.innerHTML !== content) vnode3.dom.innerHTML = content; - } else if (vnode3.text != null || (children3 != null && children3.length !== 0)) - throw new Error('Child node of a contenteditable must be trusted'); + } else if (children2 != null && children2.length !== 0) + throw new Error('Child node of a contenteditable must be trusted.'); return true; } //remove @@ -1009,87 +802,38 @@ if (vnode3 != null) removeNode(parent, vnode3); } } - function removeNode(parent, vnode3) { - var mask = 0; + function tryBlockRemove(parent, vnode3, source, counter) { var original = vnode3.state; - var stateResult, attrsResult; - if (typeof vnode3.tag !== 'string' && typeof vnode3.state.onbeforeremove === 'function') { - var result = callHook.call(vnode3.state.onbeforeremove, vnode3); - if (result != null && typeof result.then === 'function') { - mask = 1; - stateResult = result; - } - } - if (vnode3.attrs && typeof vnode3.attrs.onbeforeremove === 'function') { - var result = callHook.call(vnode3.attrs.onbeforeremove, vnode3); - if (result != null && typeof result.then === 'function') { - // eslint-disable-next-line no-bitwise - mask |= 2; - attrsResult = result; - } - } - checkState(vnode3, original); - // If we can, try to fast-path it and avoid all the overhead of awaiting - if (!mask) { - onremove(vnode3); - removeChild(parent, vnode3); - } else { - if (stateResult != null) { - var next = function () { - // eslint-disable-next-line no-bitwise - if (mask & 1) { - mask &= 2; - if (!mask) reallyRemove(); - } - }; - stateResult.then(next, next); - } - if (attrsResult != null) { - var next = function () { - // eslint-disable-next-line no-bitwise - if (mask & 2) { - mask &= 1; - if (!mask) reallyRemove(); - } - }; - attrsResult.then(next, next); - } - } - function reallyRemove() { + var result = callHook.call(source.onbeforeremove, vnode3); + if (result == null) return; + var generation = currentRender; + for (var dom of domFor(vnode3)) delayedRemoval.set(dom, generation); + counter.v++; + Promise.resolve(result).finally(function () { checkState(vnode3, original); + tryResumeRemove(parent, vnode3, counter); + }); + } + function tryResumeRemove(parent, vnode3, counter) { + if (--counter.v === 0) { onremove(vnode3); - removeChild(parent, vnode3); + removeDOM(parent, vnode3); } } - function removeHTML(parent, vnode3) { - for (var i = 0; i < vnode3.instance.length; i++) { - parent.removeChild(vnode3.instance[i]); - } + function removeNode(parent, vnode3) { + var counter = { v: 1 }; + if (typeof vnode3.tag !== 'string' && typeof vnode3.state.onbeforeremove === 'function') + tryBlockRemove(parent, vnode3, vnode3.state, counter); + if (vnode3.attrs && typeof vnode3.attrs.onbeforeremove === 'function') + tryBlockRemove(parent, vnode3, vnode3.attrs, counter); + tryResumeRemove(parent, vnode3, counter); } - function removeChild(parent, vnode3) { - // Dodge the recursion overhead in a few of the most common cases. - while (vnode3.dom != null && vnode3.dom.parentNode === parent) { - if (typeof vnode3.tag !== 'string') { - vnode3 = vnode3.instance; - if (vnode3 != null) continue; - } else if (vnode3.tag === '<') { - removeHTML(parent, vnode3); - } else { - if (vnode3.tag !== '[') { - parent.removeChild(vnode3.dom); - if (!Array.isArray(vnode3.children)) break; - } - if (vnode3.children.length === 1) { - vnode3 = vnode3.children[0]; - if (vnode3 != null) continue; - } else { - for (var i = 0; i < vnode3.children.length; i++) { - var child = vnode3.children[i]; - if (child != null) removeChild(parent, child); - } - } - } - break; + function removeDOM(parent, vnode3) { + if (vnode3.dom == null) return; + if (vnode3.domSize == null || vnode3.domSize === 1) { + parent.removeChild(vnode3.dom); + } else { + for (var dom of domFor(vnode3)) parent.removeChild(dom); } } function onremove(vnode3) { @@ -1100,25 +844,25 @@ if (typeof vnode3.tag !== 'string') { if (vnode3.instance != null) onremove(vnode3.instance); } else { - var children3 = vnode3.children; - if (Array.isArray(children3)) { - for (var i = 0; i < children3.length; i++) { - var child = children3[i]; + if (vnode3.events != null) vnode3.events._ = null; + var children2 = vnode3.children; + if (Array.isArray(children2)) { + for (var i = 0; i < children2.length; i++) { + var child = children2[i]; if (child != null) onremove(child); } } } } - //attrs2 - function setAttrs(vnode3, attrs2, ns) { - for (var key in attrs2) { - setAttr(vnode3, key, null, attrs2[key], ns); + //attrs + function setAttrs(vnode3, attrs5, ns) { + for (var key in attrs5) { + setAttr(vnode3, key, null, attrs5[key], ns); } } function setAttr(vnode3, key, old, value, ns) { if ( key === 'key' || - key === 'is' || value == null || isLifecycleMethod(key) || (old === value && !isFormAttribute(vnode3, key) && typeof value !== 'object') @@ -1133,19 +877,26 @@ // 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 + //setting input[type=file][value] to same value causes an error to be generated if it's non-empty + //minlength/maxlength validation isn't performed on script-set values(#2256) if ( (vnode3.tag === 'input' || vnode3.tag === 'textarea') && - vnode3.dom.value === '' + value && - vnode3.dom === activeElement() + vnode3.dom.value === '' + value ) return; //setting select[value] to same value while having select open blinks select dropdown in Chrome if (vnode3.tag === 'select' && old !== null && vnode3.dom.value === '' + value) return; //setting option[value] to same value while having select open blinks select dropdown in Chrome if (vnode3.tag === 'option' && old !== null && vnode3.dom.value === '' + value) return; + //setting input[type=file][value] to different value is an error if it's non-empty + // Not ideal, but it at least works around the most common source of uncaught exceptions for now. + if (vnode3.tag === 'input' && vnode3.attrs.type === 'file' && '' + value !== '') { + console.error('`value` is read-only on file inputs!'); + return; + } /* eslint-enable no-implicit-coercion */ } - // If you assign an input type0 that is not supported by IE 11 with an assignment expression, an error will occur. + // If you assign an input type that is not supported by IE 11 with an assignment expression, an error will occur. if (vnode3.tag === 'input' && key === 'type') vnode3.dom.setAttribute(key, value); else vnode3.dom[key] = value; } else { @@ -1156,19 +907,19 @@ } } function removeAttr(vnode3, key, old, ns) { - if (key === 'key' || key === 'is' || old == null || isLifecycleMethod(key)) return; - if (key[0] === 'o' && key[1] === 'n' && !isLifecycleMethod(key)) - updateEvent(vnode3, key, undefined); + if (key === 'key' || old == null || isLifecycleMethod(key)) return; + if (key[0] === 'o' && key[1] === 'n') updateEvent(vnode3, key, undefined); else if (key === 'style') updateStyle(vnode3.dom, old, null); else if ( hasPropertyKey(vnode3, key, ns) && key !== 'className' && + key !== 'title' && // creates "null" as title !( key === 'value' && (vnode3.tag === 'option' || (vnode3.tag === 'select' && vnode3.dom.selectedIndex === -1 && - vnode3.dom === activeElement())) + vnode3.dom === activeElement(vnode3.dom))) ) && !(vnode3.tag === 'input' && key === 'type') ) { @@ -1179,42 +930,50 @@ if (old !== false) vnode3.dom.removeAttribute(key === 'className' ? 'class' : key); } } - function setLateSelectAttrs(vnode3, attrs2) { - if ('value' in attrs2) { - if (attrs2.value === null) { + function setLateSelectAttrs(vnode3, attrs5) { + if ('value' in attrs5) { + if (attrs5.value === null) { if (vnode3.dom.selectedIndex !== -1) vnode3.dom.value = null; } else { - var normalized = '' + attrs2.value; // eslint-disable-line no-implicit-coercion + var normalized = '' + attrs5.value; // eslint-disable-line no-implicit-coercion if (vnode3.dom.value !== normalized || vnode3.dom.selectedIndex === -1) { vnode3.dom.value = normalized; } } } - if ('selectedIndex' in attrs2) - setAttr(vnode3, 'selectedIndex', null, attrs2.selectedIndex, undefined); + if ('selectedIndex' in attrs5) + setAttr(vnode3, 'selectedIndex', null, attrs5.selectedIndex, undefined); } - function updateAttrs(vnode3, old, attrs2, ns) { - if (attrs2 != null) { - for (var key in attrs2) { - setAttr(vnode3, key, old && old[key], attrs2[key], ns); - } - } + function updateAttrs(vnode3, old, attrs5, ns) { + // Some attributes may NOT be case-sensitive (e.g. data-***), + // so removal should be done first to prevent accidental removal for newly setting values. var val; if (old != null) { + if (old === attrs5 && !cachedAttrsIsStaticMap.has(attrs5)) { + console.warn( + "Don't reuse attrs object, use new object for every redraw, this will throw in next major" + ); + } for (var key in old) { - if ((val = old[key]) != null && (attrs2 == null || attrs2[key] == null)) { + if ((val = old[key]) != null && (attrs5 == null || attrs5[key] == null)) { removeAttr(vnode3, key, val, ns); } } } + if (attrs5 != null) { + for (var key in attrs5) { + setAttr(vnode3, key, old && old[key], attrs5[key], ns); + } + } } function isFormAttribute(vnode3, attr) { return ( attr === 'value' || attr === 'checked' || attr === 'selectedIndex' || - (attr === 'selected' && vnode3.dom === activeElement()) || - (vnode3.tag === 'option' && vnode3.dom.parentNode === $doc.activeElement) + (attr === 'selected' && + (vnode3.dom === activeElement(vnode3.dom) || + (vnode3.tag === 'option' && vnode3.dom.parentNode === activeElement(vnode3.dom)))) ); } function isLifecycleMethod(attr) { @@ -1233,7 +992,7 @@ ns === undefined && // If it's a custom element, just keep it. (vnode3.tag.indexOf('-') > -1 || - (vnode3.attrs != null && vnode3.attrs.is) || + vnode3.is || // If it's a normal element, let's try to avoid a few browser bugs. (key !== 'href' && key !== 'list' && @@ -1245,47 +1004,43 @@ ); } //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 = ''; + element.style = ''; } else if (typeof style !== 'object') { // New style is a string, let engine deal with patching. - element.style.cssText = style; + element.style = style; } else if (old == null || typeof old !== 'object') { // `old` is missing or a string, `style` is an object. - element.style.cssText = ''; + element.style = ''; // Add new style properties for (var key in style) { var value = style[key]; - if (value != null) element.style.setProperty(normalizeKey(key), String(value)); + if (value != null) { + if (key.includes('-')) element.style.setProperty(key, String(value)); + else element.style[key] = String(value); + } } } else { // Both old & new are (different) objects. + // Remove style properties that no longer exist + // Style properties may have two cases(dash-case and camelCase), + // so removal should be done first to prevent accidental removal for newly setting values. + for (var key in old) { + if (old[key] != null && style[key] == null) { + if (key.includes('-')) element.style.removeProperty(key); + else element.style[key] = ''; + } + } // 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)); + if (key.includes('-')) element.style.setProperty(key, value); + else element.style[key] = value; } } } @@ -1296,7 +1051,7 @@ // 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. + // 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 @@ -1307,11 +1062,19 @@ } EventDict.prototype = Object.create(null); EventDict.prototype.handleEvent = function (ev) { - var handler0 = this['on' + ev.type]; + var handler = this['on' + ev.type]; var result; - if (typeof handler0 === 'function') result = handler0.call(ev.currentTarget, ev); - else if (typeof handler0.handleEvent === 'function') handler0.handleEvent(ev); - if (this._ && ev.redraw !== false) (0, this._)(); + if (typeof handler === 'function') result = handler.call(ev.currentTarget, ev); + else if (typeof handler.handleEvent === 'function') handler.handleEvent(ev); + var self = this; + if (self._ != null) { + if (ev.redraw !== false) (0, self._)(); + if (result != null && typeof result.then === 'function') { + Promise.resolve(result).then(function () { + if (self._ != null && ev.redraw !== false) (0, self._)(); + }); + } + } if (result === false) { ev.preventDefault(); ev.stopPropagation(); @@ -1320,6 +1083,7 @@ //event function updateEvent(vnode3, key, value) { if (vnode3.events != null) { + vnode3.events._ = currentRedraw; if (vnode3.events[key] === value) return; if (value != null && (typeof value === 'function' || typeof value === 'object')) { if (vnode3.events[key] == null) @@ -1364,27 +1128,31 @@ // 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 children3 and text as they are conceptually not + // need to save the children and text as they are conceptually not // unlike special "attributes" internally. vnode3.attrs = old.attrs; vnode3.children = old.children; vnode3.text = old.text; return true; } + var currentDOM; 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]); + if (!dom) throw new TypeError('DOM element being rendered to does not exist.'); + if (currentDOM != null && dom.contains(currentDOM)) { + throw new TypeError('Node is currently being rendered to and thus is locked.'); + } var prevRedraw = currentRedraw; + var prevDOM = currentDOM; + var hooks = []; + var active = activeElement(dom); + var namespace = dom.namespaceURI; + currentDOM = dom; + currentRedraw = typeof redraw === 'function' ? redraw : undefined; + currentRender = {}; try { - currentRedraw = typeof redraw === 'function' ? redraw : undefined; + // First time rendering into a node clears it out + if (dom.vnodes == null) dom.textContent = ''; + vnodes = Vnode.normalizeChildren(Array.isArray(vnodes) ? vnodes : [vnodes]); updateNodes( dom, dom.vnodes, @@ -1393,32 +1161,31 @@ null, namespace === 'http://www.w3.org/1999/xhtml' ? undefined : namespace ); + dom.vnodes = vnodes; + // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement + if (active != null && activeElement(dom) !== active && typeof active.focus === 'function') + active.focus(); + for (var i = 0; i < hooks.length; i++) hooks[i](); } finally { currentRedraw = prevRedraw; + currentDOM = prevDOM; } - 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](); }; }; - var render = _12(window); - var _15 = function (render0, schedule, console) { + var render = _14(); + var _21 = function (render2, schedule, console) { var subscriptions = []; - var rendering = false; var pending = false; + var offset = -1; function sync() { - if (rendering) throw new Error('Nested m.redraw.sync() call'); - rendering = true; - for (var i = 0; i < subscriptions.length; i += 2) { + for (offset = 0; offset < subscriptions.length; offset += 2) { try { - render0(subscriptions[i], Vnode(subscriptions[i + 1]), redraw); + render2(subscriptions[offset], Vnode(subscriptions[offset + 1]), redraw); } catch (e) { console.error(e); } } - rendering = false; + offset = -1; } function redraw() { if (!pending) { @@ -1432,21 +1199,26 @@ redraw.sync = sync; function mount(root, component) { if (component != null && component.view == null && typeof component !== 'function') { - throw new TypeError('m.mount(element, component) expects a component, not a vnode'); + throw new TypeError('m.mount expects a component, not a vnode.'); } var index = subscriptions.indexOf(root); if (index >= 0) { subscriptions.splice(index, 2); - render0(root, [], redraw); + if (index <= offset) offset -= 2; + render2(root, []); } if (component != null) { subscriptions.push(root, component); - render0(root, Vnode(component), redraw); + render2(root, Vnode(component), redraw); } } return { mount: mount, redraw: redraw }; }; - var mountRedraw0 = _15(render, requestAnimationFrame, console); + var mountRedraw = _21( + render, + typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame : null, + typeof console !== 'undefined' ? console : null + ); var buildQueryString = function (object) { if (Object.prototype.toString.call(object) !== '[object Object]') return ''; var args = []; @@ -1470,18 +1242,12 @@ ); } }; - var assign = - Object.assign || - function (target, source) { - if (source) - Object.keys(source).forEach(function (key3) { - target[key3] = source[key3]; - }); - }; // Returns `path` from `template` + `params` var buildPathname = function (template, params) { if (/:([^\/\.-]+)(\.{3})?:/.test(template)) { - throw new SyntaxError('Template parameter names *must* be separated'); + throw new SyntaxError( + "Template parameter names must be separated by either a '/', '-', or '.'." + ); } if (params == null) return template; var queryIndex = template.indexOf('?'); @@ -1490,11 +1256,11 @@ var pathEnd = queryIndex < 0 ? queryEnd : queryIndex; var path = template.slice(0, pathEnd); var query = {}; - assign(query, params); - var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function (m2, key1, variadic) { + Object.assign(query, params); + var resolved = path.replace(/:([^\/\.-]+)(\.{3})?/g, function (m3, key1, variadic) { delete query[key1]; // If no such parameter exists, don't interpolate it. - if (params[key1] == null) return m2; + if (params[key1] == null) return m3; // Escape normal parameters, but not variadic ones. return variadic ? params[key1] : encodeURIComponent(String(params[key1])); }); @@ -1513,84 +1279,22 @@ if (newHashIndex >= 0) result0 += (hashIndex < 0 ? '' : '&') + resolved.slice(newHashIndex); return result0; }; - var _18 = function ($window, Promise, oncompletion) { - var callbackCount = 0; + var _25 = function ($window, oncompletion) { function PromiseProxy(executor) { return new Promise(executor); } - // In case the global Promise is0 some userland library's where they rely on - // `foo instanceof this.constructor`, `this.constructor.resolve(value0)`, or - // similar. Let's *not* break them. - PromiseProxy.prototype = Promise.prototype; - PromiseProxy.__proto__ = Promise; // eslint-disable-line no-proto - function makeRequest(factory) { - return function (url, args) { - if (typeof url !== 'string') { - args = url; - url = url.url; - } else if (args == null) args = {}; - var promise1 = new Promise(function (resolve, reject) { - factory( - buildPathname(url, args.params), - args, - function (data) { - if (typeof args.type === 'function') { - if (Array.isArray(data)) { - for (var i = 0; i < data.length; i++) { - data[i] = new args.type(data[i]); - } - } else data = new args.type(data); - } - resolve(data); - }, - reject - ); - }); - if (args.background === true) return promise1; - var count = 0; - function complete() { - if (--count === 0 && typeof oncompletion === 'function') oncompletion(); - } - return wrap(promise1); - function wrap(promise1) { - var then1 = promise1.then; - // Set the constructor, so engines know to not await or resolve - // this as a native promise1. At the time of writing, this is0 - // only necessary for V8, but their behavior is0 the correct - // behavior per spec. See this spec issue for more details: - // https://github.com/tc39/ecma262/issues/1577. Also, see the - // corresponding comment in `request0/tests/test-request0.js` for - // a bit more background on the issue at hand. - promise1.constructor = PromiseProxy; - promise1.then = function () { - count++; - var next0 = then1.apply(promise1, arguments); - next0.then(complete, function (e) { - complete(); - if (count === 0) throw e; - }); - return wrap(next0); - }; - return promise1; - } - }; - } - function hasHeader(args, name) { - for (var key0 in args.headers) { - if ({}.hasOwnProperty.call(args.headers, key0) && name.test(key0)) return true; - } - return false; - } - return { - request: makeRequest(function (url, args, resolve, reject) { + function makeRequest(url, args) { + return new Promise(function (resolve, reject) { + url = buildPathname(url, args.params); var method = args.method != null ? args.method.toUpperCase() : 'GET'; var body = args.body; var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && - !(body instanceof $window.FormData); + !(body instanceof $window.FormData || body instanceof $window.URLSearchParams); var responseType = args.responseType || (typeof args.extract === 'function' ? '' : 'json'); var xhr = new $window.XMLHttpRequest(), - aborted = false; + aborted = false, + isTimeout = false; var original0 = xhr, replacedAbort; var abort = xhr.abort; @@ -1605,17 +1309,17 @@ typeof args.user === 'string' ? args.user : undefined, typeof args.password === 'string' ? args.password : undefined ); - if (assumeJSON && body != null && !hasHeader(args, /^content0-type1$/i)) { + if (assumeJSON && body != null && !hasHeader(args, 'content-type')) { xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8'); } - if (typeof args.deserialize !== 'function' && !hasHeader(args, /^accept$/i)) { + if (typeof args.deserialize !== 'function' && !hasHeader(args, 'accept')) { xhr.setRequestHeader('Accept', 'application/json, text/*'); } if (args.withCredentials) xhr.withCredentials = args.withCredentials; if (args.timeout) xhr.timeout = args.timeout; xhr.responseType = responseType; for (var key0 in args.headers) { - if ({}.hasOwnProperty.call(args.headers, key0)) { + if (hasOwn.call(args.headers, key0)) { xhr.setRequestHeader(key0, args.headers[key0]); } } @@ -1628,8 +1332,8 @@ (ev.target.status >= 200 && ev.target.status < 300) || ev.target.status === 304 || /^file:\/\//i.test(url); - // When the response type1 isn't "" or "text", - // `xhr.responseText` is0 the wrong thing to use. + // When the response type isn't "" or "text", + // `xhr.responseText` is the wrong thing to use. // Browsers do the right thing and throw here, and we // should honor that and do the right thing by // preferring `xhr.response` where possible/practical. @@ -1638,11 +1342,17 @@ if (responseType === 'json') { // For IE and Edge, which don't implement // `responseType: "json"`. - if (!ev.target.responseType && typeof args.extract !== 'function') - response = JSON.parse(ev.target.responseText); + if (!ev.target.responseType && typeof args.extract !== 'function') { + // Handle no-content which will not parse. + try { + response = JSON.parse(ev.target.responseText); + } catch (e) { + response = null; + } + } } else if (!responseType || responseType === 'text') { // Only use this default if it's text. If a parsed - // document is0 needed on old IE and friends (all + // document is needed on old IE and friends (all // unsupported), the user should use a custom // `config` instead. They're already using this at // their own risk. @@ -1654,23 +1364,49 @@ } else if (typeof args.deserialize === 'function') { response = args.deserialize(response); } - if (success) resolve(response); - else { - try { - message = ev.target.responseText; - } catch (e) { - message = response; + if (success) { + if (typeof args.type === 'function') { + if (Array.isArray(response)) { + for (var i = 0; i < response.length; i++) { + response[i] = new args.type(response[i]); + } + } else response = new args.type(response); } - var error = new Error(message); - error.code = ev.target.status; - error.response = response; - reject(error); + resolve(response); + } else { + var completeErrorResponse = function () { + try { + message = ev.target.responseText; + } catch (e) { + message = response; + } + var error = new Error(message); + error.code = ev.target.status; + error.response = response; + reject(error); + }; + if (xhr.status === 0) { + // Use setTimeout to push this code block onto the event queue + // This allows `xhr.ontimeout` to run in the case that there is a timeout + // Without this setTimeout, `xhr.ontimeout` doesn't have a chance to reject + // as `xhr.onreadystatechange` will run before it + setTimeout(function () { + if (isTimeout) return; + completeErrorResponse(); + }); + } else completeErrorResponse(); } } catch (e) { reject(e); } } }; + xhr.ontimeout = function (ev) { + isTimeout = true; + var error = new Error('Request timed out'); + error.code = ev.target.status; + reject(error); + }; if (typeof args.config === 'function') { xhr = args.config(xhr, args, url) || xhr; // Propagate the `abort` to any replacement XHR as well. @@ -1684,45 +1420,92 @@ } if (body == null) xhr.send(); else if (typeof args.serialize === 'function') xhr.send(args.serialize(body)); - else if (body instanceof $window.FormData) xhr.send(body); + else if (body instanceof $window.FormData || body instanceof $window.URLSearchParams) + xhr.send(body); else xhr.send(JSON.stringify(body)); - }), - jsonp: makeRequest(function (url, args, resolve, reject) { - var callbackName = - args.callbackName || - '_mithril_' + Math.round(Math.random() * 1e16) + '_' + callbackCount++; - var script = $window.document.createElement('script'); - $window[callbackName] = function (data) { - delete $window[callbackName]; - script.parentNode.removeChild(script); - resolve(data); - }; - script.onerror = function () { - delete $window[callbackName]; - script.parentNode.removeChild(script); - reject(new Error('JSONP request failed')); - }; - script.src = - url + - (url.indexOf('?') < 0 ? '?' : '&') + - encodeURIComponent(args.callbackKey || 'callback') + - '=' + - encodeURIComponent(callbackName); - $window.document.documentElement.appendChild(script); - }), + }); + } + // In case the global Promise is some userland library's where they rely on + // `foo instanceof this.constructor`, `this.constructor.resolve(value)`, or + // similar. Let's *not* break them. + PromiseProxy.prototype = Promise.prototype; + PromiseProxy.__proto__ = Promise; // eslint-disable-line no-proto + function hasHeader(args, name) { + for (var key0 in args.headers) { + if (hasOwn.call(args.headers, key0) && key0.toLowerCase() === name) return true; + } + return false; + } + return { + request: function (url, args) { + if (typeof url !== 'string') { + args = url; + url = url.url; + } else if (args == null) args = {}; + var promise = makeRequest(url, args); + if (args.background === true) return promise; + var count = 0; + function complete() { + if (--count === 0 && typeof oncompletion === 'function') oncompletion(); + } + return wrap(promise); + function wrap(promise) { + var then = promise.then; + // Set the constructor, so engines know to not await or resolve + // this as a native promise. At the time of writing, this is + // only necessary for V8, but their behavior is the correct + // behavior per spec. See this spec issue for more details: + // https://github.com/tc39/ecma262/issues/1577. Also, see the + // corresponding comment in `request/tests/test-request.js` for + // a bit more background on the issue at hand. + promise.constructor = PromiseProxy; + promise.then = function () { + count++; + var next = then.apply(promise, arguments); + next.then(complete, function (e) { + complete(); + if (count === 0) throw e; + }); + return wrap(next); + }; + return promise; + } + }, }; }; - var request = _18(window, PromisePolyfill, mountRedraw0.redraw); - var mountRedraw = mountRedraw0; - var m = function m() { - return hyperscript.apply(this, arguments); + var request = _25(typeof window !== 'undefined' ? window : null, mountRedraw.redraw); + /* +Percent encodings encode UTF-8 bytes, so this regexp needs to match that. +Here's how UTF-8 encodes stuff: +- `00-7F`: 1-byte, for U+0000-U+007F +- `C2-DF 80-BF`: 2-byte, for U+0080-U+07FF +- `E0-EF 80-BF 80-BF`: 3-byte, encodes U+0800-U+FFFF +- `F0-F4 80-BF 80-BF 80-BF`: 4-byte, encodes U+10000-U+10FFFF +In this, there's a number of invalid byte sequences: +- `80-BF`: Continuation byte, invalid as start +- `C0-C1 80-BF`: Overlong encoding for U+0000-U+007F +- `E0 80-9F 80-BF`: Overlong encoding for U+0080-U+07FF +- `ED A0-BF 80-BF`: Encoding for UTF-16 surrogate U+D800-U+DFFF +- `F0 80-8F 80-BF 80-BF`: Overlong encoding for U+0800-U+FFFF +- `F4 90-BF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. +- `F5-FF`: RFC 3629 restricted UTF-8 to only code points UTF-16 could encode. +So in reality, only the following sequences can encode are valid characters: +- 00-7F +- C2-DF 80-BF +- E0 A0-BF 80-BF +- E1-EC 80-BF 80-BF +- ED 80-9F 80-BF +- EE-EF 80-BF 80-BF +- F0 90-BF 80-BF 80-BF +- F1-F3 80-BF 80-BF 80-BF +- F4 80-8F 80-BF 80-BF +The regexp just tries to match this as compactly as possible. +*/ + var validUtf8Encodings = + /%(?:[0-7]|(?!c[01]|e0%[89]|ed%[ab]|f0%8|f4%[9ab])(?:c|d|(?:e|f[0-4]%[89ab])[\da-f]%[89ab])[\da-f]%[89ab])[\da-f]/gi; + var decodeURIComponentSafe = function (str) { + return String(str).replace(validUtf8Encodings, decodeURIComponent); }; - m.m = hyperscript; - m.trust = hyperscript.trust; - m.fragment = hyperscript.fragment; - m.mount = mountRedraw.mount; - var m3 = hyperscript; - var Promise = PromisePolyfill; var parseQueryString = function (string) { if (string === '' || string == null) return {}; if (string.charAt(0) === '?') string = string.slice(1); @@ -1731,23 +1514,23 @@ data0 = {}; for (var i = 0; i < entries.length; i++) { var entry = entries[i].split('='); - var key5 = decodeURIComponent(entry[0]); - var value2 = entry.length === 2 ? decodeURIComponent(entry[1]) : ''; + var key4 = decodeURIComponentSafe(entry[0]); + var value2 = entry.length === 2 ? decodeURIComponentSafe(entry[1]) : ''; if (value2 === 'true') value2 = true; else if (value2 === 'false') value2 = false; - var levels = key5.split(/\]\[?|\[/); + var levels = key4.split(/\]\[?|\[/); var cursor = data0; - if (key5.indexOf('[') > -1) levels.pop(); + if (key4.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)); if (level === '') { - var key5 = levels.slice(0, j0).join(); - if (counters[key5] == null) { - counters[key5] = Array.isArray(cursor) ? cursor.length : 0; + var key4 = levels.slice(0, j0).join(); + if (counters[key4] == null) { + counters[key4] = Array.isArray(cursor) ? cursor.length : 0; } - level = counters[key5]++; + level = counters[key4]++; } // Disallow direct prototype pollution else if (level === '__proto__') break; @@ -1764,7 +1547,7 @@ } return data0; }; - // Returns `{path1, params}` from `url` + // Returns `{path, params}` from `url` var parsePathname = function (url) { var queryIndex0 = url.indexOf('?'); var hashIndex0 = url.indexOf('#'); @@ -1774,17 +1557,16 @@ if (!path1) path1 = '/'; else { if (path1[0] !== '/') path1 = '/' + path1; - if (path1.length > 1 && path1[path1.length - 1] === '/') path1 = path1.slice(0, -1); } return { path: path1, params: queryIndex0 < 0 ? {} : parseQueryString(url.slice(queryIndex0 + 1, queryEnd0)), }; }; - // Compiles a template into a function that takes a resolved0 path2 (without query0 + // Compiles a template into a function that takes a resolved path (without query // strings) and returns an object containing the template parameters with their - // parsed values. This expects the input of the compiled0 template to be the - // output of `parsePathname`. Note that it does *not* remove query0 parameters + // parsed values. This expects the input of the compiled template to be the + // output of `parsePathname`. Note that it does *not* remove query parameters // specified in the template. var compileTemplate = function (template) { var templateData = parsePathname(template); @@ -1794,19 +1576,19 @@ '^' + templateData.path.replace( // I escape literal text so people can use things like `:file.:ext` or - // `:lang-:locale` in routes. This is2 all merged into one pass so I + // `:lang-:locale` in routes. This is all merged into one pass so I // don't also accidentally escape `-` and make it harder to detect it to // ban it from template parameters. /:([^\/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|\[\]{}]/g, - function (m4, key6, extra) { - if (key6 == null) return '\\' + m4; - keys.push({ k: key6, r: extra === '...' }); + function (m4, key5, extra) { + if (key5 == null) return '\\' + m4; + keys.push({ k: key5, r: extra === '...' }); if (extra === '...') return '(.*)'; if (extra === '.') return '([^/]+)\\.'; return '([^/]+)' + (extra || ''); } ) + - '$' + '\\/?$' ); return function (data1) { // First, check the params. Usually, there isn't any, and it's just @@ -1824,12 +1606,178 @@ return true; }; }; - var sentinel0 = {}; - var _25 = function ($window, mountRedraw00) { - var fireAsync; - function setPath(path0, data, options) { + // Note: this is mildly perf-sensitive. + // + // It does *not* use `delete` - dynamic `delete`s usually cause objects to bail + // out into dictionary mode and just generally cause a bunch of optimization + // issues within engines. + // + // Ideally, I would've preferred to do this, if it weren't for the optimization + // issues: + // + // ```js + // const hasOwn = hasOwn + // const magic = [ + // "key", "oninit", "oncreate", "onbeforeupdate", "onupdate", + // "onbeforeremove", "onremove", + // ] + // var censor = (attrs, extras) => { + // const result = Object.assign(Object.create(null), attrs) + // for (const key of magic) delete result[key] + // if (extras != null) for (const key of extras) delete result[key] + // return result + // } + // ``` + var magic = /^(?:key|oninit|oncreate|onbeforeupdate|onupdate|onbeforeremove|onremove)$/; + var censor = function (attrs7, extras) { + var result2 = {}; + if (extras != null) { + for (var key6 in attrs7) { + if (hasOwn.call(attrs7, key6) && !magic.test(key6) && extras.indexOf(key6) < 0) { + result2[key6] = attrs7[key6]; + } + } + } else { + for (var key6 in attrs7) { + if (hasOwn.call(attrs7, key6) && !magic.test(key6)) { + result2[key6] = attrs7[key6]; + } + } + } + return result2; + }; + var _31 = function ($window, mountRedraw0) { + var p = Promise.resolve(); + var scheduled = false; + var ready = false; + var hasBeenResolved = false; + var dom0, compiled, fallbackRoute; + var currentResolver, component, attrs6, currentPath, lastUpdate; + var RouterRoot = { + onremove: function () { + ready = hasBeenResolved = false; + $window.removeEventListener('popstate', fireAsync, false); + }, + view: function () { + // The route has already been resolved. + // Therefore, the following early return is not needed. + // if (!hasBeenResolved) return + var vnode6 = Vnode(component, attrs6.key, attrs6); + if (currentResolver) return currentResolver.render(vnode6); + // Wrap in a fragment to preserve existing key semantics + return [vnode6]; + }, + }; + var SKIP = (route.SKIP = {}); + function resolveRoute() { + scheduled = false; + // Consider the pathname holistically. The prefix might even be invalid, + // but that's not our problem. + var prefix = $window.location.hash; + if (route.prefix[0] !== '#') { + prefix = $window.location.search + prefix; + if (route.prefix[0] !== '?') { + prefix = $window.location.pathname + prefix; + if (prefix[0] !== '/') prefix = '/' + prefix; + } + } + var path0 = decodeURIComponentSafe(prefix).slice(route.prefix.length); + var data = parsePathname(path0); + Object.assign(data.params, $window.history.state); + function reject(e) { + console.error(e); + route.set(fallbackRoute, null, { replace: true }); + } + loop(0); + function loop(i) { + for (; i < compiled.length; i++) { + if (compiled[i].check(data)) { + var payload = compiled[i].component; + var matchedRoute = compiled[i].route; + var localComp = payload; + var update = (lastUpdate = function (comp) { + if (update !== lastUpdate) return; + if (comp === SKIP) return loop(i + 1); + component = + comp != null && (typeof comp.view === 'function' || typeof comp === 'function') + ? comp + : 'div'; + ((attrs6 = data.params), (currentPath = path0), (lastUpdate = null)); + currentResolver = payload.render ? payload : null; + if (hasBeenResolved) mountRedraw0.redraw(); + else { + hasBeenResolved = true; + mountRedraw0.mount(dom0, RouterRoot); + } + }); + // There's no understating how much I *wish* I could + // use `async`/`await` here... + if (payload.view || typeof payload === 'function') { + payload = {}; + update(localComp); + } else if (payload.onmatch) { + p.then(function () { + return payload.onmatch(data.params, path0, matchedRoute); + }).then(update, path0 === fallbackRoute ? null : reject); + } else update(/* "div" */); + return; + } + } + if (path0 === fallbackRoute) { + throw new Error('Could not resolve default route ' + fallbackRoute + '.'); + } + route.set(fallbackRoute, null, { replace: true }); + } + } + function fireAsync() { + if (!scheduled) { + scheduled = true; + // TODO: just do `mountRedraw.redraw()` here and elide the timer + // dependency. Note that this will muck with tests a *lot*, so it's + // not as easy of a change as it sounds. + setTimeout(resolveRoute); + } + } + function route(root, defaultRoute, routes) { + if (!root) throw new TypeError('DOM element being rendered to does not exist.'); + compiled = Object.keys(routes).map(function (route) { + if (route[0] !== '/') throw new SyntaxError("Routes must start with a '/'."); + if (/:([^\/\.-]+)(\.{3})?:/.test(route)) { + throw new SyntaxError( + "Route parameter names must be separated with either '/', '.', or '-'." + ); + } + return { + route: route, + component: routes[route], + check: compileTemplate(route), + }; + }); + fallbackRoute = defaultRoute; + if (defaultRoute != null) { + var defaultData = parsePathname(defaultRoute); + if ( + !compiled.some(function (i) { + return i.check(defaultData); + }) + ) { + throw new ReferenceError("Default route doesn't match any known routes."); + } + } + dom0 = root; + $window.addEventListener('popstate', fireAsync, false); + ready = true; + // The RouterRoot component is mounted when the route is first resolved. + resolveRoute(); + } + route.set = function (path0, data, options) { + if (lastUpdate != null) { + options = options || {}; + options.replace = true; + } + lastUpdate = null; path0 = buildPathname(path0, data); - if (fireAsync != null) { + if (ready) { fireAsync(); var state = options ? options.state : null; var title = options ? options.title : null; @@ -1839,195 +1787,40 @@ } else { $window.location.href = route.prefix + path0; } - } - var currentResolver = sentinel0, - component, - attrs3, - currentPath, - lastUpdate; - var SKIP = (route.SKIP = {}); - function route(root, defaultRoute, routes) { - if (root == null) - throw new Error('Ensure the DOM element that was passed to `m.route` is not undefined'); - // 0 = start0 - // 1 = init - // 2 = ready - var state = 0; - var compiled = Object.keys(routes).map(function (route) { - if (route[0] !== '/') throw new SyntaxError('Routes must start with a `/`'); - if (/:([^\/\.-]+)(\.{3})?:/.test(route)) { - throw new SyntaxError( - 'Route parameter names must be separated with either `/`, `.`, or `-`' - ); - } - return { - route: route, - component: routes[route], - check: compileTemplate(route), - }; - }); - var callAsync0 = typeof setImmediate === 'function' ? setImmediate : setTimeout; - var p = Promise.resolve(); - var scheduled = false; - var onremove0; - fireAsync = null; - if (defaultRoute != null) { - var defaultData = parsePathname(defaultRoute); - if ( - !compiled.some(function (i) { - return i.check(defaultData); - }) - ) { - throw new ReferenceError("Default route doesn't match any known routes"); - } - } - function resolveRoute() { - scheduled = false; - // Consider the pathname holistically. The prefix might even be invalid, - // but that's not our problem. - var prefix = $window.location.hash; - if (route.prefix[0] !== '#') { - prefix = $window.location.search + prefix; - if (route.prefix[0] !== '?') { - prefix = $window.location.pathname + prefix; - if (prefix[0] !== '/') prefix = '/' + prefix; - } - } - // This seemingly useless `.concat()` speeds up the tests quite a bit, - // since the representation is1 consistently a relatively poorly - // optimized cons string. - var path0 = prefix - .concat() - .replace(/(?:%[a-f89][a-f0-9])+/gim, decodeURIComponent) - .slice(route.prefix.length); - var data = parsePathname(path0); - assign(data.params, $window.history.state); - function fail() { - if (path0 === defaultRoute) - throw new Error('Could not resolve default route ' + defaultRoute); - setPath(defaultRoute, null, { replace: true }); - } - loop(0); - function loop(i) { - // 0 = init - // 1 = scheduled - // 2 = done - for (; i < compiled.length; i++) { - if (compiled[i].check(data)) { - var payload = compiled[i].component; - var matchedRoute = compiled[i].route; - var localComp = payload; - var update = (lastUpdate = function (comp) { - if (update !== lastUpdate) return; - if (comp === SKIP) return loop(i + 1); - component = - comp != null && (typeof comp.view === 'function' || typeof comp === 'function') - ? comp - : 'div'; - (attrs3 = data.params), (currentPath = path0), (lastUpdate = null); - currentResolver = payload.render ? payload : null; - if (state === 2) mountRedraw00.redraw(); - else { - state = 2; - mountRedraw00.redraw.sync(); - } - }); - // There's no understating how much I *wish* I could - // use `async`/`await` here... - if (payload.view || typeof payload === 'function') { - payload = {}; - update(localComp); - } else if (payload.onmatch) { - p.then(function () { - return payload.onmatch(data.params, path0, matchedRoute); - }).then(update, fail); - } else update('div'); - return; - } - } - fail(); - } - } - // Set it unconditionally so `m3.route.set` and `m3.route.Link` both work, - // even if neither `pushState` nor `hashchange` are supported. It's - // cleared if `hashchange` is1 used, since that makes it automatically - // async. - fireAsync = function () { - if (!scheduled) { - scheduled = true; - callAsync0(resolveRoute); - } - }; - if (typeof $window.history.pushState === 'function') { - onremove0 = function () { - $window.removeEventListener('popstate', fireAsync, false); - }; - $window.addEventListener('popstate', fireAsync, false); - } else if (route.prefix[0] === '#') { - fireAsync = null; - onremove0 = function () { - $window.removeEventListener('hashchange', resolveRoute, false); - }; - $window.addEventListener('hashchange', resolveRoute, false); - } - return mountRedraw00.mount(root, { - onbeforeupdate: function () { - state = state ? 2 : 1; - return !(!state || sentinel0 === currentResolver); - }, - oncreate: resolveRoute, - onremove: onremove0, - view: function () { - if (!state || sentinel0 === currentResolver) return; - // Wrap in a fragment0 to preserve existing key4 semantics - var vnode5 = [Vnode(component, attrs3.key, attrs3)]; - if (currentResolver) vnode5 = currentResolver.render(vnode5[0]); - return vnode5; - }, - }); - } - route.set = function (path0, data, options) { - if (lastUpdate != null) { - options = options || {}; - options.replace = true; - } - lastUpdate = null; - setPath(path0, data, options); }; route.get = function () { return currentPath; }; route.prefix = '#!'; route.Link = { - view: function (vnode5) { - var options = vnode5.attrs.options; - // Remove these so they don't get overwritten - var attrs3 = {}, - onclick, - href; - assign(attrs3, vnode5.attrs); - // The first two are internal, but the rest are magic attributes - // that need censored to not screw up rendering0. - attrs3.selector = attrs3.options = attrs3.key = attrs3.oninit = attrs3.oncreate = attrs3.onbeforeupdate = attrs3.onupdate = attrs3.onbeforeremove = attrs3.onremove = null; - // Do this now so we can get the most current `href` and `disabled`. - // Those attributes may also be specified in the selector, and we - // should honor that. - var child0 = m3(vnode5.attrs.selector || 'a', attrs3, vnode5.children); + view: function (vnode6) { + // Omit the used parameters from the rendered element - they are + // internal. Also, censor the various lifecycle methods. + // + // We don't strip the other parameters because for convenience we + // let them be specified in the selector as well. + var child0 = hyperscript( + vnode6.attrs.selector || 'a', + censor(vnode6.attrs, ['options', 'params', 'selector', 'onclick']), + vnode6.children + ); + var options, onclick, href; // Let's provide a *right* way to disable a route link, rather than // letting people screw up accessibility on accident. // - // The attribute is1 coerced so users don't get surprised over + // The attribute is coerced so users don't get surprised over // `disabled: 0` resulting in a button that's somehow routable // despite being visibly disabled. if ((child0.attrs.disabled = Boolean(child0.attrs.disabled))) { child0.attrs.href = null; child0.attrs['aria-disabled'] = 'true'; - // If you *really* do want to do this on a disabled link, use + // If you *really* do want add `onclick` on a disabled link, use // an `oncreate` hook to add it. - child0.attrs.onclick = null; } else { - onclick = child0.attrs.onclick; - href = child0.attrs.href; + options = vnode6.attrs.options; + onclick = vnode6.attrs.onclick; + // Easier to build it now to keep it isomorphic. + href = buildPathname(child0.attrs.href, vnode6.attrs.params); child0.attrs.href = route.prefix + href; child0.attrs.onclick = function (e) { var result1; @@ -2041,7 +1834,7 @@ // Adapted from React Router's implementation: // https://github.com/ReactTraining/react-router/blob/520a0acd48ae1b066eb0b07d6d4d1790a1d02482/packages/react-router-dom/modules/Link.js // - // Try to be flexible and intuitive in how we handle1 links. + // Try to be flexible and intuitive in how we handle links. // Fun fact: links aren't as obvious to get right as you // would expect. There's a lot more valid ways to click a // link than this, and one might want to not simply click a @@ -2053,7 +1846,7 @@ !e.defaultPrevented && // Ignore everything but left clicks (e.button === 0 || e.which === 0 || e.which === 1) && - // Let the browser handle1 `target=_blank`, etc. + // Let the browser handle `target=_blank`, etc. (!e.currentTarget.target || e.currentTarget.target === '_self') && // No modifier keys !e.ctrlKey && @@ -2070,22 +1863,31 @@ return child0; }, }; - route.param = function (key4) { - return attrs3 && key4 != null ? attrs3[key4] : attrs3; + route.param = function (key3) { + return attrs6 && key3 != null ? attrs6[key3] : attrs6; }; return route; }; - m.route = _25(window, mountRedraw); + var router = _31(typeof window !== 'undefined' ? window : null, mountRedraw); + var m = function m() { + return hyperscript.apply(this, arguments); + }; + m.m = hyperscript; + m.trust = hyperscript.trust; + m.fragment = hyperscript.fragment; + m.Fragment = '['; + m.mount = mountRedraw.mount; + m.route = router; m.render = render; m.redraw = mountRedraw.redraw; m.request = request.request; - m.jsonp = request.jsonp; m.parseQueryString = parseQueryString; m.buildQueryString = buildQueryString; m.parsePathname = parsePathname; m.buildPathname = buildPathname; m.vnode = Vnode; - m.PromisePolyfill = PromisePolyfill; + m.censor = censor; + m.domFor = domFor; if (typeof module !== 'undefined') module['exports'] = m; else window.m = m; })();