;(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" m.version = function () { return "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) { for (var i = 0; i < list.length; i++) { f(list[i], i) } } function forOwn(obj, f) { for (var prop in obj) { if (hasOwn.call(obj, prop)) { f(obj[prop], prop) } } } // 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 } function gettersetter(store) { function prop() { if (arguments.length) store = arguments[0] return store } prop.toJSON = function () { return store } return prop } function isPromise(object) { return object != null && (isObject(object) || isFunction(object)) && isFunction(object.then) } function simpleResolve(p, callback) { if (p.then) { return p.then(callback) } else { return callback() } } function propify(promise) { var prop = m.prop() promise.then(prop) prop.then = function (resolve, reject) { return promise.then(function () { return resolve(prop()) }, reject) } prop.catch = function (reject) { return promise.then(function () { return prop() }, reject) } prop.finally = function (callback) { return promise.then(function (value) { return simpleResolve(callback(), function () { return value }) }, function (reason) { return simpleResolve(callback(), function () { throw reason }) }) } return prop } m.prop = function (store) { if (isPromise(store)) { return propify(store) } else { return gettersetter(store) } } /** * @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) { for (var i = 0; i < list.length; i++) { var attrs = list[i] attrs = attrs && attrs.attrs if (attrs && attrs.key != null && f(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 `cfgCtx` 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 // // `parent` is a DOM element used for W3C DOM API calls // `pTag` is only used for handling a corner case for textarea values // `pCache` is used to remove nodes in some multi-node cases // `pIndex` 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 // `reattach` 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 // `ns` indicates the closest HTML namespace as it cascades down from an // ancestor // `cfgs` 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 buildContext( parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs ) { return { parent: parentElement, pTag: parentTag, pCache: parentCache, pIndex: parentIndex, data: data, cached: cached, reattach: shouldReattach, index: index, editable: editable, ns: namespace, cfgs: configs } } function builderBuild(inst) { inst.data = dataToString(inst.data) if (inst.data.subtree === "retain") return inst.cached builderMakeCache(inst) if (isArray(inst.data)) { return builderBuildArray(inst) } else if (inst.data != null && isObject(inst.data)) { return builderBuildObject(inst) } else if (isFunction(inst.data)) { return inst.cached } else { return builderHandleTextNode(inst) } } function builderMakeCache(inst) { if (inst.cached != null) { if (type.call(inst.cached) === type.call(inst.data)) { return } if (inst.pCache && inst.pCache.nodes) { var offset = inst.index - inst.pIndex var end = offset + (isArray(inst.data) ? inst.data : inst.cached.nodes).length clear( inst.pCache.nodes.slice(offset, end), inst.pCache.slice(offset, end)) } else if (inst.cached.nodes) { clear(inst.cached.nodes, inst.cached) } } inst.cached = new inst.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 (inst.cached.tag) inst.cached = {} inst.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 }) } function builderBuildArrayChild(inst, child, cached, count) { return builderBuild(buildContext( inst.parent, inst.pTag, inst.cached, inst.index, child, cached, inst.reattach, inst.index + count || count, inst.editable, inst.ns, inst.cfgs )) } // This is by far the most performance-sensitive method here. If you make // any changes, be careful to avoid performance regressions. Note that // variable caching doesn't help, even in the loop. function builderBuildArray(inst) { // eslint-disable-line max-statements inst.data = flatten(inst.data) var nodes = [] var intact = inst.cached.length === inst.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(inst.cached, function (attrs, i) { shouldMaintainIdentities = true existing[attrs.key] = { action: DELETION, index: i } }) buildArrayKeys(inst.data) if (shouldMaintainIdentities) { builderDiffKeys(inst, existing) } // end key algorithm // don't change: faster than forEach var cacheCount = 0 for (var i = 0, len = inst.data.length; i < len; i++) { // diff each item in the array var item = builderBuildArrayChild( inst, inst.data[i], inst.cached[cacheCount], subArrayCount ) if (item !== undefined) { intact = intact && item.nodes.intact subArrayCount += getSubArrayCount(item) inst.cached[cacheCount++] = item } } if (!intact) builderDiffArray(inst, nodes) return inst.cached } function builderDiffKeys(inst, existing) { var keysDiffer = inst.data.length !== inst.cached.length if (!keysDiffer) { forKeys(inst.data, function (attrs, i) { var cachedCell = inst.cached[i] return keysDiffer = cachedCell && cachedCell.attrs && cachedCell.attrs.key !== attrs.key }) } if (keysDiffer) { builderHandleKeysDiffer(inst, existing) } } function builderHandleKeysDiffer(inst, existing) { var cached = inst.cached.nodes forKeys(inst.data, function (key, i) { key = key.key if (existing[key]) { existing[key] = { action: MOVE, index: i, from: existing[key].index, element: cached[existing[key].index] || $document.createElement("div") } } else { existing[key] = { action: INSERTION, index: i } } }) var actions = [] forOwn(existing, function (value) { actions.push(value) }) var changes = actions.sort(sortChanges) var newCached = new Array(inst.cached.length) newCached.nodes = inst.cached.nodes.slice() forEach(changes, function (change) { var index = change.index switch (change.action) { case DELETION: clear(inst.cached[index].nodes, inst.cached[index]) newCached.splice(index, 1) break case INSERTION: var dummy = $document.createElement("div") dummy.key = inst.data[index].attrs.key insertNode(inst.parent, dummy, index) newCached.splice(index, 0, { attrs: {key: inst.data[index].attrs.key}, nodes: [dummy] }) newCached.nodes[index] = dummy break case MOVE: var changeElement = change.element // changeElement is never null if (inst.parent.childNodes[index] !== changeElement) { inst.parent.insertBefore( changeElement, inst.parent.childNodes[index] || null ) } newCached[index] = inst.cached[change.from] newCached.nodes[index] = changeElement } }) inst.cached = newCached } // diffs the array itself function builderDiffArray(inst, nodes) { // update the list of DOM nodes by collecting the nodes from each item for (var i = 0, len = inst.data.length; i < len; i++) { var item = inst.cached[i] if (item != null) { nodes.push.apply(nodes, item.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(inst.cached.nodes, function (node, i) { if (node.parentNode != null && nodes.indexOf(node) < 0) { clear([node], [inst.cached[i]]) } }) if (inst.data.length < inst.cached.length) { inst.cached.length = inst.data.length } inst.cached.nodes = nodes } function builderInitAttrs(inst) { var dataAttrs = inst.data.attrs = inst.data.attrs || {} inst.cached.attrs = inst.cached.attrs || {} var dataAttrKeys = Object.keys(inst.data.attrs) builderMaybeRecreateObject(inst, dataAttrKeys) return dataAttrKeys.length > +("key" in dataAttrs) } function builderGetObjectNamespace(inst) { var data = inst.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" : inst.ns } function builderBuildObject(inst) { var views = [] var controllers = [] builderMarkViews(inst, views, controllers) if (!inst.data.tag && controllers.length) { throw new Error("Component template must return a virtual " + "element, not an array, string, etc.") } var hasKeys = builderInitAttrs(inst) if (isString(inst.data.tag)) { return objectBuild({ builder: inst, hasKeys: hasKeys, views: views, controllers: controllers, ns: builderGetObjectNamespace(inst) }) } } function builderMarkViews(inst, views, controllers) { var cached = inst.cached && inst.cached.controllers while (inst.data.view != null) { builderCheckView(inst, cached, controllers, views) } } var forcing = false var pendingRequests = 0 function builderCheckView(inst, cached, controllers, views) { var view = inst.data.view.$original || inst.data.view var controller = getController( inst.cached.views, view, cached, inst.data.controller ) // Faster to coerce to number and check for NaN var key = +(inst.data && inst.data.attrs && inst.data.attrs.key) if (pendingRequests === 0 || forcing || cached && cached.indexOf(controller) > -1) { inst.data = inst.data.view(controller) } else { inst.data = {tag: "placeholder"} } if (inst.data.subtree === "retain") return inst.cached if (key === key) { // eslint-disable-line no-self-compare (inst.data.attrs = inst.data.attrs || {}).key = key } updateLists(views, controllers, view, controller) } var unloaders = [] function unloaderHandler(inst, ev) { inst.ctrls.splice(inst.ctrls.indexOf(inst.ctrl), 1) inst.views.splice(inst.views.indexOf(inst.view), 1) if (inst.ctrl && isFunction(inst.ctrl.onunload)) { inst.ctrl.onunload(ev) } } function updateLists(views, controllers, view, controller) { views.push(view) unloaders[controllers.push(controller) - 1] = { views: views, view: view, ctrl: controller, ctrls: controllers } } var redrawing = false m.redraw = function (force) { if (redrawing) return redrawing = true if (force) forcing = true try { attemptRedraw(force) } finally { redrawing = forcing = false } } var redrawStrategy = m.redraw.strategy = m.prop() function getController(views, view, cached, controller) { var index = redrawStrategy() === "diff" && views ? views.indexOf(view) : -1 if (index > -1) { return cached[index] } else if (isFunction(controller)) { return new controller() } else { return {} } } function builderMaybeRecreateObject(inst, dataAttrKeys) { // if an element is different enough from the one in cache, recreate it if (builderElemIsDifferentEnough(inst, dataAttrKeys)) { if (inst.cached.nodes.length) clear(inst.cached.nodes) if (inst.cached.cfgCtx && isFunction(inst.cached.cfgCtx.onunload)) { inst.cached.cfgCtx.onunload() } if (inst.cached.controllers) { forEach(inst.cached.controllers, function (controller) { if (controller.unload) { controller.onunload({preventDefault: noop}) } }) } } } // shallow array compare, assumes strings function arraySortCompare(a, b) { var len = a.length if (len !== b.length) return false // A string-integer map is used to simplify the algorithm from // two `O(n * log(n))` loops + an `O(n)` loop to just two O(n) loops // with constant-time (or a super cheap `log(n)`) string key lookup. var i = 0 var cache = Object.create(null) while (i < len) cache[b[i]] = i++ while (i !== 0) { if (cache[a[--i]] === undefined) return false } return true } function builderElemIsDifferentEnough(inst, dataAttrKeys) { var data = inst.data var cached = inst.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 (redrawStrategy() === "all") { return !cached.cfgCtx || cached.cfgCtx.retain !== true } else if (redrawStrategy() === "diff") { return cached.cfgCtx && cached.cfgCtx.retain === false } else { return false } } function objectBuildNewNode(inst) { var node = objectCreateNode(inst) inst.builder.cached = objectReconstruct( inst, node, objectCreateAttrs(inst, node), objectBuildChildren(inst, node) ) return node } function objectBuild(inst) { var builder = inst.builder var isNew = builder.cached.nodes.length === 0 var node = isNew ? objectBuildNewNode(inst) : objectBuildUpdatedNode(inst) if (isNew || builder.reattach && node != null) { insertNode(builder.parent, node, builder.index) } builderScheduleConfigs(builder, node, isNew) return builder.cached } function objectCreateNode(inst) { var data = inst.builder.data if (inst.ns === 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(inst.ns, data.tag, data.attrs.is) } else { return $document.createElementNS(inst.ns, data.tag) } } function objectCreateAttrs(inst, node) { var data = inst.builder.data if (inst.hasKeys) { return setAttributes(node, data.tag, data.attrs, {}, inst.ns) } else { return data.attrs } } function objectMakeChild(inst, node, shouldReattach) { var builder = inst.builder return builderBuild(buildContext( node, builder.data.tag, undefined, undefined, builder.data.children, builder.cached.children, shouldReattach, 0, builder.data.attrs.contenteditable ? node : builder.editable, inst.ns, builder.cfgs )) } function objectBuildChildren(inst, node) { var children = inst.builder.data.children if (children != null && children.length) { return objectMakeChild(inst, node, true) } else { return children } } function objectReconstruct(inst, node, attrs, children) { var data = inst.builder.data var cached = { tag: data.tag, attrs: attrs, children: children, nodes: [node] } objectUnloadCachedControllers(inst, cached) if (cached.children && !cached.children.nodes) { cached.children.nodes = [] } // edge case: setting value on