1 /*
  2 Copyright (c) <2011>, <Gillis Haasnoot, Maarten van Schaik>
  3 All rights reserved.
  4 
  5 Redistribution and use in source and binary forms, with or without
  6 modification, are permitted provided that the following conditions are met:
  7     * Redistributions of source code must retain the above copyright
  8       notice, this list of conditions and the following disclaimer.
  9     * Redistributions in binary form must reproduce the above copyright
 10       notice, this list of conditions and the following disclaimer in the
 11       documentation and/or other materials provided with the distribution.
 12     * Neither the name of the <organization> nor the
 13       names of its contributors may be used to endorse or promote products
 14       derived from this software without specific prior written permission.
 15 
 16 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 17 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 18 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 19 DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 20 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 21 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 22 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 23 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 24 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 25 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 26 */
 27 
 28 /**
 29  * @author Gillis Haasnoot <gillis.haasnoot@gmail.com>
 30  * @package Banana
 31  * @summary Main Application object
 32  */
 33 
 34 /**
 35  *  this is needed for internet explorer which doesnt have a native indexOf method
 36  */
 37 if (!Array.prototype.indexOf) 
 38 {
 39 	/**
 40 	 * @ignore
 41 	 */
 42 	Array.prototype.indexOf = function(obj, start) 
 43 	{
 44 		var i,j;
 45 	     for (i = (start || 0), j = this.length; i < j; i++) {
 46 	         if (this[i] === obj) { return i; }
 47 	     }
 48 	     return -1;
 49 	};
 50 }
 51 
 52 
 53 goog.provide('Banana');
 54 
 55 goog.require('Banana.Util.NameSpace');
 56 goog.require('Banana.Util.Base');
 57 goog.require('Banana.Control');
 58 
 59 goog.require('Banana.Util.ArrayBiCollection');
 60 goog.require('Banana.Util.Utils');
 61 goog.require('Banana.Util.ValidationManager');
 62 goog.require('Banana.Util.UrlManager');
 63 goog.require('Banana.Util.StateManager');
 64 goog.require('Banana.Util.DomHelper');
 65 goog.require('Banana.Util.DateTimecode');
 66 goog.require('Banana.Util.Color');
 67 goog.require('Banana.Util.LogManager');
 68 
 69 
 70 goog.require('Banana.Data.DataSet');
 71 
 72 goog.require('Banana.Page');
 73 goog.require('Banana.PageTemplate');
 74 
 75 goog.require('Banana.Controls.StatusBar');
 76 goog.require('Banana.Controls.DataControls.TextBox');
 77 goog.require('Banana.Controls.DataControls.Password');
 78 goog.require('Banana.Controls.DataControls.TextArea');
 79 goog.require('Banana.Controls.DataControls.CheckBox');
 80 goog.require('Banana.Controls.DataControls.Table');
 81 goog.require('Banana.Controls.DataControls.Label');
 82 goog.require('Banana.Controls.DataControls.LimitCharLabel');
 83 goog.require('Banana.Controls.DataControls.DatePicker');
 84 goog.require('Banana.Controls.DataControls.DateTimePicker');
 85 goog.require('Banana.Controls.DataControls.DateSpan');
 86 goog.require('Banana.Controls.DataControls.Image');
 87 goog.require('Banana.Controls.DataControls.MaskedTextBox');
 88 goog.require('Banana.Controls.DataControls.RadioButton');
 89 goog.require('Banana.Controls.DataControls.ProgressBar');
 90 goog.require('Banana.Controls.DataControls.Timecode');
 91 goog.require('Banana.Controls.DataControls.ListControls.RadioButtonList');
 92 goog.require('Banana.Controls.DataControls.ListControls.CheckBoxList');
 93 goog.require('Banana.Controls.DataControls.ListControls.DropDown');
 94 goog.require('Banana.Controls.DataControls.ListControls.MultiSelect');
 95 goog.require('Banana.Controls.DataControls.Slider');
 96 goog.require('Banana.Controls.DataControls.UnorderedList');
 97 
 98 goog.require('Banana.Controls.DataGrid');
 99 
