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/lang" // lang.hitch
6 ], function(array
, aspect
, declare
, lang
){
9 // dijit/tree/TreeStoreModel
11 return declare("dijit.tree.TreeStoreModel", null, {
13 // Implements dijit/Tree/model connecting to a dojo.data store with a single
14 // root item. Any methods passed into the constructor will override
15 // the ones defined here.
17 // store: dojo/data/api/Read
21 // childrenAttrs: String[]
22 // One or more attribute names (attributes in the dojo.data item) that specify that item's children
23 childrenAttrs
: ["children"],
25 // newItemIdAttr: String
26 // Name of attribute in the Object passed to newItem() that specifies the id.
28 // If newItemIdAttr is set then it's used when newItem() is called to see if an
29 // item with the same id already exists, and if so just links to the old item
30 // (so that the old item ends up with two parents).
32 // Setting this to null or "" will make every drop create a new item.
36 // If specified, get label for tree node from this attribute, rather
37 // than by calling store.getLabel()
40 // root: [readonly] dojo/data/Item
41 // Pointer to the root item (read only, not a parameter)
45 // Specifies datastore query to return the root item for the tree.
46 // Must only return a single item. Alternately can just pass in pointer
52 // deferItemLoadingUntilExpand: Boolean
53 // Setting this to true will cause the TreeStoreModel to defer calling loadItem on nodes
54 // until they are expanded. This allows for lazying loading where only one
55 // loadItem (and generally one network call, consequently) per expansion
56 // (rather than one for each child).
57 // This relies on partial loading of the children items; each children item of a
58 // fully loaded item should contain the label and info about having children.
59 deferItemLoadingUntilExpand
: false,
61 constructor: function(/* Object */ args
){
63 // Passed the arguments listed above (store, etc)
67 lang
.mixin(this, args
);
71 var store
= this.store
;
72 if(!store
.getFeatures()['dojo.data.api.Identity']){
73 throw new Error("dijit.tree.TreeStoreModel: store must support dojo.data.Identity");
76 // if the store supports Notification, subscribe to the notification events
77 if(store
.getFeatures()['dojo.data.api.Notification']){
78 this.connects
= this.connects
.concat([
79 aspect
.after(store
, "onNew", lang
.hitch(this, "onNewItem"), true),
80 aspect
.after(store
, "onDelete", lang
.hitch(this, "onDeleteItem"), true),
81 aspect
.after(store
, "onSet", lang
.hitch(this, "onSetItem"), true)
88 while(h
= this.connects
.pop()){ h
.remove(); }
89 // TODO: should cancel any in-progress processing of getRoot(), getChildren()
92 // =======================================================================
93 // Methods for traversing hierarchy
95 getRoot: function(onItem
, onError
){
97 // Calls onItem with the root item for the tree, possibly a fabricated item.
98 // Calls onError on error.
104 onComplete
: lang
.hitch(this, function(items
){
105 if(items
.length
!= 1){
106 throw new Error("dijit.tree.TreeStoreModel: root query returned " + items
.length
+
107 " items, but must return exactly one");
109 this.root
= items
[0];
117 mayHaveChildren: function(/*dojo/data/Item*/ item
){
119 // Tells if an item has or may have children. Implementing logic here
120 // avoids showing +/- expando icon for nodes that we know don't have children.
121 // (For efficiency reasons we may not want to check if an element actually
122 // has children until user clicks the expando node)
123 return array
.some(this.childrenAttrs
, function(attr
){
124 return this.store
.hasAttribute(item
, attr
);
128 getChildren: function(/*dojo/data/Item*/ parentItem
, /*function(items)*/ onComplete
, /*function*/ onError
){
130 // Calls onComplete() with array of child items of given parent item, all loaded.
132 var store
= this.store
;
133 if(!store
.isItemLoaded(parentItem
)){
134 // The parent is not loaded yet, we must be in deferItemLoadingUntilExpand
135 // mode, so we will load it and just return the children (without loading each
137 var getChildren
= lang
.hitch(this, arguments
.callee
);
140 onItem: function(parentItem
){
141 getChildren(parentItem
, onComplete
, onError
);
147 // get children of specified item
149 for(var i
=0; i
<this.childrenAttrs
.length
; i
++){
150 var vals
= store
.getValues(parentItem
, this.childrenAttrs
[i
]);
151 childItems
= childItems
.concat(vals
);
154 // count how many items need to be loaded
156 if(!this.deferItemLoadingUntilExpand
){
157 array
.forEach(childItems
, function(item
){ if(!store
.isItemLoaded(item
)){ _waitCount
++; } });
161 // all items are already loaded (or we aren't loading them). proceed...
162 onComplete(childItems
);
164 // still waiting for some or all of the items to load
165 array
.forEach(childItems
, function(item
, idx
){
166 if(!store
.isItemLoaded(item
)){
169 onItem: function(item
){
170 childItems
[idx
] = item
;
171 if(--_waitCount
== 0){
172 // all nodes have been loaded, send them to the tree
173 onComplete(childItems
);
183 // =======================================================================
186 isItem: function(/* anything */ something
){
187 return this.store
.isItem(something
); // Boolean
190 fetchItemByIdentity: function(/* object */ keywordArgs
){
191 this.store
.fetchItemByIdentity(keywordArgs
);
194 getIdentity: function(/* item */ item
){
195 return this.store
.getIdentity(item
); // Object
198 getLabel: function(/*dojo/data/Item*/ item
){
200 // Get the label for an item
202 return this.store
.getValue(item
,this.labelAttr
); // String
204 return this.store
.getLabel(item
); // String
208 // =======================================================================
211 newItem: function(/* dijit/tree/dndSource.__Item */ args
, /*dojo/data/api/Item*/ parent, /*int?*/ insertIndex
){
213 // Creates a new item. See `dojo/data/api/Write` for details on args.
214 // Used in drag & drop when item from external source dropped onto tree.
216 // Developers will need to override this method if new items get added
217 // to parents with multiple children attributes, in order to define which
218 // children attribute points to the new item.
220 var pInfo
= {parent
: parent
, attribute
: this.childrenAttrs
[0]}, LnewItem
;
222 if(this.newItemIdAttr
&& args
[this.newItemIdAttr
]){
223 // Maybe there's already a corresponding item in the store; if so, reuse it.
224 this.fetchItemByIdentity({identity
: args
[this.newItemIdAttr
], scope
: this, onItem: function(item
){
226 // There's already a matching item in store, use it
227 this.pasteItem(item
, null, parent
, true, insertIndex
);
229 // Create new item in the tree, based on the drag source.
230 LnewItem
=this.store
.newItem(args
, pInfo
);
231 if(LnewItem
&& (insertIndex
!=undefined)){
232 // Move new item to desired position
233 this.pasteItem(LnewItem
, parent
, parent
, false, insertIndex
);
238 // [as far as we know] there is no id so we must assume this is a new item
239 LnewItem
=this.store
.newItem(args
, pInfo
);
240 if(LnewItem
&& (insertIndex
!=undefined)){
241 // Move new item to desired position
242 this.pasteItem(LnewItem
, parent
, parent
, false, insertIndex
);
247 pasteItem: function(/*Item*/ childItem
, /*Item*/ oldParentItem
, /*Item*/ newParentItem
, /*Boolean*/ bCopy
, /*int?*/ insertIndex
){
249 // Move or copy an item from one parent item to another.
250 // Used in drag & drop
251 var store
= this.store
,
252 parentAttr
= this.childrenAttrs
[0]; // name of "children" attr in parent item
254 // remove child from source item, and record the attribute that child occurred in
256 array
.forEach(this.childrenAttrs
, function(attr
){
257 if(store
.containsValue(oldParentItem
, attr
, childItem
)){
259 var values
= array
.filter(store
.getValues(oldParentItem
, attr
), function(x
){
260 return x
!= childItem
;
262 store
.setValues(oldParentItem
, attr
, values
);
269 // modify target item's children attribute to include this item
271 if(typeof insertIndex
== "number"){
272 // call slice() to avoid modifying the original array, confusing the data store
273 var childItems
= store
.getValues(newParentItem
, parentAttr
).slice();
274 childItems
.splice(insertIndex
, 0, childItem
);
275 store
.setValues(newParentItem
, parentAttr
, childItems
);
277 store
.setValues(newParentItem
, parentAttr
,
278 store
.getValues(newParentItem
, parentAttr
).concat(childItem
));
283 // =======================================================================
286 onChange: function(/*dojo/data/Item*/ /*===== item =====*/){
288 // Callback whenever an item has changed, so that Tree
289 // can update the label, icon, etc. Note that changes
290 // to an item's children or parent(s) will trigger an
291 // onChildrenChange() so you can ignore those changes here.
296 onChildrenChange: function(/*===== parent, newChildrenList =====*/){
298 // Callback to do notifications about new, updated, or deleted items.
299 // parent: dojo/data/Item
300 // newChildrenList: dojo/data/Item[]
305 onDelete: function(/*dojo/data/Item*/ /*===== item =====*/){
307 // Callback when an item has been deleted.
309 // Note that there will also be an onChildrenChange() callback for the parent
315 // =======================================================================
316 // Events from data store
318 onNewItem: function(/* dojo/data/Item */ item
, /* Object */ parentInfo
){
320 // Handler for when new items appear in the store, either from a drop operation
321 // or some other way. Updates the tree view (if necessary).
323 // If the new item is a child of an existing item,
324 // calls onChildrenChange() with the new list of children
325 // for that existing item.
330 // We only care about the new item if it has a parent that corresponds to a TreeNode
331 // we are currently displaying
336 // Call onChildrenChange() on parent (ie, existing) item with new list of children
337 // In the common case, the new list of children is simply parentInfo.newValue or
338 // [ parentInfo.newValue ], although if items in the store has multiple
339 // child attributes (see `childrenAttr`), then it's a superset of parentInfo.newValue,
340 // so call getChildren() to be sure to get right answer.
341 this.getChildren(parentInfo
.item
, lang
.hitch(this, function(children
){
342 this.onChildrenChange(parentInfo
.item
, children
);
346 onDeleteItem: function(/*Object*/ item
){
348 // Handler for delete notifications from underlying store
352 onSetItem: function(item
, attribute
/*===== , oldValue, newValue =====*/){
354 // Updates the tree view according to changes in the data store.
356 // Handles updates to an item's children by calling onChildrenChange(), and
357 // other updates to an item by calling onChange().
359 // See `onNewItem` for more details on handling updates to an item's children.
361 // attribute: attribute-name-string
362 // oldValue: Object|Array
363 // newValue: Object|Array
367 if(array
.indexOf(this.childrenAttrs
, attribute
) != -1){
368 // item's children list changed
369 this.getChildren(item
, lang
.hitch(this, function(children
){
370 // See comments in onNewItem() about calling getChildren()
371 this.onChildrenChange(item
, children
);
374 // item's label/icon/etc. changed.