jim 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/HISTORY +28 -0
  2. data/README.rdoc +1 -1
  3. data/Rakefile +2 -1
  4. data/jim.gemspec +67 -9
  5. data/lib/jim/bundler.rb +14 -11
  6. data/lib/jim/cli.rb +51 -9
  7. data/lib/jim/index.rb +3 -7
  8. data/lib/jim/installer.rb +140 -22
  9. data/lib/jim/templates/commands +21 -6
  10. data/lib/jim/version_parser.rb +2 -3
  11. data/lib/jim.rb +19 -2
  12. data/test/fixtures/jimfile +3 -3
  13. data/test/fixtures/sammy-0.5.0/HISTORY.md +135 -0
  14. data/test/fixtures/sammy-0.5.0/LICENSE +22 -0
  15. data/test/fixtures/sammy-0.5.0/README.md +81 -0
  16. data/test/fixtures/sammy-0.5.0/Rakefile +174 -0
  17. data/test/fixtures/sammy-0.5.0/examples/backend/README.md +23 -0
  18. data/test/fixtures/sammy-0.5.0/examples/backend/Rakefile +15 -0
  19. data/test/fixtures/sammy-0.5.0/examples/backend/app.rb +17 -0
  20. data/test/fixtures/sammy-0.5.0/examples/backend/app.ru +3 -0
  21. data/test/fixtures/sammy-0.5.0/examples/backend/public/javascripts/app.js +106 -0
  22. data/test/fixtures/sammy-0.5.0/examples/backend/public/javascripts/jquery.cloudkit.js +840 -0
  23. data/test/fixtures/sammy-0.5.0/examples/backend/public/javascripts/jquery.js +19 -0
  24. data/test/fixtures/sammy-0.5.0/examples/backend/public/javascripts/sammy.js +1013 -0
  25. data/test/fixtures/sammy-0.5.0/examples/backend/public/templates/index.html.erb +11 -0
  26. data/test/fixtures/sammy-0.5.0/examples/backend/public/templates/task.html.erb +4 -0
  27. data/test/fixtures/sammy-0.5.0/examples/backend/public/templates/task_details.html.erb +4 -0
  28. data/test/fixtures/sammy-0.5.0/examples/backend/views/app.sass +63 -0
  29. data/test/fixtures/sammy-0.5.0/examples/backend/views/index.haml +18 -0
  30. data/test/fixtures/sammy-0.5.0/examples/form_handling/files/form.html +12 -0
  31. data/test/fixtures/sammy-0.5.0/examples/form_handling/index.html +65 -0
  32. data/test/fixtures/sammy-0.5.0/examples/hello_world/index.html +50 -0
  33. data/test/fixtures/sammy-0.5.0/examples/location_override/README.md +15 -0
  34. data/test/fixtures/sammy-0.5.0/examples/location_override/data.html +110 -0
  35. data/test/fixtures/sammy-0.5.0/examples/location_override/index.html +79 -0
  36. data/test/fixtures/sammy-0.5.0/examples/location_override/test.html +121 -0
  37. data/test/fixtures/sammy-0.5.0/lib/min/sammy-0.5.0.min.js +5 -0
  38. data/test/fixtures/sammy-0.5.0/lib/min/sammy-lastest.min.js +5 -0
  39. data/test/fixtures/sammy-0.5.0/lib/plugins/sammy.cache.js +117 -0
  40. data/test/fixtures/sammy-0.5.0/lib/plugins/sammy.haml.js +539 -0
  41. data/test/fixtures/sammy-0.5.0/lib/plugins/sammy.json.js +362 -0
  42. data/test/fixtures/sammy-0.5.0/lib/plugins/sammy.mustache.js +415 -0
  43. data/test/fixtures/sammy-0.5.0/lib/plugins/sammy.nested_params.js +118 -0
  44. data/test/fixtures/sammy-0.5.0/lib/plugins/sammy.storage.js +515 -0
  45. data/test/fixtures/sammy-0.5.0/lib/plugins/sammy.template.js +117 -0
  46. data/test/fixtures/sammy-0.5.0/lib/sammy.js +1367 -0
  47. data/test/fixtures/sammy-0.5.0/test/fixtures/partial +1 -0
  48. data/test/fixtures/sammy-0.5.0/test/fixtures/partial.html +1 -0
  49. data/test/fixtures/sammy-0.5.0/test/fixtures/partial.noengine +1 -0
  50. data/test/fixtures/sammy-0.5.0/test/fixtures/partial.template +1 -0
  51. data/test/fixtures/sammy-0.5.0/test/index.html +84 -0
  52. data/test/fixtures/sammy-0.5.0/test/test_sammy_application.js +953 -0
  53. data/test/fixtures/sammy-0.5.0/test/test_sammy_event_context.js +252 -0
  54. data/test/fixtures/sammy-0.5.0/test/test_sammy_location_proxy.js +91 -0
  55. data/test/fixtures/sammy-0.5.0/test/test_sammy_plugins.js +296 -0
  56. data/test/fixtures/sammy-0.5.0/test/test_sammy_storage.js +175 -0
  57. data/test/fixtures/sammy-0.5.0/test/test_server +27 -0
  58. data/test/fixtures/sammy-0.5.0/vendor/jquery-1.4.1.js +6078 -0
  59. data/test/fixtures/sammy-0.5.0/vendor/jquery-1.4.1.min.js +152 -0
  60. data/test/fixtures/sammy-0.5.0/vendor/jsdoc/doc.haml +58 -0
  61. data/test/fixtures/sammy-0.5.0/vendor/jsdoc/jsdoc.rb +143 -0
  62. data/test/fixtures/sammy-0.5.0/vendor/jslitmus.js +670 -0
  63. data/test/fixtures/sammy-0.5.0/vendor/qunit/qunit.css +119 -0
  64. data/test/fixtures/sammy-0.5.0/vendor/qunit/qunit.js +1043 -0
  65. data/test/fixtures/sammy-0.5.0/vendor/qunit-spec.js +127 -0
  66. data/test/helper.rb +23 -3
  67. data/test/test_jim_bundler.rb +9 -8
  68. data/test/test_jim_cli.rb +21 -12
  69. data/test/test_jim_installer.rb +152 -35
  70. data/test/test_jim_version_parser.rb +4 -0
  71. metadata +117 -27
  72. data/.document +0 -5
