soca 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  // name: sammy
2
- // version: 0.6.3
2
+ // version: 0.7.1
3
3
 
4
4
  // Sammy.js / http://sammyjs.org
5
5
 
@@ -8,13 +8,13 @@
8
8
  var Sammy,
9
9
  PATH_REPLACER = "([^\/]+)",
10
10
  PATH_NAME_MATCHER = /:([\w\d]+)/g,
11
- QUERY_STRING_MATCHER = /\?([^#]*)$/,
11
+ QUERY_STRING_MATCHER = /\?([^#]*)?$/,
12
12
  // mainly for making `arguments` an Array
13
13
  _makeArray = function(nonarray) { return Array.prototype.slice.call(nonarray); },
14
14
  // borrowed from jQuery
15
15
  _isFunction = function( obj ) { return Object.prototype.toString.call(obj) === "[object Function]"; },
16
16
  _isArray = function( obj ) { return Object.prototype.toString.call(obj) === "[object Array]"; },
17
- _decode = function( str ) { return decodeURIComponent(str.replace(/\+/g, ' ')); },
17
+ _decode = function( str ) { return decodeURIComponent((str || '').replace(/\+/g, ' ')); },
18
18
  _encode = encodeURIComponent,
19
19
  _escapeHTML = function(s) {
20
20
  return String(s).replace(/&(?!\w+;)/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
@@ -23,6 +23,7 @@
23
23
  return function(path, callback) { return this.route.apply(this, [verb, path, callback]); };
24
24
  },
25
25
  _template_cache = {},
26
+ _has_history = !!(window.history && history.pushState),
26
27
  loggers = [];
27
28
 
28
29
 
@@ -43,7 +44,7 @@
43
44
  // // returns the app at #main or a new app
44
45
  // Sammy('#main')
45
46
  //
46
- // // equivilent to "new Sammy.Application", except appends to apps
47
+ // // equivalent to "new Sammy.Application", except appends to apps
47
48
  // Sammy();
48
49
  // Sammy(function() { ... });
49
50
  //
@@ -64,7 +65,7 @@
64
65
  app.use(plugin);
65
66
  });
66
67
  }
67
- // if the selector changes make sure the refrence in Sammy.apps changes
68
+ // if the selector changes make sure the reference in Sammy.apps changes
68
69
  if (app.element_selector != selector) {
69
70
  delete Sammy.apps[selector];
70
71
  }
@@ -73,7 +74,7 @@
73
74
  }
74
75
  };
75
76
 
76
- Sammy.VERSION = '0.6.3';
77
+ Sammy.VERSION = '0.7.1';
77
78
 
78
79
  // Add to the global logger pool. Takes a function that accepts an
79
80
  // unknown number of arguments and should print them or send them somewhere
@@ -113,7 +114,7 @@
113
114
  makeArray: _makeArray,
114
115
  isFunction: _isFunction,
115
116
  isArray: _isArray
116
- })
117
+ });
117
118
 
118
119
  // Sammy.Object is the base for all other Sammy classes. It provides some useful
119
120
  // functionality, including cloning, iterating, etc.
@@ -171,7 +172,7 @@
171
172
 
172
173
  // Checks if the object has a value at `key` and that the value is not empty
173
174
  has: function(key) {
174
- return this[key] && $.trim(this[key].toString()) != '';
175
+ return this[key] && $.trim(this[key].toString()) !== '';
175
176
  },
176
177
 
177
178
  // convenience method to join as many arguments as you want
@@ -201,86 +202,136 @@
201
202
  }
202
203
  });
203
204
 
204
- // The HashLocationProxy is the default location proxy for all Sammy applications.
205
+ // The DefaultLocationProxy is the default location proxy for all Sammy applications.
205
206
  // A location proxy is a prototype that conforms to a simple interface. The purpose
206
207
  // of a location proxy is to notify the Sammy.Application its bound to when the location
