m.route now parses query string in given path into m.route.param
* Params gathered the old way (e.g. /test/:id) overwrite params from the query string
* /?valid => m.route.param('valid') === true
* /?blank= => in m.route.param('blank') === ''
* Supports nested values: ?test[a][b]=1&test[a][b]=2 => m.route.param('a') == {b: "1", c: "2"}
The nested values where only added to behave similar to the encoding function querystring in mithril. Maybe this is not necessary? Code could be shorter.
523 lines
18 KiB
JavaScript
523 lines
18 KiB
JavaScript
Mithril = m = new function app(window) {
|
|
var selectorCache = {}
|
|
var type = {}.toString
|
|
var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.+?)\2)?\]/
|
|
|
|
function m() {
|
|
var args = arguments
|
|
var hasAttrs = type.call(args[1]) == "[object Object]"
|
|
var attrs = hasAttrs ? args[1] : {}
|
|
var classAttrName = "class" in attrs ? "class" : "className"
|
|
var cell = selectorCache[args[0]]
|
|
if (cell === undefined) {
|
|
selectorCache[args[0]] = 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] || true
|
|
}
|
|
}
|
|
if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ")
|
|
}
|
|
cell = clone(cell)
|
|
cell.attrs = clone(cell.attrs)
|
|
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, data, cached, shouldReattach, index, editable, namespace) {
|
|
if (data === null || data === undefined) data = ""
|
|
if (data.subtree === "retain") return
|
|
|
|
var cachedType = type.call(cached), dataType = type.call(data)
|
|
if (cachedType != dataType) {
|
|
if (cached !== null && cached !== undefined) clear(cached.nodes)
|
|
cached = new data.constructor
|
|
cached.nodes = []
|
|
}
|
|
|
|
if (dataType == "[object Array]") {
|
|
var nodes = [], intact = cached.length === data.length, subArrayCount = 0
|
|
for (var i = 0, cacheCount = 0; i < data.length; i++) {
|
|
var item = build(parentElement, null, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace)
|
|
if (item === undefined) continue
|
|
if (!item.nodes.intact) intact = false
|
|
subArrayCount += item instanceof Array ? 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 = nodes.length, node; node = cached.nodes[i]; i++) if (node.parentNode !== null) 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 (typeof data.tag != "string") return
|
|
|
|
var node, isNew = cached.nodes.length === 0
|
|
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,
|
|
attrs: setAttributes(node, data.tag, data.attrs, {}, namespace),
|
|
children: build(node, data.tag, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, 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, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace)
|
|
cached.nodes.intact = true
|
|
if (shouldReattach === true) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
|
|
}
|
|
if (type.call(data.attrs["config"]) == "[object Function]") data.attrs["config"](node, !isNew)
|
|
}
|
|
else {
|
|
var node
|
|
if (cached.nodes.length === 0) {
|
|
if (data.$trusted) {
|
|
node = injectHTML(parentElement, index, data)
|
|
}
|
|
else {
|
|
node = window.document.createTextNode(data)
|
|
parentElement.insertBefore(node, parentElement.childNodes[index] || null)
|
|
}
|
|
cached = "string number boolean".indexOf(typeof data) > -1 ? new data.constructor(data) : data
|
|
cached.nodes = [node]
|
|
}
|
|
else if (cached.valueOf() !== data.valueOf() || shouldReattach === true) {
|
|
if (!editable || editable !== window.document.activeElement) {
|
|
if (data.$trusted) {
|
|
var current = cached.nodes[0], nodes = [current]
|
|
if (current) {
|
|
while (current = current.nextSibling) nodes.push(current)
|
|
clear(nodes)
|
|
node = injectHTML(parentElement, index, data)
|
|
}
|
|
else parentElement.innerHTML = data
|
|
}
|
|
else {
|
|
node = cached.nodes[0]
|
|
if (parentTag === "textarea") parentElement.value = data
|
|
else if (editable) editable.innerHTML = data
|
|
else {
|
|
parentElement.insertBefore(node, parentElement.childNodes[index] || null)
|
|
node.nodeValue = data
|
|
}
|
|
}
|
|
}
|
|
cached = new data.constructor(data)
|
|
cached.nodes = [node]
|
|
}
|
|
else cached.nodes.intact = true
|
|
}
|
|
|
|
return cached
|
|
}
|
|
function setAttributes(node, tag, dataAttrs, cachedAttrs, namespace) {
|
|
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) {
|
|
for (var i = nodes.length - 1; i > -1; i--) nodes[i].parentNode.removeChild(nodes[i])
|
|
nodes.length = 0
|
|
}
|
|
function injectHTML(parentElement, index, data) {
|
|
var nextSibling = parentElement.childNodes[index]
|
|
if (nextSibling) nextSibling.insertAdjacentHTML("beforebegin", data)
|
|
else parentElement.insertAdjacentHTML("beforeend", data)
|
|
return nextSibling ? nextSibling.previousSibling : parentElement.firstChild
|
|
}
|
|
function clone(object) {
|
|
var result = {}
|
|
for (var prop in object) result[prop] = object[prop]
|
|
return result
|
|
}
|
|
function autoredraw(callback, object) {
|
|
return function(e) {
|
|
m.startComputation()
|
|
try {return callback.call(object, e)}
|
|
finally {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 !== html) {
|
|
window.document.replaceChild(html, window.document.documentElement)
|
|
}
|
|
},
|
|
insertBefore: function(node) {
|
|
this.appendChild(node)
|
|
},
|
|
childNodes: []
|
|
}
|
|
var nodeCache = [], cellCache = {}
|
|
m.render = function(root, cell) {
|
|
var index = nodeCache.indexOf(root)
|
|
var id = index < 0 ? nodeCache.push(root) - 1 : index
|
|
var node = root == window.document || root == window.document.documentElement ? documentNode : root
|
|
cellCache[id] = build(node, null, cell, cellCache[id], false, 0, null, undefined)
|
|
}
|
|
|
|
m.trust = function(value) {
|
|
value = new String(value)
|
|
value.$trusted = true
|
|
return value
|
|
}
|
|
|
|
var roots = [], modules = [], controllers = [], now = 0, lastRedraw = 0, lastRedrawId = 0, computePostRedrawHook = null
|
|
m.module = function(root, module) {
|
|
m.startComputation()
|
|
var index = roots.indexOf(root)
|
|
if (index < 0) index = roots.length
|
|
roots[index] = root
|
|
modules[index] = module
|
|
if (controllers[index] && typeof controllers[index].onunload == "function") controllers[index].onunload()
|
|
controllers[index] = new module.controller
|
|
m.endComputation()
|
|
}
|
|
m.redraw = function() {
|
|
now = window.performance && window.performance.now ? window.performance.now() : new window.Date().getTime()
|
|
if (now - lastRedraw > 16) redraw()
|
|
else {
|
|
var cancel = window.cancelAnimationFrame || window.clearTimeout
|
|
var defer = window.requestAnimationFrame || window.setTimeout
|
|
cancel(lastRedrawId)
|
|
lastRedrawId = defer(redraw, 0)
|
|
}
|
|
}
|
|
function redraw() {
|
|
for (var i = 0; i < roots.length; i++) {
|
|
m.render(roots[i], modules[i].view(controllers[i]))
|
|
}
|
|
if (computePostRedrawHook) {
|
|
computePostRedrawHook()
|
|
computePostRedrawHook = null
|
|
}
|
|
lastRedraw = now
|
|
}
|
|
|
|
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) {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) {
|
|
currentRoute = window.location[m.route.mode].slice(modes[m.route.mode].length)
|
|
var root = arguments[0], defaultRoute = arguments[1], router = arguments[2]
|
|
redirect = function(source) {
|
|
var path = source.slice(modes[m.route.mode].length)
|
|
if (!routeByValue(root, router, path)) {
|
|
m.route(defaultRoute, true)
|
|
}
|
|
}
|
|
var listener = m.route.mode == "hash" ? "onhashchange" : "onpopstate"
|
|
window[listener] = function() {
|
|
redirect(window.location[m.route.mode])
|
|
}
|
|
computePostRedrawHook = scrollToHash
|
|
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 = 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 shouldReplaceHistoryEntry = arguments[1] === true
|
|
if (window.history.pushState) {
|
|
computePostRedrawHook = function() {
|
|
window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, window.document.title, modes[m.route.mode] + currentRoute)
|
|
scrollToHash()
|
|
}
|
|
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 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) return !void m.module(root, router[route])
|
|
|
|
var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "$")
|
|
|
|
if (matcher.test(path)) {
|
|
return !void 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, "")] = decodeURIComponent(values[i])
|
|
m.module(root, router[route])
|
|
})
|
|
}
|
|
}
|
|
}
|
|
function routeUnobtrusive(e) {
|
|
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 scrollToHash() {
|
|
if (m.route.mode != "hash" && window.location.hash) window.location.hash = window.location.hash
|
|
}
|
|
function parseQueryString(str) {
|
|
var pairs = str.split("&"), params = {};
|
|
for(var i=0; i < pairs.length; i++) {
|
|
var pair = pairs[i].split("="),
|
|
key = decodeURIComponent(pair[0]),
|
|
value = pair[1] ? decodeURIComponent(pair[1]) : (pair.length === 1 ? true : "");
|
|
if (key.indexOf('[') != -1) {
|
|
var e, regex = /\[?([^\]\[]+)\]?/g,
|
|
subParams = params;
|
|
while ((e = regex.exec(key)) !== null) {
|
|
subParams = subParams[e[1]] = (regex.lastIndex === key.length ? value : subParams[e[1]] || {})
|
|
}
|
|
} else {
|
|
params[key] = value;
|
|
}
|
|
}
|
|
return params;
|
|
}
|
|
|
|
//model
|
|
m.prop = function(store) {
|
|
var prop = function() {
|
|
if (arguments.length) store = arguments[0]
|
|
return store
|
|
}
|
|
prop.toJSON = function() {
|
|
return store
|
|
}
|
|
return prop
|
|
}
|
|
|
|
m.deferred = function() {
|
|
var resolvers = [], rejecters = [], resolved, rejected
|
|
var object = {
|
|
resolve: function(value) {
|
|
if (resolved === undefined) resolved = value
|
|
for (var i = 0; i < resolvers.length; i++) resolvers[i](value)
|
|
resolvers.length = rejecters.length = 0
|
|
},
|
|
reject: function(value) {
|
|
if (rejected === undefined) rejected = value
|
|
for (var i = 0; i < rejecters.length; i++) rejecters[i](value)
|
|
resolvers.length = rejecters.length = 0
|
|
},
|
|
promise: m.prop()
|
|
}
|
|
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 !== undefined) callback("resolve", success)(resolved)
|
|
else if (rejected !== undefined) 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(resolved) {
|
|
return function(value) {
|
|
results.push(value)
|
|
if (!resolved) method = "reject"
|
|
if (results.length == args.length) {
|
|
deferred.promise(results)
|
|
deferred[method](results)
|
|
}
|
|
return value
|
|
}
|
|
}
|
|
|
|
var deferred = m.deferred()
|
|
var results = []
|
|
for (var i = 0; i < args.length; i++) {
|
|
args[i].then(synchronizer(true), synchronizer(false))
|
|
}
|
|
return deferred.promise
|
|
}
|
|
function identity(value) {return value}
|
|
|
|
function ajax(options) {
|
|
var xhr = window.XDomainRequest ? new window.XDomainRequest : new window.XMLHttpRequest
|
|
xhr.open(options.method, options.url, true, options.user, options.password)
|
|
xhr.onload = typeof options.onload == "function" ? options.onload : function() {}
|
|
xhr.onerror = typeof options.onerror == "function" ? options.onerror : function() {}
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState === 4 && xhr.status === 0) {
|
|
xhr.onerror({type: "error", target: xhr})
|
|
}
|
|
}
|
|
if (typeof options.config == "function") options.config(xhr, options)
|
|
xhr.send(options.data)
|
|
return xhr
|
|
}
|
|
function querystring(object, prefix) {
|
|
var str = []
|
|
for(var prop in object) {
|
|
var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop]
|
|
str.push(typeof value == "object" ? querystring(value, key) : encodeURIComponent(key) + "=" + encodeURIComponent(value))
|
|
}
|
|
return str.join("&")
|
|
}
|
|
function bindData(xhrOptions, data, serialize) {
|
|
if (data && Object.keys(data).length > 0) {
|
|
if (xhrOptions.method == "GET") {
|
|
xhrOptions.url = xhrOptions.url + (xhrOptions.url.indexOf("?") < 0 ? "?" : "&") + querystring(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 || JSON.stringify
|
|
var 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) {
|
|
var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity
|
|
var response = unwrap(deserialize(extract(e.target, xhrOptions)))
|
|
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.promise(response)
|
|
deferred[e.type == "load" ? "resolve" : "reject"](response)
|
|
if (xhrOptions.background !== true) m.endComputation()
|
|
}
|
|
ajax(xhrOptions)
|
|
deferred.promise.then = propBinder(deferred.promise)
|
|
return deferred.promise
|
|
}
|
|
function propBinder(promise) {
|
|
var bind = promise.then
|
|
return function(success, error) {
|
|
var next = bind(function(value) {return next(success(value))}, function(value) {return next(error(value))})
|
|
next.then = propBinder(next)
|
|
return next
|
|
}
|
|
}
|
|
|
|
//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
|
|
}(this)
|
|
|
|
if (typeof module != "undefined" && module !== null) module.exports = m
|
|
if (typeof define == "function" && define.amd) define(function() {return m})
|