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