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, path); 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 /** 272 Get the full path of your rio application after the hash. 273 274 e.g. http://thinklinkr.com/outliner#584/revisions => "584/revisions" 275 276 @returns the path of your rio application after the hash 277 @type String 278 */ 279 getCurrentLocation: function() { 280 return dhtmlHistory.getCurrentLocation(); 281 }, 282 283 /** 284 Returns the instance of the currently loaded page in the app. 285 286 @returns the instance of the currently loaded page 287 @type rio.Page 288 */ 289 getCurrentPage: function() { 290 return this._currentPage; 291 }, 292 293 /** @private */ 294 setCurrentPage: function(page) { 295 this._currentPage = page; 296 }, 297 298 /** @private */ 299 rootUrl: function() { 300 return document.location.protocol + "//" + document.location.host; 301 }, 302 303 toString: function() { 304 return "[rio.apps.*]"; 305 } 306 }); 307 308 Object.extend(app, 309 /** 310 @scope rio.Application 311 */ 312 { 313 /** 314 Specifies the lowest priority route for an application class. 315 316 <b>You are better off specifying routes when creating an 317 application with a 'routes' parameter.</b> 318 */ 319 route: function(path, target){ 320 if (!this.__routes) { this.__routes = {}; } 321 this.__routes[path] = target; 322 323 var parts = path.split("/"); 324 if (parts.length > 1) { 325 for (var i=0; i<parts.length - 1; i++) { 326 if (parts[i].startsWith("*")) { 327 throw("Only the final part of a route can be designated as optional by the *"); 328 } 329 } 330 } 331 }, 332 333 /** 334 Specifies the application level environment variables. 335 336 <b>You are better off specifying environment when creating an 337 application with a 'environment' parameter.</b> 338 */ 339 setEnvironment: function(env) { 340 this.__env = env; 341 }, 342 343 /** 344 Returns the application level environment variables 345 */ 346 environment: function() { 347 return this.__env || {}; 348 } 349 }); 350 351 if (args.length > 0 && args.last() != undefined && !args.last().ATTR) { 352 var initializers = args.last(); 353 if (initializers.requireCss) { 354 rio.Application.includeCss(initializers.requireCss); 355 } 356 Object.keys(initializers.routes || {}).each(function(name) { 357 app.route(name, initializers.routes[name]); 358 }); 359 360 if (initializers.environment) { 361 app.setEnvironment(initializers.environment); 362 } 363 364 rio.Application.extend(app, initializers.methods || {}); 365 } 366 367 return app; 368 }, 369 370 /** @private */ 371 extend: function(app, extension) { 372 rio.Attr.extend(app, extension); 373 }, 374 375 /** 376 Alias of rio.Application.require 377 378 @param {String} fileName The path to the javascript file that will be loaded. 379 */ 380 include: function(fileName) { 381 this.require(fileName); 382 // rio.boot.loadFile(fileName); 383 }, 384 385 /** 386 Alias of rio.Application.require 387 388 @param {String} fileName The path to the javascript file that will be loaded. 389 */ 390 require: function(fileName) { 391 rio.require(fileName); 392 }, 393 394 /** @private */ 395 injectCss: function() { 396 var toLoad = []; 397 rio.boot.loadedStylesheets.each(function(s) { 398 if (!rio.preloadedStylesheets.include(s)) { toLoad.push(s); } 399 }); 400 if (toLoad.empty()) { return; } 401 402 var query = toLoad.map(function(f) { 403 return "files[]=" + f; 404 }).join("&"); 405 var linkHtml = rio.Tag.link("", { 406 href: rio.url("/rio/stylesheets?" + query + "&" + rio.cacheKey), 407 media: "screen", 408 rel: "stylesheet", 409 type: "text/css" 410 }); 411 Element.head().insert({ bottom: linkHtml }); 412 413 if (rio.ContainerLayout) { 414 rio.ContainerLayout.resize(); 415 } 416 }, 417 418 /* 419 Requires a css file 420 421 @param {String} toInclude The path to the stylesheet that will be loaded. 422 */ 423 includeCss: function(toInclude) { 424 // Because of a bug in IE, we need to remove and readd all link tags every time a new one is added. 425 var include = function(fileName) { 426 if (rio.boot.loadedStylesheets.include(fileName)) { return; } 427 rio.boot.loadedStylesheets.push(fileName); 428 if (rio.preloadedStylesheets.include(fileName)) { return; } 429 430 if (rio.environment.autoConcatCss && !(rio.app && rio.app.launched())) { 431 // Do nothing 432 } else { 433 var linkHtml = rio.Tag.link("", { 434 href: rio.url("/stylesheets/" + fileName + ".css"), 435 media: "screen", 436 rel: "stylesheet", 437 type: "text/css" 438 }); 439 Element.head().insert({ bottom: linkHtml }); 440 } 441 }.bind(this); 442 if (Object.isString(toInclude)) { 443 include(toInclude); 444 } 445 if (Object.isArray(toInclude)) { 446 toInclude.reverse().each(function(css) { 447 include(css); 448 }); 449 } 450 }, 451 452 /** @private */ 453 getToken: function() { 454 return this._token || rio.environment.railsToken; 455 }, 456 457 /** @private */ 458 setToken: function(token) { 459 this._token = token; 460 }, 461 462 /** @private */ 463 _afterLaunchFunctions: [], 464 465 /** @private */ 466 afterLaunch: function(afterLoadFunction) { 467 if (rio.app && rio.app.launched()) { 468 afterLoadFunction(rio.app); 469 } else { 470 this._afterLaunchFunctions.push(afterLoadFunction); 471 } 472 }, 473 474 /** 475 This causes the application to fail and log a 'fail' error message. If the application class 476 has a fail method, that method will be called with the message passed in here. 477 478 @param {String} msg The application failure message 479 @param {String} msg A more in depth description of the application failure 480 */ 481 fail: function(msg, description) { 482 try { 483 if (rio.app && rio.app.fail) { 484 rio.app.fail(msg); 485 rio.error("FAIL: " + msg, description || "", true); 486 } else { 487 alert("OOPS: " + msg + "\n\nTry refreshing the page or come back later."); 488 } 489 } catch(e) { 490 // Ignore errors during fail 491 } 492 }, 493 494 toString: function() { 495 return "Application"; 496 } 497 }; 498 499 if (!window.dhtmlHistoryCreated) { 500 window.dhtmlHistory.create({ 501 toJSON: function(o) { 502 return Object.toJSON(o); 503 }, 504 fromJSON: function(s) { 505 return s.evalJSON(); 506 } 507 }); 508 window.dhtmlHistoryCreated = true; 509 } 510 511