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