207
- // or 'external state' changes. The HashLocationProxy considers the state to be
208
- // changed when the 'hash' (window.location.hash / '#') changes. It does this in two
209
- // different ways depending on what browser you are using. The newest browsers
210
- // (IE, Safari > 4, FF >= 3.6) support a 'onhashchange' DOM event, thats fired whenever
211
- // the location.hash changes. In this situation the HashLocationProxy just binds
212
- // to this event and delegates it to the application. In the case of older browsers
213
- // a poller is set up to track changes to the hash. Unlike Sammy 0.3 or earlier,
214
- // the HashLocationProxy allows the poller to be a global object, eliminating the
215
- // need for multiple pollers even when thier are multiple apps on the page.
216
- Sammy.HashLocationProxy = function(app, run_interval_every) {
208
+ // or 'external state' changes.
209
+ //
210
+ // The `DefaultLocationProxy` watches for changes to the path of the current window and
211
+ // is also able to set the path based on changes in the application. It does this by
212
+ // using different methods depending on what is available in the current browser. In
213
+ // the latest and greatest browsers it used the HTML5 History API and the `pushState`
214
+ // `popState` events/methods. This allows you to use Sammy to serve a site behind normal
215
+ // URI paths as opposed to the older default of hash (#) based routing. Because the server
216
+ // can interpret the changed path on a refresh or re-entry, though, it requires additional
217
+ // support on the server side. If you'd like to force disable HTML5 history support, please
218
+ // use the `disable_push_state` setting on `Sammy.Application`. If pushState support
219
+ // is enabled, `DefaultLocationProxy` also binds to all links on the page. If a link is clicked
220
+ // that matches the current set of routes, the URL is changed using pushState instead of
221
+ // fully setting the location and the app is notified of the change.
222
+ //
223
+ // If the browser does not have support for HTML5 History, `DefaultLocationProxy` automatically
224
+ // falls back to the older hash based routing. The newest browsers (IE, Safari > 4, FF >= 3.6)
225
+ // support a 'onhashchange' DOM event, thats fired whenever the location.hash changes.
226
+ // In this situation the DefaultLocationProxy just binds to this event and delegates it to
227
+ // the application. In the case of older browsers a poller is set up to track changes to the
228
+ // hash.
229
+ Sammy.DefaultLocationProxy = function(app, run_interval_every) {
217
230
  this.app = app;
218
231
  // set is native to false and start the poller immediately
219
232
  this.is_native = false;
233
+ this.has_history = _has_history;
220
234
  this._startPolling(run_interval_every);
221
235
  };
222
236
 
223
- Sammy.HashLocationProxy.prototype = {
224
-
237
+ Sammy.DefaultLocationProxy.fullPath = function(location_obj) {
238
+ // Bypass the `window.location.hash` attribute. If a question mark
239
+ // appears in the hash IE6 will strip it and all of the following
240
+ // characters from `window.location.hash`.
241
+ var matches = location_obj.toString().match(/^[^#]*(#.+)$/);
242
+ var hash = matches ? matches[1] : '';
243
+ return [location_obj.pathname, location_obj.search, hash].join('');
244
+ };
245
+ Sammy.DefaultLocationProxy.prototype = {
225
246
  // bind the proxy events to the current app.
226
247
  bind: function() {
227
- var proxy = this, app = this.app;
248
+ var proxy = this, app = this.app, lp = Sammy.DefaultLocationProxy;
228
249
  $(window).bind('hashchange.' + this.app.eventNamespace(), function(e, non_native) {
229
250
  // if we receive a native hash change event, set the proxy accordingly
230
251
  // and stop polling
231
252
  if (proxy.is_native === false && !non_native) {
232
- Sammy.log('native hash change exists, using');
233
253
  proxy.is_native = true;
234
- window.clearInterval(Sammy.HashLocationProxy._interval);
254
+ window.clearInterval(lp._interval);
235
255
  }
236
256
  app.trigger('location-changed');
237
257
  });
238
- if (!Sammy.HashLocationProxy._bindings) {
239
- Sammy.HashLocationProxy._bindings = 0;
258
+ if (_has_history && !app.disable_push_state) {
259
+ // bind to popstate
260
+ $(window).bind('popstate.' + this.app.eventNamespace(), function(e) {
261
+ app.trigger('location-changed');
262
+ });
263
+ // bind to link clicks that have routes
264
+ $('a').live('click.history-' + this.app.eventNamespace(), function(e) {
265
+ if (e.isDefaultPrevented()) {
266
+ return;
267
+ }
268
+ var full_path = lp.fullPath(this);
269
+ if (this.hostname == window.location.hostname && app.lookupRoute('get', full_path)) {
270
+ e.preventDefault();
271
+ proxy.setLocation(full_path);
272
+ return false;
273
+ }
274
+ });
275
+ }
276
+ if (!lp._bindings) {
277
+ lp._bindings = 0;
240
278
  }
241
- Sammy.HashLocationProxy._bindings++;
279
+ lp._bindings++;
242
280
  },
243
281
 
244
282
  // unbind the proxy events from the current app
245
283
  unbind: function() {
246
284
  $(window).unbind('hashchange.' + this.app.eventNamespace());
247
- Sammy.HashLocationProxy._bindings--;
248
- if (Sammy.HashLocationProxy._bindings <= 0) {
249
- window.clearInterval(Sammy.HashLocationProxy._interval);
285
+ $(window).unbind('popstate.' + this.app.eventNamespace());
286
+ $('a').die('click.history-' + this.app.eventNamespace());
287
+ Sammy.DefaultLocationProxy._bindings--;
288
+ if (Sammy.DefaultLocationProxy._bindings <= 0) {
289
+ window.clearInterval(Sammy.DefaultLocationProxy._interval);
250
290
  }
251
291
  },
252
292
 
253
293
  // get the current location from the hash.
254
294
  getLocation: function() {
255
- // Bypass the `window.location.hash` attribute. If a question mark
256
- // appears in the hash IE6 will strip it and all of the following
257
- // characters from `window.location.hash`.
258
- var matches = window.location.toString().match(/^[^#]*(#.+)$/);
259
- return matches ? matches[1] : '';
295
+ return Sammy.DefaultLocationProxy.fullPath(window.location);
260
296
  },
261
297
 
262
298
  // set the current location to `new_location`
263
299
  setLocation: function(new_location) {
264
- return (window.location = new_location);
300
+ if (/^([^#\/]|$)/.test(new_location)) { // non-prefixed url
301
+ if (_has_history) {
302
+ new_location = '/' + new_location;
303
+ } else {
304
+ new_location = '#!/' + new_location;
305
+ }
306
+ }
307
+ if (new_location != this.getLocation()) {
308
+ // HTML5 History exists and new_location is a full path
309
+ if (_has_history && /^\//.test(new_location)) {
310
+ history.pushState({ path: new_location }, window.title, new_location);
311
+ this.app.trigger('location-changed');
312
+ } else {
313
+ return (window.location = new_location);
314
+ }
315
+ }
265
316
  },
266
317
 
267
318
  _startPolling: function(every) {
268
319
  // set up interval
269
320
  var proxy = this;
270
- if (!Sammy.HashLocationProxy._interval) {
321
+ if (!Sammy.DefaultLocationProxy._interval) {
271
322
  if (!every) { every = 10; }
272
323
  var hashCheck = function() {
273
324
  var current_location = proxy.getLocation();
274
- if (!Sammy.HashLocationProxy._last_location ||
275
- current_location != Sammy.HashLocationProxy._last_location) {
325
+ if (typeof Sammy.DefaultLocationProxy._last_location == 'undefined' ||
326
+ current_location != Sammy.DefaultLocationProxy._last_location) {
276
327
  window.setTimeout(function() {
277
328
  $(window).trigger('hashchange', [true]);
278
329
  }, 0);
279
330
  }
280
- Sammy.HashLocationProxy._last_location = current_location;
331
+ Sammy.DefaultLocationProxy._last_location = current_location;
281
332
  };
282
333
  hashCheck();
283
- Sammy.HashLocationProxy._interval = window.setInterval(hashCheck, every);
334
+ Sammy.DefaultLocationProxy._interval = window.setInterval(hashCheck, every);
284
335
  }
285
336
  }
286
337
  };
@@ -304,9 +355,9 @@
304
355
  if (_isFunction(app_function)) {
305
356
  app_function.apply(this, [this]);
306
357
  }
307
- // set the location proxy if not defined to the default (HashLocationProxy)
358
+ // set the location proxy if not defined to the default (DefaultLocationProxy)
308
359
  if (!this._location_proxy) {
309
- this.setLocationProxy(new Sammy.HashLocationProxy(this, this.run_interval_every));
360
+ this.setLocationProxy(new Sammy.DefaultLocationProxy(this, this.run_interval_every));
310
361
  }
311
362
  if (this.debug) {
312
363
  this.bindToAllEvents(function(e, data) {
@@ -335,18 +386,22 @@
335
386
  // When set to true, logs all of the default events using `log()`
336
387
  debug: false,
337
388
 
338
- // When set to true, and the error() handler is not overriden, will actually
389
+ // When set to true, and the error() handler is not overridden, will actually
339
390
  // raise JS errors in routes (500) and when routes can't be found (404)
340
391
  raise_errors: false,
341
392
 
342
393
  // The time in milliseconds that the URL is queried for changes
343
394
  run_interval_every: 50,
344
395
 
396
+ // if using the `DefaultLocationProxy` setting this to true will force the app to use
397
+ // traditional hash based routing as opposed to the new HTML5 PushState support
398
+ disable_push_state: false,
399
+
345
400
  // The default template engine to use when using `partial()` in an
346
401
  // `EventContext`. `template_engine` can either be a string that
347
402
  // corresponds to the name of a method/helper on EventContext or it can be a function
348
403
  // that takes two arguments, the content of the unrendered partial and an optional
349
- // JS object that contains interpolation data. Template engine is only called/refered
404
+ // JS object that contains interpolation data. Template engine is only called/referred
350
405
  // to if the extension of the partial is null or unknown. See `partial()`
351
406
  // for more information
352
407
  template_engine: null,
@@ -395,7 +450,7 @@
395
450
  // });
396
451
  //
397
452
  // If plugin is passed as a string it assumes your are trying to load
398
- // Sammy."Plugin". This is the prefered way of loading core Sammy plugins
453
+ // Sammy."Plugin". This is the preferred way of loading core Sammy plugins
399
454
  // as it allows for better error-messaging.
400
455
  //
401
456
  // ### Example
@@ -430,9 +485,9 @@
430
485
  },
431
486
 
432
487
  // Sets the location proxy for the current app. By default this is set to
433
- // a new `Sammy.HashLocationProxy` on initialization. However, you can set
488
+ // a new `Sammy.DefaultLocationProxy` on initialization. However, you can set
434
489
  // the location_proxy inside you're app function to give your app a custom
435
- // location mechanism. See `Sammy.HashLocationProxy` and `Sammy.DataLocationProxy`
490
+ // location mechanism. See `Sammy.DefaultLocationProxy` and `Sammy.DataLocationProxy`
436
491
  // for examples.
437
492
  //
438
493
  // `setLocationProxy()` takes an initialized location proxy.
@@ -456,6 +511,12 @@
456
511
  }
457
512
  },
458
513
 
514
+ // provide log() override for inside an app that includes the relevant application element_selector
515
+ log: function() {
516
+ Sammy.log.apply(Sammy, Array.prototype.concat.apply([this.element_selector],arguments));
517
+ },
518
+
519
+
459
520
  // `route()` is the main method for defining routes within an application.
460
521
  // For great detail on routes, check out:
461
522
  // [http://sammyjs.org/docs/routes](http://sammyjs.org/docs/routes)
@@ -469,7 +530,7 @@
469
530
  // the first argument is the path, the second is the callback and the verb
470
531
  // is assumed to be 'any'.
471
532
  // * `path` A Regexp or a String representing the path to match to invoke this verb.
472
- // * `callback` A Function that is called/evaluated whent the route is run see: `runRoute()`.
533
+ // * `callback` A Function that is called/evaluated when the route is run see: `runRoute()`.
473
534
  // It is also possible to pass a string as the callback, which is looked up as the name
474
535
  // of a method on the application.
475
536
  //
@@ -499,7 +560,7 @@
499
560
  param_names.push(path_match[1]);
500
561
  }
501
562
  // replace with the path replacement
502
- path = new RegExp("^" + path.replace(PATH_NAME_MATCHER, PATH_REPLACER) + "$");
563
+ path = new RegExp(path.replace(PATH_NAME_MATCHER, PATH_REPLACER) + "$");
503
564
  }
504
565
  // lookup callback
505
566
  if (typeof callback == 'string') {
@@ -570,7 +631,7 @@
570
631
  return ['sammy-app', this.namespace].join('-');
571
632
  },
572
633
 
573
- // Works just like `jQuery.fn.bind()` with a couple noteable differences.
634
+ // Works just like `jQuery.fn.bind()` with a couple notable differences.
574
635
  //
575
636
  // * It binds all events to the application element
576
637
  // * All events are bound within the `eventNamespace()`
@@ -683,7 +744,7 @@
683
744
  // that take a single argument `callback` which is the entire route
684
745
  // execution path wrapped up in a closure. This means you can decide whether
685
746
  // or not to proceed with execution by not invoking `callback` or,
686
- // more usefuly wrapping callback inside the result of an asynchronous execution.
747
+ // more usefully wrapping callback inside the result of an asynchronous execution.
687
748
  //
688
749
  // ### Example
689
750
  //
@@ -821,7 +882,7 @@
821
882
  this._running = true;
822
883
  // set last location
823
884
  this.last_location = null;
824
- if (this.getLocation() == '' && typeof start_url != 'undefined') {
885
+ if (!(/\#(.+)/.test(this.getLocation())) && typeof start_url != 'undefined') {
825
886
  this.setLocation(start_url);
826
887
  }
827
888
  // check url
@@ -847,7 +908,7 @@
847
908
  },
848
909
 
849
910
  // The opposite of `run()`, un-binds all event listeners and intervals
850
- // `run()` Automaticaly binds a `onunload` event to run this when
911
+ // `run()` Automatically binds a `onunload` event to run this when
851
912
  // the document is closed.
852
913
  unload: function() {
853
914
  if (!this.isRunning()) { return false; }
@@ -880,7 +941,7 @@
880
941
  });
881
942
  // next, bind to listener names (only if they dont exist in APP_EVENTS)
882
943
  $.each(this.listeners.keys(true), function(i, name) {
883
- if (app.APP_EVENTS.indexOf(name) == -1) {
944
+ if ($.inArray(name, app.APP_EVENTS) == -1) {
884
945
  app.bind(name, callback);
885
946
  }
886
947
  });
@@ -896,15 +957,16 @@
896
957
  // Given a verb and a String path, will return either a route object or false
897
958
  // if a matching route can be found within the current defined set.
898
959
  lookupRoute: function(verb, path) {
899
- var app = this, routed = false;
900
- this.trigger('lookup-route', {verb: verb, path: path});
960
+ var app = this, routed = false, i = 0, l, route;
901
961
  if (typeof this.routes[verb] != 'undefined') {
902
- $.each(this.routes[verb], function(i, route) {
962
+ l = this.routes[verb].length;
963
+ for (; i < l; i++) {
964
+ route = this.routes[verb][i];
903
965
  if (app.routablePath(path).match(route.path)) {
904
966
  routed = route;
905
- return false;
967
+ break;
906
968
  }
907
- });
969
+ }
908
970
  }
909
971
  return routed;
910
972
  },
@@ -913,7 +975,7 @@
913
975
  // possible URL params and then invokes the route's callback within a new
914
976
  // `Sammy.EventContext`. If the route can not be found, it calls
915
977
  // `notFound()`. If `raise_errors` is set to `true` and
916
- // the `error()` has not been overriden, it will throw an actual JS
978
+ // the `error()` has not been overridden, it will throw an actual JS
917
979
  // error.
918
980
  //
919
981
  // You probably will never have to call this directly.
@@ -1020,7 +1082,7 @@
1020
1082
  // // match against a path string
1021
1083
  // app.contextMatchesOptions(context, '#/mypath'); //=> true
1022
1084
  // app.contextMatchesOptions(context, '#/otherpath'); //=> false
1023
- // // equivilent to
1085
+ // // equivalent to
1024
1086
  // app.contextMatchesOptions(context, {only: {path:'#/mypath'}}); //=> true
1025
1087
  // app.contextMatchesOptions(context, {only: {path:'#/otherpath'}}); //=> false
1026
1088
  // // match against a path regexp
@@ -1035,11 +1097,20 @@
1035
1097
  // // match all except a path
1036
1098
  // app.contextMatchesOptions(context, {except: {path:'#/otherpath'}}); //=> true
1037
1099
  // app.contextMatchesOptions(context, {except: {path:'#/mypath'}}); //=> false
1100
+ // // match multiple paths
1101
+ // app.contextMatchesOptions(context, {path: ['#/mypath', '#/otherpath']}); //=> true
1102
+ // app.contextMatchesOptions(context, {path: ['#/otherpath', '#/thirdpath']}); //=> false
1103
+ // // equivalent to
1104
+ // app.contextMatchesOptions(context, {only: {path: ['#/mypath', '#/otherpath']}}); //=> true
1105
+ // app.contextMatchesOptions(context, {only: {path: ['#/otherpath', '#/thirdpath']}}); //=> false
1106
+ // // match all except multiple paths
1107
+ // app.contextMatchesOptions(context, {except: {path: ['#/mypath', '#/otherpath']}}); //=> false
1108
+ // app.contextMatchesOptions(context, {except: {path: ['#/otherpath', '#/thirdpath']}}); //=> true
1038
1109
  //
1039
1110
  contextMatchesOptions: function(context, match_options, positive) {
1040
1111
  // empty options always match
1041
1112
  var options = match_options;
1042
- if (typeof options === 'undefined' || options == {}) {
1113
+ if (typeof options === 'undefined' || $.isPlainObject(options)) {
1043
1114
  return true;
1044
1115
  }
1045
1116
  if (typeof positive === 'undefined') {
@@ -1049,6 +1120,17 @@
1049
1120
  if (typeof options === 'string' || _isFunction(options.test)) {
1050
1121
  options = {path: options};
1051
1122
  }
1123
+ // Do we have to match against multiple paths?
1124
+ if (_isArray(options.path)){
1125
+ var results, numopt, opts;
1126
+ results = [];
1127
+ for (numopt in options.path){
1128
+ opts = $.extend({}, options, {path: options.path[numopt]});
1129
+ results.push(this.contextMatchesOptions(context, opts));
1130
+ }
1131
+ var matched = $.inArray(true, results) > -1 ? true : false;
1132
+ return positive ? matched : !matched;
1133
+ }
1052
1134
  if (options.only) {
1053
1135
  return this.contextMatchesOptions(context, options.only, true);
1054
1136
  } else if (options.except) {
@@ -1056,28 +1138,31 @@
1056
1138
  }
1057
1139
  var path_matched = true, verb_matched = true;
1058
1140
  if (options.path) {
1059
- // wierd regexp test
1060
- if (_isFunction(options.path.test)) {
1061
- path_matched = options.path.test(context.path);
1062
- } else {
1063
- path_matched = (options.path.toString() === context.path);
1141
+ // weird regexp test
1142
+ if (!_isFunction(options.path.test)) {
1143
+ options.path = new RegExp(options.path.toString() + '$');
1064
1144
  }
1145
+ path_matched = options.path.test(context.path);
1065
1146
  }
1066
1147
  if (options.verb) {
1067
- verb_matched = options.verb === context.verb;
1148
+ if(typeof options.verb === 'string') {
1149
+ verb_matched = options.verb === context.verb;
1150
+ } else {
1151
+ verb_matched = options.verb.indexOf(context.verb) > -1;
1152
+ }
1068
1153
  }
1069
1154
  return positive ? (verb_matched && path_matched) : !(verb_matched && path_matched);
1070
1155
  },
1071
1156
 
1072
1157
 
1073
1158
  // Delegates to the `location_proxy` to get the current location.
1074
- // See `Sammy.HashLocationProxy` for more info on location proxies.
1159
+ // See `Sammy.DefaultLocationProxy` for more info on location proxies.
1075
1160
  getLocation: function() {
1076
1161
  return this._location_proxy.getLocation();
1077
1162
  },
1078
1163
 
1079
1164
  // Delegates to the `location_proxy` to set the current location.
1080
- // See `Sammy.HashLocationProxy` for more info on location proxies.
1165
+ // See `Sammy.DefaultLocationProxy` for more info on location proxies.
1081
1166
  //
1082
1167
  // ### Arguments
1083
1168
  //
@@ -1096,23 +1181,29 @@
1096
1181
  // var app = $.sammy(function() {
1097
1182
  //
1098
1183
  // // implements a 'fade out'/'fade in'
1099
- // this.swap = function(content) {
1100
- // this.$element().hide('slow').html(content).show('slow');
1101
- // }
1102
- //
1103
- // get('#/', function() {
1104
- // this.partial('index.html.erb') // will fade out and in
1105
- // });
1184
+ // this.swap = function(content, callback) {
1185
+ // var context = this;
1186
+ // context.$element().fadeOut('slow', function() {
1187
+ // context.$element().html(content);
1188
+ // context.$element().fadeIn('slow', function() {
1189
+ // if (callback) {
1190
+ // callback.apply();
1191
+ // }
1192
+ // });
1193
+ // });
1194
+ // };
1106
1195
  //
1107
1196
  // });
1108
1197
  //
1109
- swap: function(content) {
1110
- return this.$element().html(content);
1198
+ swap: function(content, callback) {
1199
+ var $el = this.$element().html(content);
1200
+ if (_isFunction(callback)) { callback(content); }
1201
+ return $el;
1111
1202
  },
1112
1203
 
1113
1204
  // a simple global cache for templates. Uses the same semantics as
1114
1205
  // `Sammy.Cache` and `Sammy.Storage` so can easily be replaced with
1115
- // a persistant storage that lasts beyond the current request.
1206
+ // a persistent storage that lasts beyond the current request.
1116
1207
  templateCache: function(key, value) {
1117
1208
  if (typeof value != 'undefined') {
1118
1209
  return _template_cache[key] = value;
@@ -1126,7 +1217,7 @@
1126
1217
  return _template_cache = {};
1127
1218
  },
1128
1219
 
1129
- // This thows a '404 Not Found' error by invoking `error()`.
1220
+ // This throws a '404 Not Found' error by invoking `error()`.
1130
1221
  // Override this method or `error()` to provide custom
1131
1222
  // 404 behavior (i.e redirecting to / or showing a warning)
1132
1223
  notFound: function(verb, path) {
@@ -1178,16 +1269,18 @@
1178
1269
  var $form, path, verb, params, returned;
1179
1270
  this.trigger('check-form-submission', {form: form});
1180
1271
  $form = $(form);
1181
- path = $form.attr('action');
1272
+ path = $form.attr('action') || '';
1182
1273
  verb = this._getFormVerb($form);
1183
1274
  this.log('_checkFormSubmission', $form, path, verb);
1184
1275
  if (verb === 'get') {
1185
- this.setLocation(path + '?' + this._serializeFormParams($form));
1276
+ params = this._serializeFormParams($form);
1277
+ if (params !== '') { path += '?' + params; }
1278
+ this.setLocation(path);
1186
1279
  returned = false;
1187
1280
  } else {
1188
1281
  params = $.extend({}, this._parseFormParams($form));
1189
1282
  returned = this.runRoute(verb, path, params, form.get(0));
1190
- };
1283
+ }
1191
1284
  return (typeof returned == 'undefined') ? false : returned;
1192
1285
  },
1193
1286
 
@@ -1233,7 +1326,7 @@
1233
1326
  },
1234
1327
 
1235
1328
  _parseParamPair: function(params, key, value) {
1236
- if (params[key]) {
1329
+ if (typeof params[key] !== 'undefined') {
1237
1330
  if (_isArray(params[key])) {
1238
1331
  params[key].push(value);
1239
1332
  } else {
@@ -1256,11 +1349,11 @@
1256
1349
  });
1257
1350
 
1258
1351
  // `Sammy.RenderContext` is an object that makes sequential template loading,
1259
- // rendering and interpolation seamless even when dealing with asyncronous
1352
+ // rendering and interpolation seamless even when dealing with asynchronous
1260
1353
  // operations.
1261
1354
  //
1262
1355
  // `RenderContext` objects are not usually created directly, rather they are
1263
- // instatiated from an `Sammy.EventContext` by using `render()`, `load()` or
1356
+ // instantiated from an `Sammy.EventContext` by using `render()`, `load()` or
1264
1357
  // `partial()` which all return `RenderContext` objects.
1265
1358
  //
1266
1359
  // `RenderContext` methods always returns a modified `RenderContext`
@@ -1269,7 +1362,7 @@
1269
1362
  // The core magic is in the `then()` method which puts the callback passed as
1270
1363
  // an argument into a queue to be executed once the previous callback is complete.
1271
1364
  // All the methods of `RenderContext` are wrapped in `then()` which allows you
1272
- // to queue up methods by chaining, but maintaing a guarunteed execution order
1365
+ // to queue up methods by chaining, but maintaining a guaranteed execution order
1273
1366
  // even with remote calls to fetch templates.
1274
1367
  //
1275
1368
  Sammy.RenderContext = function(event_context) {
@@ -1290,9 +1383,9 @@
1290
1383
  // is executed immediately.
1291
1384
  //
1292
1385
  // The value returned from the callback is stored in `content` for the
1293
- // subsiquent operation. If you return `false`, the queue will pause, and
1386
+ // subsequent operation. If you return `false`, the queue will pause, and
1294
1387
  // the next callback in the queue will not be executed until `next()` is
1295
- // called. This allows for the guarunteed order of execution while working
1388
+ // called. This allows for the guaranteed order of execution while working
1296
1389
  // with async operations.
1297
1390
  //
1298
1391
  // If then() is passed a string instead of a function, the string is looked
@@ -1380,7 +1473,7 @@
1380
1473
  },
1381
1474
 
1382
1475
  // Load a template into the context.
1383
- // The `location` can either be a string specifiying the remote path to the
1476
+ // The `location` can either be a string specifying the remote path to the
1384
1477
  // file, a jQuery object, or a DOM element.
1385
1478
  //
1386
1479
  // No interpolation happens by default, the content is stored in
@@ -1429,7 +1522,7 @@
1429
1522
  $.ajax($.extend({
1430
1523
  url: location,
1431
1524
  data: {},
1432
- dataType: is_json ? 'json' : null,
1525
+ dataType: is_json ? 'json' : 'text',
1433
1526
  type: 'get',
1434
1527
  success: function(data) {
1435
1528
  if (should_cache) {
@@ -1457,6 +1550,28 @@
1457
1550
  });
1458
1551
  },
1459
1552
 
1553
+ // Load partials
1554
+ //
1555
+ // ### Example
1556
+ //
1557
+ // this.loadPartials({mypartial: '/path/to/partial'});
1558
+ //
1559
+ loadPartials: function(partials) {
1560
+ var name;
1561
+ if(partials) {
1562
+ this.partials = this.partials || {};
1563
+ for(name in partials) {
1564
+ (function(context, name) {
1565
+ context.load(partials[name])
1566
+ .then(function(template) {
1567
+ this.partials[name] = template;
1568
+ });
1569
+ })(this, name);
1570
+ }
1571
+ }
1572
+ return this;
1573
+ },
1574
+
1460
1575
  // `load()` a template and then `interpolate()` it with data.
1461
1576
  //
1462
1577
  // ### Example
@@ -1465,20 +1580,28 @@
1465
1580
  // this.render('mytemplate.template', {name: 'test'});
1466
1581
  // });
1467
1582
  //
1468
- render: function(location, data, callback) {
1583
+ render: function(location, data, callback, partials) {
1469
1584
  if (_isFunction(location) && !data) {
1470
1585
  return this.then(location);
1471
1586
  } else {
1472
- return this.load(location)
1587
+ return this.loadPartials(partials)
1588
+ .load(location)
1473
1589
  .interpolate(data, location)
1474
1590
  .then(callback);
1475
1591
  }
1476
1592
  },
1477
1593
 
1478
- // `render()` the the `location` with `data` and then `swap()` the
1594
+ // `render()` the `location` with `data` and then `swap()` the
1479
1595
  // app's `$element` with the rendered content.
1480
- partial: function(location, data) {
1481
- return this.render(location, data).swap();
1596
+ partial: function(location, data, callback) {
1597
+ if (_isFunction(callback)) {
1598
+ return this.render(location, data).swap(callback);
1599
+ } else if (!callback && _isFunction(data)) {
1600
+ // invoked as partial(location, callback)
1601
+ return this.render(location).swap(data);
1602
+ } else {
1603
+ return this.render(location, data).swap();
1604
+ }
1482
1605
  },
1483
1606
 
1484
1607
  // defers the call of function to occur in order of the render queue.
@@ -1510,7 +1633,7 @@
1510
1633
  });
1511
1634
  },
1512
1635
 
1513
- // itterates over an array, applying the callback for each item item. the
1636
+ // iterates over an array, applying the callback for each item item. the
1514
1637
  // callback takes the same style of arguments as `jQuery.each()` (index, item).
1515
1638
  // The return value of each callback is collected as a single string and stored
1516
1639
  // as `content` to be used in the next iteration of the `RenderContext`.
@@ -1579,15 +1702,16 @@
1579
1702
  engine = this.next_engine;
1580
1703
  this.next_engine = false;
1581
1704
  }
1582
- var rendered = context.event_context.interpolate(content, data, engine);
1705
+ var rendered = context.event_context.interpolate(content, data, engine, this.partials);
1583
1706
  return retain ? prev + rendered : rendered;
1584
1707
  });
1585
1708
  },
1586
1709
 
1587
- // executes `EventContext#swap()` with the `content`
1588
- swap: function() {
1710
+ // Swap the return contents ensuring order. See `Application#swap`
1711
+ swap: function(callback) {
1589
1712
  return this.then(function(content) {
1590
- this.event_context.swap(content);
1713
+ this.event_context.swap(content, callback);
1714
+ return content;
1591
1715
  }).trigger('changed', {});
1592
1716
  },
1593
1717
 
@@ -1614,12 +1738,13 @@
1614
1738
  },
1615
1739
 
1616
1740
  // trigger the event in the order of the event context. Same semantics
1617
- // as `Sammy.EventContext#trigger()`. If data is ommitted, `content`
1741
+ // as `Sammy.EventContext#trigger()`. If data is omitted, `content`
1618
1742
  // is sent as `{content: content}`
1619
1743
  trigger: function(name, data) {
1620
1744
  return this.then(function(content) {
1621
1745
  if (typeof data == 'undefined') { data = {content: content}; }
1622
1746
  this.event_context.trigger(name, data);
1747
+ return content;
1623
1748
  });
1624
1749
  }
1625
1750
 
@@ -1674,10 +1799,10 @@
1674
1799
  // Look up a templating engine within the current app and context.
1675
1800
  // `engine` can be one of the following:
1676
1801
  //
1677
- // * a function: should conform to `function(content, data) { return interploated; }`
1802
+ // * a function: should conform to `function(content, data) { return interpolated; }`
1678
1803
  // * a template path: 'template.ejs', looks up the extension to match to
1679
1804
  // the `ejs()` helper
1680
- // * a string referering to the helper: "mustache" => `mustache()`
1805
+ // * a string referring to the helper: "mustache" => `mustache()`
1681
1806
  //
1682
1807
  // If no engine is found, use the app's default `template_engine`
1683
1808
  //
@@ -1687,7 +1812,7 @@
1687
1812
  if (_isFunction(engine)) { return engine; }
1688
1813
  // lookup engine name by path extension
1689
1814
  engine = (engine || context.app.template_engine).toString();
1690
- if ((engine_match = engine.match(/\.([^\.]+)$/))) {
1815
+ if ((engine_match = engine.match(/\.([^\.\?\#]+)$/))) {
1691
1816
  engine = engine_match[1];
1692
1817
  }
1693
1818
  // set the engine to the default template engine if no match is found
@@ -1703,8 +1828,8 @@
1703
1828
 
1704
1829
  // using the template `engine` found with `engineFor()`, interpolate the
1705
1830
  // `data` into `content`
1706
- interpolate: function(content, data, engine) {
1707
- return this.engineFor(engine).apply(this, [content, data]);
1831
+ interpolate: function(content, data, engine, partials) {
1832
+ return this.engineFor(engine).apply(this, [content, data, partials]);
1708
1833
  },
1709
1834
 
1710
1835
  // Create and return a `Sammy.RenderContext` calling `render()` on it.
@@ -1720,8 +1845,8 @@
1720
1845
  // .appendTo('ul');
1721
1846
  // // appends the rendered content to $('ul')
1722
1847
  //
1723
- render: function(location, data, callback) {
1724
- return new Sammy.RenderContext(this).render(location, data, callback);
1848
+ render: function(location, data, callback, partials) {
1849
+ return new Sammy.RenderContext(this).render(location, data, callback, partials);
1725
1850
  },
1726
1851
 
1727
1852
  // Create and return a `Sammy.RenderContext` calling `renderEach()` on it.
@@ -1747,10 +1872,10 @@
1747
1872
  return new Sammy.RenderContext(this).load(location, options, callback);
1748
1873
  },
1749
1874
 
1750
- // `render()` the the `location` with `data` and then `swap()` the
1875
+ // `render()` the `location` with `data` and then `swap()` the
1751
1876
  // app's `$element` with the rendered content.
1752
- partial: function(location, data) {
1753
- return new Sammy.RenderContext(this).partial(location, data);
1877
+ partial: function(location, data, callback) {
1878
+ return new Sammy.RenderContext(this).partial(location, data, callback);
1754
1879
  },
1755
1880
 
1756
1881
  // create a new `Sammy.RenderContext` calling `send()` with an arbitrary
@@ -1767,22 +1892,37 @@
1767
1892
  // ### Example
1768
1893
  //
1769
1894
  // redirect('#/other/route');
1770
- // // equivilent to
1895
+ // // equivalent to
1771
1896
  // redirect('#', 'other', 'route');
1772
1897
  //
1773
1898
  redirect: function() {
1774
1899
  var to, args = _makeArray(arguments),
1775
- current_location = this.app.getLocation();
1776
- if (args.length > 1) {
1777
- args.unshift('/');
1778
- to = this.join.apply(this, args);
1900
+ current_location = this.app.getLocation(),
1901
+ l = args.length;
1902
+ if (l > 1) {
1903
+ var i = 0, paths = [], pairs = [], params = {}, has_params = false;
1904
+ for (; i < l; i++) {
1905
+ if (typeof args[i] == 'string') {
1906
+ paths.push(args[i]);
1907
+ } else {
1908
+ $.extend(params, args[i]);
1909
+ has_params = true;
1910
+ }
1911
+ }
1912
+ to = paths.join('/');
1913
+ if (has_params) {
1914
+ for (var k in params) {
1915
+ pairs.push(this.app._encodeFormPair(k, params[k]));
1916
+ }
1917
+ to += '?' + pairs.join('&');
1918
+ }
1779
1919
  } else {
1780
1920
  to = args[0];
1781
1921
  }
1782
1922
  this.trigger('redirect', {to: to});
1783
1923
  this.app.last_location = [this.verb, this.path];
1784
1924
  this.app.setLocation(to);
1785
- if (current_location == to) {
1925
+ if (new RegExp(to).test(current_location)) {
1786
1926
  this.app.trigger('location-changed');
1787
1927
  }
1788
1928
  },
@@ -1800,8 +1940,8 @@
1800
1940
  },
1801
1941
 
1802
1942
  // A shortcut to app's `swap()`
1803
- swap: function(contents) {
1804
- return this.app.swap(contents);
1943
+ swap: function(contents, callback) {
1944
+ return this.app.swap(contents, callback);
1805
1945
  },
1806
1946
 
1807
1947
  // Raises a possible `notFound()` error for the current path.