100 goog.require('Banana.Controls.Window');
101 goog.require('Banana.Controls.Button');
102 goog.require('Banana.Controls.Link');
103 goog.require('Banana.Controls.LinkButton');
104 goog.require('Banana.Controls.Canvas');
105 goog.require('Banana.Controls.Modal');
106 goog.require('Banana.Controls.ConfirmModal');
107 goog.require('Banana.Controls.Fieldset');
108 goog.require('Banana.Controls.Legend');
109 goog.require('Banana.Controls.Title');
110 goog.require('Banana.Controls.Loader');
111 goog.require('Banana.Controls.Form');
112 goog.require('Banana.Controls.FileInput');
113 goog.require('Banana.Controls.TabSlider');
114 
115 goog.require('Banana.Controls.Decorators.Decorator');
116 goog.require('Banana.Controls.Decorators.TimecodeFieldValidator');
117 goog.require('Banana.Controls.Decorators.RequiredFieldValidator');
118 goog.require('Banana.Controls.Decorators.RegExValidator');
119 goog.require('Banana.Controls.Decorators.CharLimitFieldValidator');
120 goog.require('Banana.Controls.Decorators.LabelDecorator');
121 goog.require('Banana.Controls.Decorators.LabelDecoratorRight');
122 goog.require('Banana.Controls.Decorators.InfoDecorator');
123 goog.require('Banana.Controls.Decorators.EqualValidator');
124 
125 goog.require('Banana.Controls.HBox');
126 goog.require('Banana.Controls.VBox');
127 
128 
129 /**
130  * Eventtypes used in Banana
131  */
132 namespace('Banana.Controls').EventTypes = {
133 	'DOM_EVENT':1,
134 	'CUSTOM_EVENT':2
135 }
136 
137 
138 /**
139 @namespace Banana
140 */
141 namespace('Banana').Application = Banana.Control.extend(
142 /** @lends Banana.Application.prototype */
143 {
144 	/**
145 	 * Main Banana Application components.
146 	 * This object is responsible for loading and running pages.
147 	 * By default the application is accessible through Banana.Application.
148 	 * if you need multiple instances, an application name is required.
149 	 *
150 	 * typicaly an instance of banana is started with
151 		
152 		<div id="target"></div>
153 		
154 		<script>
155 		
156 			var applicationParameters = {};
157 			applicationParameters.renderTarget = 'target'; 
158 			applicationParameters.imagedir ='images';
159 			applicationParameters.defaultSection="Home";
160 			applicationParameters.pageTemplate="PageTemplate";
161 			applicationParameters.paths = {}
162 			applicationParameters.applicationName = "foo" //needed if you want to run multiple instances of banana
163 			applicationParameters.paths.pages = "Application.Pages";
164 			applicationParameters.paths.pageTemplates = "Application.Pages.PageTemplates";
165 			var app = new Banana.Application(applicationParameters);
166 			app.run();
167 		
168 		</script>
169 	
170 	 *
171 	 * It is also possible to launch multiple instances of banana. 
172 	 * Make sure the applicationName is unique
173 	 * 
174             var applicationParameters = {};
175             applicationParameters.renderTarget = 'target'; //id of the target div
176             applicationParameters.imagedir ='images';
177             applicationParameters.paths = {};
178             applicationParameters.applicationName = "myUniqueApp";
179             applicationParameters.paths.pages = "Application.Pages";
180             applicationParameters.paths.pageTemplates = "Application.Pages.PageTemplates";
181 
182 
183             var pageClass = Banana.Page.extend({
184 
185           		createComponents : function()
186           		{
187           			//do something here
188           		}
189             });
190 
191             var page = new pageClass();
192 
193             var app = new Banana.Application(applicationParameters);
194             app.run(page);
195        
196 	 * 
197 	 * 
198 	 * Following parameters are available for creating a new application
199 	 * 
200 	 * - renderTarget 			Id of the dom element where the application is rendered in.
201 	 * - imagedir				relative path of various images Banana uses
202 	 * - paths.pages      		namespace of Banana Pages (by default "Application.Pages") 
203 	 * - paths.pageTemplates	namespace of Banana Template Pages (by default "Application.Pages.PageTemplates") 		   
204 	 * 
205 	 * A new page can be loaded by calling loadPage(String page). The current page will be removed
206 	 * and replaced by the new page. Url history params are restored in case an already visited page is loaded.
207 	 * 
208 	 * 
209 	 * @constructs
210 	 * @param {Object} settings
211 	 */
212 	init : function(settings)
213 	{
214 		// Add the logger
215 		log = new Banana.Util.LogManager();
216 		log.addLogger(new Banana.Util.Logger.Console());
217 		log.setLevel("error", true);
218 		log.setLevel("warning", true);
219 				
220 		try
221 		{
222 			jQuery();
223 		}
224 		catch(e)
225 		{
226 			log.error("JQuery is not found. JQuery required.");
227 		}
228 
229 		if (!settings)
230 		{
231 			settings = {};
232 		}
233 		
234 		this.settings = settings;
235 		
236 		//to remember the vertical scroll position of a page
237 		this.sectionVerticalScrollPosition ={};
238 		this.id = 0; // Will increment on each page load
239 		
240 		if (settings.applicationName)
241 		{
242 			namespace('Banana')[settings.applicationName] = this;
243 		}
244 		else
245 		{
246 			namespace('Banana').Application = this;
247 		}
248 		
249 		if (!settings.pageTemplate)
250 		{
251 			settings.pageTemplate = "PageTemplate";
252 		}
253 		
254 		if (!settings.paths)
255 		{
256 			settings.paths = {};
257 		}
258 		
259 		if (!settings.paths.pages)
260 		{
261 			settings.paths.pages = "Application.Pages";
262 		}
263 			
264 		this.prepareUnload();
265 	},
266 	
267 	/**
268 	 * When a user closes/refresh the browser tab/window, 
269 	 * the current page will be removed + all registered event
270 	 * @ignore
271 	 */
272 	prepareUnload : function()
273 	{
274 		jQuery(window).unload( this.getProxy(function()
275 		{
276 			jQuery('*').unbind();
277 			if (this.activePage)
278 			{
279 				this.activePage.remove();
280 			}
281 			if (this.pageTemplate)
282 			{
283 				this.pageTemplate.remove();
284 			}
285 			delete Banana;
286 		}) );		
287 	},
288 	
289 	/**
290 	 * Automatically called by Banana
291 	 * The page template is specified in application settings with the "pageTemplate" property. 
292 	 * If no Page template is given, we use default Banana.PageTemplate
293 	 * @ignore
294 	 */
295 	loadPageTemplate : function()
296 	{
297 		if (!this.settings.paths.pageTemplates)
298 		{
299 			pageTemplateClass = Banana.PageTemplate;
300 		}
301 		else
302 		{
303 			var path = this.settings.paths.pageTemplates;
304 
305 			var objectname = path+'.'+this.settings.pageTemplate;
306 	
307 			var pageTemplateClass = Banana.Util.NamespaceToFunction(objectname);
308 		}
309 
310 		if (!pageTemplateClass)
311 		{
312 			log.error("Error: could not find Template: "+objectname);
313 
314 			return;
315 		}
316 
317 		try
318 		{
319 			this.pageTemplate = new pageTemplateClass();
320 		}
321 		catch(e)
322 		{
323 			log.error("Error: could not instantiate template: "+objectname);
324 		}
325 
326 	},
327 
328 	/**
329 	 * @returns {Array} of settings
330 	 */
331 	getSettings : function()
332 	{
333 		return this.settings;
334 	},
335 
336 	/**
337 	 * @param {String} section
338 	 */
339 	setSectionVerticalScrollPosition : function(section)
340 	{
341 		this.sectionVerticalScrollPosition[section] = window.scrollY;  
342 	},
343 	
344 	/**
345 	 * 
346 	 * @param {String} section
347 	 * @returns {String}
348 	 */
349 	getSectionVerticalScrollPosition : function(section)
350 	{
351 		return this.sectionVerticalScrollPosition[section];
352 	},
353 	
354 	
355 	/**
356 	 * Loads a page.
357 	 *
358 	 * Loads a page by specifying the full namespace name of the page. 
359 	 * Additional parameters can be send along. Parameters should be in key => value format
360 	 * stored in an Object. All parameters will be written in the hash section of the url.
361 	 * Loading a page also results in saving the url parameters of the current page. This means
362 	 * that we when we have a url like http://mysite?page=foo#section=Home&id=12
363 	 * we save the id parameter only. Unless specified we restore the original parameters when the
364 	 * page is reloaded.
365 	 *
366 	 * @param {string} page to be loaded. This is the full namespace name
367 	 * @param {object} key value format 
368 	 * @param {bool} ignoreHistoryParams if true we ignore previous url params. 
369 	 */
370 	loadPage : function(page,urlParams,ignoreHistoryParams)
371 	{
372 		//we save the current url params to use them later on when we return to this page
373 		Banana.Util.UrlManager.saveModuleHistory('section');
374 
375 		//we also save the current vertical scroll position. so when we return we also return to scroll position
376 		this.setSectionVerticalScrollPosition(this.section);
377 		
378 		//clean the url
379 		Banana.Util.UrlManager.clearUrl();
380 
381 		//Here we assign the history property to a global class property. 
382 		//We need to do this because below code which is registering url parameters in the urlmanager one by one.
383 	    //If every Url Manager action results into a url change, we would get loads of history points
384 		//in our browser. This is something we don't want. To prevent this situation we register them
385 		//but without modifing the url. Later in the Run() method we modify the url at once.
386 		this.loadingWithHistory = !ignoreHistoryParams;
387 		
388 		if (this.loadingWithHistory)
389 		{
390 			var history = Banana.Util.UrlManager.getModuleHistory(page);
391 
392 			var his;
393 			for (his in history)
394 			{
395 				if (history.hasOwnProperty(his)) 
396 				{
397 					Banana.Util.UrlManager.registerModule(his);
398 					Banana.Util.UrlManager.setModule(his,history[his], true);
399 				}
400 			}
401 		}
402 
403 		if (urlParams)
404 		{
405 			var prop;
406 			
407 			for (prop in urlParams)
408 			{
409 				if (urlParams.hasOwnProperty(prop)) 
410 				{
411 					Banana.Util.UrlManager.registerModule(prop);
412 					Banana.Util.UrlManager.setModule(prop,urlParams[prop], true);
413 				}
414 			}
415 		}
416 			
417 		this.run(page);
418 
419 
420         
421         //restore vertical scroll position
422         if (!ignoreHistoryParams)
423         {
424         	window.scrollTo(0,this.getSectionVerticalScrollPosition(page) || 0);
425         }        
426 	},
427 	
428 	/**
429 	 * This method is used when only one single page instance is attached to the application.
430 	 * 
431 	 * @ignore
432 	 */
433 	instantiateSinglePage : function()
434 	{
435 		if (!this.pageTemplate)
436 		{
437 			this.loadPageTemplate();
438 		}
439 				
440 		this.page.bind('renderComplete',this.getProxy(function(){
441 
442 			if (this.activePage)
443 			{
444 				this.activePage.removeDom();
445 				this.activePage.clearProps();
446 				this.activePage = undefined;
447 			}
448   
449 			this.activePage = this.page;
450 			this.activePage.setVisible(true);
451 		}));
452 
453 		//our new page will be not visible
454 		this.page.setVisible(false);
455 		
456 		//now we add it to the pagetemplate
457 		this.pageTemplate.addPageControl(this.page);
458 		
459 		this.pageTemplate.setApplication(this);
460 		
461 		//load the whole page
462 		this.pageTemplate.run(this);		
463 	},
464 	
465 	/**
466 	 * Run a page
467 	 *
468 	 * This function should only be initially called to bootstrap the
469 	 * framework. Consecutive pages should be loaded by using {@link #loadPage}.
470 	 *
471 	 * @param {string} page
472 	 */
473 	run : function(page)
474 	{	
475 		// Save the parameter
476 		if (page)
477 		{
478 			this.page = page;
479 		}
480 		
481 		if (page instanceof Banana.Page)
482 		{
483 			return this.instantiateSinglePage();
484 		}
485 		
486 		this.triggerEvent('onPreLoadPage');
487 	
488 		// If this is the first page we run, we register all the url parameters
489 		if (!this.activePage)
490 		{
491 			Banana.Util.UrlManager.autoRegisterModules();
492 		}
493 
494 		//if we have no pagetemplate loaded, usually the first time, we load it
495 		if (!this.pageTemplate)
496 		{
497 			this.loadPageTemplate();
498 		}
499 	
500 		// Determine page (section) to load
501 		var section = this.page || Banana.Util.UrlManager.getModule('section') || this.settings.defaultSection;
502 		this.section = section;
503 		
504 		// Unset saved parameter for next calls
505 		this.page = undefined;
506 
507 		//the register url section registers the section without modifing the url.
508 		//we receive a change event 
509 		this.registerUrlSection(section);
510 		//apply url change in the Url Manager
511 		if (!this.loadingWithHistory)
512 		{
513 			Banana.Util.UrlManager.updateUrl();
514 		}
515 		
516 		var load = this.getProxy(function(){
517 
518 			var objectname = this.settings.paths.pages+'.'+section;
519 			var pageClass = Banana.Util.NamespaceToFunction(objectname);
520 		
521 			//if the pageclass cannot be find use the default one
522 			if (!pageClass)
523 			{
524 				if (this.settings.defaultSection === section)
525 				{
526 					log.error("Error default page "+this.settings.defaultSection+" cannot be loaded");
527 					return;
528 				}
529 				else
530 				{
531 					log.error("Page "+section+" not found. Load default page "+this.settings.defaultSection);
532 				}
533 				return this.loadPage(this.settings.defaultSection);
534 			}
535 			
536 			//id of the application which thus increments every page load
537 			this.id++;
538 			
539 			var newPage = new pageClass();
540 
541 			//when a page is rendered we remove the old page.
542 			//We already removed its events and timers. now its time for the visible items as well
543 			//We also remove its properties. This is too prevent memory leaks
544 			newPage.bind('renderComplete',this.getProxy(function(){
545 
546 				if (this.activePage)
547 				{
548 					this.activePage.removeDom();
549 					this.activePage.clearProps();
550 					this.activePage = undefined;
551 				}
552 	  
553 				this.activePage = newPage;
554 				this.activePage.setVisible(true); 
555 
556 			}));
557 
558 			//our new page will be not visible yet
559 			newPage.setVisible(false);
560 			
561 			//now we add it to the pagetemplate
562 			this.pageTemplate.addPageControl(newPage);
563 
564 			this.pageTemplate.setApplication(this);
565 			
566 			//run the page. if everything is ok we will receive a renderComplete event
567 			this.pageTemplate.run();
568 			
569 			//update the url to its final state
570 			Banana.Util.UrlManager.updateUrl();
571 		});
572 						
573 		//if we have an active page we want it to be removed
574 		//but only its internal data. like (events, timers and more)
575 		//the dom needs to stay to prevent an ugly flickering between page transitions
576 		if (this.activePage)
577 		{
578 			//this is extremely important. Our remove function below is asynchronous
579 			// it might happen that the section change which is also asnchronous (statement above) results into 
580 			//a url change during removing of the controls. I have to admit that
581 			//Oh yeah is a hacky solution. and very difficult to debug or to even understand.
582 			Banana.Util.UrlManager.stopChecking(); 
583 			
584 			this.activePage.remove(true,this.getProxy(function(){
585 				Banana.Util.UrlManager.startChecking(); 
586 				load();
587 				Banana.Util.UrlManager.listenModule('section',this.getProxy(function()
588 				{
589 					this.loadPage(Banana.Util.UrlManager.getModule('section'));
590 				}));
591 				
592 			}));
593 		}
594 		else
595 		{
596 			load();
597 			Banana.Util.UrlManager.listenModule('section',this.getProxy(function()
598 			{
599 				this.loadPage(Banana.Util.UrlManager.getModule('section'));
600 			}));
601 			
602 		}
603 	},
604 	
605 	/**
606 	 * Registers section url and binds a change event to it 
607 	 * which determines which page should be loaded
608 	 * @param {String} section
609 	 * @ignore
610 	 */
611 	registerUrlSection : function(section)
612 	{
613 		Banana.Util.UrlManager.registerModule('section',section);
614 
615 		Banana.Util.UrlManager.setModule('section',section,true);
616 	}
617 });
618