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 };