var m = (function app(window, undefined) { "use strict"; var VERSION = "v0.2.1"; 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); m.version = function() { return VERSION; }; /** * @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)) != 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); } } 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) return ""; } catch (e) { return ""; } return data; } // This function was causing deopts in Chrome. function injectTextNode(parentElement, first, index, data) { try { insertNode(parentElement, first, index); 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, INSERTION = 2, MOVE = 3; function handleKeysDiffer(data, existing, cached, parentElement) { forKeys(data, function (key, i) { existing[key = key.key] = existing[key] ? { action: MOVE, index: i, from: existing[key].index, element: cached.nodes[existing[key].index] || $document.createElement("div") } : {action: INSERTION, index: i}; }); var actions = []; for (var prop in existing) actions.push(existing[prop]); var changes = actions.sort(sortChanges), 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]; return keysDiffer = cachedCell && cachedCell.attrs && cachedCell.attrs.key !== attrs.key; }); } return keysDiffer ? handleKeysDiffer(data, existing, cached, parentElement) : 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) { return data.attrs.xmlns ? data.attrs.xmlns : data.tag === "svg" ? "http://www.w3.org/2000/svg" : data.tag === "math" ? "http://www.w3.org/1998/Math/MathML" : 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 = typeof data === "string" || typeof data === "number" || typeof data === "boolean" ? new data.constructor(data) : 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); } //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 else 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 return cached.nodes.length === 0 ? handleNonexistentNodes(data, parentElement, index) : cached.valueOf() !== data.valueOf() || shouldReattach === true ? reattachNodes(data, cached, parentElement, editable, index, parentTag) : (cached.nodes.intact = true, 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 = [], 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 existing = {}, 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; //faster explicitly written 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, 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) { return namespace === undefined ? data.attrs.is ? $document.createElement(data.tag, data.attrs.is) : $document.createElement(data.tag) : data.attrs.is ? $document.createElementNS(namespace, data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag); } function constructAttrs(data, node, namespace, hasKeys) { return hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs; } function constructChildren(data, node, cached, editable, namespace, configs) { return 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; } function reconstructCached(data, attrs, children, node, namespace, views, controllers) { var 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