;(function (global, factory) { // eslint-disable-line "use strict" /* eslint-disable no-undef */ var m = factory(typeof window !== "undefined" ? window : {}) 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 */ })(this, function (window, undefined) { // eslint-disable-line "use strict" var VERSION = "v0.2.1" // Save these two. var type = {}.toString var hasOwn = {}.hasOwnProperty 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() {} function forEach(list, f, inst) { for (var i = 0; i < list.length; i++) { f.call(inst, list[i], i) } } function forOwn(obj, f, inst) { for (var prop in obj) { if (hasOwn.call(obj, prop)) { if (f.call(inst, obj[prop], prop)) break } } } var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/ // eslint-disable-line max-len // 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) // testing API m.deps = function (mock) { initialize(window = mock || window) return 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 */ function checkForAttrs(pairs) { return pairs != null && isObject(pairs) && !("tag" in pairs || "view" in pairs || "subtree" in pairs) } function parseSelector(tag, cell) { var classes = [] var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g var match while ((match = parser.exec(tag)) != null) { if (match[1] === "" && match[2] != null) { 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 assignAttrs(target, attrs, classAttr, classes) { var hasClass = false if (hasOwn.call(attrs, classAttr)) { var value = attrs[classAttr] if (value != null && value !== "") { hasClass = true classes.push(value) } } forOwn(attrs, function (value, attr) { target[attr] = attr === classAttr && hasClass ? "" : value }) if (classes.length) { target[classAttr] = classes.join(" ") } } function parameterize(component) { var args = [] for (var i = 1; i < arguments.length; i++) { args.push(arguments[i]) } var originalCtrl = component.controller || noop function Ctrl() { return originalCtrl.apply(this, args) || this } if (originalCtrl !== noop) { Ctrl.prototype = originalCtrl.prototype } var originalView = component.view || noop function view(ctrl) { var rest = [ctrl].concat(args) for (var i = 1; i < arguments.length; i++) { rest.push(arguments[i]) } return originalView.apply(component, rest) } view.$original = originalView var output = {controller: Ctrl, view: view} if (args[0] && args[0].key != null) { output.attrs = {key: args[0].key} } return output } m.component = parameterize /** * @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) { // The arguments are passed directly like this to delay array // allocation. if (isObject(tag)) return parameterize.apply(null, arguments) if (!isString(tag)) { throw new TypeError("selector in m(selector, attrs, children) " + "should be a string") } // Degenerate case frequently trips people up. Check for it here so that // people know it doesn't work. if (!tag) { throw new TypeError("selector cannot be an empty string") } var hasAttrs = checkForAttrs(pairs) var args = [] for (var i = hasAttrs ? 2 : 1; i < arguments.length; i++) { args.push(arguments[i]) } var children if (args.length === 1 && isArray(args[0])) { children = args[0] } else { children = args } var cell = { tag: "div", attrs: {}, children: children } assignAttrs( cell.attrs, hasAttrs ? pairs : {}, hasAttrs && "class" in pairs ? "class" : "className", parseSelector(tag, cell) ) return cell } function forKeys(list, f, inst) { for (var i = 0; i < list.length; i++) { var attrs = list[i] attrs = attrs && attrs.attrs if (attrs && attrs.key != null && f.call(inst, attrs, i)) { break } } } // 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 try { if (data != null && data.toString() != null) { return data } } catch (e) { // Swallow all errors here. } return "" } 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 while there is an array at this // index. i-- } } return list } function insertNode(parent, node, index) { parent.insertBefore(node, parent.childNodes[index] || null) } // the below recursively 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 function Builder( parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs ) { this.parentElement = parentElement this.parentTag = parentTag this.parentCache = parentCache this.parentIndex = parentIndex this.data = data this.cached = cached this.shouldReattach = shouldReattach this.index = index this.editable = editable this.namespace = namespace this.configs = configs } Builder.prototype.build = function () { this.data = dataToString(this.data) if (this.data.subtree === "retain") return this.cached this.makeCache() if (isArray(this.data)) { return this.buildArray() } else if (this.data != null && isObject(this.data)) { return this.buildObject() } else if (isFunction(this.data)) { return this.cached } else { return this.handleTextNode() } } Builder.prototype.makeCache = function () { if (this.cached != null) { if (type.call(this.cached) === type.call(this.data)) { return } if (this.parentCache && this.parentCache.nodes) { var offset = this.index - this.parentIndex var end = offset + (isArray(this.data) ? this.data : this.cached.nodes).length clear( this.parentCache.nodes.slice(offset, end), this.parentCache.slice(offset, end)) } else if (this.cached.nodes) { clear(this.cached.nodes, this.cached) } } this.cached = new this.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 (this.cached.tag) this.cached = {} this.cached.nodes = [] } var DELETION = 1 var INSERTION = 2 var MOVE = 3 function buildArrayKeys(data) { var guid = 0 forKeys(data, function () { forEach(data, function (attrs) { attrs = attrs && attrs.attrs if (attrs && attrs.key == null) { attrs.key = "__mithril__" + guid++ } }) return true }) } Builder.prototype.buildArrayChild = function (child, cached, count) { return new Builder( this.parentElement, this.parentTag, this.cached, this.index, child, cached, this.shouldReattach, this.index + count || count, this.editable, this.namespace, this.configs ).build() } Builder.prototype.buildArray = function () { this.data = flatten(this.data) var nodes = [] var intact = this.cached.length === this.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(this.cached, function (attrs, i) { shouldMaintainIdentities = true existing[attrs.key] = { action: DELETION, index: i } }) buildArrayKeys(this.data) if (shouldMaintainIdentities) { this.diffKeys(existing) } // end key algorithm // don't change: faster than forEach var cacheCount = 0 for (var i = 0, len = this.data.length; i < len; i++) { // diff each item in the array var item = this.buildArrayChild( this.data[i], this.cached[cacheCount], subArrayCount ) if (item !== undefined) { intact = intact && item.nodes.intact subArrayCount += getSubArrayCount(item) this.cached[cacheCount++] = item } } if (!intact) this.diffArray(nodes) return this.cached } Builder.prototype.diffKeys = function (existing) { var keysDiffer = this.data.length !== this.cached.length if (!keysDiffer) { forKeys(this.data, function (attrs, i) { var cachedCell = this[i] // eslint-disable-line no-invalid-this return keysDiffer = cachedCell && cachedCell.attrs && cachedCell.attrs.key !== attrs.key }, this.cached) } if (keysDiffer) { this.handleKeysDiffer(existing) } } // Simple `this` helper function thisPush(value) { this.push(value) // eslint-disable-line no-invalid-this } Builder.prototype.handleKeysDiffer = function (existing) { forKeys(this.data, function (key, i) { key = key.key if (existing[key]) { existing[key] = { action: MOVE, index: i, from: existing[key].index, element: this[existing[key].index] || // eslint-disable-line $document.createElement("div") } } else { existing[key] = { action: INSERTION, index: i } } }, this.cached.nodes) var actions = [] forOwn(existing, thisPush, actions) var changes = actions.sort(sortChanges) var newCached = new Array(this.cached.length) newCached.nodes = this.cached.nodes.slice() forEach(changes, function (change) { /* eslint-disable no-invalid-this */ var index = change.index switch (change.action) { case DELETION: clear(this.cached[index].nodes, this.cached[index]) newCached.splice(index, 1) break case INSERTION: var dummy = $document.createElement("div") dummy.key = this.data[index].attrs.key insertNode(this.parentElement, dummy, index) newCached.splice(index, 0, { attrs: {key: this.data[index].attrs.key}, nodes: [dummy] }) newCached.nodes[index] = dummy break case MOVE: var changeElement = change.element var maybeChanged = this.parentElement.childNodes[index] if (maybeChanged !== changeElement && changeElement !== null) { this.parentElement.insertBefore( changeElement, maybeChanged || null ) } newCached[index] = this.cached[change.from] newCached.nodes[index] = changeElement } /* eslint-enable no-invalid-this */ }, this) this.cached = newCached } // diffs the array itself Builder.prototype.diffArray = function (nodes) { // update the list of DOM nodes by collecting the nodes from each item for (var i = 0; i < this.data.length; i++) { var cached = this.cached[i] if (cached != null) { nodes.push.apply(nodes, cached.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(this.cached.nodes, function (node, i) { /* eslint-disable no-invalid-this */ if (node.parentNode != null && nodes.indexOf(node) < 0) { clear([node], [this[i]]) } /* eslint-enable no-invalid-this */ }, this.cached) if (this.data.length < this.cached.length) { this.cached.length = this.data.length } this.cached.nodes = nodes } Builder.prototype.initAttrs = function () { var dataAttrs = this.data.attrs = this.data.attrs || {} this.cached.attrs = this.cached.attrs || {} var dataAttrKeys = Object.keys(this.data.attrs) this.maybeRecreateObject(dataAttrKeys) return dataAttrKeys.length > +("key" in dataAttrs) } Builder.prototype.buildObject = function () { var views = [] var controllers = [] this.markViews(views, controllers) if (!this.data.tag && controllers.length) { throw new Error("Component template must return a virtual " + "element, not an array, string, etc.") } var hasKeys = this.initAttrs() if (isString(this.data.tag)) { return new ObjectBuilder( this, hasKeys, views, controllers ).build() } } Builder.prototype.markViews = function (views, controllers) { var cached = this.cached && this.cached.controllers while (this.data.view != null) { this.checkView(cached, controllers, views) } } var forcing = false var pendingRequests = 0 Builder.prototype.checkView = function (cached, controllers, views) { var view = this.data.view.$original || this.data.view var controller = getController( this.cached.views, view, cached, this.data.controller ) // Faster to coerce to number and check for NaN var key = +(this.data && this.data.attrs && this.data.attrs.key) if (pendingRequests === 0 || forcing || cached && cached.indexOf(controller) > -1) { this.data = this.data.view(controller) } else { this.data = {tag: "placeholder"} } if (this.data.subtree === "retain") return this.cached if (key === key) { // eslint-disable-line no-self-compare (this.data.attrs = this.data.attrs || {}).key = key } updateLists(views, controllers, view, controller) } var unloaders = [] function updateLists(views, controllers, view, controller) { views.push(view) var idx = controllers.push(controller) - 1 unloaders[idx] = { views: views, view: view, controller: controller, controllers: controllers, handler: function (ev) { var i = this.controllers.indexOf(this.controller) this.controllers.splice(i, 1) i = this.views.indexOf(this.view) this.views.splice(i, 1) var unload = this.controller && this.controller.onunload if (type.call(unload) === "[object Function]") { this.controller.onunload(ev) } } } } function getController(views, view, cached, controller) { var index = m.redraw.strategy() === "diff" && views ? views.indexOf(view) : -1 if (index > -1) { return cached[index] } else if (typeof controller === "function") { return new controller() } else { return {} } } function unloadSingleController(controller) { if (controller.unload) { controller.onunload({preventDefault: noop}) } } Builder.prototype.maybeRecreateObject = function (dataAttrKeys) { // if an element is different enough from the one in cache, recreate it if (this.elemIsDifferentEnough(dataAttrKeys)) { if (this.cached.nodes.length) clear(this.cached.nodes) if (this.cached.configContext && isFunction(this.cached.configContext.onunload)) { this.cached.configContext.onunload() } if (this.cached.controllers) { forEach(this.cached.controllers, unloadSingleController) } } } // shallow array compare, sorts function arraySortCompare(a, b) { a.sort() b.sort() var len = a.length if (len !== b.length) return false for (var i = 0; i < len; i++) { if (a[i] !== b[i]) return false } return true } Builder.prototype.elemIsDifferentEnough = function (dataAttrKeys) { var data = this.data var cached = this.cached if (data.tag !== cached.tag) return true if (!arraySortCompare(dataAttrKeys, Object.keys(cached.attrs))) { 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 } else if (m.redraw.strategy() === "diff") { return cached.configContext && cached.configContext.retain === false } else { return false } } function getObjectNamespace(builder) { var data = builder.data 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" : builder.namespace } function ObjectBuilder(builder, hasKeys, views, controllers) { this.builder = builder this.hasKeys = hasKeys this.views = views this.controllers = controllers this.namespace = getObjectNamespace(builder) } ObjectBuilder.prototype.buildNewNode = function () { var node = this.createNode() this.builder.cached = this.reconstruct( node, this.createAttrs(node), this.buildChildren(node) ) return node } ObjectBuilder.prototype.build = function () { var builder = this.builder var isNew = builder.cached.nodes.length === 0 var node = isNew ? this.buildNewNode() : this.buildUpdatedNode() if (isNew || builder.shouldReattach && node != null) { insertNode(builder.parentElement, node, builder.index) } builder.scheduleConfigs(node, isNew) return builder.cached } ObjectBuilder.prototype.createNode = function () { var data = this.builder.data if (this.namespace === undefined) { if (data.attrs.is) { return $document.createElement(data.tag, data.attrs.is) } else { return $document.createElement(data.tag) } } else if (data.attrs.is) { return $document.createElementNS(this.namespace, data.tag, data.attrs.is) } else { return $document.createElementNS(this.namespace, data.tag) } } ObjectBuilder.prototype.createAttrs = function (node) { var data = this.builder.data if (this.hasKeys) { return setAttributes(node, data.tag, data.attrs, {}, this.namespace) } else { return data.attrs } } ObjectBuilder.prototype.makeChild = function (node, shouldReattach) { var builder = this.builder return new Builder( node, builder.data.tag, undefined, undefined, builder.data.children, builder.cached.children, shouldReattach, 0, builder.data.attrs.contenteditable ? node : builder.editable, this.namespace, builder.configs ).build() } ObjectBuilder.prototype.buildChildren = function (node) { var data = this.builder.data if (data.children != null && data.children.length !== 0) { return this.makeChild(node, true) } else { return data.children } } ObjectBuilder.prototype.reconstruct = function (node, attrs, children) { var data = this.builder.data var cached = { tag: data.tag, attrs: attrs, children: children, nodes: [node] } this.unloadCachedControllers(cached) if (cached.children && !cached.children.nodes) { cached.children.nodes = [] } // edge case: setting value on