embient 0.0.1

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