/* eslint-disable wrap-iife, no-shadow-restricted-names */ var m = (function app(window, undefined) { /* eslint-enable wrap-iife, no-shadow-restricted-names */ "use strict" function noop() {} var type = {}.toString function isFunction(object) { return typeof object === "function" } function isObject(object) { return type.call(object) === "[object Object]" } function isString(object) { return type.call(object) === "[object String]" } var isArray = Array.isArray || function (object) { return type.call(object) === "[object Array]" } var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g var attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/ /* eslint-disable max-len */ var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/ /* eslint-enable max-len */ // caching commonly used variables var $document, $location, $requestAnimationFrame, $cancelAnimationFrame var roots = [] var components = [] var controllers = [] var lastRedrawId = null var lastRedrawCallTime = 0 var computePreRedrawHook = null var computePostRedrawHook = null var unloaders = [] var FRAME_BUDGET = 16 // 60 frames per second = 1 call per 16 ms var topComponent var redrawing = false var forcing = false var pendingRequests = 0 // self invoking function needed because of the way mocks work function initialize(window) { $document = window.document $location = window.location $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout } initialize(window) /** * @typedef {String} Tag * A string that looks like -> div.classname#id[param=one][param2=two] * Which describes a DOM node */ /** * * @param {Tag} The DOM node tag * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, * or splat (optional) * */ function m(tag, pairs) { for (var args = [], i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i] } if (isObject(tag)) return parameterize(tag, args) var hasAttrs = pairs != null && isObject(pairs) && !("tag" in pairs || "view" in pairs || "subtree" in pairs) var attrs = hasAttrs ? pairs : {} var classAttrName = "class" in attrs ? "class" : "className" var cell = {tag: "div", attrs: {}} var classes = [] if (!isString(tag)) { throw new Error( "selector in m(selector, attrs, children) should be a string") } var match = parser.exec(tag) while (match != null) { if (match[1] === "" && match[2]) { cell.tag = match[2] } else if (match[1] === "#") { cell.attrs.id = match[2] } else if (match[1] === ".") { classes.push(match[2]) } else if (match[3][0] === "[") { var pair = attrParser.exec(match[3]) cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" : true) } match = parser.exec(tag) } var children = hasAttrs ? args.slice(1) : args if (children.length === 1 && isArray(children[0])) { cell.children = children[0] } else { cell.children = children } for (var attrName in attrs) { if (attrs.hasOwnProperty(attrName)) { if (attrName === classAttrName && attrs[attrName] != null && attrs[attrName] !== "") { classes.push(attrs[attrName]) // create key in correct iteration order cell.attrs[attrName] = "" } else { cell.attrs[attrName] = attrs[attrName] } } } if (classes.length) cell.attrs[classAttrName] = classes.join(" ") return cell } function forEach(list, f) { for (var i = 0; i < list.length && !f(list[i], i++);) { // do nothing } } function forKeys(list, f) { forEach(list, function (attrs, i) { attrs = attrs && attrs.attrs return attrs && attrs.key != null && f(attrs, i) }) } // This function was causing deopts in Chrome. function dataToString(data) { // data.toString() might throw or return null if data is the return // value of Console.log in Firefox (behavior depends on version) try { if (data == null || data.toString() == null) return "" } catch (e) { return "" } return data } // This function was causing deopts in Chrome. function injectTextNode(parentElement, first, index, data) { try { parentElement.insertBefore( first, parentElement.childNodes[index] || null) first.nodeValue = data } catch (e) { // IE erroneously throws error when appending an empty text node // after a null } } function flatten(list) { // recursively flatten array for (var i = 0; i < list.length; i++) { if (isArray(list[i])) { list = list.concat.apply([], list) // check current index again and flatten until there are no // more nested arrays at that index i-- } } return list } function insertNode(parentElement, node, index) { parentElement.insertBefore( node, parentElement.childNodes[index] || null) } var DELETION = 1 var INSERTION = 2 var MOVE = 3 function handleKeysDiffer(data, existing, cached, parentElement) { forKeys(data, function (attrs, i) { var key = attrs.key if (existing[key]) { existing[key] = { action: MOVE, index: i, from: existing[key].index, element: cached.nodes[existing[key].index] || $document.createElement("div") } } else { existing[key] = {action: INSERTION, index: i} } }) var actions = [] for (var prop in existing) actions.push(existing[prop]) var changes = actions.sort(sortChanges) var newCached = new Array(cached.length) newCached.nodes = cached.nodes.slice() forEach(changes, function (change) { var index = change.index if (change.action === DELETION) { clear(cached[index].nodes, cached[index]) newCached.splice(index, 1) } if (change.action === INSERTION) { var dummy = $document.createElement("div") dummy.key = data[index].attrs.key insertNode(parentElement, dummy, index) newCached.splice(index, 0, { attrs: {key: data[index].attrs.key}, nodes: [dummy] }) newCached.nodes[index] = dummy } if (change.action === MOVE) { var changeElement = change.element var maybeChanged = parentElement.childNodes[index] if (maybeChanged !== changeElement && changeElement !== null) { parentElement.insertBefore(changeElement, maybeChanged || null) } newCached[index] = cached[change.from] newCached.nodes[index] = changeElement } }) return newCached } function diffKeys(data, cached, existing, parentElement) { var keysDiffer = data.length !== cached.length if (!keysDiffer) { forKeys(data, function (attrs, i) { var cachedCell = cached[i] keysDiffer = cachedCell && cachedCell.attrs && cachedCell.attrs.key !== attrs.key return keysDiffer }) } if (keysDiffer) { return handleKeysDiffer(data, existing, cached, parentElement) } else { return cached } } function diffArray(data, cached, nodes) { // diff the array itself // update the list of DOM nodes by collecting the nodes from // each item forEach(data, function (_, i) { if (cached[i] != null) { nodes.push.apply(nodes, cached[i].nodes) } }) // remove items from the end of the array if the new array is // shorter than the old one. if errors ever happen here, the // issue is most likely a bug in the construction of the // `cached` data structure somewhere earlier in the program forEach(cached.nodes, function (node, i) { if (node.parentNode != null && nodes.indexOf(node) < 0) { clear([node], [cached[i]]) } }) if (data.length < cached.length) cached.length = data.length cached.nodes = nodes } function buildArrayKeys(data) { var guid = 0 forKeys(data, function () { forEach(data, function (attrs) { if ((attrs = attrs && attrs.attrs) && attrs.key == null) { attrs.key = "__mithril__" + guid++ } }) return 1 }) } function maybeRecreateObject(data, cached, dataAttrKeys) { // if an element is different enough from the one in cache, recreate it if (data.tag !== cached.tag || dataAttrKeys.sort().join() !== Object.keys(cached.attrs).sort().join() || data.attrs.id !== cached.attrs.id || data.attrs.key !== cached.attrs.key || (m.redraw.strategy() === "all" && (!cached.configContext || cached.configContext.retain !== true)) || (m.redraw.strategy() === "diff" && cached.configContext && cached.configContext.retain === false)) { if (cached.nodes.length) clear(cached.nodes) if (cached.configContext && isFunction(cached.configContext.onunload)) { cached.configContext.onunload() } if (cached.controllers) { forEach(cached.controllers, function (controller) { if (controller.unload) { controller.onunload({preventDefault: noop}) } }) } } } function getObjectNamespace(data, namespace) { if (data.attrs.xmlns) { return data.attrs.xmlns } else if (data.tag === "svg") { return "http://www.w3.org/2000/svg" } else if (data.tag === "math") { return "http://www.w3.org/1998/Math/MathML" } else { return namespace } } function unloadCachedControllers(cached, views, controllers) { if (controllers.length) { cached.views = views cached.controllers = controllers forEach(controllers, function (controller) { if (controller.onunload && controller.onunload.$old) { controller.onunload = controller.onunload.$old } if (pendingRequests && controller.onunload) { var onunload = controller.onunload controller.onunload = noop controller.onunload.$old = onunload } }) } } function scheduleConfigsToBeCalled(configs, data, node, isNew, cached) { // schedule configs to be called. They are called after `build` // finishes running if (isFunction(data.attrs.config)) { var context = cached.configContext = cached.configContext || {} // bind configs.push(function() { return data.attrs.config.call(data, node, !isNew, context, cached) }) } } function buildUpdatedNode( cached, data, editable, hasKeys, namespace, views, configs, controllers) { var node = cached.nodes[0] if (hasKeys) { setAttributes( node, data.tag, data.attrs, cached.attrs, namespace) } cached.children = build( node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs) cached.nodes.intact = true if (controllers.length) { cached.views = views cached.controllers = controllers } return node } function handleNonexistentNodes(data, parentElement, index) { var nodes if (data.$trusted) { nodes = injectHTML(parentElement, index, data) } else { nodes = [$document.createTextNode(data)] if (!parentElement.nodeName.match(voidElements)) { insertNode(parentElement, nodes[0], index) } } var cached if (typeof data === "string" || typeof data === "number" || typeof data === "boolean") { cached = new data.constructor(data) } else { cached = data } cached.nodes = nodes return cached } function reattachNodes( data, cached, parentElement, editable, index, parentTag) { var nodes = cached.nodes if (!editable || editable !== $document.activeElement) { if (data.$trusted) { clear(nodes, cached) nodes = injectHTML(parentElement, index, data) } else { // corner case: replacing the nodeValue of a text node // that is a child of a textarea/contenteditable doesn't // work. we need to update the value property of the // parent textarea or the innerHTML of the // contenteditable element instead if (parentTag === "textarea") { parentElement.value = data } else if (editable) { editable.innerHTML = data } else { // was a trusted string if (nodes[0].nodeType === 1 || nodes.length > 1) { clear(cached.nodes, cached) nodes = [$document.createTextNode(data)] } injectTextNode(parentElement, nodes[0], index, data) } } } cached = new data.constructor(data) cached.nodes = nodes return cached } function handleText( cached, data, index, parentElement, shouldReattach, editable, parentTag) { // handle text nodes if (cached.nodes.length === 0) { return handleNonexistentNodes(data, parentElement, index) } else if (cached.valueOf() !== data.valueOf() || shouldReattach === true) { return reattachNodes( data, cached, parentElement, editable, index, parentTag) } else { cached.nodes.intact = true return cached } } function getSubArrayCount(item) { if (item.$trusted) { // fix offset of next element if item was a trusted // string w/ more than one html element // the first clause in the regexp matches elements // the second clause (after the pipe) matches text nodes var match = item.match(/<[^\/]|\>\s*[^<]/g) if (match != null) { return match.length } } else if (isArray(item)) { return item.length } return 1 } function buildArray( data, cached, parentElement, index, parentTag, shouldReattach, editable, namespace, configs) { data = flatten(data) var nodes = [] var intact = cached.length === data.length var subArrayCount = 0 // keys algorithm: sort elements without recreating them if keys are // present // 1) create a map of all existing keys, and mark all for deletion // 2) add new keys to map and mark them for addition // 3) if key exists in new list, change action from deletion to a // move // 4) for each key, handle its corresponding action as marked in // previous steps var existing = {} var shouldMaintainIdentities = false forKeys(cached, function (attrs, i) { shouldMaintainIdentities = true existing[cached[i].attrs.key] = {action: DELETION, index: i} }) buildArrayKeys(data) if (shouldMaintainIdentities) { cached = diffKeys(data, cached, existing, parentElement) } // end key algorithm var cacheCount = 0 for (var i = 0, len = data.length; i < len; i++) { // diff each item in the array var item = build( parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs) if (item !== undefined) { intact = intact && item.nodes.intact subArrayCount += getSubArrayCount(item) cached[cacheCount++] = item } } if (!intact) diffArray(data, cached, nodes) return cached } function makeCache(data, cached, index, parentIndex, parentCache) { if (cached != null) { if (type.call(cached) === type.call(data)) return cached if (parentCache && parentCache.nodes) { var offset = index - parentIndex var end = offset + (isArray(data) ? data : cached.nodes).length clear( parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) } else if (cached.nodes) { clear(cached.nodes, cached) } } cached = new data.constructor() // if constructor creates a virtual dom element, use a blank object // as the base cached node instead of copying the virtual el (#277) if (cached.tag) cached = {} cached.nodes = [] return cached } function constructNode(data, namespace) { if (data.attrs.is) { if (namespace === undefined) { return $document.createElement(data.tag, data.attrs.is) } else { return $document.createElementNS(namespace, data.tag, data.attrs.is) } } else { if (namespace === undefined) { return $document.createElement(data.tag) } else { return $document.createElementNS(namespace, data.tag) } } } function constructAttrs(data, node, namespace, hasKeys) { if (hasKeys) { return setAttributes( node, data.tag, data.attrs, {}, namespace) } else { return data.attrs } } function constructChildren( data, node, cached, editable, namespace, configs) { if (data.children != null && data.children.length > 0) { return build( node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) } else { return data.children } } function reconstructCached( data, cached, attrs, children, node, namespace, views, controllers) { cached = { tag: data.tag, attrs: attrs, children: children, nodes: [node] } unloadCachedControllers(cached, views, controllers) if (cached.children && !cached.children.nodes) { cached.children.nodes = [] } // edge case: setting value on