1 define("dijit/tree/TreeStoreModel", [
2 "dojo/_base/array", // array.filter array.forEach array.indexOf array.some
3 "dojo/aspect", // aspect.after
4 "dojo/_base/declare", // declare
5 "dojo/_base/json", // json.stringify
6 "dojo/_base/lang" // lang.hitch
7 ], function(array
, aspect
, declare
, json
, lang
){
10 // dijit/tree/TreeStoreModel
12 // Implements dijit.Tree.model connecting to a dojo.data store with a single
15 return declare("dijit.tree.TreeStoreModel", null, {
17 // Implements dijit.Tree.model connecting to a dojo.data store with a single
18 // root item. Any methods passed into the constructor will override
19 // the ones defined here.
21 // store: dojo.data.Store
25 // childrenAttrs: String[]
26 // One or more attribute names (attributes in the dojo.data item) that specify that item's children
27 childrenAttrs
: ["children"],
29 // newItemIdAttr: String
30 // Name of attribute in the Object passed to newItem() that specifies the id.
32 // If newItemIdAttr is set then it's used when newItem() is called to see if an
33 // item with the same id already exists, and if so just links to the old item
34 // (so that the old item ends up with two parents).
36 // Setting this to null or "" will make every drop create a new item.
40 // If specified, get label for tree node from this attribute, rather
41 // than by calling store.getLabel()
44 // root: [readonly] dojo.data.Item
45 // Pointer to the root item (read only, not a parameter)
49 // Specifies datastore query to return the root item for the tree.
50 // Must only return a single item. Alternately can just pass in pointer
56 // deferItemLoadingUntilExpand: Boolean
57 // Setting this to true will cause the TreeStoreModel to defer calling loadItem on nodes
58 // until they are expanded. This allows for lazying loading where only one
59 // loadItem (and generally one network call, consequently) per expansion
60 // (rather than one for each child).
61 // This relies on partial loading of the children items; each children item of a
62 // fully loaded item should contain the label and info about having children.
63 deferItemLoadingUntilExpand
: false,
65 constructor: function(/* Object */ args
){
67 // Passed the arguments listed above (store, etc)
71 lang
.mixin(this, args
);
75 var store
= this.store
;
76 if(!store
.getFeatures()['dojo.data.api.Identity']){
77 throw new Error("dijit.Tree: store must support dojo.data.Identity");
80 // if the store supports Notification, subscribe to the notification events
81 if(store
.getFeatures()['dojo.data.api.Notification']){
82 this.connects
= this.connects
.concat([
83 aspect
.after(store
, "onNew", lang
.hitch(this, "onNewItem"), true),
84 aspect
.after(store
, "onDelete", lang
.hitch(this, "onDeleteItem"), true),
85 aspect
.after(store
, "onSet", lang
.hitch(this, "onSetItem"), true)
92 while(h
= this.connects
.pop()){ h
.remove(); }
93 // TODO: should cancel any in-progress processing of getRoot(), getChildren()
96 // =======================================================================
97 // Methods for traversing hierarchy
99 getRoot: function(onItem
, onError
){
101 // Calls onItem with the root item for the tree, possibly a fabricated item.
102 // Calls onError on error.
108 onComplete
: lang
.hitch(this, function(items
){
109 if(items
.length
!= 1){
110 throw new Error(this.declaredClass
+ ": query " + json
.stringify(this.query
) + " returned " + items
.length
+
111 " items, but must return exactly one item");
113 this.root
= items
[0];
121 mayHaveChildren: function(/*dojo.data.Item*/ item
){
123 // Tells if an item has or may have children. Implementing logic here
124 // avoids showing +/- expando icon for nodes that we know don't have children.
125 // (For efficiency reasons we may not want to check if an element actually
126 // has children until user clicks the expando node)
127 return array
.some(this.childrenAttrs
, function(attr
){
128 return this.store
.hasAttribute(item
, attr
);
132 getChildren: function(/*dojo.data.Item*/ parentItem
, /*function(items)*/ onComplete
, /*function*/ onError
){
134 // Calls onComplete() with array of child items of given parent item, all loaded.
136 var store
= this.store
;
137 if(!store
.isItemLoaded(parentItem
)){
138 // The parent is not loaded yet, we must be in deferItemLoadingUntilExpand
139 // mode, so we will load it and just return the children (without loading each
141 var getChildren
= lang
.hitch(this, arguments
.callee
);
144 onItem: function(parentItem
){
145 getChildren(parentItem
, onComplete
, onError
);
151 // get children of specified item
153 for(var i
=0; i
<this.childrenAttrs
.length
; i
++){
154 var vals
= store
.getValues(parentItem
, this.childrenAttrs
[i
]);
155 childItems
= childItems
.concat(vals
);
158 // count how many items need to be loaded
160 if(!this.deferItemLoadingUntilExpand
){
161 array
.forEach(childItems
, function(item
){ if(!store
.isItemLoaded(item
)){ _waitCount
++; } });
165 // all items are already loaded (or we aren't loading them). proceed...
166 onComplete(childItems
);
168 // still waiting for some or all of the items to load
169 array
.forEach(childItems
, function(item
, idx
){
170 if(!store
.isItemLoaded(item
)){
173 onItem: function(item
){
174 childItems
[idx
] = item
;
175 if(--_waitCount
== 0){
176 // all nodes have been loaded, send them to the tree
177 onComplete(childItems
);
187 // =======================================================================
190 isItem: function(/* anything */ something
){
191 return this.store
.isItem(something
); // Boolean
194 fetchItemByIdentity: function(/* object */ keywordArgs
){
195 this.store
.fetchItemByIdentity(keywordArgs
);
198 getIdentity: function(/* item */ item
){
199 return this.store
.getIdentity(item
); // Object
202 getLabel: function(/*dojo.data.Item*/ item
){
204 // Get the label for an item
206 return this.store
.getValue(item
,this.labelAttr
); // String
208 return this.store
.getLabel(item
); // String
212 // =======================================================================
215 newItem: function(/* dojo.dnd.Item */ args
, /*Item*/ parent
, /*int?*/ insertIndex
){
217 // Creates a new item. See `dojo.data.api.Write` for details on args.
218 // Used in drag & drop when item from external source dropped onto tree.
220 // Developers will need to override this method if new items get added
221 // to parents with multiple children attributes, in order to define which
222 // children attribute points to the new item.
224 var pInfo
= {parent
: parent
, attribute
: this.childrenAttrs
[0]}, LnewItem
;
226 if(this.newItemIdAttr
&& args
[this.newItemIdAttr
]){
227 // Maybe there's already a corresponding item in the store; if so, reuse it.
228 this.fetchItemByIdentity({identity
: args
[this.newItemIdAttr
], scope
: this, onItem: function(item
){
230 // There's already a matching item in store, use it
231 this.pasteItem(item
, null, parent
, true, insertIndex
);
233 // Create new item in the tree, based on the drag source.
234 LnewItem
=this.store
.newItem(args
, pInfo
);
235 if(LnewItem
&& (insertIndex
!=undefined)){
236 // Move new item to desired position
237 this.pasteItem(LnewItem
, parent
, parent
, false, insertIndex
);
242 // [as far as we know] there is no id so we must assume this is a new item
243 LnewItem
=this.store
.newItem(args
, pInfo
);
244 if(LnewItem
&& (insertIndex
!=undefined)){
245 // Move new item to desired position
246 this.pasteItem(LnewItem
, parent
, parent
, false, insertIndex
);
251 pasteItem: function(/*Item*/ childItem
, /*Item*/ oldParentItem
, /*Item*/ newParentItem
, /*Boolean*/ bCopy
, /*int?*/ insertIndex
){
253 // Move or copy an item from one parent item to another.
254 // Used in drag & drop
255 var store
= this.store
,
256 parentAttr
= this.childrenAttrs
[0]; // name of "children" attr in parent item
258 // remove child from source item, and record the attribute that child occurred in
260 array
.forEach(this.childrenAttrs
, function(attr
){
261 if(store
.containsValue(oldParentItem
, attr
, childItem
)){
263 var values
= array
.filter(store
.getValues(oldParentItem
, attr
), function(x
){
264 return x
!= childItem
;
266 store
.setValues(oldParentItem
, attr
, values
);
273 // modify target item's children attribute to include this item
275 if(typeof insertIndex
== "number"){
276 // call slice() to avoid modifying the original array, confusing the data store
277 var childItems
= store
.getValues(newParentItem
, parentAttr
).slice();
278 childItems
.splice(insertIndex
, 0, childItem
);
279 store
.setValues(newParentItem
, parentAttr
, childItems
);
281 store
.setValues(newParentItem
, parentAttr
,
282 store
.getValues(newParentItem
, parentAttr
).concat(childItem
));
287 // =======================================================================
290 onChange: function(/*dojo.data.Item*/ /*===== item =====*/){
292 // Callback whenever an item has changed, so that Tree
293 // can update the label, icon, etc. Note that changes
294 // to an item's children or parent(s) will trigger an
295 // onChildrenChange() so you can ignore those changes here.
300 onChildrenChange: function(/*===== parent, newChildrenList =====*/){
302 // Callback to do notifications about new, updated, or deleted items.
303 // parent: dojo.data.Item
304 // newChildrenList: dojo.data.Item[]
309 onDelete: function(/*dojo.data.Item*/ /*===== item =====*/){
311 // Callback when an item has been deleted.
313 // Note that there will also be an onChildrenChange() callback for the parent
319 // =======================================================================
320 // Events from data store
322 onNewItem: function(/* dojo.data.Item */ item
, /* Object */ parentInfo
){
324 // Handler for when new items appear in the store, either from a drop operation
325 // or some other way. Updates the tree view (if necessary).
327 // If the new item is a child of an existing item,
328 // calls onChildrenChange() with the new list of children
329 // for that existing item.
334 // We only care about the new item if it has a parent that corresponds to a TreeNode
335 // we are currently displaying
340 // Call onChildrenChange() on parent (ie, existing) item with new list of children
341 // In the common case, the new list of children is simply parentInfo.newValue or
342 // [ parentInfo.newValue ], although if items in the store has multiple
343 // child attributes (see `childrenAttr`), then it's a superset of parentInfo.newValue,
344 // so call getChildren() to be sure to get right answer.
345 this.getChildren(parentInfo
.item
, lang
.hitch(this, function(children
){
346 this.onChildrenChange(parentInfo
.item
, children
);
350 onDeleteItem: function(/*Object*/ item
){
352 // Handler for delete notifications from underlying store
356 onSetItem: function(item
, attribute
/*===== , oldValue, newValue =====*/){
358 // Updates the tree view according to changes in the data store.
360 // Handles updates to an item's children by calling onChildrenChange(), and
361 // other updates to an item by calling onChange().
363 // See `onNewItem` for more details on handling updates to an item's children.
365 // attribute: attribute-name-string
366 // oldValue: object | array
367 // newValue: object | array
371 if(array
.indexOf(this.childrenAttrs
, attribute
) != -1){
372 // item's children list changed
373 this.getChildren(item
, lang
.hitch(this, function(children
){
374 // See comments in onNewItem() about calling getChildren()
375 this.onChildrenChange(item
, children
);
378 // item's label/icon/etc. changed.