UNPKG

ares-ide

Version:

A browser-based code editor and UI designer for Enyo 2 projects

521 lines (465 loc) 14.2 kB
(function (enyo) { //*@protected /** Any instance of a router will be referenced here for the global hash change handler. */ var listeners = []; //*@protected /** This is the intended global _window.hashchange_ event handler. If another handler is arbitrarily registered for this event, then it will not fire. */ var hashDidChange = function (hash) { var list = listeners; var len = list.length; var idx = 0; for (; idx < len; ++idx) { list[idx]._hashChanged(hash); } }; //*@protected var token = /\:[a-zA-Z0-9]*/g; //*@protected var prepare = function (str) { return str[0] === "#"? str.slice(1): str; }; //*@protected /** All of our actively-supported browsers support this method of registering for _hashchange_ events. */ enyo.ready(function () { enyo.dispatcher.listen(window, "hashchange", hashDidChange); }); //*@public /** _enyo.Router_ is a controller with the ability to interpret changes in the url as well as set changes to the url in a manner that is compatible across browsers. With defined route handling, the application state can be managed more closely with respect to the location state of the browser. There may be multiple router instances active at any one time. Routers only interact with the hash portion of the browser location and will not force a reload of the current page. Routes may be defined in several ways and may be added at startup or added programmatically at a later time. A route is a declarative hash with the following structure: {path: "some/path", handler: "function", context: "context"} The path is a string that may be static (explicitly matched) or dynamic (matched based on dynamic placeholders). Dynamic paths may name elements to be matched, e.g.: {path: ":user/:id"} In this case, the handler would be called with two parameters filled with the values matched by that structure. It is important to note that a dynamic route will apply the first match it finds, so care must be used to ensure that the correct route is matched (e.g., _/users/:user/:id_ is more exact because of the static _users_ portion of the path). The handler may be a function reference or a string that will be mapped to a function. A check for the function's existence is conducted first on the router, then on any provided context, and finally in the global scope. If a context is provided, the function will be executed under that context regardless of where the function was found. The context property may be an object, an instance or a string that will be mapped to an object if possible. Note that, currently, only letters and numbers are supported in dynamic routes. */ enyo.kind({ // ........................... // PUBLIC PROPERTIES //*@public name: "enyo.Router", //*@public /** If true, the router will respond to hash changes or internal events. If this flag is set to false, it will stop responding. This may be changed at any time. */ listening: true, //*@public /** If true, the router will neither respond to hash changes in the browser nor be able to trigger them. Instead, it may be used internally to maintain or trigger state changes in an application without changing location. */ internalOnly: false, //*@public /** If the desire is to force the current browser location to a particular path on startup set this value to true. Will be ignored if _triggerOnStart_ is false. */ defaultPathOnStart: false, //*@public /** The _defaultRoute_ should have the same structure as a normal route (hash). It may be arbitrarily assigned to this property or mixed into the routes array with a special _default: true_ flag set. For any unmatched hash changes, this route will be executed and passed the path that was not matched. */ defaultRoute: null, //*@public /** By default, when a router is created, it will attempt to trigger the correct route for the current browser location. Set this to false to prevent this from happening. */ triggerOnStart: true, //*@public /** The router will attempt to track history based on the events that have been generated through it. */ useHistory: false, //*@public /** The _routes_ array constitutes the handlers for this router. Routes are string paths, static or dynamic, that route particular _hashchange_ events. They are defined in an array of hashes with a _handler_ (function), _path_ (for static and dynamic paths), an optional _context_ (for the handler), or a _default_ boolean true|false where that handler will be used when no other route can handle the _hashchange_ event. */ routes: null, // ........................... // PROTECTED PROPERTIES //*@protected kind: "enyo.Controller", //*@protected _staticRoutes: null, //*@protected _dynamicRoutes: null, //*@protected _current: "", //*@protected _history: null, // ........................... // COMPUTED PROPERTIES computed: { location: ["_current", {cached: true}], defaultPath: [] }, //*@public /** A computed property that will return the location as known by this router. This property will be synchronized with _window.location.hash_ unless its _internalOnly_ flag is set to true. Passing a string to this property via _set("location", "mylocation")_ will update the inner location known by this router. */ location: function (loc) { if (loc) { loc = prepare(loc); if (!this.internalOnly) { enyo.asyncMethod(this, "trigger", {location: loc, change: true}); } else { this.set("_current", loc); } } else { return prepare(this.get("_current")); } }, //*@public /** Returns the string for the default path (if any; otherwise, an empty string). */ defaultPath: function () { return this.defaultRoute? this.defaultRoute.path: ""; }, // ........................... // PUBLIC METHODS //*@public /** Triggers a change without necessarily requiring a change to occur. If called without a parameter, it will force the route that matches the current browser location to fire. If a string is passed in, this method will trigger an internal-only event (i.e., the browser location will not be changed). If it is passed a hash, the method will try to use a _location_ property while looking for optional _change_ and _global_ properties. If the _change_ property is present and true, it will force a _location.hash_ change in the browser (this is always global). If the _global_ property is present and true and _change_ is not present or false, it will trigger an internal event that all routers will respond to (not just this instance). */ trigger: function (params) { if (!params) { params = {location: this.get("_current")}; } else if ("string" === typeof params) { params = {location: params}; } var loc = params.location; var global = params.global; var change = params.change; var current = this.get("location"); if (change) { if (current !== loc) { window.location.hash = loc; } else { this._hashChanged(loc); } } else { if (global) { hashDidChange(loc); } else { this._hashChanged(loc); } } }, //*@public /** In very rare circumstances, it may be useful to pass a path to the routes without using _trigger()_ or global hash changes with _path_ being a string that will be evaluated against the routes owned by this router. */ handle: function (path) { // fast track is to check against static routes first if (this._handleStatic(path)) { return; } // then we check against dynamic paths in this simple scheme else if (this._handleDynamic(path)) { /* do nothing */ } else { this._handleDefault(path); } }, //*@public /** If history is enabled and some history exists, attempts to revert the current known location to the previous one in the stack. */ back: function () { if (this.useHistory) { if (this._history.length >= 2) { // we shift the current location off the stack this._history.shift(); // we shift the requested location off the stack // but reapply it this.set("location", this._history.shift()); } } }, //*@public /** Arbitrarily adds history. Optional second parameter can be a boolean true to place the location at the lowest (first) position of the stack, or an integer indicating its exact location in the stack. If the index is out of bounds, it will be added at the lowest position (same as boolean true for second parameter). Returns callee for chaining. */ addHistory: function (location, idx) { if (this.useHistory) { switch (typeof idx) { case "undefined": this._history.unshift(location); break; case "number": if (idx >= 0 && idx < this._history.length) { this._history.splice(idx, 0, location); } break; case "boolean": this._history.push(location); break; } } return this; }, //*@public /** Clears any history the router has currently stored. Returns callee for chaining. */ clearHistory: function () { this._history = []; return this; }, //*@public /** Can be used to programmatically add routes to the router where _route_ is a hash as described by the _routes_ array. Returns callee for chaining. */ addRoute: function (route) { var statics = this._staticRoutes; var dynamic = this._dynamicRoutes; var regex; if (true === route['default']) { this.defaultRoute = route; } else if (token.test(route.path)) { regex = new RegExp(route.path.replace(token, "([a-zA-Z0-9-]*)")); route.regex = regex; dynamic.push(route); } else { statics[route.path] = route; } return this; }, // ........................... // PROTECTED METHODS //*@protected constructor: enyo.inherit(function (sup) { return function () { this._staticRoutes = {}; this._dynamicRoutes = []; this.routes = this.routes || []; this._history = this._history || []; sup.apply(this, arguments); }; }), //*@protected create: enyo.inherit(function (sup) { return function () { sup.apply(this, arguments); // make sure to initialize our routes prior // to registering for events this._setupRoutes(); // make sure we're up to date this.set("_current", prepare(window.location.hash)); // ok, register for events listeners.push(this); // ok, if we need to go ahead and route our current // location, lets do it if (this.triggerOnStart) { if (this.defaultPathOnStart) { this.trigger({change: true, location: this.get("defaultPath")}); } else { this.trigger(); } } }; }), //*@protected destroy: enyo.inherit(function (sup) { return function () { var idx = enyo.indexOf(this, listeners); if (!~idx) { listeners.splice(idx, 1); } sup.apply(this, arguments); }; }), //*@protected _hashChanged: function (hash) { var $hash = (function (prop) { if (!enyo.isString(prop)) { // some browsers do not support the newUrl property // so we're forced to look at the current hash prop = prop.newUrl || window.location.hash; } return prepare(prop); })(hash); if (this.listening) { this.set("_current", $hash); this.handle($hash); } }, //*@protected _execHandler: function (context, handler, args, route) { var $fn = handler; var $ctx = "string" === typeof context? enyo.getPath.call(this, context): context || this; // if the handler is defined as a string, we need to determine if // it is relative to the router, relative to the context, or a named // function in the global scope if ("string" === typeof handler) { // first check to see if the handler is a named property // on the router; otherwise, try the context itself $fn = this[handler] || $ctx[handler]; if ("function" === typeof $fn) { // in case we actually found it, let's not go hunting // next time route.handler = $fn; route.context = $ctx; } } // if we have an actual handler, let's execute it now if ($fn && "function" === typeof $fn) { $fn.apply($ctx, args); return true; } // otherwise we couldn't determine what we were supposed to // do here return false; }, //*@protected _handleStatic: function (path) { var statics = this._staticRoutes; var route; var handler; var context; if ((route = statics[path])) { handler = route.handler; context = route.context; return this._execHandler(context, handler, [path], route); } return false; }, //*@protected _handleDynamic: function (path) { var dynamic = this._dynamicRoutes; var regex; var route; var handler; var context; var matches; var idx = 0; var len = dynamic.length; for (; idx < len; ++idx) { route = dynamic[idx]; regex = route.regex; if ((matches = regex.exec(path))) { // we need to strip off the full match so we can // use the other matches as parameters matches = matches.slice(1); handler = route.handler; context = route.context; return this._execHandler(context, handler, matches, route); } } return false; }, //*@protected _handleDefault: function (path) { var route = this.defaultRoute || {}; var context = route.context; var handler = route.handler; return this._execHandler(context, handler, [path], route); }, //*@protected _setupRoutes: function () { var routes = this.routes; var idx = 0; var len = routes.length; var route; for (; idx < len; ++idx) { route = routes[idx]; if (!route) { continue; } this.addRoute(route); } }, //*@protected _currentChanged: function () { if (this.useHistory) { this._history.unshift(this.get("location")); } } }); }(enyo));