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