rasputin 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ })();