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) {
419
Y.log('Current tag: ' + req.params.tag);
420
Y.log('Current page number: ' + req.params.page);
425
router.findUser = function (req, res, next) {
426
req.user = this.get('users').findById(req.params.user);
430
router.route('/users/:user', 'findUser', function (req, res, next) {
431
// The `findUser` middleware puts the `user` object on the `req`.
432
Y.log('Current user:' req.user.get('name'));
436
@param {String|RegExp} path Path to match. May be a string or a regular
438
@param {Array|Function|String} callbacks* Callback functions to call
439
whenever this route is triggered. These can be specified as separate
440
arguments, or in arrays, or both. If a callback is specified as a
441
string, the named function will be called on this router instance.
443
@param {Object} callbacks.req Request object containing information about
444
the request. It contains the following properties.
446
@param {Array|Object} callbacks.req.params Captured parameters matched by
447
the route path specification. If a string path was used and contained
448
named parameters, then this will be a key/value hash mapping parameter
449
names to their matched values. If a regex path was used, this will be
450
an array of subpattern matches starting at index 0 for the full match,
451
then 1 for the first subpattern match, and so on.
452
@param {String} callbacks.req.path The current URL path.
453
@param {Number} callbacks.req.pendingCallbacks Number of remaining
454
callbacks the route handler has after this one in the dispatch chain.
455
@param {Number} callbacks.req.pendingRoutes Number of matching routes
456
after this one in the dispatch chain.
457
@param {Object} callbacks.req.query Query hash representing the URL
458
query string, if any. Parameter names are keys, and are mapped to
460
@param {String} callbacks.req.url The full URL.
461
@param {String} callbacks.req.src What initiated the dispatch. In an
462
HTML5 browser, when the back/forward buttons are used, this property
463
will have a value of "popstate".
465
@param {Object} callbacks.res Response object containing methods and
466
information that relate to responding to a request. It contains the
467
following properties.
468
@param {Object} callbacks.res.req Reference to the request object.
470
@param {Function} callbacks.next Function to pass control to the next
471
callback or the next matching route if no more callbacks (middleware)
472
exist for the current route handler. If you don't call this function,
473
then no further callbacks or route handlers will be executed, even if
474
there are more that match. If you do call this function, then the next
475
callback (if any) or matching route handler (if any) will be called.
476
All of these functions will receive the same `req` and `res` objects
477
that were passed to this route (so you can use these objects to pass
478
data along to subsequent callbacks and routes).
479
@param {String} [callbacks.next.err] Optional error which will stop the
480
dispatch chaining for this `req`, unless the value is `"route"`, which
481
is special cased to jump skip past any callbacks for the current route
482
and pass control the next route handler.
485
route: function (path, callbacks) {
486
callbacks = YArray.flatten(YArray(arguments, 1, true));
491
callbacks: callbacks,
494
regex : this._getRegex(path, keys),
497
callback: callbacks[0]
504
Saves a new browser history entry and dispatches to the first matching route
507
Behind the scenes, this method uses HTML5 `pushState()` in browsers that
508
support it (or the location hash in older browsers and IE) to change the
509
URL and create a history entry.
511
The specified URL must share the same origin (i.e., protocol, host, and
512
port) as the current page, or an error will occur.
515
// Starting URL: http://example.com/
517
router.save('/path/');
518
// New URL: http://example.com/path/
520
router.save('/path?foo=bar');
521
// New URL: http://example.com/path?foo=bar
524
// New URL: http://example.com/
527
@param {String} [url] URL to set. This URL needs to be of the same origin as
528
the current URL. This can be a URL relative to the router's `root`
529
attribute. If no URL is specified, the page's current URL will be used.
533
save: function (url) {
534
return this._queue(url);
538
Upgrades a hash-based URL to an HTML5 URL if necessary. In non-HTML5
539
browsers, this method is a noop.
542
@return {Boolean} `true` if the URL was upgraded, `false` otherwise.
544
upgrade: function () {
549
// Get the resolve hash path.
550
var hashPath = this._getHashPath();
553
// This is an HTML5 browser and we have a hash-based path in the
554
// URL, so we need to upgrade the URL to a non-hash URL. This
555
// will trigger a `history:change` event, which will in turn
556
// trigger a dispatch.
557
this.once(EVT_READY, function () {
558
this.replace(hashPath);
567
// -- Protected Methods ----------------------------------------------------
570
Wrapper around `decodeURIComponent` that also converts `+` chars into
574
@param {String} string String to decode.
575
@return {String} Decoded string.
578
_decode: function (string) {
579
return decodeURIComponent(string.replace(/\+/g, ' '));
583
Shifts the topmost `_save()` call off the queue and executes it. Does
584
nothing if the queue is empty.
591
_dequeue: function () {
595
// If window.onload hasn't yet fired, wait until it has before
596
// dequeueing. This will ensure that we don't call pushState() before an
597
// initial popstate event has fired.
598
if (!YUI.Env.windowLoaded) {
599
Y.once('load', function () {
606
fn = saveQueue.shift();
607
return fn ? fn() : this;
611
Dispatches to the first route handler that matches the specified _path_.
613
If called before the `ready` event has fired, the dispatch will be aborted.
614
This ensures normalized behavior between Chrome (which fires a `popstate`
615
event on every pageview) and other browsers (which do not).
618
@param {String} path URL path.
619
@param {String} url Full URL.
620
@param {String} src What initiated the dispatch.
624
_dispatch: function (path, url, src) {
626
decode = self._decode,
627
routes = self.match(path),
631
self._dispatching = self._dispatched = true;
633
if (!routes || !routes.length) {
634
self._dispatching = false;
638
req = self._getRequest(path, url, src);
639
res = self._getResponse(req);
641
req.next = function (err) {
642
var callback, name, route;
645
// Special case "route" to skip to the next route handler
646
// avoiding any additional callbacks for the current route.
647
if (err === 'route') {
654
} else if ((callback = callbacks.shift())) {
655
if (typeof callback === 'string') {
657
callback = self[name];
660
Y.error('Router: Callback not found: ' + name, null, 'router');
664
// Allow access to the number of remaining callbacks for the
666
req.pendingCallbacks = callbacks.length;
668
callback.call(self, req, res, req.next);
670
} else if ((route = routes.shift())) {
671
// Make a copy of this route's `callbacks` so the original array
673
callbacks = route.callbacks.concat();
675
// Decode each of the path matches so that the any URL-encoded
676
// path segments are decoded in the `req.params` object.
677
matches = YArray.map(route.regex.exec(path) || [], decode);
679
// Use named keys for parameter names if the route path contains
680
// named keys. Otherwise, use numerical match indices.
681
if (matches.length === route.keys.length + 1) {
682
req.params = YArray.hash(route.keys, matches.slice(1));
684
req.params = matches.concat();
687
// Allow access to the number of remaining routes for this
689
req.pendingRoutes = routes.length;
691
// Execute this route's `callbacks`.
698
self._dispatching = false;
699
return self._dequeue();
703
Returns the resolved path from the hash fragment, or an empty string if the
704
hash is not path-like.
707
@param {String} [hash] Hash fragment to resolve into a path. By default this
708
will be the hash from the current URL.
709
@return {String} Current hash path, or an empty string if the hash is empty.
712
_getHashPath: function (hash) {
713
hash || (hash = HistoryHash.getHash());
715
// Make sure the `hash` is path-like.
716
if (hash && hash.charAt(0) === '/') {
717
return this._joinURL(hash);
724
Gets the location origin (i.e., protocol, host, and port) as a URL.
730
@return {String} Location origin (i.e., protocol, host, and port).
733
_getOrigin: function () {
734
var location = Y.getLocation();
735
return location.origin || (location.protocol + '//' + location.host);
739
Gets the current route path, relative to the `root` (if any).
742
@return {String} Current route path.
745
_getPath: function () {
746
var path = (!this._html5 && this._getHashPath()) ||
747
Y.getLocation().pathname;
749
return this.removeQuery(this.removeRoot(path));
753
Returns the current path root after popping off the last path segment,
754
making it useful for resolving other URL paths against.
756
The path root will always begin and end with a '/'.
759
@return {String} The URL's path root.
763
_getPathRoot: function () {
765
path = Y.getLocation().pathname,
768
if (path.charAt(path.length - 1) === slash) {
772
segments = path.split(slash);
775
return segments.join(slash) + slash;
779
Gets the current route query string.
782
@return {String} Current route query string.
785
_getQuery: function () {
786
var location = Y.getLocation(),
790
return location.search.substring(1);
793
hash = HistoryHash.getHash();
794
matches = hash.match(this._regexUrlQuery);
796
return hash && matches ? matches[1] : location.search.substring(1);
800
Creates a regular expression from the given route specification. If _path_
801
is already a regex, it will be returned unmodified.
804
@param {String|RegExp} path Route path specification.
805
@param {Array} keys Array reference to which route parameter names will be
807
@return {RegExp} Route regex.
810
_getRegex: function (path, keys) {
811
if (path instanceof RegExp) {
815
// Special case for catchall paths.
820
path = path.replace(this._regexPathParam, function (match, operator, key) {
821
// Only `*` operators are supported for key-less matches to allowing
822
// in-path wildcards like: '/foo/*'.
824
return operator === '*' ? '.*' : match;
828
return operator === '*' ? '(.*?)' : '([^/#?]*)';
831
return new RegExp('^' + path + '$');
835
Gets a request object that can be passed to a route handler.
838
@param {String} path Current path being dispatched.
839
@param {String} url Current full URL being dispatched.
840
@param {String} src What initiated the dispatch.
841
@return {Object} Request object.
844
_getRequest: function (path, url, src) {
847
query: this._parseQuery(this._getQuery()),
854
Gets a response object that can be passed to a route handler.
857
@param {Object} req Request object.
858
@return {Object} Response Object.
861
_getResponse: function (req) {
862
// For backwards compatibility, the response object is a function that
863
// calls `next()` on the request object and returns the result.
864
var res = function () {
865
return req.next.apply(this, arguments);
873
Getter for the `routes` attribute.
876
@return {Object[]} Array of route objects.
879
_getRoutes: function () {
880
return this._routes.concat();
884
Gets the current full URL.
887
@return {String} URL.
890
_getURL: function () {
891
var url = Y.getLocation().toString();
894
url = this._upgradeURL(url);
901
Returns `true` when the specified `url` is from the same origin as the
902
current URL; i.e., the protocol, host, and port of the URLs are the same.
904
All host or path relative URLs are of the same origin. A scheme-relative URL
905
is first prefixed with the current scheme before being evaluated.
907
@method _hasSameOrigin
908
@param {String} url URL to compare origin with the current URL.
909
@return {Boolean} Whether the URL has the same origin of the current URL.
912
_hasSameOrigin: function (url) {
913
var origin = ((url && url.match(this._regexUrlOrigin)) || [])[0];
915
// Prepend current scheme to scheme-relative URLs.
916
if (origin && origin.indexOf('//') === 0) {
917
origin = Y.getLocation().protocol + origin;
920
return !origin || origin === this._getOrigin();
924
Joins the `root` URL to the specified _url_, normalizing leading/trailing
928
router.set('root', '/foo');
929
router._joinURL('bar'); // => '/foo/bar'
930
router._joinURL('/bar'); // => '/foo/bar'
932
router.set('root', '/foo/');
933
router._joinURL('bar'); // => '/foo/bar'
934
router._joinURL('/bar'); // => '/foo/bar'
937
@param {String} url URL to append to the `root` URL.
938
@return {String} Joined URL.
941
_joinURL: function (url) {
942
var root = this.get('root');
944
// Causes `url` to _always_ begin with a "/".
945
url = this.removeRoot(url);
947
if (url.charAt(0) === '/') {
948
url = url.substring(1);
951
return root && root.charAt(root.length - 1) === '/' ?
957
Returns a normalized path, ridding it of any '..' segments and properly
958
handling leading and trailing slashes.
960
@method _normalizePath
961
@param {String} path URL path to normalize.
962
@return {String} Normalized path.
966
_normalizePath: function (path) {
969
i, len, normalized, segments, segment, stack;
971
if (!path || path === slash) {
975
segments = path.split(slash);
978
for (i = 0, len = segments.length; i < len; ++i) {
979
segment = segments[i];
981
if (segment === dots) {
983
} else if (segment) {
988
normalized = slash + stack.join(slash);
990
// Append trailing slash if necessary.
991
if (normalized !== slash && path.charAt(path.length - 1) === slash) {
999
Parses a URL query string into a key/value hash. If `Y.QueryString.parse` is
1000
available, this method will be an alias to that.
1003
@param {String} query Query string to parse.
1004
@return {Object} Hash of key/value pairs for query parameters.
1007
_parseQuery: QS && QS.parse ? QS.parse : function (query) {
1008
var decode = this._decode,
1009
params = query.split('&'),
1011
len = params.length,
1015
for (; i < len; ++i) {
1016
param = params[i].split('=');
1019
result[decode(param[0])] = decode(param[1] || '');
1027
Queues up a `_save()` call to run after all previously-queued calls have
1030
This is necessary because if we make multiple `_save()` calls before the
1031
first call gets dispatched, then both calls will dispatch to the last call's
1034
All arguments passed to `_queue()` will be passed on to `_save()` when the
1035
queued function is executed.
1042
_queue: function () {
1043
var args = arguments,
1046
saveQueue.push(function () {
1048
if (Y.UA.ios && Y.UA.ios < 5) {
1049
// iOS <5 has buggy HTML5 history support, and needs to be
1051
self._save.apply(self, args);
1053
// Wrapped in a timeout to ensure that _save() calls are
1054
// always processed asynchronously. This ensures consistency
1055
// between HTML5- and hash-based history.
1056
setTimeout(function () {
1057
self._save.apply(self, args);
1061
self._dispatching = true; // otherwise we'll dequeue too quickly
1062
self._save.apply(self, args);
1068
return !this._dispatching ? this._dequeue() : this;
1072
Returns the normalized result of resolving the `path` against the current
1073
path. Falsy values for `path` will return just the current path.
1075
@method _resolvePath
1076
@param {String} path URL path to resolve.
1077
@return {String} Resolved path.
1081
_resolvePath: function (path) {
1083
return Y.getLocation().pathname;
1086
if (path.charAt(0) !== '/') {
1087
path = this._getPathRoot() + path;
1090
return this._normalizePath(path);
1094
Resolves the specified URL against the current URL.
1096
This method resolves URLs like a browser does and will always return an
1097
absolute URL. When the specified URL is already absolute, it is assumed to
1098
be fully resolved and is simply returned as is. Scheme-relative URLs are
1099
prefixed with the current protocol. Relative URLs are giving the current
1100
URL's origin and are resolved and normalized against the current path root.
1103
@param {String} url URL to resolve.
1104
@return {String} Resolved URL.
1108
_resolveURL: function (url) {
1109
var parts = url && url.match(this._regexURL),
1110
origin, path, query, hash, resolved;
1113
return Y.getLocation().toString();
1121
// Absolute and scheme-relative URLs are assumed to be fully-resolved.
1123
// Prepend the current scheme for scheme-relative URLs.
1124
if (origin.indexOf('//') === 0) {
1125
origin = Y.getLocation().protocol + origin;
1128
return origin + (path || '/') + (query || '') + (hash || '');
1131
// Will default to the current origin and current path.
1132
resolved = this._getOrigin() + this._resolvePath(path);
1134
// A path or query for the specified URL trumps the current URL's.
1135
if (path || query) {
1136
return resolved + (query || '') + (hash || '');
1139
query = this._getQuery();
1141
return resolved + (query ? ('?' + query) : '') + (hash || '');
1145
Saves a history entry using either `pushState()` or the location hash.
1147
This method enforces the same-origin security constraint; attempting to save
1148
a `url` that is not from the same origin as the current URL will result in
1152
@param {String} [url] URL for the history entry.
1153
@param {Boolean} [replace=false] If `true`, the current history entry will
1154
be replaced instead of a new one being added.
1158
_save: function (url, replace) {
1159
var urlIsString = typeof url === 'string',
1162
// Perform same-origin check on the specified URL.
1163
if (urlIsString && !this._hasSameOrigin(url)) {
1164
Y.error('Security error: The new URL must be of the same origin as the current URL.');
1168
// Joins the `url` with the `root`.
1170
url = this._joinURL(url);
1173
// Force _ready to true to ensure that the history change is handled
1174
// even if _save is called before the `ready` event fires.
1178
this._history[replace ? 'replace' : 'add'](null, {url: url});
1180
currentPath = Y.getLocation().pathname;
1181
root = this.get('root');
1183
// Determine if the `root` already exists in the current location's
1184
// `pathname`, and if it does then we can exclude it from the
1185
// hash-based path. No need to duplicate the info in the URL.
1186
if (root === currentPath || root === this._getPathRoot()) {
1187
url = this.removeRoot(url);
1190
// The `hashchange` event only fires when the new hash is actually
1191
// different. This makes sure we'll always dequeue and dispatch
1192
// _all_ router instances, mimicking the HTML5 behavior.
1193
if (url === HistoryHash.getHash()) {
1194
Y.Router.dispatch();
1196
HistoryHash[replace ? 'replaceHash' : 'setHash'](url);
1204
Setter for the `routes` attribute.
1207
@param {Object[]} routes Array of route objects.
1208
@return {Object[]} Array of route objects.
1211
_setRoutes: function (routes) {
1214
YArray.each(routes, function (route) {
1215
// Makes sure to check `callback` for back-compat.
1216
var callbacks = route.callbacks || route.callback;
1218
this.route(route.path, callbacks);
1221
return this._routes.concat();
1225
Upgrades a hash-based URL to a full-path URL, if necessary.
1227
The specified `url` will be upgraded if its of the same origin as the
1228
current URL and has a path-like hash. URLs that don't need upgrading will be
1232
app._upgradeURL('http://example.com/#/foo/'); // => 'http://example.com/foo/';
1235
@param {String} url The URL to upgrade from hash-based to full-path.
1236
@return {String} The upgraded URL, or the specified URL untouched.
1240
_upgradeURL: function (url) {
1241
// We should not try to upgrade paths for external URLs.
1242
if (!this._hasSameOrigin(url)) {
1246
var hash = (url.match(/#(.*)$/) || [])[1] || '',
1247
hashPrefix = Y.HistoryHash.hashPrefix,
1250
// Strip any hash prefix, like hash-bangs.
1251
if (hashPrefix && hash.indexOf(hashPrefix) === 0) {
1252
hash = hash.replace(hashPrefix, '');
1255
// If the hash looks like a URL path, assume it is, and upgrade it!
1257
hashPath = this._getHashPath(hash);
1260
return this._resolveURL(hashPath);
1267
// -- Protected Event Handlers ---------------------------------------------
1270
Handles `history:change` and `hashchange` events.
1272
@method _afterHistoryChange
1273
@param {EventFacade} e
1276
_afterHistoryChange: function (e) {
1279
prevURL = self._url,
1280
currentURL = self._getURL();
1282
self._url = currentURL;
1284
// Handles the awkwardness that is the `popstate` event. HTML5 browsers
1285
// fire `popstate` right before they fire `hashchange`, and Chrome fires
1286
// `popstate` on page load. If this router is not ready or the previous
1287
// and current URLs only differ by their hash, then we want to ignore
1288
// this `popstate` event.
1289
if (src === 'popstate' &&
1290
(!self._ready || prevURL.replace(/#.*$/, '') === currentURL.replace(/#.*$/, ''))) {
1295
self._dispatch(self._getPath(), currentURL, src);
1298
// -- Default Event Handlers -----------------------------------------------
1301
Default handler for the `ready` event.
1304
@param {EventFacade} e
1307
_defReadyFn: function (e) {
1311
// -- Static Properties ----------------------------------------------------
1316
Whether or not this browser is capable of using HTML5 history.
1318
Setting this to `false` will force the use of hash-based history even on
1319
HTML5 browsers, but please don't do this unless you understand the
1327
// Android versions lower than 3.0 are buggy and don't update
1328
// window.location after a pushState() call, so we fall back to
1329
// hash-based history for them.
1331
// See http://code.google.com/p/android/issues/detail?id=17471
1332
valueFn: function () { return Y.Router.html5; },
1333
writeOnce: 'initOnly'
1337
Absolute root path from which all routes should be evaluated.
1339
For example, if your router is running on a page at
1340
`http://example.com/myapp/` and you add a route with the path `/`, your
1341
route will never execute, because the path will always be preceded by
1342
`/myapp`. Setting `root` to `/myapp` would cause all routes to be
1343
evaluated relative to that root URL, so the `/` route would then execute
1344
when the user browses to `http://example.com/myapp/`.
1355
Array of route objects.
1357
Each item in the array must be an object with the following properties:
1359
* `path`: String or regex representing the path to match. See the docs
1360
for the `route()` method for more details.
1362
* `callbacks`: Function or a string representing the name of a
1363
function on this router instance that should be called when the
1364
route is triggered. An array of functions and/or strings may also be
1365
provided. See the docs for the `route()` method for more details.
1367
This attribute is intended to be used to set routes at init time, or to
1368
completely reset all routes after init. To add routes after init without
1369
resetting all existing routes, use the `route()` method.
1378
getter: '_getRoutes',
1379
setter: '_setRoutes'
1383
// Used as the default value for the `html5` attribute, and for testing.
1384
html5: Y.HistoryBase.html5 && (!Y.UA.android || Y.UA.android >= 3),
1386
// To make this testable.
1387
_instances: instances,
1390
Dispatches to the first route handler that matches the specified `path` for
1391
all active router instances.
1393
This provides a mechanism to cause all active router instances to dispatch
1394
to their route handlers without needing to change the URL or fire the
1395
`history:change` or `hashchange` event.
1401
dispatch: function () {
1404
for (i = 0, len = instances.length; i < len; i += 1) {
1405
router = instances[i];
1408
router._dispatch(router._getPath(), router._getURL());
1415
The `Controller` class was deprecated in YUI 3.5.0 and is now an alias for the
1416
`Router` class. Use that class instead. This alias will be removed in a future
1422
@deprecated Use `Router` instead.
1425
Y.Controller = Y.Router;
1428
}, '3.9.1', {"optional": ["querystring-parse"], "requires": ["array-extras", "base-build", "history"]});