3
Copyright 2012 Yahoo! Inc. All rights reserved.
4
Licensed under the BSD License.
5
http://yuilibrary.com/license/
7
YUI.add('router', function(Y) {
10
Provides URL-based routing using HTML5 `pushState()` or the location hash.
17
var HistoryHash = Y.HistoryHash,
23
// We have to queue up pushState calls to avoid race conditions, since the
24
// popstate event doesn't actually provide any info on what URL it's
29
Fired when the router is ready to begin dispatching to route handlers.
31
You shouldn't need to wait for this event unless you plan to implement some
32
kind of custom dispatching logic. It's used internally in order to avoid
33
dispatching to an initial route if a browser history change occurs first.
36
@param {Boolean} dispatched `true` if routes have already been dispatched
37
(most likely due to a history change).
43
Provides URL-based routing using HTML5 `pushState()` or the location hash.
45
This makes it easy to wire up route handlers for different application states
46
while providing full back/forward navigation support and bookmarkable, shareable
50
@param {Object} [config] Config properties.
51
@param {Boolean} [config.html5] Overrides the default capability detection
52
and forces this router to use (`true`) or not use (`false`) HTML5
54
@param {String} [config.root=''] Root path from which all routes should be
56
@param {Array} [config.routes=[]] Array of route definition objects.
62
Router.superclass.constructor.apply(this, arguments);
65
Y.Router = Y.extend(Router, Y.Base, {
66
// -- Protected Properties -------------------------------------------------
69
Whether or not `_dispatch()` has been called since this router was
79
Whether or not we're currently in the process of dispatching to routes.
81
@property _dispatching
88
Cached copy of the `html5` attribute for internal use.
96
Whether or not the `ready` event has fired yet.
105
Regex used to match parameter placeholders in route paths.
109
1. Parameter prefix character. Either a `:` for subpath parameters that
110
should only match a single level of a path, or `*` for splat parameters
111
that should match any number of path levels.
113
2. Parameter name, if specified, otherwise it is a wildcard match.
115
@property _regexPathParam
119
_regexPathParam: /([:*])([\w\-]+)?/g,
122
Regex that matches and captures the query portion of a URL, minus the
123
preceding `?` character, and discarding the hash portion of the URL if any.
125
@property _regexUrlQuery
129
_regexUrlQuery: /\?([^#]*).*$/,
132
Regex that matches everything before the path portion of a URL (the origin).
133
This will be used to strip this part of the URL from a string when we
136
@property _regexUrlOrigin
140
_regexUrlOrigin: /^(?:[^\/#?:]+:\/\/|\/\/)[^\/]*/,
142
// -- Lifecycle Methods ----------------------------------------------------
143
initializer: function (config) {
146
self._html5 = self.get('html5');
148
self._url = self._getURL();
150
// Necessary because setters don't run on init.
151
self._setRoutes(config && config.routes ? config.routes :
154
// Set up a history instance or hashchange listener.
156
self._history = new Y.HistoryHTML5({force: true});
157
Y.after('history:change', self._afterHistoryChange, self);
159
Y.on('hashchange', self._afterHistoryChange, win, self);
162
// Fire a `ready` event once we're ready to route. We wait first for all
163
// subclass initializers to finish, then for window.onload, and then an
164
// additional 20ms to allow the browser to fire a useless initial
165
// `popstate` event if it wants to (and Chrome always wants to).
166
self.publish(EVT_READY, {
167
defaultFn : self._defReadyFn,
172
self.once('initializedChange', function () {
173
Y.once('load', function () {
174
setTimeout(function () {
175
self.fire(EVT_READY, {dispatched: !!self._dispatched});
181
destructor: function () {
183
Y.detach('history:change', this._afterHistoryChange, this);
185
Y.detach('hashchange', this._afterHistoryChange, win);
189
// -- Public Methods -------------------------------------------------------
192
Dispatches to the first route handler that matches the current URL, if any.
194
If `dispatch()` is called before the `ready` event has fired, it will
195
automatically wait for the `ready` event before dispatching. Otherwise it
196
will dispatch immediately.
201
dispatch: function () {
202
this.once(EVT_READY, function () {
205
if (this._html5 && this.upgrade()) {
208
this._dispatch(this._getPath(), this._getURL());
216
Gets the current route path, relative to the `root` (if any).
219
@return {String} Current route path.
221
getPath: function () {
222
return this._getPath();
226
Returns `true` if this router has at least one route that matches the
227
specified URL, `false` otherwise.
229
This method enforces the same-origin security constraint on the specified
230
`url`; any URL which is not from the same origin as the current URL will
231
always return `false`.
234
@param {String} url URL to match.
235
@return {Boolean} `true` if there's at least one matching route, `false`
238
hasRoute: function (url) {
239
if (!this._hasSameOrigin(url)) {
243
url = this.removeQuery(this.removeRoot(url));
245
return !!this.match(url).length;
249
Returns an array of route objects that match the specified URL path.
251
This method is called internally to determine which routes match the current
252
path whenever the URL changes. You may override it if you want to customize
253
the route matching logic, although this usually shouldn't be necessary.
255
Each returned route object has the following properties:
257
* `callback`: A function or a string representing the name of a function
258
this router that should be executed when the route is triggered.
259
* `keys`: An array of strings representing the named parameters defined in
260
the route's path specification, if any.
261
* `path`: The route's path specification, which may be either a string or
263
* `regex`: A regular expression version of the route's path specification.
264
This regex is used to determine whether the route matches a given path.
267
router.route('/foo', function () {});
268
router.match('/foo');
269
// => [{callback: ..., keys: [], path: '/foo', regex: ...}]
272
@param {String} path URL path to match.
273
@return {Object[]} Array of route objects that match the specified path.
275
match: function (path) {
276
return YArray.filter(this._routes, function (route) {
277
return path.search(route.regex) > -1;
282
Removes the `root` URL from the front of _url_ (if it's there) and returns
283
the result. The returned path will always have a leading `/`.
286
@param {String} url URL.
287
@return {String} Rootless path.
289
removeRoot: function (url) {
290
var root = this.get('root');
292
// Strip out the non-path part of the URL, if any (e.g.
293
// "http://foo.com"), so that we're left with just the path.
294
url = url.replace(this._regexUrlOrigin, '');
296
if (root && url.indexOf(root) === 0) {
297
url = url.substring(root.length);
300
return url.charAt(0) === '/' ? url : '/' + url;
304
Removes a query string from the end of the _url_ (if one exists) and returns
308
@param {String} url URL.
309
@return {String} Queryless path.
311
removeQuery: function (url) {
312
return url.replace(/\?.*$/, '');
316
Replaces the current browser history entry with a new one, and dispatches to
317
the first matching route handler, if any.
319
Behind the scenes, this method uses HTML5 `pushState()` in browsers that
320
support it (or the location hash in older browsers and IE) to change the
323
The specified URL must share the same origin (i.e., protocol, host, and
324
port) as the current page, or an error will occur.
327
// Starting URL: http://example.com/
329
router.replace('/path/');
330
// New URL: http://example.com/path/
332
router.replace('/path?foo=bar');
333
// New URL: http://example.com/path?foo=bar
336
// New URL: http://example.com/
339
@param {String} [url] URL to set. This URL needs to be of the same origin as
340
the current URL. This can be a URL relative to the router's `root`
341
attribute. If no URL is specified, the page's current URL will be used.
345
replace: function (url) {
346
return this._queue(url, true);
350
Adds a route handler for the specified URL _path_.
352
The _path_ parameter may be either a string or a regular expression. If it's
353
a string, it may contain named parameters: `:param` will match any single
354
part of a URL path (not including `/` characters), and `*param` will match
355
any number of parts of a URL path (including `/` characters). These named
356
parameters will be made available as keys on the `req.params` object that's
357
passed to route handlers.
359
If the _path_ parameter is a regex, all pattern matches will be made
360
available as numbered keys on `req.params`, starting with `0` for the full
361
match, then `1` for the first subpattern match, and so on.
363
Here's a set of sample routes along with URL paths that they match:
365
* Route: `/photos/:tag/:page`
366
* URL: `/photos/kittens/1`, params: `{tag: 'kittens', page: '1'}`
367
* URL: `/photos/puppies/2`, params: `{tag: 'puppies', page: '2'}`
369
* Route: `/file/*path`
370
* URL: `/file/foo/bar/baz.txt`, params: `{path: 'foo/bar/baz.txt'}`
371
* URL: `/file/foo`, params: `{path: 'foo'}`
373
If multiple route handlers match a given URL, they will be executed in the
374
order they were added. The first route that was added will be the first to
378
router.route('/photos/:tag/:page', function (req, res, next) {
379
Y.log('Current tag: ' + req.params.tag);
380
Y.log('Current page number: ' + req.params.page);
384
@param {String|RegExp} path Path to match. May be a string or a regular
386
@param {Function|String} callback Callback function to call whenever this
387
route is triggered. If specified as a string, the named function will be
388
called on this router instance.
389
@param {Object} callback.req Request object containing information about
390
the request. It contains the following properties.
391
@param {Array|Object} callback.req.params Captured parameters matched by
392
the route path specification. If a string path was used and contained
393
named parameters, then this will be a key/value hash mapping parameter
394
names to their matched values. If a regex path was used, this will be
395
an array of subpattern matches starting at index 0 for the full match,
396
then 1 for the first subpattern match, and so on.
397
@param {String} callback.req.path The current URL path.
398
@param {Object} callback.req.query Query hash representing the URL query
399
string, if any. Parameter names are keys, and are mapped to parameter
401
@param {String} callback.req.url The full URL.
402
@param {String} callback.req.src What initiated the dispatch. In an
403
HTML5 browser, when the back/forward buttons are used, this property
404
will have a value of "popstate".
405
@param {Object} callback.res Response object containing methods and
406
information that relate to responding to a request. It contains the
407
following properties.
408
@param {Object} callback.res.req Reference to the request object.
409
@param {Function} callback.next Callback to pass control to the next
410
matching route. If you don't call this function, then no further route
411
handlers will be executed, even if there are more that match. If you do
412
call this function, then the next matching route handler (if any) will
413
be called, and will receive the same `req` object that was passed to
414
this route (so you can use the request object to pass data along to
418
route: function (path, callback) {
425
regex : this._getRegex(path, keys)
432
Saves a new browser history entry and dispatches to the first matching route
435
Behind the scenes, this method uses HTML5 `pushState()` in browsers that
436
support it (or the location hash in older browsers and IE) to change the
437
URL and create a history entry.
439
The specified URL must share the same origin (i.e., protocol, host, and
440
port) as the current page, or an error will occur.
443
// Starting URL: http://example.com/
445
router.save('/path/');
446
// New URL: http://example.com/path/
448
router.save('/path?foo=bar');
449
// New URL: http://example.com/path?foo=bar
452
// New URL: http://example.com/
455
@param {String} [url] URL to set. This URL needs to be of the same origin as
456
the current URL. This can be a URL relative to the router's `root`
457
attribute. If no URL is specified, the page's current URL will be used.
461
save: function (url) {
462
return this._queue(url);
466
Upgrades a hash-based URL to an HTML5 URL if necessary. In non-HTML5
467
browsers, this method is a noop.
470
@return {Boolean} `true` if the URL was upgraded, `false` otherwise.
472
upgrade: function () {
477
// Get the full hash in all its glory!
478
var hash = HistoryHash.getHash();
480
if (hash && hash.charAt(0) === '/') {
481
// This is an HTML5 browser and we have a hash-based path in the
482
// URL, so we need to upgrade the URL to a non-hash URL. This
483
// will trigger a `history:change` event, which will in turn
484
// trigger a dispatch.
485
this.once(EVT_READY, function () {
495
// -- Protected Methods ----------------------------------------------------
498
Wrapper around `decodeURIComponent` that also converts `+` chars into
502
@param {String} string String to decode.
503
@return {String} Decoded string.
506
_decode: function (string) {
507
return decodeURIComponent(string.replace(/\+/g, ' '));
511
Shifts the topmost `_save()` call off the queue and executes it. Does
512
nothing if the queue is empty.
519
_dequeue: function () {
523
// If window.onload hasn't yet fired, wait until it has before
524
// dequeueing. This will ensure that we don't call pushState() before an
525
// initial popstate event has fired.
526
if (!YUI.Env.windowLoaded) {
527
Y.once('load', function () {
534
fn = saveQueue.shift();
535
return fn ? fn() : this;
539
Dispatches to the first route handler that matches the specified _path_.
541
If called before the `ready` event has fired, the dispatch will be aborted.
542
This ensures normalized behavior between Chrome (which fires a `popstate`
543
event on every pageview) and other browsers (which do not).
546
@param {String} path URL path.
547
@param {String} url Full URL.
548
@param {String} src What initiated the dispatch.
552
_dispatch: function (path, url, src) {
554
routes = self.match(path),
557
self._dispatching = self._dispatched = true;
559
if (!routes || !routes.length) {
560
self._dispatching = false;
564
req = self._getRequest(path, url, src);
565
res = self._getResponse(req);
567
req.next = function (err) {
568
var callback, matches, route;
572
} else if ((route = routes.shift())) {
573
matches = route.regex.exec(path);
574
callback = typeof route.callback === 'string' ?
575
self[route.callback] : route.callback;
577
// Use named keys for parameter names if the route path contains
578
// named keys. Otherwise, use numerical match indices.
579
if (matches.length === route.keys.length + 1) {
580
req.params = YArray.hash(route.keys, matches.slice(1));
582
req.params = matches.concat();
585
callback.call(self, req, res, req.next);
591
self._dispatching = false;
592
return self._dequeue();
596
Gets the current path from the location hash, or an empty string if the
600
@return {String} Current hash path, or an empty string if the hash is empty.
603
_getHashPath: function () {
604
return HistoryHash.getHash().replace(this._regexUrlQuery, '');
608
Gets the location origin (i.e., protocol, host, and port) as a URL.
614
@return {String} Location origin (i.e., protocol, host, and port).
617
_getOrigin: function () {
618
var location = Y.getLocation();
619
return location.origin || (location.protocol + '//' + location.host);
623
Gets the current route path, relative to the `root` (if any).
626
@return {String} Current route path.
629
_getPath: function () {
630
var path = (!this._html5 && this._getHashPath()) ||
631
Y.getLocation().pathname;
633
return this.removeQuery(this.removeRoot(path));
637
Gets the current route query string.
640
@return {String} Current route query string.
643
_getQuery: function () {
644
var location = Y.getLocation(),
648
return location.search.substring(1);
651
hash = HistoryHash.getHash();
652
matches = hash.match(this._regexUrlQuery);
654
return hash && matches ? matches[1] : location.search.substring(1);
658
Creates a regular expression from the given route specification. If _path_
659
is already a regex, it will be returned unmodified.
662
@param {String|RegExp} path Route path specification.
663
@param {Array} keys Array reference to which route parameter names will be
665
@return {RegExp} Route regex.
668
_getRegex: function (path, keys) {
669
if (path instanceof RegExp) {
673
// Special case for catchall paths.
678
path = path.replace(this._regexPathParam, function (match, operator, key) {
679
// Only `*` operators are supported for key-less matches to allowing
680
// in-path wildcards like: '/foo/*'.
682
return operator === '*' ? '.*' : match;
686
return operator === '*' ? '(.*?)' : '([^/#?]*)';
689
return new RegExp('^' + path + '$');
693
Gets a request object that can be passed to a route handler.
696
@param {String} path Current path being dispatched.
697
@param {String} url Current full URL being dispatched.
698
@param {String} src What initiated the dispatch.
699
@return {Object} Request object.
702
_getRequest: function (path, url, src) {
705
query: this._parseQuery(this._getQuery()),
712
Gets a response object that can be passed to a route handler.
715
@param {Object} req Request object.
716
@return {Object} Response Object.
719
_getResponse: function (req) {
720
// For backwards compatibility, the response object is a function that
721
// calls `next()` on the request object and returns the result.
722
var res = function () {
723
return req.next.apply(this, arguments);
731
Getter for the `routes` attribute.
734
@return {Object[]} Array of route objects.
737
_getRoutes: function () {
738
return this._routes.concat();
742
Gets the current full URL.
745
@return {String} URL.
748
_getURL: function () {
749
return Y.getLocation().toString();
753
Returns `true` when the specified `url` is from the same origin as the
754
current URL; i.e., the protocol, host, and port of the URLs are the same.
756
All host or path relative URLs are of the same origin. A scheme-relative URL
757
is first prefixed with the current scheme before being evaluated.
759
@method _hasSameOrigin
760
@param {String} url URL to compare origin with the current URL.
761
@return {Boolean} Whether the URL has the same origin of the current URL.
764
_hasSameOrigin: function (url) {
765
var origin = ((url && url.match(this._regexUrlOrigin)) || [])[0];
767
// Prepend current scheme to scheme-relative URLs.
768
if (origin && origin.indexOf('//') === 0) {
769
origin = Y.getLocation().protocol + origin;
772
return !origin || origin === this._getOrigin();
776
Joins the `root` URL to the specified _url_, normalizing leading/trailing
780
router.set('root', '/foo');
781
router._joinURL('bar'); // => '/foo/bar'
782
router._joinURL('/bar'); // => '/foo/bar'
784
router.set('root', '/foo/');
785
router._joinURL('bar'); // => '/foo/bar'
786
router._joinURL('/bar'); // => '/foo/bar'
789
@param {String} url URL to append to the `root` URL.
790
@return {String} Joined URL.
793
_joinURL: function (url) {
794
var root = this.get('root');
796
url = this.removeRoot(url);
798
if (url.charAt(0) === '/') {
799
url = url.substring(1);
802
return root && root.charAt(root.length - 1) === '/' ?
808
Parses a URL query string into a key/value hash. If `Y.QueryString.parse` is
809
available, this method will be an alias to that.
812
@param {String} query Query string to parse.
813
@return {Object} Hash of key/value pairs for query parameters.
816
_parseQuery: QS && QS.parse ? QS.parse : function (query) {
817
var decode = this._decode,
818
params = query.split('&'),
824
for (; i < len; ++i) {
825
param = params[i].split('=');
828
result[decode(param[0])] = decode(param[1] || '');
836
Queues up a `_save()` call to run after all previously-queued calls have
839
This is necessary because if we make multiple `_save()` calls before the
840
first call gets dispatched, then both calls will dispatch to the last call's
843
All arguments passed to `_queue()` will be passed on to `_save()` when the
844
queued function is executed.
851
_queue: function () {
852
var args = arguments,
855
saveQueue.push(function () {
857
if (Y.UA.ios && Y.UA.ios < 5) {
858
// iOS <5 has buggy HTML5 history support, and needs to be
860
self._save.apply(self, args);
862
// Wrapped in a timeout to ensure that _save() calls are
863
// always processed asynchronously. This ensures consistency
864
// between HTML5- and hash-based history.
865
setTimeout(function () {
866
self._save.apply(self, args);
870
self._dispatching = true; // otherwise we'll dequeue too quickly
871
self._save.apply(self, args);
877
return !this._dispatching ? this._dequeue() : this;
881
Saves a history entry using either `pushState()` or the location hash.
883
This method enforces the same-origin security constraint; attempting to save
884
a `url` that is not from the same origin as the current URL will result in
888
@param {String} [url] URL for the history entry.
889
@param {Boolean} [replace=false] If `true`, the current history entry will
890
be replaced instead of a new one being added.
894
_save: function (url, replace) {
895
var urlIsString = typeof url === 'string';
897
// Perform same-origin check on the specified URL.
898
if (urlIsString && !this._hasSameOrigin(url)) {
899
Y.error('Security error: The new URL must be of the same origin as the current URL.');
903
// Force _ready to true to ensure that the history change is handled
904
// even if _save is called before the `ready` event fires.
908
this._history[replace ? 'replace' : 'add'](null, {
909
url: urlIsString ? this._joinURL(url) : url
912
// Remove the root from the URL before it's set as the hash.
913
urlIsString && (url = this.removeRoot(url));
915
// The `hashchange` event only fires when the new hash is actually
916
// different. This makes sure we'll always dequeue and dispatch,
917
// mimicking the HTML5 behavior.
918
if (url === HistoryHash.getHash()) {
919
this._dispatch(this._getPath(), this._getURL());
921
HistoryHash[replace ? 'replaceHash' : 'setHash'](url);
929
Setter for the `routes` attribute.
932
@param {Object[]} routes Array of route objects.
933
@return {Object[]} Array of route objects.
936
_setRoutes: function (routes) {
939
YArray.each(routes, function (route) {
940
this.route(route.path, route.callback);
943
return this._routes.concat();
946
// -- Protected Event Handlers ---------------------------------------------
949
Handles `history:change` and `hashchange` events.
951
@method _afterHistoryChange
952
@param {EventFacade} e
955
_afterHistoryChange: function (e) {
959
currentURL = self._getURL();
961
self._url = currentURL;
963
// Handles the awkwardness that is the `popstate` event. HTML5 browsers
964
// fire `popstate` right before they fire `hashchange`, and Chrome fires
965
// `popstate` on page load. If this router is not ready or the previous
966
// and current URLs only differ by their hash, then we want to ignore
967
// this `popstate` event.
968
if (src === 'popstate' &&
969
(!self._ready || prevURL.replace(/#.*$/, '') === currentURL.replace(/#.*$/, ''))) {
974
self._dispatch(self._getPath(), currentURL, src);
977
// -- Default Event Handlers -----------------------------------------------
980
Default handler for the `ready` event.
983
@param {EventFacade} e
986
_defReadyFn: function (e) {
990
// -- Static Properties ----------------------------------------------------
995
Whether or not this browser is capable of using HTML5 history.
997
Setting this to `false` will force the use of hash-based history even on
998
HTML5 browsers, but please don't do this unless you understand the
1006
// Android versions lower than 3.0 are buggy and don't update
1007
// window.location after a pushState() call, so we fall back to
1008
// hash-based history for them.
1010
// See http://code.google.com/p/android/issues/detail?id=17471
1011
valueFn: function () { return Y.Router.html5; },
1012
writeOnce: 'initOnly'
1016
Absolute root path from which all routes should be evaluated.
1018
For example, if your router is running on a page at
1019
`http://example.com/myapp/` and you add a route with the path `/`, your
1020
route will never execute, because the path will always be preceded by
1021
`/myapp`. Setting `root` to `/myapp` would cause all routes to be
1022
evaluated relative to that root URL, so the `/` route would then execute
1023
when the user browses to `http://example.com/myapp/`.
1034
Array of route objects.
1036
Each item in the array must be an object with the following properties:
1038
* `path`: String or regex representing the path to match. See the docs
1039
for the `route()` method for more details.
1041
* `callback`: Function or a string representing the name of a function
1042
on this router instance that should be called when the route is
1043
triggered. See the docs for the `route()` method for more details.
1045
This attribute is intended to be used to set routes at init time, or to
1046
completely reset all routes after init. To add routes after init without
1047
resetting all existing routes, use the `route()` method.
1056
getter: '_getRoutes',
1057
setter: '_setRoutes'
1061
// Used as the default value for the `html5` attribute, and for testing.
1062
html5: Y.HistoryBase.html5 && (!Y.UA.android || Y.UA.android >= 3)
1066
The `Controller` class was deprecated in YUI 3.5.0 and is now an alias for the
1067
`Router` class. Use that class instead. This alias will be removed in a future
1073
@deprecated Use `Router` instead.
1076
Y.Controller = Y.Router;
1079
}, '3.5.1' ,{requires:['array-extras', 'base-build', 'history'], optional:['querystring-parse']});