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