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)$/;
7 // caching commonly used variables
8 var $document
, $location
, $requestAnimationFrame
, $cancelAnimationFrame
;
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
;
22 * @typedef {String} Tag
23 * A string that looks like -> div.classname#id[param=one][param2=two]
24 * Which describes a DOM node
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)
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)
51 if (classes
.length
> 0) cell
.attrs
[classAttrName
] = classes
.join(" ");
54 var children
= hasAttrs
? args
[2] : args
[1];
55 if (type
.call(children
) === ARRAY
) {
56 cell
.children
= children
59 cell
.children
= hasAttrs
? args
.slice(2) : args
.slice(1)
62 for (var attrName
in attrs
) {
63 if (attrName
=== classAttrName
) {
64 if (attrs
[attrName
] !== "") cell
.attrs
[attrName
] = (cell
.attrs
[attrName
] || "") + " " + attrs
[attrName
];
66 else cell
.attrs
[attrName
] = attrs
[attrName
]
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
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
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
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
))
107 else if (cached
.nodes
) clear(cached
.nodes
, cached
)
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)
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
123 var nodes
= [], intact
= cached
.length
=== data
.length
, subArrayCount
= 0;
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
}
139 if (shouldMaintainIdentities
) {
140 if (data
.indexOf(null) > -1) data
= data
.filter(function(x
) {return x
!= null})
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
) {
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
] = {
160 from: existing
[key
].index
,
161 element
: cached
.nodes
[existing
[key
].index
] || $document
.createElement("div")
164 else unkeyed
.push({index
: i
, element
: parentElement
.childNodes
[i
] || $document
.createElement("div")})
168 for (var prop
in existing
) actions
.push(existing
[prop
])
169 var changes
= actions
.sort(sortChanges
);
170 var newCached
= new Array(cached
.length
)
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)
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
]})
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)
188 newCached
[change
.index
] = cached
[change
.from]
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
]
197 cached
.nodes
= new Array(parentElement
.childNodes
.length
);
198 for (var i
= 0, child
; child
= parentElement
.childNodes
[i
]; i
++) cached
.nodes
[i
] = child
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;
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
214 else subArrayCount
+= type
.call(item
) === ARRAY
? item
.length
: 1;
215 cached
[cacheCount
++] = item
218 //diff the array itself
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
)
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
]])
229 if (data
.length
< cached
.length
) cached
.length
= data
.length
;
233 else if (data
!= null && dataType
=== OBJECT
) {
234 if (!data
.attrs
) data
.attrs
= {};
235 if (!cached
.attrs
) cached
.attrs
= {};
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()
244 if (type
.call(data
.tag
) != STRING
) return;
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";
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
);
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
) :
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)
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)
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
|| {};
279 var callback = function(data
, args
) {
281 return data
.attrs
["config"].apply(data
, args
)
284 configs
.push(callback(data
, [node
, !isNew
, context
, cached
]))
287 else if (typeof dataType
!= FUNCTION
) {
290 if (cached
.nodes
.length
=== 0) {
292 nodes
= injectHTML(parentElement
, index
, data
)
295 nodes
= [$document
.createTextNode(data
)];
296 if (!parentElement
.nodeName
.match(voidElements
)) parentElement
.insertBefore(nodes
[0], parentElement
.childNodes
[index
] || null)
298 cached
= "string number boolean".indexOf(typeof data
) > -1 ? new data
.constructor(data
) : data
;
301 else if (cached
.valueOf() !== data
.valueOf() || shouldReattach
=== true) {
302 nodes
= cached
.nodes
;
303 if (!editable
|| editable
!== $document
.activeElement
) {
305 clear(nodes
, cached
);
306 nodes
= injectHTML(parentElement
, index
, data
)
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
;
314 if (nodes
[0].nodeType
=== 1 || nodes
.length
> 1) { //was a trusted string
315 clear(cached
.nodes
, cached
);
316 nodes
= [$document
.createTextNode(data
)]
318 parentElement
.insertBefore(nodes
[0], parentElement
.childNodes
[index
] || null);
319 nodes
[0].nodeValue
= data
323 cached
= new data
.constructor(data
);
326 else cached
.nodes
.intact
= true
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
;
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
)
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
]
350 for (var rule
in cachedAttr
) {
351 if (!(rule
in dataAttr
)) node
.style
[rule
] = ""
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
)
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
367 else node
.setAttribute(attrName
, dataAttr
)
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
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
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
])
390 if (nodes
.length
!= 0) nodes
.length
= 0
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
)
398 else if (cached
.children
.tag
) unload(cached
.children
)
401 function injectHTML(parentElement
, index
, data
) {
402 var nextSibling
= parentElement
.childNodes
[index
];
404 var isElement
= nextSibling
.nodeType
!= 1;
405 var placeholder
= $document
.createElement("span");
407 parentElement
.insertBefore(placeholder
, nextSibling
|| null);
408 placeholder
.insertAdjacentHTML("beforebegin", data
);
409 parentElement
.removeChild(placeholder
)
411 else nextSibling
.insertAdjacentHTML("beforebegin", data
)
413 else parentElement
.insertAdjacentHTML("beforeend", data
);
415 while (parentElement
.childNodes
[index
] !== nextSibling
) {
416 nodes
.push(parentElement
.childNodes
[index
]);
421 function autoredraw(callback
, object
) {
424 m
.redraw
.strategy("diff");
425 m
.startComputation();
426 try {return callback
.call(object
, e
)}
428 endFirstComputation()
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
)
440 else $document
.appendChild(node
);
441 this.childNodes
= $document
.childNodes
443 insertBefore: function(node
) {
444 this.appendChild(node
)
448 var nodeCache
= [], cellCache
= {};
449 m
.render = function(root
, cell
, forceRecreation
) {
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
]()
461 function getCellCacheKey(element
) {
462 var index
= nodeCache
.indexOf(element
);
463 return index
< 0 ? nodeCache
.push(element
) - 1 : index
466 m
.trust = function(value
) {
467 value
= new String(value
);
468 value
.$trusted
= true;
472 function gettersetter(store
) {
473 var prop = function() {
474 if (arguments
.length
) store
= arguments
[0];
478 prop
.toJSON = function() {
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
)
491 return gettersetter(store
)
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
) {
503 preventDefault: function() {isPrevented
= true}
505 controllers
[index
].onunload(event
)
508 m
.redraw
.strategy("all");
509 m
.startComputation();
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
519 endFirstComputation();
520 return controllers
[index
]
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
)
536 lastRedrawId
= $requestAnimationFrame(function() {lastRedrawId
= null}, FRAME_BUDGET
)
539 m
.redraw
.strategy
= m
.prop();
540 var blank = function() {return ""}
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
)
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
554 lastRedrawCallTime
= new Date
;
555 m
.redraw
.strategy("diff")
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()
564 var endFirstComputation = function() {
565 if (m
.redraw
.strategy() == "none") {
567 m
.redraw
.strategy("diff")
569 else m
.endComputation();
572 m
.withAttr = function(prop
, withAttrCallback
) {
575 var currentTarget
= e
.currentTarget
|| this;
576 withAttrCallback(prop
in currentTarget
? currentTarget
[prop
] : currentTarget
.getAttribute(prop
))
581 var modes
= {pathname
: "", hash
: "#", search
: "?"};
582 var redirect = function() {}, routeParams
, currentRoute
;
583 m
.route = function() {
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)
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
)) {
603 computePostRedrawHook
= setScroll
;
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
)
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
;
627 var shouldReplaceHistoryEntry
= (arguments
.length
=== 3 ? arguments
[2] : arguments
[1]) === true || oldRoute
=== arguments
[0];
629 if (window
.history
.pushState
) {
630 computePostRedrawHook = function() {
631 window
.history
[shouldReplaceHistoryEntry
? "replaceState" : "pushState"](null, $document
.title
, modes
[m
.route
.mode
] + currentRoute
);
634 redirect(modes
[m
.route
.mode
] + currentRoute
)
636 else $location
[m
.route
.mode
] = currentRoute
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
]
643 m
.route
.mode
= "search";
644 function normalizeRoute(route
) {
645 return route
.slice(modes
[m
.route
.mode
].length
)
647 function routeByValue(root
, router
, path
) {
650 var queryStart
= path
.indexOf("?");
651 if (queryStart
!== -1) {
652 routeParams
= parseQueryString(path
.substr(queryStart
+ 1, path
.length
));
653 path
= path
.substr(0, queryStart
)
656 for (var route
in router
) {
657 if (route
=== path
) {
658 m
.module(root
, router
[route
]);
662 var matcher
= new RegExp("^" + route
.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$");
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
])
675 function routeUnobtrusive(e
) {
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
)
684 function setScroll() {
685 if (m
.route
.mode
!= "hash" && $location
.hash
) $location
.hash
= $location
.hash
;
686 else window
.scrollTo(0, 0)
688 function buildQueryString(object
, prefix
) {
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
)
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]) : ""
711 function reset(root
) {
712 var cacheKey
= getCellCacheKey(root
);
713 clear(root
.childNodes
, cellCache
[cacheKey
]);
714 cellCache
[cacheKey
] = undefined
717 m
.deferred = function () {
718 var deferred
= new Deferred();
719 deferred
.promise
= propify(deferred
.promise
);
722 function propify(promise
) {
725 prop
.then = function(resolve
, reject
) {
726 return propify(promise
.then(resolve
, reject
))
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
= [];
738 self
["promise"] = {};
740 self
["resolve"] = function(value
) {
742 promiseValue
= value
;
750 self
["reject"] = function(value
) {
752 promiseValue
= value
;
760 self
.promise
["then"] = function(successCallback
, failureCallback
) {
761 var deferred
= new Deferred(successCallback
, failureCallback
);
762 if (state
=== RESOLVED
) {
763 deferred
.resolve(promiseValue
)
765 else if (state
=== REJECTED
) {
766 deferred
.reject(promiseValue
)
771 return deferred
.promise
774 function finish(type
) {
775 state
= type
|| REJECTED
;
776 next
.map(function(deferred
) {
777 state
=== RESOLVED
&& deferred
.resolve(promiseValue
) || deferred
.reject(promiseValue
)
781 function thennable(then
, successCallback
, failureCallback
, notThennableCallback
) {
782 if (((promiseValue
!= null && type
.call(promiseValue
) === OBJECT
) || typeof promiseValue
=== FUNCTION
) && typeof then
=== FUNCTION
) {
784 // count protects against abuse calls from spec checker
786 then
.call(promiseValue
, function(value
) {
788 promiseValue
= value
;
790 }, function (value
) {
792 promiseValue
= value
;
797 m
.deferred
.onerror(e
);
802 notThennableCallback()
807 // check if it's a thenable
810 then
= promiseValue
&& promiseValue
.then
813 m
.deferred
.onerror(e
);
818 thennable(then
, function() {
826 if (state
=== RESOLVING
&& typeof successCallback
=== FUNCTION
) {
827 promiseValue
= successCallback(promiseValue
)
829 else if (state
=== REJECTING
&& typeof failureCallback
=== "function") {
830 promiseValue
= failureCallback(promiseValue
);
835 m
.deferred
.onerror(e
);
840 if (promiseValue
=== self
) {
841 promiseValue
= TypeError();
845 thennable(then
, function () {
847 }, finish
, function () {
848 finish(state
=== RESOLVING
&& RESOLVED
)
854 m
.deferred
.onerror = function(e
) {
855 if (type
.call(e
) === "[object Error]" && !e
.constructor.toString().match(/ Error
/)) throw e
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
)
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))
880 else deferred
.resolve([]);
882 return deferred
.promise
884 function identity(value
) {return value
}
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");
891 window
[callbackKey
] = function(resp
) {
892 script
.parentNode
.removeChild(script
);
899 window
[callbackKey
] = undefined
902 script
.onerror = function(e
) {
903 script
.parentNode
.removeChild(script
);
909 responseText
: JSON
.stringify({error
: "Error making jsonp request"})
912 window
[callbackKey
] = undefined;
917 script
.onload = function(e
) {
921 script
.src
= options
.url
922 + (options
.url
.indexOf("?") > 0 ? "&" : "?")
923 + (options
.callbackKey
? options
.callbackKey
: "callback")
925 + "&" + buildQueryString(options
.data
|| {});
926 $document
.body
.appendChild(script
)
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
})
937 if (options
.serialize
=== JSON
.stringify
&& options
.data
&& options
.method
!== "GET") {
938 xhr
.setRequestHeader("Content-Type", "application/json; charset=utf-8")
940 if (options
.deserialize
=== JSON
.parse
) {
941 xhr
.setRequestHeader("Accept", "application/json, text/*");
943 if (typeof options
.config
=== FUNCTION
) {
944 var maybeXhr
= options
.config(xhr
, options
);
945 if (maybeXhr
!= null) xhr
= maybeXhr
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`";
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
: "")
962 else xhrOptions
.data
= serialize(data
);
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
]);
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
986 xhrOptions
.url
= parameterizeUrl(xhrOptions
.url
, xhrOptions
.data
);
987 xhrOptions
= bindData(xhrOptions
, xhrOptions
.data
, serialize
);
988 xhrOptions
.onload
= xhrOptions
.onerror = function(e
) {
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
])
997 else if (xhrOptions
.type
) response
= new xhrOptions
.type(response
)
999 deferred
[e
.type
=== "load" ? "resolve" : "reject"](response
)
1002 m
.deferred
.onerror(e
);
1005 if (xhrOptions
.background
!== true) m
.endComputation()
1008 deferred
.promise(xhrOptions
.initialValue
);
1009 return deferred
.promise
1013 m
.deps = function(mock
) {
1014 initialize(window
= mock
|| window
);
1017 //for internal testing only, do not use `m.deps.factory`
1018 m
.deps
.factory
= app
;
1021 })(typeof window
!= "undefined" ? window
: {});
1023 if (typeof module
!= "undefined" && module
!== null && module
.exports
) module
.exports
= m
;
1024 else if (typeof define
=== "function" && define
.amd
) define(function() {return m
});