summaryrefslogtreecommitdiff
path: root/lib/dijit/tree/TreeStoreModel.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dijit/tree/TreeStoreModel.js')
-rw-r--r--lib/dijit/tree/TreeStoreModel.js510
1 files changed, 373 insertions, 137 deletions
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);
+ }
+ }
+ });
+
}