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 goog.require('Banana.Controls.DataGridTileItemRender'); 9 10 goog.provide('Banana.Controls.DataGridTileListRender'); 11 12 /** @namespace Banana.Controls.DataGridTileListRender */ 13 namespace('Banana.Controls').DataGridTileListRender = Banana.Controls.DataGridBaseListRender.extend( 14 /** @lends Banana.Controls.DataGridTileListRender.prototype */ 15 { 16 17 /** 18 * 19 * Creates a datagrid tile list render. Each item in the datasource is a tile. 20 * Use setPlaceHolderWidth to have multiple tiles after each other. You can also define this in css 21 * By default each tile is rendered by a Banana.Controls.DataGridTileItemRender instance 22 * implement your own instance to construct more advanced item renders 23 * 24 * example: 25 26 //define our custom datagrid table item render 27 myCustomItemRender = Banana.Controls.DataGridTableContentItemRender.extend({ 28 29 createComponents : function() 30 { 31 var label = new Banana.Controls.Panel(); 32 label.setData(this.getData().id); 33 this.addControl(new Banana.Controls.Panel()); 34 } 35 }); 36 37 grid = new Banana.Controls.DataGrid(); 38 39 var listRender = new Banana.Controls.DataGridTileListRender() 40 listRender.setHItemCount(4); 41 listRender.setTilePadding(6); 42 43 //note that this method required you to pass a function providing the itemrender. 44 listRender.setItemRender(function(){return new myCustomItemRender()}); 45 46 grid.setListRender(listRender); 47 48 var content = [{id:1},{id:2}]; 49 50 grid.setDataSource(content); 51 52 this.addControl(grid); 53 * 54 * @constructs 55 * @extends Banana.Controls.DataGridBaseListRender 56 */ 57 init : function() 58 { 59 this._super(); 60 61 this.addCssClass("BDataGridTileListRender") 62 63 this.indexKey = null; //specifies the indexkey used to indentify items 64 65 this.indexTilePlaceHolderMap = []; 66 67 this.dataItemRenderMap = new Banana.Util.ArrayBiCollection(); //mapping of item renders to data 68 69 this.defaultContentItemRender = Banana.Controls.DataGridTileItemRender; 70 71 this.contentItemRender = null; 72 }, 73 74 /** 75 * Adds item 76 * 77 * @param {mixed} data 78 * @param {Boolean} when true we instantly renders it 79 */ 80 addItem : function(data,render) 81 { 82 this.applyUid(data); 83 84 var il = this._super(data); 85 86 if (render) 87 { 88 this.indexItemRenderFactory[il] = render; 89 } 90 91 this.clearSelectedIndices(); 92 93 this.createDivPlaceHolder(this.datasource.length-1,true); // -1 cause index start with 0 94 this.createItemRenderByIndex(this.datasource.length-1,true); 95 96 return il; 97 }, 98 99 /** 100 * Adds item to the datasource. Results in rerender of the list 101 * 102 * TODO: now every add results into instant render. we could optimize this 103 * by rendering everything at once. 104 * 105 * @param {Array} items 106 */ 107 addItems : function(items) 108 { 109 var i; 110 for (i=0, len= items.length; i < len; i++) 111 { 112 this.addItem(items[i]); 113 } 114 }, 115 116 /** 117 * At item at specific index. Results in rerender of the list 118 * 119 * @param {Object} item 120 * @param {int} index 121 * @param {Banana.Controls.DataGridTileItemRender} render 122 * @param {Boolean} preventRender when true we wont render. useful whne adding multiple items at once 123 */ 124 addItemAt : function(item,index,render,preventRender) 125 { 126 this.applyUid(item); 127 128 this.clearSelectedIndices(); 129 130 this.datasource.splice(index,0,item); 131 this.indexDataMap.splice(index,0,item); 132 133 if (render) 134 { 135 this.indexItemRenderFactory.splice(index,0,item); 136 } 137 138 //modify index helpers 139 this.indexTilePlaceHolderMap.splice(index,0,null); 140 this.indexItemRenderFactory.splice(index,0,null); 141 this.indexRenderedItemRenderMap.splice(index,0,null); 142 143 if (!preventRender) 144 { 145 this.setDataSource(this.datasource); 146 this.triggerEvent('dataSourceChanged'); 147 } 148 }, 149 150 /** 151 * At items at specific index. Results in rerender of the list 152 * 153 * @param {Object} item 154 * @param {int} index 155 * @param {Banana.Controls.DataGridTileItemRender} render 156 */ 157 addItemsAt : function(items,index,render) 158 { 159 var i; 160 for (i=0, len= items.length; i < len; i++) 161 { 162 this.addItemAt(items[i],index,render,true); 163 } 164 165 this.setDataSource(this.datasource); //force rerender 166 }, 167 168 /** 169 * removes item if found in datasource 170 * @param {mixed} data 171 */ 172 removeItem : function(data) 173 { 174 var index = this.datasource.indexOf(data); 175 176 if (index != -1) 177 { 178 this.removeItemRenderPlaceHolderByIndex(index); 179 } 180 }, 181 182 /** 183 * removes item from datagrid based on itemrender instance. 184 * @param {Banana.Controls.DataGridTileItemRender} itemRender 185 */ 186 removeItemByItemRender : function(itemRender) 187 { 188 var index = this.indexRenderedItemRenderMap.indexOf(itemRender); 189 190 this.removeItemRenderPlaceHolderByIndex(index); 191 }, 192 193 /** 194 * FIXME 195 * @ignore 196 */ 197 removeAllItems : function() 198 { 199 this._super(); 200 }, 201 202 /** 203 * @return {Banana.Controls.DataGridTileItemRender} 204 */ 205 getDefaultItemRender : function() 206 { 207 return this.defaultContentItemRender; 208 }, 209 210 /** 211 * Use this to change item render on a specific index. 212 * By default the list render will rerender the new item render 213 * 214 * @param {int} index 215 * @param {String} render 216 * @param {Boolean} dontCreate 217 * @param {Boolean} ignoreDataItemRenderMap 218 */ 219 setItemRenderByIndex : function(index,render,dontCreate,ignoreDataItemRenderMap) 220 { 221 this.indexItemRenderFactory[index] = render; 222 223 //also save the item render data map relation 224 //could be handy when we set an item render and later change the datasource 225 //the item location could be changed then. itemrender - index relation is 226 //in that situation not enough 227 if (!ignoreDataItemRenderMap && this.datasource && this.datasource[index]) 228 { 229 //TODO this is the only place a mapping between data and render is set. 230 //this should also be possible at a later moment. 231 if (this.datasource[index][this.indexKey]) 232 { 233 this.dataItemRenderMap.addItem(this.datasource[index][this.indexKey],render); 234 } 235 } 236 237 if (!dontCreate) 238 { 239 this.clearItemRenderPlaceHolderByIndex(index); 240 this.createItemRenderByIndex(index,true); 241 //TODO only trigger when changed 242 this.triggerEvent('itemRenderChanged'); 243 } 244 }, 245 246 /** 247 * Sets item renders on multi indices at once. 248 * 249 * @param {Array} indices 250 * @param {Function} renderFactory of Banana.Controls.DataGridTileItemRender 251 */ 252 setItemRenderByIndices : function(indices,renderFactory) 253 { 254 if (!indices.length) 255 { 256 return; 257 } 258 259 //if we set more than 4 item renders we rerender whole list once. otherwise per item 260 //TODO could be smarter. like somekind of percentage of total list count 261 var dontRenderPerItem = (indices.length > 4); 262 263 var i; 264 for (i = 0; i < indices.length; i++) 265 { 266 this.setItemRenderByIndex(indices[i],renderFactory,dontRenderPerItem); 267 } 268 269 if (dontRenderPerItem) 270 { 271 this.triggerEvent('itemRenderChanged'); 272 this.rerender(); 273 } 274 }, 275 276 /** 277 * Set the global item render for this list render. 278 * @param {Banana.Controls.DataGridTileItemRender} itemRender 279 */ 280 setItemRender : function(render) 281 { 282 //we set in general the item render. so we need to get rid of 283 //the data item render map. 284 this.dataItemRenderMap.clear(); 285 286 var j; 287 for (j =0, clen = this.indexRenderedItemRenderMap.length; j < clen; j++) 288 { 289 this.setItemRenderByIndex(j,render,true); 290 } 291 292 if (this.mainContainer) 293 { 294 this.mainContainer.clear(); 295 this.createItems(); 296 297 if (this.isRendered) 298 { 299 this.mainContainer.invalidateDisplay(); 300 } 301 } 302 else 303 { 304 this.defaultContentItemRender = render; 305 } 306 307 this.triggerEvent('itemRenderChanged'); 308 }, 309 310 /** 311 * Rerenders item render by index. 312 * @param {int} index 313 */ 314 rerenderIndex : function(index) 315 { 316 this.previousSelectedItems = this.getSelectedItems(); 317 this.indexTilePlaceHolderMap[index].clear(); 318 this.createItemRenderByIndex(index,true); 319 this.setSelectedItems(this.previousSelectedItems); 320 }, 321 322 /** 323 * gets rendered item render by data 324 * @param {Object} data 325 * @return {Banana.Controls.ItemRender} 326 * 327 * @ignore 328 */ 329 getRenderedItemRenderByData : function(data) 330 { 331 //TODO we have no effective index from data to index. 332 }, 333 334 /** 335 * @param {int} int 336 * @param {Banana.Controls.ItemRender} itemRender 337 * @return {boolean} 338 */ 339 hasItemRenderAt : function(index,itemRender) 340 { 341 if (this.indexRenderedItemRenderMap[index] instanceof itemRender) 342 { 343 return true; 344 } 345 346 return false; 347 }, 348 349 /** 350 * removes a placeholder by index 351 * 352 * @param {int} index 353 */ 354 removeItemRenderPlaceHolderByIndex : function(index) 355 { 356 this.datasource.splice(index,1); 357 358 this.indexTilePlaceHolderMap[index].remove(); 359 360 this.selectedIndices.remove(i); 361 this.indexTilePlaceHolderMap.splice(index,1); 362 this.indexItemRenderFactory.splice(index,1); 363 this.indexRenderedItemRenderMap.splice(index,1); 364 365 this.triggerEvent('dataSourceChanged'); 366 }, 367 368 /** 369 * Empties placeholder by index 370 */ 371 clearItemRenderPlaceHolderByIndex : function(index) 372 { 373 this.indexTilePlaceHolderMap[index].clear(); 374 }, 375 376 /** 377 * @return {int} 378 */ 379 getIndexByItemRenderPlaceHolder : function(row) 380 { 381 return this.indexTilePlaceHolderMap.indexOf(row); 382 }, 383 384 /** 385 * @param {int} 386 * @return {Banana.UiControl} 387 */ 388 getItemRenderPlaceHolderByIndex : function(index) 389 { 390 return this.indexTilePlaceHolderMap[index]; 391 }, 392 393 /** 394 * @param {Banana.Controls.ItemRender} 395 * @return {Banana.UiControl} 396 */ 397 getItemRenderPlaceHolderByItemRender : function(ir) 398 { 399 return this.indexTilePlaceHolderMap[this.indexRenderedItemRenderMap.indexOf(ir)]; 400 }, 401 402 /** 403 * @return {int} 404 */ 405 getIndexByItemRender : function(ir) 406 { 407 return this.indexTilePlaceHolderMap.indexOf(ir.parent); 408 }, 409 410 /** 411 * Invoked after changing datasource or invalidating 412 * @ignore 413 */ 414 createControls : function() 415 { 416 this.setupIndexing(); 417 418 this.previousSelectedItems = this.getSelectedItems(); 419 420 this.selectedIndices.clear(); 421 this._super(); 422 423 this.mainContainer = new Banana.Controls.Panel(); 424 425 this.mainContainer.bind('mousedown',this.getProxy(function(e){ 426 // TODO: This is catching every event in the scope of the table 427 // This should first check if user is clicking inside a input box 428 // otherwise multiselects etc don't work 429 if (e.originalEvent.shiftKey || e.originalEvent.ctrlKey) 430 { 431 e.preventDefault(); 432 } 433 })); 434 435 //FIXME this is not needed anymore? 436 437 //if we gave a horizontal item count. only render when already rendered 438 this.createItemsLater = false; 439 440 if (this.horizontalItemCount && this.isRendered) 441 { 442 this.createItems(); 443 } 444 else if (this.horizontalItemCount && !this.isRendered) 445 { 446 this.createItemsLater = true; 447 } 448 else 449 { 450 this.createItems(); 451 } 452 453 this.addControl(this.mainContainer); 454 455 this.setSelectedItems(this.previousSelectedItems); 456 457 }, 458 459 /** 460 * @ignore 461 */ 462 updateDisplay : function() 463 { 464 if (this.createItemsLater && this.getDemensions().width) 465 { 466 this.createItemsLater = false; 467 this.createItems(); 468 469 this.mainContainer.invalidateDisplay(); 470 } 471 }, 472 473 /** 474 * @ignore 475 */ 476 createItems : function() 477 { 478 if (this.datasource.length) 479 { 480 var i; 481 for (i =0, len = this.datasource.length; i < len; i++) 482 { 483 this.applyUid(this.datasource[i]); 484 485 this.createDivPlaceHolder(i); 486 this.triggerEvent('onPreCreateItemRender',{'index':i,'data':this.datasource[i]}); 487 this.createItemRenderByIndex(i); 488 } 489 490 this.mainContainer.addControl("<div style='clear:both'></div>"); 491 this.cachedDemensions = null; //clear cache 492 } 493 }, 494 495 /** 496 * @depricated 497 */ 498 setHorizontalTileCount : function(count) 499 { 500 log.warning("set horizontalTileCount datagridTileListRender is depricated. use setPlaceHolderWidth instead"); 501 return this; 502 }, 503 504 /** 505 * @depricated 506 */ 507 setTilePadding : function(padding) 508 { 509 log.warning("set tile padding in datagridTileListRender is depricated. apply style manualy in css"); 510 return this; 511 }, 512 513 /** 514 * Creates a placeholder where itemrenders are rendered in 515 * 516 * @param {int} int 517 * @param {Boolean} instantRender 518 * @ignore 519 */ 520 createDivPlaceHolder : function(index,instantRender) 521 { 522 var tileplaceholder = new Banana.Controls.Panel(); 523 tileplaceholder.addCssClass("BDataGridTilePlaceHolder"); 524 525 if (this.placeHolderWidth) 526 { 527 tileplaceholder.setCss({width:this.placeHolderWidth}); 528 } 529 530 tileplaceholder.bind('mouseenter',this.getProxy(function(e){this.onRowMouseOver(e); return true;}),tileplaceholder); 531 tileplaceholder.bind('mouseleave',this.getProxy(function(e){this.onRowMouseOut(e);return true;}),tileplaceholder); 532 tileplaceholder.bind('click',this.getProxy(function(e){this.onRowMouseClick(e);return true;}),tileplaceholder); 533 534 this.mainContainer.addControl(tileplaceholder,instantRender); 535 536 if (instantRender) 537 { 538 this.mainContainer.invalidateDisplay(); 539 } 540 this.indexTilePlaceHolderMap[index] = tileplaceholder; 541 }, 542 543 /** 544 * sets width of the placeholder. example "25%" or "50px" 545 * @param {String} width 546 */ 547 setPlaceHolderWidth : function(width) 548 { 549 this.placeHolderWidth = width; 550 }, 551 552 /** 553 * creates item render by index 554 * 555 * Item render will be either from 556 * - data to item render map 557 * - index to item render map 558 * - or default one 559 * 560 * dont directly use this method. The item render is rendered into a 561 * placeholder which doesnt gets cleared here. 562 * to recreate a itemrender use rerenderIndex() 563 * 564 * @param {int} index 565 * @param {boolean} instantRerender when true we directly render it 566 * @ignore 567 */ 568 createItemRenderByIndex : function(index,instantRerender) 569 { 570 // Optionally can be accessed by e.g. invalidateDisplay to peek 571 // at which index it is created 572 this.currentIndexCreation = index; 573 574 var itemRenderFactory = null; 575 576 var key = this.datasource[index][this.indexKey]; 577 578 if (key && this.dataItemRenderMap.getItem(key)) 579 { 580 itemRenderFactory = this.dataItemRenderMap.getItem(key); 581 } 582 else if (this.indexItemRenderFactory[index]) 583 { 584 itemRenderFactory = this.indexItemRenderFactory[index]; 585 } 586 else 587 { 588 itemRenderFactory = this.defaultContentItemRender; 589 } 590 591 var itemRender = this.getObject(itemRenderFactory); 592 593 itemRender.setData(this.datasource[index]); 594 595 itemRender.bind('dataChanged',this.getProxy(function(e,f){ 596 597 this.getDataSource()[parseInt(e.data, 10)] = e.currentTarget.getData(); 598 this.triggerEvent('dataSourceChanged'); 599 600 }),index.toString()); 601 602 //save mapping between itemRender and data 603 this.dataItemRenderMap.addItem(this.datasource[index][this.indexKey],itemRenderFactory); 604 605 this.indexRenderedItemRenderMap[index] = itemRender; 606 itemRender.setListRender(this); 607 var tilePlaceHolder = this.indexTilePlaceHolderMap[index]; 608 tilePlaceHolder.addControl(itemRender); 609 610 if (instantRerender) 611 { 612 tilePlaceHolder.invalidateDisplay(); 613 } 614 615 this.currentIndexCreation = undefined; 616 }, 617 618 /** 619 * invoked after hovering over a tile 620 * @ignore 621 */ 622 onRowMouseOver : function(e) 623 { 624 var index = this.indexTilePlaceHolderMap.indexOf(e.data); 625 626 var itemRender = this.indexRenderedItemRenderMap[index]; 627 628 itemRender.mouseOver(); 629 }, 630 631 /** 632 * invoked after mouse moves out of a tile 633 * @ignore 634 */ 635 onRowMouseOut : function(e) 636 { 637 var index = this.indexTilePlaceHolderMap.indexOf(e.data); 638 639 var itemRender = this.indexRenderedItemRenderMap[index]; 640 641 itemRender.mouseOut(); 642 }, 643 644 /** 645 * invoked after clicking on a row/tile 646 * @ignore 647 */ 648 onRowMouseClick : function(e) 649 { 650 var index = this.indexTilePlaceHolderMap.indexOf(e.data); 651 652 var itemRender = this.indexRenderedItemRenderMap[index]; 653 654 if (!itemRender.getIsSelectable()) 655 { 656 return; 657 } 658 659 if (index<0) 660 { 661 return; 662 } 663 664 var ctrlKey = e.ctrlKey; 665 var shiftKey = e.shiftKey; 666 667 if (this.getIndexIsSelected(index)) 668 { 669 if (!ctrlKey && !shiftKey) 670 { 671 this.clearSelectedIndices(); 672 } 673 else 674 { 675 this.clearSelectedIndex(index); 676 } 677 } 678 else 679 { 680 if (this.selectionType === 'single') 681 { 682 this.clearSelectedIndices(); 683 this.addSelectedIndex(index); 684 } 685 else 686 { 687 if (!ctrlKey && !shiftKey) 688 { 689 this.clearSelectedIndices(); 690 this.addSelectedIndex(index); 691 } 692 if (ctrlKey) 693 { 694 this.addSelectedIndex(index); 695 } 696 if (shiftKey) 697 { 698 699 var firstItem = parseInt(this.selectedIndices.getKeyByIndex(0), 10); 700 701 if (!firstItem) 702 { 703 firstItem = 0; 704 } 705 706 var start; 707 var end; 708 if (index > firstItem) 709 { 710 start = firstItem; 711 end = index; 712 } 713 else 714 { 715 start = index; 716 end = parseInt(this.selectedIndices.getKeyByIndex([this.selectedIndices.getLength()-1]), 10); 717 } 718 719 this.clearSelectedIndices(); 720 721 var i; 722 for (i = parseInt(start, 10); i <= parseInt(end, 10); i++) 723 { 724 this.addSelectedIndex(i); 725 } 726 } 727 728 } 729 } 730 731 this.triggerEvent('onItemSelect'); 732 }, 733 734 /** 735 * Selects the index. 736 * 737 * @param {int} index 738 */ 739 selectIndex : function(index) 740 { 741 var ir = this.getRenderedItemRenderByIndex(index); 742 743 if (ir && typeof(ir.select) == 'function' && ir.getIsSelectable()) 744 { 745 ir.select(); 746 } 747 }, 748 749 /** 750 * Selects the index. 751 * 752 * @param {int} index 753 */ 754 deSelectIndex : function(index) 755 { 756 var ir = this.getRenderedItemRenderByIndex(index); 757 758 if (ir && typeof(ir.deselect) == 'function') 759 { 760 ir.deselect(); 761 } 762 } 763 764 });