Mithril = m = new function app(window) { var type = {}.toString var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/ function m() { var args = arguments var hasAttrs = type.call(args[1]) == "[object Object]" && !("tag" in args[1]) && !("subtree" in args[1]) var attrs = hasAttrs ? args[1] : {} var classAttrName = "class" in attrs ? "class" : "className" var cell = {tag: "div", attrs: {}} var match, classes = [] while (match = parser.exec(args[0])) { if (match[1] == "") 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) } } if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ") cell.children = hasAttrs ? args[2] : args[1] for (var attrName in attrs) { if (attrName == classAttrName) cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName] else cell.attrs[attrName] = attrs[attrName] } return cell } function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { if (data === null || data === undefined) data = "" if (data.subtree === "retain") return cached var cachedType = type.call(cached), dataType = type.call(data) if (cachedType != dataType) { if (cached !== null && cached !== undefined) { if (parentCache && parentCache.nodes) { var offset = index - parentIndex var end = offset + (dataType == "[object Array]" ? data : cached.nodes).length clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) } else clear(cached.nodes, cached) } cached = new data.constructor cached.nodes = [] } if (dataType == "[object Array]") { data = flatten(data) var nodes = [], intact = cached.length === data.length, subArrayCount = 0 var DELETION = 1, INSERTION = 2 , MOVE = 3 var existing = {}, unkeyed = [], shouldMaintainIdentities = false for (var i = 0; i < cached.length; i++) { if (cached[i] && cached[i].attrs && cached[i].attrs.key !== undefined) { shouldMaintainIdentities = true existing[cached[i].attrs.key] = {action: DELETION, index: i} } } if (shouldMaintainIdentities) { for (var i = 0; i < data.length; i++) { if (data[i] && data[i].attrs) { if (data[i].attrs.key !== undefined) { var key = data[i].attrs.key if (!existing[key]) existing[key] = {action: INSERTION, index: i} else existing[key] = {action: MOVE, index: i, from: existing[key].index, element: parentElement.childNodes[existing[key].index]} } else unkeyed.push({index: i, element: parentElement.childNodes[i]}) } } var actions = Object.keys(existing).map(function(key) {return existing[key]}) var changes = actions.sort(function(a, b) {return a.action - b.action || a.index - b.index}) var newCached = cached.slice() for (var i = 0, change; change = changes[i]; i++) { if (change.action == DELETION) { clear(cached[change.index].nodes, cached[change.index]) newCached.splice(change.index, 1) } if (change.action == INSERTION) { var dummy = window.document.createElement("div") dummy.key = data[change.index].attrs.key parentElement.insertBefore(dummy, parentElement.childNodes[change.index]) newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) } if (change.action == MOVE) { if (parentElement.childNodes[change.index] !== change.element) { parentElement.insertBefore(change.element, parentElement.childNodes[change.index]) } newCached[change.index] = cached[change.from] } } for (var i = 0; i < unkeyed.length; i++) { var change = unkeyed[i] parentElement.insertBefore(change.element, parentElement.childNodes[change.index]) newCached[change.index] = cached[change.index] } cached = newCached cached.nodes = [] for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes.push(child) } for (var i = 0, cacheCount = 0; i < data.length; i++) { var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs) if (item === undefined) continue if (!item.nodes.intact) intact = false var isArray = item instanceof Array subArrayCount += isArray ? item.length : 1 cached[cacheCount++] = item } if (!intact) { for (var i = 0; i < data.length; i++) { if (cached[i] !== undefined) nodes = nodes.concat(cached[i].nodes) } for (var i = 0, node; node = cached.nodes[i]; i++) { if (node.parentNode !== null && nodes.indexOf(node) < 0) node.parentNode.removeChild(node) } for (var i = cached.nodes.length, node; node = nodes[i]; i++) { if (node.parentNode === null) parentElement.appendChild(node) } if (data.length < cached.length) cached.length = data.length cached.nodes = nodes } } else if (dataType == "[object Object]") { if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) { clear(cached.nodes) if (cached.configContext && typeof cached.configContext.onunload == "function") cached.configContext.onunload() } if (typeof data.tag != "string") 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" if (isNew) { node = namespace === undefined ? window.document.createElement(data.tag) : window.document.createElementNS(namespace, data.tag) cached = { tag: data.tag, //process children before attrs so that select.value works correctly children: data.children !== undefined ? build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : undefined, attrs: setAttributes(node, data.tag, data.attrs, {}, namespace), nodes: [node] } parentElement.insertBefore(node, parentElement.childNodes[index] || null) } else { node = cached.nodes[0] 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 (shouldReattach === true) parentElement.insertBefore(node, parentElement.childNodes[index] || null) } if (type.call(data.attrs["config"]) == "[object Function]") { configs.push(data.attrs["config"].bind(window, node, !isNew, cached.configContext = cached.configContext || {}, cached)) } } else { var nodes if (cached.nodes.length === 0) { if (data.$trusted) { nodes = injectHTML(parentElement, index, data) } else { nodes = [window.document.createTextNode(data)] parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null) } cached = "string number boolean".indexOf(typeof data) > -1 ? new data.constructor(data) : data cached.nodes = nodes } else if (cached.valueOf() !== data.valueOf() || shouldReattach === true) { nodes = cached.nodes if (!editable || editable !== window.document.activeElement) { if (data.$trusted) { clear(nodes, cached) nodes = injectHTML(parentElement, index, data) } else { if (parentTag === "textarea") parentElement.value = data else if (editable) editable.innerHTML = data else { if (nodes[0].nodeType == 1 || nodes.length > 1) { //was a trusted string clear(cached.nodes, cached) nodes = [window.document.createTextNode(data)] } parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null) nodes[0].nodeValue = data } } } cached = new data.constructor(data) cached.nodes = nodes } else cached.nodes.intact = true } return cached } function setAttributes(node, tag, dataAttrs, cachedAttrs, namespace) { var groups = {} for (var attrName in dataAttrs) { var dataAttr = dataAttrs[attrName] var cachedAttr = cachedAttrs[attrName] if (!(attrName in cachedAttrs) || (cachedAttr !== dataAttr) || node === window.document.activeElement) { cachedAttrs[attrName] = dataAttr if (attrName === "config") continue else if (typeof dataAttr == "function" && attrName.indexOf("on") == 0) { node[attrName] = autoredraw(dataAttr, node) } else if (attrName === "style" && typeof dataAttr == "object") { for (var rule in dataAttr) { if (cachedAttr === undefined || cachedAttr[rule] !== dataAttr[rule]) node.style[rule] = dataAttr[rule] } for (var rule in cachedAttr) { if (!(rule in dataAttr)) node.style[rule] = "" } } else if (namespace !== undefined) { if (attrName === "href") node.setAttributeNS("http://www.w3.org/1999/xlink", "href", dataAttr) else if (attrName === "className") node.setAttribute("class", dataAttr) else node.setAttribute(attrName, dataAttr) } else if (attrName === "value" && tag === "input") { if (node.value !== dataAttr) node.value = dataAttr } else if (attrName in node && !(attrName == "list" || attrName == "style")) { node[attrName] = dataAttr } else node.setAttribute(attrName, dataAttr) } } return cachedAttrs } function clear(nodes, cached) { for (var i = nodes.length - 1; i > -1; i--) { if (nodes[i] && nodes[i].parentNode) { nodes[i].parentNode.removeChild(nodes[i]) cached = [].concat(cached) if (cached[i]) unload(cached[i]) } } if (nodes.length != 0) nodes.length = 0 } function unload(cached) { if (cached.configContext && typeof cached.configContext.onunload == "function") cached.configContext.onunload() if (cached.children) { if (cached.children instanceof Array) for (var i = 0; i < cached.children.length; i++) unload(cached.children[i]) else if (cached.children.tag) unload(cached.children) } } function injectHTML(parentElement, index, data) { var nextSibling = parentElement.childNodes[index] if (nextSibling) { var isElement = nextSibling.nodeType != 1 var placeholder = window.document.createElement("span") if (isElement) { parentElement.insertBefore(placeholder, nextSibling) placeholder.insertAdjacentHTML("beforebegin", data) parentElement.removeChild(placeholder) } else nextSibling.insertAdjacentHTML("beforebegin", data) } else parentElement.insertAdjacentHTML("beforeend", data) var nodes = [] while (parentElement.childNodes[index] !== nextSibling) { nodes.push(parentElement.childNodes[index]) index++ } return nodes } function flatten(data) { var flattened = [] for (var i = 0; i < data.length; i++) { var item = data[i] if (item instanceof Array) flattened.push.apply(flattened, flatten(item)) else flattened.push(item) } return flattened } function autoredraw(callback, object, group) { return function(e) { e = e || event m.startComputation() try {return callback.call(object, e)} finally { if (!lastRedrawId) lastRedrawId = -1; m.endComputation() } } } var html var documentNode = { insertAdjacentHTML: function(_, data) { window.document.write(data) window.document.close() }, appendChild: function(node) { if (html === undefined) html = window.document.createElement("html") if (node.nodeName == "HTML") html = node else html.appendChild(node) if (window.document.documentElement && window.document.documentElement !== html) { window.document.replaceChild(html, window.document.documentElement) } else window.document.appendChild(html) }, insertBefore: function(node) { this.appendChild(node) }, childNodes: [] } var nodeCache = [], cellCache = {} m.render = function(root, cell) { var configs = [] if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.") var id = getCellCacheKey(root) var node = root == window.document || root == window.document.documentElement ? documentNode : root if (cellCache[id] === undefined) clear(node.childNodes) cellCache[id] = build(node, null, undefined, undefined, cell, cellCache[id], false, 0, null, undefined, configs) for (var i = 0; i < configs.length; i++) configs[i]() } function getCellCacheKey(element) { var index = nodeCache.indexOf(element) return index < 0 ? nodeCache.push(element) - 1 : index } m.trust = function(value) { value = new String(value) value.$trusted = true return value } var roots = [], modules = [], controllers = [], lastRedrawId = 0, computePostRedrawHook = null m.module = function(root, module) { var index = roots.indexOf(root) if (index < 0) index = roots.length var isPrevented = false if (controllers[index] && typeof controllers[index].onunload == "function") { var event = { preventDefault: function() {isPrevented = true} } controllers[index].onunload(event) } if (!isPrevented) { m.startComputation() roots[index] = root modules[index] = module controllers[index] = new module.controller m.endComputation() } } m.redraw = function() { var cancel = window.cancelAnimationFrame || window.clearTimeout var defer = window.requestAnimationFrame || window.setTimeout if (lastRedrawId) { cancel(lastRedrawId) lastRedrawId = defer(redraw, 0) } else { redraw() lastRedrawId = defer(function() {lastRedrawId = null}, 0) } } function redraw() { for (var i = 0; i < roots.length; i++) { if (controllers[i]) m.render(roots[i], modules[i].view(controllers[i])) } if (computePostRedrawHook) { computePostRedrawHook() computePostRedrawHook = null } lastRedrawId = null } var pendingRequests = 0 m.startComputation = function() {pendingRequests++} m.endComputation = function() { pendingRequests = Math.max(pendingRequests - 1, 0) if (pendingRequests == 0) m.redraw() } m.withAttr = function(prop, withAttrCallback) { return function(e) { e = e || event withAttrCallback(prop in e.currentTarget ? e.currentTarget[prop] : e.currentTarget.getAttribute(prop)) } } //routing var modes = {pathname: "", hash: "#", search: "?"} var redirect = function() {}, routeParams = {}, currentRoute m.route = function() { if (arguments.length === 0) return currentRoute else if (arguments.length === 3 && typeof arguments[1] == "string") { var root = arguments[0], defaultRoute = arguments[1], router = arguments[2] redirect = function(source) { var path = currentRoute = normalizeRoute(source) if (!routeByValue(root, router, path)) { m.route(defaultRoute, true) } } var listener = m.route.mode == "hash" ? "onhashchange" : "onpopstate" window[listener] = function() { if (currentRoute != normalizeRoute(window.location[m.route.mode])) { redirect(window.location[m.route.mode]) } } computePostRedrawHook = setScroll window[listener]() } else if (arguments[0].addEventListener) { var element = arguments[0] var isInitialized = arguments[1] if (element.href.indexOf(modes[m.route.mode]) < 0) { element.href = window.location.pathname + modes[m.route.mode] + element.pathname } if (!isInitialized) { element.removeEventListener("click", routeUnobtrusive) element.addEventListener("click", routeUnobtrusive) } } else if (typeof arguments[0] == "string") { currentRoute = arguments[0] var querystring = typeof arguments[1] == "object" ? buildQueryString(arguments[1]) : null if (querystring) currentRoute += (currentRoute.indexOf("?") === -1 ? "?" : "&") + querystring var shouldReplaceHistoryEntry = (arguments.length == 3 ? arguments[2] : arguments[1]) === true if (window.history.pushState) { computePostRedrawHook = function() { window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, window.document.title, modes[m.route.mode] + currentRoute) setScroll() } redirect(modes[m.route.mode] + currentRoute) } else window.location[m.route.mode] = currentRoute } } m.route.param = function(key) {return routeParams[key]} m.route.mode = "search" function normalizeRoute(route) {return route.slice(modes[m.route.mode].length)} function routeByValue(root, router, path) { routeParams = {} var queryStart = path.indexOf("?") if (queryStart !== -1) { routeParams = parseQueryString(path.substr(queryStart + 1, path.length)) path = path.substr(0, queryStart) } for (var route in router) { if (route == path) { reset(root) m.module(root, router[route]) return true } var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") if (matcher.test(path)) { reset(root) path.replace(matcher, function() { var keys = route.match(/:[^\/]+/g) || [] var values = [].slice.call(arguments, 1, -2) for (var i = 0; i < keys.length; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeSpace(values[i]) m.module(root, router[route]) }) return true } } } function reset(root) { var cacheKey = getCellCacheKey(root) clear(root.childNodes, cellCache[cacheKey]) cellCache[cacheKey] = undefined } function routeUnobtrusive(e) { e = e || event if (e.ctrlKey || e.metaKey || e.which == 2) return e.preventDefault() m.route(e.currentTarget[m.route.mode].slice(modes[m.route.mode].length)) } function setScroll() { if (m.route.mode != "hash" && window.location.hash) window.location.hash = window.location.hash else window.scrollTo(0, 0) } function buildQueryString(object, prefix) { var str = [] for(var prop in object) { var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop] str.push(typeof value == "object" ? buildQueryString(value, key) : encodeURIComponent(key) + "=" + encodeURIComponent(value)) } return str.join("&") } function parseQueryString(str) { var pairs = str.split("&"), params = {} for (var i = 0; i < pairs.length; i++) { var pair = pairs[i].split("=") params[decodeSpace(pair[0])] = pair[1] ? decodeSpace(pair[1]) : (pair.length === 1 ? true : "") } return params } function decodeSpace(string) { return decodeURIComponent(string.replace(/\+/g, " ")) } //model m.prop = function(store) { var prop = function() { if (arguments.length) store = arguments[0] return store } prop.toJSON = function() { return store } return prop } var none = {} m.deferred = function() { var resolvers = [], rejecters = [], resolved = none, rejected = none, promise = m.prop() var object = { resolve: function(value) { if (resolved === none) promise(resolved = value) for (var i = 0; i < resolvers.length; i++) resolvers[i](value) resolvers.length = rejecters.length = 0 }, reject: function(value) { if (rejected === none) rejected = value for (var i = 0; i < rejecters.length; i++) rejecters[i](value) resolvers.length = rejecters.length = 0 }, promise: promise } object.promise.resolvers = resolvers object.promise.then = function(success, error) { var next = m.deferred() if (!success) success = identity if (!error) error = identity function callback(method, callback) { return function(value) { try { var result = callback(value) if (result && typeof result.then == "function") result.then(next[method], error) else next[method](result !== undefined ? result : value) } catch (e) { if (e instanceof Error && e.constructor !== Error) throw e else next.reject(e) } } } if (resolved !== none) callback("resolve", success)(resolved) else if (rejected !== none) callback("reject", error)(rejected) else { resolvers.push(callback("resolve", success)) rejecters.push(callback("reject", error)) } return next.promise } return object } m.sync = function(args) { var method = "resolve" function synchronizer(pos, resolved) { return function(value) { results[pos] = value if (!resolved) method = "reject" if (--outstanding == 0) { deferred.promise(results) deferred[method](results) } return value } } var deferred = m.deferred() var outstanding = args.length var results = new Array(outstanding) for (var i = 0; i < args.length; i++) { args[i].then(synchronizer(i, true), synchronizer(i, false)) } return deferred.promise } function identity(value) {return value} function ajax(options) { var xhr = new window.XMLHttpRequest xhr.open(options.method, options.url, true, options.user, options.password) xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) options.onload({type: "load", target: xhr}) else options.onerror({type: "error", target: xhr}) } } if (options.serialize == JSON.stringify && options.method != "GET") { xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); } if (typeof options.config == "function") { var maybeXhr = options.config(xhr, options) if (maybeXhr !== undefined) xhr = maybeXhr } xhr.send(options.method == "GET" ? "" : options.data) return xhr } function bindData(xhrOptions, data, serialize) { if (data && Object.keys(data).length > 0) { if (xhrOptions.method == "GET") { xhrOptions.url = xhrOptions.url + (xhrOptions.url.indexOf("?") < 0 ? "?" : "&") + buildQueryString(data) } else xhrOptions.data = serialize(data) } return xhrOptions } function parameterizeUrl(url, data) { var tokens = url.match(/:[a-z]\w+/gi) if (tokens && data) { for (var i = 0; i < tokens.length; i++) { var key = tokens[i].slice(1) url = url.replace(tokens[i], data[key]) delete data[key] } } return url } m.request = function(xhrOptions) { if (xhrOptions.background !== true) m.startComputation() var deferred = m.deferred() var serialize = xhrOptions.serialize = xhrOptions.serialize || JSON.stringify var deserialize = xhrOptions.deserialize = xhrOptions.deserialize || JSON.parse var extract = xhrOptions.extract || function(xhr) { return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText } xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data) xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize) xhrOptions.onload = xhrOptions.onerror = function(e) { try { e = e || event var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity var response = unwrap(deserialize(extract(e.target, xhrOptions))) if (e.type == "load") { if (response instanceof Array && xhrOptions.type) { for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i]) } else if (xhrOptions.type) response = new xhrOptions.type(response) } deferred[e.type == "load" ? "resolve" : "reject"](response) } catch (e) { if (e instanceof SyntaxError) throw new SyntaxError("Could not parse HTTP response. See http://lhorie.github.io/mithril/mithril.request.html#using-variable-data-formats") else if (e instanceof Error && e.constructor !== Error) throw e else deferred.reject(e) } if (xhrOptions.background !== true) m.endComputation() } ajax(xhrOptions) return deferred.promise } //testing API m.deps = function(mock) {return window = mock} //for internal testing only, do not use `m.deps.factory` m.deps.factory = app return m }(typeof window != "undefined" ? window : {}) if (typeof module != "undefined" && module !== null) module.exports = m if (typeof define == "function" && define.amd) define(function() {return m}) ;;;