rasputin 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,298 @@
1
+ // ==========================================================================
2
+ // Project: metamorph
3
+ // Copyright: ©2011 My Company Inc. All rights reserved.
4
+ // ==========================================================================
5
+
6
+ (function(window) {
7
+
8
+ var K = function(){},
9
+ guid = 0,
10
+ document = window.document,
11
+
12
+ // Feature-detect the W3C range API
13
+ supportsRange = ('createRange' in document);
14
+
15
+ // Constructor that supports either Metamorph('foo') or new
16
+ // Metamorph('foo');
17
+ //
18
+ // Takes a string of HTML as the argument.
19
+
20
+ var Metamorph = function(html) {
21
+ var self;
22
+
23
+ if (this instanceof Metamorph) {
24
+ self = this;
25
+ } else {
26
+ self = new K();
27
+ }
28
+
29
+ self.innerHTML = html;
30
+ var myGuid = 'metamorph-'+(guid++);
31
+ self.start = myGuid + '-start';
32
+ self.end = myGuid + '-end';
33
+
34
+ return self;
35
+ };
36
+
37
+ K.prototype = Metamorph.prototype;
38
+
39
+ var rangeFor, htmlFunc, removeFunc, outerHTMLFunc, appendToFunc, startTagFunc, endTagFunc;
40
+
41
+ // create the outer HTML for the current metamorph. this function will be
42
+ // extended by the Internet Explorer version to work around a bug.
43
+ outerHTMLFunc = function() {
44
+ return this.startTag() + this.innerHTML + this.endTag();
45
+ };
46
+
47
+ startTagFunc = function() {
48
+ return "<script id='" + this.start + "' type='text/x-placeholder'></script>";
49
+ };
50
+
51
+ endTagFunc = function() {
52
+ return "<script id='" + this.end + "' type='text/x-placeholder'></script>";
53
+ };
54
+
55
+ // If we have the W3C range API, this process is relatively straight forward.
56
+ if (supportsRange) {
57
+
58
+ // Get a range for the current morph. Optionally include the starting and
59
+ // ending placeholders.
60
+ rangeFor = function(morph, outerToo) {
61
+ var range = document.createRange();
62
+ var before = document.getElementById(morph.start);
63
+ var after = document.getElementById(morph.end);
64
+
65
+ if (outerToo) {
66
+ range.setStartBefore(before);
67
+ range.setEndAfter(after);
68
+ } else {
69
+ range.setStartAfter(before);
70
+ range.setEndBefore(after);
71
+ }
72
+
73
+ return range;
74
+ };
75
+
76
+ htmlFunc = function(html) {
77
+ // get a range for the current metamorph object
78
+ var range = rangeFor(this);
79
+
80
+ // delete the contents of the range, which will be the
81
+ // nodes between the starting and ending placeholder.
82
+ range.deleteContents();
83
+
84
+ // create a new document fragment for the HTML
85
+ var fragment = range.createContextualFragment(html);
86
+
87
+ // inser the fragment into the range
88
+ range.insertNode(fragment);
89
+ };
90
+
91
+ removeFunc = function() {
92
+ // get a range for the current metamorph object including
93
+ // the starting and ending placeholders.
94
+ var range = rangeFor(this, true);
95
+
96
+ // delete the entire range.
97
+ range.deleteContents();
98
+ };
99
+
100
+ appendToFunc = function(node) {
101
+ var range = document.createRange();
102
+ range.setStart(node);
103
+ range.collapse(false);
104
+ var frag = range.createContextualFragment(this.outerHTML());
105
+ node.appendChild(frag);
106
+ };
107
+ } else {
108
+ /**
109
+ * This code is mostly taken from jQuery, with one exception. In jQuery's case, we
110
+ * have some HTML and we need to figure out how to convert it into some nodes.
111
+ *
112
+ * In this case, jQuery needs to scan the HTML looking for an opening tag and use
113
+ * that as the key for the wrap map. In our case, we know the parent node, and
114
+ * can use its type as the key for the wrap map.
115
+ **/
116
+ var wrapMap = {
117
+ select: [ 1, "<select multiple='multiple'>", "</select>" ],
118
+ fieldset: [ 1, "<fieldset>", "</fieldset>" ],
119
+ table: [ 1, "<table>", "</table>" ],
120
+ tbody: [ 2, "<table><tbody>", "</tbody></table>" ],
121
+ tr: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
122
+ colgroup: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
123
+ map: [ 1, "<map>", "</map>" ],
124
+ _default: [ 0, "", "" ]
125
+ };
126
+
127
+ /**
128
+ * Given a parent node and some HTML, generate a set of nodes. Return the first
129
+ * node, which will allow us to traverse the rest using nextSibling.
130
+ *
131
+ * We need to do this because innerHTML in IE does not really parse the nodes.
132
+ **/
133
+ var firstNodeFor = function(parentNode, html) {
134
+ var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default;
135
+ var depth = arr[0], start = arr[1], end = arr[2];
136
+
137
+ var element = document.createElement('div');
138
+ element.innerHTML = start + html + end;
139
+
140
+ for (var i=0; i<=depth; i++) {
141
+ element = element.firstChild;
142
+ }
143
+
144
+ return element;
145
+ };
146
+
147
+ /**
148
+ * Internet Explorer does not allow setting innerHTML if the first element
149
+ * is a "zero-scope" element. This problem can be worked around by making
150
+ * the first node an invisible text node. We, like Modernizr, use &shy;
151
+ **/
152
+ var startTagFuncWithoutShy = startTagFunc;
153
+
154
+ startTagFunc = function() {
155
+ return "&shy;" + startTagFuncWithoutShy.call(this);
156
+ }
157
+
158
+ /**
159
+ * In some cases, Internet Explorer can create an anonymous node in
160
+ * the hierarchy with no tagName. You can create this scenario via:
161
+ *
162
+ * div = document.createElement("div");
163
+ * div.innerHTML = "<table>&shy<script></script><tr><td>hi</td></tr></table>";
164
+ * div.firstChild.firstChild.tagName //=> ""
165
+ *
166
+ * If our script markers are inside such a node, we need to find that
167
+ * node and use *it* as the marker.
168
+ **/
169
+ var realNode = function(start) {
170
+ while (start.parentNode.tagName === "") {
171
+ start = start.parentNode;
172
+ }
173
+
174
+ return start;
175
+ };
176
+
177
+ /**
178
+ * When automatically adding a tbody, Internet Explorer inserts the
179
+ * tbody immediately before the first <tr>. Other browsers create it
180
+ * before the first node, no matter what.
181
+ *
182
+ * This means the the following code:
183
+ *
184
+ * div = document.createElement("div");
185
+ * div.innerHTML = "<table><script id='first'></script><tr><td>hi</td></tr><script id='last'></script></table>
186
+ *
187
+ * Generates the following DOM in IE:
188
+ *
189
+ * + div
190
+ * + table
191
+ * - script id='first'
192
+ * + tbody
193
+ * + tr
194
+ * + td
195
+ * - "hi"
196
+ * - script id='last'
197
+ *
198
+ * Which means that the two script tags, even though they were
199
+ * inserted at the same point in the hierarchy in the original
200
+ * HTML, now have different parents.
201
+ *
202
+ * This code reparents the first script tag by making it the tbody's
203
+ * first child.
204
+ **/
205
+ var fixParentage = function(start, end) {
206
+ if (start.parentNode !== end.parentNode) {
207
+ end.parentNode.insertBefore(start, end.parentNode.firstChild);
208
+ }
209
+ };
210
+
211
+ htmlFunc = function(html) {
212
+ // get the real starting node. see realNode for details.
213
+ var start = realNode(document.getElementById(this.start));
214
+ var end = document.getElementById(this.end);
215
+ var nextSibling;
216
+
217
+ // make sure that the start and end nodes share the same
218
+ // parent. If not, fix it.
219
+ fixParentage(start, end);
220
+
221
+ var node;
222
+
223
+ // remove all of the nodes after the starting placeholder and
224
+ // before the ending placeholder.
225
+ node = start.nextSibling;
226
+ while (node) {
227
+ if (node === end) { break; }
228
+ node.parentNode.removeChild(node);
229
+ node = start.nextSibling;
230
+ }
231
+
232
+ // get the first node for the HTML string, even in cases like
233
+ // tables and lists where a simple innerHTML on a div would
234
+ // swallow some of the content.
235
+ node = firstNodeFor(start.parentNode, html);
236
+
237
+ // copy the nodes for the HTML between the starting and ending
238
+ // placeholder.
239
+ while (node) {
240
+ nextSibling = node.nextSibling;
241
+ end.parentNode.insertBefore(node, end);
242
+ node = nextSibling;
243
+ }
244
+ };
245
+
246
+ // remove the nodes in the DOM representing this metamorph.
247
+ //
248
+ // this includes the starting and ending placeholders.
249
+ removeFunc = function() {
250
+ var start = realNode(document.getElementById(this.start));
251
+ var end = document.getElementById(this.end);
252
+
253
+ this.html('');
254
+ start.parentNode.removeChild(start);
255
+ end.parentNode.removeChild(end);
256
+ };
257
+
258
+ appendToFunc = function(parentNode) {
259
+ var node = firstNodeFor(parentNode, this.outerHTML());
260
+
261
+ while (node) {
262
+ nextSibling = node.nextSibling;
263
+ parentNode.appendChild(node);
264
+ node = nextSibling;
265
+ }
266
+ };
267
+ }
268
+
269
+ Metamorph.prototype.html = function(html) {
270
+ this.checkRemoved();
271
+ if (html === undefined) { return this.innerHTML; }
272
+
273
+ htmlFunc.call(this, html);
274
+
275
+ this.innerHTML = html;
276
+ };
277
+
278
+ Metamorph.prototype.remove = removeFunc;
279
+ Metamorph.prototype.outerHTML = outerHTMLFunc;
280
+ Metamorph.prototype.appendTo = appendToFunc;
281
+ Metamorph.prototype.startTag = startTagFunc;
282
+ Metamorph.prototype.endTag = endTagFunc;
283
+
284
+ Metamorph.prototype.isRemoved = function() {
285
+ var before = document.getElementById(this.start);
286
+ var after = document.getElementById(this.end);
287
+
288
+ return !before || !after;
289
+ };
290
+
291
+ Metamorph.prototype.checkRemoved = function() {
292
+ if (this.isRemoved()) {
293
+ throw new Error("Cannot perform operations on a Metamorph that is not in the DOM.");
294
+ }
295
+ };
296
+
297
+ window.Metamorph = Metamorph;
298
+ })(this);
@@ -1,4 +1,3 @@
1
-
2
1
  (function(exports) {
3
2
  // ==========================================================================
4
3
  // Project: SproutCore IndexSet
@@ -1,4 +1,3 @@
1
-
2
1
  (function() {
3
2
 
4
3
  SC.I18n = I18n;
@@ -0,0 +1,549 @@
1
+ // ==========================================================================
2
+ // Project: SproutCore - JavaScript Application Framework
3
+ // Copyright: ©2006-2011 Strobe Inc. and contributors.
4
+ // Portions ©2008-2011 Apple Inc. All rights reserved.
5
+ // License: Licensed under MIT license (see license.js)
6
+ // ==========================================================================
7
+
8
+ (function() {
9
+
10
+ var get = SC.get, set = SC.set;
11
+
12
+ /**
13
+ Wether the browser supports HTML5 history.
14
+ */
15
+ var supportsHistory = !!(window.history && window.history.pushState);
16
+
17
+ /**
18
+ Wether the browser supports the hashchange event.
19
+ */
20
+ var supportsHashChange = ('onhashchange' in window) && (document.documentMode === undefined || document.documentMode > 7);
21
+
22
+ /**
23
+ @class
24
+
25
+ Route is a class used internally by SC.routes. The routes defined by your
26
+ application are stored in a tree structure, and this is the class for the
27
+ nodes.
28
+ */
29
+ var Route = SC.Object.extend(
30
+ /** @scope Route.prototype */ {
31
+
32
+ target: null,
33
+
34
+ method: null,
35
+
36
+ staticRoutes: null,
37
+
38
+ dynamicRoutes: null,
39
+
40
+ wildcardRoutes: null,
41
+
42
+ add: function(parts, target, method) {
43
+ var part, nextRoute;
44
+
45
+ // clone the parts array because we are going to alter it
46
+ parts = SC.copy(parts);
47
+
48
+ if (!parts || parts.length === 0) {
49
+ this.target = target;
50
+ this.method = method;
51
+
52
+ } else {
53
+ part = parts.shift();
54
+
55
+ // there are 3 types of routes
56
+ switch (part.slice(0, 1)) {
57
+
58
+ // 1. dynamic routes
59
+ case ':':
60
+ part = part.slice(1, part.length);
61
+ if (!this.dynamicRoutes) this.dynamicRoutes = {};
62
+ if (!this.dynamicRoutes[part]) this.dynamicRoutes[part] = this.constructor.create();
63
+ nextRoute = this.dynamicRoutes[part];
64
+ break;
65
+
66
+ // 2. wildcard routes
67
+ case '*':
68
+ part = part.slice(1, part.length);
69
+ if (!this.wildcardRoutes) this.wildcardRoutes = {};
70
+ nextRoute = this.wildcardRoutes[part] = this.constructor.create();
71
+ break;
72
+
73
+ // 3. static routes
74
+ default:
75
+ if (!this.staticRoutes) this.staticRoutes = {};
76
+ if (!this.staticRoutes[part]) this.staticRoutes[part] = this.constructor.create();
77
+ nextRoute = this.staticRoutes[part];
78
+ }
79
+
80
+ // recursively add the rest of the route
81
+ if (nextRoute) nextRoute.add(parts, target, method);
82
+ }
83
+ },
84
+
85
+ routeForParts: function(parts, params) {
86
+ var part, key, route;
87
+
88
+ // clone the parts array because we are going to alter it
89
+ parts = SC.copy(parts);
90
+
91
+ // if parts is empty, we are done
92
+ if (!parts || parts.length === 0) {
93
+ return this.method ? this : null;
94
+
95
+ } else {
96
+ part = parts.shift();
97
+
98
+ // try to match a static route
99
+ if (this.staticRoutes && this.staticRoutes[part]) {
100
+ return this.staticRoutes[part].routeForParts(parts, params);
101
+
102
+ } else {
103
+
104
+ // else, try to match a dynamic route
105
+ for (key in this.dynamicRoutes) {
106
+ route = this.dynamicRoutes[key].routeForParts(parts, params);
107
+ if (route) {
108
+ params[key] = part;
109
+ return route;
110
+ }
111
+ }
112
+
113
+ // else, try to match a wilcard route
114
+ for (key in this.wildcardRoutes) {
115
+ parts.unshift(part);
116
+ params[key] = parts.join('/');
117
+ return this.wildcardRoutes[key].routeForParts(null, params);
118
+ }
119
+
120
+ // if nothing was found, it means that there is no match
121
+ return null;
122
+ }
123
+ }
124
+ }
125
+
126
+ });
127
+
128
+ /**
129
+ @class
130
+
131
+ SC.routes manages the browser location. You can change the hash part of the
132
+ current location. The following code
133
+
134
+ SC.routes.set('location', 'notes/edit/4');
135
+
136
+ will change the location to http://domain.tld/my_app#notes/edit/4. Adding
137
+ routes will register a handler that will be called whenever the location
138
+ changes and matches the route:
139
+
140
+ SC.routes.add(':controller/:action/:id', MyApp, MyApp.route);
141
+
142
+ You can pass additional parameters in the location hash that will be relayed
143
+ to the route handler:
144
+
145
+ SC.routes.set('location', 'notes/show/4?format=xml&language=fr');
146
+
147
+ The syntax for the location hash is described in the location property
148
+ documentation, and the syntax for adding handlers is described in the
149
+ add method documentation.
150
+
151
+ Browsers keep track of the locations in their history, so when the user
152
+ presses the 'back' or 'forward' button, the location is changed, SC.route
153
+ catches it and calls your handler. Except for Internet Explorer versions 7
154
+ and earlier, which do not modify the history stack when the location hash
155
+ changes.
156
+
157
+ SC.routes also supports HTML5 history, which uses a '/' instead of a '#'
158
+ in the URLs, so that all your website's URLs are consistent.
159
+ */
160
+ var routes = SC.routes = SC.Object.create(
161
+ /** @scope SC.routes.prototype */{
162
+
163
+ /**
164
+ Set this property to true if you want to use HTML5 history, if available on
165
+ the browser, instead of the location hash.
166
+
167
+ HTML 5 history uses the history.pushState method and the window's popstate
168
+ event.
169
+
170
+ By default it is false, so your URLs will look like:
171
+
172
+ http://domain.tld/my_app#notes/edit/4
173
+
174
+ If set to true and the browser supports pushState(), your URLs will look
175
+ like:
176
+
177
+ http://domain.tld/my_app/notes/edit/4
178
+
179
+ You will also need to make sure that baseURI is properly configured, as
180
+ well as your server so that your routes are properly pointing to your
181
+ SproutCore application.
182
+
183
+ @see http://dev.w3.org/html5/spec/history.html#the-history-interface
184
+ @property
185
+ @type {Boolean}
186
+ */
187
+ wantsHistory: false,
188
+
189
+ /**
190
+ A read-only boolean indicating whether or not HTML5 history is used. Based
191
+ on the value of wantsHistory and the browser's support for pushState.
192
+
193
+ @see wantsHistory
194
+ @property
195
+ @type {Boolean}
196
+ */
197
+ usesHistory: null,
198
+
199
+ /**
200
+ The base URI used to resolve routes (which are relative URLs). Only used
201
+ when usesHistory is equal to true.
202
+
203
+ The build tools automatically configure this value if you have the
204
+ html5_history option activated in the Buildfile:
205
+
206
+ config :my_app, :html5_history => true
207
+
208
+ Alternatively, it uses by default the value of the href attribute of the
209
+ <base> tag of the HTML document. For example:
210
+
211
+ <base href="http://domain.tld/my_app">
212
+
213
+ The value can also be customized before or during the exectution of the
214
+ main() method.
215
+
216
+ @see http://www.w3.org/TR/html5/semantics.html#the-base-element
217
+ @property
218
+ @type {String}
219
+ */
220
+ baseURI: document.baseURI,
221
+
222
+ /** @private
223
+ A boolean value indicating whether or not the ping method has been called
224
+ to setup the SC.routes.
225
+
226
+ @property
227
+ @type {Boolean}
228
+ */
229
+ _didSetup: false,
230
+
231
+ /** @private
232
+ Internal representation of the current location hash.
233
+
234
+ @property
235
+ @type {String}
236
+ */
237
+ _location: null,
238
+
239
+ /** @private
240
+ Routes are stored in a tree structure, this is the root node.
241
+
242
+ @property
243
+ @type {Route}
244
+ */
245
+ _firstRoute: null,
246
+
247
+ /** @private
248
+ An internal reference to the Route class.
249
+
250
+ @property
251
+ */
252
+ _Route: Route,
253
+
254
+ /** @private
255
+ Internal method used to extract and merge the parameters of a URL.
256
+
257
+ @returns {Hash}
258
+ */
259
+ _extractParametersAndRoute: function(obj) {
260
+ var params = {},
261
+ route = obj.route || '',
262
+ separator, parts, i, len, crumbs, key;
263
+
264
+ separator = (route.indexOf('?') < 0 && route.indexOf('&') >= 0) ? '&' : '?';
265
+ parts = route.split(separator);
266
+ route = parts[0];
267
+ if (parts.length === 1) {
268
+ parts = [];
269
+ } else if (parts.length === 2) {
270
+ parts = parts[1].split('&');
271
+ } else if (parts.length > 2) {
272
+ parts.shift();
273
+ }
274
+
275
+ // extract the parameters from the route string
276
+ len = parts.length;
277
+ for (i = 0; i < len; ++i) {
278
+ crumbs = parts[i].split('=');
279
+ params[crumbs[0]] = crumbs[1];
280
+ }
281
+
282
+ // overlay any parameter passed in obj
283
+ for (key in obj) {
284
+ if (obj.hasOwnProperty(key) && key !== 'route') {
285
+ params[key] = '' + obj[key];
286
+ }
287
+ }
288
+
289
+ // build the route
290
+ parts = [];
291
+ for (key in params) {
292
+ parts.push([key, params[key]].join('='));
293
+ }
294
+ params.params = separator + parts.join('&');
295
+ params.route = route;
296
+
297
+ return params;
298
+ },
299
+
300
+ /**
301
+ The current location hash. It is the part in the browser's location after
302
+ the '#' mark.
303
+
304
+ The following code
305
+
306
+ SC.routes.set('location', 'notes/edit/4');
307
+
308
+ will change the location to http://domain.tld/my_app#notes/edit/4 and call
309
+ the correct route handler if it has been registered with the add method.
310
+
311
+ You can also pass additional parameters. They will be relayed to the route
312
+ handler. For example, the following code
313
+
314
+ SC.routes.add(':controller/:action/:id', MyApp, MyApp.route);
315
+ SC.routes.set('location', 'notes/show/4?format=xml&language=fr');
316
+
317
+ will change the location to
318
+ http://domain.tld/my_app#notes/show/4?format=xml&language=fr and call the
319
+ MyApp.route method with the following argument:
320
+
321
+ { route: 'notes/show/4',
322
+ params: '?format=xml&language=fr',
323
+ controller: 'notes',
324
+ action: 'show',
325
+ id: '4',
326
+ format: 'xml',
327
+ language: 'fr' }
328
+
329
+ The location can also be set with a hash, the following code
330
+
331
+ SC.routes.set('location',
332
+ { route: 'notes/edit/4', format: 'xml', language: 'fr' });
333
+
334
+ will change the location to
335
+ http://domain.tld/my_app#notes/show/4?format=xml&language=fr.
336
+
337
+ The 'notes/show/4&format=xml&language=fr' syntax for passing parameters,
338
+ using a '&' instead of a '?', as used in SproutCore 1.0 is still supported.
339
+
340
+ @property
341
+ @type {String}
342
+ */
343
+ location: function(key, value) {
344
+ this._skipRoute = false;
345
+ return this._extractLocation(key, value);
346
+ }.property(),
347
+
348
+ _extractLocation: function(key, value) {
349
+ var crumbs, encodedValue;
350
+
351
+ if (value !== undefined) {
352
+ if (value === null) {
353
+ value = '';
354
+ }
355
+
356
+ if (typeof(value) === 'object') {
357
+ crumbs = this._extractParametersAndRoute(value);
358
+ value = crumbs.route + crumbs.params;
359
+ }
360
+
361
+ if (!SC.empty(value) || (this._location && this._location !== value)) {
362
+ encodedValue = encodeURI(value);
363
+
364
+ if (this.usesHistory) {
365
+ if (encodedValue.length > 0) {
366
+ encodedValue = '/' + encodedValue;
367
+ }
368
+ window.history.pushState(null, null, get(this, 'baseURI') + encodedValue);
369
+ } else {
370
+ window.location.hash = encodedValue;
371
+ }
372
+ }
373
+
374
+ this._location = value;
375
+ }
376
+
377
+ return this._location;
378
+ },
379
+
380
+ /**
381
+ You usually don't need to call this method. It is done automatically after
382
+ the application has been initialized.
383
+
384
+ It registers for the hashchange event if available. If not, it creates a
385
+ timer that looks for location changes every 150ms.
386
+ */
387
+ ping: function() {
388
+ var that;
389
+
390
+ if (!this._didSetup) {
391
+ this._didSetup = true;
392
+
393
+ if (get(this, 'wantsHistory') && supportsHistory) {
394
+ this.usesHistory = true;
395
+
396
+ popState();
397
+ jQuery(window).bind('popstate', popState);
398
+
399
+ } else {
400
+ this.usesHistory = false;
401
+
402
+ if (supportsHashChange) {
403
+ hashChange();
404
+ jQuery(window).bind('hashchange', hashChange);
405
+
406
+ } else {
407
+ // we don't use a SC.Timer because we don't want
408
+ // a run loop to be triggered at each ping
409
+ that = this;
410
+ this._invokeHashChange = function() {
411
+ that.hashChange();
412
+ setTimeout(that._invokeHashChange, 100);
413
+ };
414
+ this._invokeHashChange();
415
+ }
416
+ }
417
+ }
418
+ },
419
+
420
+ /**
421
+ Adds a route handler. Routes have the following format:
422
+
423
+ - 'users/show/5' is a static route and only matches this exact string,
424
+ - ':action/:controller/:id' is a dynamic route and the handler will be
425
+ called with the 'action', 'controller' and 'id' parameters passed in a
426
+ hash,
427
+ - '*url' is a wildcard route, it matches the whole route and the handler
428
+ will be called with the 'url' parameter passed in a hash.
429
+
430
+ Route types can be combined, the following are valid routes:
431
+
432
+ - 'users/:action/:id'
433
+ - ':controller/show/:id'
434
+ - ':controller/ *url' (ignore the space, because of jslint)
435
+
436
+ @param {String} route the route to be registered
437
+ @param {Object} target the object on which the method will be called, or
438
+ directly the function to be called to handle the route
439
+ @param {Function} method the method to be called on target to handle the
440
+ route, can be a function or a string
441
+ */
442
+ add: function(route, target, method) {
443
+ if (!this._didSetup) {
444
+ SC.run.once(this, 'ping');
445
+ }
446
+
447
+ if (method === undefined && SC.typeOf(target) === 'function') {
448
+ method = target;
449
+ target = null;
450
+ } else if (SC.typeOf(method) === 'string') {
451
+ method = target[method];
452
+ }
453
+
454
+ if (!this._firstRoute) this._firstRoute = Route.create();
455
+ this._firstRoute.add(route.split('/'), target, method);
456
+
457
+ return this;
458
+ },
459
+
460
+ /**
461
+ Observer of the 'location' property that calls the correct route handler
462
+ when the location changes.
463
+ */
464
+ locationDidChange: function() {
465
+ this.trigger();
466
+ }.observes('location'),
467
+
468
+ /**
469
+ Triggers a route even if already in that route (does change the location, if it
470
+ is not already changed, as well).
471
+
472
+ If the location is not the same as the supplied location, this simply lets "location"
473
+ handle it (which ends up coming back to here).
474
+ */
475
+ trigger: function() {
476
+ var location = get(this, 'location'),
477
+ params, route;
478
+
479
+ if (this._firstRoute) {
480
+ params = this._extractParametersAndRoute({ route: location });
481
+ location = params.route;
482
+ delete params.route;
483
+ delete params.params;
484
+
485
+ route = this.getRoute(location, params);
486
+ if (route && route.method) {
487
+ route.method.call(route.target || this, params);
488
+ }
489
+ }
490
+ },
491
+
492
+ getRoute: function(route, params) {
493
+ var firstRoute = this._firstRoute;
494
+ if (params == null) {
495
+ params = {}
496
+ }
497
+
498
+ return firstRoute.routeForParts(route.split('/'), params);
499
+ },
500
+
501
+ exists: function(route, params) {
502
+ route = this.getRoute(route, params);
503
+ return route != null && route.method != null;
504
+ }
505
+
506
+ });
507
+
508
+ /**
509
+ Event handler for the hashchange event. Called automatically by the browser
510
+ if it supports the hashchange event, or by our timer if not.
511
+ */
512
+ function hashChange(event) {
513
+ var loc = window.location.hash;
514
+
515
+ // Remove the '#' prefix
516
+ loc = (loc && loc.length > 0) ? loc.slice(1, loc.length) : '';
517
+
518
+ if (!jQuery.browser.mozilla) {
519
+ // because of bug https://bugzilla.mozilla.org/show_bug.cgi?id=483304
520
+ loc = decodeURI(loc);
521
+ }
522
+
523
+ if (get(routes, 'location') !== loc && !routes._skipRoute) {
524
+ SC.run.once(function() {
525
+ set(routes, 'location', loc);
526
+ });
527
+ }
528
+ routes._skipRoute = false;
529
+ }
530
+
531
+ function popState(event) {
532
+ var base = get(routes, 'baseURI'),
533
+ loc = document.location.href;
534
+
535
+ if (loc.slice(0, base.length) === base) {
536
+
537
+ // Remove the base prefix and the extra '/'
538
+ loc = loc.slice(base.length + 1, loc.length);
539
+
540
+ if (get(routes, 'location') !== loc && !routes._skipRoute) {
541
+ SC.run.once(function() {
542
+ set(routes, 'location', loc);
543
+ });
544
+ }
545
+ }
546
+ routes._skipRoute = false;
547
+ }
548
+
549
+ })();