[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/yuilib/3.17.2/router/ -> router-debug.js (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1