1
/* YUI 3.9.1 (build 5852) Copyright 2013 Yahoo! Inc. http://yuilibrary.com/license/ */
2
YUI.add('router', function (Y, NAME) {
5
Provides URL-based routing using HTML5 `pushState()` or the location hash.
12
var HistoryHash = Y.HistoryHash,
18
// Holds all the active router instances. This supports the static
19
// `dispatch()` method which causes all routers to dispatch.
22
// We have to queue up pushState calls to avoid race conditions, since the
23
// popstate event doesn't actually provide any info on what URL it's
28
Fired when the router is ready to begin dispatching to route handlers.
30
You shouldn't need to wait for this event unless you plan to implement some
31
kind of custom dispatching logic. It's used internally in order to avoid
32
dispatching to an initial route if a browser history change occurs first.
35
@param {Boolean} dispatched `true` if routes have already been dispatched
36
(most likely due to a history change).
42
Provides URL-based routing using HTML5 `pushState()` or the location hash.
44
This makes it easy to wire up route handlers for different application states
45
while providing full back/forward navigation support and bookmarkable, shareable
49
@param {Object} [config] Config properties.
50
@param {Boolean} [config.html5] Overrides the default capability detection
51
and forces this router to use (`true`) or not use (`false`) HTML5
53
@param {String} [config.root=''] Root path from which all routes should be
55
@param {Array} [config.routes=[]] Array of route definition objects.
61
Router.superclass.constructor.apply(this, arguments);
64
Y.Router = Y.extend(Router, Y.Base, {
65
// -- Protected Properties -------------------------------------------------
68
Whether or not `_dispatch()` has been called since this router was
78
Whether or not we're currently in the process of dispatching to routes.
80
@property _dispatching
87
History event handle for the `history:change` or `hashchange` event
90
@property _historyEvents
96
Cached copy of the `html5` attribute for internal use.
104
Whether or not the `ready` event has fired yet.
113
Regex used to match parameter placeholders in route paths.
117
1. Parameter prefix character. Either a `:` for subpath parameters that
118
should only match a single level of a path, or `*` for splat parameters
119
that should match any number of path levels.
121
2. Parameter name, if specified, otherwise it is a wildcard match.
123
@property _regexPathParam
127
_regexPathParam: /([:*])([\w\-]+)?/g,
130
Regex that matches and captures the query portion of a URL, minus the
131
preceding `?` character, and discarding the hash portion of the URL if any.
133
@property _regexUrlQuery
137
_regexUrlQuery: /\?([^#]*).*$/,
140
Regex that matches everything before the path portion of a URL (the origin).
141
This will be used to strip this part of the URL from a string when we
144
@property _regexUrlOrigin
148
_regexUrlOrigin: /^(?:[^\/#?:]+:\/\/|\/\/)[^\/]*/,
150
// -- Lifecycle Methods ----------------------------------------------------
151
initializer: function (config) {
154
self._html5 = self.get('html5');
156
self._url = self._getURL();
158
// Necessary because setters don't run on init.
159
self._setRoutes(config && config.routes ? config.routes :
162
// Set up a history instance or hashchange listener.
164
self._history = new Y.HistoryHTML5({force: true});
165
self._historyEvents =
166
Y.after('history:change', self._afterHistoryChange, self);
168
self._historyEvents =
169
Y.on('hashchange', self._afterHistoryChange, win, self);
172
// Fire a `ready` event once we're ready to route. We wait first for all
173
// subclass initializers to finish, then for window.onload, and then an
174
// additional 20ms to allow the browser to fire a useless initial
175
// `popstate` event if it wants to (and Chrome always wants to).
176
self.publish(EVT_READY, {
177
defaultFn : self._defReadyFn,
182
self.once('initializedChange', function () {
183
Y.once('load', function () {
184
setTimeout(function () {
185
self.fire(EVT_READY, {dispatched: !!self._dispatched});
190
// Store this router in the collection of all active router instances.
191
instances.push(this);
194
destructor: function () {
195
var instanceIndex = YArray.indexOf(instances, this);
197
// Remove this router from the collection of active router instances.
198
if (instanceIndex > -1) {
199
instances.splice(instanceIndex, 1);
202
if (this._historyEvents) {
203
this._historyEvents.detach();
207
// -- Public Methods -------------------------------------------------------
210
Dispatches to the first route handler that matches the current URL, if any.
212
If `dispatch()` is called before the `ready` event has fired, it will
213
automatically wait for the `ready` event before dispatching. Otherwise it
214
will dispatch immediately.
219
dispatch: function () {
220
this.once(EVT_READY, function () {
223
if (this._html5 && this.upgrade()) {
226
this._dispatch(this._getPath(), this._getURL());
234
Gets the current route path, relative to the `root` (if any).
237
@return {String} Current route path.
239
getPath: function () {
240
return this._getPath();
244
Returns `true` if this router has at least one route that matches the
245
specified URL, `false` otherwise.
247
This method enforces the same-origin security constraint on the specified
248
`url`; any URL which is not from the same origin as the current URL will
249
always return `false`.
252
@param {String} url URL to match.
253
@return {Boolean} `true` if there's at least one matching route, `false`
256
hasRoute: function (url) {
259
if (!this._hasSameOrigin(url)) {
264
url = this._upgradeURL(url);
267
path = this.removeQuery(this.removeRoot(url));
269
return !!this.match(path).length;
273
Returns an array of route objects that match the specified URL path.
275
This method is called internally to determine which routes match the current
276
path whenever the URL changes. You may override it if you want to customize
277
the route matching logic, although this usually shouldn't be necessary.
279
Each returned route object has the following properties:
281
* `callback`: A function or a string representing the name of a function
282
this router that should be executed when the route is triggered.
284
* `keys`: An array of strings representing the named parameters defined in
285
the route's path specification, if any.
287
* `path`: The route's path specification, which may be either a string or
290
* `regex`: A regular expression version of the route's path specification.
291
This regex is used to determine whether the route matches a given path.
294
router.route('/foo', function () {});
295
router.match('/foo');
296
// => [{callback: ..., keys: [], path: '/foo', regex: ...}]
299
@param {String} path URL path to match.
300
@return {Object[]} Array of route objects that match the specified path.
302
match: function (path) {
303
return YArray.filter(this._routes, function (route) {
304
return path.search(route.regex) > -1;
309
Removes the `root` URL from the front of _url_ (if it's there) and returns
310
the result. The returned path will always have a leading `/`.
313
@param {String} url URL.
314
@return {String} Rootless path.
316
removeRoot: function (url) {
317
var root = this.get('root');
319
// Strip out the non-path part of the URL, if any (e.g.
320
// "http://foo.com"), so that we're left with just the path.
321
url = url.replace(this._regexUrlOrigin, '');
323
if (root && url.indexOf(root) === 0) {
324
url = url.substring(root.length);
327
return url.charAt(0) === '/' ? url : '/' + url;
331
Removes a query string from the end of the _url_ (if one exists) and returns
335
@param {String} url URL.
336
@return {String} Queryless path.
338
removeQuery: function (url) {
339
return url.replace(/\?.*$/, '');
343
Replaces the current browser history entry with a new one, and dispatches to
344
the first matching route handler, if any.
346
Behind the scenes, this method uses HTML5 `pushState()` in browsers that
347
support it (or the location hash in older browsers and IE) to change the
350
The specified URL must share the same origin (i.e., protocol, host, and
351
port) as the current page, or an error will occur.
354
// Starting URL: http://example.com/
356
router.replace('/path/');
357
// New URL: http://example.com/path/
359
router.replace('/path?foo=bar');
360
// New URL: http://example.com/path?foo=bar
363
// New URL: http://example.com/
366
@param {String} [url] URL to set. This URL needs to be of the same origin as
367
the current URL. This can be a URL relative to the router's `root`
368
attribute. If no URL is specified, the page's current URL will be used.
372
replace: function (url) {
373
return this._queue(url, true);
377
Adds a route handler for the specified URL _path_.
379
The _path_ parameter may be either a string or a regular expression. If it's
380
a string, it may contain named parameters: `:param` will match any single
381
part of a URL path (not including `/` characters), and `*param` will match
382
any number of parts of a URL path (including `/` characters). These named
383
parameters will be made available as keys on the `req.params` object that's
384
passed to route handlers.
386
If the _path_ parameter is a regex, all pattern matches will be made
387
available as numbered keys on `req.params`, starting with `0` for the full
388
match, then `1` for the first subpattern match, and so on.
390
Here's a set of sample routes along with URL paths that they match:
392
* Route: `/photos/:tag/:page`
393
* URL: `/photos/kittens/1`, params: `{tag: 'kittens', page: '1'}`
394
* URL: `/photos/puppies/2`, params: `{tag: 'puppies', page: '2'}`
396
* Route: `/file/*path`
397
* URL: `/file/foo/bar/baz.txt`, params: `{path: 'foo/bar/baz.txt'}`
398
* URL: `/file/foo`, params: `{path: 'foo'}`
400
**Middleware**: Routes also support an arbitrary number of callback
401
functions. This allows you to easily reuse parts of your route-handling code
402
with different route. This method is liberal in how it processes the
403
specified `callbacks`, you can specify them as separate arguments, or as
406
If multiple route match a given URL, they will be executed in the order they
407
were added. The first route that was added will be the first to be executed.
409
**Passing Control**: Invoking the `next()` function within a route callback
410
will pass control to the next callback function (if any) or route handler
411
(if any). If a value is passed to `next()`, it's assumed to be an error,
412
therefore stopping the dispatch chain, unless that value is: `"route"`,
413
which is special case and dispatching will skip to the next route handler.
414
This allows middleware to skip any remaining middleware for a particular
418
router.route('/photos/:tag/:page', function (req, res, next) {
423
router.findUser = function (req, res, next) {
424
req.user = this.get('users').findById(req.params.user);
428
router.route('/users/:user', 'findUser', function (req, res, next) {
429
// The `findUser` middleware puts the `user` object on the `req`.
433
@param {String|RegExp} path Path to match. May be a string or a regular
435
@param {Array|Function|String} callbacks* Callback functions to call
436
whenever this route is triggered. These can be specified as separate
437
arguments, or in arrays, or both. If a callback is specified as a
438
string, the named function will be called on this router instance.
440
@param {Object} callbacks.req Request object containing information about
441
the request. It contains the following properties.
443
@param {Array|Object} callbacks.req.params Captured parameters matched by
444
the route path specification. If a string path was used and contained
445
named parameters, then this will be a key/value hash mapping parameter
446
names to their matched values. If a regex path was used, this will be
447
an array of subpattern matches starting at index 0 for the full match,
448
then 1 for the first subpattern match, and so on.
449
@param {String} callbacks.req.path The current URL path.
450
@param {Number} callbacks.req.pendingCallbacks Number of remaining
451
callbacks the route handler has after this one in the dispatch chain.
452
@param {Number} callbacks.req.pendingRoutes Number of matching routes
453
after this one in the dispatch chain.
454
@param {Object} callbacks.req.query Query hash representing the URL
455
query string, if any. Parameter names are keys, and are mapped to
457
@param {String} callbacks.req.url The full URL.
458
@param {String} callbacks.req.src What initiated the dispatch. In an
459
HTML5 browser, when the back/forward buttons are used, this property
460
will have a value of "popstate".
462
@param {Object} callbacks.res Response object containing methods and
463
information that relate to responding to a request. It contains the
464
following properties.
465
@param {Object} callbacks.res.req Reference to the request object.
467
@param {Function} callbacks.next Function to pass control to the next
468
callback or the next matching route if no more callbacks (middleware)
469
exist for the current route handler. If you don't call this function,
470
then no further callbacks or route handlers will be executed, even if
471
there are more that match. If you do call this function, then the next
472
callback (if any) or matching route handler (if any) will be called.
473
All of these functions will receive the same `req` and `res` objects
474
that were passed to this route (so you can use these objects to pass
475
data along to subsequent callbacks and routes).
476
@param {String} [callbacks.next.err] Optional error which will stop the
477
dispatch chaining for this `req`, unless the value is `"route"`, which
478
is special cased to jump skip past any callbacks for the current route
479
and pass control the next route handler.
482
route: function (path, callbacks) {
483
callbacks = YArray.flatten(YArray(arguments, 1, true));
488
callbacks: callbacks,
491
regex : this._getRegex(path, keys),
494
callback: callbacks[0]
501
Saves a new browser history entry and dispatches to the first matching route
504
Behind the scenes, this method uses HTML5 `pushState()` in browsers that
505
support it (or the location hash in older browsers and IE) to change the
506
URL and create a history entry.
508
The specified URL must share the same origin (i.e., protocol, host, and
509
port) as the current page, or an error will occur.
512
// Starting URL: http://example.com/
514
router.save('/path/');
515
// New URL: http://example.com/path/
517
router.save('/path?foo=bar');
518
// New URL: http://example.com/path?foo=bar
521
// New URL: http://example.com/
524
@param {String} [url] URL to set. This URL needs to be of the same origin as
525
the current URL. This can be a URL relative to the router's `root`
526
attribute. If no URL is specified, the page's current URL will be used.
530
save: function (url) {
531
return this._queue(url);
535
Upgrades a hash-based URL to an HTML5 URL if necessary. In non-HTML5
536
browsers, this method is a noop.
539
@return {Boolean} `true` if the URL was upgraded, `false` otherwise.
541
upgrade: function () {
546
// Get the resolve hash path.
547
var hashPath = this._getHashPath();
550
// This is an HTML5 browser and we have a hash-based path in the
551
// URL, so we need to upgrade the URL to a non-hash URL. This
552
// will trigger a `history:change` event, which will in turn
553
// trigger a dispatch.
554
this.once(EVT_READY, function () {
555
this.replace(hashPath);
564
// -- Protected Methods ----------------------------------------------------
567
Wrapper around `decodeURIComponent` that also converts `+` chars into
571
@param {String} string String to decode.
572
@return {String} Decoded string.
575
_decode: function (string) {
576
return decodeURIComponent(string.replace(/\+/g, ' '));
580
Shifts the topmost `_save()` call off the queue and executes it. Does
581
nothing if the queue is empty.
588
_dequeue: function () {
592
// If window.onload hasn't yet fired, wait until it has before
593
// dequeueing. This will ensure that we don't call pushState() before an
594
// initial popstate event has fired.
595
if (!YUI.Env.windowLoaded) {
596
Y.once('load', function () {
603
fn = saveQueue.shift();
604
return fn ? fn() : this;
608
Dispatches to the first route handler that matches the specified _path_.
610
If called before the `ready` event has fired, the dispatch will be aborted.
611
This ensures normalized behavior between Chrome (which fires a `popstate`
612
event on every pageview) and other browsers (which do not).
615
@param {String} path URL path.
616
@param {String} url Full URL.
617
@param {String} src What initiated the dispatch.
621
_dispatch: function (path, url, src) {
623
decode = self._decode,
624
routes = self.match(path),
628
self._dispatching = self._dispatched = true;
630
if (!routes || !routes.length) {
631
self._dispatching = false;
635
req = self._getRequest(path, url, src);
636
res = self._getResponse(req);
638
req.next = function (err) {
639
var callback, name, route;
642
// Special case "route" to skip to the next route handler
643
// avoiding any additional callbacks for the current route.
644
if (err === 'route') {
651
} else if ((callback = callbacks.shift())) {
652
if (typeof callback === 'string') {
654
callback = self[name];
657
Y.error('Router: Callback not found: ' + name, null, 'router');
661
// Allow access to the number of remaining callbacks for the
663
req.pendingCallbacks = callbacks.length;
665
callback.call(self, req, res, req.next);
667
} else if ((route = routes.shift())) {
668
// Make a copy of this route's `callbacks` so the original array
670
callbacks = route.callbacks.concat();
672
// Decode each of the path matches so that the any URL-encoded
673
// path segments are decoded in the `req.params` object.
674
matches = YArray.map(route.regex.exec(path) || [], decode);
676
// Use named keys for parameter names if the route path contains
677
// named keys. Otherwise, use numerical match indices.
678
if (matches.length === route.keys.length + 1) {
679
req.params = YArray.hash(route.keys, matches.slice(1));
681
req.params = matches.concat();
684
// Allow access to the number of remaining routes for this
686
req.pendingRoutes = routes.length;
688
// Execute this route's `callbacks`.
695
self._dispatching = false;
696
return self._dequeue();
700
Returns the resolved path from the hash fragment, or an empty string if the
701
hash is not path-like.
704
@param {String} [hash] Hash fragment to resolve into a path. By default this
705
will be the hash from the current URL.
706
@return {String} Current hash path, or an empty string if the hash is empty.
709
_getHashPath: function (hash) {
710
hash || (hash = HistoryHash.getHash());
712
// Make sure the `hash` is path-like.
713
if (hash && hash.charAt(0) === '/') {
714
return this._joinURL(hash);
721
Gets the location origin (i.e., protocol, host, and port) as a URL.
727
@return {String} Location origin (i.e., protocol, host, and port).
730
_getOrigin: function () {
731
var location = Y.getLocation();
732
return location.origin || (location.protocol + '//' + location.host);
736
Gets the current route path, relative to the `root` (if any).
739
@return {String} Current route path.
742
_getPath: function () {
743
var path = (!this._html5 && this._getHashPath()) ||
744
Y.getLocation().pathname;
746
return this.removeQuery(this.removeRoot(path));
750
Returns the current path root after popping off the last path segment,
751
making it useful for resolving other URL paths against.
753
The path root will always begin and end with a '/'.
756
@return {String} The URL's path root.
760
_getPathRoot: function () {
762
path = Y.getLocation().pathname,
765
if (path.charAt(path.length - 1) === slash) {
769
segments = path.split(slash);
772
return segments.join(slash) + slash;
776
Gets the current route query string.
779
@return {String} Current route query string.
782
_getQuery: function () {
783
var location = Y.getLocation(),
787
return location.search.substring(1);
790
hash = HistoryHash.getHash();
791
matches = hash.match(this._regexUrlQuery);
793
return hash && matches ? matches[1] : location.search.substring(1);
797
Creates a regular expression from the given route specification. If _path_
798
is already a regex, it will be returned unmodified.
801
@param {String|RegExp} path Route path specification.
802
@param {Array} keys Array reference to which route parameter names will be
804
@return {RegExp} Route regex.
807
_getRegex: function (path, keys) {
808
if (path instanceof RegExp) {
812
// Special case for catchall paths.
817
path = path.replace(this._regexPathParam, function (match, operator, key) {
818
// Only `*` operators are supported for key-less matches to allowing
819
// in-path wildcards like: '/foo/*'.
821
return operator === '*' ? '.*' : match;
825
return operator === '*' ? '(.*?)' : '([^/#?]*)';
828
return new RegExp('^' + path + '$');
832
Gets a request object that can be passed to a route handler.
835
@param {String} path Current path being dispatched.
836
@param {String} url Current full URL being dispatched.
837
@param {String} src What initiated the dispatch.
838
@return {Object} Request object.
841
_getRequest: function (path, url, src) {
844
query: this._parseQuery(this._getQuery()),
851
Gets a response object that can be passed to a route handler.
854
@param {Object} req Request object.
855
@return {Object} Response Object.
858
_getResponse: function (req) {
859
// For backwards compatibility, the response object is a function that
860
// calls `next()` on the request object and returns the result.
861
var res = function () {
862
return req.next.apply(this, arguments);
870
Getter for the `routes` attribute.
873
@return {Object[]} Array of route objects.
876
_getRoutes: function () {
877
return this._routes.concat();
881
Gets the current full URL.
884
@return {String} URL.
887
_getURL: function () {
888
var url = Y.getLocation().toString();
891
url = this._upgradeURL(url);
898
Returns `true` when the specified `url` is from the same origin as the
899
current URL; i.e., the protocol, host, and port of the URLs are the same.
901
All host or path relative URLs are of the same origin. A scheme-relative URL
902
is first prefixed with the current scheme before being evaluated.
904
@method _hasSameOrigin
905
@param {String} url URL to compare origin with the current URL.
906
@return {Boolean} Whether the URL has the same origin of the current URL.
909
_hasSameOrigin: function (url) {
910
var origin = ((url && url.match(this._regexUrlOrigin)) || [])[0];
912
// Prepend current scheme to scheme-relative URLs.
913
if (origin && origin.indexOf('//') === 0) {
914
origin = Y.getLocation().protocol + origin;
917
return !origin || origin === this._getOrigin();
921
Joins the `root` URL to the specified _url_, normalizing leading/trailing
925
router.set('root', '/foo');
926
router._joinURL('bar'); // => '/foo/bar'
927
router._joinURL('/bar'); // => '/foo/bar'
929
router.set('root', '/foo/');
930
router._joinURL('bar'); // => '/foo/bar'
931
router._joinURL('/bar'); // => '/foo/bar'
934
@param {String} url URL to append to the `root` URL.
935
@return {String} Joined URL.
938
_joinURL: function (url) {
939
var root = this.get('root');
941
// Causes `url` to _always_ begin with a "/".
942
url = this.removeRoot(url);
944
if (url.charAt(0) === '/') {
945
url = url.substring(1);
948
return root && root.charAt(root.length - 1) === '/' ?
954
Returns a normalized path, ridding it of any '..' segments and properly
955
handling leading and trailing slashes.
957
@method _normalizePath
958
@param {String} path URL path to normalize.
959
@return {String} Normalized path.
963
_normalizePath: function (path) {
966
i, len, normalized, segments, segment, stack;
968
if (!path || path === slash) {
972
segments = path.split(slash);
975
for (i = 0, len = segments.length; i < len; ++i) {
976
segment = segments[i];
978
if (segment === dots) {
980
} else if (segment) {
985
normalized = slash + stack.join(slash);
987
// Append trailing slash if necessary.
988
if (normalized !== slash && path.charAt(path.length - 1) === slash) {
996
Parses a URL query string into a key/value hash. If `Y.QueryString.parse` is
997
available, this method will be an alias to that.
1000
@param {String} query Query string to parse.
1001
@return {Object} Hash of key/value pairs for query parameters.
1004
_parseQuery: QS && QS.parse ? QS.parse : function (query) {
1005
var decode = this._decode,
1006
params = query.split('&'),
1008
len = params.length,
1012
for (; i < len; ++i) {
1013
param = params[i].split('=');
1016
result[decode(param[0])] = decode(param[1] || '');
1024
Queues up a `_save()` call to run after all previously-queued calls have
1027
This is necessary because if we make multiple `_save()` calls before the
1028
first call gets dispatched, then both calls will dispatch to the last call's
1031
All arguments passed to `_queue()` will be passed on to `_save()` when the
1032
queued function is executed.
1039
_queue: function () {
1040
var args = arguments,
1043
saveQueue.push(function () {
1045
if (Y.UA.ios && Y.UA.ios < 5) {
1046
// iOS <5 has buggy HTML5 history support, and needs to be
1048
self._save.apply(self, args);
1050
// Wrapped in a timeout to ensure that _save() calls are
1051
// always processed asynchronously. This ensures consistency
1052
// between HTML5- and hash-based history.
1053
setTimeout(function () {
1054
self._save.apply(self, args);
1058
self._dispatching = true; // otherwise we'll dequeue too quickly
1059
self._save.apply(self, args);
1065
return !this._dispatching ? this._dequeue() : this;
1069
Returns the normalized result of resolving the `path` against the current
1070
path. Falsy values for `path` will return just the current path.
1072
@method _resolvePath
1073
@param {String} path URL path to resolve.
1074
@return {String} Resolved path.
1078
_resolvePath: function (path) {
1080
return Y.getLocation().pathname;
1083
if (path.charAt(0) !== '/') {
1084
path = this._getPathRoot() + path;
1087
return this._normalizePath(path);
1091
Resolves the specified URL against the current URL.
1093
This method resolves URLs like a browser does and will always return an
1094
absolute URL. When the specified URL is already absolute, it is assumed to
1095
be fully resolved and is simply returned as is. Scheme-relative URLs are
1096
prefixed with the current protocol. Relative URLs are giving the current
1097
URL's origin and are resolved and normalized against the current path root.
1100
@param {String} url URL to resolve.
1101
@return {String} Resolved URL.
1105
_resolveURL: function (url) {
1106
var parts = url && url.match(this._regexURL),
1107
origin, path, query, hash, resolved;
1110
return Y.getLocation().toString();
1118
// Absolute and scheme-relative URLs are assumed to be fully-resolved.
1120
// Prepend the current scheme for scheme-relative URLs.
1121
if (origin.indexOf('//') === 0) {
1122
origin = Y.getLocation().protocol + origin;
1125
return origin + (path || '/') + (query || '') + (hash || '');
1128
// Will default to the current origin and current path.
1129
resolved = this._getOrigin() + this._resolvePath(path);
1131
// A path or query for the specified URL trumps the current URL's.
1132
if (path || query) {
1133
return resolved + (query || '') + (hash || '');
1136
query = this._getQuery();
1138
return resolved + (query ? ('?' + query) : '') + (hash || '');
1142
Saves a history entry using either `pushState()` or the location hash.
1144
This method enforces the same-origin security constraint; attempting to save
1145
a `url` that is not from the same origin as the current URL will result in
1149
@param {String} [url] URL for the history entry.
1150
@param {Boolean} [replace=false] If `true`, the current history entry will
1151
be replaced instead of a new one being added.
1155
_save: function (url, replace) {
1156
var urlIsString = typeof url === 'string',
1159
// Perform same-origin check on the specified URL.
1160
if (urlIsString && !this._hasSameOrigin(url)) {
1161
Y.error('Security error: The new URL must be of the same origin as the current URL.');
1165
// Joins the `url` with the `root`.
1167
url = this._joinURL(url);
1170
// Force _ready to true to ensure that the history change is handled
1171
// even if _save is called before the `ready` event fires.
1175
this._history[replace ? 'replace' : 'add'](null, {url: url});
1177
currentPath = Y.getLocation().pathname;
1178
root = this.get('root');
1180
// Determine if the `root` already exists in the current location's
1181
// `pathname`, and if it does then we can exclude it from the
1182
// hash-based path. No need to duplicate the info in the URL.
1183
if (root === currentPath || root === this._getPathRoot()) {
1184
url = this.removeRoot(url);
1187
// The `hashchange` event only fires when the new hash is actually
1188
// different. This makes sure we'll always dequeue and dispatch
1189
// _all_ router instances, mimicking the HTML5 behavior.
1190
if (url === HistoryHash.getHash()) {
1191
Y.Router.dispatch();
1193
HistoryHash[replace ? 'replaceHash' : 'setHash'](url);
1201
Setter for the `routes` attribute.
1204
@param {Object[]} routes Array of route objects.
1205
@return {Object[]} Array of route objects.
1208
_setRoutes: function (routes) {
1211
YArray.each(routes, function (route) {
1212
// Makes sure to check `callback` for back-compat.
1213
var callbacks = route.callbacks || route.callback;
1215
this.route(route.path, callbacks);
1218
return this._routes.concat();
1222
Upgrades a hash-based URL to a full-path URL, if necessary.
1224
The specified `url` will be upgraded if its of the same origin as the
1225
current URL and has a path-like hash. URLs that don't need upgrading will be
1229
app._upgradeURL('http://example.com/#/foo/'); // => 'http://example.com/foo/';
1232
@param {String} url The URL to upgrade from hash-based to full-path.
1233
@return {String} The upgraded URL, or the specified URL untouched.
1237
_upgradeURL: function (url) {
1238
// We should not try to upgrade paths for external URLs.
1239
if (!this._hasSameOrigin(url)) {
1243
var hash = (url.match(/#(.*)$/) || [])[1] || '',
1244
hashPrefix = Y.HistoryHash.hashPrefix,
1247
// Strip any hash prefix, like hash-bangs.
1248
if (hashPrefix && hash.indexOf(hashPrefix) === 0) {
1249
hash = hash.replace(hashPrefix, '');
1252
// If the hash looks like a URL path, assume it is, and upgrade it!
1254
hashPath = this._getHashPath(hash);
1257
return this._resolveURL(hashPath);
1264
// -- Protected Event Handlers ---------------------------------------------
1267
Handles `history:change` and `hashchange` events.
1269
@method _afterHistoryChange
1270
@param {EventFacade} e
1273
_afterHistoryChange: function (e) {
1276
prevURL = self._url,
1277
currentURL = self._getURL();
1279
self._url = currentURL;
1281
// Handles the awkwardness that is the `popstate` event. HTML5 browsers
1282
// fire `popstate` right before they fire `hashchange`, and Chrome fires
1283
// `popstate` on page load. If this router is not ready or the previous
1284
// and current URLs only differ by their hash, then we want to ignore
1285
// this `popstate` event.
1286
if (src === 'popstate' &&
1287
(!self._ready || prevURL.replace(/#.*$/, '') === currentURL.replace(/#.*$/, ''))) {
1292
self._dispatch(self._getPath(), currentURL, src);
1295
// -- Default Event Handlers -----------------------------------------------
1298
Default handler for the `ready` event.
1301
@param {EventFacade} e
1304
_defReadyFn: function (e) {
1308
// -- Static Properties ----------------------------------------------------
1313
Whether or not this browser is capable of using HTML5 history.
1315
Setting this to `false` will force the use of hash-based history even on
1316
HTML5 browsers, but please don't do this unless you understand the
1324
// Android versions lower than 3.0 are buggy and don't update
1325
// window.location after a pushState() call, so we fall back to
1326
// hash-based history for them.
1328
// See http://code.google.com/p/android/issues/detail?id=17471
1329
valueFn: function () { return Y.Router.html5; },
1330
writeOnce: 'initOnly'
1334
Absolute root path from which all routes should be evaluated.
1336
For example, if your router is running on a page at
1337
`http://example.com/myapp/` and you add a route with the path `/`, your
1338
route will never execute, because the path will always be preceded by
1339
`/myapp`. Setting `root` to `/myapp` would cause all routes to be
1340
evaluated relative to that root URL, so the `/` route would then execute
1341
when the user browses to `http://example.com/myapp/`.
1352
Array of route objects.
1354
Each item in the array must be an object with the following properties:
1356
* `path`: String or regex representing the path to match. See the docs
1357
for the `route()` method for more details.
1359
* `callbacks`: Function or a string representing the name of a
1360
function on this router instance that should be called when the
1361
route is triggered. An array of functions and/or strings may also be
1362
provided. See the docs for the `route()` method for more details.
1364
This attribute is intended to be used to set routes at init time, or to
1365
completely reset all routes after init. To add routes after init without
1366
resetting all existing routes, use the `route()` method.
1375
getter: '_getRoutes',
1376
setter: '_setRoutes'
1380
// Used as the default value for the `html5` attribute, and for testing.
1381
html5: Y.HistoryBase.html5 && (!Y.UA.android || Y.UA.android >= 3),
1383
// To make this testable.
1384
_instances: instances,
1387
Dispatches to the first route handler that matches the specified `path` for
1388
all active router instances.
1390
This provides a mechanism to cause all active router instances to dispatch
1391
to their route handlers without needing to change the URL or fire the
1392
`history:change` or `hashchange` event.
1398
dispatch: function () {
1401
for (i = 0, len = instances.length; i < len; i += 1) {
1402
router = instances[i];
1405
router._dispatch(router._getPath(), router._getURL());
1412
The `Controller` class was deprecated in YUI 3.5.0 and is now an alias for the
1413
`Router` class. Use that class instead. This alias will be removed in a future
1419
@deprecated Use `Router` instead.
1422
Y.Controller = Y.Router;
1425
}, '3.9.1', {"optional": ["querystring-parse"], "requires": ["array-extras", "base-build", "history"]});