1 /**
  2  * @author Gillis Haasnoot <gillis.haasnoot@gmail.com>
  3  * @package Banana
  4  * @summary Base Control component
  5  */
  6 
  7 
  8 /** @namespace Banana.Control */
  9 goog.provide('Banana.Control');
 10 
 11 goog.require('Banana.Util.Base');
 12 
 13 
 14 /**
 15  * @ignore
 16 @namespace Banana.Controls
 17 */
 18 
 19 /**
 20 @class Class
 21 @name Class
 22 */
 23 
 24 /**
 25 @class Banana
 26 @name Banana
 27 */
 28 
 29 /**
 30 @class
 31 @name Banana.Controls
 32 */
 33 
 34 /** @namespace Banana.Control */
 35 namespace('Banana').Control = Class.extend(
 36 /** @lends Banana.Control.prototype */
 37 {
 38 	/**
 39 	 *  Base control class for component controls in Banana. Provides needed methods to function well in the Banana Hierarchy.
 40 	 *  This control is suitable to use in your render hierarchy. Note that this control won't be
 41 	 *  rendered. Possible child controls can have visually different parent controls 
 42 	 *  than the internal control collection would suggest.
 43 	 *    
 44 	 *  @constructs
 45 	 *  @extends Class
 46 	 */
 47 	init : function()
 48 	{
 49 		//generate a unique id (note a internal id representation)
 50 		this.generateUniqueId();
 51 		this.customId = false; // flag if page sets id
 52 		this.binds = [];
 53 		this.controls = [];
 54 		this.debug = false;
 55 	},
 56 
 57 	/**
 58 	 * This method is called by the page during creation of the control.
 59 	 * This is the place to define your control hierarchy. 
 60 	 * Unlike the constructor (init) you have also access to properties 
 61 	 *  
 62 	 * 1. this.page - Reference to the page control
 63 	 * 2. this.parent - Reference to the parent control
 64 	 * 3. this.id - internal id
 65 	 * 4. this.clientId - client id of the dom node
 66 	 * 
 67 	 * Except for these properties there is no difference. It doesn't really matters 
 68 	 * if you create child controls in the init or in the createComponents methods.
 69 	 * Only if you need access to the parent or page control. 
 70 	 * Or when you need to know what the id or client is. 
 71 	 * You will find out that in most cases createComponents is the right place to do things.
 72 	 * Note: that createComponents is called only once. Rerendering doesn't result into a call
 73 	 * to createComponents 
 74 	 * Another important note is that both init and createComponents methods are executed before 
 75 	 * even one thing is rendered on your screen.The clientId is already known, 
 76 	 * but you cannot access the dom node. To get access to the dom you need the updateDisplay() method.
 77 	 */
 78 	createComponents : function(){},
 79 
 80 	/**
 81 	 * The method is called by the page after the dom element is rendered.
 82 	 * If you want to add controls to the collection don't forget to invalidate by calling
 83 	 * invalidateDisplay()
 84 	 */
 85 	updateDisplay : function(){},
 86 
 87 	/**
 88 	 * called after invalidating control with invalidateDisplay method
 89 	 */
 90 	onPreInvalidateContents : function(){},
 91 
 92 	/**
 93 	 * called just before destroying object
 94 	 * Use this method to manualy destroy other objects i.e to prevent memory leaks
 95 	 */
 96 	unload : function(){},
 97 
 98 	/**
 99 	 * called on every display size change
100 	 */
101 	onWindowResize : function(){}
102 
103 });
104 
105 /**
106  * @param {Function} fn function to apply to all children in control collection
107  */
108 Banana.Control.prototype.applyToChildren = function(fn)
109 {
110 	var args = this.applyToChildren.arguments;
111 	args.shift();
112 	var i;
113 	for (i = 0, len = this.controls.length; i < len; i++)
114 	{
115 		fn.apply(this,args);
116 	}
117 };
118 /**
119  * sets internal id
120  * @param {String} id of control
121  * @return {this}
122  */
123 Banana.Control.prototype.setId = function(id)
124 {
125 	this.id = id;
126 	this.customId = true;
127 	return this;
128 };
129 
130 /**
131  * @return {String} id of control
132  */
133 Banana.Control.prototype.getId = function()
134 {
135 	return this.id;
136 };
137 
138 /**
139  * sets client id used for reference to dom node
140  * @param string
141  */
142 Banana.Control.prototype.setClientId = function(cid)
143 {
144 	this.clientId = cid;
145 };
146 
147 /**
148  * @return {String}
149  */
150 Banana.Control.prototype.getClientId = function()
151 {
152 	return this.clientId;
153 };
154 
155 /**
156  * generated unique id used for dom elements
157  */
158 Banana.Control.prototype.generateUniqueId = function()
159 {
160 	this.id = Banana.Util.generateUniqueId();
161 };
162 
163 /**
164  * forces control to be rendered. Only this function to bypass the page logic
165  * i.e you want to render a control which is not part of a control hierarchy
166  *
167  * @param {Banana.Control} control
168  */
169 Banana.Control.prototype.render = function(control)
170 {
171 	this.getPage().initRender(control,this,null,null,false);
172 };
173 
174 /**
175  * sets reference to the page control
176  * @param {Banana.Page} page
177  */
178 Banana.Control.prototype.setPage = function(page)
179 {
180 	this.page = page;
181 };
182 
183 /**
184  * @return {Banana.Page} 
185  */
186 Banana.Control.prototype.getPage = function()
187 {
188 	return this.page;
189 };
190 
191 /**
192  * This method is most useful for attaching event handlers to an element
193  * where the context is pointing back to a different object.
194  * @return {Function} fn
195  */
196 Banana.Control.prototype.getProxy = function(fn)
197 {
198 	return jQuery.proxy(fn,this);
199 };
200 
201 /**
202  * Sets reference to parent control. This method is automatically invoked during page construction
203  * @param {Banana.Control} parent 
204  */
205 Banana.Control.prototype.setParent = function(parent)
206 {
207 	this.parent = parent;
208 };
209 
210 /**
211  * @return {Banana.Control} parent control
212  */
213 Banana.Control.prototype.getParent = function()
214 {
215 	return this.parent;
216 };
217 
218 Banana.Control.prototype.getEnabled = function(){};
219 
220 /**
221  * Adds a plaintext or a control to the control hierargy.
222  * Adding control after render requires call to invalidateDisplay in order to get
223  * the new control rendered visible
224  * @param {mixed} String or Banana.Control
225  */
226 Banana.Control.prototype.addControl = function(c)
227 {
228 	this.controls.push(c);
229 	return this;
230 };
231 
232 /**
233  * @return {Array} of collection of mixed types
234  */
235 Banana.Control.prototype.getControls = function()
236 {
237 	return this.controls;
238 };
239 
240 /**
241  * Recursively finds control 
242  *
243  * @param {String} id of the control
244  * @return Banana.Control
245  */
246 Banana.Control.prototype.findControl = function(id)
247 {
248 	if (id === this.getId())
249 	{
250 		return this;
251 	}
252 	else
253 	{
254 		var childs = this.getControls();
255 
256 		if (!childs) { return null;}
257 		
258 		var i,len;
259 		for (i = 0, len = childs.length; i < len; i++)
260 		{
261 			if (childs[i] instanceof Banana.Control)
262 			{
263 				var foundcontrol = childs[i].findControl(id);
264 
265 				if (foundcontrol)
266 				{
267 					return foundcontrol;
268 				}
269 			}
270 		}
271 	}
272 	return null;
273 };
274 
275 /**
276  * If control is part of a collection belonging to a page it has a page defined.
277  * we let the page handle the removement of the control.
278  * If it is a standalone control, we just unregister the events
279  * TODO: should maybe also remove dom in the situation where control is not part of a collection
280  */
281 Banana.Control.prototype.remove = function()
282 {
283 	if (this.getPage() instanceof Banana.UiControl)
284 	{
285 		this.getPage().removeControl(this);
286 	}
287 	else
288 	{
289 		this.unregisterEvents();
290 	}
291 };
292 
293 /**
294  * removes all child controls in this control
295  */
296 Banana.Control.prototype.clear = function()
297 {
298 	if (this.getPage() instanceof Banana.UiControl)
299 	{
300 		this.getPage().clearControl(this);
301 	}
302 	this.controls = [];
303 };
304 
305 /**
306  * invalidates display. triggers the page to re-render node tree beginning from this control.
307  */
308 Banana.Control.prototype.invalidateDisplay = function()
309 {
310 	var page = this.getPage();
311 	if (page && page.isRendered)
312 	{
313 		page.rerender(this.getFirstUiControl());
314 	}
315 };
316 
317 /**
318  * Triggers an event so all listeners are getting notified.
319  *
320  * To start listening use the bind method. example:
321  * 
322  * someControl.bind('test',function(){});
323  *  
324  * To Trigger an event. example
325  *  
326  * someControl.triggerEvent('test',{data:'foo'});
327  *
328  * @param {String} name of the event
329  * @mixed {mixed} params of any type
330  */
331 Banana.Control.prototype.triggerEvent = function(name,params)
332 {
333 	jQuery(this).trigger(name,params);
334 };
335 
336 /**
337  * @return {Array} of dom event types compatible with jquery
338  */
339 Banana.Control.prototype.getDomEventTypes = function()
340 {
341 	return ['mousedown',
342 	        'mousemove',
343 	        'resize',
344 			'mouseup',
345 			'ready',
346 			'click',
347 			'dblclick',
348 			'error',
349 			'ready',
350 			'select',
351 			'submit',
352 			'focusin',
353 			'focusout',
354 			'focus',
355 			'change',
356 			'mouseover',
357 			'mouseleave',
358 			'mouseenter',
359 			'mouseout',
360 			'keypress',
361 			'keyup',
362 			'keydown'
363 	        ];
364 	
365 };
366 
367 /**
368  * Binds an eventname to a method
369  * There are 2 types of events:
370  * Dom events: Events mostly fired from user actions like mouse click, keyup etc.
371  * Custom events: These events are manually triggered by Banana components. 
372  * 
373  * Important is that dom events are available after the control is rendered. 
374  * Custom events are registered instantly and available at all time.
375  * 
376  * @param {String} name of the event
377  * @param {Function} func callback function
378  * @param {mixed} data userdata
379  * 
380  * @return {this};
381  */
382 Banana.Control.prototype.bind = function(name, func, data)
383 {
384 	if (!this.binds)
385 	{
386 		this.binds = [];
387 	}
388 
389 	//dont bind duplicate name and functions
390 	if (this.hasBind(name,func))
391 	{
392 		return false;
393 	}
394 
395 	if (this.getDomEventTypes().indexOf(name) !== -1)
396 	{
397 		//collect the dom events. which can only be registered after rendering
398 		this.binds.push({'name':name,'func':func,'data':data,'type':Banana.Controls.EventTypes.DOM_EVENT});
399 
400 		this.debugEvent(Banana.Controls.EventTypes.DOM_EVENT, name, "Bind normal");
401 		
402 		if (this.isRendered)
403 		{
404 			jQuery('#'+this.getClientId()).bind(name,data,func);
405 		}
406 	}
407 	//custom events can be binded directly. no need to put these in an array for later bind
408 	//since we also call unbind on all objects, 
409 	else
410 	{
411 		this.debugEvent(Banana.Controls.EventTypes.CUSTOM_EVENT, name, "Bind directly");
412 		
413 		this.binds.push({'name':name,'func':func,'data':data,'type':Banana.Controls.EventTypes.CUSTOM_EVENT});
414 		jQuery(this).bind(name, data, func);
415 	}
416 
417 	return this;
418 };
419 
420 /**
421  * Check wether a bind to a specific function in a control exists
422  *
423  * @param {String} name of event
424  * @param {Function} func
425  * @return {boolean} 
426  */
427 Banana.Control.prototype.hasBind = function(name,func)
428 {
429 	if (!this.binds)
430 	{
431 		this.binds = [];
432 	}
433 
434 	var i,len;
435 	for (i = 0, len = this.binds.length; i < len; i++)
436 	{
437 		var b = this.binds[i];
438 
439 		if (func)
440 		{
441 			//dont bind duplicate name and functions
442 			if (b.name === name && (func.guid === b.func.guid || b.func === func))
443 			{
444 				return true;
445 			}
446 		}
447 		else
448 		{
449 			if (b.name === name)
450 			{
451 				return true;
452 			}
453 		}
454 	}
455 	return false;
456 };
457 
458 /**
459  * Unbinds an eventname
460  * If funcion is given we unbind only when the bind matches the given function.
461  * Otherwise we unbind all binds with the specified name regardless how many.
462  * 
463  * @param {String} name of event
464  * @param {Function} func optionaly 
465  */
466 Banana.Control.prototype.unbind = function(name,func)
467 {
468 	var i,len;
469 	for (i = 0, len = this.binds.length; i < len; i++)
470 	{
471 		var b = this.binds[i];
472 		
473 		this.debugEvent(3, name, "Unbind");
474 		
475 		if (!func)
476 		{
477 			if (this.getDomEventTypes().indexOf(name) !== -1)
478 			{
479 				jQuery('#'+this.getClientId()).unbind(name);
480 			}
481 			else
482 			{
483 				jQuery(this).unbind(name);
484 			}
485 
486 			this.binds.splice(i,1);
487 		}
488 		else if (b.name === name && (func.guid === b.func.guid || b.func === func))
489 		{
490 			if (this.getDomEventTypes().indexOf(name) !== -1)
491 			{
492 				jQuery('#'+this.getClientId()).unbind(name, func);
493 			}
494 			else
495 			{
496 				jQuery(this).unbind(name, func);
497 			}
498 			
499 			jQuery(this).unbind(name, func);
500 			this.binds.splice(i,1);
501 		}
502 	}
503 };
504 
505 /**
506  * register all binded events
507  * used by framework.
508  * @ignore
509  */
510 Banana.Control.prototype.registerEvents = function()
511 {
512 	if (!this.binds)
513 	{
514 		return;
515 	}
516 
517 	var i,len;
518 	for (i = 0, len = this.binds.length; i < len; i++)
519 	{
520 		var name = this.binds[i].name;
521 		var func = this.binds[i].func;
522 		var data = this.binds[i].data;
523 		var type = this.binds[i].type;
524 		
525 		//there is a difference between dom and data events. dom events can be registered only
526 		//when the dom elements are rendered. data events can be registered right when object are instantiated
527 		//if we bind an custom event during construction (before dom render, no rerender has occured) we want it instant to be registered
528 		//because this function is called after rendering and rerendering we only need to bind the custom events
529 		//after a RE-rerender, cause rerendering always starts with unbinding ALL events.
530 		if (type === Banana.Controls.EventTypes.CUSTOM_EVENT && this.getPage().isRerendering)
531 		{		
532 			this.debugEvent(type, name, "Register event");
533 			
534 			if (data)
535 			{
536 				jQuery(this).bind(name,data,func);
537 			}
538 			else
539 			{
540 				jQuery(this).bind(name,func);
541 			}
542 		}
543 		else
544 		{
545 			this.debugEvent(type, name, "Ignore register event");
546 			//no need to bind this event,
547 		}
548 	}
549 };
550 
551 /**
552  * Unregisters all binded events.
553  * note we dont touch the bind array, cause we might rebind it at a later moment. 
554  * @ignore
555  */
556 Banana.Control.prototype.unregisterEvents = function()
557 {
558 	if (!this.binds)
559 	{
560 		return;
561 	}
562 	
563 	this.debugEvent(3, "", "Unregister all events CONTROL");
564 
565 	jQuery(this).unbind(); //and all custom events
566 };
567 
568 /**
569  * returns html string of all child controls
570  * Note that Banana.Control type classes on it self won't get rendered.
571  * 
572  * note Banana.control itself wont be rendered, but it chould contain child controls
573  * which are from Banana.UiControl type
574  *
575  * @param {boolean} markAsRendered 
576  * @return {String}
577  */
578 Banana.Control.prototype.getHtml = function(markAsRendered)
579 {
580 	var html = "";
581 
582 	var childs = this.getControls();
583 
584 	var i,len;
585 	for (i =0, len = childs.length; i < len; i++)
586 	{
587 		if (childs[i] instanceof Banana.Control)
588 		{
589 			html += childs[i].getHtml(markAsRendered);
590 		}
591 
592 		else if (typeof(childs[i]) === 'string')
593 		{
594 			html += childs[i];
595 		}
596 	}
597 
598 	return html;
599 };
600 
601 Banana.Control.prototype.debugEvent = function(type,eventname,message)
602 {
603 	if (!this.debug) return;
604 	
605 	if (this.id !== 'debugme')
606 	{
607 		return;
608 	}
609 	if (type === Banana.Controls.EventTypes.DOM_EVENT)
610 	{
611 		console.log(message,'dom',eventname,this.id);
612 	}
613 	else if (type === Banana.Controls.EventTypes.CUSTOM_EVENT)
614 	{
615 		console.log(message,'customevent',eventname,this.id);
616 	}
617 	else if (type === 3)
618 	{
619 		console.log(message,'all events',eventname,this.id);
620 	}
621 };
622 
623 /**
624  * fetches the first parent control which is an instance of Banana.UiControl
625  * @return {Banana.Control}
626  */
627 Banana.Control.prototype.getFirstUiControl = function()
628 {
629 	if (this instanceof(Banana.UiControl) && !this.markAsNonRender)
630 	{
631 		return this;
632 	}
633 	else
634 	{
635 		if (this.parent)
636 		{
637 			return this.parent.getFirstUiControl();
638 		}
639 	}
640 
641 	return null;
642 };