@@ -0,0 +1,1013 @@
1
+ ;(function($) {
2
+
3
+ var PATH_REPLACER = "([^\/]+)";
4
+ var PATH_NAME_MATCHER = /:([\w\d]+)/g;
5
+ var QUERY_STRING_MATCHER = /\?([^#]*)$/;
6
+
7
+ var loggers = [];
8
+
9
+ Sammy = {};
10
+
11
+ Sammy.VERSION = '0.4.0pre';
12
+
13
+ // Add to the global logger pool. Takes a function that accepts an
14
+ // unknown number of arguments and should print them or send them somewhere
15
+ // The first argument is always a timestamp.
16
+ Sammy.addLogger = function(logger) {
17
+ loggers.push(logger);
18
+ };
19
+
20
+ // Sends a log message to each logger listed in the global
21
+ // loggers pool. Can take any number of arguments.
22
+ // Also prefixes the arguments with a timestamp.
23
+ Sammy.log = function() {
24
+ var args = $.makeArray(arguments);
25
+ args.unshift("[" + Date() + "]");
26
+ $.each(loggers, function(i, logger) {
27
+ logger.apply(Sammy, args);
28
+ });
29
+ };
30
+
31
+ if (typeof window.console != 'undefined') {
32
+ Sammy.addLogger(function() {
33
+ window.console.log.apply(window.console, arguments);
34
+ });
35
+ } else if (typeof console != 'undefined') {
36
+ Sammy.addLogger.push(function() {
37
+ console.log.apply(console, arguments);
38
+ });
39
+ }
40
+
41
+ // Sammy.Object is the base for all other Sammy classes. It provides some useful
42
+ // functionality, including cloning, iterating, etc.
43
+ Sammy.Object = function(obj) { // constructor
44
+ this.extend(obj);
45
+ };
46
+
47
+ $.extend(Sammy.Object.prototype, {
48
+
49
+ // Extend this object with the passed object
50
+ extend: function(obj) {
51
+ $.extend(this, obj);
52
+ },
53
+
54
+ // If passed an obj, clone the attributes and methods of that object
55
+ // If called without arguments, clones the callee.
56
+ clone: function(obj) {
57
+ if (typeof obj == 'undefined') obj = this;
58
+ return $.extend({}, obj);
59
+ },
60
+
61
+ // Returns a copy of the object with Functions removed.
62
+ toHash: function() {
63
+ var json = {};
64
+ this.each(function(k,v) {
65
+ if (!$.isFunction(v)) {
66
+ json[k] = v
67
+ }
68
+ });
69
+ return json;
70
+ },
71
+
72
+ // Renders a simple HTML version of this Objects attributes.
73
+ // Does not render functions.
74
+ // For example. Given this Sammy.Object:
75
+ //
76
+ // var s = new Sammy.Object({first_name: 'Sammy', last_name: 'Davis Jr.'});
77
+ // s.toHTML() //=> '<strong>first_name</strong> Sammy<br /><strong>last_name</strong> Davis Jr.<br />'
78
+ //
79
+ toHTML: function() {
80
+ var display = "";
81
+ this.each(function(k, v) {
82
+ if (!$.isFunction(v)) {
83
+ display += "<strong>" + k + "</strong> " + v + "<br />";
84
+ }
85
+ });
86
+ return display;
87
+ },
88
+
89
+ // Generates a unique identifing string. Used for application namespaceing.
90
+ uuid: function() {
91
+ if (typeof this._uuid == 'undefined' || !this._uuid) {
92
+ this._uuid = (new Date()).getTime() + '-' + parseInt(Math.random() * 1000);
93
+ }
94
+ return this._uuid;
95
+ },
96
+
97
+ // If passed an object and a callback, will iterate over the object
98
+ // with (k, v) in the context of this object.
99
+ // If passed just an argument - will itterate over
100
+ // the properties of this Sammy.Object
101
+ each: function() {
102
+ var context, object, callback, bound_callback;
103
+ context = this;
104
+ if (typeof arguments[0] != 'function') {
105
+ object = arguments[0];
106
+ callback = arguments[1];
107
+ } else {
108
+ object = this;
109
+ callback = arguments[0];
110
+ }
111
+ bound_callback = function() {
112
+ return callback.apply(context, arguments);
113
+ }
114
+ $.each(object, bound_callback);
115
+ },
116
+
117
+ // Returns an array of keys for this object. If <tt>attributes_only</tt>
118
+ // is true will not return keys that map to a <tt>function()</tt>
119
+ keys: function(attributes_only) {
120
+ var keys = [];
121
+ for (var property in this) {
122
+ if (!$.isFunction(this[property]) || !attributes_only) {
123
+ keys.push(property);
124
+ }
125
+ }
126
+ return keys;
127
+ },
128
+
129
+ // convenience method to join as many arguments as you want
130
+ // by the first argument - useful for making paths
131
+ join: function() {
132
+ var args = $.makeArray(arguments);
133
+ var delimiter = args.shift();
134
+ return args.join(delimiter);
135
+ },
136
+
137
+ // Shortcut to Sammy.log
138
+ log: function() {
139
+ Sammy.log.apply(Sammy, arguments);
140
+ },
141
+
142
+ // Returns a string representation of this object.
143
+ // if <tt>include_functions</tt> is true, it will also toString() the
144
+ // methods of this object. By default only prints the attributes.
145
+ toString: function(include_functions) {
146
+ var s = []
147
+ this.each(function(k, v) {
148
+ if (!$.isFunction(v) || include_functions) {
149
+ s.push('"' + k + '": ' + v.toString());
150
+ }
151
+ });
152
+ return "Sammy.Object: {" + s.join(',') + "}";
153
+ }
154
+ });
155
+
156
+ Sammy.HashLocationProxy = function(app, run_interval_every) {
157
+ this.app = app;
158
+
159
+ // check for native hash support
160
+ if ('onhashchange' in window) {
161
+ Sammy.log('native hash change exists, using');
162
+ this.is_native = true;
163
+ } else {
164
+ Sammy.log('no native hash change, falling back to polling');
165
+ this.is_native = false;
166
+ this._startPolling(run_interval_every);
167
+ }
168
+ };
169
+
170
+ Sammy.HashLocationProxy.prototype = {
171
+ bind: function() {
172
+ var app = this.app;
173
+ $(window).bind('hashchange.' + this.app.eventNamespace(), function() {
174
+ app.trigger('location-changed');
175
+ });
176
+ },
177
+
178
+ unbind: function() {
179
+ $(window).die('hashchange.' + this.app.eventNamespace());
180
+ },
181
+
182
+ getLocation: function() {
183
+ // Bypass the `window.location.hash` attribute. If a question mark
184
+ // appears in the hash IE6 will strip it and all of the following
185
+ // characters from `window.location.hash`.
186
+ var matches = window.location.toString().match(/^[^#]*(#.+)$/);
187
+ return matches ? matches[1] : '';
188
+ },
189
+
190
+ setLocation: function(new_location) {
191
+ return window.location = new_location;
192
+ },
193
+
194
+ _startPolling: function(every) {
195
+ // set up interval
196
+ var proxy = this;
197
+ if (!Sammy.HashLocationProxy._interval) {
198
+ if (!every) every = 10;
199
+ var hashCheck = function() {
200
+ current_location = proxy.getLocation();
201
+ // Sammy.log('getLocation', current_location);
202
+ if (!Sammy.HashLocationProxy._last_location ||
203
+ current_location != Sammy.HashLocationProxy._last_location) {
204
+ setTimeout(function() {
205
+ $(window).trigger('hashchange');
206
+ }, 1);
207
+ }
208
+ Sammy.HashLocationProxy._last_location = current_location;
209
+ }
210
+ hashCheck();
211
+ Sammy.HashLocationProxy._interval = setInterval(hashCheck, every);
212
+ $(window).bind('unload', function() {
213
+ clearInterval(Sammy.HashLocationProxy._interval);
214
+ });
215
+ }
216
+ }
217
+ };
218
+
219
+ Sammy.DataLocationProxy = function(app, data_name) {
220
+ this.app = app;
221
+ this.data_name = data_name || 'sammy-location';
222
+ };
223
+
224
+ Sammy.DataLocationProxy.prototype = {
225
+ bind: function() {
226
+ var proxy = this;
227
+ this.app.$element().bind('setData', function(e, key) {
228
+ if (key == proxy.data_name) {
229
+ proxy.app.trigger('location-changed');
230
+ }
231
+ });
232
+ },
233
+
234
+ unbind: function() {
235
+ this.app.$element().die('setData');
236
+ },
237
+
238
+ getLocation: function() {
239
+ return this.app.$element().data(this.data_name);
240
+ },
241
+
242
+ setLocation: function(new_location) {
243
+ return this.app.$element().data(this.data_name, new_location);
244
+ }
245
+ };
246
+
247
+ // Sammy.Application is the Base prototype for defining 'applications'.
248
+ // An 'application' is a collection of 'routes' and bound events that is
249
+ // attached to an element when <tt>run()</tt> is called.
250
+ // The only argument an 'app_function' is evaluated within the context of the application.
251
+ Sammy.Application = function(app_function) {
252
+ var app = this;
253
+ this.routes = {};
254
+ this.listeners = new Sammy.Object({});
255
+ this.befores = [];
256
+ this.namespace = this.uuid();
257
+ this.context_prototype = function() { Sammy.EventContext.apply(this, arguments) };
258
+ this.context_prototype.prototype = new Sammy.EventContext();
259
+
260
+ this.each(this.ROUTE_VERBS, function(i, verb) {
261
+ this._defineRouteShortcut(verb);
262
+ });
263
+ if ($.isFunction(app_function)) {
264
+ app_function.apply(this, [this]);
265
+ }
266
+ // set the location proxy if not defined to the default (HashLocationProxy)
267
+ if (!this.location_proxy) {
268
+ this.location_proxy = new Sammy.HashLocationProxy(app, this.run_interval_every);
269
+ }
270
+ if (this.debug) {
271
+ this.bindToAllEvents(function(e, data) {
272
+ app.log(app.toString(), e.cleaned_type, data || {});
273
+ });
274
+ }
275
+ };
276
+
277
+ Sammy.Application.prototype = $.extend({}, Sammy.Object.prototype, {
278
+
279
+ // the four route verbs
280
+ ROUTE_VERBS: ['get','post','put','delete'],
281
+
282
+ // An array of the default events triggered by the
283
+ // application during its lifecycle
284
+ APP_EVENTS: ['run','unload','lookup-route','run-route','route-found','event-context-before','event-context-after','changed','error-404','check-form-submission','redirect'],
285
+
286
+ _last_route: null,
287
+ _running: false,
288
+
289
+ // On <tt>run()</tt> the application object is stored in a <tt>$.data</tt> entry
290
+ // assocciated with the application's <tt>$element()</tt>
291
+ data_store_name: 'sammy-app',
292
+
293
+ // Defines what element the application is bound to. Provide a selector
294
+ // (parseable by <tt>jQuery()</tt>) and this will be used by <tt>$element()</tt>
295
+ element_selector: 'body',
296
+
297
+ // When set to true, logs all of the default events using <tt>log()</tt>
298
+ debug: false,
299
+
300
+ // When set to false, will throw a javascript error when a route is invoked
301
+ // and can not be found.
302
+ silence_404: true,
303
+
304
+ // The time in milliseconds that the URL is queried for changes
305
+ run_interval_every: 50,
306
+
307
+ // //=> Sammy.Application: body
308
+ toString: function() {
309
+ return 'Sammy.Application:' + this.element_selector;
310
+ },
311
+
312
+ // returns a jQuery object of the Applications bound element.
313
+ $element: function() {
314
+ return $(this.element_selector);
315
+ },
316
+
317
+ // <tt>use()</tt> is the entry point for including Sammy plugins.
318
+ // The first argument to use should be a function() that is evaluated
319
+ // in the context of the current application, just like the <tt>app_function</tt>
320
+ // argument to the <tt>Sammy.Application</tt> constructor.
321
+ //
322
+ // Any additional arguments are passed to the app function sequentially.
323
+ //
324
+ // For much more detail about plugins, check out:
325
+ // http://code.quirkey.com/sammy/doc/plugins.html
326
+ //
327
+ // === Example
328
+ //
329
+ // var MyPlugin = function(app, prepend) {
330
+ //
331
+ // this.helpers({
332
+ // myhelper: function(text) {
333
+ // alert(prepend + " " + text);
334
+ // }
335
+ // });
336
+ //
337
+ // };
338
+ //
339
+ // var app = $.sammy(function() {
340
+ //
341
+ // this.use(MyPlugin, 'This is my plugin');
342
+ //
343
+ // this.get('#/', function() {
344
+ // this.myhelper('and dont you forget it!');
345
+ // //=> Alerts: This is my plugin and dont you forget it!
346
+ // });
347
+ //
348
+ // });
349
+ //
350
+ use: function() {
351
+ // flatten the arguments
352
+ var args = $.makeArray(arguments);
353
+ var plugin = args.shift();
354
+ args.unshift(this);
355
+ plugin.apply(this, args);
356
+ },
357
+
358
+ // <tt>route()</tt> is the main method for defining routes within an application.
359
+ // For great detail on routes, check out: http://code.quirkey.com/sammy/doc/routes.html
360
+ //
361
+ // This method also has aliases for each of the different verbs (eg. <tt>get()</tt>, <tt>post()</tt>, etc.)
362
+ //
363
+ // === Arguments
364
+ //
365
+ // +verb+:: A String in the set of ROUTE_VERBS
366
+ // +path+:: A Regexp or a String representing the path to match to invoke this verb.
367
+ // +callback+:: A Function that is called/evaluated whent the route is run see: <tt>runRoute()</tt>
368
+ //
369
+ route: function(verb, path, callback) {
370
+ // turn path into regex
371
+ // create a simple object and add the route to it
372
+ var app = this,
373
+ param_names = [],
374
+ r;
375
+ // if path is a string turn it into a regex
376
+ if (path.constructor == String) {
377
+
378
+ // Needs to be explicitly set because IE will maintain the index unless NULL is returned,
379
+ // 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
380
+ // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/RegExp/lastIndex
381
+ PATH_NAME_MATCHER.lastIndex = 0;
382
+
383
+ // find the names
384
+ while ((path_match = PATH_NAME_MATCHER.exec(path)) != null) {
385
+ param_names.push(path_match[1]);
386
+ }
387
+ // replace with the path replacement
388
+ path = new RegExp(path.replace(PATH_NAME_MATCHER, PATH_REPLACER) + "$");
389
+ }
390
+ r = {verb: verb, path: path, callback: callback, param_names: param_names};
391
+ // add route to routes array
392
+ if (typeof this.routes[verb] == 'undefined' || this.routes[verb].length == 0) {
393
+ // add to the front of an empty array
394
+ this.routes[verb] = [r];
395
+ } else {
396
+ // place routes in order of definition
397
+ this.routes[verb].push(r);
398
+ }
399
+ // return the route
400
+ return r;
401
+ },
402
+
403
+ // A unique event namespace defined per application.
404
+ // All events bound with <tt>bind()</tt> are automatically bound within this space.
405
+ eventNamespace: function() {
406
+ return [this.data_store_name, this.namespace].join('-');
407
+ },
408
+
409
+ // Works just like <tt>jQuery.fn.bind()</tt> with a couple noteable differences.
410
+ //
411
+ // * It binds all events to the application element
412
+ // * All events are bound within the <tt>eventNamespace()</tt>
413
+ // * Events are not actually bound until the application is started with <tt>run()</tt>
414
+ // * callbacks are evaluated within the context of a Sammy.EventContext
415
+ //
416
+ // See http://code.quirkey.com/sammy/docs/events.html for more info.
417
+ //
418
+ bind: function(name, data, callback) {
419
+ var app = this;
420
+ // build the callback
421
+ // if the arity is 2, callback is the second argument
422
+ if (typeof callback == 'undefined') callback = data;
423
+ var listener_callback = function() {
424
+ // pull off the context from the arguments to the callback
425
+ var e, context, data;
426
+ e = arguments[0];
427
+ data = arguments[1];
428
+ if (data && data['context']) {
429
+ context = data['context']
430
+ delete data['context'];
431
+ } else {
432
+ context = new app.context_prototype(app, 'bind', e.type, data);
433
+ }
434
+ e.cleaned_type = e.type.replace(app.eventNamespace(), '');
435
+ callback.apply(context, [e, data]);
436
+ };
437
+
438
+ // it could be that the app element doesnt exist yet
439
+ // so attach to the listeners array and then run()
440
+ // will actually bind the event.
441
+ if (!this.listeners[name]) this.listeners[name] = [];
442
+ this.listeners[name].push(listener_callback);
443
+ if (this.isRunning()) {
444
+ // if the app is running
445
+ // *actually* bind the event to the app element
446
+ return this._listen(name, listener_callback);
447
+ }
448
+ },
449
+
450
+ // Triggers custom events defined with <tt>bind()</tt>
451
+ //
452
+ // === Arguments
453
+ //
454
+ // +name+:: The name of the event. Automatically prefixed with the <tt>eventNamespace()</tt>
455
+ // +data+:: An optional Object that can be passed to the bound callback.
456
+ // +context+:: An optional context/Object in which to execute the bound callback.
457
+ // If no context is supplied a the context is a new <tt>Sammy.EventContext</tt>
458
+ //
459
+ trigger: function(name, data) {
460
+ return this.$element().trigger([name, this.eventNamespace()].join('.'), [data]);
461
+ },
462
+
463
+ // Reruns the current route
464
+ refresh: function() {
465
+ this.last_location = null;
466
+ },
467
+
468
+ // Takes a single callback that is pushed on to a stack.
469
+ // Before any route is run, the callbacks are evaluated in order within
470
+ // the current <tt>Sammy.EventContext</tt>
471
+ //
472
+ // If any of the callbacks explicitly return false, execution of any
473
+ // further callbacks and the route itself is halted.
474
+ before: function(callback) {
475
+ return this.befores.push(callback);
476
+ },
477
+
478
+ // A shortcut for binding a callback to be run after a route is executed.
479
+ // After callbacks have no guarunteed order.
480
+ after: function(callback) {
481
+ return this.bind('event-context-after', callback);
482
+ },
483
+
484
+ // Returns a boolean of weather the current application is running.
485
+ isRunning: function() {
486
+ return this._running;
487
+ },
488
+
489
+ // Helpers extends the EventContext prototype specific to this app.
490
+ // This allows you to define app specific helper functions that can be used
491
+ // whenever you're inside of an event context (templates, routes, bind).
492
+ //
493
+ // === Example
494
+ //
495
+ // var app = $.sammy(function() {
496
+ //
497
+ // helpers({
498
+ // upcase: function(text) {
499
+ // return text.toString().toUpperCase();
500
+ // }
501
+ // });
502
+ //
503
+ // get('#/', function() { with(this) {
504
+ // // inside of this context I can use the helpers
505
+ // $('#main').html(upcase($('#main').text());
506
+ // }});
507
+ //
508
+ // });
509
+ //
510
+ //
511
+ // === Arguments
512
+ //
513
+ // +extensions+:: An object collection of functions to extend the context.
514
+ //
515
+ helpers: function(extensions) {
516
+ $.extend(this.context_prototype.prototype, extensions);
517
+ },
518
+
519
+ // Actually starts the application's lifecycle. <tt>run()</tt> should be invoked
520
+ // within a document.ready block to ensure the DOM exists before binding events, etc.
521
+ //
522
+ // === Example
523
+ //
524
+ // var app = $.sammy(function() { ... }); // your application
525
+ // $(function() { // document.ready
526
+ // app.run();
527
+ // });
528
+ //
529
+ // === Arguments
530
+ //
531
+ // +start_url+:: "value", Optionally, a String can be passed which the App will redirect to
532
+ // after the events/routes have been bound.
533
+ run: function(start_url) {
534
+ if (this.isRunning()) return false;
535
+ var app = this;
536
+
537
+ // actually bind all the listeners
538
+ this.each(this.listeners.toHash(), function(name, callbacks) {
539
+ this.each(callbacks, function(i, listener_callback) {
540
+ this._listen(name, listener_callback);
541
+ });
542
+ });
543
+
544
+ this.trigger('run', {start_url: start_url});
545
+ this._running = true;
546
+ // set data for app
547
+ this.$element().data(this.data_store_name, this);
548
+ // set last location
549
+ this.last_location = null;
550
+ if (this.getLocation() == '' && typeof start_url != 'undefined') {
551
+ this.setLocation(start_url);
552
+ }
553
+ // check url
554
+ this._checkLocation();
555
+ this.location_proxy.bind();
556
+ this.bind('location-changed', function() {
557
+ app._checkLocation();
558
+ });
559
+
560
+ // bind re-binding to after route
561
+ this.bind('changed', function() {
562
+ // bind form submission
563
+ app.$element()
564
+ .find('form:not(.' + app.eventNamespace() + ')')
565
+ .bind('submit', function() {
566
+ return app._checkFormSubmission(this);
567
+ })
568
+ .addClass(app.eventNamespace());
569
+ });
570
+ // bind unload to body unload
571
+ $('body').bind('onunload', function() {
572
+ app.unload();
573
+ });
574
+
575
+ // trigger html changed
576
+ this.trigger('changed');
577
+ },
578
+
579
+ // The opposite of <tt>run()</tt>, un-binds all event listeners and intervals
580
+ // <tt>run()</tt> Automaticaly binds a <tt>onunload</tt> event to run this when
581
+ // the document is closed.
582
+ unload: function() {
583
+ if (!this.isRunning()) return false;
584
+ var app = this;
585
+ this.trigger('unload');
586
+ // clear interval
587
+ this.location_proxy.unbind();
588
+ // unbind form submits
589
+ this.$element().find('form')
590
+ .unbind('submit')
591
+ .removeClass(app.eventNamespace());
592
+ // clear data
593
+ this.$element().removeData(this.data_store_name);
594
+ // unbind all events
595
+ this.each(this.listeners.toHash() , function(name, listeners) {
596
+ this.each(listeners, function(i, listener_callback) {
597
+ this._unlisten(name, listener_callback);
598
+ });
599
+ });
600
+ this._running = false;
601
+ },
602
+
603
+ // Will bind a single callback function to every event that is already
604
+ // being listened to in the app. This includes all the <tt>APP_EVENTS</tt>
605
+ // as well as any custom events defined with <tt>bind()</tt>.
606
+ //
607
+ // Used internally for debug logging.
608
+ bindToAllEvents: function(callback) {
609
+ // bind to the APP_EVENTS first
610
+ this.each(this.APP_EVENTS, function(i, e) {
611
+ this.bind(e, callback);
612
+ });
613
+ // next, bind to listener names (only if they dont exist in APP_EVENTS)
614
+ this.each(this.listeners.keys(true), function(i, name) {
615
+ if (this.APP_EVENTS.indexOf(name) == -1) {
616
+ this.bind(name, callback);
617
+ }
618
+ });
619
+ },
620
+
621
+ // Returns a copy of the given path with any query string after the hash
622
+ // removed.
623
+ routablePath: function(path) {
624
+ return path.replace(QUERY_STRING_MATCHER, '');
625
+ },
626
+
627
+ // Given a verb and a String path, will return either a route object or false
628
+ // if a matching route can be found within the current defined set.
629
+ lookupRoute: function(verb, path) {
630
+ var routed = false;
631
+ this.trigger('lookup-route', {verb: verb, path: path});
632
+ if (typeof this.routes[verb] != 'undefined') {
633
+ this.each(this.routes[verb], function(i, route) {
634
+ if (this.routablePath(path).match(route.path)) {
635
+ routed = route;
636
+ return false;
637
+ }
638
+ });
639
+ }
640
+ return routed;
641
+ },
642
+
643
+ // First, invokes <tt>lookupRoute()</tt> and if a route is found, parses the
644
+ // possible URL params and then invokes the route's callback within a new
645
+ // <tt>Sammy.EventContext</tt>. If the route can not be found, it calls
646
+ // <tt>notFound()</tt> and raise an error. If <tt>silence_404</tt> is <tt>true</tt>
647
+ // this error will be caught be the internal methods that call <tt>runRoute</tt>.
648
+ //
649
+ // You probably will never have to call this directly.
650
+ //
651
+ // === Arguments
652
+ //
653
+ // +verb+:: A String for the verb.
654
+ // +path+:: A String path to lookup.
655
+ // +params+:: An Object of Params pulled from the URI or passed directly.
656
+ //
657
+ // === Returns
658
+ //
659
+ // Either returns the value returned by the route callback or raises a 404 Not Found error.
660
+ //
661
+ runRoute: function(verb, path, params) {
662
+ this.log('runRoute', [verb, path].join(' '));
663
+ this.trigger('run-route', {verb: verb, path: path, params: params});
664
+ if (typeof params == 'undefined') params = {};
665
+
666
+ $.extend(params, this._parseQueryString(path));
667
+
668
+ var route = this.lookupRoute(verb, path);
669
+ if (route) {
670
+ this.trigger('route-found', {route: route});
671
+ // pull out the params from the path
672
+ if ((path_params = route.path.exec(this.routablePath(path))) != null) {
673
+ // first match is the full path
674
+ path_params.shift();
675
+ // for each of the matches
676
+ this.each(path_params, function(i, param) {
677
+ // if theres a matching param name
678
+ if (route.param_names[i]) {
679
+ // set the name to the match
680
+ params[route.param_names[i]] = param;
681
+ } else {
682
+ // initialize 'splat'
683
+ if (!params['splat']) params['splat'] = [];
684
+ params['splat'].push(param);
685
+ }
686
+ });
687
+ }
688
+
689
+ // set event context
690
+ var context = new this.context_prototype(this, verb, path, params);
691
+ this.last_route = route;
692
+ // run all the before filters
693
+ var before_value = true;
694
+ var befores = this.befores.slice(0);
695
+ while (befores.length > 0) {
696
+ if (befores.shift().apply(context) === false) return false;
697
+ }
698
+ context.trigger('event-context-before', {context: context});
699
+ var returned = route.callback.apply(context, [context]);
700
+ context.trigger('event-context-after', {context: context});
701
+ return returned;
702
+ } else {
703
+ this.notFound(verb, path);
704
+ }
705
+ },
706
+
707
+ // The default behavior is to return the current window's location hash.
708
+ // Override this and <tt>setLocation()</tt> to detach the app from the
709
+ // window.location object.
710
+ getLocation: function() {
711
+ return this.location_proxy.getLocation()
712
+ },
713
+
714
+ // The default behavior is to set the current window's location.
715
+ // Override this and <tt>getLocation()</tt> to detach the app from the
716
+ // window.location object.
717
+ //
718
+ // === Arguments
719
+ //
720
+ // +new_location+:: A new location string (e.g. '#/')
721
+ //
722
+ setLocation: function(new_location) {
723
+ return this.location_proxy.setLocation(new_location);
724
+ },
725
+
726
+ // Swaps the content of <tt>$element()</tt> with <tt>content</tt>
727
+ // You can override this method to provide an alternate swap behavior
728
+ // for <tt>EventContext.partial()</tt>.
729
+ //
730
+ // === Example
731
+ //
732
+ // var app = $.sammy(function() {
733
+ //
734
+ // // implements a 'fade out'/'fade in'
735
+ // this.swap = function(content) {
736
+ // this.$element().hide('slow').html(content).show('slow');
737
+ // }
738
+ //
739
+ // get('#/', function() {
740
+ // this.partial('index.html.erb') // will fade out and in
741
+ // });
742
+ //
743
+ // });
744
+ //
745
+ swap: function(content) {
746
+ return this.$element().html(content);
747
+ },
748
+
749
+ // This thows a '404 Not Found' error.
750
+ notFound: function(verb, path) {
751
+ this.trigger('error-404', {verb: verb, path: path});
752
+ throw('404 Not Found ' + verb + ' ' + path);
753
+ },
754
+
755
+ _defineRouteShortcut: function(verb) {
756
+ var app = this;
757
+ this[verb] = function(path, callback) {
758
+ app.route.apply(app, [verb, path, callback]);
759
+ }
760
+ },
761
+
762
+ _checkLocation: function() {
763
+ try { // try, catch 404s
764
+ // get current location
765
+ var location, returned;
766
+ location = this.getLocation();
767
+ // compare to see if hash has changed
768
+ if (location != this.last_location) {
769
+ // lookup route for current hash
770
+ returned = this.runRoute('get', location);
771
+ }
772
+ // reset last location
773
+ this.last_location = location;
774
+ } catch(e) {
775
+ // reset last location
776
+ this.last_location = location;
777
+ // unless the error is a 404 and 404s are silenced
778
+ if (e.toString().match(/^404/) && this.silence_404) {
779
+ return returned;
780
+ } else {
781
+ throw(e);
782
+ }
783
+ }
784
+ return returned;
785
+ },
786
+
787
+ _checkFormSubmission: function(form) {
788
+ var $form, path, verb, params, returned;
789
+ this.trigger('check-form-submission', {form: form});
790
+ $form = $(form);
791
+ path = $form.attr('action');
792
+ verb = $form.attr('method').toString().toLowerCase();
793
+ params = $.extend({}, this._parseFormParams($form), {'$form': $form});
794
+
795
+ try { // catch 404s
796
+ returned = this.runRoute(verb, path, params);
797
+ } catch(e) {
798
+ if (e.toString().match(/^404/) && this.silence_404) {
799
+ return true;
800
+ } else {
801
+ throw(e);
802
+ }
803
+ }
804
+ return (typeof returned == 'undefined') ? false : returned;
805
+ },
806
+
807
+ _parseFormParams: function($form) {
808
+ var params = {};
809
+ $.each($form.serializeArray(), function(i, field) {
810
+ if (params[field.name]) {
811
+ if ($.isArray(params[field.name])) {
812
+ params[field.name].push(field.value);
813
+ } else {
814
+ params[field.name] = [params[field.name], field.value];
815
+ }
816
+ } else {
817
+ params[field.name] = field.value;
818
+ }
819
+ });
820
+ return params;
821
+ },
822
+
823
+ _parseQueryString: function(path) {
824
+ var query = {}, parts, pairs, pair, i;
825
+
826
+ parts = path.match(QUERY_STRING_MATCHER);
827
+ if (parts) {
828
+ pairs = parts[1].split('&');
829
+ for (i = 0; i < pairs.length; i += 1) {
830
+ pair = pairs[i].split('=');
831
+ query[pair[0]] = pair[1];
832
+ }
833
+ }
834
+
835
+ return query;
836
+ },
837
+
838
+ _listen: function(name, callback) {
839
+ return this.$element().bind([name, this.eventNamespace()].join('.'), callback);
840
+ },
841
+
842
+ _unlisten: function(name, callback) {
843
+ return this.$element().unbind([name, this.eventNamespace()].join('.'), callback);
844
+ }
845
+
846
+ });
847
+
848
+ // <tt>Sammy.EventContext</tt> objects are created every time a route is run or a
849
+ // bound event is triggered. The callbacks for these events are evaluated within a <tt>Sammy.EventContext</tt>
850
+ // This within these callbacks the special methods of <tt>EventContext</tt> are available.
851
+ //
852
+ // === Example
853
+ //
854
+ // $.sammy(function() { with(this) {
855
+ // // The context here is this Sammy.Application
856
+ // get('#/:name', function() { with(this) {
857
+ // // The context here is a new Sammy.EventContext
858
+ // if (params['name'] == 'sammy') {
859
+ // partial('name.html.erb', {name: 'Sammy'});
860
+ // } else {
861
+ // redirect('#/somewhere-else')
862
+ // }
863
+ // }});
864
+ // }});
865
+ //
866
+ // Initialize a new EventContext
867
+ //
868
+ // === Arguments
869
+ //
870
+ // +app+:: The <tt>Sammy.Application</tt> this event is called within.
871
+ // +verb+:: The verb invoked to run this context/route.
872
+ // +path+:: The string path invoked to run this context/route.
873
+ // +params+:: An Object of optional params to pass to the context. Is converted
874
+ // to a <tt>Sammy.Object</tt>.
875
+ Sammy.EventContext = function(app, verb, path, params) {
876
+ this.app = app;
877
+ this.verb = verb;
878
+ this.path = path;
879
+ this.params = new Sammy.Object(params);
880
+ }
881
+
882
+ Sammy.EventContext.prototype = $.extend({}, Sammy.Object.prototype, {
883
+
884
+ // A shortcut to the app's <tt>$element()</tt>
885
+ $element: function() {
886
+ return this.app.$element();
887
+ },
888
+
889
+ // Used for rendering remote templates or documents within the current application/DOM.
890
+ // By default Sammy and <tt>partial()</tt> know nothing about how your templates
891
+ // should be interpeted/rendered. This is easy to change, though. <tt>partial()</tt> looks
892
+ // for a method in <tt>EventContext</tt> that matches the extension of the file you're
893
+ // fetching (e.g. 'myfile.template' will look for a template() method, 'myfile.haml' => haml(), etc.)
894
+ // If no matching render method is found it just takes the file contents as is.
895
+ //
896
+ // === Caching
897
+ //
898
+ // If you use the <tt>Sammy.Cache</tt> plugin, remote requests will be automatically cached unless
899
+ // you explicitly set <tt>cache_partials</tt> to <tt>false</tt>
900
+ //
901
+ // === Examples
902
+ //
903
+ // There are a couple different ways to use <tt>partial()</tt>:
904
+ //
905
+ // partial('doc.html');
906
+ // //=> Replaces $element() with the contents of doc.html
907
+ //
908
+ // use(Sammy.Template);
909
+ // //=> includes the template() method
910
+ // partial('doc.template', {name: 'Sammy'});
911
+ // //=> Replaces $element() with the contents of doc.template run through <tt>template()</tt>
912
+ //
913
+ // partial('doc.html', function(data) {
914
+ // // data is the contents of the template.
915
+ // $('.other-selector').html(data);
916
+ // });
917
+ //
918
+ partial: function(path, data, callback) {
919
+ var file_data,
920
+ wrapped_callback,
921
+ engine,
922
+ cache_key = 'partial:' + path,
923
+ context = this;
924
+
925
+ if ((engine = path.match(/\.([^\.]+)$/))) { engine = engine[1]; }
926
+ if (typeof callback == 'undefined') {
927
+ if ($.isFunction(data)) {
928
+ // callback is in the data position
929
+ callback = data;
930
+ data = {};
931
+ } else {
932
+ // we should use the default callback
933
+ callback = function(response) {
934
+ context.app.swap(response);
935
+ }
936
+ }
937
+ }
938
+ data = $.extend({}, data, this);
939
+ wrapped_callback = function(response) {
940
+ if (engine && $.isFunction(context[engine])) {
941
+ response = context[engine].apply(context, [response, data]);
942
+ }
943
+ callback.apply(context, [response]);
944
+ context.trigger('changed');
945
+ };
946
+ if (this.app.cache_partials && this.cache(cache_key)) {
947
+ // try to load the template from the cache
948
+ wrapped_callback.apply(context, [this.cache(cache_key)])
949
+ } else {
950
+ // the template wasnt cached, we need to fetch it
951
+ $.get(path, function(response) {
952
+ if (context.app.cache_partials) context.cache(cache_key, response);
953
+ wrapped_callback.apply(context, [response])
954
+ });
955
+ }
956
+ },
957
+
958
+ // Changes the location of the current window. If <tt>to</tt> begins with
959
+ // '#' it only changes the document's hash. If passed more than 1 argument
960
+ // redirect will join them together with forward slashes.
961
+ //
962
+ // === Example
963
+ //
964
+ // redirect('#/other/route');
965
+ // // equivilent to
966
+ // redirect('#', 'other', 'route');
967
+ //
968
+ redirect: function() {
969
+ var to, args = $.makeArray(arguments),
970
+ current_location = this.app.getLocation();
971
+ if (args.length > 1) {
972
+ args.unshift('/');
973
+ to = this.join.apply(this, args);
974
+ } else {
975
+ to = args[0];
976
+ }
977
+ this.trigger('redirect', {to: to});
978
+ this.app.last_location = this.path;
979
+ this.app.setLocation(to);
980
+ if (current_location == to) {
981
+ this.app.trigger('location-changed');
982
+ }
983
+ },
984
+
985
+ // Triggers events on <tt>app</tt> within the current context.
986
+ trigger: function(name, data) {
987
+ if (typeof data == 'undefined') data = {};
988
+ if (!data.context) data.context = this;
989
+ return this.app.trigger(name, data);
990
+ },
991
+
992
+ // A shortcut to app's <tt>eventNamespace()</tt>
993
+ eventNamespace: function() {
994
+ return this.app.eventNamespace();
995
+ },
996
+
997
+ // Raises a possible <tt>notFound()</tt> error for the current path.
998
+ notFound: function() {
999
+ return this.app.notFound(this.verb, this.path);
1000
+ },
1001
+
1002
+ // //=> Sammy.EventContext: get #/ {}
1003
+ toString: function() {
1004
+ return "Sammy.EventContext: " + [this.verb, this.path, this.params].join(' ');
1005
+ }
1006
+
1007
+ });
1008
+
1009
+ $.sammy = function(app_function) {
1010
+ return new Sammy.Application(app_function);
1011
+ };
1012
+
1013
+ })(jQuery);