1 /** 2 * @author Gillis Haasnoot <gillis.haasnoot@gmail.com> 3 * @package Banana.Controls 4 * @summary DataGridDataTreeListRender 5 */ 6 7 goog.require('Banana.Controls.DataGridBaseListRender'); 8 9 goog.require('Banana.Controls.DataGridTableHeaderItemRender'); 10 goog.require('Banana.Controls.DataGridTableContentItemRender'); 11 goog.require('Banana.Controls.DataGridTableFooterItemRender'); 12 13 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridControlColumn'); 14 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridCharLimitColumn'); 15 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridImageButtonColumn'); 16 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridLinkColumn'); 17 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridImageColumn'); 18 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridStatusColumn'); 19 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridHeaderColumn'); 20 goog.require('Banana.Controls.DataControls.ListControls.DataGrid.ColumnControls.DataGridButtonColumn'); 21 22 goog.provide('Banana.Controls.DataGridTableListRender'); 23 24 /** @namespace Banana.Controls.DataGridTableListRender */ 25 namespace('Banana.Controls').DataGridTableListRender = Banana.Controls.DataGridBaseListRender.extend( 26 /** @lends Banana.Controls.DataGridTableListRender.prototype */ 27 { 28 /** 29 * Creates a datagrid table list render. This is the most standard grid. 30 * Columns can be inserted 31 * 32 * bindable events 33 * 34 * - onRowCreated 35 * - onInitColumn //triggered during creation of the column 36 * - datagridmouseover 37 * - dataSourceChanged 38 * - onItemSelect 39 * - itemRenderChanged 40 * 41 * Example: 42 * 43 * 44 //specify the datasource we put inside the grid 45 var datasource = [ 46 {'name':'a name','description':'a description'}, 47 {'name':'a name','description':'a description'}, 48 {'name':'a name','description':'a description'} 49 ]; 50 51 //specify the columns 52 var columns = [ 53 new Banana.Controls.DataGridColumn().setHeaderText('name').setDataField('name'), 54 new Banana.Controls.DataGridColumn().setHeaderText('description').setDataField('description') 55 ] 56 57 var datagrid = new Banana.Controls.DataGrid(); 58 datagrid.setDataSource(datasource); 59 60 //by default we have a Banana.Controls.DataGridTableListRender inside the datagrid. 61 var listRender = datagrid.getListRender(); 62 listRender.setColumns(columns); 63 64 this.addControl(datagrid); 65 * 66 * By default the table list render contains a headerItemRender, itemRender and footerItemRender 67 * 68 * @constructs 69 * @extends Banana.Controls.DataGridBaseListRender 70 */ 71 init : function() 72 { 73 this._super(); 74 75 this.addCssClass("BDataGridTableListRender") 76 77 this.columns = []; 78 this.indexRowMap = []; 79 this.dataItemRenderMap = new Banana.Util.ArrayBiCollection(); //mapping of item renders to data 80 81 this.defaultHeaderItemRender = Banana.Controls.DataGridTableHeaderItemRender; 82 this.defaultContentItemRender = Banana.Controls.DataGridTableContentItemRender; 83 this.defaultFooterItemRender = Banana.Controls.DataGridTableFooterItemRender; 84 85 this.headerItemRender = null; 86 this.contentItemRender = null; 87 this.headerItemRender = null; 88 }, 89 90 /** 91 * @param {mixed} data 92 * @param {Banana.Controls.DataGridTableItemRender} render 93 * 94 * @return {int} position where item is inserted 95 */ 96 addItem : function(data,render) 97 { 98 var il = this._super(data); 99 100 if (render) 101 { 102 this.indexItemRenderFactory[il] = render; 103 } 104 105 this.createRowByIndex(il,true); 106 this.createItemRenderByIndex(il,true); 107 108 return il; 109 }, 110 111 /** 112 * removes item from list. 113 * @param {mixed} data 114 */ 115 removeItem : function(data) 116 { 117 var index = this._super(data); 118 119 if (index >=0) 120 { 121 this.removeRowByIndex(index); 122 } 123 }, 124 125 /** 126 * clears complete list 127 */ 128 removeAllItems : function() 129 { 130 this._super(); 131 132 this.indexRowMap = []; 133 this.indexItemRenderFactory = []; 134 this.tableBody.clear(); 135 }, 136 137 /** 138 * @return Banana.Controls.DataGridTableContentItemRender 139 */ 140 getDefaultItemRender : function() 141 { 142 return this.defaultContentItemRender; 143 }, 144 145 /** 146 * @param {Banana.Controls.DataGridTableHeaderItemRender} r 147 */ 148 setHeaderItemRender : function(r) 149 { 150 this.headerItemRender = r; 151 }, 152 153 /** 154 * @return {Banana.Controls.DataGridTableHeaderItemRender} 155 */ 156 getHeaderItemRender : function(r) 157 { 158 return this.headerItemRender || this.defaultHeaderItemRender; 159 }, 160 161 /** 162 * sets item render on index 163 * 164 * @param {int} index 165 * @param {String} render 166 * @param {boolean} dontCreate when true we do NOT instantly create the itemrender. use this when list is in prerender phase 167 * @param {boolean} ignoreDataItemRenderMap 168 */ 169 setItemRenderByIndex : function(index,render,dontCreate,ignoreDataItemRenderMap) 170 { 171 this.indexItemRenderFactory[index] = render; 172 173 //also save the item render data map relation 174 //could be handy when we set an item render and later change the datasource 175 //the item location could be changed then. itemrender - index relation is 176 //in that situation not enough 177 if (!ignoreDataItemRenderMap && this.datasource[index]) 178 { 179 //TODO this is the only place a mapping between data and render is set. 180 //this should also be possible at a later moment. 181 if (this.datasource[index][this.indexKey]) 182 { 183 this.dataItemRenderMap.addItem(this.datasource[index][this.indexKey],render); 184 } 185 } 186 187 if (!dontCreate) 188 { 189 this.clearRowByIndex(index); 190 this.createItemRenderByIndex(index,true); 191 this.triggerEvent('itemRenderChanged'); 192 } 193 }, 194 195 /** 196 * sets an itemrender on given indices. 197 * 198 * @param {Array} indices 199 * @param {method} renderFactory 200 */ 201 setItemRenderByIndices : function(indices,renderFactory) 202 { 203 if (!indices) 204 { 205 return; 206 } 207 208 if (!indices instanceof Array) 209 { 210 log.error("Calling setItemRenderByIndices without indices specified in listrender "+this.id); 211 return; 212 } 213 214 //if we set more than 4 item renders we rerender whole list once. otherwise per item 215 //TODO could be smarter. like somekind of percentage of total list count 216 var dontRenderPerItem = (indices.length > 4); 217 218 for (var i = 0; i < indices.length; i++) 219 { 220 this.setItemRenderByIndex(indices[i],renderFactory,dontRenderPerItem); 221 } 222 223 if (dontRenderPerItem) 224 { 225 this.triggerEvent('itemRenderChanged'); 226 this.rerender(); 227 } 228 }, 229 230 /** 231 * sets an itemrender. Render will use for all indices the given itemrender 232 * 233 * @param {Banana.Controls.DataGridTableItemRender} render 234 * @return {this} 235 */ 236 setItemRender : function(render) 237 { 238 //grid is not yet initialized. set itemrender as default 239 if (!this.isInitialized) 240 { 241 this.defaultContentItemRender = render; 242 return this; 243 } 244 245 this.dataItemRenderMap.clear(); 246 247 for (var j =0, clen = this.indexRenderedItemRenderMap.length; j < clen; j++) 248 { 249 this.setItemRenderByIndex(j,render,true) 250 } 251 252 this.tableBody.clear(); 253 this.createTableParts(); 254 255 if (this.isRendered) 256 { 257 this.tableBody.invalidateDisplay(); 258 } 259 this.triggerEvent('itemRenderChanged'); 260 261 return this; 262 }, 263 264 /** 265 * @return {boolean} true when given index and itemrender exists 266 */ 267 hasItemRenderAt : function(index,itemRender) 268 { 269 if (this.indexRenderedItemRenderMap[index] instanceof itemRender) 270 { 271 return true; 272 } 273 274 return false; 275 }, 276 277 /** 278 * @param {Banana.Controls.DataGridTableFooterItemRender} r 279 */ 280 setFooterItemRender : function(r) 281 { 282 this.footerItemRender = r; 283 }, 284 285 /** 286 * @return {Banana.Controls.DataGridTableFooterItemRender} 287 */ 288 getFooterItemRender : function() 289 { 290 return this.footerItemRender; 291 }, 292 293 /** 294 * @param {Banana.Controls.ItemRender} 295 * @return {Banana.UiControl} 296 */ 297 getRowByItemRender : function(ir) 298 { 299 return this.getRowByIndex([this.indexRenderedItemRenderMap.indexOf(ir)]); 300 }, 301 302 /** 303 * @param {int} index 304 * @return {Banana.Control.Panel} 305 */ 306 getRowByIndex : function(index) 307 { 308 return this.indexRowMap[index]; 309 }, 310 311 /** 312 * Removes complete row. New indices will be calculated for remaining rows 313 * 314 * @param {int} index 315 */ 316 removeRowByIndex : function(index) 317 { 318 this.indexRowMap[index].remove(); 319 320 this.selectedIndices.remove(i); 321 this.indexRowMap.splice(index,1); 322 this.indexItemRenderFactory.splice(index,1); 323 this.indexRenderedItemRenderMap.splice(index,1); 324 }, 325 326 /** 327 * @param {int} index of row to be cleared. all gui data inside will be removed 328 */ 329 clearRowByIndex : function(index) 330 { 331 this.indexRowMap[index].clear(); 332 }, 333 334 /** 335 * @return {int} index in list by given row 336 */ 337 getIndexByRow : function(row) 338 { 339 return this.indexRowMap.indexOf(row); 340 }, 341 342 /** 343 * @return {int} index in list by given itemrender 344 */ 345 getIndexByItemRender : function(ir) 346 { 347 return this.indexRowMap.indexOf(ir.parent); 348 }, 349 350 /** 351 * TODO: 352 * @ignore 353 */ 354 getItemRenderByData : function(data) 355 { 356 // TODO 357 }, 358 359 /** 360 * Use this method to supply columns 361 * @param {Array} of new Banana.Controls.DataGridColumn 362 */ 363 setColumns : function(cols) 364 { 365 this.columns = cols; 366 }, 367 368 /** 369 * start creation of controls here 370 * we also give the user possibility to to something with the columns by looping over it 371 * and trigger 'onInitColumn' even 372 * Previous selected items will be reselected 373 * @ignore 374 */ 375 createControls : function() 376 { 377 if (!this.columns || !this.columns.length) 378 { 379 log.warning("Unable to render grid "+this.id+". No columns specified"); 380 } 381 382 for (var i=0; i<this.columns.length;i++) 383 { 384 this.triggerEvent('onInitColumn',this.columns[i]); 385 } 386 387 this.previousSelectedItems = this.getSelectedItems(); 388 389 this.selectedIndices.clear(); 390 this._super(); 391 392 this.table = new Banana.Controls.Table(); 393 this.table.setAttribute('cellspacing','0'); 394 395 this.tableHead = new Banana.Controls.TableHead(); 396 this.tableHead.addCssClass("BDataGridTableListHead"); 397 this.table.addControl(this.tableHead); 398 399 this.tableBody = new Banana.Controls.TableBody(); 400 this.tableBody.addCssClass("BDataGridTableListBody"); 401 this.table.addControl(this.tableBody); 402 403 this.table.bind('mousedown',this.getProxy(function(e){ 404 // TODO: This is catching every event in the scope of the table 405 // This should first check if user is clicking inside a input box 406 // otherwise multiselects etc don't work 407 if (e.originalEvent.shiftKey || e.originalEvent.ctrlKey) 408 { 409 e.preventDefault(); 410 } 411 })) 412 413 for (var j =0, clen = this.columns.length; j < clen; j++) 414 { 415 this.columns[j].setListRender(this); 416 } 417 418 this.createTableParts(); 419 420 this.addControl(this.table); 421 422 this.setSelectedItems(this.previousSelectedItems); 423 }, 424 425 /** 426 * create the parts of the table here 427 * @ignore 428 */ 429 createTableParts : function() 430 { 431 if (this.datasource.length) 432 { 433 this.createHeader(); 434 this.createRows(); 435 this.createFooter(); 436 } 437 }, 438 439 /** 440 * create header 441 * @ignore 442 */ 443 createHeader : function() 444 { 445 var row = new Banana.Controls.TableRow(); 446 447 var itemRender = this.getObject(this.getHeaderItemRender()); 448 itemRender.setListRender(this); 449 450 row.addControl(itemRender); 451 452 this.tableHead.addControl(row); 453 }, 454 455 /** 456 * create rows 457 * @ignore 458 */ 459 createRows : function() 460 { 461 //loop through the datasource to create the rows 462 for (var i =0, len = this.datasource.length; i < len; i++) 463 { 464 this.createRowByIndex(i); 465 this.createItemRenderByIndex(i) 466 } 467 }, 468 469 /** 470 * create footer 471 * @ignore 472 */ 473 createFooter : function() 474 { 475 if (!this.getFooterItemRender()) return; 476 477 var row = new Banana.Controls.TableRow(); 478 479 var itemRender = this.getObject(this.getFooterItemRender()); 480 itemRender.setListRender(this); 481 row.addControl(itemRender); 482 483 this.tableBody.addControl(row); 484 }, 485 486 /** 487 * 488 * @param {int} index 489 * @param {boolean} instantRender when true we instantly render the row 490 * 491 * @ignore 492 */ 493 createRowByIndex : function(index,instantRender) 494 { 495 var row = new Banana.Controls.TableRow(); 496 row.addCssClass((index % 2) ? 'BDataGridTableListRenderRow' : 'BDataGridTableListRenderRowAlt'); 497 498 row.bind('mouseover',this.getProxy(function(e){this.onRowMouseOver(e)}),row); 499 row.bind('mouseout',this.getProxy(function(e){this.onRowMouseOut(e)}),row); 500 row.bind('click',this.getProxy(function(e){this.onRowMouseClick(e)}),row); 501 502 this.tableBody.addControl(row,instantRender); 503 504 if (instantRender) 505 { 506 this.tableBody.invalidateDisplay(); 507 } 508 509 this.indexRowMap[index] = row; 510 511 this.triggerEvent('onRowCreated', {'index':index,'row': row, 'data': this.datasource[index]}); 512 }, 513 514 /** 515 * creates item render by index 516 * 517 * Item render will be either from 518 * - data to item render map 519 * - index to item render map 520 * - or default one 521 * 522 * used internally. don't call this manually. 523 * 524 * @param {int} index 525 * @param {boolean} instantRerender when true we instantly rerender the item 526 * 527 * @ignore 528 */ 529 createItemRenderByIndex : function(index,instantRerender) 530 { 531 // Optionally can be accessed by e.g. invalidateDisplay to peek 532 // at which index it is created 533 this.currentIndexCreation = index; 534 var itemRenderFactory = null; 535 536 if (this.datasource[index][this.indexKey] && this.dataItemRenderMap.getItem(this.datasource[index][this.indexKey])) 537 { 538 itemRenderFactory = this.dataItemRenderMap.getItem(this.datasource[index][this.indexKey]); 539 } 540 else if (this.indexItemRenderFactory[index]) 541 { 542 itemRenderFactory = this.indexItemRenderFactory[index]; 543 } 544 else 545 { 546 itemRenderFactory = this.defaultContentItemRender; 547 } 548 549 var itemRender = this.getObject(itemRenderFactory); 550 551 itemRender.setData(this.datasource[index]); 552 553 itemRender.bind('dataChanged',this.getProxy(function(e,f){ 554 555 this.getDataSource()[parseInt(e.data)] = e.currentTarget.getData(); 556 this.triggerEvent('dataSourceChanged'); 557 558 }),index.toString()) 559 560 //save mapping between itemRender and data 561 this.dataItemRenderMap.addItem(this.datasource[index][this.indexKey],itemRenderFactory); 562 563 this.indexRenderedItemRenderMap[index] = itemRender; 564 itemRender.setListRender(this); 565 var row = this.indexRowMap[index]; 566 row.addControl(itemRender); 567 568 if (instantRerender) 569 { 570 row.invalidateDisplay(); 571 } 572 573 this.currentIndexCreation = undefined; 574 }, 575 576 /** 577 * triggered when use moves over. we hightlight a row here 578 * 579 * @param {event} e 580 * @ignore 581 */ 582 onRowMouseOver : function(e) 583 { 584 var index = this.indexRowMap.indexOf(e.data); 585 586 var itemRender = this.indexRenderedItemRenderMap[index]; 587 588 if (!itemRender.getIsSelectable()) return; 589 590 itemRender.mouseOver(); 591 }, 592 593 /** 594 * triggered when use moves outside 595 * 596 * @param {event} e 597 * @ignore 598 */ 599 onRowMouseOut : function(e) 600 { 601 var index = this.indexRowMap.indexOf(e.data); 602 603 var itemRender = this.indexRenderedItemRenderMap[index]; 604 605 if (!itemRender.getIsSelectable()) return; 606 607 itemRender.mouseOut(); 608 }, 609 610 /** 611 * triggered when use moves outside 612 * if row is unselected we trigger event 'onItemSelect' 613 * 614 * @param {event} e 615 * @ignore 616 */ 617 onRowMouseClick : function(e) 618 { 619 var index = this.indexRowMap.indexOf(e.data); 620 621 var itemRender = this.indexRenderedItemRenderMap[index]; 622 623 if (!itemRender.getIsSelectable()) return; 624 625 if (index<0) {return} 626 627 var ctrlKey = e.ctrlKey; 628 var shiftKey = e.shiftKey; 629 630 if (this.getIndexIsSelected(index)) 631 { 632 if (!ctrlKey && !shiftKey) 633 { 634 this.clearSelectedIndices(); 635 } 636 else 637 { 638 this.clearSelectedIndex(index); 639 } 640 } 641 else 642 { 643 if (this.selectionType == 'single') 644 { 645 this.clearSelectedIndices(); 646 this.addSelectedIndex(index); 647 } 648 else 649 { 650 if (!ctrlKey && !shiftKey) 651 { 652 this.clearSelectedIndices(); 653 this.addSelectedIndex(index); 654 } 655 if (ctrlKey) 656 { 657 this.addSelectedIndex(index); 658 } 659 if (shiftKey) 660 { 661 662 var firstItem = parseInt(this.selectedIndices.getKeyByIndex(0)); 663 664 if (!firstItem) firstItem = 0; 665 666 var start; 667 var end; 668 if (index > firstItem) 669 { 670 start = firstItem; 671 end = index; 672 } 673 else 674 { 675 start = index; 676 end = parseInt(this.selectedIndices.getKeyByIndex([this.selectedIndices.getLength()-1])); 677 } 678 679 this.clearSelectedIndices(); 680 681 for (var i = parseInt(start); i <= parseInt(end); i++) 682 { 683 this.addSelectedIndex(i); 684 } 685 } 686 687 } 688 } 689 690 this.triggerEvent('onItemSelect'); 691 }, 692 693 /** 694 * Selects the index. also adds BDataGridRowSelected css class to the item render 695 * 696 * @param {int} index 697 */ 698 selectIndex : function(index) 699 { 700 var ir = this.getRenderedItemRenderByIndex(index); 701 702 if (ir && typeof(ir.select) == 'function' && ir.getIsSelectable()) 703 { 704 ir.select(); 705 } 706 }, 707 708 /** 709 * Selects the index. also removes BDataGridRowSelected css class from the item render 710 * 711 * @param {int} index 712 */ 713 deSelectIndex : function(index) 714 { 715 var ir = this.getRenderedItemRenderByIndex(index); 716 717 if (ir && typeof(ir.deselect) == 'function') 718 { 719 ir.deselect(); 720 } 721 } 722 }); 723