1 /**
  2  * @author Gillis Haasnoot <gillis.haasnoot@gmail.com>
  3  * @package Banana
  4  * @summary Page 
  5  */
  6 
  7 goog.provide('Banana.Page');
  8 
  9 goog.require('Banana.Controls.Panel');
 10 
 11 /** @namespace Banana.Page */
 12 namespace('Banana').Page = Banana.Controls.Panel.extend(
 13 /** @lends Banana.Page.prototype */
 14 {
 15 	/**
 16 	 * Creates a page component.
 17 	 *
 18 	 * Page is responsible for
 19 	 *
 20 	 *  rendering its control collection,
 21 	 *  register events on controls, 
 22 	 *  unregister events on controls, 
 23 	 *  apply databinding on controls, 
 24 	 *  removing controls from data/dom, 
 25 	 *
 26 	 * page should not be manually instantiated. Application is responsible for this.
 27 	 *
 28 	 * example of a page: 
 29 	 
 30 	   var mypage = Banana.Page.extend({});
 31 	 *
 32 	 * point your url to §ion=mypage
 33 	 * @constructs
 34 	 * @extends Banana.Controls.Panel
 35 	 */
 36 	init : function()
 37 	{
 38 		this.triggerEvent('onInit');
 39 		
 40 		this.addCssClass('BPage');
 41 	
 42 		this._super();
 43 
 44 		this.uniqueId = 0;
 45 		this.validators = [];
 46 		this.dataSets = [];
 47 
 48 		if (!this.resizefunc)
 49 		{
 50 			this.resizefunc = this.getProxy(function()
 51 			{
 52 				this.onWindowResize(this);
 53 			});
 54 		}
 55 		jQuery(window).bind('resize',this.resizefunc);
 56 	}
 57 	
 58 });
 59 
 60 /**
 61  * reference to application Default = Banana.Application
 62  * @param {Banana.Application} app
 63  */
 64 Banana.Page.prototype.setApplication = function(app)
 65 {
 66 	this.application = app;
 67 };
 68 
 69 /**
 70  * @return {Banana.Application}
 71  */
 72 Banana.Page.prototype.getApplication = function()
 73 {
 74 	return this.application;
 75 };
 76 
 77 
 78 /**
 79  * Starting point to initialize new page render.
 80  * The application is responsible for calling this method
 81  *
 82  * @param {mixed} target could be a string or object. in case of string we assume we have a dom id.
 83  */
 84 Banana.Page.prototype.run = function(target)
 85 {
 86 	this.validationControls = {};
 87 	
 88 	this.initRender(this, target || this.getApplication().settings.renderTarget);
 89 };
 90 
 91 
 92 /**
 93  * initialize rendering process
 94  *
 95  * @param {Banana.Control} control
 96  * @param {Banana.UiControl} target
 97  * @param {Banana.UiControl} place optional replace control
 98  * @param {boolean} wasRendering true if this control is already in a rerender phase
 99  * @param {boolean} parentRerendering true when parent control is currently rerendering
100  */
101 Banana.Page.prototype.initRender = function(control,target,place,wasRerendering,parentRendering)
102 { 
103 	this.rendering = true;
104 	
105 	if (wasRerendering)
106 	{
107 		//if the page was already rerendering we dont need to render this action again
108 		//we still need to rerender and register events on the control.
109 		this.renderControl(control,target,place);
110 		this.rendering = false;
111 		this.recursiveRegisterEvents(control);
112 		return; 
113 	}
114 	
115 	this.isRendered = false;
116 
117 	this.initializeControl(control,target);
118 
119 	this.renderControl(control,target,place);
120 
121 	this.isRendered = true;
122 	this.setVisible(true);
123 
124 	//some controls are not Banana.Controls but plain text
125 	if (control && control.triggerEvent)
126 	{
127 		control.triggerEvent('renderComplete',this);
128 	}
129 	
130 	
131 	//if parent is already rendering we dont have to register events
132 	//parent control will do that for us
133 	if (!parentRendering)
134 	{
135 		this.recursiveRegisterEvents(control);
136 		
137 		//only set this boolean to false when parent is not rerendering. then the parent will eventualy make sure that
138 		//this bool will be false
139 		this.rendering = false;
140 		
141 		this.parentFirstTimeRendering = false;
142 	}
143 	
144 	this.recursiveUpdateDisplay(control);
145 };
146 
147 /**
148  * rerenders control
149  *
150  * @param {Banana.Control} control
151  */
152 Banana.Page.prototype.rerender = function(control)
153 {
154 	///this bool is needed in the following situation:
155 	// call method updatedisplay on child controls from current method updatedisplay 
156 	// creates a situation that the the isRerendering bool is set to false in the first updatedisplay
157 	// updatedisplay A -> isRerendering = true 
158 	// updateDisplay B -> isRerendering = true
159 	// updateDisplay B is finished we set isRerendering to false
160 	// updateDisplay A is not finished yet and still needs to register events. but isRerendering is false
161 	// to prevent this situation we added this bool
162 	var parentRerendering = this.isRerendering;
163 	var parentRendering = this.rendering;
164 	 
165 	//exception case
166 	if (parentRendering && !parentRerendering)
167 	{
168 		this.parentFirstTimeRendering = true;
169 	}
170 	
171 	this.isRerendering = true;
172 	
173 	if (!parentRendering)
174 	{
175 		this.recursiveUnRegisterEvents(control);
176 	}
177 	
178 	var orig = control.getFirstUiControl();
179 	var parentControl = orig.getParent();
180 
181 	this.initRender(control, parentControl, orig,parentRerendering,parentRendering);
182 	
183 	// Restore this.isRerendering
184 	this.isRerendering = parentRerendering;
185 };
186 
187 /**
188  * sets content placeholder page
189  *
190  * @param {Banana.PageTemplate} ph
191  */
192 Banana.Page.prototype.setContentPlaceHolder = function(ph)
193 {
194 	this.contentPlaceHolder = ph;
195 };
196 
197 /**
198  * gets content placeholder
199  *
200  * @return  {Banana.PageTemplate}
201  */
202 Banana.Page.prototype.getContentPlaceHolder = function()
203 {
204 	return this.contentPlaceHolder;
205 };
206 
207 /**
208  * sets content placeholder page
209  *
210  * @param {Banana.PageTemplate} ph
211  */
212 Banana.Page.prototype.setPageTemplate = function(ph)
213 {
214 	this.contentPlaceHolder = ph;
215 };
216 
217 /**
218  * gets content placeholder
219  *
220  * @return  {Banana.PageTemplate}
221  */
222 Banana.Page.prototype.getPageTemplate = function()
223 {
224 	return this.contentPlaceHolder;
225 };
226 
227 
228 /**
229  * Hides loader
230  */
231 Banana.Page.prototype.hideLoader = function()
232 {
233 	if (!this.loader) {return;}
234 	this.loader.hide();
235 };
236 
237 /**
238  * Datasets are to centralize data storage of your controls
239  * You can either use stand alone datasets and manually assign them to your controls or 
240  * add datasets to the page and let the page automatically handle the data.
241  *
242  * @param {String} id for the dataset
243  * @param {Banana.Data.DataSet} the dataset
244  */
245 Banana.Page.prototype.addDataSet = function(id,d)
246 {
247 	if (this.dataSets[id])
248 	{
249 		log.warning('Dataset id "'+id+'" already exists');
250 		return;
251 	}
252 	d.id = id;
253 
254 	this.dataSets[id] = d;
255 };
256 
257 /**
258  * @return Banana.Data.DataSet
259  *
260  * @param string id of the dataset
261  */
262 Banana.Page.prototype.getDataSet = function(id)
263 {
264 	return this.dataSets[id];
265 };
266 
267 /**
268  *	removes all datasets from page
269  */
270 Banana.Page.prototype.removeDataSets = function()
271 {
272 	var index;
273 	for (index in this.dataSets)
274 	{
275 		if (typeof(this.dataSets[index]) === 'function') { continue;}
276 
277 		this.dataSets[index].clear();
278 	}
279 	
280 	this.dataSets = [];
281 };
282 
283 /**
284  * validates all controls 
285  *
286  */
287 Banana.Page.prototype.isValid = function()
288 {
289 	return Banana.Util.ValidationManager.validateAll();
290 };
291 
292 /**
293  * initialize controls
294  * this method walks through all controls and sets the following things
295  *
296  * clientId -> for all Banana.Controls
297  * parent -> parent control holding this control
298  * page -> reference to the page
299  *
300  * Page also automatically binds controls to their datasets
301  *
302  * @param {Banana.Control} control
303  * @param {Banana.Control} target control used to determine parent
304  */
305 Banana.Page.prototype.initializeControl = function(control,target)
306 {
307 	//only handle banana controls
308 	if (!(control instanceof Banana.Control))
309 	{
310 		return;
311 	}
312 
313 	//if the target is not a ui control we fetch the first anchester which is a ui control
314 	if (target instanceof Banana.Control)
315 	{
316 		if (!control.clientId)
317 		{
318 			control.setClientId(target.getClientId()+'-'+this.uniqueId++);
319 		}
320 	}
321 	//if the target is just a string then we assume it the first element
322 	else
323 	{
324 		var sn = this.getApplication().settings.applicationName;
325 		
326 		if (sn)
327 		{
328 			control.setClientId(sn+this.uniqueId++);
329 		}
330 		else
331 		{
332 			control.setClientId(this.uniqueId++);
333 		}
334 	}
335 
336 	control.setParent(target);
337 	control.setPage(this);
338 	
339 	if (!control.isInitialized)
340 	{
341 		/*
342 		@createComponents Method is applied on all controls during creation of the page. 
343 		In this state thi
344 		*/
345 		control.createComponents();
346 		control.isInitialized = true;
347 		
348 		//bs/bd[0] = dataset name
349 		//bs/bd[1] = dataset property name
350 		var bs = control.bindedDataSource;
351 		if (bs)
352 		{
353 			if (this.getDataSet(bs[0]))
354 			{
355 				this.getDataSet(bs[0]).bindControlToDataSource(control);
356 			}
357 		}
358 	
359 		var bd = control.bindedData;
360 		if (bd)
361 		{
362 			if (this.getDataSet(bd[0]))
363 			{
364 				this.getDataSet(bd[0]).bindControlToData(control);
365 			}
366 		}
367 	}
368 	
369 	// Because in the create components other controls can be added, we
370 	// call this function after the createComponents
371 	var childs = control.getControls(); 
372 
373 	var i, len;
374 	for (i = 0, len = childs.length; i < len; i++)
375 	{
376 		this.initializeControl(childs[i],control);
377 	}
378 };
379 
380 /**
381  * We render the control by fetching all html data from the control.
382  *
383  * @param {Banana.Control|String} control Control which should be rendered.
384  * @param {Banana.Control} target The target where the control should be rendered in.
385  * @param {Banana.Control} place (optional) If given we replace the old control html data with new data
386  */
387 Banana.Page.prototype.renderControl = function(control,target,place)
388 {
389 	if (control instanceof Banana.Control)
390 	{
391 		var data = control.getHtml(true); 
392 	}
393 	else
394 	{
395 		data = control;
396 	}
397 
398 	if (place)
399 	{
400 		Banana.Util.DomHelper.replace(data,place);
401 	}
402 	else
403 	{
404 		Banana.Util.DomHelper.render(data,target);
405 	}
406 };
407 
408 
409 /**
410  * Call updateDisplay on all controls in the hierarchy
411  *
412  * Controls can perform post-render actions in this function. These
413  * will also be called on rerender.
414  *
415  * As a side-effect, the isRendered-property of all controls will be set.
416  * This should happen in the {@link #renderControl}-function, but we don't
417  * have a recursive pass there.
418  *
419  * Events are not registered yet in updateDisplay!
420  *
421  * @param {Banana.Control|string} control Control which is rendered
422  */
423 Banana.Page.prototype.recursiveUpdateDisplay = function(control)
424 {
425 	if (control instanceof Banana.Control)
426 	{
427 		// TODO: a parent control asking a child control to be rendered returns false.
428 		// but in reality it is rendered. This flag should be moved to earlier phase
429 		control.isRendered = true;
430 
431 		control.updateDisplay();
432 
433 		var childs = control.getControls();
434 
435 		var i, len;
436 
437 		for (i = 0, len = childs.length; i < len; i++)
438 		{
439 			this.recursiveUpdateDisplay(childs[i]);
440 		}
441 	}
442 };
443 
444 
445 /**
446  * Register all binded events to control
447  *
448  * @param {Banana.Control|string} control
449  */
450 Banana.Page.prototype.recursiveRegisterEvents = function(control)
451 {
452 	if (control instanceof Banana.Control)
453 	{
454 		control.registerEvents();
455 		var childs = control.getControls();
456 
457 		var i, len;
458 		for (i = 0, len = childs.length; i < len; i++)
459 		{
460 			this.recursiveRegisterEvents(childs[i]);
461 		}
462 	}
463 };
464 
465 /**
466  * unregister events from control
467  *
468  * @param {Banana.Control} control
469  */
470 Banana.Page.prototype.recursiveUnRegisterEvents = function(control)
471 {
472 	if (control instanceof Banana.Control)
473 	{
474 		control.onPreInvalidateContents();
475 		control.unregisterEvents();
476 
477 		if (control instanceof Banana.Control)
478 		{
479 			var childs = control.getControls();
480 
481 			var i, len;
482 			for (i = 0, len = childs.length; i < len; i++)
483 			{
484 				this.recursiveUnRegisterEvents(childs[i]);
485 			}
486 
487 		}
488 	}
489 };
490 
491 /**
492  * completely removes a control from dom and data model
493  *
494  * @param Banana.Control
495  * @param bool dont remove dom when true. this is an optimalisation.
496  * from the controls which are getting removed, the root items (the one user want to remove) should
497  * be treated differently.
498  *
499  * <Root item>                               -  <ChildItems>
500  * parent control collection altered            parent control collection stays
501  * call dom remove                              dont call dom remove (done in root)
502  *
503  */
504 Banana.Page.prototype.removeControl = function(control,dontRemoveDom)
505 {
506 	if (!(control instanceof Banana.Control))
507 	{
508 		return;
509 	}
510 
511 	var childs =  control.controls.slice(); //slice to clone
512 
513 	var i;
514 	for (i = childs.length-1; i >= 0; i--)
515 	{
516 		this.removeControl(childs[i],true);
517 		childs.pop();
518 	}
519 
520 	control.unregisterEvents();
521 	
522 	control.unload();
523 
524 	if (!dontRemoveDom)
525 	{
526 		Banana.Util.DomHelper.remove(control);
527 	}
528 	
529 	//if our control got a parent, then we also need to remove it from the parent array controls
530 	//we only do this is we directly call remove on this control, otherwise we remove the controls array anywayy
531 	var parent = control.parent;
532 
533 	if (parent)
534 	{
535 		var indexInParent = parent.controls.indexOf(control);
536 
537 		parent.controls.splice(indexInParent,1);
538 	}
539 
540 	//delete all props to prevent memory leaks ---> Does it really helps????
541 //	var prop;
542 //	for (prop in control)
543 //	{
544 //		if (prop !== 'parent')
545 //		{
546 //			delete control[prop];
547 //		}
548 //	}
549 
550 	control = undefined;	
551 };
552 
553 
554 /**
555  * clears a control by removing all children
556  *
557  * @param {Banana.Control} control
558  */
559 Banana.Page.prototype.clearControl = function(control)
560 {
561 	var childs = control.getControls().slice();
562 
563 	var totalCleared = 0;
564 
565 	var i, len;
566 	for (i = 0, len = childs.length; i < len; i++)
567 	{
568 		totalCleared++;
569 		this.removeControl(childs[i],true);
570 	}
571 
572 	control.controls = null;
573 
574 	childs = null;
575 
576 	//TODO added this to also remove non object controls ie strings.
577 	//move this to domhelper.js
578 	jQuery('#'+control.getClientId()).empty();
579 };
580 
581 /**
582  * removes complete page plus all its controls/events/binds and dom data
583  * We traverse through child controls in a async operation. This is needed
584  * to prevent slow script running detects by various browsers
585  *
586  * @param {boolean} keepDom when true we dont remove the dom
587  */
588 Banana.Page.prototype.remove = function(keepPageDom,cb)
589 {
590 	//get flatten depth first collection of control hierargie 
591 	var flat = Banana.Util.flattenTreeDepthFirst(this,'controls');
592 	
593 	flat.pop(); //remove the last page which is {this} we handle {this} is the complete event
594 	
595 	Banana.Util.arrayInteratorAsync(flat,this.getProxy(function(control,i){
596 		
597 		if (!(control instanceof Banana.Control))
598 		{
599 			return;
600 		}
601 			
602 		control.unregisterEvents();
603 		control.unload();
604 						
605 		var parent = control.parent;
606 		
607 		if (parent && parent.controls)
608 		{
609 			parent.controls.splice(parent.controls.indexOf(control),1);
610 		}
611 		control = undefined;	
612 	}),
613 	0, //timeout for each chuck
614 	this.getProxy(function(){ //callback called after completion
615 		
616 		this.unload();
617 		
618 		//remove all validators
619 		Banana.Util.ValidationManager.removeValidators();
620 
621 		this.clearIds();
622 		
623 		//if we clear the page we also need to unregister the resize event. otherwise the callback will always
624 		//be called after screensize changes.
625 		jQuery(window).unbind();
626 
627 		this.unbind();
628 		this.removeDataSets();
629 		
630 		clearTimeout(this.timer);
631 
632 		if (!keepPageDom)
633 		{
634 			Banana.Util.DomHelper.remove(this);
635 		}
636 		
637 		jQuery('#debug').remove();
638 		clearTimeout(this.debugTimer);
639 		
640 		if (cb){
641 			cb();
642 		}	
643 	})
644 	,this.getProxy(function(){
645 		
646 		//not implemented here
647 	}));
648 };
649 
650 /**
651  * used internally -> should move to another location
652  * @ignore
653  */
654 Banana.Page.prototype.removeDom = function()
655 {
656 	jQuery('#cleared').remove();
657 };
658 
659 /**
660  * @ignore
661  */
662 Banana.Page.prototype.clearProps = function()
663 {
664 //	var prop;
665 //	for (prop in this)
666 //	{
667 //		//this[prop] = undefined;
668 //	}
669 };
670 
671 /**
672  *
673  * @param Banana.UiControl
674  */
675 Banana.Page.prototype.onWindowResize = function(control)
676 {
677 	if (control instanceof Banana.UiControl)
678 	{
679 		control.onWindowResize();
680 
681 		var childs = control.getControls();
682 
683 		var i, len;
684 		for (i = 0, len = childs.length; i < len; i++)
685 		{
686 			this.onWindowResize(childs[i]);
687 		}
688 	}
689 };
690 
691 /**
692  * Clears all ids in the page.
693  * Used by application during page transition.
694  */
695 Banana.Page.prototype.clearIds = function(control)
696 {
697 	if (!control)
698 	{
699 		control = this;
700 	}
701 	Banana.Util.DomHelper.clearIdsFrom(control.getClientId());
702 };