1 | // name: sammy |
---|
2 | // version: 0.6.2 |
---|
3 | /* |
---|
4 | Licensed to the Apache Software Foundation (ASF) under one or more |
---|
5 | contributor license agreements. See the NOTICE file distributed with |
---|
6 | this work for additional information regarding copyright ownership. |
---|
7 | The ASF licenses this file to You under the Apache License, Version 2.0 |
---|
8 | (the "License"); you may not use this file except in compliance with |
---|
9 | the License. You may obtain a copy of the License at |
---|
10 | |
---|
11 | http://www.apache.org/licenses/LICENSE-2.0 |
---|
12 | |
---|
13 | Unless required by applicable law or agreed to in writing, software |
---|
14 | distributed under the License is distributed on an "AS IS" BASIS, |
---|
15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
---|
16 | See the License for the specific language governing permissions and |
---|
17 | limitations under the License. |
---|
18 | */ |
---|
19 | |
---|
20 | (function($, window) { |
---|
21 | |
---|
22 | var Sammy, |
---|
23 | PATH_REPLACER = "([^\/]+)", |
---|
24 | PATH_NAME_MATCHER = /:([\w\d]+)/g, |
---|
25 | QUERY_STRING_MATCHER = /\?([^#]*)$/, |
---|
26 | // mainly for making `arguments` an Array |
---|
27 | _makeArray = function(nonarray) { return Array.prototype.slice.call(nonarray); }, |
---|
28 | // borrowed from jQuery |
---|
29 | _isFunction = function( obj ) { return Object.prototype.toString.call(obj) === "[object Function]"; }, |
---|
30 | _isArray = function( obj ) { return Object.prototype.toString.call(obj) === "[object Array]"; }, |
---|
31 | _decode = decodeURIComponent, |
---|
32 | _encode = encodeURIComponent, |
---|
33 | _escapeHTML = function(s) { |
---|
34 | return String(s).replace(/&(?!\w+;)/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); |
---|
35 | }, |
---|
36 | _routeWrapper = function(verb) { |
---|
37 | return function(path, callback) { return this.route.apply(this, [verb, path, callback]); }; |
---|
38 | }, |
---|
39 | _template_cache = {}, |
---|
40 | loggers = []; |
---|
41 | |
---|
42 | |
---|
43 | // `Sammy` (also aliased as $.sammy) is not only the namespace for a |
---|
44 | // number of prototypes, its also a top level method that allows for easy |
---|
45 | // creation/management of `Sammy.Application` instances. There are a |
---|
46 | // number of different forms for `Sammy()` but each returns an instance |
---|
47 | // of `Sammy.Application`. When a new instance is created using |
---|
48 | // `Sammy` it is added to an Object called `Sammy.apps`. This |
---|
49 | // provides for an easy way to get at existing Sammy applications. Only one |
---|
50 | // instance is allowed per `element_selector` so when calling |
---|
51 | // `Sammy('selector')` multiple times, the first time will create |
---|
52 | // the application and the following times will extend the application |
---|
53 | // already added to that selector. |
---|
54 | // |
---|
55 | // ### Example |
---|
56 | // |
---|
57 | // // returns the app at #main or a new app |
---|
58 | // Sammy('#main') |
---|
59 | // |
---|
60 | // // equivilent to "new Sammy.Application", except appends to apps |
---|
61 | // Sammy(); |
---|
62 | // Sammy(function() { ... }); |
---|
63 | // |
---|
64 | // // extends the app at '#main' with function. |
---|
65 | // Sammy('#main', function() { ... }); |
---|
66 | // |
---|
67 | Sammy = function() { |
---|
68 | var args = _makeArray(arguments), |
---|
69 | app, selector; |
---|
70 | Sammy.apps = Sammy.apps || {}; |
---|
71 | if (args.length === 0 || args[0] && _isFunction(args[0])) { // Sammy() |
---|
72 | return Sammy.apply(Sammy, ['body'].concat(args)); |
---|
73 | } else if (typeof (selector = args.shift()) == 'string') { // Sammy('#main') |
---|
74 | app = Sammy.apps[selector] || new Sammy.Application(); |
---|
75 | app.element_selector = selector; |
---|
76 | if (args.length > 0) { |
---|
77 | $.each(args, function(i, plugin) { |
---|
78 | app.use(plugin); |
---|
79 | }); |
---|
80 | } |
---|
81 | // if the selector changes make sure the refrence in Sammy.apps changes |
---|
82 | if (app.element_selector != selector) { |
---|
83 | delete Sammy.apps[selector]; |
---|
84 | } |
---|
85 | Sammy.apps[app.element_selector] = app; |
---|
86 | return app; |
---|
87 | } |
---|
88 | }; |
---|
89 | |
---|
90 | Sammy.VERSION = '0.6.2'; |
---|
91 | |
---|
92 | // Add to the global logger pool. Takes a function that accepts an |
---|
93 | // unknown number of arguments and should print them or send them somewhere |
---|
94 | // The first argument is always a timestamp. |
---|
95 | Sammy.addLogger = function(logger) { |
---|
96 | loggers.push(logger); |
---|
97 | }; |
---|
98 | |
---|
99 | // Sends a log message to each logger listed in the global |
---|
100 | // loggers pool. Can take any number of arguments. |
---|
101 | // Also prefixes the arguments with a timestamp. |
---|
102 | Sammy.log = function() { |
---|
103 | var args = _makeArray(arguments); |
---|
104 | args.unshift("[" + Date() + "]"); |
---|
105 | $.each(loggers, function(i, logger) { |
---|
106 | logger.apply(Sammy, args); |
---|
107 | }); |
---|
108 | }; |
---|
109 | |
---|
110 | if (typeof window.console != 'undefined') { |
---|
111 | if (_isFunction(window.console.log.apply)) { |
---|
112 | Sammy.addLogger(function() { |
---|
113 | window.console.log.apply(window.console, arguments); |
---|
114 | }); |
---|
115 | } else { |
---|
116 | Sammy.addLogger(function() { |
---|
117 | window.console.log(arguments); |
---|
118 | }); |
---|
119 | } |
---|
120 | } else if (typeof console != 'undefined') { |
---|
121 | Sammy.addLogger(function() { |
---|
122 | console.log.apply(console, arguments); |
---|
123 | }); |
---|
124 | } |
---|
125 | |
---|
126 | $.extend(Sammy, { |
---|
127 | makeArray: _makeArray, |
---|
128 | isFunction: _isFunction, |
---|
129 | isArray: _isArray |
---|
130 | }) |
---|
131 | |
---|
132 | // Sammy.Object is the base for all other Sammy classes. It provides some useful |
---|
133 | // functionality, including cloning, iterating, etc. |
---|
134 | Sammy.Object = function(obj) { // constructor |
---|
135 | return $.extend(this, obj || {}); |
---|
136 | }; |
---|
137 | |
---|
138 | $.extend(Sammy.Object.prototype, { |
---|
139 | |
---|
140 | // Escape HTML in string, use in templates to prevent script injection. |
---|
141 | // Also aliased as `h()` |
---|
142 | escapeHTML: _escapeHTML, |
---|
143 | h: _escapeHTML, |
---|
144 | |
---|
145 | // Returns a copy of the object with Functions removed. |
---|
146 | toHash: function() { |
---|
147 | var json = {}; |
---|
148 | $.each(this, function(k,v) { |
---|
149 | if (!_isFunction(v)) { |
---|
150 | json[k] = v; |
---|
151 | } |
---|
152 | }); |
---|
153 | return json; |
---|
154 | }, |
---|
155 | |
---|
156 | // Renders a simple HTML version of this Objects attributes. |
---|
157 | // Does not render functions. |
---|
158 | // For example. Given this Sammy.Object: |
---|
159 | // |
---|
160 | // var s = new Sammy.Object({first_name: 'Sammy', last_name: 'Davis Jr.'}); |
---|
161 | // s.toHTML() //=> '<strong>first_name</strong> Sammy<br /><strong>last_name</strong> Davis Jr.<br />' |
---|
162 | // |
---|
163 | toHTML: function() { |
---|
164 | var display = ""; |
---|
165 | $.each(this, function(k, v) { |
---|
166 | if (!_isFunction(v)) { |
---|
167 | display += "<strong>" + k + "</strong> " + v + "<br />"; |
---|
168 | } |
---|
169 | }); |
---|
170 | return display; |
---|
171 | }, |
---|
172 | |
---|
173 | // Returns an array of keys for this object. If `attributes_only` |
---|
174 | // is true will not return keys that map to a `function()` |
---|
175 | keys: function(attributes_only) { |
---|
176 | var keys = []; |
---|
177 | for (var property in this) { |
---|
178 | if (!_isFunction(this[property]) || !attributes_only) { |
---|
179 | keys.push(property); |
---|
180 | } |
---|
181 | } |
---|
182 | return keys; |
---|
183 | }, |
---|
184 | |
---|
185 | // Checks if the object has a value at `key` and that the value is not empty |
---|
186 | has: function(key) { |
---|
187 | return this[key] && $.trim(this[key].toString()) != ''; |
---|
188 | }, |
---|
189 | |
---|
190 | // convenience method to join as many arguments as you want |
---|
191 | // by the first argument - useful for making paths |
---|
192 | join: function() { |
---|
193 | var args = _makeArray(arguments); |
---|
194 | var delimiter = args.shift(); |
---|
195 | return args.join(delimiter); |
---|
196 | }, |
---|
197 | |
---|
198 | // Shortcut to Sammy.log |
---|
199 | log: function() { |
---|
200 | Sammy.log.apply(Sammy, arguments); |
---|
201 | }, |
---|
202 | |
---|
203 | // Returns a string representation of this object. |
---|
204 | // if `include_functions` is true, it will also toString() the |
---|
205 | // methods of this object. By default only prints the attributes. |
---|
206 | toString: function(include_functions) { |
---|
207 | var s = []; |
---|
208 | $.each(this, function(k, v) { |
---|
209 | if (!_isFunction(v) || include_functions) { |
---|
210 | s.push('"' + k + '": ' + v.toString()); |
---|
211 | } |
---|
212 | }); |
---|
213 | return "Sammy.Object: {" + s.join(',') + "}"; |
---|
214 | } |
---|
215 | }); |
---|
216 | |
---|
217 | // The HashLocationProxy is the default location proxy for all Sammy applications. |
---|
218 | // A location proxy is a prototype that conforms to a simple interface. The purpose |
---|
219 | // of a location proxy is to notify the Sammy.Application its bound to when the location |
---|
220 | // or 'external state' changes. The HashLocationProxy considers the state to be |
---|
221 | // changed when the 'hash' (window.location.hash / '#') changes. It does this in two |
---|
222 | // different ways depending on what browser you are using. The newest browsers |
---|
223 | // (IE, Safari > 4, FF >= 3.6) support a 'onhashchange' DOM event, thats fired whenever |
---|
224 | // the location.hash changes. In this situation the HashLocationProxy just binds |
---|
225 | // to this event and delegates it to the application. In the case of older browsers |
---|
226 | // a poller is set up to track changes to the hash. Unlike Sammy 0.3 or earlier, |
---|
227 | // the HashLocationProxy allows the poller to be a global object, eliminating the |
---|
228 | // need for multiple pollers even when thier are multiple apps on the page. |
---|
229 | Sammy.HashLocationProxy = function(app, run_interval_every) { |
---|
230 | this.app = app; |
---|
231 | // set is native to false and start the poller immediately |
---|
232 | this.is_native = false; |
---|
233 | this._startPolling(run_interval_every); |
---|
234 | }; |
---|
235 | |
---|
236 | Sammy.HashLocationProxy.prototype = { |
---|
237 | |
---|
238 | // bind the proxy events to the current app. |
---|
239 | bind: function() { |
---|
240 | var proxy = this, app = this.app; |
---|
241 | $(window).bind('hashchange.' + this.app.eventNamespace(), function(e, non_native) { |
---|
242 | // if we receive a native hash change event, set the proxy accordingly |
---|
243 | // and stop polling |
---|
244 | if (proxy.is_native === false && !non_native) { |
---|
245 | Sammy.log('native hash change exists, using'); |
---|
246 | proxy.is_native = true; |
---|
247 | window.clearInterval(Sammy.HashLocationProxy._interval); |
---|
248 | } |
---|
249 | app.trigger('location-changed'); |
---|
250 | }); |
---|
251 | if (!Sammy.HashLocationProxy._bindings) { |
---|
252 | Sammy.HashLocationProxy._bindings = 0; |
---|
253 | } |
---|
254 | Sammy.HashLocationProxy._bindings++; |
---|
255 | }, |
---|
256 | |
---|
257 | // unbind the proxy events from the current app |
---|
258 | unbind: function() { |
---|
259 | $(window).unbind('hashchange.' + this.app.eventNamespace()); |
---|
260 | Sammy.HashLocationProxy._bindings--; |
---|
261 | if (Sammy.HashLocationProxy._bindings <= 0) { |
---|
262 | window.clearInterval(Sammy.HashLocationProxy._interval); |
---|
263 | } |
---|
264 | }, |
---|
265 | |
---|
266 | // get the current location from the hash. |
---|
267 | getLocation: function() { |
---|
268 | // Bypass the `window.location.hash` attribute. If a question mark |
---|
269 | // appears in the hash IE6 will strip it and all of the following |
---|
270 | // characters from `window.location.hash`. |
---|
271 | var matches = window.location.toString().match(/^[^#]*(#.+)$/); |
---|
272 | return matches ? matches[1] : ''; |
---|
273 | }, |
---|
274 | |
---|
275 | // set the current location to `new_location` |
---|
276 | setLocation: function(new_location) { |
---|
277 | return (window.location = new_location); |
---|
278 | }, |
---|
279 | |
---|
280 | _startPolling: function(every) { |
---|
281 | // set up interval |
---|
282 | var proxy = this; |
---|
283 | if (!Sammy.HashLocationProxy._interval) { |
---|
284 | if (!every) { every = 10; } |
---|
285 | var hashCheck = function() { |
---|
286 | var current_location = proxy.getLocation(); |
---|
287 | if (!Sammy.HashLocationProxy._last_location || |
---|
288 | current_location != Sammy.HashLocationProxy._last_location) { |
---|
289 | window.setTimeout(function() { |
---|
290 | $(window).trigger('hashchange', [true]); |
---|
291 | }, 13); |
---|
292 | } |
---|
293 | Sammy.HashLocationProxy._last_location = current_location; |
---|
294 | }; |
---|
295 | hashCheck(); |
---|
296 | Sammy.HashLocationProxy._interval = window.setInterval(hashCheck, every); |
---|
297 | } |
---|
298 | } |
---|
299 | }; |
---|
300 | |
---|
301 | |
---|
302 | // Sammy.Application is the Base prototype for defining 'applications'. |
---|
303 | // An 'application' is a collection of 'routes' and bound events that is |
---|
304 | // attached to an element when `run()` is called. |
---|
305 | // The only argument an 'app_function' is evaluated within the context of the application. |
---|
306 | Sammy.Application = function(app_function) { |
---|
307 | var app = this; |
---|
308 | this.routes = {}; |
---|
309 | this.listeners = new Sammy.Object({}); |
---|
310 | this.arounds = []; |
---|
311 | this.befores = []; |
---|
312 | // generate a unique namespace |
---|
313 | this.namespace = (new Date()).getTime() + '-' + parseInt(Math.random() * 1000, 10); |
---|
314 | this.context_prototype = function() { Sammy.EventContext.apply(this, arguments); }; |
---|
315 | this.context_prototype.prototype = new Sammy.EventContext(); |
---|
316 | |
---|
317 | if (_isFunction(app_function)) { |
---|
318 | app_function.apply(this, [this]); |
---|
319 | } |
---|
320 | // set the location proxy if not defined to the default (HashLocationProxy) |
---|
321 | if (!this._location_proxy) { |
---|
322 | this.setLocationProxy(new Sammy.HashLocationProxy(this, this.run_interval_every)); |
---|
323 | } |
---|
324 | if (this.debug) { |
---|
325 | this.bindToAllEvents(function(e, data) { |
---|
326 | app.log(app.toString(), e.cleaned_type, data || {}); |
---|
327 | }); |
---|
328 | } |
---|
329 | }; |
---|
330 | |
---|
331 | Sammy.Application.prototype = $.extend({}, Sammy.Object.prototype, { |
---|
332 | |
---|
333 | // the four route verbs |
---|
334 | ROUTE_VERBS: ['get','post','put','delete'], |
---|
335 | |
---|
336 | // An array of the default events triggered by the |
---|
337 | // application during its lifecycle |
---|
338 | APP_EVENTS: ['run', |
---|
339 | 'unload', |
---|
340 | 'lookup-route', |
---|
341 | 'run-route', |
---|
342 | 'route-found', |
---|
343 | 'event-context-before', |
---|
344 | 'event-context-after', |
---|
345 | 'changed', |
---|
346 | 'error', |
---|
347 | 'check-form-submission', |
---|
348 | 'redirect', |
---|
349 | 'location-changed'], |
---|
350 | |
---|
351 | _last_route: null, |
---|
352 | _location_proxy: null, |
---|
353 | _running: false, |
---|
354 | |
---|
355 | // Defines what element the application is bound to. Provide a selector |
---|
356 | // (parseable by `jQuery()`) and this will be used by `$element()` |
---|
357 | element_selector: 'body', |
---|
358 | |
---|
359 | // When set to true, logs all of the default events using `log()` |
---|
360 | debug: false, |
---|
361 | |
---|
362 | // When set to true, and the error() handler is not overriden, will actually |
---|
363 | // raise JS errors in routes (500) and when routes can't be found (404) |
---|
364 | raise_errors: false, |
---|
365 | |
---|
366 | // The time in milliseconds that the URL is queried for changes |
---|
367 | run_interval_every: 50, |
---|
368 | |
---|
369 | // The default template engine to use when using `partial()` in an |
---|
370 | // `EventContext`. `template_engine` can either be a string that |
---|
371 | // corresponds to the name of a method/helper on EventContext or it can be a function |
---|
372 | // that takes two arguments, the content of the unrendered partial and an optional |
---|
373 | // JS object that contains interpolation data. Template engine is only called/refered |
---|
374 | // to if the extension of the partial is null or unknown. See `partial()` |
---|
375 | // for more information |
---|
376 | template_engine: null, |
---|
377 | |
---|
378 | // //=> Sammy.Application: body |
---|
379 | toString: function() { |
---|
380 | return 'Sammy.Application:' + this.element_selector; |
---|
381 | }, |
---|
382 | |
---|
383 | // returns a jQuery object of the Applications bound element. |
---|
384 | $element: function(selector) { |
---|
385 | return selector ? $(this.element_selector).find(selector) : $(this.element_selector); |
---|
386 | }, |
---|
387 | |
---|
388 | // `use()` is the entry point for including Sammy plugins. |
---|
389 | // The first argument to use should be a function() that is evaluated |
---|
390 | // in the context of the current application, just like the `app_function` |
---|
391 | // argument to the `Sammy.Application` constructor. |
---|
392 | // |
---|
393 | // Any additional arguments are passed to the app function sequentially. |
---|
394 | // |
---|
395 | // For much more detail about plugins, check out: |
---|
396 | // http://code.quirkey.com/sammy/doc/plugins.html |
---|
397 | // |
---|
398 | // ### Example |
---|
399 | // |
---|
400 | // var MyPlugin = function(app, prepend) { |
---|
401 | // |
---|
402 | // this.helpers({ |
---|
403 | // myhelper: function(text) { |
---|
404 | // alert(prepend + " " + text); |
---|
405 | // } |
---|
406 | // }); |
---|
407 | // |
---|
408 | // }; |
---|
409 | // |
---|
410 | // var app = $.sammy(function() { |
---|
411 | // |
---|
412 | // this.use(MyPlugin, 'This is my plugin'); |
---|
413 | // |
---|
414 | // this.get('#/', function() { |
---|
415 | // this.myhelper('and dont you forget it!'); |
---|
416 | // //=> Alerts: This is my plugin and dont you forget it! |
---|
417 | // }); |
---|
418 | // |
---|
419 | // }); |
---|
420 | // |
---|
421 | // If plugin is passed as a string it assumes your are trying to load |
---|
422 | // Sammy."Plugin". This is the prefered way of loading core Sammy plugins |
---|
423 | // as it allows for better error-messaging. |
---|
424 | // |
---|
425 | // ### Example |
---|
426 | // |
---|
427 | // $.sammy(function() { |
---|
428 | // this.use('Mustache'); //=> Sammy.Mustache |
---|
429 | // this.use('Storage'); //=> Sammy.Storage |
---|
430 | // }); |
---|
431 | // |
---|
432 | use: function() { |
---|
433 | // flatten the arguments |
---|
434 | var args = _makeArray(arguments), |
---|
435 | plugin = args.shift(), |
---|
436 | plugin_name = plugin || ''; |
---|
437 | try { |
---|
438 | args.unshift(this); |
---|
439 | if (typeof plugin == 'string') { |
---|
440 | plugin_name = 'Sammy.' + plugin; |
---|
441 | plugin = Sammy[plugin]; |
---|
442 | } |
---|
443 | plugin.apply(this, args); |
---|
444 | } catch(e) { |
---|
445 | if (typeof plugin === 'undefined') { |
---|
446 | this.error("Plugin Error: called use() but plugin (" + plugin_name.toString() + ") is not defined", e); |
---|
447 | } else if (!_isFunction(plugin)) { |
---|
448 | this.error("Plugin Error: called use() but '" + plugin_name.toString() + "' is not a function", e); |
---|
449 | } else { |
---|
450 | this.error("Plugin Error", e); |
---|
451 | } |
---|
452 | } |
---|
453 | return this; |
---|
454 | }, |
---|
455 | |
---|
456 | // Sets the location proxy for the current app. By default this is set to |
---|
457 | // a new `Sammy.HashLocationProxy` on initialization. However, you can set |
---|
458 | // the location_proxy inside you're app function to give your app a custom |
---|
459 | // location mechanism. See `Sammy.HashLocationProxy` and `Sammy.DataLocationProxy` |
---|
460 | // for examples. |
---|
461 | // |
---|
462 | // `setLocationProxy()` takes an initialized location proxy. |
---|
463 | // |
---|
464 | // ### Example |
---|
465 | // |
---|
466 | // // to bind to data instead of the default hash; |
---|
467 | // var app = $.sammy(function() { |
---|
468 | // this.setLocationProxy(new Sammy.DataLocationProxy(this)); |
---|
469 | // }); |
---|
470 | // |
---|
471 | setLocationProxy: function(new_proxy) { |
---|
472 | var original_proxy = this._location_proxy; |
---|
473 | this._location_proxy = new_proxy; |
---|
474 | if (this.isRunning()) { |
---|
475 | if (original_proxy) { |
---|
476 | // if there is already a location proxy, unbind it. |
---|
477 | original_proxy.unbind(); |
---|
478 | } |
---|
479 | this._location_proxy.bind(); |
---|
480 | } |
---|
481 | }, |
---|
482 | |
---|
483 | // `route()` is the main method for defining routes within an application. |
---|
484 | // For great detail on routes, check out: http://code.quirkey.com/sammy/doc/routes.html |
---|
485 | // |
---|
486 | // This method also has aliases for each of the different verbs (eg. `get()`, `post()`, etc.) |
---|
487 | // |
---|
488 | // ### Arguments |
---|
489 | // |
---|
490 | // * `verb` A String in the set of ROUTE_VERBS or 'any'. 'any' will add routes for each |
---|
491 | // of the ROUTE_VERBS. If only two arguments are passed, |
---|
492 | // the first argument is the path, the second is the callback and the verb |
---|
493 | // is assumed to be 'any'. |
---|
494 | // * `path` A Regexp or a String representing the path to match to invoke this verb. |
---|
495 | // * `callback` A Function that is called/evaluated whent the route is run see: `runRoute()`. |
---|
496 | // It is also possible to pass a string as the callback, which is looked up as the name |
---|
497 | // of a method on the application. |
---|
498 | // |
---|
499 | route: function(verb, path, callback) { |
---|
500 | var app = this, param_names = [], add_route, path_match; |
---|
501 | |
---|
502 | // if the method signature is just (path, callback) |
---|
503 | // assume the verb is 'any' |
---|
504 | if (!callback && _isFunction(path)) { |
---|
505 | path = verb; |
---|
506 | callback = path; |
---|
507 | verb = 'any'; |
---|
508 | } |
---|
509 | |
---|
510 | verb = verb.toLowerCase(); // ensure verb is lower case |
---|
511 | |
---|
512 | // if path is a string turn it into a regex |
---|
513 | if (path.constructor == String) { |
---|
514 | |
---|
515 | // Needs to be explicitly set because IE will maintain the index unless NULL is returned, |
---|
516 | // which means that with two consecutive routes that contain params, the second set of params will not be found and end up in splat instead of params |
---|
517 | // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/RegExp/lastIndex |
---|
518 | PATH_NAME_MATCHER.lastIndex = 0; |
---|
519 | |
---|
520 | // find the names |
---|
521 | while ((path_match = PATH_NAME_MATCHER.exec(path)) !== null) { |
---|
522 | param_names.push(path_match[1]); |
---|
523 | } |
---|
524 | // replace with the path replacement |
---|
525 | path = new RegExp("^" + path.replace(PATH_NAME_MATCHER, PATH_REPLACER) + "$"); |
---|
526 | } |
---|
527 | // lookup callback |
---|
528 | if (typeof callback == 'string') { |
---|
529 | callback = app[callback]; |
---|
530 | } |
---|
531 | |
---|
532 | add_route = function(with_verb) { |
---|
533 | var r = {verb: with_verb, path: path, callback: callback, param_names: param_names}; |
---|
534 | // add route to routes array |
---|
535 | app.routes[with_verb] = app.routes[with_verb] || []; |
---|
536 | // place routes in order of definition |
---|
537 | app.routes[with_verb].push(r); |
---|
538 | }; |
---|
539 | |
---|
540 | if (verb === 'any') { |
---|
541 | $.each(this.ROUTE_VERBS, function(i, v) { add_route(v); }); |
---|
542 | } else { |
---|
543 | add_route(verb); |
---|
544 | } |
---|
545 | |
---|
546 | // return the app |
---|
547 | return this; |
---|
548 | }, |
---|
549 | |
---|
550 | // Alias for route('get', ...) |
---|
551 | get: _routeWrapper('get'), |
---|
552 | |
---|
553 | // Alias for route('post', ...) |
---|
554 | post: _routeWrapper('post'), |
---|
555 | |
---|
556 | // Alias for route('put', ...) |
---|
557 | put: _routeWrapper('put'), |
---|
558 | |
---|
559 | // Alias for route('delete', ...) |
---|
560 | del: _routeWrapper('delete'), |
---|
561 | |
---|
562 | // Alias for route('any', ...) |
---|
563 | any: _routeWrapper('any'), |
---|
564 | |
---|
565 | // `mapRoutes` takes an array of arrays, each array being passed to route() |
---|
566 | // as arguments, this allows for mass definition of routes. Another benefit is |
---|
567 | // this makes it possible/easier to load routes via remote JSON. |
---|
568 | // |
---|
569 | // ### Example |
---|
570 | // |
---|
571 | // var app = $.sammy(function() { |
---|
572 | // |
---|
573 | // this.mapRoutes([ |
---|
574 | // ['get', '#/', function() { this.log('index'); }], |
---|
575 | // // strings in callbacks are looked up as methods on the app |
---|
576 | // ['post', '#/create', 'addUser'], |
---|
577 | // // No verb assumes 'any' as the verb |
---|
578 | // [/dowhatever/, function() { this.log(this.verb, this.path)}]; |
---|
579 | // ]); |
---|
580 | // }) |
---|
581 | // |
---|
582 | mapRoutes: function(route_array) { |
---|
583 | var app = this; |
---|
584 | $.each(route_array, function(i, route_args) { |
---|
585 | app.route.apply(app, route_args); |
---|
586 | }); |
---|
587 | return this; |
---|
588 | }, |
---|
589 | |
---|
590 | // A unique event namespace defined per application. |
---|
591 | // All events bound with `bind()` are automatically bound within this space. |
---|
592 | eventNamespace: function() { |
---|
593 | return ['sammy-app', this.namespace].join('-'); |
---|
594 | }, |
---|
595 | |
---|
596 | // Works just like `jQuery.fn.bind()` with a couple noteable differences. |
---|
597 | // |
---|
598 | // * It binds all events to the application element |
---|
599 | // * All events are bound within the `eventNamespace()` |
---|
600 | // * Events are not actually bound until the application is started with `run()` |
---|
601 | // * callbacks are evaluated within the context of a Sammy.EventContext |
---|
602 | // |
---|
603 | // See http://code.quirkey.com/sammy/docs/events.html for more info. |
---|
604 | // |
---|
605 | bind: function(name, data, callback) { |
---|
606 | var app = this; |
---|
607 | // build the callback |
---|
608 | // if the arity is 2, callback is the second argument |
---|
609 | if (typeof callback == 'undefined') { callback = data; } |
---|
610 | var listener_callback = function() { |
---|
611 | // pull off the context from the arguments to the callback |
---|
612 | var e, context, data; |
---|
613 | e = arguments[0]; |
---|
614 | data = arguments[1]; |
---|
615 | if (data && data.context) { |
---|
616 | context = data.context; |
---|
617 | delete data.context; |
---|
618 | } else { |
---|
619 | context = new app.context_prototype(app, 'bind', e.type, data, e.target); |
---|
620 | } |
---|
621 | e.cleaned_type = e.type.replace(app.eventNamespace(), ''); |
---|
622 | callback.apply(context, [e, data]); |
---|
623 | }; |
---|
624 | |
---|
625 | // it could be that the app element doesnt exist yet |
---|
626 | // so attach to the listeners array and then run() |
---|
627 | // will actually bind the event. |
---|
628 | if (!this.listeners[name]) { this.listeners[name] = []; } |
---|
629 | this.listeners[name].push(listener_callback); |
---|
630 | if (this.isRunning()) { |
---|
631 | // if the app is running |
---|
632 | // *actually* bind the event to the app element |
---|
633 | this._listen(name, listener_callback); |
---|
634 | } |
---|
635 | return this; |
---|
636 | }, |
---|
637 | |
---|
638 | // Triggers custom events defined with `bind()` |
---|
639 | // |
---|
640 | // ### Arguments |
---|
641 | // |
---|
642 | // * `name` The name of the event. Automatically prefixed with the `eventNamespace()` |
---|
643 | // * `data` An optional Object that can be passed to the bound callback. |
---|
644 | // * `context` An optional context/Object in which to execute the bound callback. |
---|
645 | // If no context is supplied a the context is a new `Sammy.EventContext` |
---|
646 | // |
---|
647 | trigger: function(name, data) { |
---|
648 | this.$element().trigger([name, this.eventNamespace()].join('.'), [data]); |
---|
649 | return this; |
---|
650 | }, |
---|
651 | |
---|
652 | // Reruns the current route |
---|
653 | refresh: function() { |
---|
654 | this.last_location = null; |
---|
655 | this.trigger('location-changed'); |
---|
656 | return this; |
---|
657 | }, |
---|
658 | |
---|
659 | // Takes a single callback that is pushed on to a stack. |
---|
660 | // Before any route is run, the callbacks are evaluated in order within |
---|
661 | // the current `Sammy.EventContext` |
---|
662 | // |
---|
663 | // If any of the callbacks explicitly return false, execution of any |
---|
664 | // further callbacks and the route itself is halted. |
---|
665 | // |
---|
666 | // You can also provide a set of options that will define when to run this |
---|
667 | // before based on the route it proceeds. |
---|
668 | // |
---|
669 | // ### Example |
---|
670 | // |
---|
671 | // var app = $.sammy(function() { |
---|
672 | // |
---|
673 | // // will run at #/route but not at #/ |
---|
674 | // this.before('#/route', function() { |
---|
675 | // //... |
---|
676 | // }); |
---|
677 | // |
---|
678 | // // will run at #/ but not at #/route |
---|
679 | // this.before({except: {path: '#/route'}}, function() { |
---|
680 | // this.log('not before #/route'); |
---|
681 | // }); |
---|
682 | // |
---|
683 | // this.get('#/', function() {}); |
---|
684 | // |
---|
685 | // this.get('#/route', function() {}); |
---|
686 | // |
---|
687 | // }); |
---|
688 | // |
---|
689 | // See `contextMatchesOptions()` for a full list of supported options |
---|
690 | // |
---|
691 | before: function(options, callback) { |
---|
692 | if (_isFunction(options)) { |
---|
693 | callback = options; |
---|
694 | options = {}; |
---|
695 | } |
---|
696 | this.befores.push([options, callback]); |
---|
697 | return this; |
---|
698 | }, |
---|
699 | |
---|
700 | // A shortcut for binding a callback to be run after a route is executed. |
---|
701 | // After callbacks have no guarunteed order. |
---|
702 | after: function(callback) { |
---|
703 | return this.bind('event-context-after', callback); |
---|
704 | }, |
---|
705 | |
---|
706 | |
---|
707 | // Adds an around filter to the application. around filters are functions |
---|
708 | // that take a single argument `callback` which is the entire route |
---|
709 | // execution path wrapped up in a closure. This means you can decide whether |
---|
710 | // or not to proceed with execution by not invoking `callback` or, |
---|
711 | // more usefuly wrapping callback inside the result of an asynchronous execution. |
---|
712 | // |
---|
713 | // ### Example |
---|
714 | // |
---|
715 | // The most common use case for around() is calling a _possibly_ async function |
---|
716 | // and executing the route within the functions callback: |
---|
717 | // |
---|
718 | // var app = $.sammy(function() { |
---|
719 | // |
---|
720 | // var current_user = false; |
---|
721 | // |
---|
722 | // function checkLoggedIn(callback) { |
---|
723 | // // /session returns a JSON representation of the logged in user |
---|
724 | // // or an empty object |
---|
725 | // if (!current_user) { |
---|
726 | // $.getJSON('/session', function(json) { |
---|
727 | // if (json.login) { |
---|
728 | // // show the user as logged in |
---|
729 | // current_user = json; |
---|
730 | // // execute the route path |
---|
731 | // callback(); |
---|
732 | // } else { |
---|
733 | // // show the user as not logged in |
---|
734 | // current_user = false; |
---|
735 | // // the context of aroundFilters is an EventContext |
---|
736 | // this.redirect('#/login'); |
---|
737 | // } |
---|
738 | // }); |
---|
739 | // } else { |
---|
740 | // // execute the route path |
---|
741 | // callback(); |
---|
742 | // } |
---|
743 | // }; |
---|
744 | // |
---|
745 | // this.around(checkLoggedIn); |
---|
746 | // |
---|
747 | // }); |
---|
748 | // |
---|
749 | around: function(callback) { |
---|
750 | this.arounds.push(callback); |
---|
751 | return this; |
---|
752 | }, |
---|
753 | |
---|
754 | // Returns `true` if the current application is running. |
---|
755 | isRunning: function() { |
---|
756 | return this._running; |
---|
757 | }, |
---|
758 | |
---|
759 | // Helpers extends the EventContext prototype specific to this app. |
---|
760 | // This allows you to define app specific helper functions that can be used |
---|
761 | // whenever you're inside of an event context (templates, routes, bind). |
---|
762 | // |
---|
763 | // ### Example |
---|
764 | // |
---|
765 | // var app = $.sammy(function() { |
---|
766 | // |
---|
767 | // helpers({ |
---|
768 | // upcase: function(text) { |
---|
769 | // return text.toString().toUpperCase(); |
---|
770 | // } |
---|
771 | // }); |
---|
772 | // |
---|
773 | // get('#/', function() { with(this) { |
---|
774 | // // inside of this context I can use the helpers |
---|
775 | // $('#main').html(upcase($('#main').text()); |
---|
776 | // }}); |
---|
777 | // |
---|
778 | // }); |
---|
779 | // |
---|
780 | // |
---|
781 | // ### Arguments |
---|
782 | // |
---|
783 | // * `extensions` An object collection of functions to extend the context. |
---|
784 | // |
---|
785 | helpers: function(extensions) { |
---|
786 | $.extend(this.context_prototype.prototype, extensions); |
---|
787 | return this; |
---|
788 | }, |
---|
789 | |
---|
790 | // Helper extends the event context just like `helpers()` but does it |
---|
791 | // a single method at a time. This is especially useful for dynamically named |
---|
792 | // helpers |
---|
793 | // |
---|
794 | // ### Example |
---|
795 | // |
---|
796 | // // Trivial example that adds 3 helper methods to the context dynamically |
---|
797 | // var app = $.sammy(function(app) { |
---|
798 | // |
---|
799 | // $.each([1,2,3], function(i, num) { |
---|
800 | // app.helper('helper' + num, function() { |
---|
801 | // this.log("I'm helper number " + num); |
---|
802 | // }); |
---|
803 | // }); |
---|
804 | // |
---|
805 | // this.get('#/', function() { |
---|
806 | // this.helper2(); //=> I'm helper number 2 |
---|
807 | // }); |
---|
808 | // }); |
---|
809 | // |
---|
810 | // ### Arguments |
---|
811 | // |
---|
812 | // * `name` The name of the method |
---|
813 | // * `method` The function to be added to the prototype at `name` |
---|
814 | // |
---|
815 | helper: function(name, method) { |
---|
816 | this.context_prototype.prototype[name] = method; |
---|
817 | return this; |
---|
818 | }, |
---|
819 | |
---|
820 | // Actually starts the application's lifecycle. `run()` should be invoked |
---|
821 | // within a document.ready block to ensure the DOM exists before binding events, etc. |
---|
822 | // |
---|
823 | // ### Example |
---|
824 | // |
---|
825 | // var app = $.sammy(function() { ... }); // your application |
---|
826 | // $(function() { // document.ready |
---|
827 | // app.run(); |
---|
828 | // }); |
---|
829 | // |
---|
830 | // ### Arguments |
---|
831 | // |
---|
832 | // * `start_url` Optionally, a String can be passed which the App will redirect to |
---|
833 | // after the events/routes have been bound. |
---|
834 | run: function(start_url) { |
---|
835 | if (this.isRunning()) { return false; } |
---|
836 | var app = this; |
---|
837 | |
---|
838 | // actually bind all the listeners |
---|
839 | $.each(this.listeners.toHash(), function(name, callbacks) { |
---|
840 | $.each(callbacks, function(i, listener_callback) { |
---|
841 | app._listen(name, listener_callback); |
---|
842 | }); |
---|
843 | }); |
---|
844 | |
---|
845 | this.trigger('run', {start_url: start_url}); |
---|
846 | this._running = true; |
---|
847 | // set last location |
---|
848 | this.last_location = null; |
---|
849 | if (this.getLocation() == '' && typeof start_url != 'undefined') { |
---|
850 | this.setLocation(start_url); |
---|
851 | } |
---|
852 | // check url |
---|
853 | this._checkLocation(); |
---|
854 | this._location_proxy.bind(); |
---|
855 | this.bind('location-changed', function() { |
---|
856 | app._checkLocation(); |
---|
857 | }); |
---|
858 | |
---|
859 | // bind to submit to capture post/put/delete routes |
---|
860 | /* |
---|
861 | this.bind('submit', function(e) { |
---|
862 | var returned = app._checkFormSubmission($(e.target).closest('form')); |
---|
863 | return (returned === false) ? e.preventDefault() : false; |
---|
864 | }); |
---|
865 | */ |
---|
866 | |
---|
867 | // bind unload to body unload |
---|
868 | $(window).bind('beforeunload', function() { |
---|
869 | app.unload(); |
---|
870 | }); |
---|
871 | |
---|
872 | // trigger html changed |
---|
873 | return this.trigger('changed'); |
---|
874 | }, |
---|
875 | |
---|
876 | // The opposite of `run()`, un-binds all event listeners and intervals |
---|
877 | // `run()` Automaticaly binds a `onunload` event to run this when |
---|
878 | // the document is closed. |
---|
879 | unload: function() { |
---|
880 | if (!this.isRunning()) { return false; } |
---|
881 | var app = this; |
---|
882 | this.trigger('unload'); |
---|
883 | // clear interval |
---|
884 | this._location_proxy.unbind(); |
---|
885 | // unbind form submits |
---|
886 | this.$element().unbind('submit').removeClass(app.eventNamespace()); |
---|
887 | // unbind all events |
---|
888 | $.each(this.listeners.toHash() , function(name, listeners) { |
---|
889 | $.each(listeners, function(i, listener_callback) { |
---|
890 | app._unlisten(name, listener_callback); |
---|
891 | }); |
---|
892 | }); |
---|
893 | this._running = false; |
---|
894 | return this; |
---|
895 | }, |
---|
896 | |
---|
897 | // Will bind a single callback function to every event that is already |
---|
898 | // being listened to in the app. This includes all the `APP_EVENTS` |
---|
899 | // as well as any custom events defined with `bind()`. |
---|
900 | // |
---|
901 | // Used internally for debug logging. |
---|
902 | bindToAllEvents: function(callback) { |
---|
903 | var app = this; |
---|
904 | // bind to the APP_EVENTS first |
---|
905 | $.each(this.APP_EVENTS, function(i, e) { |
---|
906 | app.bind(e, callback); |
---|
907 | }); |
---|
908 | // next, bind to listener names (only if they dont exist in APP_EVENTS) |
---|
909 | $.each(this.listeners.keys(true), function(i, name) { |
---|
910 | if (app.APP_EVENTS.indexOf(name) == -1) { |
---|
911 | app.bind(name, callback); |
---|
912 | } |
---|
913 | }); |
---|
914 | return this; |
---|
915 | }, |
---|
916 | |
---|
917 | // Returns a copy of the given path with any query string after the hash |
---|
918 | // removed. |
---|
919 | routablePath: function(path) { |
---|
920 | return path.replace(QUERY_STRING_MATCHER, ''); |
---|
921 | }, |
---|
922 | |
---|
923 | // Given a verb and a String path, will return either a route object or false |
---|
924 | // if a matching route can be found within the current defined set. |
---|
925 | lookupRoute: function(verb, path) { |
---|
926 | var app = this, routed = false; |
---|
927 | this.trigger('lookup-route', {verb: verb, path: path}); |
---|
928 | if (typeof this.routes[verb] != 'undefined') { |
---|
929 | $.each(this.routes[verb], function(i, route) { |
---|
930 | if (app.routablePath(path).match(route.path)) { |
---|
931 | routed = route; |
---|
932 | return false; |
---|
933 | } |
---|
934 | }); |
---|
935 | } |
---|
936 | return routed; |
---|
937 | }, |
---|
938 | |
---|
939 | // First, invokes `lookupRoute()` and if a route is found, parses the |
---|
940 | // possible URL params and then invokes the route's callback within a new |
---|
941 | // `Sammy.EventContext`. If the route can not be found, it calls |
---|
942 | // `notFound()`. If `raise_errors` is set to `true` and |
---|
943 | // the `error()` has not been overriden, it will throw an actual JS |
---|
944 | // error. |
---|
945 | // |
---|
946 | // You probably will never have to call this directly. |
---|
947 | // |
---|
948 | // ### Arguments |
---|
949 | // |
---|
950 | // * `verb` A String for the verb. |
---|
951 | // * `path` A String path to lookup. |
---|
952 | // * `params` An Object of Params pulled from the URI or passed directly. |
---|
953 | // |
---|
954 | // ### Returns |
---|
955 | // |
---|
956 | // Either returns the value returned by the route callback or raises a 404 Not Found error. |
---|
957 | // |
---|
958 | runRoute: function(verb, path, params, target) { |
---|
959 | var app = this, |
---|
960 | route = this.lookupRoute(verb, path), |
---|
961 | context, |
---|
962 | wrapped_route, |
---|
963 | arounds, |
---|
964 | around, |
---|
965 | befores, |
---|
966 | before, |
---|
967 | callback_args, |
---|
968 | path_params, |
---|
969 | final_returned; |
---|
970 | |
---|
971 | this.log('runRoute', [verb, path].join(' ')); |
---|
972 | this.trigger('run-route', {verb: verb, path: path, params: params}); |
---|
973 | if (typeof params == 'undefined') { params = {}; } |
---|
974 | |
---|
975 | $.extend(params, this._parseQueryString(path)); |
---|
976 | |
---|
977 | if (route) { |
---|
978 | this.trigger('route-found', {route: route}); |
---|
979 | // pull out the params from the path |
---|
980 | if ((path_params = route.path.exec(this.routablePath(path))) !== null) { |
---|
981 | // first match is the full path |
---|
982 | path_params.shift(); |
---|
983 | // for each of the matches |
---|
984 | $.each(path_params, function(i, param) { |
---|
985 | // if theres a matching param name |
---|
986 | if (route.param_names[i]) { |
---|
987 | // set the name to the match |
---|
988 | params[route.param_names[i]] = _decode(param); |
---|
989 | } else { |
---|
990 | // initialize 'splat' |
---|
991 | if (!params.splat) { params.splat = []; } |
---|
992 | params.splat.push(_decode(param)); |
---|
993 | } |
---|
994 | }); |
---|
995 | } |
---|
996 | |
---|
997 | // set event context |
---|
998 | context = new this.context_prototype(this, verb, path, params, target); |
---|
999 | // ensure arrays |
---|
1000 | arounds = this.arounds.slice(0); |
---|
1001 | befores = this.befores.slice(0); |
---|
1002 | // set the callback args to the context + contents of the splat |
---|
1003 | callback_args = [context].concat(params.splat); |
---|
1004 | // wrap the route up with the before filters |
---|
1005 | wrapped_route = function() { |
---|
1006 | var returned; |
---|
1007 | while (befores.length > 0) { |
---|
1008 | before = befores.shift(); |
---|
1009 | // check the options |
---|
1010 | if (app.contextMatchesOptions(context, before[0])) { |
---|
1011 | returned = before[1].apply(context, [context]); |
---|
1012 | if (returned === false) { return false; } |
---|
1013 | } |
---|
1014 | } |
---|
1015 | app.last_route = route; |
---|
1016 | context.trigger('event-context-before', {context: context}); |
---|
1017 | returned = route.callback.apply(context, callback_args); |
---|
1018 | context.trigger('event-context-after', {context: context}); |
---|
1019 | return returned; |
---|
1020 | }; |
---|
1021 | $.each(arounds.reverse(), function(i, around) { |
---|
1022 | var last_wrapped_route = wrapped_route; |
---|
1023 | wrapped_route = function() { return around.apply(context, [last_wrapped_route]); }; |
---|
1024 | }); |
---|
1025 | try { |
---|
1026 | final_returned = wrapped_route(); |
---|
1027 | } catch(e) { |
---|
1028 | this.error(['500 Error', verb, path].join(' '), e); |
---|
1029 | } |
---|
1030 | return final_returned; |
---|
1031 | } else { |
---|
1032 | return this.notFound(verb, path); |
---|
1033 | } |
---|
1034 | }, |
---|
1035 | |
---|
1036 | // Matches an object of options against an `EventContext` like object that |
---|
1037 | // contains `path` and `verb` attributes. Internally Sammy uses this |
---|
1038 | // for matching `before()` filters against specific options. You can set the |
---|
1039 | // object to _only_ match certain paths or verbs, or match all paths or verbs _except_ |
---|
1040 | // those that match the options. |
---|
1041 | // |
---|
1042 | // ### Example |
---|
1043 | // |
---|
1044 | // var app = $.sammy(), |
---|
1045 | // context = {verb: 'get', path: '#/mypath'}; |
---|
1046 | // |
---|
1047 | // // match against a path string |
---|
1048 | // app.contextMatchesOptions(context, '#/mypath'); //=> true |
---|
1049 | // app.contextMatchesOptions(context, '#/otherpath'); //=> false |
---|
1050 | // // equivilent to |
---|
1051 | // app.contextMatchesOptions(context, {only: {path:'#/mypath'}}); //=> true |
---|
1052 | // app.contextMatchesOptions(context, {only: {path:'#/otherpath'}}); //=> false |
---|
1053 | // // match against a path regexp |
---|
1054 | // app.contextMatchesOptions(context, /path/); //=> true |
---|
1055 | // app.contextMatchesOptions(context, /^path/); //=> false |
---|
1056 | // // match only a verb |
---|
1057 | // app.contextMatchesOptions(context, {only: {verb:'get'}}); //=> true |
---|
1058 | // app.contextMatchesOptions(context, {only: {verb:'post'}}); //=> false |
---|
1059 | // // match all except a verb |
---|
1060 | // app.contextMatchesOptions(context, {except: {verb:'post'}}); //=> true |
---|
1061 | // app.contextMatchesOptions(context, {except: {verb:'get'}}); //=> false |
---|
1062 | // // match all except a path |
---|
1063 | // app.contextMatchesOptions(context, {except: {path:'#/otherpath'}}); //=> true |
---|
1064 | // app.contextMatchesOptions(context, {except: {path:'#/mypath'}}); //=> false |
---|
1065 | // |
---|
1066 | contextMatchesOptions: function(context, match_options, positive) { |
---|
1067 | // empty options always match |
---|
1068 | var options = match_options; |
---|
1069 | if (typeof options === 'undefined' || options == {}) { |
---|
1070 | return true; |
---|
1071 | } |
---|
1072 | if (typeof positive === 'undefined') { |
---|
1073 | positive = true; |
---|
1074 | } |
---|
1075 | // normalize options |
---|
1076 | if (typeof options === 'string' || _isFunction(options.test)) { |
---|
1077 | options = {path: options}; |
---|
1078 | } |
---|
1079 | if (options.only) { |
---|
1080 | return this.contextMatchesOptions(context, options.only, true); |
---|
1081 | } else if (options.except) { |
---|
1082 | return this.contextMatchesOptions(context, options.except, false); |
---|
1083 | } |
---|
1084 | var path_matched = true, verb_matched = true; |
---|
1085 | if (options.path) { |
---|
1086 | // wierd regexp test |
---|
1087 | if (_isFunction(options.path.test)) { |
---|
1088 | path_matched = options.path.test(context.path); |
---|
1089 | } else { |
---|
1090 | path_matched = (options.path.toString() === context.path); |
---|
1091 | } |
---|
1092 | } |
---|
1093 | if (options.verb) { |
---|
1094 | verb_matched = options.verb === context.verb; |
---|
1095 | } |
---|
1096 | return positive ? (verb_matched && path_matched) : !(verb_matched && path_matched); |
---|
1097 | }, |
---|
1098 | |
---|
1099 | |
---|
1100 | // Delegates to the `location_proxy` to get the current location. |
---|
1101 | // See `Sammy.HashLocationProxy` for more info on location proxies. |
---|
1102 | getLocation: function() { |
---|
1103 | return this._location_proxy.getLocation(); |
---|
1104 | }, |
---|
1105 | |
---|
1106 | // Delegates to the `location_proxy` to set the current location. |
---|
1107 | // See `Sammy.HashLocationProxy` for more info on location proxies. |
---|
1108 | // |
---|
1109 | // ### Arguments |
---|
1110 | // |
---|
1111 | // * `new_location` A new location string (e.g. '#/') |
---|
1112 | // |
---|
1113 | setLocation: function(new_location) { |
---|
1114 | return this._location_proxy.setLocation(new_location); |
---|
1115 | }, |
---|
1116 | |
---|
1117 | // Swaps the content of `$element()` with `content` |
---|
1118 | // You can override this method to provide an alternate swap behavior |
---|
1119 | // for `EventContext.partial()`. |
---|
1120 | // |
---|
1121 | // ### Example |
---|
1122 | // |
---|
1123 | // var app = $.sammy(function() { |
---|
1124 | // |
---|
1125 | // // implements a 'fade out'/'fade in' |
---|
1126 | // this.swap = function(content) { |
---|
1127 | // this.$element().hide('slow').html(content).show('slow'); |
---|
1128 | // } |
---|
1129 | // |
---|
1130 | // get('#/', function() { |
---|
1131 | // this.partial('index.html.erb') // will fade out and in |
---|
1132 | // }); |
---|
1133 | // |
---|
1134 | // }); |
---|
1135 | // |
---|
1136 | swap: function(content) { |
---|
1137 | return this.$element().html(content); |
---|
1138 | }, |
---|
1139 | |
---|
1140 | // a simple global cache for templates. Uses the same semantics as |
---|
1141 | // `Sammy.Cache` and `Sammy.Storage` so can easily be replaced with |
---|
1142 | // a persistant storage that lasts beyond the current request. |
---|
1143 | templateCache: function(key, value) { |
---|
1144 | if (typeof value != 'undefined') { |
---|
1145 | return _template_cache[key] = value; |
---|
1146 | } else { |
---|
1147 | return _template_cache[key]; |
---|
1148 | } |
---|
1149 | }, |
---|
1150 | |
---|
1151 | // clear the templateCache |
---|
1152 | clearTemplateCache: function() { |
---|
1153 | return _template_cache = {}; |
---|
1154 | }, |
---|
1155 | |
---|
1156 | // This thows a '404 Not Found' error by invoking `error()`. |
---|
1157 | // Override this method or `error()` to provide custom |
---|
1158 | // 404 behavior (i.e redirecting to / or showing a warning) |
---|
1159 | notFound: function(verb, path) { |
---|
1160 | var ret = this.error(['404 Not Found', verb, path].join(' ')); |
---|
1161 | return (verb === 'get') ? ret : true; |
---|
1162 | }, |
---|
1163 | |
---|
1164 | // The base error handler takes a string `message` and an `Error` |
---|
1165 | // object. If `raise_errors` is set to `true` on the app level, |
---|
1166 | // this will re-throw the error to the browser. Otherwise it will send the error |
---|
1167 | // to `log()`. Override this method to provide custom error handling |
---|
1168 | // e.g logging to a server side component or displaying some feedback to the |
---|
1169 | // user. |
---|
1170 | error: function(message, original_error) { |
---|
1171 | if (!original_error) { original_error = new Error(); } |
---|
1172 | original_error.message = [message, original_error.message].join(' '); |
---|
1173 | this.trigger('error', {message: original_error.message, error: original_error}); |
---|
1174 | if (this.raise_errors) { |
---|
1175 | throw(original_error); |
---|
1176 | } else { |
---|
1177 | this.log(original_error.message, original_error); |
---|
1178 | } |
---|
1179 | }, |
---|
1180 | |
---|
1181 | _checkLocation: function() { |
---|
1182 | var location, returned; |
---|
1183 | // get current location |
---|
1184 | location = this.getLocation(); |
---|
1185 | // compare to see if hash has changed |
---|
1186 | if (!this.last_location || this.last_location[0] != 'get' || this.last_location[1] != location) { |
---|
1187 | // reset last location |
---|
1188 | this.last_location = ['get', location]; |
---|
1189 | // lookup route for current hash |
---|
1190 | returned = this.runRoute('get', location); |
---|
1191 | } |
---|
1192 | return returned; |
---|
1193 | }, |
---|
1194 | |
---|
1195 | _getFormVerb: function(form) { |
---|
1196 | var $form = $(form), verb, $_method; |
---|
1197 | $_method = $form.find('input[name="_method"]'); |
---|
1198 | if ($_method.length > 0) { verb = $_method.val(); } |
---|
1199 | if (!verb) { verb = $form[0].getAttribute('method'); } |
---|
1200 | return $.trim(verb.toString().toLowerCase()); |
---|
1201 | }, |
---|
1202 | |
---|
1203 | _checkFormSubmission: function(form) { |
---|
1204 | var $form, path, verb, params, returned; |
---|
1205 | this.trigger('check-form-submission', {form: form}); |
---|
1206 | $form = $(form); |
---|
1207 | path = $form.attr('action'); |
---|
1208 | verb = this._getFormVerb($form); |
---|
1209 | if (!verb || verb == '') { verb = 'get'; } |
---|
1210 | this.log('_checkFormSubmission', $form, path, verb); |
---|
1211 | if (verb === 'get') { |
---|
1212 | this.setLocation(path + '?' + this._serializeFormParams($form)); |
---|
1213 | returned = false; |
---|
1214 | } else { |
---|
1215 | params = $.extend({}, this._parseFormParams($form)); |
---|
1216 | returned = this.runRoute(verb, path, params, form.get(0)); |
---|
1217 | }; |
---|
1218 | return (typeof returned == 'undefined') ? false : returned; |
---|
1219 | }, |
---|
1220 | |
---|
1221 | _serializeFormParams: function($form) { |
---|
1222 | var queryString = "", |
---|
1223 | fields = $form.serializeArray(), |
---|
1224 | i; |
---|
1225 | if (fields.length > 0) { |
---|
1226 | queryString = this._encodeFormPair(fields[0].name, fields[0].value); |
---|
1227 | for (i = 1; i < fields.length; i++) { |
---|
1228 | queryString = queryString + "&" + this._encodeFormPair(fields[i].name, fields[i].value); |
---|
1229 | } |
---|
1230 | } |
---|
1231 | return queryString; |
---|
1232 | }, |
---|
1233 | |
---|
1234 | _encodeFormPair: function(name, value){ |
---|
1235 | return _encode(name) + "=" + _encode(value); |
---|
1236 | }, |
---|
1237 | |
---|
1238 | _parseFormParams: function($form) { |
---|
1239 | var params = {}, |
---|
1240 | form_fields = $form.serializeArray(), |
---|
1241 | i; |
---|
1242 | for (i = 0; i < form_fields.length; i++) { |
---|
1243 | params = this._parseParamPair(params, form_fields[i].name, form_fields[i].value); |
---|
1244 | } |
---|
1245 | return params; |
---|
1246 | }, |
---|
1247 | |
---|
1248 | _parseQueryString: function(path) { |
---|
1249 | var params = {}, parts, pairs, pair, i; |
---|
1250 | |
---|
1251 | parts = path.match(QUERY_STRING_MATCHER); |
---|
1252 | if (parts) { |
---|
1253 | pairs = parts[1].split('&'); |
---|
1254 | for (i = 0; i < pairs.length; i++) { |
---|
1255 | pair = pairs[i].split('='); |
---|
1256 | params = this._parseParamPair(params, _decode(pair[0]), _decode(pair[1])); |
---|
1257 | } |
---|
1258 | } |
---|
1259 | return params; |
---|
1260 | }, |
---|
1261 | |
---|
1262 | _parseParamPair: function(params, key, value) { |
---|
1263 | if (params[key]) { |
---|
1264 | if (_isArray(params[key])) { |
---|
1265 | params[key].push(value); |
---|
1266 | } else { |
---|
1267 | params[key] = [params[key], value]; |
---|
1268 | } |
---|
1269 | } else { |
---|
1270 | params[key] = value; |
---|
1271 | } |
---|
1272 | return params; |
---|
1273 | }, |
---|
1274 | |
---|
1275 | _listen: function(name, callback) { |
---|
1276 | return this.$element().bind([name, this.eventNamespace()].join('.'), callback); |
---|
1277 | }, |
---|
1278 | |
---|
1279 | _unlisten: function(name, callback) { |
---|
1280 | return this.$element().unbind([name, this.eventNamespace()].join('.'), callback); |
---|
1281 | } |
---|
1282 | |
---|
1283 | }); |
---|
1284 | |
---|
1285 | // `Sammy.RenderContext` is an object that makes sequential template loading, |
---|
1286 | // rendering and interpolation seamless even when dealing with asyncronous |
---|
1287 | // operations. |
---|
1288 | // |
---|
1289 | // `RenderContext` objects are not usually created directly, rather they are |
---|
1290 | // instatiated from an `Sammy.EventContext` by using `render()`, `load()` or |
---|
1291 | // `partial()` which all return `RenderContext` objects. |
---|
1292 | // |
---|
1293 | // `RenderContext` methods always returns a modified `RenderContext` |
---|
1294 | // for chaining (like jQuery itself). |
---|
1295 | // |
---|
1296 | // The core magic is in the `then()` method which puts the callback passed as |
---|
1297 | // an argument into a queue to be executed once the previous callback is complete. |
---|
1298 | // All the methods of `RenderContext` are wrapped in `then()` which allows you |
---|
1299 | // to queue up methods by chaining, but maintaing a guarunteed execution order |
---|
1300 | // even with remote calls to fetch templates. |
---|
1301 | // |
---|
1302 | Sammy.RenderContext = function(event_context) { |
---|
1303 | this.event_context = event_context; |
---|
1304 | this.callbacks = []; |
---|
1305 | this.previous_content = null; |
---|
1306 | this.content = null; |
---|
1307 | this.next_engine = false; |
---|
1308 | this.waiting = false; |
---|
1309 | }; |
---|
1310 | |
---|
1311 | Sammy.RenderContext.prototype = $.extend({}, Sammy.Object.prototype, { |
---|
1312 | |
---|
1313 | // The "core" of the `RenderContext` object, adds the `callback` to the |
---|
1314 | // queue. If the context is `waiting` (meaning an async operation is happening) |
---|
1315 | // then the callback will be executed in order, once the other operations are |
---|
1316 | // complete. If there is no currently executing operation, the `callback` |
---|
1317 | // is executed immediately. |
---|
1318 | // |
---|
1319 | // The value returned from the callback is stored in `content` for the |
---|
1320 | // subsiquent operation. If you return `false`, the queue will pause, and |
---|
1321 | // the next callback in the queue will not be executed until `next()` is |
---|
1322 | // called. This allows for the guarunteed order of execution while working |
---|
1323 | // with async operations. |
---|
1324 | // |
---|
1325 | // If then() is passed a string instead of a function, the string is looked |
---|
1326 | // up as a helper method on the event context. |
---|
1327 | // |
---|
1328 | // ### Example |
---|
1329 | // |
---|
1330 | // this.get('#/', function() { |
---|
1331 | // // initialize the RenderContext |
---|
1332 | // // Even though `load()` executes async, the next `then()` |
---|
1333 | // // wont execute until the load finishes |
---|
1334 | // this.load('myfile.txt') |
---|
1335 | // .then(function(content) { |
---|
1336 | // // the first argument to then is the content of the |
---|
1337 | // // prev operation |
---|
1338 | // $('#main').html(content); |
---|
1339 | // }); |
---|
1340 | // }); |
---|
1341 | // |
---|
1342 | then: function(callback) { |
---|
1343 | if (!_isFunction(callback)) { |
---|
1344 | // if a string is passed to then, assume we want to call |
---|
1345 | // a helper on the event context in its context |
---|
1346 | if (typeof callback === 'string' && callback in this.event_context) { |
---|
1347 | var helper = this.event_context[callback]; |
---|
1348 | callback = function(content) { |
---|
1349 | return helper.apply(this.event_context, [content]); |
---|
1350 | }; |
---|
1351 | } else { |
---|
1352 | return this; |
---|
1353 | } |
---|
1354 | } |
---|
1355 | var context = this; |
---|
1356 | if (this.waiting) { |
---|
1357 | this.callbacks.push(callback); |
---|
1358 | } else { |
---|
1359 | this.wait(); |
---|
1360 | window.setTimeout(function() { |
---|
1361 | var returned = callback.apply(context, [context.content, context.previous_content]); |
---|
1362 | if (returned !== false) { |
---|
1363 | context.next(returned); |
---|
1364 | } |
---|
1365 | }, 13); |
---|
1366 | } |
---|
1367 | return this; |
---|
1368 | }, |
---|
1369 | |
---|
1370 | // Pause the `RenderContext` queue. Combined with `next()` allows for async |
---|
1371 | // operations. |
---|
1372 | // |
---|
1373 | // ### Example |
---|
1374 | // |
---|
1375 | // this.get('#/', function() { |
---|
1376 | // this.load('mytext.json') |
---|
1377 | // .then(function(content) { |
---|
1378 | // var context = this, |
---|
1379 | // data = JSON.parse(content); |
---|
1380 | // // pause execution |
---|
1381 | // context.wait(); |
---|
1382 | // // post to a url |
---|
1383 | // $.post(data.url, {}, function(response) { |
---|
1384 | // context.next(JSON.parse(response)); |
---|
1385 | // }); |
---|
1386 | // }) |
---|
1387 | // .then(function(data) { |
---|
1388 | // // data is json from the previous post |
---|
1389 | // $('#message').text(data.status); |
---|
1390 | // }); |
---|
1391 | // }); |
---|
1392 | wait: function() { |
---|
1393 | this.waiting = true; |
---|
1394 | }, |
---|
1395 | |
---|
1396 | // Resume the queue, setting `content` to be used in the next operation. |
---|
1397 | // See `wait()` for an example. |
---|
1398 | next: function(content) { |
---|
1399 | this.waiting = false; |
---|
1400 | if (typeof content !== 'undefined') { |
---|
1401 | this.previous_content = this.content; |
---|
1402 | this.content = content; |
---|
1403 | } |
---|
1404 | if (this.callbacks.length > 0) { |
---|
1405 | this.then(this.callbacks.shift()); |
---|
1406 | } |
---|
1407 | }, |
---|
1408 | |
---|
1409 | // Load a template into the context. |
---|
1410 | // The `location` can either be a string specifiying the remote path to the |
---|
1411 | // file, a jQuery object, or a DOM element. |
---|
1412 | // |
---|
1413 | // No interpolation happens by default, the content is stored in |
---|
1414 | // `content`. |
---|
1415 | // |
---|
1416 | // In the case of a path, unless the option `{cache: false}` is passed the |
---|
1417 | // data is stored in the app's `templateCache()`. |
---|
1418 | // |
---|
1419 | // If a jQuery or DOM object is passed the `innerHTML` of the node is pulled in. |
---|
1420 | // This is useful for nesting templates as part of the initial page load wrapped |
---|
1421 | // in invisible elements or `<script>` tags. With template paths, the template |
---|
1422 | // engine is looked up by the extension. For DOM/jQuery embedded templates, |
---|
1423 | // this isnt possible, so there are a couple of options: |
---|
1424 | // |
---|
1425 | // * pass an `{engine:}` option. |
---|
1426 | // * define the engine in the `data-engine` attribute of the passed node. |
---|
1427 | // * just store the raw template data and use `interpolate()` manually |
---|
1428 | // |
---|
1429 | // If a `callback` is passed it is executed after the template load. |
---|
1430 | load: function(location, options, callback) { |
---|
1431 | var context = this; |
---|
1432 | return this.then(function() { |
---|
1433 | var should_cache, cached, is_json, location_array; |
---|
1434 | if (_isFunction(options)) { |
---|
1435 | callback = options; |
---|
1436 | options = {}; |
---|
1437 | } else { |
---|
1438 | options = $.extend({}, options); |
---|
1439 | } |
---|
1440 | if (callback) { this.then(callback); } |
---|
1441 | if (typeof location === 'string') { |
---|
1442 | // its a path |
---|
1443 | is_json = (location.match(/\.json$/) || options.json); |
---|
1444 | should_cache = ((is_json && options.cache === true) || options.cache !== false); |
---|
1445 | context.next_engine = context.event_context.engineFor(location); |
---|
1446 | delete options.cache; |
---|
1447 | delete options.json; |
---|
1448 | if (options.engine) { |
---|
1449 | context.next_engine = options.engine; |
---|
1450 | delete options.engine; |
---|
1451 | } |
---|
1452 | if (should_cache && (cached = this.event_context.app.templateCache(location))) { |
---|
1453 | return cached; |
---|
1454 | } |
---|
1455 | this.wait(); |
---|
1456 | $.ajax($.extend({ |
---|
1457 | url: location, |
---|
1458 | data: {}, |
---|
1459 | dataType: is_json ? 'json' : null, |
---|
1460 | type: 'get', |
---|
1461 | success: function(data) { |
---|
1462 | if (should_cache) { |
---|
1463 | context.event_context.app.templateCache(location, data); |
---|
1464 | } |
---|
1465 | context.next(data); |
---|
1466 | } |
---|
1467 | }, options)); |
---|
1468 | return false; |
---|
1469 | } else { |
---|
1470 | // its a dom/jQuery |
---|
1471 | if (location.nodeType) { |
---|
1472 | return location.innerHTML; |
---|
1473 | } |
---|
1474 | if (location.selector) { |
---|
1475 | // its a jQuery |
---|
1476 | context.next_engine = location.attr('data-engine'); |
---|
1477 | if (options.clone === false) { |
---|
1478 | return location.remove()[0].innerHTML.toString(); |
---|
1479 | } else { |
---|
1480 | return location[0].innerHTML.toString(); |
---|
1481 | } |
---|
1482 | } |
---|
1483 | } |
---|
1484 | }); |
---|
1485 | }, |
---|
1486 | |
---|
1487 | // `load()` a template and then `interpolate()` it with data. |
---|
1488 | // |
---|
1489 | // ### Example |
---|
1490 | // |
---|
1491 | // this.get('#/', function() { |
---|
1492 | // this.render('mytemplate.template', {name: 'test'}); |
---|
1493 | // }); |
---|
1494 | // |
---|
1495 | render: function(location, data, callback) { |
---|
1496 | if (_isFunction(location) && !data) { |
---|
1497 | return this.then(location); |
---|
1498 | } else { |
---|
1499 | if (!data && this.content) { data = this.content; } |
---|
1500 | return this.load(location) |
---|
1501 | .interpolate(data, location) |
---|
1502 | .then(callback); |
---|
1503 | } |
---|
1504 | }, |
---|
1505 | |
---|
1506 | // `render()` the the `location` with `data` and then `swap()` the |
---|
1507 | // app's `$element` with the rendered content. |
---|
1508 | partial: function(location, data) { |
---|
1509 | return this.render(location, data).swap(); |
---|
1510 | }, |
---|
1511 | |
---|
1512 | // defers the call of function to occur in order of the render queue. |
---|
1513 | // The function can accept any number of arguments as long as the last |
---|
1514 | // argument is a callback function. This is useful for putting arbitrary |
---|
1515 | // asynchronous functions into the queue. The content passed to the |
---|
1516 | // callback is passed as `content` to the next item in the queue. |
---|
1517 | // |
---|
1518 | // === Example |
---|
1519 | // |
---|
1520 | // this.send($.getJSON, '/app.json') |
---|
1521 | // .then(function(json) { |
---|
1522 | // $('#message).text(json['message']); |
---|
1523 | // }); |
---|
1524 | // |
---|
1525 | // |
---|
1526 | send: function() { |
---|
1527 | var context = this, |
---|
1528 | args = _makeArray(arguments), |
---|
1529 | fun = args.shift(); |
---|
1530 | |
---|
1531 | if (_isArray(args[0])) { args = args[0]; } |
---|
1532 | |
---|
1533 | return this.then(function(content) { |
---|
1534 | args.push(function(response) { context.next(response); }); |
---|
1535 | context.wait(); |
---|
1536 | fun.apply(fun, args); |
---|
1537 | return false; |
---|
1538 | }); |
---|
1539 | }, |
---|
1540 | |
---|
1541 | // itterates over an array, applying the callback for each item item. the |
---|
1542 | // callback takes the same style of arguments as `jQuery.each()` (index, item). |
---|
1543 | // The return value of each callback is collected as a single string and stored |
---|
1544 | // as `content` to be used in the next iteration of the `RenderContext`. |
---|
1545 | collect: function(array, callback, now) { |
---|
1546 | var context = this; |
---|
1547 | var coll = function() { |
---|
1548 | if (_isFunction(array)) { |
---|
1549 | callback = array; |
---|
1550 | array = this.content; |
---|
1551 | } |
---|
1552 | var contents = [], doms = false; |
---|
1553 | $.each(array, function(i, item) { |
---|
1554 | var returned = callback.apply(context, [i, item]); |
---|
1555 | if (returned.jquery && returned.length == 1) { |
---|
1556 | returned = returned[0]; |
---|
1557 | doms = true; |
---|
1558 | } |
---|
1559 | contents.push(returned); |
---|
1560 | return returned; |
---|
1561 | }); |
---|
1562 | return doms ? contents : contents.join(''); |
---|
1563 | }; |
---|
1564 | return now ? coll() : this.then(coll); |
---|
1565 | }, |
---|
1566 | |
---|
1567 | // loads a template, and then interpolates it for each item in the `data` |
---|
1568 | // array. If a callback is passed, it will call the callback with each |
---|
1569 | // item in the array _after_ interpolation |
---|
1570 | renderEach: function(location, name, data, callback) { |
---|
1571 | if (_isArray(name)) { |
---|
1572 | callback = data; |
---|
1573 | data = name; |
---|
1574 | name = null; |
---|
1575 | } |
---|
1576 | return this.load(location).then(function(content) { |
---|
1577 | var rctx = this; |
---|
1578 | if (!data) { |
---|
1579 | data = _isArray(this.previous_content) ? this.previous_content : []; |
---|
1580 | } |
---|
1581 | if (callback) { |
---|
1582 | $.each(data, function(i, value) { |
---|
1583 | var idata = {}, engine = this.next_engine || location; |
---|
1584 | name ? (idata[name] = value) : (idata = value); |
---|
1585 | callback(value, rctx.event_context.interpolate(content, idata, engine)); |
---|
1586 | }); |
---|
1587 | } else { |
---|
1588 | return this.collect(data, function(i, value) { |
---|
1589 | var idata = {}, engine = this.next_engine || location; |
---|
1590 | name ? (idata[name] = value) : (idata = value); |
---|
1591 | return this.event_context.interpolate(content, idata, engine); |
---|
1592 | }, true); |
---|
1593 | } |
---|
1594 | }); |
---|
1595 | }, |
---|
1596 | |
---|
1597 | // uses the previous loaded `content` and the `data` object to interpolate |
---|
1598 | // a template. `engine` defines the templating/interpolation method/engine |
---|
1599 | // that should be used. If `engine` is not passed, the `next_engine` is |
---|
1600 | // used. If `retain` is `true`, the final interpolated data is appended to |
---|
1601 | // the `previous_content` instead of just replacing it. |
---|
1602 | interpolate: function(data, engine, retain) { |
---|
1603 | var context = this; |
---|
1604 | return this.then(function(content, prev) { |
---|
1605 | if (!data && prev) { data = prev; } |
---|
1606 | if (this.next_engine) { |
---|
1607 | engine = this.next_engine; |
---|
1608 | this.next_engine = false; |
---|
1609 | } |
---|
1610 | var rendered = context.event_context.interpolate(content, data, engine); |
---|
1611 | return retain ? prev + rendered : rendered; |
---|
1612 | }); |
---|
1613 | }, |
---|
1614 | |
---|
1615 | // executes `EventContext#swap()` with the `content` |
---|
1616 | swap: function() { |
---|
1617 | return this.then(function(content) { |
---|
1618 | this.event_context.swap(content); |
---|
1619 | }).trigger('changed', {}); |
---|
1620 | }, |
---|
1621 | |
---|
1622 | // Same usage as `jQuery.fn.appendTo()` but uses `then()` to ensure order |
---|
1623 | appendTo: function(selector) { |
---|
1624 | return this.then(function(content) { |
---|
1625 | $(selector).append(content); |
---|
1626 | }).trigger('changed', {}); |
---|
1627 | }, |
---|
1628 | |
---|
1629 | // Same usage as `jQuery.fn.prependTo()` but uses `then()` to ensure order |
---|
1630 | prependTo: function(selector) { |
---|
1631 | return this.then(function(content) { |
---|
1632 | $(selector).prepend(content); |
---|
1633 | }).trigger('changed', {}); |
---|
1634 | }, |
---|
1635 | |
---|
1636 | // Replaces the `$(selector)` using `html()` with the previously loaded |
---|
1637 | // `content` |
---|
1638 | replace: function(selector) { |
---|
1639 | return this.then(function(content) { |
---|
1640 | $(selector).html(content); |
---|
1641 | }).trigger('changed', {}); |
---|
1642 | }, |
---|
1643 | |
---|
1644 | // trigger the event in the order of the event context. Same semantics |
---|
1645 | // as `Sammy.EventContext#trigger()`. If data is ommitted, `content` |
---|
1646 | // is sent as `{content: content}` |
---|
1647 | trigger: function(name, data) { |
---|
1648 | return this.then(function(content) { |
---|
1649 | if (typeof data == 'undefined') { data = {content: content}; } |
---|
1650 | this.event_context.trigger(name, data); |
---|
1651 | }); |
---|
1652 | } |
---|
1653 | |
---|
1654 | }); |
---|
1655 | |
---|
1656 | // `Sammy.EventContext` objects are created every time a route is run or a |
---|
1657 | // bound event is triggered. The callbacks for these events are evaluated within a `Sammy.EventContext` |
---|
1658 | // This within these callbacks the special methods of `EventContext` are available. |
---|
1659 | // |
---|
1660 | // ### Example |
---|
1661 | // |
---|
1662 | // $.sammy(function() { |
---|
1663 | // // The context here is this Sammy.Application |
---|
1664 | // this.get('#/:name', function() { |
---|
1665 | // // The context here is a new Sammy.EventContext |
---|
1666 | // if (this.params['name'] == 'sammy') { |
---|
1667 | // this.partial('name.html.erb', {name: 'Sammy'}); |
---|
1668 | // } else { |
---|
1669 | // this.redirect('#/somewhere-else') |
---|
1670 | // } |
---|
1671 | // }); |
---|
1672 | // }); |
---|
1673 | // |
---|
1674 | // Initialize a new EventContext |
---|
1675 | // |
---|
1676 | // ### Arguments |
---|
1677 | // |
---|
1678 | // * `app` The `Sammy.Application` this event is called within. |
---|
1679 | // * `verb` The verb invoked to run this context/route. |
---|
1680 | // * `path` The string path invoked to run this context/route. |
---|
1681 | // * `params` An Object of optional params to pass to the context. Is converted |
---|
1682 | // to a `Sammy.Object`. |
---|
1683 | // * `target` a DOM element that the event that holds this context originates |
---|
1684 | // from. For post, put and del routes, this is the form element that triggered |
---|
1685 | // the route. |
---|
1686 | // |
---|
1687 | Sammy.EventContext = function(app, verb, path, params, target) { |
---|
1688 | this.app = app; |
---|
1689 | this.verb = verb; |
---|
1690 | this.path = path; |
---|
1691 | this.params = new Sammy.Object(params); |
---|
1692 | this.target = target; |
---|
1693 | }; |
---|
1694 | |
---|
1695 | Sammy.EventContext.prototype = $.extend({}, Sammy.Object.prototype, { |
---|
1696 | |
---|
1697 | // A shortcut to the app's `$element()` |
---|
1698 | $element: function() { |
---|
1699 | return this.app.$element(_makeArray(arguments).shift()); |
---|
1700 | }, |
---|
1701 | |
---|
1702 | // Look up a templating engine within the current app and context. |
---|
1703 | // `engine` can be one of the following: |
---|
1704 | // |
---|
1705 | // * a function: should conform to `function(content, data) { return interploated; }` |
---|
1706 | // * a template path: 'template.ejs', looks up the extension to match to |
---|
1707 | // the `ejs()` helper |
---|
1708 | // * a string referering to the helper: "mustache" => `mustache()` |
---|
1709 | // |
---|
1710 | // If no engine is found, use the app's default `template_engine` |
---|
1711 | // |
---|
1712 | engineFor: function(engine) { |
---|
1713 | var context = this, engine_match; |
---|
1714 | // if path is actually an engine function just return it |
---|
1715 | if (_isFunction(engine)) { return engine; } |
---|
1716 | // lookup engine name by path extension |
---|
1717 | engine = (engine || context.app.template_engine).toString(); |
---|
1718 | if ((engine_match = engine.match(/\.([^\.]+)$/))) { |
---|
1719 | engine = engine_match[1]; |
---|
1720 | } |
---|
1721 | // set the engine to the default template engine if no match is found |
---|
1722 | if (engine && _isFunction(context[engine])) { |
---|
1723 | return context[engine]; |
---|
1724 | } |
---|
1725 | |
---|
1726 | if (context.app.template_engine) { |
---|
1727 | return this.engineFor(context.app.template_engine); |
---|
1728 | } |
---|
1729 | return function(content, data) { return content; }; |
---|
1730 | }, |
---|
1731 | |
---|
1732 | // using the template `engine` found with `engineFor()`, interpolate the |
---|
1733 | // `data` into `content` |
---|
1734 | interpolate: function(content, data, engine) { |
---|
1735 | return this.engineFor(engine).apply(this, [content, data]); |
---|
1736 | }, |
---|
1737 | |
---|
1738 | // Create and return a `Sammy.RenderContext` calling `render()` on it. |
---|
1739 | // Loads the template and interpolate the data, however does not actual |
---|
1740 | // place it in the DOM. |
---|
1741 | // |
---|
1742 | // ### Example |
---|
1743 | // |
---|
1744 | // // mytemplate.mustache <div class="name">{{name}}</div> |
---|
1745 | // render('mytemplate.mustache', {name: 'quirkey'}); |
---|
1746 | // // sets the `content` to <div class="name">quirkey</div> |
---|
1747 | // render('mytemplate.mustache', {name: 'quirkey'}) |
---|
1748 | // .appendTo('ul'); |
---|
1749 | // // appends the rendered content to $('ul') |
---|
1750 | // |
---|
1751 | render: function(location, data, callback) { |
---|
1752 | return new Sammy.RenderContext(this).render(location, data, callback); |
---|
1753 | }, |
---|
1754 | |
---|
1755 | // Create and return a `Sammy.RenderContext` calling `renderEach()` on it. |
---|
1756 | // Loads the template and interpolates the data for each item, |
---|
1757 | // however does not actual place it in the DOM. |
---|
1758 | // |
---|
1759 | // ### Example |
---|
1760 | // |
---|
1761 | // // mytemplate.mustache <div class="name">{{name}}</div> |
---|
1762 | // renderEach('mytemplate.mustache', [{name: 'quirkey'}, {name: 'endor'}]) |
---|
1763 | // // sets the `content` to <div class="name">quirkey</div><div class="name">endor</div> |
---|
1764 | // renderEach('mytemplate.mustache', [{name: 'quirkey'}, {name: 'endor'}]).appendTo('ul'); |
---|
1765 | // // appends the rendered content to $('ul') |
---|
1766 | // |
---|
1767 | renderEach: function(location, name, data, callback) { |
---|
1768 | return new Sammy.RenderContext(this).renderEach(location, name, data, callback); |
---|
1769 | }, |
---|
1770 | |
---|
1771 | // create a new `Sammy.RenderContext` calling `load()` with `location` and |
---|
1772 | // `options`. Called without interpolation or placement, this allows for |
---|
1773 | // preloading/caching the templates. |
---|
1774 | load: function(location, options, callback) { |
---|
1775 | return new Sammy.RenderContext(this).load(location, options, callback); |
---|
1776 | }, |
---|
1777 | |
---|
1778 | // `render()` the the `location` with `data` and then `swap()` the |
---|
1779 | // app's `$element` with the rendered content. |
---|
1780 | partial: function(location, data) { |
---|
1781 | return new Sammy.RenderContext(this).partial(location, data); |
---|
1782 | }, |
---|
1783 | |
---|
1784 | // create a new `Sammy.RenderContext` calling `send()` with an arbitrary |
---|
1785 | // function |
---|
1786 | send: function() { |
---|
1787 | var rctx = new Sammy.RenderContext(this); |
---|
1788 | return rctx.send.apply(rctx, arguments); |
---|
1789 | }, |
---|
1790 | |
---|
1791 | // Changes the location of the current window. If `to` begins with |
---|
1792 | // '#' it only changes the document's hash. If passed more than 1 argument |
---|
1793 | // redirect will join them together with forward slashes. |
---|
1794 | // |
---|
1795 | // ### Example |
---|
1796 | // |
---|
1797 | // redirect('#/other/route'); |
---|
1798 | // // equivilent to |
---|
1799 | // redirect('#', 'other', 'route'); |
---|
1800 | // |
---|
1801 | redirect: function() { |
---|
1802 | var to, args = _makeArray(arguments), |
---|
1803 | current_location = this.app.getLocation(); |
---|
1804 | if (args.length > 1) { |
---|
1805 | args.unshift('/'); |
---|
1806 | to = this.join.apply(this, args); |
---|
1807 | } else { |
---|
1808 | to = args[0]; |
---|
1809 | } |
---|
1810 | this.trigger('redirect', {to: to}); |
---|
1811 | this.app.last_location = [this.verb, this.path]; |
---|
1812 | this.app.setLocation(to); |
---|
1813 | if (current_location == to) { |
---|
1814 | this.app.trigger('location-changed'); |
---|
1815 | } |
---|
1816 | }, |
---|
1817 | |
---|
1818 | // Triggers events on `app` within the current context. |
---|
1819 | trigger: function(name, data) { |
---|
1820 | if (typeof data == 'undefined') { data = {}; } |
---|
1821 | if (!data.context) { data.context = this; } |
---|
1822 | return this.app.trigger(name, data); |
---|
1823 | }, |
---|
1824 | |
---|
1825 | // A shortcut to app's `eventNamespace()` |
---|
1826 | eventNamespace: function() { |
---|
1827 | return this.app.eventNamespace(); |
---|
1828 | }, |
---|
1829 | |
---|
1830 | // A shortcut to app's `swap()` |
---|
1831 | swap: function(contents) { |
---|
1832 | return this.app.swap(contents); |
---|
1833 | }, |
---|
1834 | |
---|
1835 | // Raises a possible `notFound()` error for the current path. |
---|
1836 | notFound: function() { |
---|
1837 | return this.app.notFound(this.verb, this.path); |
---|
1838 | }, |
---|
1839 | |
---|
1840 | // Default JSON parsing uses jQuery's `parseJSON()`. Include `Sammy.JSON` |
---|
1841 | // plugin for the more conformant "crockford special". |
---|
1842 | json: function(string) { |
---|
1843 | return $.parseJSON(string); |
---|
1844 | }, |
---|
1845 | |
---|
1846 | // //=> Sammy.EventContext: get #/ {} |
---|
1847 | toString: function() { |
---|
1848 | return "Sammy.EventContext: " + [this.verb, this.path, this.params].join(' '); |
---|
1849 | } |
---|
1850 | |
---|
1851 | }); |
---|
1852 | |
---|
1853 | // An alias to Sammy |
---|
1854 | $.sammy = window.Sammy = Sammy; |
---|
1855 | |
---|
1856 | })(jQuery, window); |
---|