/* The MIT License (MIT) Copyright (c) 2014 Leo Horie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ ;(function (global, factory) { // eslint-disable-line "use strict" /* eslint-disable no-undef */ var m = factory(global) if (typeof module === "object" && module != null && module.exports) { module.exports = m } else if (typeof define === "function" && define.amd) { define(function () { return m }) } else { global.m = m } /* eslint-enable no-undef */ })(typeof window !== "undefined" ? window : {}, function (global, undefined) { // eslint-disable-line "use strict" m.version = function () { return "v0.2.2-rc.1" } var hasOwn = {}.hasOwnProperty 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]" } function noop() {} /* 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 // self invoking function needed because of the way mocks work function initialize(mock) { $document = mock.document $location = mock.location $cancelAnimationFrame = mock.cancelAnimationFrame || mock.clearTimeout $requestAnimationFrame = mock.requestAnimationFrame || mock.setTimeout } // testing API m.deps = function (mock) { initialize(global = mock || window) return global } m.deps(global) /** * @typedef {String} Tag * A string that looks like -> div.classname#id[param=one][param2=two] * Which describes a DOM node */ function parseTagAttrs(cell, tag) { var classes = [] var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g var match 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 = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/.exec(match[3]) cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" : true) } } return classes } function getVirtualChildren(args, hasAttrs) { var children = hasAttrs ? args.slice(1) : args if (children.length === 1 && isArray(children[0])) { return children[0] } else { return children } } function assignAttrs(target, attrs, classes) { var classAttr = "class" in attrs ? "class" : "className" for (var attrName in attrs) { if (hasOwn.call(attrs, attrName)) { if (attrName === classAttr && attrs[attrName] != null && attrs[attrName] !== "") { classes.push(attrs[attrName]) // create key in correct iteration order target[attrName] = "" } else { target[attrName] = attrs[attrName] } } } if (classes.length) target[classAttr] = classes.join(" ") } /** * * @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) if (!isString(tag)) { throw new Error("selector in m(selector, attrs, children) should " + "be a string") } var hasAttrs = pairs != null && isObject(pairs) && !("tag" in pairs || "view" in pairs || "subtree" in pairs) var attrs = hasAttrs ? pairs : {} var cell = { tag: "div", attrs: {}, children: getVirtualChildren(args, hasAttrs) } assignAttrs(cell.attrs, attrs, parseTagAttrs(cell, tag)) return cell } function forEach(list, f) { for (var i = 0; i < list.length && !f(list[i], i++);) { // function called in condition } } 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 some versions of Firefox (behavior depends on // version) try { if (data != null && data.toString() != null) return data } catch (e) { // silently ignore errors } return "" } // 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 var INSERTION = 2 var 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) if (hasOwn.call(existing, prop)) { 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] return keysDiffer = cachedCell && cachedCell.attrs && cachedCell.attrs.key !== attrs.key }) } 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 isDifferentEnough(data, cached, dataAttrKeys) { if (data.tag !== cached.tag) return true if (dataAttrKeys.sort().join() !== Object.keys(cached.attrs).sort().join()) { return true } if (data.attrs.id !== cached.attrs.id) { return true } if (data.attrs.key !== cached.attrs.key) { return true } if (m.redraw.strategy() === "all") { return !cached.configContext || cached.configContext.retain !== true } if (m.redraw.strategy() === "diff") { return cached.configContext && cached.configContext.retain === false } return false } function maybeRecreateObject(data, cached, dataAttrKeys) { // if an element is different enough from the one in cache, recreate it if (isDifferentEnough(data, cached, dataAttrKeys)) { 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.onunload) controller.onunload({preventDefault: noop}); }); } } } function getObjectNamespace(data, namespace) { if (data.attrs.xmlns) return data.attrs.xmlns if (data.tag === "svg") return "http://www.w3.org/2000/svg" if (data.tag === "math") return "http://www.w3.org/1998/Math/MathML" return namespace } var pendingRequests = 0 m.startComputation = function () { pendingRequests++ } m.endComputation = function () { if (pendingRequests > 1) { pendingRequests-- } else { pendingRequests = 0 m.redraw() } } 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 if (parentTag === "textarea") { //