1 /** 2 @class 3 4 Application is used to create new rio application classes. It provides functionality for dependency management, 5 routing, history management and page management. 6 7 @extends rio.Attr 8 */ 9 rio.Application = { 10 /** 11 Creates an instance of rio.Application. 12 13 @param {String} name (optional) The name of this Application. Used primarily for testing reports. 14 @param {Object} extends (optional) An Attr class to use as a superclass. 15 @param {Object} args (optional) The definition of the class. 16 @returns a new instance of type Application 17 @type rio.Application 18 @example 19 rio.apps.example = rio.Application.create({ 20 require: ["pages/example_page"], 21 requireCss: ["css_reset", "example"], 22 routes: { 23 "": "examplePage" 24 }, 25 attrAccessors: [], 26 attrReaders: [], 27 methods: { 28 initialize: function(options) { 29 }, 30 31 examplePage: function() { 32 return new rio.pages.ExamplePage(); 33 } 34 } 35 }); 36 */ 37 create: function() { 38 var args = $A(arguments); 39 if (args.length > 0 && args.last() != undefined && !args.last().ATTR) { 40 args[args.size() - 1] = Object.extend({ noExtend: true }, args.last()); 41 } 42 var app = rio.Attr.create.apply(this, args); 43 44 app.addMethods( 45 /** 46 @scope rio.Application.prototype 47 */ 48 { 49 50 /** @private */ 51 initHistory: function() { 52 dhtmlHistory.initialize(); 53 dhtmlHistory.addListener(this.applyHistoryEntry.bind(this)); 54 }, 55 56 /** @private */ 57 applyHistoryEntry: function(location, historyData) { 58 if (this.__revertingTransientHistoryEntry) { 59 this.addHistoryEntry(this.__revertingTransientHistoryEntry[0], this.__revertingTransientHistoryEntry[1]); 60 this.__revertingTransientHistoryEntry = false; 61 } else { 62 this.navigateTo(location, true); 63 } 64 }, 65 66 /** @private */ 67 addHistoryEntry: function(location, transient) { 68 if (historyStorage.hasKey(this.getCurrentLocation()) && historyStorage.get(this.getCurrentLocation()).transient) { 69 this.__revertingTransientHistoryEntry = [location, transient]; 70 history.back(1); 71 } else { 72 dhtmlHistory.add(location, { transient: transient }); 73 } 74 }, 75 76 /** @private */ 77 resize: function() { 78 this.getCurrentPage().resize(); 79 }, 80 81 /** @private */ 82 keyPress: function(e) { 83 var currentPage = this.getCurrentPage(); 84 if (currentPage) { 85 this.getCurrentPage().keyPress(e); 86 } 87 }, 88 89 /** @private */ 90 keyDown: function(e) { 91 var keyMap = this.getKeyMap(); 92 if (keyMap) { 93 keyMap.handle(e); 94 } 95 var currentPage = this.getCurrentPage(); 96 if (currentPage) { 97 this.getCurrentPage()._keyDown(e); 98 } 99 }, 100 101 /** 102 This method is called just before the page is unloaded. This can be triggered by 103 following a link, closing the window, using the back button, etc. 104 105 <i>This method is meant to be overriden</i> 106 */ 107 unload: function() { 108 // meant to be overriden 109 }, 110 111 /** @private */ 112 getKeyMap: function() { 113 if (this._keyMap) { return this._keyMap; } 114 if (!this.keyMap) { return; } 115 116 this._keyMap = rio.KeyMap.build(this.keyMap()); 117 118 return this._keyMap; 119 }, 120 121 /** @private */ 122 launch: function() { 123 document.observe("keypress", this.keyPress.bind(this)); 124 document.observe("keydown", this.keyDown.bind(this)); 125 Event.observe(window, "beforeunload", this.unload.bind(this)); 126 127 if (this.noRoutes()) { return; } 128 129 this.initHistory(); 130 this.navigateTo(this.getCurrentLocation()); 131 132 Event.observe(window, "resize", this.resize.bind(this)); 133 134 rio.Application._afterLaunchFunctions.each(function(fcn) { 135 fcn(this); 136 }); 137 rio.Application._afterLaunchFunctions.clear(); 138 139 this._launched = true; 140 }, 141 142 /** @private */ 143 launched: function() { 144 return this._launched || false; 145 }, 146 147 /** @private */ 148 noRoutes: function() { 149 return (app.__routes == undefined) || ($H(app.__routes).keys().size() == 0); 150 }, 151 152 /** @private */ 153 avoidAnimation: function() { 154 return Prototype.Browser.IE; 155 }, 156 157 /** @private */ 158 matchRoutePath: function(path) { 159 return Object.keys(app.__routes).detect(function(routePath) { 160 if (routePath == "") { return true; } 161 var routeParts = routePath.split("/"); 162 var pathParts = path.split("/"); 163 var match = true; 164 for(var i=0; i<routeParts.length; i++) { 165 match = match && (routeParts[i].startsWith(":") || routeParts[i].startsWith("*") || routeParts[i] == pathParts[i]); 166 } 167 return match; 168 }); 169 }, 170 171 /** @private */ 172 matchRouteTarget: function(path) { 173 return app.__routes[this.matchRoutePath(path)]; 174 }, 175 176 /** @private */ 177 remainingPath: function(path) { 178 var match = this.matchRoutePath(path); 179 if (match == "") { return path; } 180 181 var matchParts = match.split("/"); 182 var matchLength = matchParts.last().startsWith("*") ? matchParts.length - 1 : matchParts.length; 183 184 return path.split("/").slice(matchLength).join("/"); 185 }, 186 187 /** @private */ 188 pathParts: function(path) { 189 var parts = {}; 190 var pathParts = path.split("/"); 191 var routePathParts = this.matchRoutePath(path).split("/"); 192 for(var i=0; i<routePathParts.length; i++) { 193 var routePathPart = routePathParts[i]; 194 if (routePathPart.startsWith(":") || routePathPart.startsWith("*")) { 195 parts[routePathPart.slice(1)] = pathParts[i]; 196 } 197 } 198 return parts; 199 }, 200 201 /** @private */ 202 navigateTo: function(path, noHistoryEntry) { 203 var subPath = this.matchRoutePath(path); 204 var target = this.matchRouteTarget(path); 205 if (!target) { rio.Application.fail("Path not found."); } 206 207 if (path != "" && !noHistoryEntry) { 208 this.addHistoryEntry(path); 209 } 210 var remainingPath = this.remainingPath(path); 211 var pathParts = this.pathParts(path); 212 213 if (!this.__pages) { this.__pages = {}; } 214 // Right now the && this.__pages[target] == this.getCurrentPage() prevents double rendering. 215 var page; 216 if (this.__pages[target] && this.__pages[target] == this.getCurrentPage()) { 217 page = this.__pages[target]; 218 } else { 219 page = this[target](); 220 this.__pages[target] = page; 221 } 222 223 if (page != this.getCurrentPage()) { 224 if (this.getCurrentPage()) { 225 this.clearPage(); 226 } 227 [Element.body(), Element.html()].each(function(elt) { 228 elt.setStyle({ 229 width: page.isManagingLayout() ? "100%" : "", 230 height: page.isManagingLayout() ? "100%" : "" 231 // overflow: page.isManagingLayout() ? "hidden" : "" 232 }); 233 }); 234 Element.body().insert(page.html()); 235 page.render(); 236 this.setCurrentPage(page); 237 } 238 239 page.applyHistoryEntry(remainingPath, pathParts); 240 }, 241 242 /** @private */ 243 clearPage: function() { 244 Element.body().childElements().each(function(elt) { 245 if (elt.tagName.toLowerCase() == 'iframe' || elt.id == 'rshStorageForm' || elt.id == 'rshStorageField' || elt.id == 'juggernaut_flash' || elt.hasClassName("preserve")) { 246 // keep it 247 } else { 248 elt.remove(); 249 } 250 }); 251 }, 252 253 /** 254 Refreshes the browser. This will reload your app's source code 255 and reinitialize your app. This is more severe than rebooting. 256 */ 257 refresh: function() { 258 document.location.reload(); 259 }, 260 261 /** 262 Reboots your application. Rebooting your application will reset and reload the 263 current page. 264 */ 265 reboot: function() { 266 this.clearPage(); 267 this.setCurrentPage(null); 268 this.navigateTo(this.getCurrentLocation()); 269 }, 270 271 /** @private */ 272 getCurrentLocation: function() { 273 return dhtmlHistory.getCurrentLocation(); 274 }, 275 276 /** 277 Returns the instance of the currently loaded page in the app. 278 279 @returns the instance of the currently loaded page 280 @type rio.Page 281 */ 282 getCurrentPage: function() { 283 return this._currentPage; 284 }, 285 286 /** @private */ 287 setCurrentPage: function(page) { 288 this._currentPage = page; 289 }, 290 291 /** @private */ 292 rootUrl: function() { 293 return document.location.protocol + "//" + document.location.host; 294 }, 295 296 toString: function() { 297 return "[rio.apps.*]"; 298 } 299 }); 300 301 Object.extend(app, 302 /** 303 @scope rio.Application 304 */ 305 { 306 /** 307 Specifies the lowest priority route for an application class. 308 309 <b>You are better off specifying routes when creating an 310 application with a 'routes' parameter.</b> 311 */ 312 route: function(path, target){ 313 if (!this.__routes) { this.__routes = {}; } 314 this.__routes[path] = target; 315 316 var parts = path.split("/"); 317 if (parts.length > 1) { 318 for (var i=0; i<parts.length - 1; i++) { 319 if (parts[i].startsWith("*")) { 320 throw("Only the final part of a route can be designated as optional by the *"); 321 } 322 } 323 } 324 } 325 }); 326 327 if (args.length > 0 && args.last() != undefined && !args.last().ATTR) { 328 var initializers = args.last(); 329 if (initializers.requireCss) { 330 rio.Application.includeCss(initializers.requireCss); 331 } 332 Object.keys(initializers.routes || {}).each(function(name) { 333 app.route(name, initializers.routes[name]); 334 }); 335 336 rio.Application.extend(app, initializers.methods || {}); 337 } 338 339 return app; 340 }, 341 342 /** @private */ 343 extend: function(app, extension) { 344 rio.Attr.extend(app, extension); 345 }, 346 347 /** 348 Alias of rio.Application.require 349 350 @param {String} fileName The path to the javascript file that will be loaded. 351 */ 352 include: function(fileName) { 353 this.require(fileName); 354 // rio.boot.loadFile(fileName); 355 }, 356 357 /** 358 Alias of rio.Application.require 359 360 @param {String} fileName The path to the javascript file that will be loaded. 361 */ 362 require: function(fileName) { 363 rio.require(fileName); 364 }, 365 366 /** @private */ 367 injectCss: function() { 368 var toLoad = []; 369 rio.boot.loadedStylesheets.each(function(s) { 370 if (!rio.preloadedStylesheets.include(s)) { toLoad.push(s); } 371 }); 372 if (toLoad.empty()) { return; } 373 374 var query = toLoad.map(function(f) { 375 return "files[]=" + f; 376 }).join("&"); 377 var linkHtml = rio.Tag.link("", { 378 href: rio.url("/rio/stylesheets?" + query + "&" + rio.cacheKey), 379 media: "screen", 380 rel: "stylesheet", 381 type: "text/css" 382 }); 383 Element.head().insert({ bottom: linkHtml }); 384 385 if (rio.ContainerLayout) { 386 rio.ContainerLayout.resize(); 387 } 388 }, 389 390 /* 391 Requires a css file 392 393 @param {String} toInclude The path to the stylesheet that will be loaded. 394 */ 395 includeCss: function(toInclude) { 396 // Because of a bug in IE, we need to remove and readd all link tags every time a new one is added. 397 var include = function(fileName) { 398 if (rio.boot.loadedStylesheets.include(fileName)) { return; } 399 rio.boot.loadedStylesheets.push(fileName); 400 if (rio.preloadedStylesheets.include(fileName)) { return; } 401 402 if (rio.environment.autoConcatCss && !(rio.app && rio.app.launched())) { 403 // Do nothing 404 } else { 405 var linkHtml = rio.Tag.link("", { 406 href: rio.url("/stylesheets/" + fileName + ".css"), 407 media: "screen", 408 rel: "stylesheet", 409 type: "text/css" 410 }); 411 Element.head().insert({ bottom: linkHtml }); 412 } 413 }.bind(this); 414 if (Object.isString(toInclude)) { 415 include(toInclude); 416 } 417 if (Object.isArray(toInclude)) { 418 toInclude.reverse().each(function(css) { 419 include(css); 420 }); 421 } 422 }, 423 424 /** @private */ 425 getToken: function() { 426 return this._token || rio.environment.railsToken; 427 }, 428 429 /** @private */ 430 setToken: function(token) { 431 this._token = token; 432 }, 433 434 /** @private */ 435 _afterLaunchFunctions: [], 436 437 /** @private */ 438 afterLaunch: function(afterLoadFunction) { 439 if (rio.app && rio.app.launched()) { 440 afterLoadFunction(rio.app); 441 } else { 442 this._afterLaunchFunctions.push(afterLoadFunction); 443 } 444 }, 445 446 /** 447 This causes the application to fail and log a 'fail' error message. If the application class 448 has a fail method, that method will be called with the message passed in here. 449 450 @param {String} msg The application failure message 451 @param {String} msg A more in depth description of the application failure 452 */ 453 fail: function(msg, description) { 454 try { 455 if (rio.app && rio.app.fail) { 456 rio.app.fail(msg); 457 rio.error("FAIL: " + msg, description || "", true); 458 } else { 459 alert("OOPS: " + msg + "\n\nTry refreshing the page or come back later."); 460 } 461 } catch(e) { 462 // Ignore errors during fail 463 } 464 }, 465 466 toString: function() { 467 return "Application"; 468 } 469 }; 470 471 if (!window.dhtmlHistoryCreated) { 472 window.dhtmlHistory.create({ 473 toJSON: function(o) { 474 return Object.toJSON(o); 475 }, 476 fromJSON: function(s) { 477 return s.evalJSON(); 478 } 479 }); 480 window.dhtmlHistoryCreated = true; 481 } 482 483