diff options
author | Andrew Dolgov <[email protected]> | 2011-11-08 20:40:44 +0400 |
---|---|---|
committer | Andrew Dolgov <[email protected]> | 2011-11-08 20:40:44 +0400 |
commit | 81bea17aefb26859f825b9293c7c99192874806e (patch) | |
tree | fb244408ca271affa2899adb634788802c9a89d8 /lib/dijit/tree | |
parent | 870a70e109ac9e80a88047044530de53d0404ec7 (diff) |
upgrade Dojo to 1.6.1
Diffstat (limited to 'lib/dijit/tree')
-rw-r--r-- | lib/dijit/tree/ForestStoreModel.js | 345 | ||||
-rw-r--r-- | lib/dijit/tree/TreeStoreModel.js | 510 | ||||
-rw-r--r-- | lib/dijit/tree/_dndContainer.js | 216 | ||||
-rw-r--r-- | lib/dijit/tree/_dndSelector.js | 392 | ||||
-rw-r--r-- | lib/dijit/tree/dndSource.js | 762 | ||||
-rw-r--r-- | lib/dijit/tree/model.js | 143 |
6 files changed, 1760 insertions, 608 deletions
diff --git a/lib/dijit/tree/ForestStoreModel.js b/lib/dijit/tree/ForestStoreModel.js index aa51b0023..5e1b25efb 100644 --- a/lib/dijit/tree/ForestStoreModel.js +++ b/lib/dijit/tree/ForestStoreModel.js @@ -1,83 +1,280 @@ /* - Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. Available via Academic Free License >= 2.1 OR the modified BSD license. see: http://dojotoolkit.org/license for details */ -if(!dojo._hasResource["dijit.tree.ForestStoreModel"]){ -dojo._hasResource["dijit.tree.ForestStoreModel"]=true; +if(!dojo._hasResource["dijit.tree.ForestStoreModel"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.tree.ForestStoreModel"] = true; dojo.provide("dijit.tree.ForestStoreModel"); dojo.require("dijit.tree.TreeStoreModel"); -dojo.declare("dijit.tree.ForestStoreModel",dijit.tree.TreeStoreModel,{rootId:"$root$",rootLabel:"ROOT",query:null,constructor:function(_1){ -this.root={store:this,root:true,id:_1.rootId,label:_1.rootLabel,children:_1.rootChildren}; -},mayHaveChildren:function(_2){ -return _2===this.root||this.inherited(arguments); -},getChildren:function(_3,_4,_5){ -if(_3===this.root){ -if(this.root.children){ -_4(this.root.children); -}else{ -this.store.fetch({query:this.query,onComplete:dojo.hitch(this,function(_6){ -this.root.children=_6; -_4(_6); -}),onError:_5}); -} -}else{ -this.inherited(arguments); -} -},isItem:function(_7){ -return (_7===this.root)?true:this.inherited(arguments); -},fetchItemByIdentity:function(_8){ -if(_8.identity==this.root.id){ -var _9=_8.scope?_8.scope:dojo.global; -if(_8.onItem){ -_8.onItem.call(_9,this.root); -} -}else{ -this.inherited(arguments); -} -},getIdentity:function(_a){ -return (_a===this.root)?this.root.id:this.inherited(arguments); -},getLabel:function(_b){ -return (_b===this.root)?this.root.label:this.inherited(arguments); -},newItem:function(_c,_d,_e){ -if(_d===this.root){ -this.onNewRootItem(_c); -return this.store.newItem(_c); -}else{ -return this.inherited(arguments); -} -},onNewRootItem:function(_f){ -},pasteItem:function(_10,_11,_12,_13,_14){ -if(_11===this.root){ -if(!_13){ -this.onLeaveRoot(_10); -} -} -dijit.tree.TreeStoreModel.prototype.pasteItem.call(this,_10,_11===this.root?null:_11,_12===this.root?null:_12,_13,_14); -if(_12===this.root){ -this.onAddToRoot(_10); -} -},onAddToRoot:function(_15){ -},onLeaveRoot:function(_16){ -},_requeryTop:function(){ -var _17=this.root.children||[]; -this.store.fetch({query:this.query,onComplete:dojo.hitch(this,function(_18){ -this.root.children=_18; -if(_17.length!=_18.length||dojo.some(_17,function(_19,idx){ -return _18[idx]!=_19; -})){ -this.onChildrenChange(this.root,_18); -} -})}); -},onNewItem:function(_1a,_1b){ -this._requeryTop(); -this.inherited(arguments); -},onDeleteItem:function(_1c){ -if(dojo.indexOf(this.root.children,_1c)!=-1){ -this._requeryTop(); -} -this.inherited(arguments); -}}); + + +dojo.declare("dijit.tree.ForestStoreModel", dijit.tree.TreeStoreModel, { + // summary: + // Interface between a dijit.Tree and a dojo.data store that doesn't have a root item, + // a.k.a. a store that has multiple "top level" items. + // + // description + // Use this class to wrap a dojo.data store, making all the items matching the specified query + // appear as children of a fabricated "root item". If no query is specified then all the + // items returned by fetch() on the underlying store become children of the root item. + // This class allows dijit.Tree to assume a single root item, even if the store doesn't have one. + // + // When using this class the developer must override a number of methods according to their app and + // data, including: + // - onNewRootItem + // - onAddToRoot + // - onLeaveRoot + // - onNewItem + // - onSetItem + + // Parameters to constructor + + // rootId: String + // ID of fabricated root item + rootId: "$root$", + + // rootLabel: String + // Label of fabricated root item + rootLabel: "ROOT", + + // query: String + // Specifies the set of children of the root item. + // example: + // | {type:'continent'} + query: null, + + // End of parameters to constructor + + constructor: function(params){ + // summary: + // Sets up variables, etc. + // tags: + // private + + // Make dummy root item + this.root = { + store: this, + root: true, + id: params.rootId, + label: params.rootLabel, + children: params.rootChildren // optional param + }; + }, + + // ======================================================================= + // Methods for traversing hierarchy + + mayHaveChildren: function(/*dojo.data.Item*/ item){ + // summary: + // Tells if an item has or may have children. Implementing logic here + // avoids showing +/- expando icon for nodes that we know don't have children. + // (For efficiency reasons we may not want to check if an element actually + // has children until user clicks the expando node) + // tags: + // extension + return item === this.root || this.inherited(arguments); + }, + + getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ callback, /*function*/ onError){ + // summary: + // Calls onComplete() with array of child items of given parent item, all loaded. + if(parentItem === this.root){ + if(this.root.children){ + // already loaded, just return + callback(this.root.children); + }else{ + this.store.fetch({ + query: this.query, + onComplete: dojo.hitch(this, function(items){ + this.root.children = items; + callback(items); + }), + onError: onError + }); + } + }else{ + this.inherited(arguments); + } + }, + + // ======================================================================= + // Inspecting items + + isItem: function(/* anything */ something){ + return (something === this.root) ? true : this.inherited(arguments); + }, + + fetchItemByIdentity: function(/* object */ keywordArgs){ + if(keywordArgs.identity == this.root.id){ + var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; + if(keywordArgs.onItem){ + keywordArgs.onItem.call(scope, this.root); + } + }else{ + this.inherited(arguments); + } + }, + + getIdentity: function(/* item */ item){ + return (item === this.root) ? this.root.id : this.inherited(arguments); + }, + + getLabel: function(/* item */ item){ + return (item === this.root) ? this.root.label : this.inherited(arguments); + }, + + // ======================================================================= + // Write interface + + newItem: function(/* dojo.dnd.Item */ args, /*Item*/ parent, /*int?*/ insertIndex){ + // summary: + // Creates a new item. See dojo.data.api.Write for details on args. + // Used in drag & drop when item from external source dropped onto tree. + if(parent === this.root){ + this.onNewRootItem(args); + return this.store.newItem(args); + }else{ + return this.inherited(arguments); + } + }, + + onNewRootItem: function(args){ + // summary: + // User can override this method to modify a new element that's being + // added to the root of the tree, for example to add a flag like root=true + }, + + pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){ + // summary: + // Move or copy an item from one parent item to another. + // Used in drag & drop + if(oldParentItem === this.root){ + if(!bCopy){ + // It's onLeaveRoot()'s responsibility to modify the item so it no longer matches + // this.query... thus triggering an onChildrenChange() event to notify the Tree + // that this element is no longer a child of the root node + this.onLeaveRoot(childItem); + } + } + dijit.tree.TreeStoreModel.prototype.pasteItem.call(this, childItem, + oldParentItem === this.root ? null : oldParentItem, + newParentItem === this.root ? null : newParentItem, + bCopy, + insertIndex + ); + if(newParentItem === this.root){ + // It's onAddToRoot()'s responsibility to modify the item so it matches + // this.query... thus triggering an onChildrenChange() event to notify the Tree + // that this element is now a child of the root node + this.onAddToRoot(childItem); + } + }, + + // ======================================================================= + // Handling for top level children + + onAddToRoot: function(/* item */ item){ + // summary: + // Called when item added to root of tree; user must override this method + // to modify the item so that it matches the query for top level items + // example: + // | store.setValue(item, "root", true); + // tags: + // extension + console.log(this, ": item ", item, " added to root"); + }, + + onLeaveRoot: function(/* item */ item){ + // summary: + // Called when item removed from root of tree; user must override this method + // to modify the item so it doesn't match the query for top level items + // example: + // | store.unsetAttribute(item, "root"); + // tags: + // extension + console.log(this, ": item ", item, " removed from root"); + }, + + // ======================================================================= + // Events from data store + + _requeryTop: function(){ + // reruns the query for the children of the root node, + // sending out an onSet notification if those children have changed + var oldChildren = this.root.children || []; + this.store.fetch({ + query: this.query, + onComplete: dojo.hitch(this, function(newChildren){ + this.root.children = newChildren; + + // If the list of children or the order of children has changed... + if(oldChildren.length != newChildren.length || + dojo.some(oldChildren, function(item, idx){ return newChildren[idx] != item;})){ + this.onChildrenChange(this.root, newChildren); + } + }) + }); + }, + + onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){ + // summary: + // Handler for when new items appear in the store. Developers should override this + // method to be more efficient based on their app/data. + // description: + // Note that the default implementation requeries the top level items every time + // a new item is created, since any new item could be a top level item (even in + // addition to being a child of another item, since items can have multiple parents). + // + // If developers can detect which items are possible top level items (based on the item and the + // parentInfo parameters), they should override this method to only call _requeryTop() for top + // level items. Often all top level items have parentInfo==null, but + // that will depend on which store you use and what your data is like. + // tags: + // extension + this._requeryTop(); + + this.inherited(arguments); + }, + + onDeleteItem: function(/*Object*/ item){ + // summary: + // Handler for delete notifications from underlying store + + // check if this was a child of root, and if so send notification that root's children + // have changed + if(dojo.indexOf(this.root.children, item) != -1){ + this._requeryTop(); + } + + this.inherited(arguments); + }, + + onSetItem: function(/* item */ item, + /* attribute-name-string */ attribute, + /* object | array */ oldValue, + /* object | array */ newValue){ + // summary: + // Updates the tree view according to changes to an item in the data store. + // Developers should override this method to be more efficient based on their app/data. + // description: + // Handles updates to an item's children by calling onChildrenChange(), and + // other updates to an item by calling onChange(). + // + // Also, any change to any item re-executes the query for the tree's top-level items, + // since this modified item may have started/stopped matching the query for top level items. + // + // If possible, developers should override this function to only call _requeryTop() when + // the change to the item has caused it to stop/start being a top level item in the tree. + // tags: + // extension + + this._requeryTop(); + this.inherited(arguments); + } + +}); + } diff --git a/lib/dijit/tree/TreeStoreModel.js b/lib/dijit/tree/TreeStoreModel.js index c03bef526..5164176b7 100644 --- a/lib/dijit/tree/TreeStoreModel.js +++ b/lib/dijit/tree/TreeStoreModel.js @@ -1,145 +1,381 @@ /* - Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. Available via Academic Free License >= 2.1 OR the modified BSD license. see: http://dojotoolkit.org/license for details */ -if(!dojo._hasResource["dijit.tree.TreeStoreModel"]){ -dojo._hasResource["dijit.tree.TreeStoreModel"]=true; +if(!dojo._hasResource["dijit.tree.TreeStoreModel"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.tree.TreeStoreModel"] = true; dojo.provide("dijit.tree.TreeStoreModel"); -dojo.declare("dijit.tree.TreeStoreModel",null,{store:null,childrenAttrs:["children"],newItemIdAttr:"id",labelAttr:"",root:null,query:null,deferItemLoadingUntilExpand:false,constructor:function(_1){ -dojo.mixin(this,_1); -this.connects=[]; -var _2=this.store; -if(!_2.getFeatures()["dojo.data.api.Identity"]){ -throw new Error("dijit.Tree: store must support dojo.data.Identity"); -} -if(_2.getFeatures()["dojo.data.api.Notification"]){ -this.connects=this.connects.concat([dojo.connect(_2,"onNew",this,"onNewItem"),dojo.connect(_2,"onDelete",this,"onDeleteItem"),dojo.connect(_2,"onSet",this,"onSetItem")]); -} -},destroy:function(){ -dojo.forEach(this.connects,dojo.disconnect); -},getRoot:function(_3,_4){ -if(this.root){ -_3(this.root); -}else{ -this.store.fetch({query:this.query,onComplete:dojo.hitch(this,function(_5){ -if(_5.length!=1){ -throw new Error(this.declaredClass+": query "+dojo.toJson(this.query)+" returned "+_5.length+" items, but must return exactly one item"); -} -this.root=_5[0]; -_3(this.root); -}),onError:_4}); -} -},mayHaveChildren:function(_6){ -return dojo.some(this.childrenAttrs,function(_7){ -return this.store.hasAttribute(_6,_7); -},this); -},getChildren:function(_8,_9,_a){ -var _b=this.store; -if(!_b.isItemLoaded(_8)){ -var _c=dojo.hitch(this,arguments.callee); -_b.loadItem({item:_8,onItem:function(_d){ -_c(_d,_9,_a); -},onError:_a}); -return; -} -var _e=[]; -for(var i=0;i<this.childrenAttrs.length;i++){ -var _f=_b.getValues(_8,this.childrenAttrs[i]); -_e=_e.concat(_f); -} -var _10=0; -if(!this.deferItemLoadingUntilExpand){ -dojo.forEach(_e,function(_11){ -if(!_b.isItemLoaded(_11)){ -_10++; -} -}); -} -if(_10==0){ -_9(_e); -}else{ -dojo.forEach(_e,function(_12,idx){ -if(!_b.isItemLoaded(_12)){ -_b.loadItem({item:_12,onItem:function(_13){ -_e[idx]=_13; -if(--_10==0){ -_9(_e); -} -},onError:_a}); -} -}); -} -},isItem:function(_14){ -return this.store.isItem(_14); -},fetchItemByIdentity:function(_15){ -this.store.fetchItemByIdentity(_15); -},getIdentity:function(_16){ -return this.store.getIdentity(_16); -},getLabel:function(_17){ -if(this.labelAttr){ -return this.store.getValue(_17,this.labelAttr); -}else{ -return this.store.getLabel(_17); -} -},newItem:function(_18,_19,_1a){ -var _1b={parent:_19,attribute:this.childrenAttrs[0],insertIndex:_1a}; -if(this.newItemIdAttr&&_18[this.newItemIdAttr]){ -this.fetchItemByIdentity({identity:_18[this.newItemIdAttr],scope:this,onItem:function(_1c){ -if(_1c){ -this.pasteItem(_1c,null,_19,true,_1a); -}else{ -this.store.newItem(_18,_1b); -} -}}); -}else{ -this.store.newItem(_18,_1b); -} -},pasteItem:function(_1d,_1e,_1f,_20,_21){ -var _22=this.store,_23=this.childrenAttrs[0]; -if(_1e){ -dojo.forEach(this.childrenAttrs,function(_24){ -if(_22.containsValue(_1e,_24,_1d)){ -if(!_20){ -var _25=dojo.filter(_22.getValues(_1e,_24),function(x){ -return x!=_1d; -}); -_22.setValues(_1e,_24,_25); -} -_23=_24; -} -}); -} -if(_1f){ -if(typeof _21=="number"){ -var _26=_22.getValues(_1f,_23).slice(); -_26.splice(_21,0,_1d); -_22.setValues(_1f,_23,_26); -}else{ -_22.setValues(_1f,_23,_22.getValues(_1f,_23).concat(_1d)); -} -} -},onChange:function(_27){ -},onChildrenChange:function(_28,_29){ -},onDelete:function(_2a,_2b){ -},onNewItem:function(_2c,_2d){ -if(!_2d){ -return; -} -this.getChildren(_2d.item,dojo.hitch(this,function(_2e){ -this.onChildrenChange(_2d.item,_2e); -})); -},onDeleteItem:function(_2f){ -this.onDelete(_2f); -},onSetItem:function(_30,_31,_32,_33){ -if(dojo.indexOf(this.childrenAttrs,_31)!=-1){ -this.getChildren(_30,dojo.hitch(this,function(_34){ -this.onChildrenChange(_30,_34); -})); -}else{ -this.onChange(_30); -} -}}); + + +dojo.declare( + "dijit.tree.TreeStoreModel", + null, + { + // summary: + // Implements dijit.Tree.model connecting to a store with a single + // root item. Any methods passed into the constructor will override + // the ones defined here. + + // store: dojo.data.Store + // Underlying store + store: null, + + // childrenAttrs: String[] + // One or more attribute names (attributes in the dojo.data item) that specify that item's children + childrenAttrs: ["children"], + + // newItemIdAttr: String + // Name of attribute in the Object passed to newItem() that specifies the id. + // + // If newItemIdAttr is set then it's used when newItem() is called to see if an + // item with the same id already exists, and if so just links to the old item + // (so that the old item ends up with two parents). + // + // Setting this to null or "" will make every drop create a new item. + newItemIdAttr: "id", + + // labelAttr: String + // If specified, get label for tree node from this attribute, rather + // than by calling store.getLabel() + labelAttr: "", + + // root: [readonly] dojo.data.Item + // Pointer to the root item (read only, not a parameter) + root: null, + + // query: anything + // Specifies datastore query to return the root item for the tree. + // Must only return a single item. Alternately can just pass in pointer + // to root item. + // example: + // | {id:'ROOT'} + query: null, + + // deferItemLoadingUntilExpand: Boolean + // Setting this to true will cause the TreeStoreModel to defer calling loadItem on nodes + // until they are expanded. This allows for lazying loading where only one + // loadItem (and generally one network call, consequently) per expansion + // (rather than one for each child). + // This relies on partial loading of the children items; each children item of a + // fully loaded item should contain the label and info about having children. + deferItemLoadingUntilExpand: false, + + constructor: function(/* Object */ args){ + // summary: + // Passed the arguments listed above (store, etc) + // tags: + // private + + dojo.mixin(this, args); + + this.connects = []; + + var store = this.store; + if(!store.getFeatures()['dojo.data.api.Identity']){ + throw new Error("dijit.Tree: store must support dojo.data.Identity"); + } + + // if the store supports Notification, subscribe to the notification events + if(store.getFeatures()['dojo.data.api.Notification']){ + this.connects = this.connects.concat([ + dojo.connect(store, "onNew", this, "onNewItem"), + dojo.connect(store, "onDelete", this, "onDeleteItem"), + dojo.connect(store, "onSet", this, "onSetItem") + ]); + } + }, + + destroy: function(){ + dojo.forEach(this.connects, dojo.disconnect); + // TODO: should cancel any in-progress processing of getRoot(), getChildren() + }, + + // ======================================================================= + // Methods for traversing hierarchy + + getRoot: function(onItem, onError){ + // summary: + // Calls onItem with the root item for the tree, possibly a fabricated item. + // Calls onError on error. + if(this.root){ + onItem(this.root); + }else{ + this.store.fetch({ + query: this.query, + onComplete: dojo.hitch(this, function(items){ + if(items.length != 1){ + throw new Error(this.declaredClass + ": query " + dojo.toJson(this.query) + " returned " + items.length + + " items, but must return exactly one item"); + } + this.root = items[0]; + onItem(this.root); + }), + onError: onError + }); + } + }, + + mayHaveChildren: function(/*dojo.data.Item*/ item){ + // summary: + // Tells if an item has or may have children. Implementing logic here + // avoids showing +/- expando icon for nodes that we know don't have children. + // (For efficiency reasons we may not want to check if an element actually + // has children until user clicks the expando node) + return dojo.some(this.childrenAttrs, function(attr){ + return this.store.hasAttribute(item, attr); + }, this); + }, + + getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){ + // summary: + // Calls onComplete() with array of child items of given parent item, all loaded. + + var store = this.store; + if(!store.isItemLoaded(parentItem)){ + // The parent is not loaded yet, we must be in deferItemLoadingUntilExpand + // mode, so we will load it and just return the children (without loading each + // child item) + var getChildren = dojo.hitch(this, arguments.callee); + store.loadItem({ + item: parentItem, + onItem: function(parentItem){ + getChildren(parentItem, onComplete, onError); + }, + onError: onError + }); + return; + } + // get children of specified item + var childItems = []; + for(var i=0; i<this.childrenAttrs.length; i++){ + var vals = store.getValues(parentItem, this.childrenAttrs[i]); + childItems = childItems.concat(vals); + } + + // count how many items need to be loaded + var _waitCount = 0; + if(!this.deferItemLoadingUntilExpand){ + dojo.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } }); + } + + if(_waitCount == 0){ + // all items are already loaded (or we aren't loading them). proceed... + onComplete(childItems); + }else{ + // still waiting for some or all of the items to load + dojo.forEach(childItems, function(item, idx){ + if(!store.isItemLoaded(item)){ + store.loadItem({ + item: item, + onItem: function(item){ + childItems[idx] = item; + if(--_waitCount == 0){ + // all nodes have been loaded, send them to the tree + onComplete(childItems); + } + }, + onError: onError + }); + } + }); + } + }, + + // ======================================================================= + // Inspecting items + + isItem: function(/* anything */ something){ + return this.store.isItem(something); // Boolean + }, + + fetchItemByIdentity: function(/* object */ keywordArgs){ + this.store.fetchItemByIdentity(keywordArgs); + }, + + getIdentity: function(/* item */ item){ + return this.store.getIdentity(item); // Object + }, + + getLabel: function(/*dojo.data.Item*/ item){ + // summary: + // Get the label for an item + if(this.labelAttr){ + return this.store.getValue(item,this.labelAttr); // String + }else{ + return this.store.getLabel(item); // String + } + }, + + // ======================================================================= + // Write interface + + newItem: function(/* dojo.dnd.Item */ args, /*Item*/ parent, /*int?*/ insertIndex){ + // summary: + // Creates a new item. See `dojo.data.api.Write` for details on args. + // Used in drag & drop when item from external source dropped onto tree. + // description: + // Developers will need to override this method if new items get added + // to parents with multiple children attributes, in order to define which + // children attribute points to the new item. + + var pInfo = {parent: parent, attribute: this.childrenAttrs[0]}, LnewItem; + + if(this.newItemIdAttr && args[this.newItemIdAttr]){ + // Maybe there's already a corresponding item in the store; if so, reuse it. + this.fetchItemByIdentity({identity: args[this.newItemIdAttr], scope: this, onItem: function(item){ + if(item){ + // There's already a matching item in store, use it + this.pasteItem(item, null, parent, true, insertIndex); + }else{ + // Create new item in the tree, based on the drag source. + LnewItem=this.store.newItem(args, pInfo); + if (LnewItem && (insertIndex!=undefined)){ + // Move new item to desired position + this.pasteItem(LnewItem, parent, parent, false, insertIndex); + } + } + }}); + }else{ + // [as far as we know] there is no id so we must assume this is a new item + LnewItem=this.store.newItem(args, pInfo); + if (LnewItem && (insertIndex!=undefined)){ + // Move new item to desired position + this.pasteItem(LnewItem, parent, parent, false, insertIndex); + } + } + }, + + pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){ + // summary: + // Move or copy an item from one parent item to another. + // Used in drag & drop + var store = this.store, + parentAttr = this.childrenAttrs[0]; // name of "children" attr in parent item + + // remove child from source item, and record the attribute that child occurred in + if(oldParentItem){ + dojo.forEach(this.childrenAttrs, function(attr){ + if(store.containsValue(oldParentItem, attr, childItem)){ + if(!bCopy){ + var values = dojo.filter(store.getValues(oldParentItem, attr), function(x){ + return x != childItem; + }); + store.setValues(oldParentItem, attr, values); + } + parentAttr = attr; + } + }); + } + + // modify target item's children attribute to include this item + if(newParentItem){ + if(typeof insertIndex == "number"){ + // call slice() to avoid modifying the original array, confusing the data store + var childItems = store.getValues(newParentItem, parentAttr).slice(); + childItems.splice(insertIndex, 0, childItem); + store.setValues(newParentItem, parentAttr, childItems); + }else{ + store.setValues(newParentItem, parentAttr, + store.getValues(newParentItem, parentAttr).concat(childItem)); + } + } + }, + + // ======================================================================= + // Callbacks + + onChange: function(/*dojo.data.Item*/ item){ + // summary: + // Callback whenever an item has changed, so that Tree + // can update the label, icon, etc. Note that changes + // to an item's children or parent(s) will trigger an + // onChildrenChange() so you can ignore those changes here. + // tags: + // callback + }, + + onChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){ + // summary: + // Callback to do notifications about new, updated, or deleted items. + // tags: + // callback + }, + + onDelete: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){ + // summary: + // Callback when an item has been deleted. + // description: + // Note that there will also be an onChildrenChange() callback for the parent + // of this item. + // tags: + // callback + }, + + // ======================================================================= + // Events from data store + + onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){ + // summary: + // Handler for when new items appear in the store, either from a drop operation + // or some other way. Updates the tree view (if necessary). + // description: + // If the new item is a child of an existing item, + // calls onChildrenChange() with the new list of children + // for that existing item. + // + // tags: + // extension + + // We only care about the new item if it has a parent that corresponds to a TreeNode + // we are currently displaying + if(!parentInfo){ + return; + } + + // Call onChildrenChange() on parent (ie, existing) item with new list of children + // In the common case, the new list of children is simply parentInfo.newValue or + // [ parentInfo.newValue ], although if items in the store has multiple + // child attributes (see `childrenAttr`), then it's a superset of parentInfo.newValue, + // so call getChildren() to be sure to get right answer. + this.getChildren(parentInfo.item, dojo.hitch(this, function(children){ + this.onChildrenChange(parentInfo.item, children); + })); + }, + + onDeleteItem: function(/*Object*/ item){ + // summary: + // Handler for delete notifications from underlying store + this.onDelete(item); + }, + + onSetItem: function(/* item */ item, + /* attribute-name-string */ attribute, + /* object | array */ oldValue, + /* object | array */ newValue){ + // summary: + // Updates the tree view according to changes in the data store. + // description: + // Handles updates to an item's children by calling onChildrenChange(), and + // other updates to an item by calling onChange(). + // + // See `onNewItem` for more details on handling updates to an item's children. + // tags: + // extension + + if(dojo.indexOf(this.childrenAttrs, attribute) != -1){ + // item's children list changed + this.getChildren(item, dojo.hitch(this, function(children){ + // See comments in onNewItem() about calling getChildren() + this.onChildrenChange(item, children); + })); + }else{ + // item's label/icon/etc. changed. + this.onChange(item); + } + } + }); + } diff --git a/lib/dijit/tree/_dndContainer.js b/lib/dijit/tree/_dndContainer.js index e925283df..9ee8e32ba 100644 --- a/lib/dijit/tree/_dndContainer.js +++ b/lib/dijit/tree/_dndContainer.js @@ -1,49 +1,187 @@ /* - Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. Available via Academic Free License >= 2.1 OR the modified BSD license. see: http://dojotoolkit.org/license for details */ -if(!dojo._hasResource["dijit.tree._dndContainer"]){ -dojo._hasResource["dijit.tree._dndContainer"]=true; +if(!dojo._hasResource["dijit.tree._dndContainer"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.tree._dndContainer"] = true; dojo.provide("dijit.tree._dndContainer"); dojo.require("dojo.dnd.common"); dojo.require("dojo.dnd.Container"); -dojo.declare("dijit.tree._dndContainer",null,{constructor:function(_1,_2){ -this.tree=_1; -this.node=_1.domNode; -dojo.mixin(this,_2); -this.map={}; -this.current=null; -this.containerState=""; -dojo.addClass(this.node,"dojoDndContainer"); -this.events=[dojo.connect(this.node,"onmouseenter",this,"onOverEvent"),dojo.connect(this.node,"onmouseleave",this,"onOutEvent"),dojo.connect(this.tree,"_onNodeMouseEnter",this,"onMouseOver"),dojo.connect(this.tree,"_onNodeMouseLeave",this,"onMouseOut"),dojo.connect(this.node,"ondragstart",dojo,"stopEvent"),dojo.connect(this.node,"onselectstart",dojo,"stopEvent")]; -},getItem:function(_3){ -var _4=this.selection[_3],_5={data:dijit.getEnclosingWidget(_4),type:["treeNode"]}; -return _5; -},destroy:function(){ -dojo.forEach(this.events,dojo.disconnect); -this.node=this.parent=null; -},onMouseOver:function(_6,_7){ -this.current=_6.rowNode; -this.currentWidget=_6; -},onMouseOut:function(_8,_9){ -this.current=null; -this.currentWidget=null; -},_changeState:function(_a,_b){ -var _c="dojoDnd"+_a; -var _d=_a.toLowerCase()+"State"; -dojo.removeClass(this.node,_c+this[_d]); -dojo.addClass(this.node,_c+_b); -this[_d]=_b; -},_addItemClass:function(_e,_f){ -dojo.addClass(_e,"dojoDndItem"+_f); -},_removeItemClass:function(_10,_11){ -dojo.removeClass(_10,"dojoDndItem"+_11); -},onOverEvent:function(){ -this._changeState("Container","Over"); -},onOutEvent:function(){ -this._changeState("Container",""); -}}); + + +dojo.getObject("tree", true, dojo); + +dijit.tree._compareNodes = function(n1, n2){ + if(n1 === n2){ + return 0; + } + + if('sourceIndex' in document.documentElement){ //IE + //TODO: does not yet work if n1 and/or n2 is a text node + return n1.sourceIndex - n2.sourceIndex; + }else if('compareDocumentPosition' in document.documentElement){ //FF, Opera + return n1.compareDocumentPosition(n2) & 2 ? 1: -1; + }else if(document.createRange){ //Webkit + var r1 = doc.createRange(); + r1.setStartBefore(n1); + + var r2 = doc.createRange(); + r2.setStartBefore(n2); + + return r1.compareBoundaryPoints(r1.END_TO_END, r2); + }else{ + throw Error("dijit.tree._compareNodes don't know how to compare two different nodes in this browser"); + } +}; + +dojo.declare("dijit.tree._dndContainer", + null, + { + + // summary: + // This is a base class for `dijit.tree._dndSelector`, and isn't meant to be used directly. + // It's modeled after `dojo.dnd.Container`. + // tags: + // protected + + /*===== + // current: DomNode + // The currently hovered TreeNode.rowNode (which is the DOM node + // associated w/a given node in the tree, excluding it's descendants) + current: null, + =====*/ + + constructor: function(tree, params){ + // summary: + // A constructor of the Container + // tree: Node + // Node or node's id to build the container on + // params: dijit.tree.__SourceArgs + // A dict of parameters, which gets mixed into the object + // tags: + // private + this.tree = tree; + this.node = tree.domNode; // TODO: rename; it's not a TreeNode but the whole Tree + dojo.mixin(this, params); + + // class-specific variables + this.map = {}; + this.current = null; // current TreeNode's DOM node + + // states + this.containerState = ""; + dojo.addClass(this.node, "dojoDndContainer"); + + // set up events + this.events = [ + // container level events + dojo.connect(this.node, "onmouseenter", this, "onOverEvent"), + dojo.connect(this.node, "onmouseleave", this, "onOutEvent"), + + // switching between TreeNodes + dojo.connect(this.tree, "_onNodeMouseEnter", this, "onMouseOver"), + dojo.connect(this.tree, "_onNodeMouseLeave", this, "onMouseOut"), + + // cancel text selection and text dragging + dojo.connect(this.node, "ondragstart", dojo, "stopEvent"), + dojo.connect(this.node, "onselectstart", dojo, "stopEvent") + ]; + }, + + getItem: function(/*String*/ key){ + // summary: + // Returns the dojo.dnd.Item (representing a dragged node) by it's key (id). + // Called by dojo.dnd.Source.checkAcceptance(). + // tags: + // protected + + var widget = this.selection[key], + ret = { + data: widget, + type: ["treeNode"] + }; + + return ret; // dojo.dnd.Item + }, + + destroy: function(){ + // summary: + // Prepares this object to be garbage-collected + + dojo.forEach(this.events, dojo.disconnect); + // this.clearItems(); + this.node = this.parent = null; + }, + + // mouse events + onMouseOver: function(/*TreeNode*/ widget, /*Event*/ evt){ + // summary: + // Called when mouse is moved over a TreeNode + // tags: + // protected + this.current = widget; + }, + + onMouseOut: function(/*TreeNode*/ widget, /*Event*/ evt){ + // summary: + // Called when mouse is moved away from a TreeNode + // tags: + // protected + this.current = null; + }, + + _changeState: function(type, newState){ + // summary: + // Changes a named state to new state value + // type: String + // A name of the state to change + // newState: String + // new state + var prefix = "dojoDnd" + type; + var state = type.toLowerCase() + "State"; + //dojo.replaceClass(this.node, prefix + newState, prefix + this[state]); + dojo.replaceClass(this.node, prefix + newState, prefix + this[state]); + this[state] = newState; + }, + + _addItemClass: function(node, type){ + // summary: + // Adds a class with prefix "dojoDndItem" + // node: Node + // A node + // type: String + // A variable suffix for a class name + dojo.addClass(node, "dojoDndItem" + type); + }, + + _removeItemClass: function(node, type){ + // summary: + // Removes a class with prefix "dojoDndItem" + // node: Node + // A node + // type: String + // A variable suffix for a class name + dojo.removeClass(node, "dojoDndItem" + type); + }, + + onOverEvent: function(){ + // summary: + // This function is called once, when mouse is over our container + // tags: + // protected + this._changeState("Container", "Over"); + }, + + onOutEvent: function(){ + // summary: + // This function is called once, when mouse is out of our container + // tags: + // protected + this._changeState("Container", ""); + } +}); + } diff --git a/lib/dijit/tree/_dndSelector.js b/lib/dijit/tree/_dndSelector.js index 3d7795598..5f9310e71 100644 --- a/lib/dijit/tree/_dndSelector.js +++ b/lib/dijit/tree/_dndSelector.js @@ -1,125 +1,287 @@ /* - Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. Available via Academic Free License >= 2.1 OR the modified BSD license. see: http://dojotoolkit.org/license for details */ -if(!dojo._hasResource["dijit.tree._dndSelector"]){ -dojo._hasResource["dijit.tree._dndSelector"]=true; +if(!dojo._hasResource["dijit.tree._dndSelector"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.tree._dndSelector"] = true; dojo.provide("dijit.tree._dndSelector"); dojo.require("dojo.dnd.common"); dojo.require("dijit.tree._dndContainer"); -dojo.declare("dijit.tree._dndSelector",dijit.tree._dndContainer,{constructor:function(_1,_2){ -this.selection={}; -this.anchor=null; -this.simpleSelection=false; -this.events.push(dojo.connect(this.tree.domNode,"onmousedown",this,"onMouseDown"),dojo.connect(this.tree.domNode,"onmouseup",this,"onMouseUp"),dojo.connect(this.tree.domNode,"onmousemove",this,"onMouseMove")); -},singular:false,getSelectedNodes:function(){ -return this.selection; -},selectNone:function(){ -return this._removeSelection()._removeAnchor(); -},destroy:function(){ -this.inherited(arguments); -this.selection=this.anchor=null; -},onMouseDown:function(e){ -if(!this.current){ -return; -} -if(e.button==dojo.mouseButtons.RIGHT){ -return; -} -var _3=dijit.getEnclosingWidget(this.current),id=_3.id+"-dnd"; -if(!dojo.hasAttr(this.current,"id")){ -dojo.attr(this.current,"id",id); -} -if(!this.singular&&!dojo.isCopyKey(e)&&!e.shiftKey&&(this.current.id in this.selection)){ -this.simpleSelection=true; -dojo.stopEvent(e); -return; -} -if(this.singular){ -if(this.anchor==this.current){ -if(dojo.isCopyKey(e)){ -this.selectNone(); -} -}else{ -this.selectNone(); -this.anchor=this.current; -this._addItemClass(this.anchor,"Anchor"); -this.selection[this.current.id]=this.current; -} -}else{ -if(!this.singular&&e.shiftKey){ -if(dojo.isCopyKey(e)){ -}else{ -} -}else{ -if(dojo.isCopyKey(e)){ -if(this.anchor==this.current){ -delete this.selection[this.anchor.id]; -this._removeAnchor(); -}else{ -if(this.current.id in this.selection){ -this._removeItemClass(this.current,"Selected"); -delete this.selection[this.current.id]; -}else{ -if(this.anchor){ -this._removeItemClass(this.anchor,"Anchor"); -this._addItemClass(this.anchor,"Selected"); -} -this.anchor=this.current; -this._addItemClass(this.current,"Anchor"); -this.selection[this.current.id]=this.current; -} -} -}else{ -if(!(id in this.selection)){ -this.selectNone(); -this.anchor=this.current; -this._addItemClass(this.current,"Anchor"); -this.selection[id]=this.current; -} -} -} -} -dojo.stopEvent(e); -},onMouseUp:function(e){ -if(!this.simpleSelection){ -return; -} -this.simpleSelection=false; -this.selectNone(); -if(this.current){ -this.anchor=this.current; -this._addItemClass(this.anchor,"Anchor"); -this.selection[this.current.id]=this.current; -} -},onMouseMove:function(e){ -this.simpleSelection=false; -},_removeSelection:function(){ -var e=dojo.dnd._empty; -for(var i in this.selection){ -if(i in e){ -continue; -} -var _4=dojo.byId(i); -if(_4){ -this._removeItemClass(_4,"Selected"); -} -} -this.selection={}; -return this; -},_removeAnchor:function(){ -if(this.anchor){ -this._removeItemClass(this.anchor,"Anchor"); -this.anchor=null; -} -return this; -},forInSelectedItems:function(f,o){ -o=o||dojo.global; -for(var id in this.selection){ -f.call(o,this.getItem(id),id,this); -} -}}); + + +dojo.declare("dijit.tree._dndSelector", + dijit.tree._dndContainer, + { + // summary: + // This is a base class for `dijit.tree.dndSource` , and isn't meant to be used directly. + // It's based on `dojo.dnd.Selector`. + // tags: + // protected + + /*===== + // selection: Hash<String, DomNode> + // (id, DomNode) map for every TreeNode that's currently selected. + // The DOMNode is the TreeNode.rowNode. + selection: {}, + =====*/ + + constructor: function(tree, params){ + // summary: + // Initialization + // tags: + // private + + this.selection={}; + this.anchor = null; + + dijit.setWaiState(this.tree.domNode, "multiselect", !this.singular); + + this.events.push( + dojo.connect(this.tree.domNode, "onmousedown", this,"onMouseDown"), + dojo.connect(this.tree.domNode, "onmouseup", this,"onMouseUp"), + dojo.connect(this.tree.domNode, "onmousemove", this,"onMouseMove") + ); + }, + + // singular: Boolean + // Allows selection of only one element, if true. + // Tree hasn't been tested in singular=true mode, unclear if it works. + singular: false, + + // methods + getSelectedTreeNodes: function(){ + // summary: + // Returns a list of selected node(s). + // Used by dndSource on the start of a drag. + // tags: + // protected + var nodes=[], sel = this.selection; + for(var i in sel){ + nodes.push(sel[i]); + } + return nodes; + }, + + selectNone: function(){ + // summary: + // Unselects all items + // tags: + // private + + this.setSelection([]); + return this; // self + }, + + destroy: function(){ + // summary: + // Prepares the object to be garbage-collected + this.inherited(arguments); + this.selection = this.anchor = null; + }, + addTreeNode: function(/*dijit._TreeNode*/node, /*Boolean?*/isAnchor){ + // summary + // add node to current selection + // node: Node + // node to add + // isAnchor: Boolean + // Whether the node should become anchor. + + this.setSelection(this.getSelectedTreeNodes().concat( [node] )); + if(isAnchor){ this.anchor = node; } + return node; + }, + removeTreeNode: function(/*dijit._TreeNode*/node){ + // summary + // remove node from current selection + // node: Node + // node to remove + this.setSelection(this._setDifference(this.getSelectedTreeNodes(), [node])) + return node; + }, + isTreeNodeSelected: function(/*dijit._TreeNode*/node){ + // summary + // return true if node is currently selected + // node: Node + // the node to check whether it's in the current selection + + return node.id && !!this.selection[node.id]; + }, + setSelection: function(/*dijit._treeNode[]*/ newSelection){ + // summary + // set the list of selected nodes to be exactly newSelection. All changes to the + // selection should be passed through this function, which ensures that derived + // attributes are kept up to date. Anchor will be deleted if it has been removed + // from the selection, but no new anchor will be added by this function. + // newSelection: Node[] + // list of tree nodes to make selected + var oldSelection = this.getSelectedTreeNodes(); + dojo.forEach(this._setDifference(oldSelection, newSelection), dojo.hitch(this, function(node){ + node.setSelected(false); + if(this.anchor == node){ + delete this.anchor; + } + delete this.selection[node.id]; + })); + dojo.forEach(this._setDifference(newSelection, oldSelection), dojo.hitch(this, function(node){ + node.setSelected(true); + this.selection[node.id] = node; + })); + this._updateSelectionProperties(); + }, + _setDifference: function(xs,ys){ + // summary + // Returns a copy of xs which lacks any objects + // occurring in ys. Checks for membership by + // modifying and then reading the object, so it will + // not properly handle sets of numbers or strings. + + dojo.forEach(ys, function(y){ y.__exclude__ = true; }); + var ret = dojo.filter(xs, function(x){ return !x.__exclude__; }); + + // clean up after ourselves. + dojo.forEach(ys, function(y){ delete y['__exclude__'] }); + return ret; + }, + _updateSelectionProperties: function() { + // summary + // Update the following tree properties from the current selection: + // path[s], selectedItem[s], selectedNode[s] + + var selected = this.getSelectedTreeNodes(); + var paths = [], nodes = []; + dojo.forEach(selected, function(node) { + nodes.push(node); + paths.push(node.getTreePath()); + }); + var items = dojo.map(nodes,function(node) { return node.item; }); + this.tree._set("paths", paths); + this.tree._set("path", paths[0] || []); + this.tree._set("selectedNodes", nodes); + this.tree._set("selectedNode", nodes[0] || null); + this.tree._set("selectedItems", items); + this.tree._set("selectedItem", items[0] || null); + }, + // mouse events + onMouseDown: function(e){ + // summary: + // Event processor for onmousedown + // e: Event + // mouse event + // tags: + // protected + + // ignore click on expando node + if(!this.current || this.tree.isExpandoNode( e.target, this.current)){ return; } + + if(e.button == dojo.mouseButtons.RIGHT){ return; } // ignore right-click + + dojo.stopEvent(e); + + var treeNode = this.current, + copy = dojo.isCopyKey(e), id = treeNode.id; + + // if shift key is not pressed, and the node is already in the selection, + // delay deselection until onmouseup so in the case of DND, deselection + // will be canceled by onmousemove. + if(!this.singular && !e.shiftKey && this.selection[id]){ + this._doDeselect = true; + return; + }else{ + this._doDeselect = false; + } + this.userSelect(treeNode, copy, e.shiftKey); + }, + + onMouseUp: function(e){ + // summary: + // Event processor for onmouseup + // e: Event + // mouse event + // tags: + // protected + + // _doDeselect is the flag to indicate that the user wants to either ctrl+click on + // a already selected item (to deselect the item), or click on a not-yet selected item + // (which should remove all current selection, and add the clicked item). This can not + // be done in onMouseDown, because the user may start a drag after mousedown. By moving + // the deselection logic here, the user can drags an already selected item. + if(!this._doDeselect){ return; } + this._doDeselect = false; + this.userSelect(this.current, dojo.isCopyKey( e ), e.shiftKey); + }, + onMouseMove: function(e){ + // summary + // event processor for onmousemove + // e: Event + // mouse event + this._doDeselect = false; + }, + + userSelect: function(node, multi, range){ + // summary: + // Add or remove the given node from selection, responding + // to a user action such as a click or keypress. + // multi: Boolean + // Indicates whether this is meant to be a multi-select action (e.g. ctrl-click) + // range: Boolean + // Indicates whether this is meant to be a ranged action (e.g. shift-click) + // tags: + // protected + + if(this.singular){ + if(this.anchor == node && multi){ + this.selectNone(); + }else{ + this.setSelection([node]); + this.anchor = node; + } + }else{ + if(range && this.anchor){ + var cr = dijit.tree._compareNodes(this.anchor.rowNode, node.rowNode), + begin, end, anchor = this.anchor; + + if(cr < 0){ //current is after anchor + begin = anchor; + end = node; + }else{ //current is before anchor + begin = node; + end = anchor; + } + nodes = []; + //add everything betweeen begin and end inclusively + while(begin != end) { + nodes.push(begin) + begin = this.tree._getNextNode(begin); + } + nodes.push(end) + + this.setSelection(nodes); + }else{ + if( this.selection[ node.id ] && multi ) { + this.removeTreeNode( node ); + } else if(multi) { + this.addTreeNode(node, true); + } else { + this.setSelection([node]); + this.anchor = node; + } + } + } + }, + + forInSelectedItems: function(/*Function*/ f, /*Object?*/ o){ + // summary: + // Iterates over selected items; + // see `dojo.dnd.Container.forInItems()` for details + o = o || dojo.global; + for(var id in this.selection){ + // console.log("selected item id: " + id); + f.call(o, this.getItem(id), id, this); + } + } +}); + } diff --git a/lib/dijit/tree/dndSource.js b/lib/dijit/tree/dndSource.js index ef129d9cb..4fc4660cc 100644 --- a/lib/dijit/tree/dndSource.js +++ b/lib/dijit/tree/dndSource.js @@ -1,240 +1,544 @@ /* - Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. Available via Academic Free License >= 2.1 OR the modified BSD license. see: http://dojotoolkit.org/license for details */ -if(!dojo._hasResource["dijit.tree.dndSource"]){ -dojo._hasResource["dijit.tree.dndSource"]=true; +if(!dojo._hasResource["dijit.tree.dndSource"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dijit.tree.dndSource"] = true; dojo.provide("dijit.tree.dndSource"); dojo.require("dijit.tree._dndSelector"); dojo.require("dojo.dnd.Manager"); -dojo.declare("dijit.tree.dndSource",dijit.tree._dndSelector,{isSource:true,accept:["text","treeNode"],copyOnly:false,dragThreshold:5,betweenThreshold:0,constructor:function(_1,_2){ -if(!_2){ -_2={}; -} -dojo.mixin(this,_2); -this.isSource=typeof _2.isSource=="undefined"?true:_2.isSource; -var _3=_2.accept instanceof Array?_2.accept:["text","treeNode"]; -this.accept=null; -if(_3.length){ -this.accept={}; -for(var i=0;i<_3.length;++i){ -this.accept[_3[i]]=1; -} -} -this.isDragging=false; -this.mouseDown=false; -this.targetAnchor=null; -this.targetBox=null; -this.dropPosition=""; -this._lastX=0; -this._lastY=0; -this.sourceState=""; -if(this.isSource){ -dojo.addClass(this.node,"dojoDndSource"); -} -this.targetState=""; -if(this.accept){ -dojo.addClass(this.node,"dojoDndTarget"); -} -this.topics=[dojo.subscribe("/dnd/source/over",this,"onDndSourceOver"),dojo.subscribe("/dnd/start",this,"onDndStart"),dojo.subscribe("/dnd/drop",this,"onDndDrop"),dojo.subscribe("/dnd/cancel",this,"onDndCancel")]; -},checkAcceptance:function(_4,_5){ -return true; -},copyState:function(_6){ -return this.copyOnly||_6; -},destroy:function(){ -this.inherited("destroy",arguments); -dojo.forEach(this.topics,dojo.unsubscribe); -this.targetAnchor=null; -},_onDragMouse:function(e){ -var m=dojo.dnd.manager(),_7=this.targetAnchor,_8=this.current,_9=this.currentWidget,_a=this.dropPosition; -var _b="Over"; -if(_8&&this.betweenThreshold>0){ -if(!this.targetBox||_7!=_8){ -this.targetBox=dojo.position(_8,true); -} -if((e.pageY-this.targetBox.y)<=this.betweenThreshold){ -_b="Before"; -}else{ -if((e.pageY-this.targetBox.y)>=(this.targetBox.h-this.betweenThreshold)){ -_b="After"; -} -} -} -if(_8!=_7||_b!=_a){ -if(_7){ -this._removeItemClass(_7,_a); -} -if(_8){ -this._addItemClass(_8,_b); -} -if(!_8){ -m.canDrop(false); -}else{ -if(_9==this.tree.rootNode&&_b!="Over"){ -m.canDrop(false); -}else{ -if(m.source==this&&(_8.id in this.selection)){ -m.canDrop(false); -}else{ -if(this.checkItemAcceptance(_8,m.source,_b.toLowerCase())&&!this._isParentChildDrop(m.source,_8)){ -m.canDrop(true); -}else{ -m.canDrop(false); -} -} -} -} -this.targetAnchor=_8; -this.dropPosition=_b; -} -},onMouseMove:function(e){ -if(this.isDragging&&this.targetState=="Disabled"){ -return; -} -this.inherited(arguments); -var m=dojo.dnd.manager(); -if(this.isDragging){ -this._onDragMouse(e); -}else{ -if(this.mouseDown&&this.isSource&&(Math.abs(e.pageX-this._lastX)>=this.dragThreshold||Math.abs(e.pageY-this._lastY)>=this.dragThreshold)){ -var n=this.getSelectedNodes(); -var _c=[]; -for(var i in n){ -_c.push(n[i]); -} -if(_c.length){ -m.startDrag(this,_c,this.copyState(dojo.isCopyKey(e))); -} -} -} -},onMouseDown:function(e){ -this.mouseDown=true; -this.mouseButton=e.button; -this._lastX=e.pageX; -this._lastY=e.pageY; -this.inherited("onMouseDown",arguments); -},onMouseUp:function(e){ -if(this.mouseDown){ -this.mouseDown=false; -this.inherited("onMouseUp",arguments); -} -},onMouseOut:function(){ -this.inherited(arguments); -this._unmarkTargetAnchor(); -},checkItemAcceptance:function(_d,_e,_f){ -return true; -},onDndSourceOver:function(_10){ -if(this!=_10){ -this.mouseDown=false; -this._unmarkTargetAnchor(); -}else{ -if(this.isDragging){ -var m=dojo.dnd.manager(); -m.canDrop(false); -} -} -},onDndStart:function(_11,_12,_13){ -if(this.isSource){ -this._changeState("Source",this==_11?(_13?"Copied":"Moved"):""); -} -var _14=this.checkAcceptance(_11,_12); -this._changeState("Target",_14?"":"Disabled"); -if(this==_11){ -dojo.dnd.manager().overSource(this); -} -this.isDragging=true; -},itemCreator:function(_15,_16,_17){ -return dojo.map(_15,function(_18){ -return {"id":_18.id,"name":_18.textContent||_18.innerText||""}; + + +/*===== +dijit.tree.__SourceArgs = function(){ + // summary: + // A dict of parameters for Tree source configuration. + // isSource: Boolean? + // Can be used as a DnD source. Defaults to true. + // accept: String[] + // List of accepted types (text strings) for a target; defaults to + // ["text", "treeNode"] + // copyOnly: Boolean? + // Copy items, if true, use a state of Ctrl key otherwise, + // dragThreshold: Number + // The move delay in pixels before detecting a drag; 0 by default + // betweenThreshold: Integer + // Distance from upper/lower edge of node to allow drop to reorder nodes + this.isSource = isSource; + this.accept = accept; + this.autoSync = autoSync; + this.copyOnly = copyOnly; + this.dragThreshold = dragThreshold; + this.betweenThreshold = betweenThreshold; +} +=====*/ + +dojo.declare("dijit.tree.dndSource", dijit.tree._dndSelector, { + // summary: + // Handles drag and drop operations (as a source or a target) for `dijit.Tree` + + // isSource: [private] Boolean + // Can be used as a DnD source. + isSource: true, + + // accept: String[] + // List of accepted types (text strings) for the Tree; defaults to + // ["text"] + accept: ["text", "treeNode"], + + // copyOnly: [private] Boolean + // Copy items, if true, use a state of Ctrl key otherwise + copyOnly: false, + + // dragThreshold: Number + // The move delay in pixels before detecting a drag; 5 by default + dragThreshold: 5, + + // betweenThreshold: Integer + // Distance from upper/lower edge of node to allow drop to reorder nodes + betweenThreshold: 0, + + constructor: function(/*dijit.Tree*/ tree, /*dijit.tree.__SourceArgs*/ params){ + // summary: + // a constructor of the Tree DnD Source + // tags: + // private + if(!params){ params = {}; } + dojo.mixin(this, params); + this.isSource = typeof params.isSource == "undefined" ? true : params.isSource; + var type = params.accept instanceof Array ? params.accept : ["text", "treeNode"]; + this.accept = null; + if(type.length){ + this.accept = {}; + for(var i = 0; i < type.length; ++i){ + this.accept[type[i]] = 1; + } + } + + // class-specific variables + this.isDragging = false; + this.mouseDown = false; + this.targetAnchor = null; // DOMNode corresponding to the currently moused over TreeNode + this.targetBox = null; // coordinates of this.targetAnchor + this.dropPosition = ""; // whether mouse is over/after/before this.targetAnchor + this._lastX = 0; + this._lastY = 0; + + // states + this.sourceState = ""; + if(this.isSource){ + dojo.addClass(this.node, "dojoDndSource"); + } + this.targetState = ""; + if(this.accept){ + dojo.addClass(this.node, "dojoDndTarget"); + } + + // set up events + this.topics = [ + dojo.subscribe("/dnd/source/over", this, "onDndSourceOver"), + dojo.subscribe("/dnd/start", this, "onDndStart"), + dojo.subscribe("/dnd/drop", this, "onDndDrop"), + dojo.subscribe("/dnd/cancel", this, "onDndCancel") + ]; + }, + + // methods + checkAcceptance: function(source, nodes){ + // summary: + // Checks if the target can accept nodes from this source + // source: dijit.tree.dndSource + // The source which provides items + // nodes: DOMNode[] + // Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if + // source is a dijit.Tree. + // tags: + // extension + return true; // Boolean + }, + + copyState: function(keyPressed){ + // summary: + // Returns true, if we need to copy items, false to move. + // It is separated to be overwritten dynamically, if needed. + // keyPressed: Boolean + // The "copy" control key was pressed + // tags: + // protected + return this.copyOnly || keyPressed; // Boolean + }, + destroy: function(){ + // summary: + // Prepares the object to be garbage-collected. + this.inherited("destroy",arguments); + dojo.forEach(this.topics, dojo.unsubscribe); + this.targetAnchor = null; + }, + + _onDragMouse: function(e){ + // summary: + // Helper method for processing onmousemove/onmouseover events while drag is in progress. + // Keeps track of current drop target. + + var m = dojo.dnd.manager(), + oldTarget = this.targetAnchor, // the TreeNode corresponding to TreeNode mouse was previously over + newTarget = this.current, // TreeNode corresponding to TreeNode mouse is currently over + oldDropPosition = this.dropPosition; // the previous drop position (over/before/after) + + // calculate if user is indicating to drop the dragged node before, after, or over + // (i.e., to become a child of) the target node + var newDropPosition = "Over"; + if(newTarget && this.betweenThreshold > 0){ + // If mouse is over a new TreeNode, then get new TreeNode's position and size + if(!this.targetBox || oldTarget != newTarget){ + this.targetBox = dojo.position(newTarget.rowNode, true); + } + if((e.pageY - this.targetBox.y) <= this.betweenThreshold){ + newDropPosition = "Before"; + }else if((e.pageY - this.targetBox.y) >= (this.targetBox.h - this.betweenThreshold)){ + newDropPosition = "After"; + } + } + + if(newTarget != oldTarget || newDropPosition != oldDropPosition){ + if(oldTarget){ + this._removeItemClass(oldTarget.rowNode, oldDropPosition); + } + if(newTarget){ + this._addItemClass(newTarget.rowNode, newDropPosition); + } + + // Check if it's ok to drop the dragged node on/before/after the target node. + if(!newTarget){ + m.canDrop(false); + }else if(newTarget == this.tree.rootNode && newDropPosition != "Over"){ + // Can't drop before or after tree's root node; the dropped node would just disappear (at least visually) + m.canDrop(false); + }else if(m.source == this && (newTarget.id in this.selection)){ + // Guard against dropping onto yourself (TODO: guard against dropping onto your descendant, #7140) + m.canDrop(false); + }else if(this.checkItemAcceptance(newTarget.rowNode, m.source, newDropPosition.toLowerCase()) + && !this._isParentChildDrop(m.source, newTarget.rowNode)){ + m.canDrop(true); + }else{ + m.canDrop(false); + } + + this.targetAnchor = newTarget; + this.dropPosition = newDropPosition; + } + }, + + onMouseMove: function(e){ + // summary: + // Called for any onmousemove events over the Tree + // e: Event + // onmousemouse event + // tags: + // private + if(this.isDragging && this.targetState == "Disabled"){ return; } + this.inherited(arguments); + var m = dojo.dnd.manager(); + if(this.isDragging){ + this._onDragMouse(e); + }else{ + if(this.mouseDown && this.isSource && + (Math.abs(e.pageX-this._lastX)>=this.dragThreshold || Math.abs(e.pageY-this._lastY)>=this.dragThreshold)){ + var nodes = this.getSelectedTreeNodes(); + if(nodes.length){ + if(nodes.length > 1){ + //filter out all selected items which has one of their ancestor selected as well + var seen = this.selection, i = 0, r = [], n, p; + nextitem: while((n = nodes[i++])){ + for(p = n.getParent(); p && p !== this.tree; p = p.getParent()){ + if(seen[p.id]){ //parent is already selected, skip this node + continue nextitem; + } + } + //this node does not have any ancestors selected, add it + r.push(n); + } + nodes = r; + } + nodes = dojo.map(nodes, function(n){return n.domNode}); + m.startDrag(this, nodes, this.copyState(dojo.isCopyKey(e))); + } + } + } + }, + + onMouseDown: function(e){ + // summary: + // Event processor for onmousedown + // e: Event + // onmousedown event + // tags: + // private + this.mouseDown = true; + this.mouseButton = e.button; + this._lastX = e.pageX; + this._lastY = e.pageY; + this.inherited(arguments); + }, + + onMouseUp: function(e){ + // summary: + // Event processor for onmouseup + // e: Event + // onmouseup event + // tags: + // private + if(this.mouseDown){ + this.mouseDown = false; + this.inherited(arguments); + } + }, + + onMouseOut: function(){ + // summary: + // Event processor for when mouse is moved away from a TreeNode + // tags: + // private + this.inherited(arguments); + this._unmarkTargetAnchor(); + }, + + checkItemAcceptance: function(target, source, position){ + // summary: + // Stub function to be overridden if one wants to check for the ability to drop at the node/item level + // description: + // In the base case, this is called to check if target can become a child of source. + // When betweenThreshold is set, position="before" or "after" means that we + // are asking if the source node can be dropped before/after the target node. + // target: DOMNode + // The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to + // Use dijit.getEnclosingWidget(target) to get the TreeNode. + // source: dijit.tree.dndSource + // The (set of) nodes we are dropping + // position: String + // "over", "before", or "after" + // tags: + // extension + return true; + }, + + // topic event processors + onDndSourceOver: function(source){ + // summary: + // Topic event processor for /dnd/source/over, called when detected a current source. + // source: Object + // The dijit.tree.dndSource / dojo.dnd.Source which has the mouse over it + // tags: + // private + if(this != source){ + this.mouseDown = false; + this._unmarkTargetAnchor(); + }else if(this.isDragging){ + var m = dojo.dnd.manager(); + m.canDrop(false); + } + }, + onDndStart: function(source, nodes, copy){ + // summary: + // Topic event processor for /dnd/start, called to initiate the DnD operation + // source: Object + // The dijit.tree.dndSource / dojo.dnd.Source which is providing the items + // nodes: DomNode[] + // The list of transferred items, dndTreeNode nodes if dragging from a Tree + // copy: Boolean + // Copy items, if true, move items otherwise + // tags: + // private + + if(this.isSource){ + this._changeState("Source", this == source ? (copy ? "Copied" : "Moved") : ""); + } + var accepted = this.checkAcceptance(source, nodes); + + this._changeState("Target", accepted ? "" : "Disabled"); + + if(this == source){ + dojo.dnd.manager().overSource(this); + } + + this.isDragging = true; + }, + + itemCreator: function(/*DomNode[]*/ nodes, target, /*dojo.dnd.Source*/ source){ + // summary: + // Returns objects passed to `Tree.model.newItem()` based on DnD nodes + // dropped onto the tree. Developer must override this method to enable + // dropping from external sources onto this Tree, unless the Tree.model's items + // happen to look like {id: 123, name: "Apple" } with no other attributes. + // description: + // For each node in nodes[], which came from source, create a hash of name/value + // pairs to be passed to Tree.model.newItem(). Returns array of those hashes. + // returns: Object[] + // Array of name/value hashes for each new item to be added to the Tree, like: + // | [ + // | { id: 123, label: "apple", foo: "bar" }, + // | { id: 456, label: "pear", zaz: "bam" } + // | ] + // tags: + // extension + + // TODO: for 2.0 refactor so itemCreator() is called once per drag node, and + // make signature itemCreator(sourceItem, node, target) (or similar). + + return dojo.map(nodes, function(node){ + return { + "id": node.id, + "name": node.textContent || node.innerText || "" + }; + }); // Object[] + }, + + onDndDrop: function(source, nodes, copy){ + // summary: + // Topic event processor for /dnd/drop, called to finish the DnD operation. + // description: + // Updates data store items according to where node was dragged from and dropped + // to. The tree will then respond to those data store updates and redraw itself. + // source: Object + // The dijit.tree.dndSource / dojo.dnd.Source which is providing the items + // nodes: DomNode[] + // The list of transferred items, dndTreeNode nodes if dragging from a Tree + // copy: Boolean + // Copy items, if true, move items otherwise + // tags: + // protected + if(this.containerState == "Over"){ + var tree = this.tree, + model = tree.model, + target = this.targetAnchor, + requeryRoot = false; // set to true iff top level items change + + this.isDragging = false; + + // Compute the new parent item + var targetWidget = target; + var newParentItem; + var insertIndex; + newParentItem = (targetWidget && targetWidget.item) || tree.item; + if(this.dropPosition == "Before" || this.dropPosition == "After"){ + // TODO: if there is no parent item then disallow the drop. + // Actually this should be checked during onMouseMove too, to make the drag icon red. + newParentItem = (targetWidget.getParent() && targetWidget.getParent().item) || tree.item; + // Compute the insert index for reordering + insertIndex = targetWidget.getIndexInParent(); + if(this.dropPosition == "After"){ + insertIndex = targetWidget.getIndexInParent() + 1; + } + }else{ + newParentItem = (targetWidget && targetWidget.item) || tree.item; + } + + // If necessary, use this variable to hold array of hashes to pass to model.newItem() + // (one entry in the array for each dragged node). + var newItemsParams; + + dojo.forEach(nodes, function(node, idx){ + // dojo.dnd.Item representing the thing being dropped. + // Don't confuse the use of item here (meaning a DnD item) with the + // uses below where item means dojo.data item. + var sourceItem = source.getItem(node.id); + + // Information that's available if the source is another Tree + // (possibly but not necessarily this tree, possibly but not + // necessarily the same model as this Tree) + if(dojo.indexOf(sourceItem.type, "treeNode") != -1){ + var childTreeNode = sourceItem.data, + childItem = childTreeNode.item, + oldParentItem = childTreeNode.getParent().item; + } + + if(source == this){ + // This is a node from my own tree, and we are moving it, not copying. + // Remove item from old parent's children attribute. + // TODO: dijit.tree.dndSelector should implement deleteSelectedNodes() + // and this code should go there. + + if(typeof insertIndex == "number"){ + if(newParentItem == oldParentItem && childTreeNode.getIndexInParent() < insertIndex){ + insertIndex -= 1; + } + } + model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex); + }else if(model.isItem(childItem)){ + // Item from same model + // (maybe we should only do this branch if the source is a tree?) + model.pasteItem(childItem, oldParentItem, newParentItem, copy, insertIndex); + }else{ + // Get the hash to pass to model.newItem(). A single call to + // itemCreator() returns an array of hashes, one for each drag source node. + if(!newItemsParams){ + newItemsParams = this.itemCreator(nodes, target.rowNode, source); + } + + // Create new item in the tree, based on the drag source. + model.newItem(newItemsParams[idx], newParentItem, insertIndex); + } + }, this); + + // Expand the target node (if it's currently collapsed) so the user can see + // where their node was dropped. In particular since that node is still selected. + this.tree._expandNode(targetWidget); + } + this.onDndCancel(); + }, + + onDndCancel: function(){ + // summary: + // Topic event processor for /dnd/cancel, called to cancel the DnD operation + // tags: + // private + this._unmarkTargetAnchor(); + this.isDragging = false; + this.mouseDown = false; + delete this.mouseButton; + this._changeState("Source", ""); + this._changeState("Target", ""); + }, + + // When focus moves in/out of the entire Tree + onOverEvent: function(){ + // summary: + // This method is called when mouse is moved over our container (like onmouseenter) + // tags: + // private + this.inherited(arguments); + dojo.dnd.manager().overSource(this); + }, + onOutEvent: function(){ + // summary: + // This method is called when mouse is moved out of our container (like onmouseleave) + // tags: + // private + this._unmarkTargetAnchor(); + var m = dojo.dnd.manager(); + if(this.isDragging){ + m.canDrop(false); + } + m.outSource(this); + + this.inherited(arguments); + }, + + _isParentChildDrop: function(source, targetRow){ + // summary: + // Checks whether the dragged items are parent rows in the tree which are being + // dragged into their own children. + // + // source: + // The DragSource object. + // + // targetRow: + // The tree row onto which the dragged nodes are being dropped. + // + // tags: + // private + + // If the dragged object is not coming from the tree this widget belongs to, + // it cannot be invalid. + if(!source.tree || source.tree != this.tree){ + return false; + } + + + var root = source.tree.domNode; + var ids = source.selection; + + var node = targetRow.parentNode; + + // Iterate up the DOM hierarchy from the target drop row, + // checking of any of the dragged nodes have the same ID. + while(node != root && !ids[node.id]){ + node = node.parentNode; + } + + return node.id && ids[node.id]; + }, + + _unmarkTargetAnchor: function(){ + // summary: + // Removes hover class of the current target anchor + // tags: + // private + if(!this.targetAnchor){ return; } + this._removeItemClass(this.targetAnchor.rowNode, this.dropPosition); + this.targetAnchor = null; + this.targetBox = null; + this.dropPosition = null; + }, + + _markDndStatus: function(copy){ + // summary: + // Changes source's state based on "copy" status + this._changeState("Source", copy ? "Copied" : "Moved"); + } }); -},onDndDrop:function(_19,_1a,_1b){ -if(this.containerState=="Over"){ -var _1c=this.tree,_1d=_1c.model,_1e=this.targetAnchor,_1f=false; -this.isDragging=false; -var _20=dijit.getEnclosingWidget(_1e); -var _21; -var _22; -_21=(_20&&_20.item)||_1c.item; -if(this.dropPosition=="Before"||this.dropPosition=="After"){ -_21=(_20.getParent()&&_20.getParent().item)||_1c.item; -_22=_20.getIndexInParent(); -if(this.dropPosition=="After"){ -_22=_20.getIndexInParent()+1; -} -}else{ -_21=(_20&&_20.item)||_1c.item; -} -var _23; -dojo.forEach(_1a,function(_24,idx){ -var _25=_19.getItem(_24.id); -if(dojo.indexOf(_25.type,"treeNode")!=-1){ -var _26=_25.data,_27=_26.item,_28=_26.getParent().item; -} -if(_19==this){ -if(typeof _22=="number"){ -if(_21==_28&&_26.getIndexInParent()<_22){ -_22-=1; -} -} -_1d.pasteItem(_27,_28,_21,_1b,_22); -}else{ -if(_1d.isItem(_27)){ -_1d.pasteItem(_27,_28,_21,_1b,_22); -}else{ -if(!_23){ -_23=this.itemCreator(_1a,_1e,_19); -} -_1d.newItem(_23[idx],_21,_22); -} -} -},this); -this.tree._expandNode(_20); -} -this.onDndCancel(); -},onDndCancel:function(){ -this._unmarkTargetAnchor(); -this.isDragging=false; -this.mouseDown=false; -delete this.mouseButton; -this._changeState("Source",""); -this._changeState("Target",""); -},onOverEvent:function(){ -this.inherited(arguments); -dojo.dnd.manager().overSource(this); -},onOutEvent:function(){ -this._unmarkTargetAnchor(); -var m=dojo.dnd.manager(); -if(this.isDragging){ -m.canDrop(false); -} -m.outSource(this); -this.inherited(arguments); -},_isParentChildDrop:function(_29,_2a){ -if(!_29.tree||_29.tree!=this.tree){ -return false; -} -var _2b=_29.tree.domNode; -var ids={}; -for(var x in _29.selection){ -ids[_29.selection[x].parentNode.id]=true; -} -var _2c=_2a.parentNode; -while(_2c!=_2b&&(!_2c.id||!ids[_2c.id])){ -_2c=_2c.parentNode; -} -return _2c.id&&ids[_2c.id]; -},_unmarkTargetAnchor:function(){ -if(!this.targetAnchor){ -return; -} -this._removeItemClass(this.targetAnchor,this.dropPosition); -this.targetAnchor=null; -this.targetBox=null; -this.dropPosition=null; -},_markDndStatus:function(_2d){ -this._changeState("Source",_2d?"Copied":"Moved"); -}}); + } diff --git a/lib/dijit/tree/model.js b/lib/dijit/tree/model.js index 091d72fca..46b27c676 100644 --- a/lib/dijit/tree/model.js +++ b/lib/dijit/tree/model.js @@ -1,20 +1,135 @@ /* - Copyright (c) 2004-2010, The Dojo Foundation All Rights Reserved. + Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved. Available via Academic Free License >= 2.1 OR the modified BSD license. see: http://dojotoolkit.org/license for details */ -dojo.declare("dijit.tree.model",null,{destroy:function(){ -},getRoot:function(_1){ -},mayHaveChildren:function(_2){ -},getChildren:function(_3,_4){ -},isItem:function(_5){ -},fetchItemByIdentity:function(_6){ -},getIdentity:function(_7){ -},getLabel:function(_8){ -},newItem:function(_9,_a,_b){ -},pasteItem:function(_c,_d,_e,_f){ -},onChange:function(_10){ -},onChildrenChange:function(_11,_12){ -}}); + +dojo.declare( + "dijit.tree.model", + null, +{ + // summary: + // Contract for any data provider object for the tree. + // description: + // Tree passes in values to the constructor to specify the callbacks. + // "item" is typically a dojo.data.Item but it's just a black box so + // it could be anything. + // + // This (like `dojo.data.api.Read`) is just documentation, and not meant to be used. + + destroy: function(){ + // summary: + // Destroys this object, releasing connections to the store + // tags: + // extension + }, + + // ======================================================================= + // Methods for traversing hierarchy + + getRoot: function(onItem){ + // summary: + // Calls onItem with the root item for the tree, possibly a fabricated item. + // Throws exception on error. + // tags: + // extension + }, + + mayHaveChildren: function(/*dojo.data.Item*/ item){ + // summary: + // Tells if an item has or may have children. Implementing logic here + // avoids showing +/- expando icon for nodes that we know don't have children. + // (For efficiency reasons we may not want to check if an element actually + // has children until user clicks the expando node) + // tags: + // extension + }, + + getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete){ + // summary: + // Calls onComplete() with array of child items of given parent item, all loaded. + // Throws exception on error. + // tags: + // extension + }, + + // ======================================================================= + // Inspecting items + + isItem: function(/* anything */ something){ + // summary: + // Returns true if *something* is an item and came from this model instance. + // Returns false if *something* is a literal, an item from another model instance, + // or is any object other than an item. + // tags: + // extension + }, + + fetchItemByIdentity: function(/* object */ keywordArgs){ + // summary: + // Given the identity of an item, this method returns the item that has + // that identity through the onItem callback. Conforming implementations + // should return null if there is no item with the given identity. + // Implementations of fetchItemByIdentity() may sometimes return an item + // from a local cache and may sometimes fetch an item from a remote server. + // tags: + // extension + }, + + getIdentity: function(/* item */ item){ + // summary: + // Returns identity for an item + // tags: + // extension + }, + + getLabel: function(/*dojo.data.Item*/ item){ + // summary: + // Get the label for an item + // tags: + // extension + }, + + // ======================================================================= + // Write interface + + newItem: function(/* dojo.dnd.Item */ args, /*Item*/ parent, /*int?*/ insertIndex){ + // summary: + // Creates a new item. See `dojo.data.api.Write` for details on args. + // tags: + // extension + }, + + pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy){ + // summary: + // Move or copy an item from one parent item to another. + // Used in drag & drop. + // If oldParentItem is specified and bCopy is false, childItem is removed from oldParentItem. + // If newParentItem is specified, childItem is attached to newParentItem. + // tags: + // extension + }, + + // ======================================================================= + // Callbacks + + onChange: function(/*dojo.data.Item*/ item){ + // summary: + // Callback whenever an item has changed, so that Tree + // can update the label, icon, etc. Note that changes + // to an item's children or parent(s) will trigger an + // onChildrenChange() so you can ignore those changes here. + // tags: + // callback + }, + + onChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){ + // summary: + // Callback to do notifications about new, updated, or deleted items. + // tags: + // callback + } +}); + |