UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

507 lines (436 loc) 15.2 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2012 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Martin Wittemann (wittemann) * Tino Butz (tbtz) ************************************************************************ */ /** * * Basic application routing manager. * * Define routes to react on certain GET / POST / DELETE / PUT operations. * * * GET is triggered when the hash value of the url is changed. Can be called * manually by calling the {@link #executeGet} method. * * POST / DELETE / PUT has to be triggered manually right now (will be changed later) * by calling the {@link #executePost}, {@link #executeDelete}, {@link #executePut} method. * * This manager can also be used to provide browser history. * * *Example* * * Here is a little example of how to use the widget. * * <pre class='javascript'> * var r = new qx.application.Routing(); * * // show the start page, when no hash is given or the hash is "#/" * r.onGet("/", function(data) { * startPage.show(); * }, this); * * // whenever the url /address is called show the address book page. * r.onGet("/address", function(data) { * addressBookPage.show(); * }, this); * * // address with the parameter "id" * r.onGet("/address/{id}", function(data) { * addressPage.show(); * model.loadAddress(data.params.id); * }, this); * * // Alternative you can use regExp for a route * r.onGet(/address\/(.*)/, function(data) { * addressPage.show(); * model.loadAddress(data.params.0); * }, this); * * // make sure that the data is always loaded * r.onGet("/address.*", function(data) { * if (!model.isLoaded()) { * model.loadAddresses(); * } * }, this); * * // update the address * r.onPost("/address/{id}", function(data) { * model.updateAddress(data.params.id); * }, this); * * // delete the address and navigate back * r.onDelete("/address/{id}", function(data) { * model.deleteAddress(data.params.id); * r.executeGet("/address", {reverse:true}); * }, this); * </pre> * * This example defines different routes to handle navigation events. * * Note this class must be disposed after use */ qx.Bootstrap.define("qx.application.Routing", { implement: [ qx.core.IDisposable ], construct : function() { this.__messaging = new qx.event.Messaging(); this.__navigationHandler = qx.bom.History.getInstance(); this.__navigationHandler.addListener("changeState", this.__onChangeHash, this); }, statics : { DEFAULT_PATH : "/", __back : [], __forward : [] }, members : { __navigationHandler : null, __messaging : null, __currentGetPath : null, /** * Initialization method used to execute the get route for the currently set history path. * If no path is set, either the given argument named <code>defaultPath</code> * or the {@link #DEFAULT_PATH} will be used for initialization. * * @param defaultPath {String?} Optional default path for initialization. */ init : function(defaultPath) { if (qx.core.Environment.get("qx.debug")) { if (defaultPath != null) { qx.core.Assert.assertString(defaultPath, "Invalid argument 'defaultPath'"); } } var path = this.getState(); path = this._getPathOrFallback(path, defaultPath); this._executeGet(path, null, true); }, /** * Checks if path is valid and registered in channel "get" and then just returns it. * If the path is not valid either the <code>defaultPath</code> (if given) or the * {@link #DEFAULT_PATH} will be returned. * * @param path {String} Path which gets checked. * @param defaultPath {String?} Optional default path. * @return {String} A valid path. */ _getPathOrFallback : function(path, defaultPath) { if (path == "" || path == null || !this.__messaging.has("get", path)) { path = defaultPath || qx.application.Routing.DEFAULT_PATH; } return path; }, /** * Adds a route handler for the "get" operation. The route gets called * when the {@link #executeGet} method found a match. * * @param route {String|RegExp} The route, used for checking if the executed path matches. * @param handler {Function} The handler to call, when the route matches with the executed path. * @param scope {Object} The scope of the handler. * @return {String} Event listener ID */ onGet : function(route, handler, scope) { return this.__messaging.on("get", route, handler, scope); }, /** * This is a shorthand for {@link #onGet}. * * @param route {String|RegExp} The route, used for checking if the executed path matches. * @param handler {Function} The handler to call, when the route matches with the executed path. * @param scope {Object} The scope of the handler. * @return {String} Event listener ID */ on : function(route, handler, scope) { return this.onGet(route, handler, scope); }, /** * Adds a route handler for the "post" operation. The route gets called * when the {@link #executePost} method found a match. * * @param route {String|RegExp} The route, used for checking if the executed path matches. * @param handler {Function} The handler to call, when the route matches with the executed path. * @param scope {Object} The scope of the handler. * @return {String} Event listener ID */ onPost : function(route, handler, scope) { return this.__messaging.on("post", route, handler, scope); }, /** * Adds a route handler for the "put" operation. The route gets called * when the {@link #executePut} method found a match. * * @param route {String|RegExp} The route, used for checking if the executed path matches * @param handler {Function} The handler to call, when the route matches with the executed path * @param scope {Object} The scope of the handler * @return {String} Event listener ID */ onPut : function(route, handler, scope) { return this.__messaging.on("put", route, handler, scope); }, /** * Adds a route handler for the "delete" operation. The route gets called * when the {@link #executeDelete} method found a match. * * @param route {String|RegExp} The route, used for checking if the executed path matches * @param handler {Function} The handler to call, when the route matches with the executed path * @param scope {Object} The scope of the handler * @return {String} Event listener ID */ onDelete : function(route, handler, scope) { return this.__messaging.on("delete", route, handler, scope); }, /** * Adds a route handler for the "any" operation. The "any" operation is called * before all other operations. * * @param route {String|RegExp} The route, used for checking if the executed path matches * @param handler {Function} The handler to call, when the route matches with the executed path * @param scope {Object} The scope of the handler * @return {String} Event listener ID */ onAny : function(route, handler, scope) { return this.__messaging.onAny(route, handler, scope); }, /** * Removes a registered route by the given id. * * @param id {String} The id of the registered route */ remove : function(id) { this.__messaging.remove(id); }, /** * Hash change event handler. * * @param evt {qx.event.type.Data} The changeHash event. */ __onChangeHash : function(evt) { var path = evt.getData(); path = this._getPathOrFallback(path); if (path != this.__currentGetPath) { this._executeGet(path, null, true); } }, /** * Executes the get operation and informs all matching route handler. * * @param path {String} The path to execute * @param customData {var} The given custom data that should be propagated * @param fromEvent {var} Determines whether this method was called from history * */ _executeGet : function(path, customData, fromEvent) { this.__currentGetPath = path; var history = this.__getFromHistory(path); if (history) { if (!customData) { customData = history.data.customData || {}; customData.fromHistory = true; customData.action = history.action; customData.fromEvent = fromEvent; } else { this.__replaceCustomData(path, customData); } } else { this.__addToHistory(path, customData); qx.application.Routing.__forward = []; } this.__navigationHandler.setState(path); this.__messaging.emit("get", path, null, customData); }, /** * Executes the get operation and informs all matching route handler. * * @param path {String} The path to execute * @param customData {var} The given custom data that should be propagated */ executeGet : function(path, customData) { this._executeGet(path, customData); }, /** * This is a shorthand for {@link #executeGet}. * * @param path {String} The path to execute * @param customData {var} The given custom data that should be propagated */ execute : function(path, customData) { this.executeGet(path, customData); }, /** * Executes the post operation and informs all matching route handler. * * @param path {String} The path to execute * @param params {Map} The given parameters that should be propagated * @param customData {var} The given custom data that should be propagated */ executePost : function(path, params, customData) { this.__messaging.emit("post", path, params, customData); }, /** * Executes the put operation and informs all matching route handler. * * @param path {String} The path to execute * @param params {Map} The given parameters that should be propagated * @param customData {var} The given custom data that should be propagated */ executePut : function(path, params, customData) { this.__messaging.emit("put", path, params, customData); }, /** * Executes the delete operation and informs all matching route handler. * * @param path {String} The path to execute * @param params {Map} The given parameters that should be propagated * @param customData {var} The given custom data that should be propagated */ executeDelete : function(path, params, customData) { this.__messaging.emit("delete", path, params, customData); }, /** * Returns state value (history hash) of the navigation handler. * @return {String} State of history navigation handler */ getState : function() { return this.__navigationHandler.getState(); }, /** * Adds the custom data of a given path to the history. * * @param path {String} The path to store. * @param customData {var} The custom data to store */ __addToHistory : function(path, customData) { qx.application.Routing.__back.unshift({ path : path, customData : customData }); }, /** * Replaces the customData in the history objects with the recent custom data. * @param path {String} The path to replace. * @param customData {var} The custom data to store. */ __replaceCustomData : function(path, customData) { var register = [qx.application.Routing.__back, qx.application.Routing.__forward]; for (var i=0; i < register.length; i++) { for (var j=0; j < register[i].length; j++) { if (register[i][j].path == path) { register[i][j].customData = customData; } } } }, /** * Returns a history entry for a certain path. * * @param path {String} The path of the entry * @return {Map|null} The retrieved entry. <code>null</code> when no entry was found. */ __getFromHistory : function(path) { var back = qx.application.Routing.__back; var forward = qx.application.Routing.__forward; var found = false; var entry = null; var length = back.length; for (var i = 0; i < length; i++) { if (back[i].path == path) { entry = back[i]; var toForward = back.splice(0,i); for (var a=0; a<toForward.length; a++){ forward.unshift(toForward[a]); } found = true; break; } } if (found){ return { data : entry, action : "back" }; } var length = forward.length; for (var i = 0; i < length; i++) { if (forward[i].path == path) { entry = forward[i]; var toBack = forward.splice(0,i+1); for (var a=0; a<toBack.length; a++){ back.unshift(toBack[a]); } break; } } if (entry){ return { data : entry, action : "forward" }; } return entry; }, /** * Navigates back to the previously executed path. * * @param customData {Map?} The given custom data that should be propagated. * If it contains a key <code>defaultPath</code> and no history data is * available, its value is used as a target path. If it does not include * such a key, the routing's default path is used instead (again only for * empty history). * This behavior is useful for instance when reloading a page during * development but expecting the page's back button always to work. */ back : function(customData) { var data = customData; if (data) { data["action"] = "back"; } else { data = { "action": "back" }; } var path, back = qx.application.Routing.__back; if (back.length > 0) { // Remove current state back.shift(); } if (back.length > 0) { // Get previous state var state = back.shift(); this._executeGet(state.path, data); } else if (data.defaultPath) { path = data.defaultPath; delete data.defaultPath; this._executeGet(path, data); } else if (qx.application.Routing.DEFAULT_PATH) { this._executeGet(qx.application.Routing.DEFAULT_PATH, data); } }, /** * Decouples the Routing from the navigation handler. */ dispose : function() { this.__navigationHandler.removeListener("changeState", this.__onChangeHash, this); } } });