1 /** 2 * @author Gillis Haasnoot <gillis.haasnoot@gmail.com> 3 * @package Banana.Controls 4 * @summary DataGridDataTreeListRender 5 */ 6 7 goog.provide('Banana.Controls.DataGridTreeListRender'); 8 9 goog.require('Banana.Controls.DataGridTreeListHolder'); 10 goog.require('Banana.Controls.DataGridTreeListNodeToggle'); 11 goog.require('Banana.Controls.DataGridTreeItemRender'); 12 13 /** @namespace Banana.Controls.DataGridTreeListRender */ 14 namespace('Banana.Controls').DataGridTreeListRender = Banana.Controls.DataGridBaseListRender.extend( 15 /** @lends Banana.Controls.DataGridTreeListRender.prototype */ 16 { 17 /** 18 * Create a datagrid tree list render. a tree is a consists out of nodes with optional child nodes 19 * For each node the list creates a node info object with various node state parameters. 20 * 21 * bindable events 22 * 23 * - dataSourceChanged 24 * - onRequestChilds 25 * - onItemOpened 26 * - onItemClosed 27 * - onPreCreateItem 28 * - onPostCreateIndex 29 * - onItemMouseClick 30 * - onItemMouseOut 31 * - onItemMouseEnter 32 * - onSelectIndex 33 * - onDeselectIndex 34 * - onItemSelect 35 * 36 * example 37 38 //make some datasource 39 var datasource =[]; 40 41 var s1 = {'name':'s1','children':[]}; 42 var s2 = {'name':'s2','children':[]}; 43 var s3 = {'name':'s3','children':[]}; 44 var s4 = {'name':'s4','children':[]}; 45 var s5 = {'name':'s5','children':[]}; 46 var s6 = {'name':'s6','children':[]}; 47 48 root.push(s1); 49 root.push(s2); 50 root.push(s3); 51 52 s1.children.push(s4); 53 s1.children.push(s5); 54 s4.children.push(s6); 55 56 var list = new Banana.Controls.DataGrid(); 57 58 var listRender = new Banana.Controls.DataGridDataTreeListRender(); 59 listRender.setChildProperty("children"); 60 listRender.setDefaultOpen(true); 61 62 listRender.bind('selectIndex',this.getProxy(function(e,index){ 63 listRender.selectAllFromIndex(index) 64 })); 65 66 listRender.setSortfunction(function(a,b) 67 { 68 return a.name.toString().toLowerCase().localeCompare(b.name.toString().toLowerCase()) 69 }); 70 71 list.setListRender(listRender); 72 73 list.setDataSource(datasource); 74 75 listRender.bind('dataSourceChanged',this.getProxy(function(){ 76 77 this.dataChanged = true; 78 })); 79 80 listRender.bind('dataChanged',this.getProxy(function(){ 81 82 })); 83 84 this.addControl(list); 85 86 * @constructs 87 * @extends Banana.Controls.DataGridBaseListRender 88 */ 89 init : function() 90 { 91 this._super(); 92 93 this.addCssClass("BDataGridTree"); 94 95 this.indexKey = null; 96 this.nodeData = []; //mapping between index key and nodeinfo 97 //this.debug = true; 98 99 this.columns = []; 100 this.dataItemRenderMap = new Banana.Util.ArrayBiCollection(); //mapping of item renders to data 101 this.keyDataIndex = []; //mapping between key and datasource 102 this.dataKeyIndex = []; //mapping between data key and index 103 this.indexHolderMap = []; 104 this.indexOrder = []; 105 106 //default sort function 107 this.sortFunc = null; 108 109 this.childRequestSizeLimit = 20; 110 this.childKey = 'children'; 111 this.rootNodeVisible = true; 112 this.defaultContentItemRender = Banana.Controls.DataGridTreeItemRender; 113 }, 114 115 /** 116 * @param {Banana.Controls.TreeItemRender} render 117 */ 118 setDefaultItemRender : function(render) 119 { 120 this.defaultContentItemRender = render; 121 }, 122 123 /** 124 * sets item render on index 125 * @param {int} index 126 * @param {Banana.Controls.TreeItemRender} ir 127 * @return {this} 128 */ 129 setIndexItemRender : function(index,ir) 130 { 131 if (!(typeof(ir) === 'function')) 132 { 133 throw "Item render should be a factory"; 134 } 135 this.indexItemRenderFactory[index] = ir; 136 137 return this; 138 }, 139 140 /** 141 * By default only the root nodes are opened. 142 * if you want to have all nodes opened by default, call this method 143 * 144 * @Param {boolean} bool 145 * @return {this} 146 */ 147 setDefaultOpen : function(bool) 148 { 149 this.defaultOpen = bool; 150 return this; 151 }, 152 153 /** 154 * @param {String} messsage 155 */ 156 toLogger : function() 157 { 158 if (!this.debug) 159 { 160 return; 161 } 162 console.log(arguments); 163 }, 164 165 /** 166 * sort function which is used to order nodes per node level 167 * @param {Object} sf 168 * @return {this} 169 */ 170 setSortfunction : function(sf) 171 { 172 this.sortFunc = sf; 173 return this; 174 }, 175 176 /** 177 * Use this to change item render on a specific index. 178 * By default the list render will rerender the new item render 179 * 180 * @param {int} index 181 * @param {String} render 182 * @param {Boolean} dontCreate 183 * @param {Boolean} ignoreDataItemRenderMap 184 */ 185 setItemRenderByIndex : function(index,render,dontCreate,ignoreDataItemRenderMap) 186 { 187 this.indexItemRenderFactory[index] = render; 188 189 //also save the item render data map relation 190 //could be handy when we set an item render and later change the datasource 191 //the item location could be changed then. itemrender - index relation is 192 //in that situation not enough 193 var datasource = this.getDataSourceByIndex(index); 194 195 if (!ignoreDataItemRenderMap && datasource) 196 { 197 this.dataItemRenderMap.addItem(datasource[this.indexKey],render); 198 } 199 200 if (!dontCreate) 201 { 202 //first check if holder is rendered 203 if (this.getHolder(index)) 204 { 205 this.createItemRenderByIndex(index, true, true); 206 this.triggerEvent('itemRenderChanged'); 207 } 208 } 209 }, 210 211 /** 212 * use this method to determine if a node should have auto width (fits the content inside the node) 213 * or maximum width (fits the grid container) 214 * We compute the width of each node and thus a small performance loss. 215 * @param {boolean} bool 216 * @return {this} 217 */ 218 setMaximizeNodeWidth : function(bool) 219 { 220 this.maximizeNodeWidth = bool; 221 return this; 222 }, 223 224 /** 225 * Specify which property contain the childs 226 * @param {String} ck 227 * @return {this} 228 */ 229 setChildProperty : function(ck) 230 { 231 this.childKey = ck; 232 return this; 233 }, 234 235 /** 236 * sets child size request limit 237 * use this when loading from external resources is needed 238 * 239 * @param {int} limit 240 * @return {this} 241 */ 242 setChildRequestSizeLimit : function(limit) 243 { 244 this.childRequestSizeLimit = limit; 245 return this; 246 }, 247 248 /** 249 * gives true when item is found by given index key 250 * use this function to ensure uniquality 251 * @param key 252 * @return {Boolean} 253 */ 254 hasItemIndexKey : function(key) 255 { 256 return this.keyDataIndex[key] ? true : false; 257 }, 258 259 /** 260 * always returns a source, when invalid key is given we return the root one 261 * 262 * @return {Object} 263 */ 264 getDataSourceByKey : function(key) 265 { 266 var source = this.keyDataIndex[key]; 267 268 if (!source) //we are adding to root 269 { 270 source = this.getDataSourceByIndex(0); 271 } 272 273 return source; 274 }, 275 276 /** 277 * @param {Object} ds 278 * @return {Data} 279 */ 280 getNodeDataByData : function(data) 281 { 282 return this.nodeData[data[this.indexKey]]; 283 }, 284 285 /** 286 * Ensures data. Needed if items are added to an empty datasource 287 * 288 * @ignore 289 */ 290 ensureRootData : function() 291 { 292 if (!this.rootData) 293 { 294 this.setupIndexing(); 295 296 var root = {}; 297 root[this.indexKey] = this.getUid(); 298 root[this.childKey] = this.datasource; 299 this.rootData = root; 300 301 var nodeData = {}; 302 nodeData.addedChildren = []; 303 nodeData.isRoot = true; 304 nodeData.open = true; 305 306 this.setNodeDataByData(nodeData,root); 307 } 308 }, 309 310 /** 311 * Sets node data 312 * @param {Object} nodeData 313 */ 314 setNodeData : function(nodeData) 315 { 316 this.ensureRootData(); 317 318 this.setNodeDataByData(nodeData,this.rootData); 319 }, 320 321 /** 322 * gets node data from root 323 * @return {Object} 324 */ 325 getNodeData : function() 326 { 327 return this.getNodeDataByData(this.rootData); 328 }, 329 330 /** 331 * @param {Object} data 332 * @param {Object} source 333 */ 334 setNodeDataByData : function(data,source) 335 { 336 if(!source) 337 { 338 key = '__root__'; 339 } 340 else 341 { 342 this.applyUid(source); 343 var key = source[this.indexKey]; 344 } 345 346 this.nodeData[key] = data; 347 }, 348 349 /** 350 * return {Banana.Controls.UiControl) 351 */ 352 getRenderedItemRenderByData : function(data) 353 { 354 var nodeData = this.getNodeDataByData(data); 355 356 if (!nodeData) 357 { 358 return null; 359 } 360 return this.getRenderedItemRenderByIndex(nodeData.index); 361 }, 362 363 /** 364 * We assume we can reach the data. this assumtion is made already 365 * in the create controls method 366 * 367 * TODO: we are using this method a lot and it is slow. make is quicker by caching 368 * but watch out. the index datasource mapping relation is changed a lot also 369 * 370 * @param {int} i 371 * @return {Object} 372 */ 373 getDataSourceByIndex : function(i) 374 { 375 if (!this.datasource) 376 { 377 return null; 378 } 379 380 i = i.toString(); 381 var split = i.split("-"); 382 var result = null; 383 var datasource = this.datasource; 384 385 var x; 386 for (x = 0; x< split.length; x++) 387 { 388 result = datasource[split[x]]; 389 var key = result[this.indexKey]; 390 391 //if no node info available then break out. we dont have childs 392 if (!result || !this.nodeData[key]) 393 { 394 break; 395 } 396 397 datasource = result[this.childKey]; 398 399 if (!datasource) 400 { 401 break; 402 } 403 } 404 405 return result; 406 }, 407 408 /** 409 * @param {Array} datasource 410 * @return {int} index 411 */ 412 getIndexByDataSource : function(datasource) 413 { 414 return this.dataKeyIndex[datasource[this.indexKey]]; 415 }, 416 417 /** 418 * @param {int} i 419 * @return {String} parent index 420 * 421 * TODO: we are using this method a lot and it is slow. make it quicker by caching 422 */ 423 getParentIndex :function(i) 424 { 425 var split = i.toString().split("-"); 426 split.pop(); 427 428 if (split.length) 429 { 430 return split.join('-'); 431 } 432 433 return null; 434 }, 435 436 /** 437 * @param {String} i index 438 * 439 * @return {Banana.Controls.DataGridTreeListHolder} 440 */ 441 getHolder : function(i) 442 { 443 return this.indexHolderMap[i]; 444 }, 445 446 /** 447 * @param {String} i index 448 * @return {Banana.Controls.DataGridTreeListHolder} 449 * 450 * @ignore 451 */ 452 getParentHolder: function(i) 453 { 454 var parentIndex = this.getParentIndex(i); 455 456 if (parentIndex) 457 { 458 return this.indexHolderMap[parentIndex]; 459 } 460 461 return null; 462 }, 463 464 /** 465 * called by constructor. creation of controls is starting here 466 * @ignore 467 */ 468 createControls : function() 469 { 470 try 471 { 472 this.setupIndexing(); 473 474 this.ensureRootData(); 475 476 var nodeData = this.getNodeDataByData(this.nodeData); 477 478 this.rootData[this.childKey] = this.datasource; 479 480 this.datasource = [this.rootData]; 481 482 this.toLogger("create Controls",this.datasource); 483 484 this.toLogger("determined key index as",this.indexKey); 485 486 this.bind('mousedown',this.getProxy(function(e){ 487 // TODO: This is catching every event in the scope of the table 488 // This should first check if user is clicking inside a input box 489 // otherwise multiselects etc don't work 490 if (e.originalEvent.shiftKey || e.originalEvent.ctrlKey) 491 { 492 e.preventDefault(); 493 } 494 else 495 { 496 //this.clearSelectedIndices(); 497 } 498 })); 499 500 this.createNodeInfo(this.datasource,null,this.getProxy(function(item){ 501 502 if (this.defaultOpen) 503 { 504 this.nodeData[item[this.indexKey]].open = true; 505 } 506 })); 507 508 this.createNodes(); 509 510 this.restoreSelectedIndices(); 511 this.addControl('<div style="clear:both"></div>'); 512 } 513 catch(e) 514 { 515 console.log(e.message) 516 } 517 }, 518 519 /** 520 * TODO selected indices should be saved in nodedata 521 * now its seperated 522 */ 523 restoreSelectedIndices : function() 524 { 525 var i=0; 526 var len; 527 var indices = this.getSelectedIndices(true); 528 529 for (i = 0,len = indices.length; i < len; i++) 530 { 531 var ir = this.getRenderedItemRenderByIndex(indices[i]); 532 533 if (!ir) 534 { 535 continue; 536 } 537 538 ir.select(); 539 } 540 }, 541 542 /** 543 * inserts recursively info to datasource items. 544 * this method should always be called after node modification(s) 545 * 546 * @param {object} datasource 547 * @param {int} parentIndex 548 * @param {Object} func function which get applied on every item 549 * 550 */ 551 createNodeInfo : function(datasource,parentIndex,func) 552 { 553 //sort first if sort method is supplied 554 if (this.sortFunc) 555 { 556 datasource.sort(this.sortFunc); 557 } 558 559 var i, len; 560 for (i = 0, len = datasource.length; i < len; i++) 561 { 562 var part = datasource[i]; 563 564 this.applyUid(part); 565 566 var key = part[this.indexKey]; 567 568 this.toLogger("create nodeinfo for key ",key,this.indexKey); 569 if (!this.nodeData[key]) 570 { 571 this.nodeData[key] = []; 572 } 573 574 //if function is supplied 575 if (typeof(func) === 'function') 576 { 577 func(part); 578 } 579 580 if (!this.nodeData[key].addedChildren) 581 { 582 this.nodeData[key].addedChildren = []; 583 } 584 585 if (parentIndex !== null && parentIndex !== undefined) 586 { 587 currentIndex = parentIndex+"-"+i; 588 this.nodeData[key].hasParent = true; 589 590 if (parentIndex.toString().split('-').length === 1) 591 { 592 this.nodeData[key].parentIsRoot = true; 593 } 594 } 595 else 596 { 597 currentIndex = i; 598 this.nodeData[key].hasParent = false; 599 } 600 601 this.keyDataIndex[part[this.indexKey]] = part; 602 603 //this temp var is set when we have deleted an item 604 //we need to reindex the index holder mapping. thats why we saved the old 605 //scenario to keep a reference 606 if (this.tempIndexHolderMap) 607 { 608 var oldHolderIndex = this.tempIndexHolderMap[this.nodeData[key].index]; 609 610 if (oldHolderIndex) 611 { 612 this.indexHolderMap[currentIndex] = oldHolderIndex; 613 } 614 } 615 616 this.indexOrder.push(currentIndex); 617 this.indexDataMap[currentIndex] = part; 618 619 this.dataKeyIndex[part[this.indexKey]] = currentIndex; 620 621 this.nodeData[key].index = currentIndex; 622 623 if (part[this.childKey] && part[this.childKey].length) 624 { 625 this.toLogger(key,"has childs on",this.childKey," default open = ",this.defaultOpen); 626 this.nodeData[key].hasChilds = true; 627 this.nodeData[key].childProperty = this.childKey; 628 629 if (!this.nodeData[key].hasParent || (this.nodeData[key].open || (this.nodeData[key].open == undefined && this.defaultOpen))) 630 { 631 this.createNodeInfo(part[this.childKey],currentIndex,func); 632 } 633 } 634 else 635 { 636 delete this.nodeData[key].childProperty; 637 this.nodeData[key].hasChilds = false; 638 } 639 } 640 }, 641 642 /** 643 * create nodes and child nodes if they exist. 644 * 645 * @param {Array} datasource nodes 646 * @param {String} parentIndex 647 * 648 * @ignore 649 */ 650 createNodes : function(datasource,parentIndex) 651 { 652 ///we are starting at root 653 if (!datasource) 654 { 655 datasource = this.datasource; 656 parentIndex = null; 657 } 658 659 var i, len; 660 for (i =0, len = datasource.length; i < len; i++) 661 { 662 var ds = datasource[i]; 663 var key = ds[this.indexKey]; 664 var nodeData = this.nodeData[key]; 665 666 this.toLogger("create node from ",key,ds,"with child property ",nodeData); 667 if (typeof(ds) === 'function') 668 { 669 continue; 670 } 671 672 this.createItemRenderByIndex(nodeData.index); 673 674 if (nodeData && typeof(ds[nodeData.childProperty]) === 'object' && ds[nodeData.childProperty].length) 675 { 676 if (nodeData.open) 677 { 678 this.createNodes(ds[nodeData.childProperty],currentIndex); 679 } 680 } 681 this.determineLoadNextButton(nodeData.index); 682 } 683 684 this.toLogger("create node complete"); 685 }, 686 687 /** 688 * @param {Object} item 689 */ 690 addItem : function(item) 691 { 692 //if no datasource exist we create a dummy where we insert the item 693 if (!this.datasource || !this.datasource.length) 694 { 695 this.setupIndexing(); 696 this.toLogger("add item as dummy ",this.indexKey,item,this.datasource); 697 var dummynode = {}; 698 dummynode[this.childKey] = [item]; 699 dummynode.nodeinfo = {}; 700 dummynode.nodeinfo.childProperty = this.childKey; 701 dummynode.nodeinfo.open = true; 702 dummynode.hide = false; 703 dummynode[this.indexKey] = "root"; 704 705 this.datagrid.setDataSource([dummynode]); 706 } 707 else 708 { 709 source = this.datasource[this.datasource.length-1]; 710 this.addDataSource(source,item,true); 711 } 712 }, 713 714 /** 715 * 716 */ 717 removeSelectedItems : function() 718 { 719 this._super(); 720 if (!this.datasource 721 || !this.datasource.length 722 || !this.datasource[0][this.childKey] 723 || !this.datasource[0][this.childKey].length) 724 { 725 this.datagrid.setDataSource([]); 726 } 727 }, 728 729 /** 730 * 731 * How are we adding new data? 732 * Every node in our tree got a nodeinfo object with params. The following params are 733 * important for this function 734 * 735 * nodeinfo.addedChildren -> children added which are not in the regular child collection 736 * nodeinfo.childCount -> the initial child collection child. this is without the nodeinfo.addedChildren 737 * 738 * If we add new data with the instant boolean to false we add the children 739 * to the nodeinfo.addedChildren array. When we receive new data later we check if 740 * children exists in the addedChildren array. If yes we remove it from addedChildren 741 * and increment the nodeinfo.childcount. This is useful to determine paging/loadnext buttons 742 * If the instant boolean is true we just add the children to the regular children 743 * 744 * Note: children are always compared to children in nodeinfo.addedChildren. 745 * 746 * use the instant boolean when you know for sure that the addedDatasource consists out of new items 747 * use false if there might be children which are already in the list 748 * 749 * @param {mixed} source to add items on 750 * @param {Array} targetItems 751 * @param {boolean} instant when true we instantly render the new items 752 */ 753 addDataSource : function(source,targetItems,instant) 754 { 755 try 756 { 757 if (!source && (!this.datasource || !this.datasource.length)) 758 { 759 this.createRootNode(); 760 return this.addDataSource(null,targetItems,instant); 761 } 762 else if (!source) 763 { 764 source = this.datasource[0]; 765 this.toLogger("addDatasource no source, pick",source) 766 } 767 768 if (!source[this.childKey]) 769 { 770 source[this.childKey] = []; 771 } 772 773 if (!(targetItems instanceof Array)) 774 { 775 targetItems = [targetItems]; 776 } 777 778 var nodeData = this.getNodeDataByData(source); 779 var childsToAdd = 0; 780 var offset= source[this.childKey].length-nodeData.addedChildren.length-1; 781 782 783 var i, len; 784 for (i = 0, len = targetItems.length; i< len;i++) 785 { 786 var target = targetItems[i]; 787 788 this.toLogger("addDataSource",target,this.indexKey); 789 790 var index = this.keyDataIndex[target[this.indexKey]]; 791 792 this.toLogger("addDataSource",'use index',index); 793 794 if (index !== undefined) 795 { 796 this.toLogger('found existing remove from added, and add childcount'); 797 var childFoundIndex = nodeData.addedChildren.indexOf(index); 798 799 if (childFoundIndex >=0) 800 { 801 nodeData.addedChildren.splice(nodeData.addedChildren.indexOf(childFoundIndex,1)); 802 childsToAdd++; 803 continue; 804 } 805 } 806 807 if (!instant) 808 { 809 nodeData.addedChildren.push(target); 810 } 811 812 if (this.sortFunc && source[this.childKey][offset]) 813 { 814 var sortResult = this.sortFunc(target,source[this.childKey][offset]); 815 this.toLogger('sort result of '+target.name+' and '+source[this.childKey][offset].name+' ',sortResult); 816 if (sortResult < 0) 817 { 818 this.toLogger('remove '+target.name+' from addedChildren, we are before the last one'); 819 nodeData.addedChildren.splice(nodeData.addedChildren.indexOf(target),1); 820 childsToAdd++; 821 } 822 } 823 824 source[this.childKey].push(target); 825 } 826 827 //nodeData.childCount+=childsToAdd; 828 829 this.toLogger('current childcount prop',nodeData.childCount,"children",source[this.childKey].length); 830 831 this.prepareSourceToRender(source); 832 } 833 catch(e) 834 { 835 console.log(e) 836 } 837 }, 838 839 /** 840 * Creates a root node 841 * @param {mixed} source 842 * @ignore 843 */ 844 createRootNode : function(source) 845 { 846 var root = {}; 847 root[this.childKey]= []; 848 root.nodeinfo = []; 849 root.open = true; 850 851 if (!this.datasource) 852 { 853 this.datasource = []; 854 } 855 856 this.datasource.push(root); 857 858 this.triggerEvent('dataSourceChanged'); 859 this.setDataSource(this.datasource); 860 }, 861 862 /** 863 * @param {Object} source 864 * @ignore 865 */ 866 prepareSourceToRender : function(source) 867 { 868 this.toLogger("PrepareSourceToRender",source); 869 this.indexOrder = []; 870 this.keyDataIndex = []; 871 var nodeData = this.getNodeDataByData(source); 872 873 //set new info on datasource, mainly to reindex the tree TODO not neccessarly needed from root 874 this.createNodeInfo(this.datasource,null,this.getProxy(function(item){ 875 876 var itemNodeData = this.getNodeDataByData(item); 877 this.toLogger("PrepareSourceToRender callback",item); 878 if (this.defaultOpen && itemNodeData.open === undefined) 879 { 880 itemNodeData.open = true; 881 } 882 })); 883 884 this.indexHolderMap[nodeData.index].childsHolder.clear(); 885 886 if (nodeData.index != 0) 887 { 888 this.createToggleControl(nodeData.index); 889 } 890 891 //create the new nodes for the children 892 //note that this.createNodeInfo() automaticly index the new nodes 893 this.createNodes(source[this.childKey],nodeData.index); 894 895 //remove the loader if there is one 896 this.hideLoaderInNode(nodeData.index); 897 898 //remove holder button. we might add it later again 899 this.indexHolderMap[nodeData.index].buttonHolder.clear(); 900 901 //add loader button 902 this.determineLoadNextButton(nodeData.index); 903 904 //rerender source with new nodes 905 this.indexHolderMap[nodeData.index].invalidateDisplay(); 906 }, 907 908 /** 909 * removes all data from item and children recursivly 910 * note this function does not modify gui objects 911 * 912 * @param {Array} item 913 */ 914 removeChildDataRecursivly : function(item) 915 { 916 if (!item) 917 { 918 return; //this could happen if root and sub node are both selected. root already deleted sub 919 } 920 921 var nodeData = this.getNodeDataByData(item); 922 923 if (!nodeData.removedItems) 924 { 925 nodeData.removedItems = []; 926 } 927 928 if (item[this.childKey] && item[this.childKey].length) 929 { 930 var i, len; 931 for (i=0, len = item[this.childKey].length; i< len; i++) 932 { 933 var child = item[this.childKey][i]; 934 935 nodeData.removedItems.push(child); 936 937 this.removeChildDataRecursivly(child); 938 item[this.childKey]= []; 939 } 940 } 941 942 item[this.childKey]= []; 943 }, 944 945 /** 946 * removes node by key 947 * key is the {object}[this.indexKey]; 948 * 949 * @param {String} key 950 */ 951 removeNodeByKey : function(key) 952 { 953 var item = this.keyDataIndex[key]; 954 955 if (item) 956 { 957 this.removeItem(item); 958 } 959 }, 960 961 /** 962 * 963 * @param {Object} item 964 */ 965 removeItem : function(item) 966 { 967 this._super(item); 968 969 var nodeData= this.getNodeDataByData(item); 970 //remove all children 971 this.removeChildDataRecursivly(item); 972 973 //remove node from parent 974 var parentIndex = this.getParentIndex(nodeData.index); 975 976 var parentData = this.getDataSourceByIndex(parentIndex); 977 978 if (parentData && parentData[this.childKey]) 979 { 980 var childIndex = parentData[this.childKey].indexOf(item) ; 981 982 if (childIndex !== -1) 983 { 984 parentData[this.childKey].splice(childIndex,1); 985 986 var parentNodeData= this.getNodeDataByData(parentData); 987 988 var addedChildIndex = parentNodeData.addedChildren.indexOf(item); 989 if (addedChildIndex >= 0) 990 { 991 parentNodeData.addedChildren.splice(addedChildIndex,1); 992 } 993 994 parentNodeData.childCount--; 995 } 996 } 997 998 //remove gui node 999 this.indexHolderMap[nodeData.index].remove(); 1000 1001 //reset some mappings 1002 this.indexOrder = []; 1003 this.keyDataIndex = []; 1004 1005 //our generate nodeinfo should reindex the indexholder map. 1006 this.tempIndexHolderMap = Banana.Util.Clone(this.indexHolderMap); 1007 this.createNodeInfo(this.datasource); 1008 this.tempIndexHolderMap = null; 1009 1010 if(parentData && !parentData[this.childKey].length) 1011 { 1012 this.removeToggleControl(parentNodeData.index,true); 1013 this.indexHolderMap[parentNodeData.index].invalidateDisplay(); 1014 } 1015 }, 1016 1017 /** 1018 * shows a loader inside a node 1019 * 1020 * @param {int} index 1021 */ 1022 showLoaderInNode : function(index) 1023 { 1024 this.showingLoader = true; 1025 var loader = new Banana.Controls.Panel(); 1026 loader.setId("loader-"+index); 1027 loader.addCssClass("BDataGridTreeLoader"); 1028 this.indexHolderMap[index].addControl(loader); 1029 this.indexHolderMap[index].invalidateDisplay(); 1030 }, 1031 1032 /** 1033 * hides the loader 1034 * also sets this.showingLoader to false; 1035 * 1036 * @param {int} index 1037 */ 1038 hideLoaderInNode : function(index) 1039 { 1040 this.showingLoader = false; 1041 var loaderControl = this.indexHolderMap[index].findControl('loader-'+index); 1042 if (loaderControl) 1043 { 1044 loaderControl.remove(); 1045 } 1046 }, 1047 1048 /** 1049 * @param {int} index 1050 * @param {Object} datasource 1051 */ 1052 openNode : function(index,datasource) 1053 { 1054 var nodeData = this.nodeData[datasource[this.indexKey]]; 1055 1056 //set new info on datasource, mainly to reindex the tree TODO not neccessarly needed from root 1057 this.createNodeInfo(this.datasource); 1058 1059 if (this.showingLoader) 1060 { 1061 return; 1062 } 1063 1064 if (nodeData.needsChilds) 1065 { 1066 var params = {}; 1067 params.targetIndex = index; 1068 params.datasource = datasource; 1069 params.limit = this.childRequestSizeLimit; 1070 params.offset = 0; 1071 1072 this.triggerEvent("onRequestChilds",params); 1073 } 1074 else 1075 { 1076 this.createNodes(datasource[this.childKey],index); 1077 this.determineLoadNextButton(index); 1078 this.indexHolderMap[index].invalidateDisplay(); 1079 } 1080 1081 //clear the indexorder 1082 this.indexOrder = []; 1083 this.keyDataIndex = []; 1084 1085 this.triggerEvent("onItemOpened"); 1086 }, 1087 1088 /** 1089 * @param {int} index 1090 * @param {Object} datasource 1091 */ 1092 closeNode : function(index,datasource) 1093 { 1094 this.indexHolderMap[index].childsHolder.clear(); 1095 this.indexHolderMap[index].buttonHolder.clear(); 1096 1097 //clear the indexorder 1098 this.indexOrder = []; 1099 this.keyDataIndex = []; 1100 1101 //set new info on datasource, mainly to reindex the tree TODO not neccessarly needed from root 1102 this.createNodeInfo(this.datasource); 1103 1104 this.triggerEvent("onItemClosed"); 1105 }, 1106 1107 /** 1108 * invoked when node is clicked 1109 * we open the node or close it 1110 * if in nodeinfo the property needschilds is true we trigger an event to let 1111 * the user load another childs 1112 * 1113 * @param {index} int 1114 * @param {boolean} true when node is opened 1115 */ 1116 nodeClicked : function(index,open) 1117 { 1118 if (this.showingLoader) 1119 { 1120 return; 1121 } 1122 1123 var datasource = this.getDataSourceByIndex(index); 1124 var nodeData = this.nodeData[datasource[this.indexKey]]; 1125 1126 nodeData.open = open ? true : false; 1127 1128 if (nodeData.open) 1129 { 1130 this.openNode(index,datasource); 1131 } 1132 else 1133 { 1134 this.closeNode(index,datasource); 1135 } 1136 }, 1137 1138 /** 1139 * determines if a load next button is needed 1140 * we also keep in count of deleted items. 1141 * 1142 * @ignore 1143 */ 1144 determineLoadNextButton : function(index) 1145 { 1146 var source = this.getDataSourceByIndex(index); 1147 var nodeData = this.getNodeDataByData(source); 1148 1149 this.toLogger("determine load next button ",this.indexKey); 1150 1151 if (!nodeData) 1152 { 1153 return; 1154 } 1155 1156 if (source[this.childKey] && nodeData.open) 1157 { 1158 var serverChildCount = nodeData.childCount; 1159 var clientChildCount = source[this.childKey].length; 1160 var addedChildCount = nodeData.addedChildren.length; 1161 1162 this.toLogger('server',serverChildCount,'client',clientChildCount,'added',addedChildCount); 1163 1164 var offset = clientChildCount - addedChildCount; 1165 1166 if (offset < serverChildCount) 1167 { 1168 var limit = serverChildCount - offset; 1169 1170 if (limit > this.childRequestSizeLimit) 1171 { 1172 limit = this.childRequestSizeLimit; 1173 } 1174 1175 nodeData.loadnext = true; 1176 1177 // var debug = "serverCount "+serverChildCount + " clientChilds"+clientChildCount+ " addedChildCount"+addedChildCount+ "===>"+offset+","+limit; 1178 1179 return this.createLoadMoreButton(index,limit,nodeData); 1180 } 1181 } 1182 1183 nodeData.loadnext = false; 1184 }, 1185 1186 /** 1187 * creates item render by index 1188 * 1189 * Item render will be either from 1190 * - data to item render map 1191 * - index to item render map 1192 * - or default one 1193 * 1194 * @ignore 1195 */ 1196 createItemRenderByIndex : function(index,instantRerender,useExistingHolder) 1197 { 1198 var datasource = this.getDataSourceByIndex(index); 1199 var nodeData = this.nodeData[datasource[this.indexKey]]; 1200 1201 this.toLogger("create item render by index ",index,datasource); 1202 1203 this.triggerEvent('onPreCreateItem',{index:index,data:datasource}); 1204 1205 //this situation is only possible when user quickly closes and opens a node while loading server data 1206 if (!nodeData) 1207 { 1208 this.toLogger("no nodeinfo. dont render"); 1209 return; 1210 } 1211 1212 // Optionally can be accessed by e.g. invalidateDisplay to peek 1213 // at which index it is created 1214 this.currentIndexCreation = index; 1215 1216 var itemRenderFactory = null; 1217 1218 var key = datasource[this.indexKey]; 1219 1220 if (key && this.dataItemRenderMap.getItem(key)) 1221 { 1222 itemRenderFactory = this.dataItemRenderMap.getItem(key); 1223 } 1224 else if (this.indexItemRenderFactory[index]) 1225 { 1226 itemRenderFactory = this.indexItemRenderFactory[index]; 1227 } 1228 else 1229 { 1230 itemRenderFactory = this.defaultContentItemRender; 1231 } 1232 1233 var itemRender = this.getObject(itemRenderFactory); 1234 1235 itemRender.setData(datasource); 1236 1237 itemRender.bind('dataChanged',this.getProxy(function(e,f){ 1238 1239 this.getDataSource()[parseInt(e.data, 10)] = e.currentTarget.getData(); 1240 this.triggerEvent('dataSourceChanged'); 1241 1242 }),index.toString()); 1243 1244 //save mapping between itemRender and data 1245 this.dataItemRenderMap.addItem(key,itemRenderFactory); 1246 1247 //save mapping between index and item render 1248 this.indexRenderedItemRenderMap[index] = itemRender; 1249 1250 itemRender.setListRender(this); 1251 1252 var parentHolder = this.getParentHolder(index); 1253 var holder = null; 1254 1255 //if we use existing holder we need to clear the item render inside 1256 if (useExistingHolder) 1257 { 1258 holder = this.getHolder(index); 1259 holder.getItemRenderHolder().clear(); 1260 } 1261 else 1262 { 1263 if (!parentHolder) 1264 { 1265 this.rootHolder = new Banana.Controls.DataGridTreeListRootHolder(); 1266 this.addControl(this.rootHolder); 1267 holder = this.rootHolder; 1268 } 1269 else 1270 { 1271 holder = this.getHolder(index); 1272 //create a holder to put our itemrender in 1273 var holder = new Banana.Controls.DataGridTreeListHolder(); 1274 } 1275 } 1276 1277 //give a reference of this to the holder 1278 holder.listRender = this; 1279 holder.maximizeAutoWidth = this.maximizeNodeWidth; 1280 //set an unique id for which we later check if index map is correct 1281 holder.setId(key); 1282 //save mapping between index and holder 1283 this.indexHolderMap[index] = holder; 1284 1285 //look for a parent holder. 1286 var parentHolder = this.getParentHolder(index); 1287 1288 if (parentHolder) 1289 { 1290 if (!nodeData.parentIsRoot) 1291 { 1292 holder.addCssClass("BDataGridTreeItemHolder"); 1293 } 1294 else 1295 { 1296 holder.addCssClass("BDataGridTreeItemHolderRoot"); 1297 } 1298 1299 //if we use existing holder we dont need to add it 1300 if (!useExistingHolder) 1301 { 1302 parentHolder.childsHolder.addControl(holder); 1303 } 1304 1305 holder.itemRenderHolder.addControl(itemRender); 1306 1307 if (nodeData.childProperty || nodeData.needsChilds) 1308 { 1309 this.createToggleControl(index); 1310 } 1311 else if (parentHolder) 1312 { 1313 this.removeToggleControl(index); 1314 } 1315 1316 if (this.selectedIndices.getItem(index)) 1317 { 1318 this.selectIndex(index); 1319 } 1320 1321 this.bindMouseHandler(holder, datasource); 1322 this.triggerEvent('onPostCreateIndex',{index:index,data:datasource,holder:holder}); 1323 } 1324 1325 this.currentIndexCreation = undefined; 1326 1327 if (instantRerender) 1328 { 1329 if (parentHolder) 1330 { 1331 holder.invalidateDisplay(); 1332 } 1333 else 1334 { 1335 this.rootHolder.invalidateDisplay(); 1336 } 1337 } 1338 }, 1339 1340 /** 1341 * binds a click handler overwrite this function to use your own logic for binding click 1342 * 1343 * @param {Banana.Controls.DataGridTreeListHolder} holder 1344 * @param {Object} datasource item 1345 * 1346 * @ignore 1347 */ 1348 bindMouseHandler : function(holder,datasource) 1349 { 1350 holder.itemRenderHolder.bind('click',this.getProxy(function(e){ 1351 1352 this.onRowMouseClick(e); 1353 this.triggerEvent('onItemMouseClick',e); 1354 1355 }),datasource); 1356 1357 holder.itemRenderHolder.bind('mouseenter',this.getProxy(function(e){ 1358 1359 this.triggerEvent('onItemMouseEnter',e); 1360 1361 }),datasource); 1362 1363 holder.itemRenderHolder.bind('mouseleave',this.getProxy(function(e){ 1364 1365 this.triggerEvent('onItemMouseOut',e); 1366 1367 }),datasource); 1368 }, 1369 1370 /** 1371 * @param {int} index 1372 * @param {Boolean} rerender 1373 * 1374 * @ignore 1375 */ 1376 createToggleControl : function(index,rerender) 1377 { 1378 var datasource = this.getDataSourceByIndex(index); 1379 var nodeData = this.nodeData[datasource[this.indexKey]]; 1380 1381 //dont add when root and root should not be visible 1382 // if (!this.rootNodeVisible && !nodeData.hasParent) 1383 // { 1384 // // return; 1385 // } 1386 1387 var holder =this.indexHolderMap[index]; 1388 1389 holder.toggleHolder.clear(); 1390 1391 var toggle = new Banana.Controls.DataGridTreeListNodeToggle().addCssClass('BDataGridTreeToggle'); 1392 toggle.index = index; 1393 1394 toggle.setOn(nodeData.open); 1395 1396 toggle.bind('toggle',this.getProxy(function(e,d){ 1397 1398 var index = this.getIndexByDataSource(e.data); 1399 1400 this.nodeClicked(index,d); 1401 1402 }),datasource); 1403 holder.toggleHolder.addControl(toggle); 1404 1405 if (rerender) 1406 { 1407 holder.toggleHolder.invalidateDisplay(); 1408 } 1409 }, 1410 1411 /** 1412 * @param {int} index 1413 * @param {Boolean} rerender 1414 * 1415 */ 1416 removeToggleControl : function(index,rerender) 1417 { 1418 var holder =this.indexHolderMap[index]; 1419 var datasource = this.getDataSourceByIndex(index); 1420 var nodeData = this.getNodeDataByData(datasource); 1421 1422 holder.toggleHolder.clear(); 1423 1424 //dont add dummy when root and root should not be visible 1425 if (!this.rootNodeVisible && !nodeData.hasParent) 1426 { 1427 return; 1428 } 1429 1430 var dummy = new Banana.Controls.Panel().addCssClass('BDataGridTreeEmptyNode'); 1431 holder.toggleHolder.addControl(dummy); 1432 1433 if (rerender) 1434 { 1435 holder.toggleHolder.invalidateDisplay(); 1436 } 1437 }, 1438 1439 /** 1440 * creates a load more button 1441 * 1442 * @param {String} index 1443 * @param {int} limit 1444 */ 1445 createLoadMoreButton : function(index,limit,nodeData) 1446 { 1447 var holder =this.indexHolderMap[index]; 1448 1449 var button = new Banana.Controls.Button(); 1450 1451 if (index != 0) 1452 { 1453 button.addCssClass("BDataGridTreeLoadNextButton"); 1454 } 1455 else 1456 { 1457 button.addCssClass("BDataGridTreeLoadNextButtonRoot"); 1458 } 1459 1460 var datasource = this.getDataSourceByIndex(index); 1461 var nodeData = this.getNodeDataByData(datasource); 1462 1463 var totalrendered = nodeData.childCount - datasource.children.length; 1464 1465 button.setText('Load next '+limit+ '/'+totalrendered); 1466 1467 button.bind('click',this.getProxy(function(){ 1468 1469 if (this.showingLoader) 1470 { 1471 return; 1472 } 1473 1474 var datasource = this.getDataSourceByIndex(index); 1475 1476 var offset = 0; 1477 1478 var info = datasource[this.childKey].length - nodeData.addedChildren.length; 1479 1480 if (info) 1481 { 1482 offset =info; 1483 } 1484 1485 var params = {}; 1486 params.targetIndex = index; 1487 params.datasource = datasource; 1488 params.limit = limit; 1489 params.offset = offset; 1490 1491 this.triggerEvent("onRequestChilds",params); 1492 1493 })); 1494 1495 holder.buttonHolder.addControl(button); 1496 holder.buttonHolder.addControl('<div style="clear:both"></div>'); 1497 }, 1498 1499 /** 1500 * calls the select method in the item render 1501 * 1502 * @param {int} index 1503 */ 1504 selectIndex : function(index,preventEvent) 1505 { 1506 var ir = this.getRenderedItemRenderByIndex(index); 1507 1508 if (!ir) 1509 { 1510 return; 1511 } 1512 1513 ir.select(); 1514 1515 if (!preventEvent) 1516 { 1517 this.triggerEvent('onSelectIndex',index); 1518 } 1519 }, 1520 1521 /** 1522 * calls the deselect method in the item render 1523 * 1524 * @param {int} index 1525 */ 1526 deSelectIndex : function(index) 1527 { 1528 var ir = this.getRenderedItemRenderByIndex(index); 1529 1530 if (!ir) 1531 { 1532 return; 1533 } 1534 1535 ir.deselect(); 1536 1537 this.triggerEvent('onDeselectIndex',index); 1538 }, 1539 1540 /** 1541 * Selects all indices from given index. 1542 * @param {String} index 1543 * @param {mixed} datasource for internal use only 1544 */ 1545 selectAllFromIndex : function(index,datasource) 1546 { 1547 if (!datasource) 1548 { 1549 datasource = this.getDataSourceByIndex(index); 1550 } 1551 1552 if (!datasource) 1553 { 1554 return; 1555 } 1556 1557 var nodeData = this.getNodeDataByData(datasource); 1558 1559 if (!nodeData) 1560 { 1561 return; 1562 } 1563 var i, len; 1564 for (i =0, len = datasource[this.childKey].length; i < len; i++) 1565 { 1566 var item = datasource[this.childKey][i]; 1567 index = nodeData.index; 1568 1569 var ir = this.getRenderedItemRenderByIndex(i); 1570 1571 if (ir && !ir.getIsSelectable()) 1572 { 1573 1574 } 1575 else 1576 { 1577 this.selectedIndices.addItem(index,true); 1578 1579 this.selectIndex(index,true); 1580 1581 if (item[this.childKey]) 1582 { 1583 this.selectAllFromIndex(index,item); 1584 } 1585 } 1586 } 1587 }, 1588 1589 /** 1590 * @return {Boolean} 1591 * @ignore 1592 */ 1593 indexBeforeIndex : function(indexA,indexB) 1594 { 1595 var i; 1596 for (i = 0; i < this.indexOrder.length;i++) 1597 { 1598 if (this.indexOrder[i] === indexA) 1599 { 1600 return true; 1601 } 1602 1603 if (this.indexOrder[i]===indexB) 1604 { 1605 return false; 1606 } 1607 } 1608 1609 return false; 1610 }, 1611 1612 /** 1613 * Gets the rendered item render instance by index 1614 * @return {Object} 1615 */ 1616 getRenderedItemRenderByIndex : function(index) 1617 { 1618 var holder = this.indexHolderMap[index]; 1619 1620 if (!holder) 1621 { 1622 return null; 1623 } 1624 1625 return holder.getItemRender(); 1626 }, 1627 1628 1629 /** 1630 * triggered when use moves outside 1631 * if row is unselected we trigger event 'onItemSelect' 1632 * 1633 * @param {event} e 1634 * @ignore 1635 */ 1636 onRowMouseClick : function(e) 1637 { 1638 var index = this.getNodeDataByData(e.data).index; 1639 1640 var itemRender = this.getRenderedItemRenderByIndex(index); 1641 1642 if (!itemRender.getIsSelectable()) 1643 { 1644 return; 1645 } 1646 1647 var ctrlKey = e.ctrlKey; 1648 var shiftKey = e.shiftKey; 1649 1650 if (this.getIndexIsSelected(index)) 1651 { 1652 if (!ctrlKey && !shiftKey) 1653 { 1654 this.previousShiftFirstItem = null; 1655 this.clearSelectedIndices(); 1656 } 1657 else 1658 { 1659 this.previousShiftFirstItem = null; 1660 this.clearSelectedIndex(index); 1661 } 1662 } 1663 else 1664 { 1665 if (this.selectionType === 'single') 1666 { 1667 this.clearSelectedIndices(); 1668 this.addSelectedIndex(index); 1669 } 1670 else 1671 { 1672 if (!ctrlKey && !shiftKey) 1673 { 1674 this.previousShiftFirstItem = null; 1675 this.clearSelectedIndices(); 1676 this.addSelectedIndex(index); 1677 } 1678 if (ctrlKey) 1679 { 1680 this.previousShiftFirstItem = null; 1681 this.addSelectedIndex(index); 1682 } 1683 if (shiftKey) 1684 { 1685 //use our previous shift item 1686 if (!this.previousShiftFirstItem) 1687 { 1688 this.previousShiftFirstItem = this.selectedIndices.getKeyByIndex(0); 1689 } 1690 1691 var firstItem = this.previousShiftFirstItem; 1692 1693 if (this.indexOrder.indexOf(firstItem) === -1) 1694 { 1695 firstItem = this.indexOrder[0]; 1696 } 1697 1698 if (!firstItem) 1699 { 1700 return; 1701 } 1702 1703 //reverse it when selected is above first 1704 if (this.indexBeforeIndex(index,firstItem)) 1705 { 1706 var temp = index; 1707 index = firstItem; 1708 firstItem = temp; 1709 } 1710 1711 var go = false; 1712 1713 this.clearSelectedIndices(); 1714 1715 var x, len; 1716 for (x = 0, len = this.indexOrder.length; x < len; x++) 1717 { 1718 if (typeof(this.indexOrder[x]) === 'function') 1719 { 1720 continue; 1721 } 1722 1723 if (this.indexOrder[x] === firstItem) 1724 { 1725 go = true; 1726 } 1727 1728 if (!go) 1729 { 1730 continue; 1731 } 1732 1733 if (this.indexOrder[x]===index) 1734 { 1735 go = false; 1736 } 1737 1738 var ir = this.getRenderedItemRenderByIndex(this.indexOrder[x]); 1739 1740 if (ir && !ir.getIsSelectable()) 1741 { 1742 1743 } 1744 else 1745 { 1746 this.addSelectedIndex(this.indexOrder[x]); 1747 } 1748 } 1749 } 1750 1751 } 1752 } 1753 this.triggerEvent('onItemSelect'); 1754 } 1755 });