var m = (function app(window, undefined) { 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 type = {}.toString; var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; var noop = function () {}; // caching commonly used variables var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; // 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 match, classes = []; if (!isString(tag)) throw new Error("selector in m(selector, attrs, children) should be a string"); while (match = parser.exec(tag)) { 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); } } 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]); cell.attrs[attrName] = ""; //create key in correct iteration order } 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++);) {} } function forKeys(list, f) { forEach(list, function (attrs, i) { return (attrs = attrs && attrs.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) data = ""; } catch (e) { data = ""; } 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 build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached` //the diff algorithm can be summarized as this: //1 - compare `data` and `cached` //2 - if they are different, copy `data` to `cached` and update the DOM based on what the difference is //3 - recursively apply this algorithm for every array and for the children of every virtual element //the `cached` data structure is essentially the same as the previous redraw's `data` data structure, with a few additions: //- `cached` always has a property called `nodes`, which is a list of DOM elements that correspond to the data represented by the respective virtual element //- in order to support attaching `nodes` as a property of `cached`, `cached` is *always* a non-primitive object, i.e. if the data was a string, then cached is a String instance. If data was `null` or `undefined`, cached is `new String("")` //- `cached also has a `configContext` property, which is the state storage object exposed by config(element, isInitialized, context) //- when `cached` is an Object, it represents a virtual element; when it's an Array, it represents a list of elements; when it's a String, Number or Boolean, it represents a text node //`parentElement` is a DOM element used for W3C DOM API calls //`parentTag` is only used for handling a corner case for textarea values //`parentCache` is used to remove nodes in some multi-node cases //`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable //`data` and `cached` are, respectively, the new and old nodes being diffed //`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent) //`editable` is a flag that indicates whether an ancestor is contenteditable //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor //`configs` is a list of config functions to run after the topmost `build` call finishes running //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")} //- it simplifies diffing code data = dataToString(data); if (data.subtree === "retain") return cached; if (cached == null || type.call(cached) !== type.call(data)) { if (cached != null) { 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 (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277) cached.nodes = []; } if (isArray(data)) { //recursively flatten array for (var i = 0; i < data.length; i++) { if (isArray(data[i])) { data = data.concat.apply([], data); i--; //check current index again and flatten until there are no more nested arrays at that index } } var nodes = [], intact = cached.length === data.length, 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 DELETION = 1, INSERTION = 2 , MOVE = 3; var existing = {}, shouldMaintainIdentities = false; forKeys(cached, function (attrs, i) { shouldMaintainIdentities = true; existing[cached[i].attrs.key] = {action: DELETION, index: i}; }); var guid = 0; forKeys(data, function () { forEach(data, function (attrs) { if ((attrs = attrs && attrs.attrs) && attrs.key == null) { attrs.key = "__mithril__" + guid++; } }); return 1; }); if (shouldMaintainIdentities) { var keysDiffer = data.length != cached.length; if (!keysDiffer) { forKeys(data, function (attrs, i) { var cachedCell = cached[i]; return keysDiffer = cachedCell && cachedCell.attrs && cachedCell.attrs.key != attrs.key; }); } if (keysDiffer) { 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) { if (change.action === DELETION) { clear(cached[change.index].nodes, cached[change.index]); newCached.splice(change.index, 1); } if (change.action === INSERTION) { var dummy = $document.createElement("div"); dummy.key = data[change.index].attrs.key; parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null); newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}); newCached.nodes[change.index] = dummy; } if (change.action === MOVE) { if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null); } newCached[change.index] = cached[change.from]; newCached.nodes[change.index] = change.element; } }); cached = newCached; } } //end key algorithm var cacheCount = 0; forEach(data, function (entry) { //diff each item in the array var item = build(parentElement, parentTag, cached, index, entry, cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs); if (item !== undefined) { if (!item.nodes.intact) intact = false; 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 subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length; } else subArrayCount += isArray(item) ? item.length : 1; cached[cacheCount++] = item; } }); if (!intact) { //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; } } else if (data != null && isObject(data)) { var views = [], controllers = []; while (data.view) { var view = data.view.$original || data.view; var controllerIndex = m.redraw.strategy() == "diff" && cached.views ? cached.views.indexOf(view) : -1; var controller = controllerIndex > -1 ? cached.controllers[controllerIndex] : new (data.controller || noop); var key = data && data.attrs && data.attrs.key; data = (pendingRequests == 0 || forcing) || (cached && cached.controllers && cached.controllers.indexOf(controller) > -1) ? data.view(controller) : {tag: "placeholder"}; if (data.subtree === "retain") return cached; if (key != null) { if (!data.attrs) data.attrs = {}; data.attrs.key = key; } if (controller.onunload) unloaders.push({controller: controller, handler: controller.onunload}); views.push(view); controllers.push(controller); } if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc."); if (!data.attrs) data.attrs = {}; if (!cached.attrs) cached.attrs = {}; var dataAttrKeys = Object.keys(data.attrs); var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0); //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) { for (var i = 0, controller; controller = cached.controllers[i]; i++) { if (isFunction(controller.onunload)) controller.onunload({preventDefault: noop}); } } } if (!isString(data.tag)) return; var node, isNew = cached.nodes.length === 0; if (data.attrs.xmlns) namespace = data.attrs.xmlns; else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"; else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML"; if (isNew) { if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is); else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag); cached = { tag: data.tag, //set attributes first, then create children attrs: hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs, children: data.children != null && data.children.length > 0 ? build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : data.children, nodes: [node] }; if (controllers.length) { cached.views = views; cached.controllers = controllers; for (var i = 0, controller; controller = controllers[i]; i++) { 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; } } } if (cached.children && !cached.children.nodes) cached.children.nodes = []; //edge case: setting value on