Advertisement
Guest User

improved router

a guest
Apr 8th, 2015
273
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. (function(enyo) {
  2.  
  3.     /**
  4.     * All of our actively-supported browsers support this method of registering
  5.     * for `hashchange` events.
  6.     *
  7.     * @private
  8.     */
  9.     enyo.ready(function() {
  10.         enyo.dispatcher.listen(window, "hashchange", enyo.Router.hashDidChange);
  11.     });
  12.  
  13.     /**
  14.     * {@link enyo.Router} is a controller with the ability to interpret changes in
  15.     * the URL and make changes to the URL in a manner that is compatible across
  16.     * browsers. With defined route handling, the application state can be managed
  17.     * more closely with respect to the location state of the browser. There may be
  18.     * multiple router instances active at any one time. Routers only interact with
  19.     * the hash portion of the browser location and will not force a reload of the
  20.     * current page.
  21.     *
  22.     * [Routes]{@link enyo.Router#routes} may be defined in several ways and
  23.     * may be added at startup or [added programmatically]{@link enyo.Router#addRoute}
  24.     * at a later time.
  25.     *
  26.     * A route is a declarative hash with the following structure:
  27.     *
  28.     * ```
  29.     * {path: 'some/path', handler: 'function', context: 'context'}
  30.     * ```
  31.     *
  32.     * The `path` is a string that may be static (explicitly matched) or dynamic
  33.     * (matched based on dynamic placeholders). Dynamic paths may name elements to
  34.     * be matched, e.g.:
  35.     *
  36.     * ```
  37.     * {path: ':user/:id'}
  38.     * ```
  39.     *
  40.     * In this case, `handler` would be called with two parameters filled with
  41.     * the values matched by that structure. It is important to note that a dynamic
  42.     * route will apply the first match it finds, so care must be taken to ensure
  43.     * that the correct route is matched (e.g., `/users/:user/:id` is more exact
  44.     * because it includes the static `users` portion of the path).
  45.     *
  46.     * `handler` may be a function reference or a string that will be mapped to a
  47.     * function. A check for the function's existence is conducted first on the
  48.     * router, then on any provided context, and finally in the global scope. If a
  49.     * context is provided, the function will be executed under that context
  50.     * regardless of where the function was found.
  51.     *
  52.     * The `context` property may be an object, an instance, or a string that will
  53.     * be mapped to an object, if possible.
  54.     *
  55.     * Note that, currently, only letters and numbers are supported in dynamic routes.
  56.     *
  57.     * @class enyo.Router
  58.     * @extends enyo.Controller
  59.     * @public
  60.     */
  61.     enyo.kind({
  62.         /** @lends enyo.Router.prototype*/
  63.  
  64.         // ...........................
  65.         // PUBLIC PROPERTIES
  66.  
  67.         /*
  68.         * @public
  69.         */
  70.         name: "enyo.Router",
  71.  
  72.         /**
  73.         * If `true`, the router will respond to hash changes or internal events. If this flag is set
  74.         * to `false`, it will stop responding. This may be changed at any time.
  75.         *
  76.         * @type {Boolean}
  77.         * @default true
  78.         * @public
  79.         */
  80.         listening: true,
  81.  
  82.         /**
  83.         * If `true`, the router will neither respond to hash changes in the browser
  84.         * nor be able to trigger them. Instead, the router may be used internally to
  85.         * maintain or trigger state changes in an application without changing
  86.         * location.
  87.         *
  88.         * @type {Boolean}
  89.         * @default false
  90.         * @public
  91.         */
  92.         internalOnly: false,
  93.  
  94.         /**
  95.         * Set this to `true` to force the current browser location to a particular
  96.         * path on startup. This flag will be ignored if
  97.         * [triggerOnStart]{@link enyo.Router#triggerOnStart} is `false`.
  98.         *
  99.         * @type {Boolean}
  100.         * @default false
  101.         * @public
  102.         */
  103.         defaultPathOnStart: false,
  104.  
  105.         /**
  106.         * The `defaultRoute` should have the same structure as a normal route (hash). It may be
  107.         * arbitrarily assigned to this property or mixed into the
  108.         * [routes]{@link enyo.Router#routes} array with a special `default: true` flag set. For
  109.         * any unmatched hash changes, this route will be executed and passed the path that was not
  110.         * matched.
  111.         *
  112.         * @type {Object}
  113.         * @default null
  114.         * @public
  115.         */
  116.         defaultRoute: null,
  117.  
  118.         /**
  119.         * By default, when a router is created, it will attempt to trigger the correct route for the
  120.         * current browser location. Set this flag to `false` to prevent this behavior.
  121.         *
  122.         * @type {Boolean}
  123.         * @default true
  124.         * @public
  125.         */
  126.         triggerOnStart: true,
  127.  
  128.         /**
  129.         * The router will attempt to track history based on the events that have been generated
  130.         * through it. This allows the usage of the browser's 'Back' and 'Forward' buttons.
  131.         *
  132.         * @type {Boolean}
  133.         * @default false
  134.         * @public
  135.         */
  136.         useHistory: false,
  137.  
  138.         /**
  139.         * The `routes` [array]{@glossary Array} specifies the handlers for
  140.         * the current Router instance. Routes are string paths, static or dynamic,
  141.         * that route particular `hashchange` events. They are defined in an array of
  142.         * [hashes]{@glossary Object} with a `handler` (function name), `path`
  143.         * (for static and dynamic paths), an optional `context` (for the `handler`),
  144.         * or a `default` Boolean `true`|`false` value indicating whether the handler
  145.         * should be used when no other route can handle the `hashchange` event.
  146.         *
  147.         * @example
  148.         * routes: [
  149.         *     {path: 'users/:userName', handler: 'loadUser'},
  150.         *     {path: 'users', handler: 'usersIndex'},
  151.         *     {path: 'about/moreDetail', handler: 'detailedAboutScreen'},
  152.         *     {path: 'about', handler: 'aboutScreen'},
  153.         *     {path: 'home', handler: 'homeScreen', default: true},
  154.         *     {path: '', handler: 'handleBlankRoute'}
  155.         * ]
  156.         *
  157.         * @type {Array}
  158.         * @public
  159.         */
  160.         routes: null,
  161.  
  162.         /// ...........................
  163.         // PROTECTED PROPERTIES
  164.  
  165.         /*
  166.         * @private
  167.         */
  168.         kind: "enyo.Controller",
  169.  
  170.         /*
  171.         * @private
  172.         */
  173.         _token: /\:[a-zA-Z0-9-_]*/g,
  174.  
  175.         /*
  176.         * @private
  177.         */
  178.         _tokenValue: "([a-zA-Z0-9-_.!~*'()%+]*)",
  179.  
  180.         /*
  181.         * @private
  182.         */
  183.         _staticRoutes: null,
  184.  
  185.         /*
  186.         * @private
  187.         */
  188.         _dynamicRoutes: null,
  189.  
  190.         /*
  191.         * @private
  192.         */
  193.         _current: "",
  194.  
  195.         /*
  196.         * @private
  197.         */
  198.         _history: null,
  199.  
  200.         // ...........................
  201.         // COMPUTED PROPERTIES
  202.  
  203.         computed: [{
  204.             method: 'location',
  205.             path: '_current',
  206.             config: {
  207.                 cached: true
  208.             }
  209.         }, {
  210.             method: 'defaultPath'
  211.         }],
  212.  
  213.         /**
  214.         * A computed property that returns the location as known by this router. This property
  215.         * will be synchronized with [window.location.hash]{@glossary window.location}
  216.         * unless the [internalOnly]{@link enyo.Router#internalOnly} flag is set to `true`.
  217.         * Passing a string to this property via `_set('location', 'mylocation')` will update the
  218.         * inner location known by this router.
  219.         *
  220.         * @public
  221.         */
  222.         location: function(location) {
  223.             if (location) {
  224.                 location = this._prepare(location);
  225.                 if (!this.internalOnly) {
  226.                     enyo.asyncMethod(this, "trigger", {
  227.                         location: location,
  228.                         change: true
  229.                     });
  230.                 } else {
  231.                     this.set("_current", location);
  232.                 }
  233.             } else {
  234.                 return this._prepare(this.get("_current"));
  235.             }
  236.         },
  237.  
  238.         /**
  239.         * Returns the string for the default path, or an empty string if no default
  240.         * path exists.
  241.         *
  242.         * @public
  243.         */
  244.         defaultPath: function() {
  245.             return this.defaultRoute ? this.defaultRoute.path : "";
  246.         },
  247.  
  248.         // ...........................
  249.         // PUBLIC METHODS
  250.  
  251.         /**
  252.         * Triggers a change without necessarily requiring a change to occur. If
  253.         * called without a parameter, it will force the route that matches the
  254.         * current browser location to fire. If a string is passed in, this method
  255.         * will trigger an internal-only event (i.e., the browser location will not
  256.         * be changed). If it is passed a hash, the method will try to use a
  257.         * `location` property while looking for optional `change` and `global`
  258.         * properties.
  259.         *
  260.         * If the `change` property is present and `true`, it will force a
  261.         * [location.hash]{@glossary window.location} change in the
  262.         * browser (this is always global). If the `global` property is present and
  263.         * `true` and `change` is not present or `false`, it will trigger an internal
  264.         * event that all routers will respond to (not just this instance).
  265.         *
  266.         * @public
  267.         */
  268.         trigger: function(params) {
  269.             var location,
  270.                 global,
  271.                 change,
  272.                 current;
  273.  
  274.             if (!params) {
  275.                 params = {
  276.                     location: this.get("_current")
  277.                 };
  278.             } else if ("string" === typeof params) {
  279.                 params = {
  280.                     location: params
  281.                 };
  282.             }
  283.             location = params.location;
  284.             global = params.global;
  285.             change = params.change;
  286.             current = this.get("location");
  287.             if (change) {
  288.                 if (current !== location) {
  289.                     window.location.hash = location;
  290.                 } else {
  291.                     this._hashChanged(location);
  292.                 }
  293.             } else {
  294.                 if (global) {
  295.                     enyo.Router.hashDidChange(location);
  296.                 } else {
  297.                     this._hashChanged(location);
  298.                 }
  299.             }
  300.         },
  301.  
  302.         /**
  303.         * In very rare circumstances, it may be useful to pass a path to the
  304.         * [routes]{@link enyo.Router#routes} without using
  305.         * [trigger()]{@link enyo.Router#trigger} or global hash changes with
  306.         * `path` being a string that will be evaluated against the routes owned by
  307.         * this router.
  308.         *
  309.         * @param {String} path - A path to test against this router's
  310.         *   [routes]{@link enyo.Router#routes}.
  311.         * @public
  312.         */
  313.         handle: function(path) {
  314.             // fast track is to check against static routes first
  315.             if (this._handleStatic(path)) {
  316.                 return;
  317.             }
  318.             // then we check against dynamic paths in this simple scheme
  319.             else if (this._handleDynamic(path)) {
  320.                 /* do nothing */
  321.             } else {
  322.                 this._handleDefault(path);
  323.             }
  324.         },
  325.  
  326.         /**
  327.         * If history is enabled and some history exists, attempts to revert the
  328.         * current known location to the previous one in the stack.
  329.         *
  330.         * @return {Boolean} True if a previous location was found and applied.
  331.         * @public
  332.         */
  333.         back: function() {
  334.             if (this.useHistory) {
  335.                 if (this._history.length >= 2) {
  336.                     // we shift the current location off the stack
  337.                     this._history.shift();
  338.                     // we shift the requested location off the stack
  339.                     // but reapply it
  340.                     this.location(this._history.shift());
  341.                     return true;
  342.                 }
  343.             }
  344.             return false;
  345.         },
  346.  
  347.         /**
  348.         * Arbitrarily adds history. The optional second parameter may be set to a
  349.         * boolean `true` to add the location at the lowest (first) position in the
  350.         * stack, or to an integer indicating the exact position for the location in
  351.         * the stack. If the index is out of bounds, the location will be added at
  352.         * the lowest position (the same as if boolean `true` is passed as the second
  353.         * parameter). Returns callee for chaining.
  354.         *
  355.         * @param {String} location - The location string to add to the history.
  356.         * @param {(Number|Boolean)} [idx] - Position in the history stack where the
  357.         * new location should be added. Pass `true` for the first/oldest position,
  358.         * or a number indicating the index where the location should be added. If no
  359.         * value (or `undefined`) is specified, the location will be added at the
  360.         *   last/most-recent position in the history stack.
  361.         * @returns {this} The callee for chaining.
  362.         * @public
  363.         */
  364.         addHistory: function(location, idx) {
  365.             if (this.useHistory) {
  366.                 switch (typeof idx) {
  367.                     case "undefined":
  368.                         this._history.unshift(location);
  369.                         break;
  370.                     case "number":
  371.                         if (idx >= 0 && idx < this._history.length) {
  372.                             this._history.splice(idx, 0, location);
  373.                         }
  374.                         break;
  375.                     case "boolean":
  376.                         this._history.push(location);
  377.                         break;
  378.                 }
  379.             }
  380.             return this;
  381.         },
  382.  
  383.         /**
  384.         * Clears any history the router has stored. Returns callee for chaining.
  385.         *
  386.         * @returns {this} The callee for chaining.
  387.         * @public
  388.         */
  389.         clearHistory: function() {
  390.             this._history = [];
  391.             return this;
  392.         },
  393.  
  394.         /**
  395.         * May be used to programmatically add [routes]{@link enyo.Router#routes}
  396.         * to the router. `route` is a hash as described by the
  397.         * [routes]{@link enyo.Router#routes} array. Returns callee for chaining.
  398.         *
  399.         * @param {Object} route - The route to add.
  400.         * @returns {this} The callee for chaining.
  401.         * @public
  402.         */
  403.         addRoute: function(route) {
  404.             var token = new RegExp(this._token),
  405.                 tokenValue = this._tokenValue,
  406.                 statics = this._staticRoutes,
  407.                 dynamic = this._dynamicRoutes,
  408.                 tokens,
  409.                 key,
  410.                 regex;
  411.  
  412.             if (true === route["default"]) {
  413.                 this.defaultRoute = route;
  414.             } else if (token.test(route.path)) {
  415.                 token.lastIndex = 0;
  416.                 route.tokens = [];
  417.                 tokens = token.exec(route.path);
  418.                 for (i = 0; (key = tokens[i]); i++) {
  419.                     route.tokens.push(key.substr(1));
  420.                 }
  421.                 regex = new RegExp(route.path.replace(token, tokenValue));
  422.                 route.regex = regex;
  423.                 dynamic.push(route);
  424.             } else {
  425.                 statics[route.path] = route;
  426.             }
  427.             return this;
  428.         },
  429.  
  430.         // ...........................
  431.         // PROTECTED METHODS
  432.  
  433.         /*
  434.         * @private
  435.         */
  436.         constructor: enyo.inherit(function(sup) {
  437.             return function() {
  438.                 this._staticRoutes = {};
  439.                 this._dynamicRoutes = [];
  440.                 this.routes = this.routes || [];
  441.                 this._history = this._history || [];
  442.                 sup.apply(this, arguments);
  443.             };
  444.         }),
  445.  
  446.         /*
  447.         * @private
  448.         */
  449.         create: enyo.inherit(function(sup) {
  450.             return function() {
  451.                 sup.apply(this, arguments);
  452.                 // make sure to initialize our routes prior
  453.                 // to registering for events
  454.                 this._setupRoutes();
  455.                 // make sure we're up to date
  456.                 this.set("_current", this._prepare(window.location.hash));
  457.                 // ok, register for events
  458.                 enyo.Router.listeners.push(this);
  459.                 // ok, if we need to go ahead and route our current
  460.                 // location, lets do it, but let create finish first
  461.                 if (this.triggerOnStart) {
  462.                     if (this.defaultPathOnStart) {
  463.                         enyo.asyncMethod(this, 'trigger', {change: true, location: this.get('defaultPath')});
  464.                     } else {
  465.                         enyo.asyncMethod(this, 'trigger');
  466.                     }
  467.                 }
  468.             };
  469.         }),
  470.  
  471.         /*
  472.         * @private
  473.         */
  474.         destroy: enyo.inherit(function(sup) {
  475.             return function() {
  476.                 var listeners = enyo.Router.listeners,
  477.                     idx = enyo.indexOf(this, listeners);
  478.  
  479.                 if (!~idx) {
  480.                     listeners.splice(idx, 1);
  481.                 }
  482.                 sup.apply(this, arguments);
  483.             };
  484.         }),
  485.  
  486.         /*
  487.         * @private
  488.         */
  489.         _prepare: function(str) {
  490.             return str[0] === "#" ? str.slice(1) : str;
  491.         },
  492.  
  493.         /*
  494.         * @private
  495.         */
  496.         _hashChanged: function(hash) {
  497.             var that = this,
  498.                 $hash;
  499.  
  500.             $hash = (function(prop) {
  501.                 if (!enyo.isString(prop)) {
  502.                     // some browsers do not support the newUrl property
  503.                     // so we're forced to look at the current hash
  504.                     prop = prop.newUrl || window.location.hash;
  505.                 }
  506.                 return that._prepare(prop);
  507.             })(hash);
  508.             if (this.listening) {
  509.                 this.set("_current", $hash);
  510.                 this.handle($hash);
  511.             }
  512.         },
  513.  
  514.         /*
  515.         * @private
  516.         */
  517.         _execHandler: function(context, handler, route, path, parameters) {
  518.             var $fn = handler,
  519.                 $ctx;
  520.  
  521.             $ctx = "string" === typeof context ? enyo.getPath.call(this, context) : context;
  522.             // if the handler is defined as a string, we need to determine if
  523.             // it is relative to the router, relative to the context, or a named
  524.             // function in the global scope
  525.             if ("string" === typeof handler) {
  526.                 if (typeof this[handler] === 'function') {
  527.                     $fn = this[handler];
  528.                     $ctx = $ctx || this;
  529.                 } else if (typeof this.owner[handler] === 'function') {
  530.                     $fn = this.owner[handler];
  531.                     $ctx = $ctx || this.owner;
  532.                 } else if (typeof $ctx[handler] === 'function') {
  533.                     $fn = $ctx[handler];
  534.                 }
  535.                 if ("function" === typeof $fn) {
  536.                     // in case we actually found it, let's not go hunting
  537.                     // next time
  538.                     route.handler = $fn;
  539.                     route.context = $ctx;
  540.                 }
  541.             }
  542.             // if we have an actual handler, let's execute it now
  543.             if ($fn && "function" === typeof $fn) {
  544.                 $fn.apply($ctx, [path, parameters]);
  545.                 return true;
  546.             }
  547.             // otherwise we couldn't determine what we were supposed to
  548.             // do here
  549.             return false;
  550.         },
  551.  
  552.         /*
  553.         * @private
  554.         */
  555.         _handleStatic: function(path) {
  556.             var statics = this._staticRoutes,
  557.                 route,
  558.                 handler,
  559.                 context;
  560.  
  561.             if ((route = statics[path])) {
  562.                 handler = route.handler;
  563.                 context = route.context;
  564.                 return this._execHandler(context, handler, route, path);
  565.             }
  566.             return false;
  567.         },
  568.  
  569.         /*
  570.         * @private
  571.         */
  572.         _handleDynamic: function(path) {
  573.             var idx,
  574.                 dynamic = this._dynamicRoutes,
  575.                 len = dynamic.length,
  576.                 route,
  577.                 regex,
  578.                 matches,
  579.                 i,
  580.                 match,
  581.                 key,
  582.                 parameters = {},
  583.                 handler,
  584.                 context;
  585.  
  586.             for (idx = 0; idx < len; ++idx) {
  587.                 route = dynamic[idx];
  588.                 regex = route.regex;
  589.                 if ((matches = regex.exec(path))) {
  590.                     // we need to strip off the full match so we can
  591.                     // use the other matches as parameters
  592.                     matches = matches.slice(1);
  593.                     for (i = 0;
  594.                         (match = matches[i]); i++) {
  595.                         key = route.tokens[i];
  596.                         parameters[key] = decodeURIComponent(match.replace(/\+/g, "%20"));
  597.                     }
  598.                     handler = route.handler;
  599.                     context = route.context;
  600.                     return this._execHandler(context, handler, route, path, parameters);
  601.                 }
  602.             }
  603.  
  604.             return false;
  605.         },
  606.  
  607.         /*
  608.         * @private
  609.         */
  610.         _handleDefault: function(path) {
  611.             var route = this.defaultRoute || {},
  612.                 context = route.context,
  613.                 handler = route.handler;
  614.  
  615.             return this._execHandler(context, handler, route, path);
  616.         },
  617.  
  618.         /*
  619.         * @private
  620.         */
  621.         _setupRoutes: function() {
  622.             var routes = this.routes,
  623.                 idx = 0,
  624.                 len = routes.length,
  625.                 route;
  626.  
  627.             for (; idx < len; ++idx) {
  628.                 route = routes[idx];
  629.                 if (!route) {
  630.                     continue;
  631.                 }
  632.                 this.addRoute(route);
  633.             }
  634.         },
  635.  
  636.         /*
  637.         * @private
  638.         */
  639.         _currentChanged: function() {
  640.             if (this.useHistory) {
  641.                 this._history.unshift(this.get("location"));
  642.             }
  643.         },
  644.  
  645.         statics: {
  646.             /**
  647.             * Any instance of a router will be referenced here for the global hash change handler.
  648.             *
  649.             * @private
  650.             */
  651.             listeners: [],
  652.  
  653.             /**
  654.             * This is the intended global `window.hashchange` event handler. If another handler is
  655.             * arbitrarily registered for this event, then it will not fire.
  656.             *
  657.             * @private
  658.             */
  659.             hashDidChange: function(hash) {
  660.                 var list = enyo.Router.listeners;
  661.                 var len = list.length;
  662.                 var idx = 0;
  663.                 for (; idx < len; ++idx) {
  664.                     list[idx]._hashChanged(hash);
  665.                 }
  666.             }
  667.         }
  668.     });
  669.  
  670. }(enyo));
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement