├── .gitignore
├── lib
├── main.js
└── core.js
├── package.json
├── README.md
└── tests
└── routing.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .bpm/
2 | .spade/
3 | assets/
4 | *.bpkg
5 |
--------------------------------------------------------------------------------
/lib/main.js:
--------------------------------------------------------------------------------
1 | require('ember-runtime');
2 | require('sproutcore-routing/core');
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sproutcore-routing",
3 | "summary": "SproutCore Routing",
4 | "description": "HTML5 History based routing (with a hashchange fallback)",
5 | "homepage": "http://sproutcore.com/",
6 | "author": "Strobe Inc., Apple Inc. and contributors",
7 | "version": "2.0.beta.8",
8 | "directories": {
9 | "lib": "lib"
10 | },
11 | "dependencies": {
12 | "spade": "~> 1.0",
13 | "jquery": ">= 0",
14 | "ember-runtime": ">= 0"
15 | },
16 | "dependencies:development": {
17 | "spade-qunit": "~> 1.0.0"
18 | },
19 | "bpm:build": {
20 | "bpm_libs.js": {
21 | "files": ["lib"],
22 | "modes": "*"
23 | },
24 | "sproutcore-routing/bpm_tests.js": {
25 | "files": ["tests"],
26 | "modes": ["debug"]
27 | }
28 | },
29 | "bpm": "1.0.0"
30 | }
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SproutCore Routing
2 | ------------------
3 |
4 | SC.routes manages the browser location. You can change the hash part of the
5 | current location. The following code
6 |
7 | ```javascript
8 | SC.routes.set('location', 'notes/edit/4');
9 | ```
10 |
11 | will change the location to http://domain.tld/my_app#notes/edit/4. Adding
12 | routes will register a handler that will be called whenever the location
13 | changes and matches the route:
14 |
15 | ```javascript
16 | SC.routes.add(':controller/:action/:id', MyApp, MyApp.route);
17 | ```
18 |
19 | You can pass additional parameters in the location hash that will be relayed
20 | to the route handler:
21 |
22 | ```javascript
23 | SC.routes.set('location', 'notes/show/4?format=xml&language=fr');
24 | ```
25 |
26 | The syntax for the location hash is described in the location property
27 | documentation, and the syntax for adding handlers is described in the
28 | add method documentation.
29 |
30 | Browsers keep track of the locations in their history, so when the user
31 | presses the 'back' or 'forward' button, the location is changed, SC.route
32 | catches it and calls your handler. Except for Internet Explorer versions 7
33 | and earlier, which do not modify the history stack when the location hash
34 | changes.
35 |
36 | SC.routes also supports HTML5 history, which uses a '/' instead of a '#'
37 | in the URLs, so that all your website's URLs are consistent.
38 |
--------------------------------------------------------------------------------
/tests/routing.js:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 | // Project: SproutCore - JavaScript Application Framework
3 | // Copyright: ©2006-2011 Strobe Inc. and contributors.
4 | // ©2008-2011 Apple Inc. All rights reserved.
5 | // License: Licensed under MIT license (see license.js)
6 | // ==========================================================================
7 | // ========================================================================
8 | // SC.routes Base Tests
9 | // ========================================================================
10 | /*globals module test ok isObj equals expects */
11 |
12 | var router;
13 |
14 | SC.routes.wantsHistory = YES;
15 |
16 | module('SC.routes setup');
17 |
18 | test('Setup', function() {
19 | equals(SC.routes._didSetup, NO, 'SC.routes should not have been setup yet');
20 | });
21 |
22 | module('SC.routes setup', {
23 |
24 | setup: function() {
25 | router = SC.Object.create({
26 | route: function() {
27 | return;
28 | }
29 | });
30 | SC.run(function() {
31 | SC.routes.add('foo', router, router.route);
32 | });
33 | }
34 |
35 | });
36 |
37 | test('Setup', function() {
38 | equals(SC.routes._didSetup, YES, 'SC.routes should have been setup');
39 | });
40 |
41 | test('Initial route', function() {
42 | equals(SC.routes.get('location'), '', 'Initial route is an empty string');
43 | });
44 |
45 | module('SC.routes._Route', {
46 |
47 | setup: function() {
48 | SC.routes._firstRoute = null;
49 | router = SC.Object.create({
50 | route: function() {
51 | return;
52 | }
53 | });
54 | }
55 |
56 | });
57 |
58 | test('Route tree', function() {
59 | var r = SC.routes._Route.create(),
60 | abc = ['a', 'b', 'c'],
61 | abd = ['a', 'b', 'd'],
62 | abe = ['a', 'b', ':e'],
63 | fs = ['f', '*foo'],
64 | a, b, c, d, e, s, p;
65 |
66 | r.add(abc, router, router.route);
67 | r.add(abd, router, router.route);
68 | r.add(abe, router, router.route);
69 | r.add(fs, router, router.route);
70 |
71 | a = r.staticRoutes.a;
72 | ok(a, 'There should be a staticRoutes tree for a');
73 | ok(!a.target, 'A node should not have a target');
74 | ok(!a.method, 'A node should not have a method');
75 |
76 | b = a.staticRoutes.b;
77 | ok(b, 'There should be a staticRoutes tree for b');
78 | ok(!b.target, 'A node should not have a target');
79 | ok(!b.method, 'A node should not have a method');
80 |
81 | c = b.staticRoutes.c;
82 | ok(c, 'There should be a staticRoutes tree for c');
83 | equals(c.target, router, 'A leaf should have a target');
84 | equals(c.method, router.route, 'A leaf should have a method');
85 |
86 | d = b.staticRoutes.d;
87 | ok(d, 'There should be a staticRoutes tree for d');
88 | equals(d.target, router, 'A leaf should have a target');
89 | equals(d.method, router.route, 'A leaf should have a method');
90 |
91 | e = b.dynamicRoutes.e;
92 | ok(e, 'There should be a dynamicRoutes tree for e');
93 | equals(d.target, router, 'A leaf should have a target');
94 | equals(d.method, router.route, 'A leaf should have a method');
95 |
96 | s = r.staticRoutes.f.wildcardRoutes.foo;
97 | ok(s, 'There should be a wildcardRoutes tree for f');
98 |
99 | equals(r.routeForParts(['a'], {}), null, 'routeForParts should return null for non existant routes');
100 | equals(r.routeForParts(['a', 'b'], {}), null, 'routeForParts should return null for non existant routes');
101 | equals(r.routeForParts(abc, {}), c, 'routeForParts should return the correct route for a/b/c');
102 |
103 | equals(r.routeForParts(abd, {}), d, 'routeForParts should return the correct route for a/b/d');
104 |
105 | abe[2] = 'foo';
106 | p = {};
107 | equals(r.routeForParts(abe, p), e, 'routeForParts should return the correct route for a/b/:e');
108 | equals(p.e, 'foo', 'routeForParts should return the params for a/b/:e');
109 |
110 | p = {};
111 | equals(r.routeForParts(['f', 'double', 'double', 'toil', 'and', 'trouble'], p), s, 'routeForParts should return the correct route for f/*foo');
112 | equals(p.foo, 'double/double/toil/and/trouble', 'routeForParts should return the params for f/*foo');
113 | });
114 |
115 | module('SC.routes location', {
116 |
117 | setup: function() {
118 | SC.routes._firstRoute = null
119 | },
120 |
121 | teardown: function() {
122 | SC.routes.set('location', null);
123 | }
124 |
125 | });
126 |
127 | var routeWorks = function(route, name) {
128 | stop();
129 |
130 | SC.routes.set('location', route);
131 | equals(SC.routes.get('location'), route, name + ' route has been set');
132 |
133 | setTimeout(function() {
134 | equals(SC.routes.get('location'), route, name + ' route is still the same');
135 | start();
136 | }, 300);
137 | };
138 |
139 | test('Null route', function() {
140 | SC.routes.set('location', null);
141 | equals(SC.routes.get('location'), '', 'Null route is the empty string');
142 | });
143 |
144 | test('Simple route', function() {
145 | routeWorks('sixty-six', 'simple');
146 | });
147 |
148 | test('UTF-8 route', function() {
149 | routeWorks('éàçù߀', 'UTF-8');
150 | });
151 |
152 | test('Already escaped route', function() {
153 | routeWorks('%C3%A9%C3%A0%20%C3%A7%C3%B9%20%C3%9F%E2%82%AC', 'already escaped');
154 | });
155 |
156 | module('SC.routes defined routes', {
157 |
158 | setup: function() {
159 | SC.routes._firstRoute = null;
160 | router = SC.Object.create({
161 | params: null,
162 | triggered: NO,
163 | route: function(params) {
164 | this.set('params', params);
165 | },
166 | triggerRoute: function() {
167 | this.triggered = YES;
168 | }
169 | });
170 | },
171 |
172 | teardown: function() {
173 | SC.routes.set('location', null);
174 | }
175 |
176 | });
177 |
178 | test('setting location triggers function when only passed function', function() {
179 | var barred = false;
180 |
181 | SC.routes.add('bar', function(params) {
182 | barred = true;
183 | });
184 | SC.routes.set('location', 'bar');
185 |
186 | ok(barred, 'Function was called');
187 | });
188 |
189 | test('setting location simply triggers route', function() {
190 | SC.routes.add("foo", router, "triggerRoute");
191 | SC.routes.set('location', 'bar');
192 | ok(!router.triggered, "Router not triggered with nonexistent route.");
193 |
194 | SC.routes.set('location', 'foo');
195 | ok(router.triggered, "Router triggered.");
196 | });
197 |
198 | test('calling trigger() triggers current location (again)', function() {
199 | SC.routes.add("foo", router, "triggerRoute");
200 | SC.routes.set('location', 'foo');
201 | ok(router.triggered, "Router triggered first time.");
202 | router.triggered = NO;
203 |
204 | SC.routes.trigger();
205 | ok(router.triggered, "Router triggered (again).");
206 | });
207 |
208 | test('A mix of static, dynamic and wildcard route', function() {
209 | stop();
210 |
211 | var timer = setTimeout(function() {
212 | ok(false, 'Route change was not notified within 2 seconds');
213 | start();
214 | }, 2000);
215 |
216 | router.addObserver('params', function() {
217 | router.removeObserver('params', this);
218 | same(router.get('params'), { controller: 'users', action: 'éàçù߀', id: '5', witches: 'double/double/toil/and/trouble' });
219 | clearTimeout(timer);
220 | start();
221 | });
222 |
223 | SC.routes.add('foo/:controller/:action/bar/:id/*witches', router, router.route);
224 | SC.routes.set('location', 'foo/users/éàçù߀/bar/5/double/double/toil/and/trouble');
225 | });
226 |
227 | test('Route with parameters defined in a string', function() {
228 | stop();
229 |
230 | var timer = setTimeout(function() {
231 | ok(false, 'Route change was not notified within 2 seconds');
232 | start();
233 | }, 2000);
234 |
235 | router.addObserver('params', function() {
236 | router.removeObserver('params', this);
237 | same(router.get('params'), { cuisine: 'french', party: '4', url: '' });
238 | clearTimeout(timer);
239 | start();
240 | });
241 |
242 | SC.routes.add('*url', router, router.route);
243 | SC.routes.set('location', '?cuisine=french&party=4');
244 | });
245 |
246 | test('Route with parameters defined in a hash', function() {
247 | stop();
248 |
249 | var timer = setTimeout(function() {
250 | ok(false, 'Route change was not notified within 2 seconds');
251 | start();
252 | }, 2000);
253 |
254 | router.addObserver('params', function() {
255 | router.removeObserver('params', this);
256 | same(router.get('params'), { cuisine: 'french', party: '4', url: '' });
257 | clearTimeout(timer);
258 | start();
259 | });
260 |
261 | SC.routes.add('*url', router, router.route);
262 | SC.routes.set('location', { cuisine: 'french', party: '4' });
263 | });
264 |
265 | test('A mix of everything', function() {
266 | stop();
267 |
268 | var timer = setTimeout(function() {
269 | ok(false, 'Route change was not notified within 2 seconds');
270 | start();
271 | }, 2000);
272 |
273 | router.addObserver('params', function() {
274 | router.removeObserver('params', this);
275 | same(router.get('params'), { controller: 'users', action: 'éàçù߀', id: '5', witches: 'double/double/toil/and/trouble', cuisine: 'french', party: '4' });
276 | clearTimeout(timer);
277 | start();
278 | });
279 |
280 | SC.routes.add('foo/:controller/:action/bar/:id/*witches', router, router.route);
281 | SC.routes.set('location', 'foo/users/éàçù߀/bar/5/double/double/toil/and/trouble?cuisine=french&party=4');
282 | });
283 |
284 | test('calling exists() returns whether the route is defined or not', function() {
285 | equal(SC.routes.exists('foo'), false, 'Route /foo should not exist');
286 |
287 | SC.routes.add("foo", router, "triggerRoute");
288 |
289 | equal(SC.routes.exists('foo'), true, 'Route /foo should exist');
290 | equal(SC.routes.exists('quux'), false, 'Route /quux should not exist');
291 | });
292 |
293 | module('SC.routes location observing', {
294 |
295 | setup: function() {
296 | SC.routes._firstRoute = null;
297 | router = SC.Object.create({
298 | hasBeenNotified: NO,
299 | route: function(params) {
300 | this.set('hasBeenNotified', YES);
301 | }
302 | });
303 | },
304 |
305 | teardown: function() {
306 | SC.routes.set('location', null);
307 | }
308 |
309 | });
310 |
311 | test('Location change', function() {
312 | if (!SC.routes.get('usesHistory')) {
313 | stop();
314 |
315 | var timer = setTimeout(function() {
316 | ok(false, 'Route change was not notified within 2 seconds');
317 | start();
318 | }, 2000);
319 |
320 | router.addObserver('hasBeenNotified', function() {
321 | clearTimeout(timer);
322 | equals(router.get('hasBeenNotified'), YES, 'router should have been notified');
323 | start();
324 | });
325 |
326 | SC.routes.add('foo', router, router.route);
327 | window.location.hash = 'foo';
328 | }
329 | });
330 |
331 | module('_extractParametersAndRoute');
332 |
333 | test('_extractParametersAndRoute with ? syntax', function() {
334 | same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264' }),
335 | { route: 'videos/5', params:'?format=h264', format: 'h264' },
336 | 'route parameters should be correctly extracted');
337 |
338 | same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264&size=small' }),
339 | { route: 'videos/5', params:'?format=h264&size=small', format: 'h264', size: 'small' },
340 | 'route parameters should be correctly extracted');
341 |
342 | same(SC.routes._extractParametersAndRoute({ route: 'videos/5?format=h264&size=small', format: 'ogg' }),
343 | { route: 'videos/5', params:'?format=ogg&size=small', format: 'ogg', size: 'small' },
344 | 'route parameters should be extracted and overwritten');
345 |
346 | same(SC.routes._extractParametersAndRoute({ route: 'videos/5', format: 'h264', size: 'small' }),
347 | { route: 'videos/5', params:'?format=h264&size=small', format: 'h264', size: 'small' },
348 | 'route should be well formatted with the given parameters');
349 |
350 | same(SC.routes._extractParametersAndRoute({ format: 'h264', size: 'small' }),
351 | { route: '', params:'?format=h264&size=small', format: 'h264', size: 'small' },
352 | 'route should be well formatted with the given parameters even if there is no initial route');
353 | });
354 |
355 | test('_extractParametersAndRoute with & syntax', function() {
356 | same(SC.routes._extractParametersAndRoute({ route: 'videos/5&format=h264' }),
357 | { route: 'videos/5', params:'&format=h264', format: 'h264' },
358 | 'route parameters should be correctly extracted');
359 |
360 | same(SC.routes._extractParametersAndRoute({ route: 'videos/5&format=h264&size=small' }),
361 | { route: 'videos/5', params:'&format=h264&size=small', format: 'h264', size: 'small' },
362 | 'route parameters should be correctly extracted');
363 |
364 | same(SC.routes._extractParametersAndRoute({ route: 'videos/5&format=h264&size=small', format: 'ogg' }),
365 | { route: 'videos/5', params:'&format=ogg&size=small', format: 'ogg', size: 'small' },
366 | 'route parameters should be extracted and overwritten');
367 | });
368 |
--------------------------------------------------------------------------------
/lib/core.js:
--------------------------------------------------------------------------------
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 | // Ember no longer aliases SC
9 | window.SC = window.SC || Ember.Namespace.create();
10 |
11 | var get = Ember.get, set = Ember.set;
12 |
13 | /**
14 | Wether the browser supports HTML5 history.
15 | */
16 | var supportsHistory = !!(window.history && window.history.pushState);
17 |
18 | /**
19 | Wether the browser supports the hashchange event.
20 | */
21 | var supportsHashChange = ('onhashchange' in window) && (document.documentMode === undefined || document.documentMode > 7);
22 |
23 | /**
24 | @class
25 |
26 | Route is a class used internally by SC.routes. The routes defined by your
27 | application are stored in a tree structure, and this is the class for the
28 | nodes.
29 | */
30 | var Route = Ember.Object.extend(
31 | /** @scope Route.prototype */ {
32 |
33 | target: null,
34 |
35 | method: null,
36 |
37 | staticRoutes: null,
38 |
39 | dynamicRoutes: null,
40 |
41 | wildcardRoutes: null,
42 |
43 | add: function(parts, target, method) {
44 | var part, nextRoute;
45 |
46 | // clone the parts array because we are going to alter it
47 | parts = Ember.copy(parts);
48 |
49 | if (!parts || parts.length === 0) {
50 | this.target = target;
51 | this.method = method;
52 |
53 | } else {
54 | part = parts.shift();
55 |
56 | // there are 3 types of routes
57 | switch (part.slice(0, 1)) {
58 |
59 | // 1. dynamic routes
60 | case ':':
61 | part = part.slice(1, part.length);
62 | if (!this.dynamicRoutes) this.dynamicRoutes = {};
63 | if (!this.dynamicRoutes[part]) this.dynamicRoutes[part] = this.constructor.create();
64 | nextRoute = this.dynamicRoutes[part];
65 | break;
66 |
67 | // 2. wildcard routes
68 | case '*':
69 | part = part.slice(1, part.length);
70 | if (!this.wildcardRoutes) this.wildcardRoutes = {};
71 | nextRoute = this.wildcardRoutes[part] = this.constructor.create();
72 | break;
73 |
74 | // 3. static routes
75 | default:
76 | if (!this.staticRoutes) this.staticRoutes = {};
77 | if (!this.staticRoutes[part]) this.staticRoutes[part] = this.constructor.create();
78 | nextRoute = this.staticRoutes[part];
79 | }
80 |
81 | // recursively add the rest of the route
82 | if (nextRoute) nextRoute.add(parts, target, method);
83 | }
84 | },
85 |
86 | routeForParts: function(parts, params) {
87 | var part, key, route;
88 |
89 | // clone the parts array because we are going to alter it
90 | parts = Ember.copy(parts);
91 |
92 | // if parts is empty, we are done
93 | if (!parts || parts.length === 0) {
94 | return this.method ? this : null;
95 |
96 | } else {
97 | part = parts.shift();
98 |
99 | // try to match a static route
100 | if (this.staticRoutes && this.staticRoutes[part]) {
101 | route = this.staticRoutes[part].routeForParts(parts, params);
102 | if (route) {
103 | return route;
104 | }
105 | }
106 |
107 | // else, try to match a dynamic route
108 | for (key in this.dynamicRoutes) {
109 | route = this.dynamicRoutes[key].routeForParts(parts, params);
110 | if (route) {
111 | params[key] = part;
112 | return route;
113 | }
114 | }
115 |
116 | // else, try to match a wilcard route
117 | for (key in this.wildcardRoutes) {
118 | parts.unshift(part);
119 | params[key] = parts.join('/');
120 | return this.wildcardRoutes[key].routeForParts(null, params);
121 | }
122 |
123 | // if nothing was found, it means that there is no match
124 | return null;
125 | }
126 | }
127 |
128 | });
129 |
130 | /**
131 | @class
132 |
133 | SC.routes manages the browser location. You can change the hash part of the
134 | current location. The following code
135 |
136 | SC.routes.set('location', 'notes/edit/4');
137 |
138 | will change the location to http://domain.tld/my_app#notes/edit/4. Adding
139 | routes will register a handler that will be called whenever the location
140 | changes and matches the route:
141 |
142 | SC.routes.add(':controller/:action/:id', MyApp, MyApp.route);
143 |
144 | You can pass additional parameters in the location hash that will be relayed
145 | to the route handler:
146 |
147 | SC.routes.set('location', 'notes/show/4?format=xml&language=fr');
148 |
149 | The syntax for the location hash is described in the location property
150 | documentation, and the syntax for adding handlers is described in the
151 | add method documentation.
152 |
153 | Browsers keep track of the locations in their history, so when the user
154 | presses the 'back' or 'forward' button, the location is changed, SC.route
155 | catches it and calls your handler. Except for Internet Explorer versions 7
156 | and earlier, which do not modify the history stack when the location hash
157 | changes.
158 |
159 | SC.routes also supports HTML5 history, which uses a '/' instead of a '#'
160 | in the URLs, so that all your website's URLs are consistent.
161 | */
162 | var routes = SC.routes = Ember.Object.create(
163 | /** @scope SC.routes.prototype */{
164 |
165 | /**
166 | Set this property to true if you want to use HTML5 history, if available on
167 | the browser, instead of the location hash.
168 |
169 | HTML 5 history uses the history.pushState method and the window's popstate
170 | event.
171 |
172 | By default it is false, so your URLs will look like:
173 |
174 | http://domain.tld/my_app#notes/edit/4
175 |
176 | If set to true and the browser supports pushState(), your URLs will look
177 | like:
178 |
179 | http://domain.tld/my_app/notes/edit/4
180 |
181 | You will also need to make sure that baseURI is properly configured, as
182 | well as your server so that your routes are properly pointing to your
183 | SproutCore application.
184 |
185 | @see http://dev.w3.org/html5/spec/history.html#the-history-interface
186 | @property
187 | @type {Boolean}
188 | */
189 | wantsHistory: false,
190 |
191 | /**
192 | A read-only boolean indicating whether or not HTML5 history is used. Based
193 | on the value of wantsHistory and the browser's support for pushState.
194 |
195 | @see wantsHistory
196 | @property
197 | @type {Boolean}
198 | */
199 | usesHistory: null,
200 |
201 | /**
202 | The base URI used to resolve routes (which are relative URLs). Only used
203 | when usesHistory is equal to true.
204 |
205 | The build tools automatically configure this value if you have the
206 | html5_history option activated in the Buildfile:
207 |
208 | config :my_app, :html5_history => true
209 |
210 | Alternatively, it uses by default the value of the href attribute of the
211 | tag of the HTML document. For example:
212 |
213 |
214 |
215 | The value can also be customized before or during the exectution of the
216 | main() method.
217 |
218 | @see http://www.w3.org/TR/html5/semantics.html#the-base-element
219 | @property
220 | @type {String}
221 | */
222 | baseURI: document.baseURI,
223 |
224 | /** @private
225 | A boolean value indicating whether or not the ping method has been called
226 | to setup the SC.routes.
227 |
228 | @property
229 | @type {Boolean}
230 | */
231 | _didSetup: false,
232 |
233 | /** @private
234 | Internal representation of the current location hash.
235 |
236 | @property
237 | @type {String}
238 | */
239 | _location: null,
240 |
241 | /** @private
242 | Routes are stored in a tree structure, this is the root node.
243 |
244 | @property
245 | @type {Route}
246 | */
247 | _firstRoute: null,
248 |
249 | /** @private
250 | An internal reference to the Route class.
251 |
252 | @property
253 | */
254 | _Route: Route,
255 |
256 | /** @private
257 | Internal method used to extract and merge the parameters of a URL.
258 |
259 | @returns {Hash}
260 | */
261 | _extractParametersAndRoute: function(obj) {
262 | var params = {},
263 | route = obj.route || '',
264 | separator, parts, i, len, crumbs, key;
265 |
266 | separator = (route.indexOf('?') < 0 && route.indexOf('&') >= 0) ? '&' : '?';
267 | parts = route.split(separator);
268 | route = parts[0];
269 | if (parts.length === 1) {
270 | parts = [];
271 | } else if (parts.length === 2) {
272 | parts = parts[1].split('&');
273 | } else if (parts.length > 2) {
274 | parts.shift();
275 | }
276 |
277 | // extract the parameters from the route string
278 | len = parts.length;
279 | for (i = 0; i < len; ++i) {
280 | crumbs = parts[i].split('=');
281 | params[crumbs[0]] = crumbs[1];
282 | }
283 |
284 | // overlay any parameter passed in obj
285 | for (key in obj) {
286 | if (obj.hasOwnProperty(key) && key !== 'route') {
287 | params[key] = '' + obj[key];
288 | }
289 | }
290 |
291 | // build the route
292 | parts = [];
293 | for (key in params) {
294 | parts.push([key, params[key]].join('='));
295 | }
296 | params.params = separator + parts.join('&');
297 | params.route = route;
298 |
299 | return params;
300 | },
301 |
302 | /**
303 | The current location hash. It is the part in the browser's location after
304 | the '#' mark.
305 |
306 | The following code
307 |
308 | SC.routes.set('location', 'notes/edit/4');
309 |
310 | will change the location to http://domain.tld/my_app#notes/edit/4 and call
311 | the correct route handler if it has been registered with the add method.
312 |
313 | You can also pass additional parameters. They will be relayed to the route
314 | handler. For example, the following code
315 |
316 | SC.routes.add(':controller/:action/:id', MyApp, MyApp.route);
317 | SC.routes.set('location', 'notes/show/4?format=xml&language=fr');
318 |
319 | will change the location to
320 | http://domain.tld/my_app#notes/show/4?format=xml&language=fr and call the
321 | MyApp.route method with the following argument:
322 |
323 | { route: 'notes/show/4',
324 | params: '?format=xml&language=fr',
325 | controller: 'notes',
326 | action: 'show',
327 | id: '4',
328 | format: 'xml',
329 | language: 'fr' }
330 |
331 | The location can also be set with a hash, the following code
332 |
333 | SC.routes.set('location',
334 | { route: 'notes/edit/4', format: 'xml', language: 'fr' });
335 |
336 | will change the location to
337 | http://domain.tld/my_app#notes/show/4?format=xml&language=fr.
338 |
339 | The 'notes/show/4&format=xml&language=fr' syntax for passing parameters,
340 | using a '&' instead of a '?', as used in SproutCore 1.0 is still supported.
341 |
342 | @property
343 | @type {String}
344 | */
345 | location: Ember.computed(function(key, value) {
346 | this._skipRoute = false;
347 | return this._extractLocation(key, value);
348 | }).property(),
349 |
350 | _extractLocation: function(key, value) {
351 | var crumbs, encodedValue;
352 |
353 | if (value !== undefined) {
354 | if (value === null) {
355 | value = '';
356 | }
357 |
358 | if (typeof(value) === 'object') {
359 | crumbs = this._extractParametersAndRoute(value);
360 | value = crumbs.route + crumbs.params;
361 | }
362 |
363 | if (!this._skipPush && (!Ember.empty(value) || (this._location && this._location !== value))) {
364 | encodedValue = encodeURI(value);
365 |
366 | if (this.usesHistory) {
367 | var basePath = get(this, 'baseURI');
368 | if (basePath.charAt(basePath.length-1) !== "/") {
369 | basePath += "/";
370 | }
371 |
372 | window.history.pushState(null, null, basePath + encodedValue);
373 | } else if (encodedValue.length > 0 || window.location.hash.length > 0) {
374 | window.location.hash = encodedValue;
375 | }
376 | }
377 |
378 | this._location = value;
379 | }
380 |
381 | return this._location;
382 | },
383 |
384 | updateLocation: function(loc){
385 | this._skipRoute = true;
386 | return this._extractLocation('location', loc);
387 | },
388 |
389 | /**
390 | You usually don't need to call this method. It is done automatically after
391 | the application has been initialized.
392 |
393 | It registers for the hashchange event if available. If not, it creates a
394 | timer that looks for location changes every 150ms.
395 | */
396 | ping: function() {
397 | if (!this._didSetup) {
398 | this._didSetup = true;
399 | var state;
400 |
401 | if (get(this, 'wantsHistory') && supportsHistory) {
402 | this.usesHistory = true;
403 |
404 | // Move any hash state to url state
405 | // TODO: Make sure we have a hash before adding slash
406 | state = window.location.hash.slice(1);
407 | if (state.length > 0) {
408 | state = '/' + state;
409 | window.history.replaceState(null, null, get(this, 'baseURI')+state);
410 | }
411 |
412 | popState();
413 | jQuery(window).bind('popstate', popState);
414 |
415 | } else {
416 | this.usesHistory = false;
417 |
418 | if (get(this, 'wantsHistory')) {
419 | // Move any url state to hash
420 | var base = get(this, 'baseURI');
421 | var loc = (base.charAt(0) === '/') ? document.location.pathname : document.location.href.replace(document.location.hash, '');
422 | state = loc.slice(base.length+1);
423 | if (state.length > 0) {
424 | window.location.href = base+'#'+state;
425 | }
426 | }
427 |
428 | if (supportsHashChange) {
429 | hashChange();
430 | jQuery(window).bind('hashchange', hashChange);
431 |
432 | } else {
433 | var invokeHashChange = function() {
434 | hashChange();
435 | setTimeout(invokeHashChange, 100);
436 | };
437 | invokeHashChange();
438 | }
439 | }
440 | }
441 | },
442 |
443 | /**
444 | Adds a route handler. Routes have the following format:
445 |
446 | - 'users/show/5' is a static route and only matches this exact string,
447 | - ':action/:controller/:id' is a dynamic route and the handler will be
448 | called with the 'action', 'controller' and 'id' parameters passed in a
449 | hash,
450 | - '*url' is a wildcard route, it matches the whole route and the handler
451 | will be called with the 'url' parameter passed in a hash.
452 |
453 | Route types can be combined, the following are valid routes:
454 |
455 | - 'users/:action/:id'
456 | - ':controller/show/:id'
457 | - ':controller/ *url' (ignore the space, because of jslint)
458 |
459 | @param {String} route the route to be registered
460 | @param {Object} target the object on which the method will be called, or
461 | directly the function to be called to handle the route
462 | @param {Function} method the method to be called on target to handle the
463 | route, can be a function or a string
464 | */
465 | add: function(route, target, method) {
466 | if (!this._didSetup) {
467 | Ember.run.once(this, 'ping');
468 | }
469 |
470 | if (method === undefined && Ember.typeOf(target) === 'function') {
471 | method = target;
472 | target = null;
473 | } else if (Ember.typeOf(method) === 'string') {
474 | method = target[method];
475 | }
476 |
477 | if (!this._firstRoute) this._firstRoute = Route.create();
478 | this._firstRoute.add(route.split('/'), target, method);
479 |
480 | return this;
481 | },
482 |
483 | /**
484 | Observer of the 'location' property that calls the correct route handler
485 | when the location changes.
486 | */
487 | locationDidChange: Ember.observer(function() {
488 | this.trigger();
489 | }, 'location'),
490 |
491 | /**
492 | Triggers a route even if already in that route (does change the location, if it
493 | is not already changed, as well).
494 |
495 | If the location is not the same as the supplied location, this simply lets "location"
496 | handle it (which ends up coming back to here).
497 | */
498 | trigger: function() {
499 | var location = get(this, 'location'),
500 | params, route;
501 |
502 | if (this._firstRoute) {
503 | params = this._extractParametersAndRoute({ route: location });
504 | location = params.route;
505 | delete params.route;
506 | delete params.params;
507 |
508 | route = this.getRoute(location, params);
509 | if (route && route.method) {
510 | route.method.call(route.target || this, params);
511 | }
512 | }
513 | },
514 |
515 | getRoute: function(route, params) {
516 | var firstRoute = this._firstRoute;
517 | if (firstRoute == null) {
518 | return null;
519 | }
520 |
521 | if (params == null) {
522 | params = {};
523 | }
524 |
525 | return firstRoute.routeForParts(route.split('/'), params);
526 | },
527 |
528 | exists: function(route, params) {
529 | route = this.getRoute(route, params);
530 | return route !== null && route.method !== null;
531 | }
532 |
533 | });
534 |
535 | /**
536 | Event handler for the hashchange event. Called automatically by the browser
537 | if it supports the hashchange event, or by our timer if not.
538 | */
539 | function hashChange(event) {
540 | var loc = window.location.hash;
541 |
542 | // Remove the '#' prefix
543 | loc = (loc && loc.length > 0) ? loc.slice(1, loc.length) : '';
544 |
545 | if (!jQuery.browser.mozilla) {
546 | // because of bug https://bugzilla.mozilla.org/show_bug.cgi?id=483304
547 | loc = decodeURI(loc);
548 | }
549 |
550 | if (get(routes, 'location') !== loc && !routes._skipRoute) {
551 | Ember.run.once(function() {
552 | routes._skipPush = true;
553 | set(routes, 'location', loc);
554 | routes._skipPush = false;
555 | });
556 | }
557 | routes._skipRoute = false;
558 | }
559 |
560 | function popState(event) {
561 | var base = get(routes, 'baseURI'),
562 | loc = (base.charAt(0) === '/') ? document.location.pathname : document.location.href;
563 |
564 | if (loc.slice(0, base.length) === base) {
565 | // Remove the base prefix and the extra '/'
566 | loc = loc.slice(base.length + 1, loc.length);
567 |
568 | if (get(routes, 'location') !== loc && !routes._skipRoute) {
569 | Ember.run.once(function() {
570 | routes._skipPush = true;
571 | set(routes, 'location', loc);
572 | routes._skipPush = false;
573 | });
574 | }
575 | }
576 | routes._skipRoute = false;
577 | }
578 |
--------------------------------------------------------------------------------