mithril_rails 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,7 @@ var m = (function app(window, undefined) {
3
3
  var type = {}.toString;
4
4
  var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/;
5
5
  var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/;
6
+ var noop = function() {}
6
7
 
7
8
  // caching commonly used variables
8
9
  var $document, $location, $requestAnimationFrame, $cancelAnimationFrame;
@@ -18,13 +19,13 @@ var m = (function app(window, undefined) {
18
19
  initialize(window);
19
20
 
20
21
 
21
- /*
22
+ /**
22
23
  * @typedef {String} Tag
23
24
  * A string that looks like -> div.classname#id[param=one][param2=two]
24
25
  * Which describes a DOM node
25
26
  */
26
27
 
27
- /*
28
+ /**
28
29
  *
29
30
  * @param {Tag} The DOM node tag
30
31
  * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs
@@ -33,7 +34,7 @@ var m = (function app(window, undefined) {
33
34
  */
34
35
  function m() {
35
36
  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 hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1] || "view" in args[1]) && !("subtree" in args[1]);
37
38
  var attrs = hasAttrs ? args[1] : {};
38
39
  var classAttrName = "class" in attrs ? "class" : "className";
39
40
  var cell = {tag: "div", attrs: {}};
@@ -48,21 +49,26 @@ var m = (function app(window, undefined) {
48
49
  cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true)
49
50
  }
50
51
  }
51
- if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ");
52
-
53
52
 
54
- var children = hasAttrs ? args[2] : args[1];
55
- if (type.call(children) === ARRAY) {
56
- cell.children = children
53
+ var children = hasAttrs ? args.slice(2) : args.slice(1);
54
+ if (children.length === 1 && type.call(children[0]) === ARRAY) {
55
+ cell.children = children[0]
57
56
  }
58
57
  else {
59
- cell.children = hasAttrs ? args.slice(2) : args.slice(1)
58
+ cell.children = children
60
59
  }
61
-
60
+
62
61
  for (var attrName in attrs) {
63
- if (attrName === classAttrName) cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName];
64
- else cell.attrs[attrName] = attrs[attrName]
62
+ if (attrs.hasOwnProperty(attrName)) {
63
+ if (attrName === classAttrName && attrs[attrName] != null && attrs[attrName] !== "") {
64
+ classes.push(attrs[attrName])
65
+ cell.attrs[attrName] = "" //create key in correct iteration order
66
+ }
67
+ else cell.attrs[attrName] = attrs[attrName]
68
+ }
65
69
  }
70
+ if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ");
71
+
66
72
  return cell
67
73
  }
68
74
  function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) {
@@ -91,8 +97,8 @@ var m = (function app(window, undefined) {
91
97
  //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings
92
98
  //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")}
93
99
  //- it simplifies diffing code
94
- //data.toString() is null if data is the return value of Console.log in Firefox
95
- if (data == null || data.toString() == null) data = "";
100
+ //data.toString() might throw or return null if data is the return value of Console.log in Firefox (behavior depends on version)
101
+ try {if (data == null || data.toString() == null) data = "";} catch (e) {data = ""}
96
102
  if (data.subtree === "retain") return cached;
97
103
  var cachedType = type.call(cached), dataType = type.call(data);
98
104
  if (cached == null || cachedType !== dataType) {
@@ -115,6 +121,7 @@ var m = (function app(window, undefined) {
115
121
  if (type.call(data[i]) === ARRAY) {
116
122
  data = data.concat.apply([], data);
117
123
  i-- //check current index again and flatten until there are no more nested arrays at that index
124
+ len = data.length
118
125
  }
119
126
  }
120
127
 
@@ -125,18 +132,26 @@ var m = (function app(window, undefined) {
125
132
  //2) add new keys to map and mark them for addition
126
133
  //3) if key exists in new list, change action from deletion to a move
127
134
  //4) for each key, handle its corresponding action as marked in previous steps
128
- //5) copy unkeyed items into their respective gaps
129
135
  var DELETION = 1, INSERTION = 2 , MOVE = 3;
130
- var existing = {}, unkeyed = [], shouldMaintainIdentities = false;
136
+ var existing = {}, shouldMaintainIdentities = false;
131
137
  for (var i = 0; i < cached.length; i++) {
132
138
  if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) {
133
139
  shouldMaintainIdentities = true;
134
140
  existing[cached[i].attrs.key] = {action: DELETION, index: i}
135
141
  }
136
142
  }
143
+
144
+ var guid = 0
145
+ for (var i = 0, len = data.length; i < len; i++) {
146
+ if (data[i] && data[i].attrs && data[i].attrs.key != null) {
147
+ for (var j = 0, len = data.length; j < len; j++) {
148
+ if (data[j] && data[j].attrs && data[j].attrs.key == null) data[j].attrs.key = "__mithril__" + guid++
149
+ }
150
+ break
151
+ }
152
+ }
153
+
137
154
  if (shouldMaintainIdentities) {
138
- if (data.indexOf(null) > -1) data = data.filter(function(x) {return x != null})
139
-
140
155
  var keysDiffer = false
141
156
  if (data.length != cached.length) keysDiffer = true
142
157
  else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) {
@@ -156,16 +171,16 @@ var m = (function app(window, undefined) {
156
171
  action: MOVE,
157
172
  index: i,
158
173
  from: existing[key].index,
159
- element: parentElement.childNodes[existing[key].index] || $document.createElement("div")
174
+ element: cached.nodes[existing[key].index] || $document.createElement("div")
160
175
  }
161
176
  }
162
- else unkeyed.push({index: i, element: parentElement.childNodes[i] || $document.createElement("div")})
163
177
  }
164
178
  }
165
179
  var actions = []
166
180
  for (var prop in existing) actions.push(existing[prop])
167
181
  var changes = actions.sort(sortChanges);
168
182
  var newCached = new Array(cached.length)
183
+ newCached.nodes = cached.nodes.slice()
169
184
 
170
185
  for (var i = 0, change; change = changes[i]; i++) {
171
186
  if (change.action === DELETION) {
@@ -177,6 +192,7 @@ var m = (function app(window, undefined) {
177
192
  dummy.key = data[change.index].attrs.key;
178
193
  parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null);
179
194
  newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]})
195
+ newCached.nodes[change.index] = dummy
180
196
  }
181
197
 
182
198
  if (change.action === MOVE) {
@@ -184,16 +200,10 @@ var m = (function app(window, undefined) {
184
200
  parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null)
185
201
  }
186
202
  newCached[change.index] = cached[change.from]
203
+ newCached.nodes[change.index] = change.element
187
204
  }
188
205
  }
189
- for (var i = 0, len = unkeyed.length; i < len; i++) {
190
- var change = unkeyed[i];
191
- parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null);
192
- newCached[change.index] = cached[change.index]
193
- }
194
206
  cached = newCached;
195
- cached.nodes = new Array(parentElement.childNodes.length);
196
- for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes[i] = child
197
207
  }
198
208
  }
