mithril_rails 0.0.1 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,454 +1,989 @@
1
- new function(window) {
2
- var selectorCache = {}
3
- var type = {}.toString
4
- var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.+?)\2)?\]/
5
-
6
- Mithril = m = function() {
7
- var args = arguments
8
- var hasAttrs = type.call(args[1]) == "[object Object]"
9
- var attrs = hasAttrs ? args[1] : {}
10
- var classAttrName = "class" in attrs ? "class" : "className"
11
- var cell = selectorCache[args[0]]
12
- if (cell === undefined) {
13
- selectorCache[args[0]] = cell = {tag: "div", attrs: {}}
14
- var match, classes = []
15
- while (match = parser.exec(args[0])) {
16
- if (match[1] == "") cell.tag = match[2]
17
- else if (match[1] == "#") cell.attrs.id = match[2]
18
- else if (match[1] == ".") classes.push(match[2])
19
- else if (match[3][0] == "[") {
20
- var pair = attrParser.exec(match[3])
21
- cell.attrs[pair[1]] = pair[3] || true
22
- }
23
- }
24
- if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ")
25
- }
26
- cell = clone(cell)
27
- cell.attrs = clone(cell.attrs)
28
- cell.children = hasAttrs ? args[2] : args[1]
29
- for (var attrName in attrs) {
30
- if (attrName == classAttrName) cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName]
31
- else cell.attrs[attrName] = attrs[attrName]
32
- }
33
- return cell
34
- }
35
- function build(parent, data, cached, shouldReattach, index, namespace) {
36
- if (data === null || data === undefined) {
37
- if (cached) clear(cached.nodes)
38
- return
39
- }
40
- if (data.subtree === "retain") return
41
-
42
- var cachedType = type.call(cached), dataType = type.call(data)
43
- if (cachedType != dataType) {
44
- if (cached !== null && cached !== undefined) clear(cached.nodes)
45
- cached = new data.constructor
46
- cached.nodes = []
47
- }
48
-
49
- if (dataType == "[object Array]") {
50
- var nodes = [], intact = cached.length === data.length, subArrayCount = 0
51
- for (var i = 0; i < data.length; i++) {
52
- var item = build(parent, data[i], cached[i], shouldReattach, index + subArrayCount || subArrayCount, namespace)
53
- if (item === undefined) continue
54
- if (!item.nodes.intact) intact = false
55
- subArrayCount += item instanceof Array ? item.length : 1
56
- cached[i] = item
57
- }
58
- if (!intact) {
59
- for (var i = 0; i < data.length; i++) if (cached[i] !== undefined) nodes = nodes.concat(cached[i].nodes)
60
- for (var i = nodes.length, node; node = cached.nodes[i]; i++) if (node.parentNode !== null) node.parentNode.removeChild(node)
61
- for (var i = cached.nodes.length, node; node = nodes[i]; i++) if (node.parentNode === null) parent.appendChild(node)
62
- cached.length = data.length
63
- cached.nodes = nodes
64
- }
65
- }
66
- else if (dataType == "[object Object]") {
67
- if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join()) clear(cached.nodes)
68
- if (typeof data.tag != "string") return
69
-
70
- var node, isNew = cached.nodes.length === 0
71
- if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"
72
- if (isNew) {
73
- node = namespace === undefined ? window.document.createElement(data.tag) : window.document.createElementNS(namespace, data.tag)
74
- cached = {
75
- tag: data.tag,
76
- attrs: setAttributes(node, data.attrs, {}, namespace),
77
- children: build(node, data.children, cached.children, true, index, namespace),
78
- nodes: [node]
79
- }
80
- parent.insertBefore(node, index !== undefined ? parent.childNodes[index] : null)
81
- }
82
- else {
83
- node = cached.nodes[0]
84
- setAttributes(node, data.attrs, cached.attrs, namespace)
85
- cached.children = build(node, data.children, cached.children, false, index, namespace)
86
- cached.nodes.intact = true
87
- if (shouldReattach === true) parent.insertBefore(node, index !== undefined ? parent.childNodes[index] : null)
88
- }
89
- if (type.call(data.attrs["config"]) == "[object Function]") data.attrs["config"](node, !isNew)
90
- }
91
- else {
92
- var node
93
- if (cached.nodes.length === 0) {
94
- if (data.$trusted) {
95
- var lastChild = parent.lastChild
96
- parent.insertAdjacentHTML("beforeend", data)
97
- node = lastChild ? lastChild.nextSibling : parent.firstChild
98
- }
99
- else {
100
- node = window.document.createTextNode(data)
101
- parent.insertBefore(node, index !== undefined ? parent.childNodes[index] : null)
102
- }
103
- cached = "string number boolean".indexOf(typeof data) > -1 ? new data.constructor(data) : data
104
- cached.nodes = [node]
105
- }
106
- else if (cached.valueOf() !== data.valueOf() || shouldReattach === true) {
107
- if (data.$trusted) {
108
- var current = cached.nodes[0], nodes = [current]
109
- if (current) {
110
- while (current = current.nextSibling) nodes.push(current)
111
- clear(nodes)
112
- var lastChild = parent.lastChild
113
- parent.insertAdjacentHTML("beforeend", data)
114
- node = lastChild ? lastChild.nextSibling : parent.firstChild
115
- }
116
- else parent.innerHTML = data
117
- }
118
- else {
119
- node = cached.nodes[0]
120
- parent.insertBefore(node, index !== undefined ? parent.childNodes[index] : null)
121
- node.nodeValue = data
122
- }
123
- cached = new data.constructor(data)
124
- cached.nodes = [node]
125
- }
126
- else cached.nodes.intact = true
127
- }
128
-
129
- return cached
130
- }
131
- function setAttributes(node, dataAttrs, cachedAttrs, namespace) {
132
- for (var attrName in dataAttrs) {
133
- var dataAttr = dataAttrs[attrName]
134
- var cachedAttr = cachedAttrs[attrName]
135
- if (!(attrName in cachedAttrs) || (cachedAttr !== dataAttr) || node === window.document.activeElement) {
136
- cachedAttrs[attrName] = dataAttr
137
- if (attrName === "config") continue
138
- else if (typeof dataAttr == "function" && attrName.indexOf("on") == 0) {
139
- node[attrName] = autoredraw(dataAttr, node)
140
- }
141
- else if (attrName === "style") {
142
- for (var rule in dataAttr) {
143
- if (cachedAttr === undefined || cachedAttr[rule] !== dataAttr[rule]) node.style[rule] = dataAttr[rule]
144
- }
145
- }
146
- else if (namespace !== undefined) {
147
- if (attrName === "href") node.setAttributeNS("http://www.w3.org/1999/xlink", "href", dataAttr)
148
- else if (attrName === "className") node.setAttribute("class", dataAttr)
149
- else node.setAttribute(attrName, dataAttr)
150
- }
151
- else if (attrName in node) node[attrName] = dataAttr
152
- else node.setAttribute(attrName, dataAttr)
153
- }
154
- }
155
- return cachedAttrs
156
- }
157
- function clear(nodes) {
158
- for (var i = 0; i < nodes.length; i++) nodes[i].parentNode.removeChild(nodes[i])
159
- nodes.length = 0
160
- }
161
- function clone(object) {
162
- var result = {}
163
- for (var prop in object) result[prop] = object[prop]
164
- return result
165
- }
166
- function autoredraw(callback, object) {
167
- return function(e) {
168
- m.startComputation()
169
- var output = callback.call(object, e)
170
- m.endComputation()
171
- return output
172
- }
173
- }
174
-
175
- var html
176
- var documentNode = {
177
- insertAdjacentHTML: function(_, data) {
178
- window.document.write(data)
179
- window.document.close()
180
- },
181
- appendChild: function(node) {
182
- if (html === undefined) html = window.document.createElement("html")
183
- if (node.nodeName == "HTML") html = node
184
- else html.appendChild(node)
185
- if (window.document.documentElement !== html) {
186
- window.document.replaceChild(html, window.document.documentElement)
187
- }
188
- },
189
- insertBefore: function(node, reference) {
190
- this.appendChild(node)
191
- },
192
- childNodes: []
193
- }
194
- var nodeCache = [], cellCache = {}
195
- m.render = function(root, cell) {
196
- var index = nodeCache.indexOf(root)
197
- var id = index < 0 ? nodeCache.push(root) - 1 : index
198
- var node = root == window.document || root == window.document.documentElement ? documentNode : root
199
- cellCache[id] = build(node, cell, cellCache[id], false, 0)
200
- }
201
-
202
- m.trust = function(value) {
203
- value = new String(value)
204
- value.$trusted = true
205
- return value
206
- }
207
-
208
- var currentRoot, currentModule = {view: function() {}}, currentController = {}, now = 0, lastRedraw = 0, lastRedrawId = 0
209
- m.module = function(root, module) {
210
- m.startComputation()
211
- currentRoot = root
212
- currentModule = module
213
- currentController = new module.controller
214
- m.endComputation()
215
- }
216
- m.redraw = function() {
217
- m.render(currentRoot, currentModule.view(currentController))
218
- lastRedraw = now
219
- }
220
- function redraw() {
221
- now = window.performance && window.performance.now ? window.performance.now() : new window.Date().getTime()
222
- if (now - lastRedraw > 16) m.redraw()
223
- else {
224
- var cancel = window.cancelAnimationFrame || window.clearTimeout
225
- var defer = window.requestAnimationFrame || window.setTimeout
226
- cancel(lastRedrawId)
227
- lastRedrawId = defer(m.redraw, 0)
228
- }
229
- }
230
-
231
- var pendingRequests = 0, computePostRedrawHook = null
232
- m.startComputation = function() {pendingRequests++}
233
- m.endComputation = function() {
234
- pendingRequests = Math.max(pendingRequests - 1, 0)
235
- if (pendingRequests == 0) {
236
- redraw()
237
- if (computePostRedrawHook) {
238
- computePostRedrawHook()
239
- computePostRedrawHook = null
240
- }
241
- }
242
- }
243
-
244
- m.withAttr = function(prop, withAttrCallback) {
245
- return function(e) {withAttrCallback(prop in e.currentTarget ? e.currentTarget[prop] : e.currentTarget.getAttribute(prop))}
246
- }
247
-
248
- //routing
249
- var modes = {pathname: "", hash: "#", search: "?"}
250
- var redirect = function() {}, routeParams = {}
251
- m.route = function() {
252
- if (arguments.length == 3) {
253
- var root = arguments[0], defaultRoute = arguments[1], router = arguments[2]
254
- redirect = function(source) {
255
- var path = source.slice(modes[m.route.mode].length)
256
- if (!routeByValue(root, router, path)) {
257
- m.route(defaultRoute, true)
258
- }
259
- }
260
- var listener = m.route.mode == "hash" ? "onhashchange" : "onpopstate"
261
- window[listener] = function() {
262
- redirect(window.location[m.route.mode])
263
- }
264
- computePostRedrawHook = scrollToHash
265
- window[listener]()
266
- }
267
- else if (arguments[0].addEventListener) {
268
- var element = arguments[0]
269
- var isInitialized = arguments[1]
270
- if (!isInitialized) {
271
- element.removeEventListener("click", routeUnobtrusive)
272
- element.addEventListener("click", routeUnobtrusive)
273
- }
274
- }
275
- else if (typeof arguments[0] == "string") {
276
- var route = arguments[0]
277
- var shouldReplaceHistoryEntry = arguments[1] === true
278
- if (window.history.pushState) {
279
- computePostRedrawHook = function() {
280
- window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, window.document.title, modes[m.route.mode] + route)
281
- scrollToHash()
282
- }
283
- redirect(modes[m.route.mode] + route)
284
- }
285
- else window.location[m.route.mode] = route
286
- }
287
- }
288
- m.route.param = function(key) {return routeParams[key]}
289
- m.route.mode = "search"
290
- function routeByValue(root, router, path) {
291
- routeParams = {}
292
- for (var route in router) {
293
- if (route == path) return !void m.module(root, router[route])
294
-
295
- var matcher = new RegExp("^" + route.replace(/:[^\/]+/g, "([^\\/]+)") + "$")
296
- if (matcher.test(path)) {
297
- return !void path.replace(matcher, function() {
298
- var keys = route.match(/:[^\/]+/g)
299
- var values = [].slice.call(arguments, 1, -2)
300
- for (var i = 0; i < keys.length; i++) routeParams[keys[i].slice(1)] = values[i]
301
- m.module(root, router[route])
302
- })
303
- }
304
- }
305
- }
306
- function routeUnobtrusive(e) {
307
- e.preventDefault()
308
- m.route(e.currentTarget.getAttribute("href"))
309
- }
310
- function scrollToHash() {
311
- if (m.route.mode != "hash" && window.location.hash) window.location.hash = window.location.hash
312
- }
313
-
314
- //model
315
- m.prop = function(store) {
316
- return function() {
317
- if (arguments.length) store = arguments[0]
318
- return store
319
- }
320
- }
321
-
322
- m.deferred = function() {
323
- var resolvers = [], rejecters = []
324
- var object = {
325
- resolve: function(value) {
326
- for (var i = 0; i < resolvers.length; i++) resolvers[i](value)
327
- },
328
- reject: function(value) {
329
- for (var i = 0; i < rejecters.length; i++) rejecters[i](value)
330
- },
331
- promise: m.prop()
332
- }
333
- object.promise.resolvers = resolvers
334
- object.promise.then = function(success, error) {
335
- var next = m.deferred()
336
- if (!success) success = identity
337
- if (!error) error = identity
338
- function push(list, method, callback) {
339
- list.push(function(value) {
340
- try {
341
- var result = callback(value)
342
- if (result && typeof result.then == "function") result.then(next[method], error)
343
- else next[method](result !== undefined ? result : value)
344
- }
345
- catch (e) {
346
- if (e instanceof Error && e.constructor !== Error) throw e
347
- else next.reject(e)
348
- }
349
- })
350
- }
351
- push(resolvers, "resolve", success)
352
- push(rejecters, "reject", error)
353
- return next.promise
354
- }
355
- return object
356
- }
357
- m.sync = function(args) {
358
- var method = "resolve"
359
- function synchronizer(resolved) {
360
- return function(value) {
361
- results.push(value)
362
- if (!resolved) method = "reject"
363
- if (results.length == args.length) {
364
- deferred.promise(results)
365
- deferred[method](results)
366
- }
367
- return value
368
- }
369
- }
370
-
371
- var deferred = m.deferred()
372
- var results = []
373
- for (var i = 0; i < args.length; i++) {
374
- args[i].then(synchronizer(true), synchronizer(false))
375
- }
376
- return deferred.promise
377
- }
378
- function identity(value) {return value}
379
-
380
- function ajax(options) {
381
- var xhr = window.XDomainRequest ? new window.XDomainRequest : new window.XMLHttpRequest
382
- xhr.open(options.method, options.url, true, options.user, options.password)
383
- xhr.onload = typeof options.onload == "function" ? options.onload : function() {}
384
- xhr.onerror = typeof options.onerror == "function" ? options.onerror : function() {}
385
- if (typeof options.config == "function") options.config(xhr, options)
386
- xhr.send(options.data)
387
- return xhr
388
- }
389
- function querystring(object, prefix) {
390
- var str = []
391
- for(var prop in object) {
392
- var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop]
393
- str.push(typeof value == "object" ? querystring(value, key) : encodeURIComponent(key) + "=" + encodeURIComponent(value))
394
- }
395
- return str.join("&")
396
- }
397
- function bindData(xhrOptions, data, serialize) {
398
- if (data && Object.keys(data).length > 0) {
399
- if (xhrOptions.method == "GET") {
400
- xhrOptions.url = xhrOptions.url + (xhrOptions.url.indexOf("?") < 0 ? "?" : "&") + querystring(data)
401
- }
402
- else xhrOptions.data = serialize(data)
403
- }
404
- return xhrOptions
405
- }
406
- function parameterizeUrl(url, data) {
407
- var tokens = url.match(/:[a-z]\w+/gi)
408
- if (tokens && data) {
409
- for (var i = 0; i < tokens.length; i++) {
410
- var key = tokens[i].slice(1)
411
- url = url.replace(tokens[i], data[key])
412
- delete data[key]
413
- }
414
- }
415
- return url
416
- }
417
-
418
- m.request = function(xhrOptions) {
419
- m.startComputation()
420
- var deferred = m.deferred()
421
- var serialize = xhrOptions.serialize || JSON.stringify
422
- var deserialize = xhrOptions.deserialize || JSON.parse
423
- xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data)
424
- xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize)
425
- xhrOptions.onload = xhrOptions.onerror = function(e) {
426
- var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity
427
- var response = unwrap(deserialize(e.target.responseText))
428
- if (response instanceof Array && xhrOptions.type) {
429
- for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i])
430
- }
431
- else if (xhrOptions.type) response = new xhrOptions.type(response)
432
- deferred.promise(response)
433
- deferred[e.type == "load" ? "resolve" : "reject"](response)
434
- m.endComputation()
435
- }
436
- ajax(xhrOptions)
437
- deferred.promise.then = propBinder(deferred.promise)
438
- return deferred.promise
439
- }
440
- function propBinder(promise) {
441
- var bind = promise.then
442
- return function(success, error) {
443
- var next = bind(function(value) {return next(success(value))}, function(value) {return next(error(value))})
444
- next.then = propBinder(next)
445
- return next
446
- }
447
- }
448
-
449
- if (typeof module != "undefined" && module !== null) module.exports = m
450
- if (typeof define == "function" && define.amd) define(function() {return m})
451
-
452
- //testing API
453
- m.deps = function(mock) {return window = mock}
454
- }(this)
1
+ var m = (function app(window, undefined) {
2
+ var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function";
3
+ var type = {}.toString;
4
+ var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/;
5
+ var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/;
6
+
7
+ // caching commonly used variables
8
+ var $document, $location, $requestAnimationFrame, $cancelAnimationFrame;
9
+
10
+ // self invoking function needed because of the way mocks work
11
+ function initialize(window){
12
+ $document = window.document;
13
+ $location = window.location;
14
+ $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout;
15
+ $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout;
16
+ }
17
+
18
+ initialize(window);
19
+
20
+
21
+ /*
22
+ * @typedef {String} Tag
23
+ * A string that looks like -> div.classname#id[param=one][param2=two]
24
+ * Which describes a DOM node
25
+ */
26
+
27
+ /*
28
+ *
29
+ * @param {Tag} The DOM node tag
30
+ * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs
31
+ * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional)
32
+ *
33
+ */
34
+ function m() {
35
+ var args = [].slice.call(arguments);
36
+ var hasAttrs = args[1] != null && type.call(args[1]) == OBJECT && !("tag" in args[1]) && !("subtree" in args[1]);
37
+ var attrs = hasAttrs ? args[1] : {};
38
+ var classAttrName = "class" in attrs ? "class" : "className";
39
+ var cell = {tag: "div", attrs: {}};
40
+ var match, classes = [];
41
+ if (type.call(args[0]) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string")
42
+ while (match = parser.exec(args[0])) {
43
+ if (match[1] == "" && match[2]) cell.tag = match[2];
44
+ else if (match[1] == "#") cell.attrs.id = match[2];
45
+ else if (match[1] == ".") classes.push(match[2]);
46
+ else if (match[3][0] == "[") {
47
+ var pair = attrParser.exec(match[3]);
48
+ cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true)
49
+ }
50
+ }
51
+ if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ");
52
+
53
+
54
+ var children = hasAttrs ? args[2] : args[1];
55
+ if (type.call(children) == ARRAY) {
56
+ cell.children = children
57
+ }
58
+ else {
59
+ cell.children = hasAttrs ? args.slice(2) : args.slice(1)
60
+ }
61
+
62
+ for (var attrName in attrs) {
63
+ if (attrName == classAttrName) cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName];
64
+ else cell.attrs[attrName] = attrs[attrName]
65
+ }
66
+ return cell
67
+ }
68
+ function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) {
69
+ //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached`
70
+ //the diff algorithm can be summarized as this:
71
+ //1 - compare `data` and `cached`
72
+ //2 - if they are different, copy `data` to `cached` and update the DOM based on what the difference is
73
+ //3 - recursively apply this algorithm for every array and for the children of every virtual element
74
+
75
+ //the `cached` data structure is essentially the same as the previous redraw's `data` data structure, with a few additions:
76
+ //- `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
77
+ //- 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("")`
78
+ //- `cached also has a `configContext` property, which is the state storage object exposed by config(element, isInitialized, context)
79
+ //- 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
80
+
81
+ //`parentElement` is a DOM element used for W3C DOM API calls
82
+ //`parentTag` is only used for handling a corner case for textarea values
83
+ //`parentCache` is used to remove nodes in some multi-node cases
84
+ //`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
85
+ //`data` and `cached` are, respectively, the new and old nodes being diffed
86
+ //`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)
87
+ //`editable` is a flag that indicates whether an ancestor is contenteditable
88
+ //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor
89
+ //`configs` is a list of config functions to run after the topmost `build` call finishes running
90
+
91
+ //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings
92
+ //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")}
93
+ //- it simplifies diffing code
94
+ if (data == null) data = "";
95
+ if (data.subtree === "retain") return cached;
96
+ var cachedType = type.call(cached), dataType = type.call(data);
97
+ if (cached == null || cachedType != dataType) {
98
+ if (cached != null) {
99
+ if (parentCache && parentCache.nodes) {
100
+ var offset = index - parentIndex;
101
+ var end = offset + (dataType == ARRAY ? data : cached.nodes).length;
102
+ clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end))
103
+ }
104
+ else if (cached.nodes) clear(cached.nodes, cached)
105
+ }
106
+ cached = new data.constructor;
107
+ if (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277)
108
+ cached.nodes = []
109
+ }
110
+
111
+ if (dataType == ARRAY) {
112
+ //recursively flatten array
113
+ for (var i = 0; i < data.length; i++) {
114
+ if (type.call(data[i]) == ARRAY) {
115
+ data = data.concat.apply([], data);
116
+ i-- //check current index again and flatten until there are no more nested arrays at that index
117
+ }
118
+ }
119
+
120
+ var nodes = [], intact = cached.length === data.length, subArrayCount = 0;
121
+
122
+ //keys algorithm: sort elements without recreating them if keys are present
123
+ //1) create a map of all existing keys, and mark all for deletion
124
+ //2) add new keys to map and mark them for addition
125
+ //3) if key exists in new list, change action from deletion to a move
126
+ //4) for each key, handle its corresponding action as marked in previous steps
127
+ //5) copy unkeyed items into their respective gaps
128
+ var DELETION = 1, INSERTION = 2 , MOVE = 3;
129
+ var existing = {}, unkeyed = [], shouldMaintainIdentities = false;
130
+ for (var i = 0; i < cached.length; i++) {
131
+ if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) {
132
+ shouldMaintainIdentities = true;
133
+ existing[cached[i].attrs.key] = {action: DELETION, index: i}
134
+ }
135
+ }
136
+ if (shouldMaintainIdentities) {
137
+ for (var i = 0; i < data.length; i++) {
138
+ if (data[i] && data[i].attrs) {
139
+ if (data[i].attrs.key != null) {
140
+ var key = data[i].attrs.key;
141
+ if (!existing[key]) existing[key] = {action: INSERTION, index: i};
142
+ else existing[key] = {
143
+ action: MOVE,
144
+ index: i,
145
+ from: existing[key].index,
146
+ element: parentElement.childNodes[existing[key].index] || $document.createElement("div")
147
+ }
148
+ }
149
+ else unkeyed.push({index: i, element: parentElement.childNodes[i] || $document.createElement("div")})
150
+ }
151
+ }
152
+ var actions = Object.keys(existing).map(function(key) {return existing[key]});
153
+ var changes = actions.sort(function(a, b) {return a.action - b.action || a.index - b.index});
154
+ var newCached = cached.slice();
155
+
156
+ for (var i = 0, change; change = changes[i]; i++) {
157
+ if (change.action == DELETION) {
158
+ clear(cached[change.index].nodes, cached[change.index]);
159
+ newCached.splice(change.index, 1)
160
+ }
161
+ if (change.action == INSERTION) {
162
+ var dummy = $document.createElement("div");
163
+ dummy.key = data[change.index].attrs.key;
164
+ parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null);
165
+ newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]})
166
+ }
167
+
168
+ if (change.action == MOVE) {
169
+ if (parentElement.childNodes[change.index] !== change.element && change.element !== null) {
170
+ parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null)
171
+ }
172
+ newCached[change.index] = cached[change.from]
173
+ }
174
+ }
175
+ for (var i = 0; i < unkeyed.length; i++) {
176
+ var change = unkeyed[i];
177
+ parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null);
178
+ newCached[change.index] = cached[change.index]
179
+ }
180
+ cached = newCached;
181
+ cached.nodes = [];
182
+ for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes.push(child)
183
+ }
184
+ //end key algorithm
185
+
186
+ for (var i = 0, cacheCount = 0; i < data.length; i++) {
187
+ //diff each item in the array
188
+ var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs);
189
+ if (item === undefined) continue;
190
+ if (!item.nodes.intact) intact = false;
191
+ if (item.$trusted) {
192
+ //fix offset of next element if item was a trusted string w/ more than one html element
193
+ //the first clause in the regexp matches elements
194
+ //the second clause (after the pipe) matches text nodes
195
+ subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || []).length
196
+ }
197
+ else subArrayCount += type.call(item) == ARRAY ? item.length : 1;
198
+ cached[cacheCount++] = item
199
+ }
200
+ if (!intact) {
201
+ //diff the array itself
202
+
203
+ //update the list of DOM nodes by collecting the nodes from each item
204
+ for (var i = 0; i < data.length; i++) {
205
+ if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes)
206
+ }
207
+ //remove items from the end of the array if the new array is shorter than the old one
208
+ //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
209
+ for (var i = 0, node; node = cached.nodes[i]; i++) {
210
+ if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]])
211
+ }
212
+ if (data.length < cached.length) cached.length = data.length;
213
+ cached.nodes = nodes
214
+ }
215
+ }
216
+ else if (data != null && dataType == OBJECT) {
217
+ if (!data.attrs) data.attrs = {};
218
+ if (!cached.attrs) cached.attrs = {};
219
+
220
+ var dataAttrKeys = Object.keys(data.attrs);
221
+ var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0)
222
+ //if an element is different enough from the one in cache, recreate it
223
+ if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) {
224
+ if (cached.nodes.length) clear(cached.nodes);
225
+ if (cached.configContext && typeof cached.configContext.onunload == FUNCTION) cached.configContext.onunload()
226
+ }
227
+ if (type.call(data.tag) != STRING) return;
228
+
229
+ var node, isNew = cached.nodes.length === 0;
230
+ if (data.attrs.xmlns) namespace = data.attrs.xmlns;
231
+ else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg";
232
+ else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML";
233
+ if (isNew) {
234
+ if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is);
235
+ else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag);
236
+ cached = {
237
+ tag: data.tag,
238
+ //set attributes first, then create children
239
+ attrs: dataAttrKeys.length ? setAttributes(node, data.tag, data.attrs, {}, namespace) : {},
240
+ children: data.children != null && data.children.length > 0 ?
241
+ build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) :
242
+ data.children,
243
+ nodes: [node]
244
+ };
245
+ if (cached.children && !cached.children.nodes) cached.children.nodes = [];
246
+ //edge case: setting value on <select> doesn't work before children exist, so set it again after children have been created
247
+ if (data.tag == "select" && data.attrs.value) setAttributes(node, data.tag, {value: data.attrs.value}, {}, namespace);
248
+ parentElement.insertBefore(node, parentElement.childNodes[index] || null)
249
+ }
250
+ else {
251
+ node = cached.nodes[0];
252
+ if (dataAttrKeys.length) setAttributes(node, data.tag, data.attrs, cached.attrs, namespace);
253
+ cached.children = build(node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs);
254
+ cached.nodes.intact = true;
255
+ if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
256
+ }
257
+ //schedule configs to be called. They are called after `build` finishes running
258
+ if (typeof data.attrs["config"] == FUNCTION) {
259
+ var context = cached.configContext = cached.configContext || {};
260
+
261
+ // bind
262
+ var callback = function(data, args) {
263
+ return function() {
264
+ return data.attrs["config"].apply(data, args)
265
+ }
266
+ };
267
+ configs.push(callback(data, [node, !isNew, context, cached]))
268
+ }
269
+ }
270
+ else if (typeof dataType != FUNCTION) {
271
+ //handle text nodes
272
+ var nodes;
273
+ if (cached.nodes.length === 0) {
274
+ if (data.$trusted) {
275
+ nodes = injectHTML(parentElement, index, data)
276
+ }
277
+ else {
278
+ nodes = [$document.createTextNode(data)];
279
+ if (!parentElement.nodeName.match(voidElements)) parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null)
280
+ }
281
+ cached = "string number boolean".indexOf(typeof data) > -1 ? new data.constructor(data) : data;
282
+ cached.nodes = nodes
283
+ }
284
+ else if (cached.valueOf() !== data.valueOf() || shouldReattach === true) {
285
+ nodes = cached.nodes;
286
+ if (!editable || editable !== $document.activeElement) {
287
+ if (data.$trusted) {
288
+ clear(nodes, cached);
289
+ nodes = injectHTML(parentElement, index, data)
290
+ }
291
+ else {
292
+ //corner case: replacing the nodeValue of a text node that is a child of a textarea/contenteditable doesn't work
293
+ //we need to update the value property of the parent textarea or the innerHTML of the contenteditable element instead
294
+ if (parentTag === "textarea") parentElement.value = data;
295
+ else if (editable) editable.innerHTML = data;
296
+ else {
297
+ if (nodes[0].nodeType == 1 || nodes.length > 1) { //was a trusted string
298
+ clear(cached.nodes, cached);
299
+ nodes = [$document.createTextNode(data)]
300
+ }
301
+ parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null);
302
+ nodes[0].nodeValue = data
303
+ }
304
+ }
305
+ }
306
+ cached = new data.constructor(data);
307
+ cached.nodes = nodes
308
+ }
309
+ else cached.nodes.intact = true
310
+ }
311
+
312
+ return cached
313
+ }
314
+ function setAttributes(node, tag, dataAttrs, cachedAttrs, namespace) {
315
+ for (var attrName in dataAttrs) {
316
+ var dataAttr = dataAttrs[attrName];
317
+ var cachedAttr = cachedAttrs[attrName];
318
+ if (!(attrName in cachedAttrs) || (cachedAttr !== dataAttr)) {
319
+ cachedAttrs[attrName] = dataAttr;
320
+ try {
321
+ //`config` isn't a real attributes, so ignore it
322
+ //we don't ignore `key` because it must be unique and having it on the DOM helps debugging
323
+ if (attrName === "config") continue;
324
+ //hook event handlers to the auto-redrawing system
325
+ else if (typeof dataAttr == FUNCTION && attrName.indexOf("on") == 0) {
326
+ node[attrName] = autoredraw(dataAttr, node)
327
+ }
328
+ //handle `style: {...}`
329
+ else if (attrName === "style" && dataAttr != null && type.call(dataAttr) == OBJECT) {
330
+ for (var rule in dataAttr) {
331
+ if (cachedAttr == null || cachedAttr[rule] !== dataAttr[rule]) node.style[rule] = dataAttr[rule]
332
+ }
333
+ for (var rule in cachedAttr) {
334
+ if (!(rule in dataAttr)) node.style[rule] = ""
335
+ }
336
+ }
337
+ //handle SVG
338
+ else if (namespace != null) {
339
+ if (attrName === "href") node.setAttributeNS("http://www.w3.org/1999/xlink", "href", dataAttr);
340
+ else if (attrName === "className") node.setAttribute("class", dataAttr);
341
+ else node.setAttribute(attrName, dataAttr)
342
+ }
343
+ //handle cases that are properties (but ignore cases where we should use setAttribute instead)
344
+ //- list and form are typically used as strings, but are DOM element references in js
345
+ //- when using CSS selectors (e.g. `m("[style='']")`), style is used as a string, but it's an object in js
346
+ else if (attrName in node && !(attrName == "list" || attrName == "style" || attrName == "form" || attrName == "type")) {
347
+ //#348 don't set the value if not needed otherwise cursor placement breaks in Chrome
348
+ if (node[attrName] != dataAttr) node[attrName] = dataAttr
349
+ }
350
+ else node.setAttribute(attrName, dataAttr)
351
+ }
352
+ catch (e) {
353
+ //swallow IE's invalid argument errors to mimic HTML's fallback-to-doing-nothing-on-invalid-attributes behavior
354
+ if (e.message.indexOf("Invalid argument") < 0) throw e
355
+ }
356
+ }
357
+ //#348 dataAttr may not be a string, so use loose comparison (double equal) instead of strict (triple equal)
358
+ else if (attrName === "value" && tag === "input" && node.value != dataAttr) {
359
+ node.value = dataAttr
360
+ }
361
+ }
362
+ return cachedAttrs
363
+ }
364
+ function clear(nodes, cached) {
365
+ for (var i = nodes.length - 1; i > -1; i--) {
366
+ if (nodes[i] && nodes[i].parentNode) {
367
+ try {nodes[i].parentNode.removeChild(nodes[i])}
368
+ catch (e) {} //ignore if this fails due to order of events (see http://stackoverflow.com/questions/21926083/failed-to-execute-removechild-on-node)
369
+ cached = [].concat(cached);
370
+ if (cached[i]) unload(cached[i])
371
+ }
372
+ }
373
+ if (nodes.length != 0) nodes.length = 0
374
+ }
375
+ function unload(cached) {
376
+ if (cached.configContext && typeof cached.configContext.onunload == FUNCTION) cached.configContext.onunload();
377
+ if (cached.children) {
378
+ if (type.call(cached.children) == ARRAY) {
379
+ for (var i = 0; i < cached.children.length; i++) unload(cached.children[i])
380
+ }
381
+ else if (cached.children.tag) unload(cached.children)
382
+ }
383
+ }
384
+ function injectHTML(parentElement, index, data) {
385
+ var nextSibling = parentElement.childNodes[index];
386
+ if (nextSibling) {
387
+ var isElement = nextSibling.nodeType != 1;
388
+ var placeholder = $document.createElement("span");
389
+ if (isElement) {
390
+ parentElement.insertBefore(placeholder, nextSibling || null);
391
+ placeholder.insertAdjacentHTML("beforebegin", data);
392
+ parentElement.removeChild(placeholder)
393
+ }
394
+ else nextSibling.insertAdjacentHTML("beforebegin", data)
395
+ }
396
+ else parentElement.insertAdjacentHTML("beforeend", data);
397
+ var nodes = [];
398
+ while (parentElement.childNodes[index] !== nextSibling) {
399
+ nodes.push(parentElement.childNodes[index]);
400
+ index++
401
+ }
402
+ return nodes
403
+ }
404
+ function autoredraw(callback, object) {
405
+ return function(e) {
406
+ e = e || event;
407
+ m.redraw.strategy("diff");
408
+ m.startComputation();
409
+ try {return callback.call(object, e)}
410
+ finally {
411
+ m.endComputation()
412
+ }
413
+ }
414
+ }
415
+
416
+ var html;
417
+ var documentNode = {
418
+ appendChild: function(node) {
419
+ if (html === undefined) html = $document.createElement("html");
420
+ if ($document.documentElement && $document.documentElement !== node) {
421
+ $document.replaceChild(node, $document.documentElement)
422
+ }
423
+ else $document.appendChild(node);
424
+ this.childNodes = $document.childNodes
425
+ },
426
+ insertBefore: function(node) {
427
+ this.appendChild(node)
428
+ },
429
+ childNodes: []
430
+ };
431
+ var nodeCache = [], cellCache = {};
432
+ m.render = function(root, cell, forceRecreation) {
433
+ var configs = [];
434
+ if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.");
435
+ var id = getCellCacheKey(root);
436
+ var isDocumentRoot = root == $document;
437
+ var node = isDocumentRoot || root == $document.documentElement ? documentNode : root;
438
+ if (isDocumentRoot && cell.tag != "html") cell = {tag: "html", attrs: {}, children: cell};
439
+ if (cellCache[id] === undefined) clear(node.childNodes);
440
+ if (forceRecreation === true) reset(root);
441
+ cellCache[id] = build(node, null, undefined, undefined, cell, cellCache[id], false, 0, null, undefined, configs);
442
+ for (var i = 0; i < configs.length; i++) configs[i]()
443
+ };
444
+ function getCellCacheKey(element) {
445
+ var index = nodeCache.indexOf(element);
446
+ return index < 0 ? nodeCache.push(element) - 1 : index
447
+ }
448
+
449
+ m.trust = function(value) {
450
+ value = new String(value);
451
+ value.$trusted = true;
452
+ return value
453
+ };
454
+
455
+ function gettersetter(store) {
456
+ var prop = function() {
457
+ if (arguments.length) store = arguments[0];
458
+ return store
459
+ };
460
+
461
+ prop.toJSON = function() {
462
+ return store
463
+ };
464
+
465
+ return prop
466
+ }
467
+
468
+ m.prop = function (store) {
469
+ //note: using non-strict equality check here because we're checking if store is null OR undefined
470
+ if (((store != null && type.call(store) == OBJECT) || typeof store == FUNCTION) && typeof store.then == FUNCTION) {
471
+ return propify(store)
472
+ }
473
+
474
+ return gettersetter(store)
475
+ };
476
+
477
+ var roots = [], modules = [], controllers = [], lastRedrawId = null, lastRedrawCallTime = 0, computePostRedrawHook = null, prevented = false, topModule;
478
+ var FRAME_BUDGET = 16; //60 frames per second = 1 call per 16 ms
479
+ m.module = function(root, module) {
480
+ var index = roots.indexOf(root);
481
+ if (index < 0) index = roots.length;
482
+ var isPrevented = false;
483
+ if (controllers[index] && typeof controllers[index].onunload == FUNCTION) {
484
+ var event = {
485
+ preventDefault: function() {isPrevented = true}
486
+ };
487
+ controllers[index].onunload(event)
488
+ }
489
+ if (!isPrevented) {
490
+ m.redraw.strategy("all");
491
+ m.startComputation();
492
+ roots[index] = root;
493
+ var currentModule = topModule = module;
494
+ var controller = new module.controller;
495
+ //controllers may call m.module recursively (via m.route redirects, for example)
496
+ //this conditional ensures only the last recursive m.module call is applied
497
+ if (currentModule == topModule) {
498
+ controllers[index] = controller;
499
+ modules[index] = module
500
+ }
501
+ m.endComputation();
502
+ return controllers[index]
503
+ }
504
+ };
505
+ m.redraw = function(force) {
506
+ //lastRedrawId is a positive number if a second redraw is requested before the next animation frame
507
+ //lastRedrawID is null if it's the first redraw and not an event handler
508
+ if (lastRedrawId && force !== true) {
509
+ //when setTimeout: only reschedule redraw if time between now and previous redraw is bigger than a frame, otherwise keep currently scheduled timeout
510
+ //when rAF: always reschedule redraw
511
+ if (new Date - lastRedrawCallTime > FRAME_BUDGET || $requestAnimationFrame == window.requestAnimationFrame) {
512
+ if (lastRedrawId > 0) $cancelAnimationFrame(lastRedrawId);
513
+ lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET)
514
+ }
515
+ }
516
+ else {
517
+ redraw();
518
+ lastRedrawId = $requestAnimationFrame(function() {lastRedrawId = null}, FRAME_BUDGET)
519
+ }
520
+ };
521
+ m.redraw.strategy = m.prop();
522
+ function redraw() {
523
+ var mode = m.redraw.strategy();
524
+ for (var i = 0; i < roots.length; i++) {
525
+ if (controllers[i] && mode != "none") {
526
+ m.render(roots[i], modules[i].view(controllers[i]), mode == "all")
527
+ }
528
+ }
529
+ //after rendering within a routed context, we need to scroll back to the top, and fetch the document title for history.pushState
530
+ if (computePostRedrawHook) {
531
+ computePostRedrawHook();
532
+ computePostRedrawHook = null
533
+ }
534
+ lastRedrawId = null;
535
+ lastRedrawCallTime = new Date;
536
+ m.redraw.strategy("diff")
537
+ }
538
+
539
+ var pendingRequests = 0;
540
+ m.startComputation = function() {pendingRequests++};
541
+ m.endComputation = function() {
542
+ pendingRequests = Math.max(pendingRequests - 1, 0);
543
+ if (pendingRequests == 0) m.redraw()
544
+ };
545
+
546
+ m.withAttr = function(prop, withAttrCallback) {
547
+ return function(e) {
548
+ e = e || event;
549
+ var currentTarget = e.currentTarget || this;
550
+ withAttrCallback(prop in currentTarget ? currentTarget[prop] : currentTarget.getAttribute(prop))
551
+ }
552
+ };
553
+
554
+ //routing
555
+ var modes = {pathname: "", hash: "#", search: "?"};
556
+ var redirect = function() {}, routeParams, currentRoute;
557
+ m.route = function() {
558
+ //m.route()
559
+ if (arguments.length === 0) return currentRoute;
560
+ //m.route(el, defaultRoute, routes)
561
+ else if (arguments.length === 3 && type.call(arguments[1]) == STRING) {
562
+ var root = arguments[0], defaultRoute = arguments[1], router = arguments[2];
563
+ redirect = function(source) {
564
+ var path = currentRoute = normalizeRoute(source);
565
+ if (!routeByValue(root, router, path)) {
566
+ m.route(defaultRoute, true)
567
+ }
568
+ };
569
+ var listener = m.route.mode == "hash" ? "onhashchange" : "onpopstate";
570
+ window[listener] = function() {
571
+ if (currentRoute != normalizeRoute($location[m.route.mode])) {
572
+ redirect($location[m.route.mode])
573
+ }
574
+ };
575
+ computePostRedrawHook = setScroll;
576
+ window[listener]()
577
+ }
578
+ //config: m.route
579
+ else if (arguments[0].addEventListener) {
580
+ var element = arguments[0];
581
+ var isInitialized = arguments[1];
582
+ var context = arguments[2];
583
+ element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + this.attrs.href;
584
+ element.removeEventListener("click", routeUnobtrusive);
585
+ element.addEventListener("click", routeUnobtrusive)
586
+ }
587
+ //m.route(route, params)
588
+ else if (type.call(arguments[0]) == STRING) {
589
+ currentRoute = arguments[0];
590
+ var args = arguments[1] || {}
591
+ var queryIndex = currentRoute.indexOf("?")
592
+ var params = queryIndex > -1 ? parseQueryString(currentRoute.slice(queryIndex + 1)) : {}
593
+ for (var i in args) params[i] = args[i]
594
+ var querystring = buildQueryString(params)
595
+ var currentPath = queryIndex > -1 ? currentRoute.slice(0, queryIndex) : currentRoute
596
+ if (querystring) currentRoute = currentPath + (currentPath.indexOf("?") === -1 ? "?" : "&") + querystring;
597
+
598
+ var shouldReplaceHistoryEntry = (arguments.length == 3 ? arguments[2] : arguments[1]) === true;
599
+
600
+ if (window.history.pushState) {
601
+ computePostRedrawHook = function() {
602
+ window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, $document.title, modes[m.route.mode] + currentRoute);
603
+ setScroll()
604
+ };
605
+ redirect(modes[m.route.mode] + currentRoute)
606
+ }
607
+ else $location[m.route.mode] = currentRoute
608
+ }
609
+ };
610
+ m.route.param = function(key) {
611
+ if (!routeParams) throw new Error("You must call m.route(element, defaultRoute, routes) before calling m.route.param()")
612
+ return routeParams[key]
613
+ };
614
+ m.route.mode = "search";
615
+ function normalizeRoute(route) {return route.slice(modes[m.route.mode].length)}
616
+ function routeByValue(root, router, path) {
617
+ routeParams = {};
618
+
619
+ var queryStart = path.indexOf("?");
620
+ if (queryStart !== -1) {
621
+ routeParams = parseQueryString(path.substr(queryStart + 1, path.length));
622
+ path = path.substr(0, queryStart)
623
+ }
624
+
625
+ for (var route in router) {
626
+ if (route == path) {
627
+ m.module(root, router[route]);
628
+ return true
629
+ }
630
+
631
+ var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$");
632
+
633
+ if (matcher.test(path)) {
634
+ path.replace(matcher, function() {
635
+ var keys = route.match(/:[^\/]+/g) || [];
636
+ var values = [].slice.call(arguments, 1, -2);
637
+ for (var i = 0; i < keys.length; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i])
638
+ m.module(root, router[route])
639
+ });
640
+ return true
641
+ }
642
+ }
643
+ }
644
+ function routeUnobtrusive(e) {
645
+ e = e || event;
646
+ if (e.ctrlKey || e.metaKey || e.which == 2) return;
647
+ if (e.preventDefault) e.preventDefault();
648
+ else e.returnValue = false;
649
+ var currentTarget = e.currentTarget || this;
650
+ var args = m.route.mode == "pathname" && currentTarget.search ? parseQueryString(currentTarget.search.slice(1)) : {};
651
+ m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args)
652
+ }
653
+ function setScroll() {
654
+ if (m.route.mode != "hash" && $location.hash) $location.hash = $location.hash;
655
+ else window.scrollTo(0, 0)
656
+ }
657
+ function buildQueryString(object, prefix) {
658
+ var str = [];
659
+ for(var prop in object) {
660
+ var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop];
661
+ str.push(value != null && type.call(value) == OBJECT ? buildQueryString(value, key) : encodeURIComponent(key) + "=" + encodeURIComponent(value))
662
+ }
663
+ return str.join("&")
664
+ }
665
+ function parseQueryString(str) {
666
+ var pairs = str.split("&"), params = {};
667
+ for (var i = 0; i < pairs.length; i++) {
668
+ var pair = pairs[i].split("=");
669
+ params[decodeSpace(pair[0])] = pair[1] ? decodeSpace(pair[1]) : ""
670
+ }
671
+ return params
672
+ }
673
+ function decodeSpace(string) {
674
+ return decodeURIComponent(string.replace(/\+/g, " "))
675
+ }
676
+ function reset(root) {
677
+ var cacheKey = getCellCacheKey(root);
678
+ clear(root.childNodes, cellCache[cacheKey]);
679
+ cellCache[cacheKey] = undefined
680
+ }
681
+
682
+ m.deferred = function () {
683
+ var deferred = new Deferred();
684
+ deferred.promise = propify(deferred.promise);
685
+ return deferred
686
+ };
687
+ function propify(promise) {
688
+ var prop = m.prop();
689
+ promise.then(prop);
690
+ prop.then = function(resolve, reject) {
691
+ return propify(promise.then(resolve, reject))
692
+ };
693
+ return prop
694
+ }
695
+ //Promiz.mithril.js | Zolmeister | MIT
696
+ //a modified version of Promiz.js, which does not conform to Promises/A+ for two reasons:
697
+ //1) `then` callbacks are called synchronously (because setTimeout is too slow, and the setImmediate polyfill is too big
698
+ //2) throwing subclasses of Error cause the error to be bubbled up instead of triggering rejection (because the spec does not account for the important use case of default browser error handling, i.e. message w/ line number)
699
+ function Deferred(successCallback, failureCallback) {
700
+ var RESOLVING = 1, REJECTING = 2, RESOLVED = 3, REJECTED = 4;
701
+ var self = this, state = 0, promiseValue = 0, next = [];
702
+
703
+ self["promise"] = {};
704
+
705
+ self["resolve"] = function(value) {
706
+ if (!state) {
707
+ promiseValue = value;
708
+ state = RESOLVING;
709
+
710
+ fire()
711
+ }
712
+ return this
713
+ };
714
+
715
+ self["reject"] = function(value) {
716
+ if (!state) {
717
+ promiseValue = value;
718
+ state = REJECTING;
719
+
720
+ fire()
721
+ }
722
+ return this
723
+ };
724
+
725
+ self.promise["then"] = function(successCallback, failureCallback) {
726
+ var deferred = new Deferred(successCallback, failureCallback);
727
+ if (state == RESOLVED) {
728
+ deferred.resolve(promiseValue)
729
+ }
730
+ else if (state == REJECTED) {
731
+ deferred.reject(promiseValue)
732
+ }
733
+ else {
734
+ next.push(deferred)
735
+ }
736
+ return deferred.promise
737
+ };
738
+
739
+ function finish(type) {
740
+ state = type || REJECTED;
741
+ next.map(function(deferred) {
742
+ state == RESOLVED && deferred.resolve(promiseValue) || deferred.reject(promiseValue)
743
+ })
744
+ }
745
+
746
+ function thennable(then, successCallback, failureCallback, notThennableCallback) {
747
+ if (((promiseValue != null && type.call(promiseValue) == OBJECT) || typeof promiseValue == FUNCTION) && typeof then == FUNCTION) {
748
+ try {
749
+ // count protects against abuse calls from spec checker
750
+ var count = 0;
751
+ then.call(promiseValue, function(value) {
752
+ if (count++) return;
753
+ promiseValue = value;
754
+ successCallback()
755
+ }, function (value) {
756
+ if (count++) return;
757
+ promiseValue = value;
758
+ failureCallback()
759
+ })
760
+ }
761
+ catch (e) {
762
+ m.deferred.onerror(e);
763
+ promiseValue = e;
764
+ failureCallback()
765
+ }
766
+ } else {
767
+ notThennableCallback()
768
+ }
769
+ }
770
+
771
+ function fire() {
772
+ // check if it's a thenable
773
+ var then;
774
+ try {
775
+ then = promiseValue && promiseValue.then
776
+ }
777
+ catch (e) {
778
+ m.deferred.onerror(e);
779
+ promiseValue = e;
780
+ state = REJECTING;
781
+ return fire()
782
+ }
783
+ thennable(then, function() {
784
+ state = RESOLVING;
785
+ fire()
786
+ }, function() {
787
+ state = REJECTING;
788
+ fire()
789
+ }, function() {
790
+ try {
791
+ if (state == RESOLVING && typeof successCallback == FUNCTION) {
792
+ promiseValue = successCallback(promiseValue)
793
+ }
794
+ else if (state == REJECTING && typeof failureCallback == "function") {
795
+ promiseValue = failureCallback(promiseValue);
796
+ state = RESOLVING
797
+ }
798
+ }
799
+ catch (e) {
800
+ m.deferred.onerror(e);
801
+ promiseValue = e;
802
+ return finish()
803
+ }
804
+
805
+ if (promiseValue == self) {
806
+ promiseValue = TypeError();
807
+ finish()
808
+ }
809
+ else {
810
+ thennable(then, function () {
811
+ finish(RESOLVED)
812
+ }, finish, function () {
813
+ finish(state == RESOLVING && RESOLVED)
814
+ })
815
+ }
816
+ })
817
+ }
818
+ }
819
+ m.deferred.onerror = function(e) {
820
+ if (type.call(e) == "[object Error]" && !e.constructor.toString().match(/ Error/)) throw e
821
+ };
822
+
823
+ m.sync = function(args) {
824
+ var method = "resolve";
825
+ function synchronizer(pos, resolved) {
826
+ return function(value) {
827
+ results[pos] = value;
828
+ if (!resolved) method = "reject";
829
+ if (--outstanding == 0) {
830
+ deferred.promise(results);
831
+ deferred[method](results)
832
+ }
833
+ return value
834
+ }
835
+ }
836
+
837
+ var deferred = m.deferred();
838
+ var outstanding = args.length;
839
+ var results = new Array(outstanding);
840
+ if (args.length > 0) {
841
+ for (var i = 0; i < args.length; i++) {
842
+ args[i].then(synchronizer(i, true), synchronizer(i, false))
843
+ }
844
+ }
845
+ else deferred.resolve([]);
846
+
847
+ return deferred.promise
848
+ };
849
+ function identity(value) {return value}
850
+
851
+ function ajax(options) {
852
+ if (options.dataType && options.dataType.toLowerCase() === "jsonp") {
853
+ var callbackKey = "mithril_callback_" + new Date().getTime() + "_" + (Math.round(Math.random() * 1e16)).toString(36);
854
+ var script = $document.createElement("script");
855
+
856
+ window[callbackKey] = function(resp) {
857
+ $document.body.removeChild(script);
858
+ options.onload({
859
+ type: "load",
860
+ target: {
861
+ responseText: resp
862
+ }
863
+ });
864
+ window[callbackKey] = undefined
865
+ };
866
+
867
+ script.onerror = function(e) {
868
+ $document.body.removeChild(script);
869
+
870
+ options.onerror({
871
+ type: "error",
872
+ target: {
873
+ status: 500,
874
+ responseText: JSON.stringify({error: "Error making jsonp request"})
875
+ }
876
+ });
877
+ window[callbackKey] = undefined;
878
+
879
+ return false
880
+ };
881
+
882
+ script.onload = function(e) {
883
+ return false
884
+ };
885
+
886
+ script.src = options.url
887
+ + (options.url.indexOf("?") > 0 ? "&" : "?")
888
+ + (options.callbackKey ? options.callbackKey : "callback")
889
+ + "=" + callbackKey
890
+ + "&" + buildQueryString(options.data || {});
891
+ $document.body.appendChild(script)
892
+ }
893
+ else {
894
+ var xhr = new window.XMLHttpRequest;
895
+ xhr.open(options.method, options.url, true, options.user, options.password);
896
+ xhr.onreadystatechange = function() {
897
+ if (xhr.readyState === 4) {
898
+ if (xhr.status >= 200 && xhr.status < 300) options.onload({type: "load", target: xhr});
899
+ else options.onerror({type: "error", target: xhr})
900
+ }
901
+ };
902
+ if (options.serialize == JSON.stringify && options.data && options.method != "GET") {
903
+ xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8")
904
+ }
905
+ if (options.deserialize == JSON.parse) {
906
+ xhr.setRequestHeader("Accept", "application/json, text/*");
907
+ }
908
+ if (typeof options.config == FUNCTION) {
909
+ var maybeXhr = options.config(xhr, options);
910
+ if (maybeXhr != null) xhr = maybeXhr
911
+ }
912
+
913
+ var data = options.method == "GET" || !options.data ? "" : options.data
914
+ if (data && (type.call(data) != STRING && data.constructor != window.FormData)) {
915
+ throw "Request data should be either be a string or FormData. Check the `serialize` option in `m.request`";
916
+ }
917
+ xhr.send(data);
918
+ return xhr
919
+ }
920
+ }
921
+ function bindData(xhrOptions, data, serialize) {
922
+ if (xhrOptions.method == "GET" && xhrOptions.dataType != "jsonp") {
923
+ var prefix = xhrOptions.url.indexOf("?") < 0 ? "?" : "&";
924
+ var querystring = buildQueryString(data);
925
+ xhrOptions.url = xhrOptions.url + (querystring ? prefix + querystring : "")
926
+ }
927
+ else xhrOptions.data = serialize(data);
928
+ return xhrOptions
929
+ }
930
+ function parameterizeUrl(url, data) {
931
+ var tokens = url.match(/:[a-z]\w+/gi);
932
+ if (tokens && data) {
933
+ for (var i = 0; i < tokens.length; i++) {
934
+ var key = tokens[i].slice(1);
935
+ url = url.replace(tokens[i], data[key]);
936
+ delete data[key]
937
+ }
938
+ }
939
+ return url
940
+ }
941
+
942
+ m.request = function(xhrOptions) {
943
+ if (xhrOptions.background !== true) m.startComputation();
944
+ var deferred = m.deferred();
945
+ var isJSONP = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp";
946
+ var serialize = xhrOptions.serialize = isJSONP ? identity : xhrOptions.serialize || JSON.stringify;
947
+ var deserialize = xhrOptions.deserialize = isJSONP ? identity : xhrOptions.deserialize || JSON.parse;
948
+ var extract = xhrOptions.extract || function(xhr) {
949
+ return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText
950
+ };
951
+ xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data);
952
+ xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize);
953
+ xhrOptions.onload = xhrOptions.onerror = function(e) {
954
+ try {
955
+ e = e || event;
956
+ var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity;
957
+ var response = unwrap(deserialize(extract(e.target, xhrOptions)));
958
+ if (e.type == "load") {
959
+ if (type.call(response) == ARRAY && xhrOptions.type) {
960
+ for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i])
961
+ }
962
+ else if (xhrOptions.type) response = new xhrOptions.type(response)
963
+ }
964
+ deferred[e.type == "load" ? "resolve" : "reject"](response)
965
+ }
966
+ catch (e) {
967
+ m.deferred.onerror(e);
968
+ deferred.reject(e)
969
+ }
970
+ if (xhrOptions.background !== true) m.endComputation()
971
+ };
972
+ ajax(xhrOptions);
973
+ deferred.promise(xhrOptions.initialValue);
974
+ return deferred.promise
975
+ };
976
+
977
+ //testing API
978
+ m.deps = function(mock) {
979
+ initialize(window = mock || window);
980
+ return window;
981
+ };
982
+ //for internal testing only, do not use `m.deps.factory`
983
+ m.deps.factory = app;
984
+
985
+ return m
986
+ })(typeof window != "undefined" ? window : {});
987
+
988
+ if (typeof module != "undefined" && module !== null && module.exports) module.exports = m;
989
+ else if (typeof define == "function" && define.amd) define(function() {return m});