Changeset 12309

Show
Ignore:
Timestamp:
02/07/08 08:42:43 (11 months ago)
Author:
bill
Message:

- Fix DnD for Tree, specifically when dragging elements within the Tree

- Refactor Tree to take a single item as the root node, rather than a set of items specified by a query. The root node of the tree can be hidden via the showRoot parameter. Query and label parameters deprecated; will be removed in v2.

- implemented ForestStoreDecorator? which wraps a given store, mapping all items matching a given query as children of a synthesized root item. Use this to connect Tree to stores that don't have a single root item.

Reason for changes: The old tree design worked for simple cases but broke down for DnD. Refactoring let's users define handlers for when a node is added/removed from the top of the tree (ie, when it changes between a top level item and a child of another item, or vice-versa), and for efficiently detecting changes to the list/order of top level items (and then notifying Tree).

Note that Tree DnD won't work with ItemFileWriteStore? and nested data (rather than _reference). See #5786 about that.

Fixes #4544, #4977, #5560, refs #5536

!strict

Location:
dijit/trunk
Files:
1 added
5 modified

Legend:

Unmodified
Added
Removed
  • dijit/trunk/tests/tree/test_Tree_DnD.html

    r12020 r12309  
    8383                //create a custom label for tree one consisting of the label property pluss the value of the numberOfItems Column 
    8484                function tree1CustomLabel(item){ 
    85                         var label = catStore.getLabel(item);  
    86                         var num = catStore.getValues(item,"numberOfItems"); 
    87                         //return the new label 
     85                        var label = catStore.getLabel(item); 
     86                        var num = catStore.hasAttribute(item, "numberOfItems") ? catStore.getValues(item,"numberOfItems") : "?"; 
    8887                        return label + ' (' + num+ ')'; 
    8988                } 
    9089 
    91                 //on tree two, we only want to drop on containers, not on items in the containers 
     90                //on tree two, we only want to drop on containers, or the root node itself, not on items in the containers 
    9291                function tree2CheckItemAcceptance(node,source) { 
    9392                        var item = dijit.getEnclosingWidget(node).item; 
    94                         if (item && this.tree.store.hasAttribute(item,"numberOfItems")){ 
    95                                 var numItems=this.tree.store.getValues(item, "numberOfItems"); 
     93                        if (item && (item.root || this.tree.store.hasAttribute(item,"numberOfItems"))){ 
    9694                                return true; 
    9795                        } 
  • dijit/trunk/Tree.js

    r12273 r12309  
    77dojo.require("dijit._Container"); 
    88dojo.require("dojo.cookie"); 
     9 
     10// Maps a forest of top level item into a single (fake) top level item 
     11// with the other items as children of the fake root. 
     12dojo.require("dijit._tree.ForestStoreDecorator"); 
    913 
    1014dojo.declare( 
     
    4145                this.setLabelNode(this.label); 
    4246 
    43                 if(this.parent || !this._hideRoot){ 
    44                         // set expand icon for leaf 
    45                         this._setExpando(); 
    46  
    47                         // set icon and label class based on item 
    48                         this._updateItemClasses(this.item); 
    49                 } 
     47                // set expand icon for leaf 
     48                this._setExpando(); 
     49 
     50                // set icon and label class based on item 
     51                this._updateItemClasses(this.item); 
    5052 
    5153                if(this.isExpandable){ 
     
    6668 
    6769        _updateItemClasses: function(item){ 
    68                 // summary: set appropriate CSS classes for item (used to allow for item updates to change respective CSS) 
    69                 this.iconNode.className = "dijitInline dijitTreeIcon " + this.tree.getIconClass(item, this.isExpanded); 
    70                 this.labelNode.className = "dijitTreeLabel " + this.tree.getLabelClass(item, this.isExpanded); 
    71         }, 
    72          
     70                // summary: set appropriate CSS classes for icon and label dom node (used to allow for item updates to change respective CSS) 
     71                var tree = this.tree, store = tree.store; 
     72                if(tree._v10Compat && item === store.root){ 
     73                        // For back-compat with 1.0, need to use null to specify root item (TODO: remove in 2.0) 
     74                        item = null; 
     75                } 
     76                this.iconNode.className = "dijitInline dijitTreeIcon " + tree.getIconClass(item, this.isExpanded); 
     77                this.labelNode.className = "dijitTreeLabel " + tree.getLabelClass(item, this.isExpanded); 
     78        }, 
     79 
    7380        _updateLayout: function(){ 
    7481                // summary: set appropriate CSS classes for this.domNode 
    7582                var parent = this.getParent(); 
    76                 if(parent && parent.isTree && parent._hideRoot){ 
     83                if(!parent || parent.rowNode.style.display == "none"){ 
    7784                        /* if we are hiding the root node then make every first level child look like a root node */ 
    7885                        dojo.addClass(this.domNode, "dijitTreeIsRoot"); 
     
    206213                } 
    207214 
    208                 if(this.isTree && this._hideRoot){ 
    209                         // put first child in tab index if one exists. 
    210                         var fc = this.getChildren()[0]; 
    211                         var tabnode = fc ? fc.labelNode : this.domNode; 
     215                // On initial tree show, put focus on either the root node of the tree, 
     216                // or the first child, if the root node is hidden 
     217                // TODO: move to Tree.postCreate?  (but can't execute until root node and child nodes finish loading) 
     218                if(!this.parent){ 
     219                        var fc = this.tree.showRoot ? this : this.getChildren()[0], 
     220                                tabnode = fc ? fc.labelNode : this.domNode; 
    212221                        tabnode.setAttribute("tabIndex", "0"); 
    213222                } 
     
    245254dojo.declare( 
    246255        "dijit.Tree", 
    247         dijit._TreeNode, 
     256        [dijit._Widget, dijit._Templated], 
    248257{ 
    249258        // summary 
     
    261270        store: null, 
    262271 
     272        // root: String 
     273        //      id of item in store corresponding to root of tree 
     274        root: "", 
     275 
    263276        // query: String 
    264         //      query to get top level node(s) of tree (ex: {type:'continent'}) 
     277        //      Deprecated.  Use dijit._tree.ForestStoreDecorator directly instead. 
     278        //      Specifies a set of "top level" items for the tree, rather than just a single item. 
     279        //      If a label is also specified, the tree is given a fake root node (not corresponding to an item in 
     280        //      the data store), whose children are the items that match this query. 
     281        //  
     282        // example: 
     283        //              {type:'continent'} 
    265284        query: null, 
     285 
     286        // label: String 
     287        //      Deprecated.  Use dijit._tree.ForestStoreDecorator directly instead. 
     288        //      Used in conjunction with query parameter. 
     289        //      If a query is specified (rather than a root node id), and a label is also specified, 
     290        //      then a fake root node is created and displayed, with this label. 
     291        label: "", 
     292 
     293        // showRoot: Boolean 
     294        //      Should the root node be displayed, or hidden? 
     295        showRoot: true, 
    266296 
    267297        // childrenAttr: String 
     
    305335        postMixInProperties: function(){ 
    306336                this.tree = this; 
    307                 this.lastFocused = this.labelNode; 
    308337 
    309338                this._itemNodeMap={}; 
    310339 
    311                 this._hideRoot = !this.label; 
     340                if(!this.root){ 
     341                        // 1.0 compatible behavior. 
     342                        // Provide (possibly hidden) fake root node not corresponding to any data store item, 
     343                        // which fathers all the items returned by fetch({query: this.query}) 
     344                        dojo.deprecated("Tree: from version 2.0, must specify root item id (using root parameter) rather than query/label parameters when constructing a dijit.Tree; use dijit._tree.ForestStoreDecorator if your data store has no root item."); 
     345                        this._v10Compat = true; 
     346                        this.underlyingStore = this.store; 
     347                        this.root = "$root$"; 
     348                        this.store = new dijit._tree.ForestStoreDecorator({ 
     349                                id: this.id + "_ForestStoreDecorator", 
     350                                store: this.underlyingStore, 
     351                                query: this.query, 
     352                                childrenAttr: this.childrenAttr[0], 
     353                                rootId: this.root, 
     354                                rootLabel: this.label||"ROOT" 
     355                        }); 
     356 
     357                        // For backwards compatibility, the visibility of the root node is controlled by 
     358                        // whether or not the user has specified a label 
     359                        this.showRoot = Boolean(this.label); 
     360                } 
    312361 
    313362                if(!this.store.getFeatures()['dojo.data.api.Identity']){ 
     
    347396                this.containerNodeTemplate = div; 
    348397 
    349                 if(this._hideRoot){ 
    350                         this.rowNode.style.display="none"; 
    351                 } 
     398                // load root node (possibly hidden) and it's children 
     399                var _this = this; 
     400                this.store.fetchItemByIdentity({ 
     401                        identity: this.root, 
     402                        onItem: function(item){ 
     403                                _this.rootItem = item; 
     404 
     405                                var rn = _this.rootNode = new dijit._TreeNode({ 
     406                                        item: item, 
     407                                        tree: _this, 
     408                                        isExpandable: true, 
     409                                        label: _this.label || _this.getLabel(item) 
     410                                }); 
     411                                if(!_this.showRoot){ 
     412                                        rn.rowNode.style.display="none"; 
     413                                } 
     414                                _this.domNode.appendChild(rn.domNode); 
     415                                _this._itemNodeMap[_this.root] = rn; 
     416 
     417                                rn._updateLayout();             // sets "dijitTreeIsRoot" CSS classname 
     418 
     419                                // load top level children 
     420                                _this._expandNode(rn); 
     421                        } 
     422                }); 
    352423 
    353424                this.inherited("postCreate", arguments); 
    354  
    355                 // load top level children 
    356                 this._expandNode(this); 
    357425 
    358426                if(this.dndController){ 
     
    392460                //              User overridable function that return array of child items of given parent item, 
    393461                //              or if parentItem==null then return top items in tree 
     462                // TODO: 
     463                //      Remove this in 2.0 because: 
     464                //              - no way to override this function and support updates to the tree, 
     465                //              - if acceess of children is non-standard then user can write decorator store like dijit._tree.ForestStoreDecorator 
    394466                var store = this.store; 
    395                 if(parentItem == null){ 
    396                         // get top level nodes 
    397                         store.fetch({ query: this.query, onComplete: onComplete}); 
     467                parentItem = parentItem || this.store.root; 
     468 
     469                // get children of specified item 
     470                var childItems = []; 
     471                for (var i=0; i<this.childrenAttr.length; i++){ 
     472                        var vals = store.getValues(parentItem, this.childrenAttr[i]); 
     473                        childItems = childItems.concat(vals); 
     474                } 
     475 
     476                // count how many items need to be loaded 
     477                var _waitCount = 0; 
     478                dojo.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } }); 
     479 
     480                if(_waitCount == 0){ 
     481                        // all items are already loaded.  proceed... 
     482                        onComplete(childItems); 
    398483                }else{ 
    399                         // get children of specified node 
    400                         var childItems = []; 
    401                         for (var i=0; i<this.childrenAttr.length; i++){  
    402                                 childItems= childItems.concat(store.getValues(parentItem, this.childrenAttr[i])); 
    403                         } 
    404                         // count how many items need to be loaded 
    405                         var _waitCount = 0; 
    406                         dojo.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } }); 
    407  
    408                         if(_waitCount == 0){ 
    409                                 // all items are already loaded.  proceed.. 
    410                                 onComplete(childItems); 
    411                         }else{ 
    412                                 // still waiting for some or all of the items to load 
    413                                 var onItem = function onItem(item){ 
    414                                         if(--_waitCount == 0){ 
    415                                                 // all nodes have been loaded, send them to the tree 
    416                                                 onComplete(childItems); 
    417                                         } 
     484                        // still waiting for some or all of the items to load 
     485                        var onItem = function onItem(item){ 
     486                                if(--_waitCount == 0){ 
     487                                        // all nodes have been loaded, send them to the tree 
     488                                        onComplete(childItems); 
    418489                                } 
    419                                 dojo.forEach(childItems, function(item){ 
    420                                         if(!store.isItemLoaded(item)){ 
    421                                                 store.loadItem({ 
    422                                                         item: item, 
    423                                                         onItem: onItem 
    424                                                 }); 
    425                                         } 
    426                                 }); 
    427                         } 
     490                        } 
     491                        dojo.forEach(childItems, function(item){ 
     492                                if(!store.isItemLoaded(item)){ 
     493                                        store.loadItem({ 
     494                                                item: item, 
     495                                                onItem: onItem 
     496                                        }); 
     497                                } 
     498                        }); 
    428499                } 
    429500        }, 
     
    434505                //              on a newItem() call to the data store (or null if no parent specified). 
    435506                //              It's called with args from dojo.store.Notification.onNew. 
     507                // TODO: remove in 2.0 
    436508                return this.store.getIdentity(parentInfo.item);         // String 
    437509        }, 
     
    530602                        // unless the parent is the root of a tree with a hidden root 
    531603                        var parent = node.getParent(); 
    532                         if(!(this._hideRoot && parent === this)){ 
     604                        if(!(!this.showRoot && parent === this.rootNode)){ 
    533605                                node = parent; 
    534606                        } 
     
    672744        _getRootOrFirstNode: function(){ 
    673745                // summary: get first visible node 
    674                 return this._hideRoot ? this.getChildren()[0] : this; 
     746                return this.showRoot ? this.rootNode : this.rootNode.getChildren()[0]; 
    675747        }, 
    676748 
     
    705777 
    706778                // clicking the expando node might have erased focus from the current item; restore it 
    707                 var t = node.tree; 
    708                 if(t.lastFocused){ t.focusNode(t.lastFocused); } 
     779                if(this.lastFocused){ this.focusNode(this.lastFocused); } 
    709780 
    710781                if(!node.isExpandable){ 
     
    712783                } 
    713784 
    714                 var store = this.store; 
    715                 var getValue = this.store.getValue; 
     785                var store = this.store, 
     786                        item = node.item; 
    716787 
    717788                switch(node.state){ 
     
    728799                                        _this._onLoadAllItems(node, childItems, true); 
    729800                                }; 
    730                                 this.getItemChildren(node.item, onComplete); 
     801                                this.getItemChildren((this._v10Compat && item === store.root) ? null : item, onComplete); 
    731802                                break; 
    732803 
    733804                        default: 
    734805                                // data is already loaded; just proceed 
    735                                 if(node.expand){        // top level Tree doesn't have expand() method 
    736                                         node.expand(); 
    737                                         if(this.persist && node.item){ 
    738                                                 this._openedItemIds[this.store.getIdentity(node.item)] = true; 
    739                                                 this._saveState(); 
    740                                         } 
     806                                node.expand(); 
     807                                if(this.persist && item){ 
     808                                        this._openedItemIds[store.getIdentity(item)] = true; 
     809                                        this._saveState(); 
    741810                                } 
    742811                } 
     
    796865        _onNewItem: function(/* dojo.data.Item */ item, parentInfo){ 
    797866                //summary: callback when new item has been added to the store. 
    798                 parentInfo ? this.onNewChildItem(item, parentInfo) : this.onNewTopItem(item); 
    799         }, 
    800          
    801         onNewChildItem: function(/* dojo.data.Item */ item, parentInfo){ 
    802                 // summary: called when store.newItem(item, parentInfo) has been called with non-null parentInfo 
     867                 
     868                if(!parentInfo){ 
     869                        return; 
     870                } 
     871 
    803872                var parentNode = this._itemNodeMap[this.getItemParentIdentity(item, parentInfo)]; 
    804873 
     
    818887        }, 
    819888         
    820         onNewTopItem: function(/* dojo.data.Item */ item){ 
    821                 // summary: 
    822                 //              Called when store.newItem(item, null) has been called with null parentInfo. 
    823                 //              By default reruns the query for all top level items; user should override 
    824                 //              with more efficient function. 
    825                 this.reload(); 
    826         }, 
    827  
    828         reload: function(){ 
    829                 // summary: reload the list of top level items in the tree 
    830                 this.markProcessing(); 
    831                 var _this = this; 
    832                 var onComplete = function(childItems){ 
    833                         _this.unmarkProcessing(); 
    834                         _this._onLoadAllItems(_this, childItems, false); 
    835                 }; 
    836                 this.getItemChildren(null, onComplete); 
    837         }, 
    838  
    839889        _onDeleteItem: function(/*Object*/ item){ 
    840890                //summary: delete event from the store 
    841                 //since the object has just been deleted, we need to 
    842                 //use the name directly 
     891 
    843892                var identity = this.store.getIdentity(item); 
    844893                var node = this._itemNodeMap[identity]; 
     
    860909                                        /* object | array */ newValue){ 
    861910                //summary: set data event on an item in the store 
    862                 var identity = this.store.getIdentity(item), 
     911                var store = this.store, 
     912                        identity = store.getIdentity(item), 
    863913                        node = this._itemNodeMap[identity]; 
    864914 
    865915                if(node){ 
    866                         node.setLabelNode(this.getLabel(item)); 
    867                         node._updateItemClasses(item); 
    868  
     916                        if(!(this._v10Compat && item === store.root)){ 
     917                                node.setLabelNode(this.getLabel(item)); 
     918                                node._updateItemClasses(item); 
     919                        } 
     920         
    869921                        // If this item's children have changed, update tree accordingly. 
    870922                        // Have to download the new nodes, which may be an async operation. 
     
    876928                                        _this._onLoadAllItems(node, childItems, false); 
    877929                                }; 
    878                                 this.getItemChildren(node.item, onComplete); 
     930                                this.getItemChildren((this._v10Compat && item === store.root) ? null : item, onComplete); 
    879931                        } 
    880932                } 
     
    891943                } 
    892944                dojo.cookie(this.cookieName, ary.join(",")); 
     945        }, 
     946 
     947        destroy: function(){ 
     948                if(this.rootNode){ 
     949                        this.rootNode.destroyRecursive(); 
     950                } 
     951                this.rootNode = null; 
     952                this.inherited(arguments); 
     953        }, 
     954         
     955        destroyRecursive: function(){ 
     956                // A tree is treated as a leaf, not as a node with children (like a grid), 
     957                // but defining destroyRecursive for back-compat. 
     958                this.destroy(); 
    893959        } 
    894960}); 
  • dijit/trunk/_tree/dndSource.js

    r12020 r12309  
    260260                        this.isDragging = false; 
    261261 
    262                         // Compute the new parent item (if we are *not* dropping at the top level) 
     262                        // Compute the new parent item 
    263263                        var targetWidget = dijit.getEnclosingWidget(target), 
    264                                 newParentItem; 
    265                         if(targetWidget && targetWidget.item){ 
    266                                 // dropping onto another item 
    267                                 newParentItem = targetWidget.item; 
    268                         }else{ 
    269                                 // dropping onto root 
    270                                 requeryRoot = true; 
    271                         } 
     264                                newParentItem = (targetWidget && targetWidget.item) || tree.item; 
    272265 
    273266                        // If we are dragging from another source (or at least, another source 
     
    292285                                                oldParentItem = childTreeNode.getParent().item; 
    293286 
    294                                         if( oldParentItem ){ 
    295                                                 dojo.forEach(tree.childrenAttr, function(attr){ 
    296                                                         if(store.containsValue(oldParentItem, attr, childItem)){ 
    297                                                                 var values = dojo.filter(store.getValues(oldParentItem, attr), function(x){ 
    298                                                                         return x != childItem; 
    299                                                                 }); 
    300                                                                 store.setValues(oldParentItem, attr, values); 
    301                                                                 parentAttr = attr; 
    302                                                         } 
    303                                                 }); 
    304                                         } 
    305  
    306                                         if(newParentItem){ 
    307                                                 // modify target item's children attribute to include this item 
    308                                                 store.setValues(newParentItem, parentAttr, 
    309                                                         store.getValues(newParentItem, parentAttr).concat(childItem)); 
    310                                         } 
     287                                        dojo.forEach(tree.childrenAttr, function(attr){ 
     288                                                if(store.containsValue(oldParentItem, attr, childItem)){ 
     289                                                        var values = dojo.filter(store.getValues(oldParentItem, attr), function(x){ 
     290                                                                return x != childItem; 
     291                                                        }); 
     292                                                        store.setValues(oldParentItem, attr, values); 
     293                                                        parentAttr = attr; 
     294                                                } 
     295                                        }); 
     296 
     297                                        // modify target item's children attribute to include this item 
     298                                        store.setValues(newParentItem, parentAttr, 
     299                                                store.getValues(newParentItem, parentAttr).concat(childItem)); 
    311300                                }else{ 
    312                                         var pInfo = newParentItem ? {parent: newParentItem, attribute: parentAttr} : null; 
     301                                        var pInfo = {parent: newParentItem, attribute: parentAttr}; 
    313302                                        store.newItem(newItemsParams[idx], pInfo); 
    314303                                } 
    315304                        }, this); 
    316                         if(requeryRoot){ 
    317                                 // The list of top level children changed, so update it. 
    318                                 tree.reload(); 
    319                         } 
    320305                } 
    321306                this.onDndCancel(); 
  • dijit/trunk/_tree/Node.html

    r11200 r12309  
    1 <div class="dijitTreeNode dijitTreeExpandLeaf dijitTreeChildrenNo" waiRole="presentation" 
    2         ><span dojoAttachPoint="expandoNode" class="dijitTreeExpando" waiRole="presentation" 
    3         ></span 
    4         ><span dojoAttachPoint="expandoNodeText" class="dijitExpandoText" waiRole="presentation" 
    5         ></span 
    6         > 
    7         <div dojoAttachPoint="contentNode" class="dijitTreeContent" waiRole="presentation"> 
    8                 <div dojoAttachPoint="iconNode" class="dijitInline dijitTreeIcon" waiRole="presentation"></div> 
    9                 <span dojoAttachPoint="labelNode" class="dijitTreeLabel" wairole="treeitem" tabindex="-1"></span> 
    10         </div> 
     1<div class="dijitTreeNode" waiRole="presentation" 
     2        ><div dojoAttachPoint="rowNode" 
     3                ><span dojoAttachPoint="expandoNode" class="dijitTreeExpando" waiRole="presentation" 
     4                ></span 
     5                ><span dojoAttachPoint="expandoNodeText" class="dijitExpandoText" waiRole="presentation" 
     6