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 /** 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 if (args.length > 0 && args.last() != undefined && !args.last().ATTR) { 335 var initializers = args.last(); 336 if (initializers.requireCss) { 337 rio.Application.includeCss(initializers.requireCss); 338 } 339 Object.keys(initializers.routes || {}).each(function(name) { 340 app.route(name, initializers.routes[name]); 341 }); 342 343 rio.Application.extend(app, initializers.methods || {}); 344 } 345 346 return app; 347 }, 348 349 /** @private */ 350 extend: function(app, extension) { 351 rio.Attr.extend(app, extension); 352 }, 353 354 /** 355 Alias of rio.Application.require 356 357 @param {String} fileName The path to the javascript file that will be loaded. 358 */ 359 include: function(fileName) { 360 this.require(fileName); 361 // rio.boot.loadFile(fileName); 362 }, 363 364 /** 365 Alias of rio.Application.require 366 367 @param {String} fileName The path to the javascript file that will be loaded. 368 */ 369 require: function(fileName) { 370 rio.require(fileName); 371 }, 372 373 /** @private */ 374 injectCss: function() { 375 var toLoad = []; 376 rio.boot.loadedStylesheets.each(function(s) { 377 if (!rio.preloadedStylesheets.include(s)) { toLoad.push(s); } 378 }); 379 if (toLoad.empty()) { return; } 380 381 var query = toLoad.map(function(f) { 382 return "files[]=" + f; 383 }).join("&"); 384 var linkHtml = rio.Tag.link("", { 385 href: rio.url("/rio/stylesheets?" + query + "&" + rio.cacheKey), 386 media: "screen", 387 rel: "stylesheet", 388 type: "text/css" 389 }); 390 Element.head().insert({ bottom: linkHtml }); 391 392 if (rio.ContainerLayout) { 393 rio.ContainerLayout.resize(); 394 } 395 }, 396 397 /* 398 Requires a css file 399 400 @param {String} toInclude The path to the stylesheet that will be loaded. 401 */ 402 includeCss: function(toInclude) { 403 // Because of a bug in IE, we need to remove and readd all link tags every time a new one is added. 404 var include = function(fileName) { 405 if (rio.boot.loadedStylesheets.include(fileName)) { return; } 406 rio.boot.loadedStylesheets.push(fileName); 407 if (rio.preloadedStylesheets.include(fileName)) { return; } 408 409 if (rio.environment.autoConcatCss && !(rio.app && rio.app.launched())) { 410 // Do nothing 411 } else { 412 var linkHtml = rio.Tag.link("", { 413 href: rio.url("/stylesheets/" + fileName + ".css"), 414 media: "screen", 415 rel: "stylesheet", 416 type: "text/css" 417 }); 418 Element.head().insert({ bottom: linkHtml }); 419 } 420 }.bind(this); 421 if (Object.isString(toInclude)) { 422 include(toInclude); 423 } 424 if (Object.isArray(toInclude)) { 425 toInclude.reverse().each(function(css) { 426 include(css); 427 }); 428 } 429 }, 430 431 /** @private */ 432 getToken: function() { 433 return this._token || rio.environment.railsToken; 434 }, 435 436 /** @private */ 437 setToken: function(token) { 438 this._token = token; 439 }, 440 441 /** @private */ 442 _afterLaunchFunctions: [], 443 444 /** @private */ 445 afterLaunch: function(afterLoadFunction) { 446 if (rio.app && rio.app.launched()) { 447 afterLoadFunction(rio.app); 448 } else { 449 this._afterLaunchFunctions.push(afterLoadFunction); 450 } 451 }, 452 453 /** 454 This causes the application to fail and log a 'fail' error message. If the application class 455 has a fail method, that method will be called with the message passed in here. 456 457 @param {String} msg The application failure message 458 @param {String} msg A more in depth description of the application failure 459 */ 460 fail: function(msg, description) { 461 try { 462 if (rio.app && rio.app.fail) { 463 rio.app.fail(msg); 464 rio.error("FAIL: " + msg, description || "", true); 465 } else { 466 alert("OOPS: " + msg + "\n\nTry refreshing the page or come back later."); 467 } 468 } catch(e) { 469 // Ignore errors during fail 470 } 471 }, 472 473 toString: function() { 474 return "Application"; 475 } 476 }; 477 478 if (!window.dhtmlHistoryCreated) { 479 window.dhtmlHistory.create({ 480 toJSON: function(o) { 481 return Object.toJSON(o); 482 }, 483 fromJSON: function(s) { 484 return s.evalJSON(); 485 } 486 }); 487 window.dhtmlHistoryCreated = true; 488 } 489 490