Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('router', function (Y, NAME) {
2
 
3
/**
4
Provides URL-based routing using HTML5 `pushState()` or the location hash.
5
 
6
@module app
7
@submodule router
8
@since 3.4.0
9
**/
10
 
11
var HistoryHash = Y.HistoryHash,
12
    QS          = Y.QueryString,
13
    YArray      = Y.Array,
14
    YLang       = Y.Lang,
15
    YObject     = Y.Object,
16
 
17
    win = Y.config.win,
18
 
19
    // Holds all the active router instances. This supports the static
20
    // `dispatch()` method which causes all routers to dispatch.
21
    instances = [],
22
 
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
25
    // associated with.
26
    saveQueue = [],
27
 
28
    /**
29
    Fired when the router is ready to begin dispatching to route handlers.
30
 
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.
34
 
35
    @event ready
36
    @param {Boolean} dispatched `true` if routes have already been dispatched
37
      (most likely due to a history change).
38
    @fireOnce
39
    **/
40
    EVT_READY = 'ready';
41
 
42
/**
43
Provides URL-based routing using HTML5 `pushState()` or the location hash.
44
 
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
47
URLs.
48
 
49
@class Router
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
53
        history.
54
    @param {String} [config.root=''] Root path from which all routes should be
55
        evaluated.
56
    @param {Array} [config.routes=[]] Array of route definition objects.
57
@constructor
58
@extends Base
59
@since 3.4.0
60
**/
61
function Router() {
62
    Router.superclass.constructor.apply(this, arguments);
63
}
64
 
65
Y.Router = Y.extend(Router, Y.Base, {
66
    // -- Protected Properties -------------------------------------------------
67
 
68
    /**
69
    Whether or not `_dispatch()` has been called since this router was
70
    instantiated.
71
 
72
    @property _dispatched
73
    @type Boolean
74
    @default undefined
75
    @protected
76
    **/
77
 
78
    /**
79
    Whether or not we're currently in the process of dispatching to routes.
80
 
81
    @property _dispatching
82
    @type Boolean
83
    @default undefined
84
    @protected
85
    **/
86
 
87
    /**
88
    History event handle for the `history:change` or `hashchange` event
89
    subscription.
90
 
91
    @property _historyEvents
92
    @type EventHandle
93
    @protected
94
    **/
95
 
96
    /**
97
    Cached copy of the `html5` attribute for internal use.
98
 
99
    @property _html5
100
    @type Boolean
101
    @protected
102
    **/
103
 
104
    /**
105
    Map which holds the registered param handlers in the form:
106
    `name` -> RegExp | Function.
107
 
108
    @property _params
109
    @type Object
110
    @protected
111
    @since 3.12.0
112
    **/
113
 
114
    /**
115
    Whether or not the `ready` event has fired yet.
116
 
117
    @property _ready
118
    @type Boolean
119
    @default undefined
120
    @protected
121
    **/
122
 
123
    /**
124
    Regex used to break up a URL string around the URL's path.
125
 
126
    Subpattern captures:
127
 
128
      1. Origin, everything before the URL's path-part.
129
      2. The URL's path-part.
130
      3. The URL's query.
131
      4. The URL's hash fragment.
132
 
133
    @property _regexURL
134
    @type RegExp
135
    @protected
136
    @since 3.5.0
137
    **/
138
    _regexURL: /^((?:[^\/#?:]+:\/\/|\/\/)[^\/]*)?([^?#]*)(\?[^#]*)?(#.*)?$/,
139
 
140
    /**
141
    Regex used to match parameter placeholders in route paths.
142
 
143
    Subpattern captures:
144
 
145
      1. Parameter prefix character. Either a `:` for subpath parameters that
146
         should only match a single level of a path, or `*` for splat parameters
147
         that should match any number of path levels.
148
 
149
      2. Parameter name, if specified, otherwise it is a wildcard match.
150
 
151
    @property _regexPathParam
152
    @type RegExp
153
    @protected
154
    **/
155
    _regexPathParam: /([:*])([\w\-]+)?/g,
156
 
157
    /**
158
    Regex that matches and captures the query portion of a URL, minus the
159
    preceding `?` character, and discarding the hash portion of the URL if any.
160
 
161
    @property _regexUrlQuery
162
    @type RegExp
163
    @protected
164
    **/
165
    _regexUrlQuery: /\?([^#]*).*$/,
166
 
167
    /**
168
    Regex that matches everything before the path portion of a URL (the origin).
169
    This will be used to strip this part of the URL from a string when we
170
    only want the path.
171
 
172
    @property _regexUrlOrigin
173
    @type RegExp
174
    @protected
175
    **/
176
    _regexUrlOrigin: /^(?:[^\/#?:]+:\/\/|\/\/)[^\/]*/,
177
 
178
    /**
179
    Collection of registered routes.
180
 
181
    @property _routes
182
    @type Array
183
    @protected
184
    **/
185
 
186
    // -- Lifecycle Methods ----------------------------------------------------
187
    initializer: function (config) {
188
        var self = this;
189
 
190
        self._html5  = self.get('html5');
191
        self._params = {};
192
        self._routes = [];
193
        self._url    = self._getURL();
194
 
195
        // Necessary because setters don't run on init.
196
        self._setRoutes(config && config.routes ? config.routes :
197
                self.get('routes'));
198
 
199
        // Set up a history instance or hashchange listener.
200
        if (self._html5) {
201
            self._history       = new Y.HistoryHTML5({force: true});
202
            self._historyEvents =
203
                    Y.after('history:change', self._afterHistoryChange, self);
204
        } else {
205
            self._historyEvents =
206
                    Y.on('hashchange', self._afterHistoryChange, win, self);
207
        }
208
 
209
        // Fire a `ready` event once we're ready to route. We wait first for all
210
        // subclass initializers to finish, then for window.onload, and then an
211
        // additional 20ms to allow the browser to fire a useless initial
212
        // `popstate` event if it wants to (and Chrome always wants to).
213
        self.publish(EVT_READY, {
214
            defaultFn  : self._defReadyFn,
215
            fireOnce   : true,
216
            preventable: false
217
        });
218
 
219
        self.once('initializedChange', function () {
220
            Y.once('load', function () {
221
                setTimeout(function () {
222
                    self.fire(EVT_READY, {dispatched: !!self._dispatched});
223
                }, 20);
224
            });
225
        });
226
 
227
        // Store this router in the collection of all active router instances.
228
        instances.push(this);
229
    },
230
 
231
    destructor: function () {
232
        var instanceIndex = YArray.indexOf(instances, this);
233
 
234
        // Remove this router from the collection of active router instances.
235
        if (instanceIndex > -1) {
236
            instances.splice(instanceIndex, 1);
237
        }
238
 
239
        if (this._historyEvents) {
240
            this._historyEvents.detach();
241
        }
242
    },
243
 
244
    // -- Public Methods -------------------------------------------------------
245
 
246
    /**
247
    Dispatches to the first route handler that matches the current URL, if any.
248
 
249
    If `dispatch()` is called before the `ready` event has fired, it will
250
    automatically wait for the `ready` event before dispatching. Otherwise it
251
    will dispatch immediately.
252
 
253
    @method dispatch
254
    @chainable
255
    **/
256
    dispatch: function () {
257
        this.once(EVT_READY, function () {
258
            var req, res;
259
 
260
            this._ready = true;
261
 
262
            if (!this.upgrade()) {
263
                req = this._getRequest('dispatch');
264
                res = this._getResponse(req);
265
 
266
                this._dispatch(req, res);
267
            }
268
        });
269
 
270
        return this;
271
    },
272
 
273
    /**
274
    Gets the current route path.
275
 
276
    @method getPath
277
    @return {String} Current route path.
278
    **/
279
    getPath: function () {
280
        return this._getPath();
281
    },
282
 
283
    /**
284
    Returns `true` if this router has at least one route that matches the
285
    specified URL, `false` otherwise. This also checks that any named `param`
286
    handlers also accept app param values in the `url`.
287
 
288
    This method enforces the same-origin security constraint on the specified
289
    `url`; any URL which is not from the same origin as the current URL will
290
    always return `false`.
291
 
292
    @method hasRoute
293
    @param {String} url URL to match.
294
    @return {Boolean} `true` if there's at least one matching route, `false`
295
      otherwise.
296
    **/
297
    hasRoute: function (url) {
298
        var path, routePath, routes;
299
 
300
        if (!this._hasSameOrigin(url)) {
301
            return false;
302
        }
303
 
304
        if (!this._html5) {
305
            url = this._upgradeURL(url);
306
        }
307
 
308
        // Get just the path portion of the specified `url`. The `match()`
309
        // method does some special checking that the `path` is within the root.
310
        path   = this.removeQuery(url.replace(this._regexUrlOrigin, ''));
311
        routes = this.match(path);
312
 
313
        if (!routes.length) {
314
            return false;
315
        }
316
 
317
        routePath = this.removeRoot(path);
318
 
319
        // Check that there's at least one route whose param handlers also
320
        // accept all the param values.
321
        return !!YArray.filter(routes, function (route) {
322
            // Get the param values for the route and path to see whether the
323
            // param handlers accept or reject the param values. Include any
324
            // route whose named param handlers accept *all* param values. This
325
            // will return `false` if a param handler rejects a param value.
326
            return this._getParamValues(route, routePath);
327
        }, this).length;
328
    },
329
 
330
    /**
331
    Returns an array of route objects that match the specified URL path.
332
 
333
    If this router has a `root`, then the specified `path` _must_ be
334
    semantically within the `root` path to match any routes.
335
 
336
    This method is called internally to determine which routes match the current
337
    path whenever the URL changes. You may override it if you want to customize
338
    the route matching logic, although this usually shouldn't be necessary.
339
 
340
    Each returned route object has the following properties:
341
 
342
      * `callback`: A function or a string representing the name of a function
343
        this router that should be executed when the route is triggered.
344
 
345
      * `keys`: An array of strings representing the named parameters defined in
346
        the route's path specification, if any.
347
 
348
      * `path`: The route's path specification, which may be either a string or
349
        a regex.
350
 
351
      * `regex`: A regular expression version of the route's path specification.
352
        This regex is used to determine whether the route matches a given path.
353
 
354
    @example
355
        router.route('/foo', function () {});
356
        router.match('/foo');
357
        // => [{callback: ..., keys: [], path: '/foo', regex: ...}]
358
 
359
    @method match
360
    @param {String} path URL path to match. This should be an absolute path that
361
        starts with a slash: "/".
362
    @return {Object[]} Array of route objects that match the specified path.
363
    **/
364
    match: function (path) {
365
        var root = this.get('root');
366
 
367
        if (root) {
368
            // The `path` must be semantically within this router's `root` path
369
            // or mount point, if it's not then no routes should be considered a
370
            // match.
371
            if (!this._pathHasRoot(root, path)) {
372
                return [];
373
            }
374
 
375
            // Remove this router's `root` from the `path` before checking the
376
            // routes for any matches.
377
            path = this.removeRoot(path);
378
        }
379
 
380
        return YArray.filter(this._routes, function (route) {
381
            return path.search(route.regex) > -1;
382
        });
383
    },
384
 
385
    /**
386
    Adds a handler for a route param specified by _name_.
387
 
388
    Param handlers can be registered via this method and are used to
389
    validate/format values of named params in routes before dispatching to the
390
    route's handler functions. Using param handlers allows routes to defined
391
    using string paths which allows for `req.params` to use named params, but
392
    still applying extra validation or formatting to the param values parsed
393
    from the URL.
394
 
395
    If a param handler regex or function returns a value of `false`, `null`,
396
    `undefined`, or `NaN`, the current route will not match and be skipped. All
397
    other return values will be used in place of the original param value parsed
398
    from the URL.
399
 
400
    @example
401
        router.param('postId', function (value) {
402
            return parseInt(value, 10);
403
        });
404
 
405
        router.param('username', /^\w+$/);
406
 
407
        router.route('/posts/:postId', function (req) {
408
        });
409
 
410
        router.route('/users/:username', function (req) {
411
            // `req.params.username` is an array because the result of calling
412
            // `exec()` on the regex is assigned as the param's value.
413
        });
414
 
415
        router.route('*', function () {
416
        });
417
 
418
        // URLs which match routes:
419
        router.save('/posts/1');     // => "Post: 1"
420
        router.save('/users/ericf'); // => "User: ericf"
421
 
422
        // URLs which do not match routes because params fail validation:
423
        router.save('/posts/a');            // => "Catch-all no routes matched!"
424
        router.save('/users/ericf,rgrove'); // => "Catch-all no routes matched!"
425
 
426
    @method param
427
    @param {String} name Name of the param used in route paths.
428
    @param {Function|RegExp} handler Function to invoke or regular expression to
429
        `exec()` during route dispatching whose return value is used as the new
430
        param value. Values of `false`, `null`, `undefined`, or `NaN` will cause
431
        the current route to not match and be skipped. When a function is
432
        specified, it will be invoked in the context of this instance with the
433
        following parameters:
434
      @param {String} handler.value The current param value parsed from the URL.
435
      @param {String} handler.name The name of the param.
436
    @chainable
437
    @since 3.12.0
438
    **/
439
    param: function (name, handler) {
440
        this._params[name] = handler;
441
        return this;
442
    },
443
 
444
    /**
445
    Removes the `root` URL from the front of _url_ (if it's there) and returns
446
    the result. The returned path will always have a leading `/`.
447
 
448
    @method removeRoot
449
    @param {String} url URL.
450
    @return {String} Rootless path.
451
    **/
452
    removeRoot: function (url) {
453
        var root = this.get('root'),
454
            path;
455
 
456
        // Strip out the non-path part of the URL, if any (e.g.
457
        // "http://foo.com"), so that we're left with just the path.
458
        url = url.replace(this._regexUrlOrigin, '');
459
 
460
        // Return the host-less URL if there's no `root` path to further remove.
461
        if (!root) {
462
            return url;
463
        }
464
 
465
        path = this.removeQuery(url);
466
 
467
        // Remove the `root` from the `url` if it's the same or its path is
468
        // semantically within the root path.
469
        if (path === root || this._pathHasRoot(root, path)) {
470
            url = url.substring(root.length);
471
        }
472
 
473
        return url.charAt(0) === '/' ? url : '/' + url;
474
    },
475
 
476
    /**
477
    Removes a query string from the end of the _url_ (if one exists) and returns
478
    the result.
479
 
480
    @method removeQuery
481
    @param {String} url URL.
482
    @return {String} Queryless path.
483
    **/
484
    removeQuery: function (url) {
485
        return url.replace(/\?.*$/, '');
486
    },
487
 
488
    /**
489
    Replaces the current browser history entry with a new one, and dispatches to
490
    the first matching route handler, if any.
491
 
492
    Behind the scenes, this method uses HTML5 `pushState()` in browsers that
493
    support it (or the location hash in older browsers and IE) to change the
494
    URL.
495
 
496
    The specified URL must share the same origin (i.e., protocol, host, and
497
    port) as the current page, or an error will occur.
498
 
499
    @example
500
        // Starting URL: http://example.com/
501
 
502
        router.replace('/path/');
503
        // New URL: http://example.com/path/
504
 
505
        router.replace('/path?foo=bar');
506
        // New URL: http://example.com/path?foo=bar
507
 
508
        router.replace('/');
509
        // New URL: http://example.com/
510
 
511
    @method replace
512
    @param {String} [url] URL to set. This URL needs to be of the same origin as
513
      the current URL. This can be a URL relative to the router's `root`
514
      attribute. If no URL is specified, the page's current URL will be used.
515
    @chainable
516
    @see save()
517
    **/
518
    replace: function (url) {
519
        return this._queue(url, true);
520
    },
521
 
522
    /**
523
    Adds a route handler for the specified `route`.
524
 
525
    The `route` parameter may be a string or regular expression to represent a
526
    URL path, or a route object. If it's a string (which is most common), it may
527
    contain named parameters: `:param` will match any single part of a URL path
528
    (not including `/` characters), and `*param` will match any number of parts
529
    of a URL path (including `/` characters). These named parameters will be
530
    made available as keys on the `req.params` object that's passed to route
531
    handlers.
532
 
533
    If the `route` parameter is a regex, all pattern matches will be made
534
    available as numbered keys on `req.params`, starting with `0` for the full
535
    match, then `1` for the first subpattern match, and so on.
536
 
537
    Alternatively, an object can be provided to represent the route and it may
538
    contain a `path` property which is a string or regular expression which
539
    causes the route to be process as described above. If the route object
540
    already contains a `regex` or `regexp` property, the route will be
541
    considered fully-processed and will be associated with any `callacks`
542
    specified on the object and those specified as parameters to this method.
543
    **Note:** Any additional data contained on the route object will be
544
    preserved.
545
 
546
    Here's a set of sample routes along with URL paths that they match:
547
 
548
      * Route: `/photos/:tag/:page`
549
        * URL: `/photos/kittens/1`, params: `{tag: 'kittens', page: '1'}`
550
        * URL: `/photos/puppies/2`, params: `{tag: 'puppies', page: '2'}`
551
 
552
      * Route: `/file/*path`
553
        * URL: `/file/foo/bar/baz.txt`, params: `{path: 'foo/bar/baz.txt'}`
554
        * URL: `/file/foo`, params: `{path: 'foo'}`
555
 
556
    **Middleware**: Routes also support an arbitrary number of callback
557
    functions. This allows you to easily reuse parts of your route-handling code
558
    with different route. This method is liberal in how it processes the
559
    specified `callbacks`, you can specify them as separate arguments, or as
560
    arrays, or both.
561
 
562
    If multiple route match a given URL, they will be executed in the order they
563
    were added. The first route that was added will be the first to be executed.
564
 
565
    **Passing Control**: Invoking the `next()` function within a route callback
566
    will pass control to the next callback function (if any) or route handler
567
    (if any). If a value is passed to `next()`, it's assumed to be an error,
568
    therefore stopping the dispatch chain, unless that value is: `"route"`,
569
    which is special case and dispatching will skip to the next route handler.
570
    This allows middleware to skip any remaining middleware for a particular
571
    route.
572
 
573
    @example
574
        router.route('/photos/:tag/:page', function (req, res, next) {
575
        });
576
 
577
        // Using middleware.
578
 
579
        router.findUser = function (req, res, next) {
580
            req.user = this.get('users').findById(req.params.user);
581
            next();
582
        };
583
 
584
        router.route('/users/:user', 'findUser', function (req, res, next) {
585
            // The `findUser` middleware puts the `user` object on the `req`.
586
        });
587
 
588
    @method route
589
    @param {String|RegExp|Object} route Route to match. May be a string or a
590
      regular expression, or a route object.
591
    @param {Array|Function|String} callbacks* Callback functions to call
592
        whenever this route is triggered. These can be specified as separate
593
        arguments, or in arrays, or both. If a callback is specified as a
594
        string, the named function will be called on this router instance.
595
 
596
      @param {Object} callbacks.req Request object containing information about
597
          the request. It contains the following properties.
598
 
599
        @param {Array|Object} callbacks.req.params Captured parameters matched
600
          by the route path specification. If a string path was used and
601
          contained named parameters, then this will be a key/value hash mapping
602
          parameter names to their matched values. If a regex path was used,
603
          this will be an array of subpattern matches starting at index 0 for
604
          the full match, then 1 for the first subpattern match, and so on.
605
        @param {String} callbacks.req.path The current URL path.
606
        @param {Number} callbacks.req.pendingCallbacks Number of remaining
607
          callbacks the route handler has after this one in the dispatch chain.
608
        @param {Number} callbacks.req.pendingRoutes Number of matching routes
609
          after this one in the dispatch chain.
610
        @param {Object} callbacks.req.query Query hash representing the URL
611
          query string, if any. Parameter names are keys, and are mapped to
612
          parameter values.
613
        @param {Object} callbacks.req.route Reference to the current route
614
          object whose callbacks are being dispatched.
615
        @param {Object} callbacks.req.router Reference to this router instance.
616
        @param {String} callbacks.req.src What initiated the dispatch. In an
617
          HTML5 browser, when the back/forward buttons are used, this property
618
          will have a value of "popstate". When the `dispath()` method is
619
          called, the `src` will be `"dispatch"`.
620
        @param {String} callbacks.req.url The full URL.
621
 
622
      @param {Object} callbacks.res Response object containing methods and
623
          information that relate to responding to a request. It contains the
624
          following properties.
625
        @param {Object} callbacks.res.req Reference to the request object.
626
 
627
      @param {Function} callbacks.next Function to pass control to the next
628
          callback or the next matching route if no more callbacks (middleware)
629
          exist for the current route handler. If you don't call this function,
630
          then no further callbacks or route handlers will be executed, even if
631
          there are more that match. If you do call this function, then the next
632
          callback (if any) or matching route handler (if any) will be called.
633
          All of these functions will receive the same `req` and `res` objects
634
          that were passed to this route (so you can use these objects to pass
635
          data along to subsequent callbacks and routes).
636
        @param {String} [callbacks.next.err] Optional error which will stop the
637
          dispatch chaining for this `req`, unless the value is `"route"`, which
638
          is special cased to jump skip past any callbacks for the current route
639
          and pass control the next route handler.
640
    @chainable
641
    **/
642
    route: function (route, callbacks) {
643
        // Grab callback functions from var-args.
644
        callbacks = YArray(arguments, 1, true);
645
 
646
        var keys, regex;
647
 
648
        // Supports both the `route(path, callbacks)` and `route(config)` call
649
        // signatures, allowing for fully-processed route configs to be passed.
650
        if (typeof route === 'string' || YLang.isRegExp(route)) {
651
            // Flatten `callbacks` into a single dimension array.
652
            callbacks = YArray.flatten(callbacks);
653
 
654
            keys  = [];
655
            regex = this._getRegex(route, keys);
656
 
657
            route = {
658
                callbacks: callbacks,
659
                keys     : keys,
660
                path     : route,
661
                regex    : regex
662
            };
663
        } else {
664
            // Look for any configured `route.callbacks` and fallback to
665
            // `route.callback` for back-compat, append var-arg `callbacks`,
666
            // then flatten the entire collection to a single dimension array.
667
            callbacks = YArray.flatten(
668
                [route.callbacks || route.callback || []].concat(callbacks)
669
            );
670
 
671
            // Check for previously generated regex, also fallback to `regexp`
672
            // for greater interop.
673
            keys  = route.keys;
674
            regex = route.regex || route.regexp;
675
 
676
            // Generates the route's regex if it doesn't already have one.
677
            if (!regex) {
678
                keys  = [];
679
                regex = this._getRegex(route.path, keys);
680
            }
681
 
682
            // Merge specified `route` config object with processed data.
683
            route = Y.merge(route, {
684
                callbacks: callbacks,
685
                keys     : keys,
686
                path     : route.path || regex,
687
                regex    : regex
688
            });
689
        }
690
 
691
        this._routes.push(route);
692
        return this;
693
    },
694
 
695
    /**
696
    Saves a new browser history entry and dispatches to the first matching route
697
    handler, if any.
698
 
699
    Behind the scenes, this method uses HTML5 `pushState()` in browsers that
700
    support it (or the location hash in older browsers and IE) to change the
701
    URL and create a history entry.
702
 
703
    The specified URL must share the same origin (i.e., protocol, host, and
704
    port) as the current page, or an error will occur.
705
 
706
    @example
707
        // Starting URL: http://example.com/
708
 
709
        router.save('/path/');
710
        // New URL: http://example.com/path/
711
 
712
        router.save('/path?foo=bar');
713
        // New URL: http://example.com/path?foo=bar
714
 
715
        router.save('/');
716
        // New URL: http://example.com/
717
 
718
    @method save
719
    @param {String} [url] URL to set. This URL needs to be of the same origin as
720
      the current URL. This can be a URL relative to the router's `root`
721
      attribute. If no URL is specified, the page's current URL will be used.
722
    @chainable
723
    @see replace()
724
    **/
725
    save: function (url) {
726
        return this._queue(url);
727
    },
728
 
729
    /**
730
    Upgrades a hash-based URL to an HTML5 URL if necessary. In non-HTML5
731
    browsers, this method is a noop.
732
 
733
    @method upgrade
734
    @return {Boolean} `true` if the URL was upgraded, `false` otherwise.
735
    **/
736
    upgrade: function () {
737
        if (!this._html5) {
738
            return false;
739
        }
740
 
741
        // Get the resolve hash path.
742
        var hashPath = this._getHashPath();
743
 
744
        if (hashPath) {
745
            // This is an HTML5 browser and we have a hash-based path in the
746
            // URL, so we need to upgrade the URL to a non-hash URL. This
747
            // will trigger a `history:change` event, which will in turn
748
            // trigger a dispatch.
749
            this.once(EVT_READY, function () {
750
                this.replace(hashPath);
751
            });
752
 
753
            return true;
754
        }
755
 
756
        return false;
757
    },
758
 
759
    // -- Protected Methods ----------------------------------------------------
760
 
761
    /**
762
    Wrapper around `decodeURIComponent` that also converts `+` chars into
763
    spaces.
764
 
765
    @method _decode
766
    @param {String} string String to decode.
767
    @return {String} Decoded string.
768
    @protected
769
    **/
770
    _decode: function (string) {
771
        return decodeURIComponent(string.replace(/\+/g, ' '));
772
    },
773
 
774
    /**
775
    Shifts the topmost `_save()` call off the queue and executes it. Does
776
    nothing if the queue is empty.
777
 
778
    @method _dequeue
779
    @chainable
780
    @see _queue
781
    @protected
782
    **/
783
    _dequeue: function () {
784
        var self = this,
785
            fn;
786
 
787
        // If window.onload hasn't yet fired, wait until it has before
788
        // dequeueing. This will ensure that we don't call pushState() before an
789
        // initial popstate event has fired.
790
        if (!YUI.Env.windowLoaded) {
791
            Y.once('load', function () {
792
                self._dequeue();
793
            });
794
 
795
            return this;
796
        }
797
 
798
        fn = saveQueue.shift();
799
        return fn ? fn() : this;
800
    },
801
 
802
    /**
803
    Dispatches to the first route handler that matches the specified _path_.
804
 
805
    If called before the `ready` event has fired, the dispatch will be aborted.
806
    This ensures normalized behavior between Chrome (which fires a `popstate`
807
    event on every pageview) and other browsers (which do not).
808
 
809
    @method _dispatch
810
    @param {object} req Request object.
811
    @param {String} res Response object.
812
    @chainable
813
    @protected
814
    **/
815
    _dispatch: function (req, res) {
816
        var self      = this,
817
            routes    = self.match(req.path),
818
            callbacks = [],
819
            routePath, paramValues;
820
 
821
        self._dispatching = self._dispatched = true;
822
 
823
        if (!routes || !routes.length) {
824
            self._dispatching = false;
825
            return self;
826
        }
827
 
828
        routePath = self.removeRoot(req.path);
829
 
830
        function next(err) {
831
            var callback, name, route;
832
 
833
            if (err) {
834
                // Special case "route" to skip to the next route handler
835
                // avoiding any additional callbacks for the current route.
836
                if (err === 'route') {
837
                    callbacks = [];
838
                    next();
839
                } else {
840
                    Y.error(err);
841
                }
842
 
843
            } else if ((callback = callbacks.shift())) {
844
                if (typeof callback === 'string') {
845
                    name     = callback;
846
                    callback = self[name];
847
 
848
                    if (!callback) {
849
                        Y.error('Router: Callback not found: ' + name, null, 'router');
850
                    }
851
                }
852
 
853
                // Allow access to the number of remaining callbacks for the
854
                // route.
855
                req.pendingCallbacks = callbacks.length;
856
 
857
                callback.call(self, req, res, next);
858
 
859
            } else if ((route = routes.shift())) {
860
                paramValues = self._getParamValues(route, routePath);
861
 
862
                if (!paramValues) {
863
                    // Skip this route because one of the param handlers
864
                    // rejected a param value in the `routePath`.
865
                    next('route');
866
                    return;
867
                }
868
 
869
                // Expose the processed param values.
870
                req.params = paramValues;
871
 
872
                // Allow access to current route and the number of remaining
873
                // routes for this request.
874
                req.route         = route;
875
                req.pendingRoutes = routes.length;
876
 
877
                // Make a copy of this route's `callbacks` so the original array
878
                // is preserved.
879
                callbacks = route.callbacks.concat();
880
 
881
                // Execute this route's `callbacks`.
882
                next();
883
            }
884
        }
885
 
886
        next();
887
 
888
        self._dispatching = false;
889
        return self._dequeue();
890
    },
891
 
892
    /**
893
    Returns the resolved path from the hash fragment, or an empty string if the
894
    hash is not path-like.
895
 
896
    @method _getHashPath
897
    @param {String} [hash] Hash fragment to resolve into a path. By default this
898
        will be the hash from the current URL.
899
    @return {String} Current hash path, or an empty string if the hash is empty.
900
    @protected
901
    **/
902
    _getHashPath: function (hash) {
903
        hash || (hash = HistoryHash.getHash());
904
 
905
        // Make sure the `hash` is path-like.
906
        if (hash && hash.charAt(0) === '/') {
907
            return this._joinURL(hash);
908
        }
909
 
910
        return '';
911
    },
912
 
913
    /**
914
    Gets the location origin (i.e., protocol, host, and port) as a URL.
915
 
916
    @example
917
        http://example.com
918
 
919
    @method _getOrigin
920
    @return {String} Location origin (i.e., protocol, host, and port).
921
    @protected
922
    **/
923
    _getOrigin: function () {
924
        var location = Y.getLocation();
925
        return location.origin || (location.protocol + '//' + location.host);
926
    },
927
 
928
    /**
929
    Getter for the `params` attribute.
930
 
931
    @method _getParams
932
    @return {Object} Mapping of param handlers: `name` -> RegExp | Function.
933
    @protected
934
    @since 3.12.0
935
    **/
936
    _getParams: function () {
937
        return Y.merge(this._params);
938
    },
939
 
940
    /**
941
    Gets the param values for the specified `route` and `path`, suitable to use
942
    form `req.params`.
943
 
944
    **Note:** This method will return `false` if a named param handler rejects a
945
    param value.
946
 
947
    @method _getParamValues
948
    @param {Object} route The route to get param values for.
949
    @param {String} path The route path (root removed) that provides the param
950
        values.
951
    @return {Boolean|Array|Object} The collection of processed param values.
952
        Either a hash of `name` -> `value` for named params processed by this
953
        router's param handlers, or an array of matches for a route with unnamed
954
        params. If a named param handler rejects a value, then `false` will be
955
        returned.
956
    @protected
957
    @since 3.16.0
958
    **/
959
    _getParamValues: function (route, path) {
960
        var matches, paramsMatch, paramValues;
961
 
962
        // Decode each of the path params so that the any URL-encoded path
963
        // segments are decoded in the `req.params` object.
964
        matches = YArray.map(route.regex.exec(path) || [], function (match) {
965
            // Decode matches, or coerce `undefined` matches to an empty
966
            // string to match expectations of working with `req.params`
967
            // in the context of route dispatching, and normalize
968
            // browser differences in their handling of regex NPCGs:
969
            // https://github.com/yui/yui3/issues/1076
970
            return (match && this._decode(match)) || '';
971
        }, this);
972
 
973
        // Simply return the array of decoded values when the route does *not*
974
        // use named parameters.
975
        if (matches.length - 1 !== route.keys.length) {
976
            return matches;
977
        }
978
 
979
        // Remove the first "match" from the param values, because it's just the
980
        // `path` processed by the route's regex, and map the values to the keys
981
        // to create the name params collection.
982
        paramValues = YArray.hash(route.keys, matches.slice(1));
983
 
984
        // Pass each named param value to its handler, if there is one, for
985
        // validation/processing. If a param value is rejected by a handler,
986
        // then the params don't match and a falsy value is returned.
987
        paramsMatch = YArray.every(route.keys, function (name) {
988
            var paramHandler = this._params[name],
989
                value        = paramValues[name];
990
 
991
            if (paramHandler && value && typeof value === 'string') {
992
                // Check if `paramHandler` is a RegExp, because this
993
                // is true in Android 2.3 and other browsers!
994
                // `typeof /.*/ === 'function'`
995
                value = YLang.isRegExp(paramHandler) ?
996
                        paramHandler.exec(value) :
997
                        paramHandler.call(this, value, name);
998
 
999
                if (value !== false && YLang.isValue(value)) {
1000
                    // Update the named param to the value from the handler.
1001
                    paramValues[name] = value;
1002
                    return true;
1003
                }
1004
 
1005
                // Consider the param value as rejected by the handler.
1006
                return false;
1007
            }
1008
 
1009
            return true;
1010
        }, this);
1011
 
1012
        if (paramsMatch) {
1013
            return paramValues;
1014
        }
1015
 
1016
        // Signal that a param value was rejected by a named param handler.
1017
        return false;
1018
    },
1019
 
1020
    /**
1021
    Gets the current route path.
1022
 
1023
    @method _getPath
1024
    @return {String} Current route path.
1025
    @protected
1026
    **/
1027
    _getPath: function () {
1028
        var path = (!this._html5 && this._getHashPath()) ||
1029
                Y.getLocation().pathname;
1030
 
1031
        return this.removeQuery(path);
1032
    },
1033
 
1034
    /**
1035
    Returns the current path root after popping off the last path segment,
1036
    making it useful for resolving other URL paths against.
1037
 
1038
    The path root will always begin and end with a '/'.
1039
 
1040
    @method _getPathRoot
1041
    @return {String} The URL's path root.
1042
    @protected
1043
    @since 3.5.0
1044
    **/
1045
    _getPathRoot: function () {
1046
        var slash = '/',
1047
            path  = Y.getLocation().pathname,
1048
            segments;
1049
 
1050
        if (path.charAt(path.length - 1) === slash) {
1051
            return path;
1052
        }
1053
 
1054
        segments = path.split(slash);
1055
        segments.pop();
1056
 
1057
        return segments.join(slash) + slash;
1058
    },
1059
 
1060
    /**
1061
    Gets the current route query string.
1062
 
1063
    @method _getQuery
1064
    @return {String} Current route query string.
1065
    @protected
1066
    **/
1067
    _getQuery: function () {
1068
        var location = Y.getLocation(),
1069
            hash, matches;
1070
 
1071
        if (this._html5) {
1072
            return location.search.substring(1);
1073
        }
1074
 
1075
        hash    = HistoryHash.getHash();
1076
        matches = hash.match(this._regexUrlQuery);
1077
 
1078
        return hash && matches ? matches[1] : location.search.substring(1);
1079
    },
1080
 
1081
    /**
1082
    Creates a regular expression from the given route specification. If _path_
1083
    is already a regex, it will be returned unmodified.
1084
 
1085
    @method _getRegex
1086
    @param {String|RegExp} path Route path specification.
1087
    @param {Array} keys Array reference to which route parameter names will be
1088
      added.
1089
    @return {RegExp} Route regex.
1090
    @protected
1091
    **/
1092
    _getRegex: function (path, keys) {
1093
        if (YLang.isRegExp(path)) {
1094
            return path;
1095
        }
1096
 
1097
        // Special case for catchall paths.
1098
        if (path === '*') {
1099
            return (/.*/);
1100
        }
1101
 
1102
        path = path.replace(this._regexPathParam, function (match, operator, key) {
1103
            // Only `*` operators are supported for key-less matches to allowing
1104
            // in-path wildcards like: '/foo/*'.
1105
            if (!key) {
1106
                return operator === '*' ? '.*' : match;
1107
            }
1108
 
1109
            keys.push(key);
1110
            return operator === '*' ? '(.*?)' : '([^/#?]+)';
1111
        });
1112
 
1113
        return new RegExp('^' + path + '$');
1114
    },
1115
 
1116
    /**
1117
    Gets a request object that can be passed to a route handler.
1118
 
1119
    @method _getRequest
1120
    @param {String} src What initiated the URL change and need for the request.
1121
    @return {Object} Request object.
1122
    @protected
1123
    **/
1124
    _getRequest: function (src) {
1125
        return {
1126
            path  : this._getPath(),
1127
            query : this._parseQuery(this._getQuery()),
1128
            url   : this._getURL(),
1129
            router: this,
1130
            src   : src
1131
        };
1132
    },
1133
 
1134
    /**
1135
    Gets a response object that can be passed to a route handler.
1136
 
1137
    @method _getResponse
1138
    @param {Object} req Request object.
1139
    @return {Object} Response Object.
1140
    @protected
1141
    **/
1142
    _getResponse: function (req) {
1143
        return {req: req};
1144
    },
1145
 
1146
    /**
1147
    Getter for the `routes` attribute.
1148
 
1149
    @method _getRoutes
1150
    @return {Object[]} Array of route objects.
1151
    @protected
1152
    **/
1153
    _getRoutes: function () {
1154
        return this._routes.concat();
1155
    },
1156
 
1157
    /**
1158
    Gets the current full URL.
1159
 
1160
    @method _getURL
1161
    @return {String} URL.
1162
    @protected
1163
    **/
1164
    _getURL: function () {
1165
        var url = Y.getLocation().toString();
1166
 
1167
        if (!this._html5) {
1168
            url = this._upgradeURL(url);
1169
        }
1170
 
1171
        return url;
1172
    },
1173
 
1174
    /**
1175
    Returns `true` when the specified `url` is from the same origin as the
1176
    current URL; i.e., the protocol, host, and port of the URLs are the same.
1177
 
1178
    All host or path relative URLs are of the same origin. A scheme-relative URL
1179
    is first prefixed with the current scheme before being evaluated.
1180
 
1181
    @method _hasSameOrigin
1182
    @param {String} url URL to compare origin with the current URL.
1183
    @return {Boolean} Whether the URL has the same origin of the current URL.
1184
    @protected
1185
    **/
1186
    _hasSameOrigin: function (url) {
1187
        var origin = ((url && url.match(this._regexUrlOrigin)) || [])[0];
1188
 
1189
        // Prepend current scheme to scheme-relative URLs.
1190
        if (origin && origin.indexOf('//') === 0) {
1191
            origin = Y.getLocation().protocol + origin;
1192
        }
1193
 
1194
        return !origin || origin === this._getOrigin();
1195
    },
1196
 
1197
    /**
1198
    Joins the `root` URL to the specified _url_, normalizing leading/trailing
1199
    `/` characters.
1200
 
1201
    @example
1202
        router.set('root', '/foo');
1203
        router._joinURL('bar');  // => '/foo/bar'
1204
        router._joinURL('/bar'); // => '/foo/bar'
1205
 
1206
        router.set('root', '/foo/');
1207
        router._joinURL('bar');  // => '/foo/bar'
1208
        router._joinURL('/bar'); // => '/foo/bar'
1209
 
1210
    @method _joinURL
1211
    @param {String} url URL to append to the `root` URL.
1212
    @return {String} Joined URL.
1213
    @protected
1214
    **/
1215
    _joinURL: function (url) {
1216
        var root = this.get('root');
1217
 
1218
        // Causes `url` to _always_ begin with a "/".
1219
        url = this.removeRoot(url);
1220
 
1221
        if (url.charAt(0) === '/') {
1222
            url = url.substring(1);
1223
        }
1224
 
1225
        return root && root.charAt(root.length - 1) === '/' ?
1226
                root + url :
1227
                root + '/' + url;
1228
    },
1229
 
1230
    /**
1231
    Returns a normalized path, ridding it of any '..' segments and properly
1232
    handling leading and trailing slashes.
1233
 
1234
    @method _normalizePath
1235
    @param {String} path URL path to normalize.
1236
    @return {String} Normalized path.
1237
    @protected
1238
    @since 3.5.0
1239
    **/
1240
    _normalizePath: function (path) {
1241
        var dots  = '..',
1242
            slash = '/',
1243
            i, len, normalized, segments, segment, stack;
1244
 
1245
        if (!path || path === slash) {
1246
            return slash;
1247
        }
1248
 
1249
        segments = path.split(slash);
1250
        stack    = [];
1251
 
1252
        for (i = 0, len = segments.length; i < len; ++i) {
1253
            segment = segments[i];
1254
 
1255
            if (segment === dots) {
1256
                stack.pop();
1257
            } else if (segment) {
1258
                stack.push(segment);
1259
            }
1260
        }
1261
 
1262
        normalized = slash + stack.join(slash);
1263
 
1264
        // Append trailing slash if necessary.
1265
        if (normalized !== slash && path.charAt(path.length - 1) === slash) {
1266
            normalized += slash;
1267
        }
1268
 
1269
        return normalized;
1270
    },
1271
 
1272
    /**
1273
    Parses a URL query string into a key/value hash. If `Y.QueryString.parse` is
1274
    available, this method will be an alias to that.
1275
 
1276
    @method _parseQuery
1277
    @param {String} query Query string to parse.
1278
    @return {Object} Hash of key/value pairs for query parameters.
1279
    @protected
1280
    **/
1281
    _parseQuery: QS && QS.parse ? QS.parse : function (query) {
1282
        var decode = this._decode,
1283
            params = query.split('&'),
1284
            i      = 0,
1285
            len    = params.length,
1286
            result = {},
1287
            param;
1288
 
1289
        for (; i < len; ++i) {
1290
            param = params[i].split('=');
1291
 
1292
            if (param[0]) {
1293
                result[decode(param[0])] = decode(param[1] || '');
1294
            }
1295
        }
1296
 
1297
        return result;
1298
    },
1299
 
1300
    /**
1301
    Returns `true` when the specified `path` is semantically within the
1302
    specified `root` path.
1303
 
1304
    If the `root` does not end with a trailing slash ("/"), one will be added
1305
    before the `path` is evaluated against the root path.
1306
 
1307
    @example
1308
        this._pathHasRoot('/app',  '/app/foo'); // => true
1309
        this._pathHasRoot('/app/', '/app/foo'); // => true
1310
        this._pathHasRoot('/app/', '/app/');    // => true
1311
 
1312
        this._pathHasRoot('/app',  '/foo/bar'); // => false
1313
        this._pathHasRoot('/app/', '/foo/bar'); // => false
1314
        this._pathHasRoot('/app/', '/app');     // => false
1315
        this._pathHasRoot('/app',  '/app');     // => false
1316
 
1317
    @method _pathHasRoot
1318
    @param {String} root Root path used to evaluate whether the specificed
1319
        `path` is semantically within. A trailing slash ("/") will be added if
1320
        it does not already end with one.
1321
    @param {String} path Path to evaluate for containing the specified `root`.
1322
    @return {Boolean} Whether or not the `path` is semantically within the
1323
        `root` path.
1324
    @protected
1325
    @since 3.13.0
1326
    **/
1327
    _pathHasRoot: function (root, path) {
1328
        var rootPath = root.charAt(root.length - 1) === '/' ? root : root + '/';
1329
        return path.indexOf(rootPath) === 0;
1330
    },
1331
 
1332
    /**
1333
    Queues up a `_save()` call to run after all previously-queued calls have
1334
    finished.
1335
 
1336
    This is necessary because if we make multiple `_save()` calls before the
1337
    first call gets dispatched, then both calls will dispatch to the last call's
1338
    URL.
1339
 
1340
    All arguments passed to `_queue()` will be passed on to `_save()` when the
1341
    queued function is executed.
1342
 
1343
    @method _queue
1344
    @chainable
1345
    @see _dequeue
1346
    @protected
1347
    **/
1348
    _queue: function () {
1349
        var args = arguments,
1350
            self = this;
1351
 
1352
        saveQueue.push(function () {
1353
            if (self._html5) {
1354
                if (Y.UA.ios && Y.UA.ios < 5) {
1355
                    // iOS <5 has buggy HTML5 history support, and needs to be
1356
                    // synchronous.
1357
                    self._save.apply(self, args);
1358
                } else {
1359
                    // Wrapped in a timeout to ensure that _save() calls are
1360
                    // always processed asynchronously. This ensures consistency
1361
                    // between HTML5- and hash-based history.
1362
                    setTimeout(function () {
1363
                        self._save.apply(self, args);
1364
                    }, 1);
1365
                }
1366
            } else {
1367
                self._dispatching = true; // otherwise we'll dequeue too quickly
1368
                self._save.apply(self, args);
1369
            }
1370
 
1371
            return self;
1372
        });
1373
 
1374
        return !this._dispatching ? this._dequeue() : this;
1375
    },
1376
 
1377
    /**
1378
    Returns the normalized result of resolving the `path` against the current
1379
    path. Falsy values for `path` will return just the current path.
1380
 
1381
    @method _resolvePath
1382
    @param {String} path URL path to resolve.
1383
    @return {String} Resolved path.
1384
    @protected
1385
    @since 3.5.0
1386
    **/
1387
    _resolvePath: function (path) {
1388
        if (!path) {
1389
            return Y.getLocation().pathname;
1390
        }
1391
 
1392
        if (path.charAt(0) !== '/') {
1393
            path = this._getPathRoot() + path;
1394
        }
1395
 
1396
        return this._normalizePath(path);
1397
    },
1398
 
1399
    /**
1400
    Resolves the specified URL against the current URL.
1401
 
1402
    This method resolves URLs like a browser does and will always return an
1403
    absolute URL. When the specified URL is already absolute, it is assumed to
1404
    be fully resolved and is simply returned as is. Scheme-relative URLs are
1405
    prefixed with the current protocol. Relative URLs are giving the current
1406
    URL's origin and are resolved and normalized against the current path root.
1407
 
1408
    @method _resolveURL
1409
    @param {String} url URL to resolve.
1410
    @return {String} Resolved URL.
1411
    @protected
1412
    @since 3.5.0
1413
    **/
1414
    _resolveURL: function (url) {
1415
        var parts    = url && url.match(this._regexURL),
1416
            origin, path, query, hash, resolved;
1417
 
1418
        if (!parts) {
1419
            return Y.getLocation().toString();
1420
        }
1421
 
1422
        origin = parts[1];
1423
        path   = parts[2];
1424
        query  = parts[3];
1425
        hash   = parts[4];
1426
 
1427
        // Absolute and scheme-relative URLs are assumed to be fully-resolved.
1428
        if (origin) {
1429
            // Prepend the current scheme for scheme-relative URLs.
1430
            if (origin.indexOf('//') === 0) {
1431
                origin = Y.getLocation().protocol + origin;
1432
            }
1433
 
1434
            return origin + (path || '/') + (query || '') + (hash || '');
1435
        }
1436
 
1437
        // Will default to the current origin and current path.
1438
        resolved = this._getOrigin() + this._resolvePath(path);
1439
 
1440
        // A path or query for the specified URL trumps the current URL's.
1441
        if (path || query) {
1442
            return resolved + (query || '') + (hash || '');
1443
        }
1444
 
1445
        query = this._getQuery();
1446
 
1447
        return resolved + (query ? ('?' + query) : '') + (hash || '');
1448
    },
1449
 
1450
    /**
1451
    Saves a history entry using either `pushState()` or the location hash.
1452
 
1453
    This method enforces the same-origin security constraint; attempting to save
1454
    a `url` that is not from the same origin as the current URL will result in
1455
    an error.
1456
 
1457
    @method _save
1458
    @param {String} [url] URL for the history entry.
1459
    @param {Boolean} [replace=false] If `true`, the current history entry will
1460
      be replaced instead of a new one being added.
1461
    @chainable
1462
    @protected
1463
    **/
1464
    _save: function (url, replace) {
1465
        var urlIsString = typeof url === 'string',
1466
            currentPath, root, hash;
1467
 
1468
        // Perform same-origin check on the specified URL.
1469
        if (urlIsString && !this._hasSameOrigin(url)) {
1470
            Y.error('Security error: The new URL must be of the same origin as the current URL.');
1471
            return this;
1472
        }
1473
 
1474
        // Joins the `url` with the `root`.
1475
        if (urlIsString) {
1476
            url = this._joinURL(url);
1477
        }
1478
 
1479
        // Force _ready to true to ensure that the history change is handled
1480
        // even if _save is called before the `ready` event fires.
1481
        this._ready = true;
1482
 
1483
        if (this._html5) {
1484
            this._history[replace ? 'replace' : 'add'](null, {url: url});
1485
        } else {
1486
            currentPath = Y.getLocation().pathname;
1487
            root        = this.get('root');
1488
            hash        = HistoryHash.getHash();
1489
 
1490
            if (!urlIsString) {
1491
                url = hash;
1492
            }
1493
 
1494
            // Determine if the `root` already exists in the current location's
1495
            // `pathname`, and if it does then we can exclude it from the
1496
            // hash-based path. No need to duplicate the info in the URL.
1497
            if (root === currentPath || root === this._getPathRoot()) {
1498
                url = this.removeRoot(url);
1499
            }
1500
 
1501
            // The `hashchange` event only fires when the new hash is actually
1502
            // different. This makes sure we'll always dequeue and dispatch
1503
            // _all_ router instances, mimicking the HTML5 behavior.
1504
            if (url === hash) {
1505
                Y.Router.dispatch();
1506
            } else {
1507
                HistoryHash[replace ? 'replaceHash' : 'setHash'](url);
1508
            }
1509
        }
1510
 
1511
        return this;
1512
    },
1513
 
1514
    /**
1515
    Setter for the `params` attribute.
1516
 
1517
    @method _setParams
1518
    @param {Object} params Map in the form: `name` -> RegExp | Function.
1519
    @return {Object} The map of params: `name` -> RegExp | Function.
1520
    @protected
1521
    @since 3.12.0
1522
    **/
1523
    _setParams: function (params) {
1524
        this._params = {};
1525
 
1526
        YObject.each(params, function (regex, name) {
1527
            this.param(name, regex);
1528
        }, this);
1529
 
1530
        return Y.merge(this._params);
1531
    },
1532
 
1533
    /**
1534
    Setter for the `routes` attribute.
1535
 
1536
    @method _setRoutes
1537
    @param {Object[]} routes Array of route objects.
1538
    @return {Object[]} Array of route objects.
1539
    @protected
1540
    **/
1541
    _setRoutes: function (routes) {
1542
        this._routes = [];
1543
 
1544
        YArray.each(routes, function (route) {
1545
            this.route(route);
1546
        }, this);
1547
 
1548
        return this._routes.concat();
1549
    },
1550
 
1551
    /**
1552
    Upgrades a hash-based URL to a full-path URL, if necessary.
1553
 
1554
    The specified `url` will be upgraded if its of the same origin as the
1555
    current URL and has a path-like hash. URLs that don't need upgrading will be
1556
    returned as-is.
1557
 
1558
    @example
1559
        app._upgradeURL('http://example.com/#/foo/'); // => 'http://example.com/foo/';
1560
 
1561
    @method _upgradeURL
1562
    @param {String} url The URL to upgrade from hash-based to full-path.
1563
    @return {String} The upgraded URL, or the specified URL untouched.
1564
    @protected
1565
    @since 3.5.0
1566
    **/
1567
    _upgradeURL: function (url) {
1568
        // We should not try to upgrade paths for external URLs.
1569
        if (!this._hasSameOrigin(url)) {
1570
            return url;
1571
        }
1572
 
1573
        var hash       = (url.match(/#(.*)$/) || [])[1] || '',
1574
            hashPrefix = Y.HistoryHash.hashPrefix,
1575
            hashPath;
1576
 
1577
        // Strip any hash prefix, like hash-bangs.
1578
        if (hashPrefix && hash.indexOf(hashPrefix) === 0) {
1579
            hash = hash.replace(hashPrefix, '');
1580
        }
1581
 
1582
        // If the hash looks like a URL path, assume it is, and upgrade it!
1583
        if (hash) {
1584
            hashPath = this._getHashPath(hash);
1585
 
1586
            if (hashPath) {
1587
                return this._resolveURL(hashPath);
1588
            }
1589
        }
1590
 
1591
        return url;
1592
    },
1593
 
1594
    // -- Protected Event Handlers ---------------------------------------------
1595
 
1596
    /**
1597
    Handles `history:change` and `hashchange` events.
1598
 
1599
    @method _afterHistoryChange
1600
    @param {EventFacade} e
1601
    @protected
1602
    **/
1603
    _afterHistoryChange: function (e) {
1604
        var self       = this,
1605
            src        = e.src,
1606
            prevURL    = self._url,
1607
            currentURL = self._getURL(),
1608
            req, res;
1609
 
1610
        self._url = currentURL;
1611
 
1612
        // Handles the awkwardness that is the `popstate` event. HTML5 browsers
1613
        // fire `popstate` right before they fire `hashchange`, and Chrome fires
1614
        // `popstate` on page load. If this router is not ready or the previous
1615
        // and current URLs only differ by their hash, then we want to ignore
1616
        // this `popstate` event.
1617
        if (src === 'popstate' &&
1618
                (!self._ready || prevURL.replace(/#.*$/, '') === currentURL.replace(/#.*$/, ''))) {
1619
 
1620
            return;
1621
        }
1622
 
1623
        req = self._getRequest(src);
1624
        res = self._getResponse(req);
1625
 
1626
        self._dispatch(req, res);
1627
    },
1628
 
1629
    // -- Default Event Handlers -----------------------------------------------
1630
 
1631
    /**
1632
    Default handler for the `ready` event.
1633
 
1634
    @method _defReadyFn
1635
    @param {EventFacade} e
1636
    @protected
1637
    **/
1638
    _defReadyFn: function (e) {
1639
        this._ready = true;
1640
    }
1641
}, {
1642
    // -- Static Properties ----------------------------------------------------
1643
    NAME: 'router',
1644
 
1645
    ATTRS: {
1646
        /**
1647
        Whether or not this browser is capable of using HTML5 history.
1648
 
1649
        Setting this to `false` will force the use of hash-based history even on
1650
        HTML5 browsers, but please don't do this unless you understand the
1651
        consequences.
1652
 
1653
        @attribute html5
1654
        @type Boolean
1655
        @initOnly
1656
        **/
1657
        html5: {
1658
            // Android versions lower than 3.0 are buggy and don't update
1659
            // window.location after a pushState() call, so we fall back to
1660
            // hash-based history for them.
1661
            //
1662
            // See http://code.google.com/p/android/issues/detail?id=17471
1663
            valueFn: function () { return Y.Router.html5; },
1664
            writeOnce: 'initOnly'
1665
        },
1666
 
1667
        /**
1668
        Map of params handlers in the form: `name` -> RegExp | Function.
1669
 
1670
        If a param handler regex or function returns a value of `false`, `null`,
1671
        `undefined`, or `NaN`, the current route will not match and be skipped.
1672
        All other return values will be used in place of the original param
1673
        value parsed from the URL.
1674
 
1675
        This attribute is intended to be used to set params at init time, or to
1676
        completely reset all params after init. To add params after init without
1677
        resetting all existing params, use the `param()` method.
1678
 
1679
        @attribute params
1680
        @type Object
1681
        @default `{}`
1682
        @see param
1683
        @since 3.12.0
1684
        **/
1685
        params: {
1686
            value : {},
1687
            getter: '_getParams',
1688
            setter: '_setParams'
1689
        },
1690
 
1691
        /**
1692
        Absolute root path from which all routes should be evaluated.
1693
 
1694
        For example, if your router is running on a page at
1695
        `http://example.com/myapp/` and you add a route with the path `/`, your
1696
        route will never execute, because the path will always be preceded by
1697
        `/myapp`. Setting `root` to `/myapp` would cause all routes to be
1698
        evaluated relative to that root URL, so the `/` route would then execute
1699
        when the user browses to `http://example.com/myapp/`.
1700
 
1701
        @example
1702
            router.set('root', '/myapp');
1703
            router.route('/foo', function () { ... });
1704
 
1705
 
1706
            // Updates the URL to: "/myapp/foo"
1707
            router.save('/foo');
1708
 
1709
        @attribute root
1710
        @type String
1711
        @default `''`
1712
        **/
1713
        root: {
1714
            value: ''
1715
        },
1716
 
1717
        /**
1718
        Array of route objects.
1719
 
1720
        Each item in the array must be an object with the following properties
1721
        in order to be processed by the router:
1722
 
1723
          * `path`: String or regex representing the path to match. See the docs
1724
            for the `route()` method for more details.
1725
 
1726
          * `callbacks`: Function or a string representing the name of a
1727
            function on this router instance that should be called when the
1728
            route is triggered. An array of functions and/or strings may also be
1729
            provided. See the docs for the `route()` method for more details.
1730
 
1731
        If a route object contains a `regex` or `regexp` property, or if its
1732
        `path` is a regular express, then the route will be considered to be
1733
        fully-processed. Any fully-processed routes may contain the following
1734
        properties:
1735
 
1736
          * `regex`: The regular expression representing the path to match, this
1737
            property may also be named `regexp` for greater compatibility.
1738
 
1739
          * `keys`: Array of named path parameters used to populate `req.params`
1740
            objects when dispatching to route handlers.
1741
 
1742
        Any additional data contained on these route objects will be retained.
1743
        This is useful to store extra metadata about a route; e.g., a `name` to
1744
        give routes logical names.
1745
 
1746
        This attribute is intended to be used to set routes at init time, or to
1747
        completely reset all routes after init. To add routes after init without
1748
        resetting all existing routes, use the `route()` method.
1749
 
1750
        @attribute routes
1751
        @type Object[]
1752
        @default `[]`
1753
        @see route
1754
        **/
1755
        routes: {
1756
            value : [],
1757
            getter: '_getRoutes',
1758
            setter: '_setRoutes'
1759
        }
1760
    },
1761
 
1762
    // Used as the default value for the `html5` attribute, and for testing.
1763
    html5: Y.HistoryBase.html5 && (!Y.UA.android || Y.UA.android >= 3),
1764
 
1765
    // To make this testable.
1766
    _instances: instances,
1767
 
1768
    /**
1769
    Dispatches to the first route handler that matches the specified `path` for
1770
    all active router instances.
1771
 
1772
    This provides a mechanism to cause all active router instances to dispatch
1773
    to their route handlers without needing to change the URL or fire the
1774
    `history:change` or `hashchange` event.
1775
 
1776
    @method dispatch
1777
    @static
1778
    @since 3.6.0
1779
    **/
1780
    dispatch: function () {
1781
        var i, len, router, req, res;
1782
 
1783
        for (i = 0, len = instances.length; i < len; i += 1) {
1784
            router = instances[i];
1785
 
1786
            if (router) {
1787
                req = router._getRequest('dispatch');
1788
                res = router._getResponse(req);
1789
 
1790
                router._dispatch(req, res);
1791
            }
1792
        }
1793
    }
1794
});
1795
 
1796
/**
1797
The `Controller` class was deprecated in YUI 3.5.0 and is now an alias for the
1798
`Router` class. Use that class instead. This alias will be removed in a future
1799
version of YUI.
1800
 
1801
@class Controller
1802
@constructor
1803
@extends Base
1804
@deprecated Use `Router` instead.
1805
@see Router
1806
**/
1807
Y.Controller = Y.Router;
1808
 
1809
 
1810
}, '3.18.1', {"optional": ["querystring-parse"], "requires": ["array-extras", "base-build", "history"]});