199
209
  //end key algorithm
@@ -207,7 +217,7 @@ var m = (function app(window, undefined) {
207
217
  //fix offset of next element if item was a trusted string w/ more than one html element
208
218
  //the first clause in the regexp matches elements
209
219
  //the second clause (after the pipe) matches text nodes
210
- subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || []).length
220
+ subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length
211
221
  }
212
222
  else subArrayCount += type.call(item) === ARRAY ? item.length : 1;
213
223
  cached[cacheCount++] = item
@@ -229,15 +239,37 @@ var m = (function app(window, undefined) {
229
239
  }
230
240
  }
231
241
  else if (data != null && dataType === OBJECT) {
242
+ var views = [], controllers = []
243
+ while (data.view) {
244
+ var view = data.view.$original || data.view
245
+ var controllerIndex = m.redraw.strategy() == "diff" && cached.views ? cached.views.indexOf(view) : -1
246
+ var controller = controllerIndex > -1 ? cached.controllers[controllerIndex] : new (data.controller || noop)
247
+ var key = data && data.attrs && data.attrs.key
248
+ data = pendingRequests == 0 || (cached && cached.controllers && cached.controllers.indexOf(controller) > -1) ? data.view(controller) : {tag: "placeholder"}
249
+ if (data.subtree === "retain") return cached;
250
+ if (key) {
251
+ if (!data.attrs) data.attrs = {}
252
+ data.attrs.key = key
253
+ }
254
+ if (controller.onunload) unloaders.push({controller: controller, handler: controller.onunload})
255
+ views.push(view)
256
+ controllers.push(controller)
257
+ }
258
+ if (!data.tag && controllers.length) throw new Error("Component template must return a virtual element, not an array, string, etc.")
232
259
  if (!data.attrs) data.attrs = {};
233
260
  if (!cached.attrs) cached.attrs = {};
234
261
 
235
262
  var dataAttrKeys = Object.keys(data.attrs)
236
263
  var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0)
237
264
  //if an element is different enough from the one in cache, recreate it
238
- if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) {
265
+ if (data.tag != cached.tag || dataAttrKeys.sort().join() != Object.keys(cached.attrs).sort().join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key || (m.redraw.strategy() == "all" && (!cached.configContext || cached.configContext.retain !== true)) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) {
239
266
  if (cached.nodes.length) clear(cached.nodes);
240
267
  if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload()
268
+ if (cached.controllers) {
269
+ for (var i = 0, controller; controller = cached.controllers[i]; i++) {
270
+ if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop})
271
+ }
272
+ }
241
273
  }
242
274
  if (type.call(data.tag) != STRING) return;
243
275
 
@@ -245,6 +277,7 @@ var m = (function app(window, undefined) {
245
277
  if (data.attrs.xmlns) namespace = data.attrs.xmlns;
246
278
  else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg";
247
279
  else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML";
280
+
248
281
  if (isNew) {
249
282
  if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is);
250
283
  else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag);
@@ -257,9 +290,22 @@ var m = (function app(window, undefined) {
257
290
  data.children,
258
291
  nodes: [node]
259
292
  };
293
+ if (controllers.length) {
294
+ cached.views = views
295
+ cached.controllers = controllers
296
+ for (var i = 0, controller; controller = controllers[i]; i++) {
297
+ if (controller.onunload && controller.onunload.$old) controller.onunload = controller.onunload.$old
298
+ if (pendingRequests && controller.onunload) {
299
+ var onunload = controller.onunload
300
+ controller.onunload = noop
301
+ controller.onunload.$old = onunload
302
+ }
303
+ }
304
+ }
305
+
260
306
  if (cached.children && !cached.children.nodes) cached.children.nodes = [];
261
307
  //edge case: setting value on <select> doesn't work before children exist, so set it again after children have been created
262
- if (data.tag === "select" && data.attrs.value) setAttributes(node, data.tag, {value: data.attrs.value}, {}, namespace);
308
+ if (data.tag === "select" && "value" in data.attrs) setAttributes(node, data.tag, {value: data.attrs.value}, {}, namespace);
263
309
  parentElement.insertBefore(node, parentElement.childNodes[index] || null)
264
310
  }
265
311
  else {
@@ -267,6 +313,10 @@ var m = (function app(window, undefined) {
267
313
  if (hasKeys) setAttributes(node, data.tag, data.attrs, cached.attrs, namespace);
268
314
  cached.children = build(node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs);
269
315
  cached.nodes.intact = true;
316
+ if (controllers.length) {
317
+ cached.views = views
318
+ cached.controllers = controllers
319
+ }
270
320
  if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null)
271
321
  }
272
322
  //schedule configs to be called. They are called after `build` finishes running
@@ -282,7 +332,7 @@ var m = (function app(window, undefined) {
282
332
  configs.push(callback(data, [node, !isNew, context, cached]))
283
333
  }
284
334
  }
285
- else if (typeof dataType != FUNCTION) {
335
+ else if (typeof data != FUNCTION) {
286
336
  //handle text nodes
287
337
  var nodes;
288
338
  if (cached.nodes.length === 0) {
@@ -358,7 +408,7 @@ var m = (function app(window, undefined) {
358
408
  //handle cases that are properties (but ignore cases where we should use setAttribute instead)
359
409
  //- list and form are typically used as strings, but are DOM element references in js
360
410
  //- when using CSS selectors (e.g. `m("[style='']")`), style is used as a string, but it's an object in js
361
- else if (attrName in node && !(attrName === "list" || attrName === "style" || attrName === "form" || attrName === "type")) {
411
+ else if (attrName in node && !(attrName === "list" || attrName === "style" || attrName === "form" || attrName === "type" || attrName === "width" || attrName === "height")) {
362
412
  //#348 don't set the value if not needed otherwise cursor placement breaks in Chrome
363
413
  if (tag !== "input" || node[attrName] !== dataAttr) node[attrName] = dataAttr
364
414
  }
@@ -388,7 +438,15 @@ var m = (function app(window, undefined) {
388
438
  if (nodes.length != 0) nodes.length = 0
389
439
  }
390
440
  function unload(cached) {
391
- if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload();
441
+ if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) {
442
+ cached.configContext.onunload();
443
+ cached.configContext.onunload = null
444
+ }
445
+ if (cached.controllers) {
446
+ for (var i = 0, controller; controller = cached.controllers[i]; i++) {
447
+ if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop});
448
+ }
449
+ }
392
450
  if (cached.children) {
393
451
  if (type.call(cached.children) === ARRAY) {
394
452
  for (var i = 0, child; child = cached.children[i]; i++) unload(child)
@@ -446,7 +504,7 @@ var m = (function app(window, undefined) {
446
504
  var nodeCache = [], cellCache = {};
447
505
  m.render = function(root, cell, forceRecreation) {
448
506
  var configs = [];
449
- if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.");
507
+ if (!root) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.");
450
508
  var id = getCellCacheKey(root);
451
509
  var isDocumentRoot = root === $document;
452
510
  var node = isDocumentRoot || root === $document.documentElement ? documentNode : root;
@@ -489,42 +547,75 @@ var m = (function app(window, undefined) {
489
547
  return gettersetter(store)
490
548
  };
491
549
 
492
- var roots = [], modules = [], controllers = [], lastRedrawId = null, lastRedrawCallTime = 0, computePostRedrawHook = null, prevented = false, topModule;
550
+ var roots = [], components = [], controllers = [], lastRedrawId = null, lastRedrawCallTime = 0, computePreRedrawHook = null, computePostRedrawHook = null, prevented = false, topComponent, unloaders = [];
493
551
  var FRAME_BUDGET = 16; //60 frames per second = 1 call per 16 ms
494
- m.module = function(root, module) {
552
+ function parameterize(component, args) {
553
+ var controller = function() {
554
+ return (component.controller || noop).apply(this, args) || this
555
+ }
556
+ var view = function(ctrl) {
557
+ if (arguments.length > 1) args = args.concat([].slice.call(arguments, 1))
558
+ return component.view.apply(component, args ? [ctrl].concat(args) : [ctrl])
559
+ }
560
+ view.$original = component.view
561
+ var output = {controller: controller, view: view}
562
+ if (args[0] && args[0].key != null) output.attrs = {key: args[0].key}
563
+ return output
564
+ }
565
+ m.component = function(component) {
566
+ return parameterize(component, [].slice.call(arguments, 1))
567
+ }
568
+ m.mount = m.module = function(root, component) {
495
569
  if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.");
496
570
  var index = roots.indexOf(root);
497
571
  if (index < 0) index = roots.length;
572
+
498
573
  var isPrevented = false;
574
+ var event = {preventDefault: function() {
575
+ isPrevented = true;
576
+ computePreRedrawHook = computePostRedrawHook = null;
577
+ }};
578
+ for (var i = 0, unloader; unloader = unloaders[i]; i++) {
579
+ unloader.handler.call(unloader.controller, event)
580
+ unloader.controller.onunload = null
581
+ }
582
+ if (isPrevented) {
583
+ for (var i = 0, unloader; unloader = unloaders[i]; i++) unloader.controller.onunload = unloader.handler
584
+ }
585
+ else unloaders = []
586
+
499
587
  if (controllers[index] && typeof controllers[index].onunload === FUNCTION) {
500
- var event = {
501
- preventDefault: function() {isPrevented = true}
502
- };
503
588
  controllers[index].onunload(event)
504
589
  }
590
+
505
591
  if (!isPrevented) {
506
592
  m.redraw.strategy("all");
507
593
  m.startComputation();
508
594
  roots[index] = root;
509
- var currentModule = topModule = module;
510
- var controller = new module.controller;
511
- //controllers may call m.module recursively (via m.route redirects, for example)
512
- //this conditional ensures only the last recursive m.module call is applied
513
- if (currentModule === topModule) {
595
+ if (arguments.length > 2) component = subcomponent(component, [].slice.call(arguments, 2))
596
+ var currentComponent = topComponent = component = component || {controller: function() {}};
597
+ var constructor = component.controller || noop
598
+ var controller = new constructor;
599
+ //controllers may call m.mount recursively (via m.route redirects, for example)
600
+ //this conditional ensures only the last recursive m.mount call is applied
601
+ if (currentComponent === topComponent) {
514
602
  controllers[index] = controller;
515
- modules[index] = module
603
+ components[index] = component
516
604
  }
517
605
  endFirstComputation();
518
606
  return controllers[index]
519
607
  }
520
608
  };
609
+ var redrawing = false
521
610
  m.redraw = function(force) {
611
+ if (redrawing) return
612
+ redrawing = true
522
613
  //lastRedrawId is a positive number if a second redraw is requested before the next animation frame
523
614
  //lastRedrawID is null if it's the first redraw and not an event handler
524
615
  if (lastRedrawId && force !== true) {
525
616
  //when setTimeout: only reschedule redraw if time between now and previous redraw is bigger than a frame, otherwise keep currently scheduled timeout
526
617
  //when rAF: always reschedule redraw
527
- if (new Date - lastRedrawCallTime > FRAME_BUDGET || $requestAnimationFrame === window.requestAnimationFrame) {
618
+ if ($requestAnimationFrame === window.requestAnimationFrame || new Date - lastRedrawCallTime > FRAME_BUDGET) {
528
619
  if (lastRedrawId > 0) $cancelAnimationFrame(lastRedrawId);
529
620
  lastRedrawId = $requestAnimationFrame(redraw, FRAME_BUDGET)
530
621
  }
@@ -533,13 +624,18 @@ var m = (function app(window, undefined) {
533
624
  redraw();
534
625
  lastRedrawId = $requestAnimationFrame(function() {lastRedrawId = null}, FRAME_BUDGET)
535
626
  }
627
+ redrawing = false
536
628
  };
537
629
  m.redraw.strategy = m.prop();
538
630
  function redraw() {
539
- var forceRedraw = m.redraw.strategy() === "all";
631
+ if (computePreRedrawHook) {
632
+ computePreRedrawHook()
633
+ computePreRedrawHook = null
634
+ }
540
635
  for (var i = 0, root; root = roots[i]; i++) {
541
636
  if (controllers[i]) {
542
- m.render(root, modules[i].view(controllers[i]), forceRedraw)
637
+ var args = components[i].controller && components[i].controller.$$args ? [controllers[i]].concat(components[i].controller.$$args) : [controllers[i]]
638
+ m.render(root, components[i].view ? components[i].view(controllers[i], args) : "")
543
639
  }
544
640
  }
545
641
  //after rendering within a routed context, we need to scroll back to the top, and fetch the document title for history.pushState
@@ -576,7 +672,7 @@ var m = (function app(window, undefined) {
576
672
 
577
673
  //routing
578
674
  var modes = {pathname: "", hash: "#", search: "?"};
579
- var redirect = function() {}, routeParams, currentRoute;
675
+ var redirect = noop, routeParams, currentRoute, isDefaultRoute = false;
580
676
  m.route = function() {
581
677
  //m.route()
582
678
  if (arguments.length === 0) return currentRoute;
@@ -586,29 +682,42 @@ var m = (function app(window, undefined) {
586
682
  redirect = function(source) {
587
683
  var path = currentRoute = normalizeRoute(source);
588
684
  if (!routeByValue(root, router, path)) {
685
+ if (isDefaultRoute) throw new Error("Ensure the default route matches one of the routes defined in m.route")
686
+ isDefaultRoute = true
589
687
  m.route(defaultRoute, true)
688
+ isDefaultRoute = false
590
689
  }
591
690
  };
592
691
  var listener = m.route.mode === "hash" ? "onhashchange" : "onpopstate";
593
692
  window[listener] = function() {
594
- if (currentRoute != normalizeRoute($location[m.route.mode])) {
595
- redirect($location[m.route.mode])
693
+ var path = $location[m.route.mode]
694
+ if (m.route.mode === "pathname") path += $location.search
695
+ if (currentRoute != normalizeRoute(path)) {
696
+ redirect(path)
596
697
  }
597
698
  };
598
- computePostRedrawHook = setScroll;
699
+ computePreRedrawHook = setScroll;
599
700
  window[listener]()
600
701
  }
601
702
  //config: m.route
602
- else if (arguments[0].addEventListener) {
703
+ else if (arguments[0].addEventListener || arguments[0].attachEvent) {
603
704
  var element = arguments[0];
604
705
  var isInitialized = arguments[1];
605
706
  var context = arguments[2];
606
- element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + this.attrs.href;
607
- element.removeEventListener("click", routeUnobtrusive);
608
- element.addEventListener("click", routeUnobtrusive)
707
+ var vdom = arguments[3];
708
+ element.href = (m.route.mode !== 'pathname' ? $location.pathname : '') + modes[m.route.mode] + vdom.attrs.href;
709
+ if (element.addEventListener) {
710
+ element.removeEventListener("click", routeUnobtrusive);
711
+ element.addEventListener("click", routeUnobtrusive)
712
+ }
713
+ else {
714
+ element.detachEvent("onclick", routeUnobtrusive);
715
+ element.attachEvent("onclick", routeUnobtrusive)
716
+ }
609
717
  }
610
- //m.route(route, params)
718
+ //m.route(route, params, shouldReplaceHistoryEntry)
611
719
  else if (type.call(arguments[0]) === STRING) {
720
+ var oldRoute = currentRoute;
612
721
  currentRoute = arguments[0];
613
722
  var args = arguments[1] || {}
614
723
  var queryIndex = currentRoute.indexOf("?")
@@ -618,16 +727,19 @@ var m = (function app(window, undefined) {
618
727
  var currentPath = queryIndex > -1 ? currentRoute.slice(0, queryIndex) : currentRoute
619
728
  if (querystring) currentRoute = currentPath + (currentPath.indexOf("?") === -1 ? "?" : "&") + querystring;
620
729
 
621
- var shouldReplaceHistoryEntry = (arguments.length === 3 ? arguments[2] : arguments[1]) === true;
730
+ var shouldReplaceHistoryEntry = (arguments.length === 3 ? arguments[2] : arguments[1]) === true || oldRoute === arguments[0];
622
731
 
623
732
  if (window.history.pushState) {
733
+ computePreRedrawHook = setScroll
624
734
  computePostRedrawHook = function() {
625
735
  window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, $document.title, modes[m.route.mode] + currentRoute);
626
- setScroll()
627
736
  };
628
737
  redirect(modes[m.route.mode] + currentRoute)
629
738
  }
630
- else $location[m.route.mode] = currentRoute
739
+ else {
740
+ $location[m.route.mode] = currentRoute
741
+ redirect(modes[m.route.mode] + currentRoute)
742
+ }
631
743
  }
632
744
  };
633
745
  m.route.param = function(key) {
@@ -635,7 +747,9 @@ var m = (function app(window, undefined) {
635
747
  return routeParams[key]
636
748
  };
637
749
  m.route.mode = "search";
638
- function normalizeRoute(route) {return route.slice(modes[m.route.mode].length)}
750
+ function normalizeRoute(route) {
751
+ return route.slice(modes[m.route.mode].length)
752
+ }
639
753
  function routeByValue(root, router, path) {
640
754
  routeParams = {};
641
755
 
@@ -645,9 +759,18 @@ var m = (function app(window, undefined) {
645
759
  path = path.substr(0, queryStart)
646
760
  }
647
761
 
762
+ // Get all routes and check if there's
763
+ // an exact match for the current path
764
+ var keys = Object.keys(router);
765
+ var index = keys.indexOf(path);
766
+ if(index !== -1){
767
+ m.mount(root, router[keys [index]]);
768
+ return true;
769
+ }
770
+
648
771
  for (var route in router) {
649
772
  if (route === path) {
650
- m.module(root, router[route]);
773
+ m.mount(root, router[route]);
651
774
  return true
652
775
  }
653
776
 
@@ -658,7 +781,7 @@ var m = (function app(window, undefined) {
658
781
  var keys = route.match(/:[^\/]+/g) || [];
659
782
  var values = [].slice.call(arguments, 1, -2);
660
783
  for (var i = 0, len = keys.length; i < len; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i])
661
- m.module(root, router[route])
784
+ m.mount(root, router[route])
662
785
  });
663
786
  return true
664
787
  }
@@ -669,8 +792,9 @@ var m = (function app(window, undefined) {
669
792
  if (e.ctrlKey || e.metaKey || e.which === 2) return;
670
793
  if (e.preventDefault) e.preventDefault();
671
794
  else e.returnValue = false;
672
- var currentTarget = e.currentTarget || this;
795
+ var currentTarget = e.currentTarget || e.srcElement;
673
796
  var args = m.route.mode === "pathname" && currentTarget.search ? parseQueryString(currentTarget.search.slice(1)) : {};
797
+ while (currentTarget && currentTarget.nodeName.toUpperCase() != "A") currentTarget = currentTarget.parentNode
674
798
  m.route(currentTarget[m.route.mode].slice(modes[m.route.mode].length), args)
675
799
  }
676
800
  function setScroll() {
@@ -678,24 +802,46 @@ var m = (function app(window, undefined) {
678
802
  else window.scrollTo(0, 0)
679
803
  }
680
804
  function buildQueryString(object, prefix) {
681
- var str = [];
682
- for(var prop in object) {
683
- var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop];
684
- str.push(value != null && type.call(value) === OBJECT ? buildQueryString(value, key) : encodeURIComponent(key) + "=" + encodeURIComponent(value))
805
+ var duplicates = {}
806
+ var str = []
807
+ for (var prop in object) {
808
+ var key = prefix ? prefix + "[" + prop + "]" : prop
809
+ var value = object[prop]
810
+ var valueType = type.call(value)
811
+ var pair = (value === null) ? encodeURIComponent(key) :
812
+ valueType === OBJECT ? buildQueryString(value, key) :
813
+ valueType === ARRAY ? value.reduce(function(memo, item) {
814
+ if (!duplicates[key]) duplicates[key] = {}
815
+ if (!duplicates[key][item]) {
816
+ duplicates[key][item] = true
817
+ return memo.concat(encodeURIComponent(key) + "=" + encodeURIComponent(item))
818
+ }
819
+ return memo
820
+ }, []).join("&") :
821
+ encodeURIComponent(key) + "=" + encodeURIComponent(value)
822
+ if (value !== undefined) str.push(pair)
685
823
  }
686
824
  return str.join("&")
687
825
  }
688
826
  function parseQueryString(str) {
827
+ if (str.charAt(0) === "?") str = str.substring(1);
828
+
689
829
  var pairs = str.split("&"), params = {};
690
830
  for (var i = 0, len = pairs.length; i < len; i++) {
691
831
  var pair = pairs[i].split("=");
692
- params[decodeSpace(pair[0])] = pair[1] ? decodeSpace(pair[1]) : ""
832
+ var key = decodeURIComponent(pair[0])
833
+ var value = pair.length == 2 ? decodeURIComponent(pair[1]) : null
834
+ if (params[key] != null) {
835
+ if (type.call(params[key]) !== ARRAY) params[key] = [params[key]]
836
+ params[key].push(value)
837
+ }
838
+ else params[key] = value
693
839
  }
694
840
  return params
695
841
  }
696
- function decodeSpace(string) {
697
- return decodeURIComponent(string.replace(/\+/g, " "))
698
- }
842
+ m.route.buildQueryString = buildQueryString
843
+ m.route.parseQueryString = parseQueryString
844
+
699
845
  function reset(root) {
700
846
  var cacheKey = getCellCacheKey(root);
701
847
  clear(root.childNodes, cellCache[cacheKey]);
@@ -707,11 +853,11 @@ var m = (function app(window, undefined) {
707
853
  deferred.promise = propify(deferred.promise);
708
854
  return deferred
709
855
  };
710
- function propify(promise) {
711
- var prop = m.prop();
856
+ function propify(promise, initialValue) {
857
+ var prop = m.prop(initialValue);
712
858
  promise.then(prop);
713
859
  prop.then = function(resolve, reject) {
714
- return propify(promise.then(resolve, reject))
860
+ return propify(promise.then(resolve, reject), initialValue)
715
861
  };
716
862
  return prop
717
863
  }
@@ -877,7 +1023,7 @@ var m = (function app(window, undefined) {
877
1023
  var script = $document.createElement("script");
878
1024
 
879
1025
  window[callbackKey] = function(resp) {
880
- $document.body.removeChild(script);
1026
+ script.parentNode.removeChild(script);
881
1027
  options.onload({
882
1028
  type: "load",
883
1029
  target: {
@@ -888,7 +1034,7 @@ var m = (function app(window, undefined) {
888
1034
  };
889
1035
 
890
1036
  script.onerror = function(e) {
891
- $document.body.removeChild(script);
1037
+ script.parentNode.removeChild(script);
892
1038
 
893
1039
  options.onerror({
894
1040
  type: "error",
@@ -964,20 +1110,21 @@ var m = (function app(window, undefined) {
964
1110
 
965
1111
  m.request = function(xhrOptions) {
966
1112
  if (xhrOptions.background !== true) m.startComputation();
967
- var deferred = m.deferred();
1113
+ var deferred = new Deferred();
968
1114
  var isJSONP = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp";
969
1115
  var serialize = xhrOptions.serialize = isJSONP ? identity : xhrOptions.serialize || JSON.stringify;
970
1116
  var deserialize = xhrOptions.deserialize = isJSONP ? identity : xhrOptions.deserialize || JSON.parse;
971
- var extract = xhrOptions.extract || function(xhr) {
1117
+ var extract = isJSONP ? function(jsonp) {return jsonp.responseText} : xhrOptions.extract || function(xhr) {
972
1118
  return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText
973
1119
  };
1120
+ xhrOptions.method = (xhrOptions.method || 'GET').toUpperCase();
974
1121
  xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data);
975
1122
  xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize);
976
1123
  xhrOptions.onload = xhrOptions.onerror = function(e) {
977
1124
  try {
978
1125
  e = e || event;
979
1126
  var unwrap = (e.type === "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity;
980
- var response = unwrap(deserialize(extract(e.target, xhrOptions)));
1127
+ var response = unwrap(deserialize(extract(e.target, xhrOptions)), e.target);
981
1128
  if (e.type === "load") {
982
1129
  if (type.call(response) === ARRAY && xhrOptions.type) {
983
1130
  for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i])
@@ -993,7 +1140,7 @@ var m = (function app(window, undefined) {
993
1140
  if (xhrOptions.background !== true) m.endComputation()
994
1141
  };
995
1142
  ajax(xhrOptions);
996
- deferred.promise(xhrOptions.initialValue);
1143
+ deferred.promise = propify(deferred.promise, xhrOptions.initialValue);
997
1144
  return deferred.promise
998
1145
  };
999
1146