source: Dev/trunk/src/client/dijit/tree/TreeStoreModel.js @ 483

Last change on this file since 483 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 13.2 KB
Line 
1define([
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){
7
8        // module:
9        //              dijit/tree/TreeStoreModel
10
11        return declare("dijit.tree.TreeStoreModel", null, {
12                // summary:
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.
16
17                // store: dojo/data/api/Read
18                //              Underlying store
19                store: null,
20
21                // childrenAttrs: String[]
22                //              One or more attribute names (attributes in the dojo.data item) that specify that item's children
23                childrenAttrs: ["children"],
24
25                // newItemIdAttr: String
26                //              Name of attribute in the Object passed to newItem() that specifies the id.
27                //
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).
31                //
32                //              Setting this to null or "" will make every drop create a new item.
33                newItemIdAttr: "id",
34
35                // labelAttr: String
36                //              If specified, get label for tree node from this attribute, rather
37                //              than by calling store.getLabel()
38                labelAttr: "",
39
40                // root: [readonly] dojo/data/Item
41                //              Pointer to the root item (read only, not a parameter)
42                root: null,
43
44                // query: anything
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
47                //              to root item.
48                // example:
49                //      |       {id:'ROOT'}
50                query: null,
51
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,
60
61                constructor: function(/* Object */ args){
62                        // summary:
63                        //              Passed the arguments listed above (store, etc)
64                        // tags:
65                        //              private
66
67                        lang.mixin(this, args);
68
69                        this.connects = [];
70
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");
74                        }
75
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)
82                                ]);
83                        }
84                },
85
86                destroy: function(){
87                        var h;
88                        while(h = this.connects.pop()){ h.remove(); }
89                        // TODO: should cancel any in-progress processing of getRoot(), getChildren()
90                },
91
92                // =======================================================================
93                // Methods for traversing hierarchy
94
95                getRoot: function(onItem, onError){
96                        // summary:
97                        //              Calls onItem with the root item for the tree, possibly a fabricated item.
98                        //              Calls onError on error.
99                        if(this.root){
100                                onItem(this.root);
101                        }else{
102                                this.store.fetch({
103                                        query: this.query,
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");
108                                                }
109                                                this.root = items[0];
110                                                onItem(this.root);
111                                        }),
112                                        onError: onError
113                                });
114                        }
115                },
116
117                mayHaveChildren: function(/*dojo/data/Item*/ item){
118                        // summary:
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);
125                        }, this);
126                },
127
128                getChildren: function(/*dojo/data/Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){
129                        // summary:
130                        //              Calls onComplete() with array of child items of given parent item, all loaded.
131
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
136                                // child item)
137                                var getChildren = lang.hitch(this, arguments.callee);
138                                store.loadItem({
139                                        item: parentItem,
140                                        onItem: function(parentItem){
141                                                getChildren(parentItem, onComplete, onError);
142                                        },
143                                        onError: onError
144                                });
145                                return;
146                        }
147                        // get children of specified item
148                        var childItems = [];
149                        for(var i=0; i<this.childrenAttrs.length; i++){
150                                var vals = store.getValues(parentItem, this.childrenAttrs[i]);
151                                childItems = childItems.concat(vals);
152                        }
153
154                        // count how many items need to be loaded
155                        var _waitCount = 0;
156                        if(!this.deferItemLoadingUntilExpand){
157                                array.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } });
158                        }
159
160                        if(_waitCount == 0){
161                                // all items are already loaded (or we aren't loading them).  proceed...
162                                onComplete(childItems);
163                        }else{
164                                // still waiting for some or all of the items to load
165                                array.forEach(childItems, function(item, idx){
166                                        if(!store.isItemLoaded(item)){
167                                                store.loadItem({
168                                                        item: 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);
174                                                                }
175                                                        },
176                                                        onError: onError
177                                                });
178                                        }
179                                });
180                        }
181                },
182
183                // =======================================================================
184                // Inspecting items
185
186                isItem: function(/* anything */ something){
187                        return this.store.isItem(something);    // Boolean
188                },
189
190                fetchItemByIdentity: function(/* object */ keywordArgs){
191                        // summary:
192                        //              Given the identity of an item, this method returns the item that has
193                        //              that identity through the onItem callback.  Conforming implementations
194                        //              should return null if there is no item with the given identity.
195                        //              Implementations of fetchItemByIdentity() may sometimes return an item
196                        //              from a local cache and may sometimes fetch an item from a remote server.
197                        // tags:
198                        //              extension
199                        this.store.fetchItemByIdentity(keywordArgs);
200                },
201
202                getIdentity: function(/* item */ item){
203                        return this.store.getIdentity(item);    // Object
204                },
205
206                getLabel: function(/*dojo/data/Item*/ item){
207                        // summary:
208                        //              Get the label for an item
209                        if(this.labelAttr){
210                                return this.store.getValue(item,this.labelAttr);        // String
211                        }else{
212                                return this.store.getLabel(item);       // String
213                        }
214                },
215
216                // =======================================================================
217                // Write interface
218
219                newItem: function(/* dijit/tree/dndSource.__Item */ args, /*dojo/data/api/Item*/ parent, /*int?*/ insertIndex){
220                        // summary:
221                        //              Creates a new item.   See `dojo/data/api/Write` for details on args.
222                        //              Used in drag & drop when item from external source dropped onto tree.
223                        // description:
224                        //              Developers will need to override this method if new items get added
225                        //              to parents with multiple children attributes, in order to define which
226                        //              children attribute points to the new item.
227
228                        var pInfo = {parent: parent, attribute: this.childrenAttrs[0]}, LnewItem;
229
230                        if(this.newItemIdAttr && args[this.newItemIdAttr]){
231                                // Maybe there's already a corresponding item in the store; if so, reuse it.
232                                this.fetchItemByIdentity({identity: args[this.newItemIdAttr], scope: this, onItem: function(item){
233                                        if(item){
234                                                // There's already a matching item in store, use it
235                                                this.pasteItem(item, null, parent, true, insertIndex);
236                                        }else{
237                                                // Create new item in the tree, based on the drag source.
238                                                LnewItem=this.store.newItem(args, pInfo);
239                                                if(LnewItem && (insertIndex!=undefined)){
240                                                        // Move new item to desired position
241                                                        this.pasteItem(LnewItem, parent, parent, false, insertIndex);
242                                                }
243                                        }
244                                }});
245                        }else{
246                                // [as far as we know] there is no id so we must assume this is a new item
247                                LnewItem=this.store.newItem(args, pInfo);
248                                if(LnewItem && (insertIndex!=undefined)){
249                                        // Move new item to desired position
250                                        this.pasteItem(LnewItem, parent, parent, false, insertIndex);
251                                }
252                        }
253                },
254
255                pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){
256                        // summary:
257                        //              Move or copy an item from one parent item to another.
258                        //              Used in drag & drop
259                        var store = this.store,
260                                parentAttr = this.childrenAttrs[0];     // name of "children" attr in parent item
261
262                        // remove child from source item, and record the attribute that child occurred in
263                        if(oldParentItem){
264                                array.forEach(this.childrenAttrs, function(attr){
265                                        if(store.containsValue(oldParentItem, attr, childItem)){
266                                                if(!bCopy){
267                                                        var values = array.filter(store.getValues(oldParentItem, attr), function(x){
268                                                                return x != childItem;
269                                                        });
270                                                        store.setValues(oldParentItem, attr, values);
271                                                }
272                                                parentAttr = attr;
273                                        }
274                                });
275                        }
276
277                        // modify target item's children attribute to include this item
278                        if(newParentItem){
279                                if(typeof insertIndex == "number"){
280                                        // call slice() to avoid modifying the original array, confusing the data store
281                                        var childItems = store.getValues(newParentItem, parentAttr).slice();
282                                        childItems.splice(insertIndex, 0, childItem);
283                                        store.setValues(newParentItem, parentAttr, childItems);
284                                }else{
285                                        store.setValues(newParentItem, parentAttr,
286                                                store.getValues(newParentItem, parentAttr).concat(childItem));
287                                }
288                        }
289                },
290
291                // =======================================================================
292                // Callbacks
293
294                onChange: function(/*dojo/data/Item*/ /*===== item =====*/){
295                        // summary:
296                        //              Callback whenever an item has changed, so that Tree
297                        //              can update the label, icon, etc.   Note that changes
298                        //              to an item's children or parent(s) will trigger an
299                        //              onChildrenChange() so you can ignore those changes here.
300                        // tags:
301                        //              callback
302                },
303
304                onChildrenChange: function(/*===== parent, newChildrenList =====*/){
305                        // summary:
306                        //              Callback to do notifications about new, updated, or deleted items.
307                        // parent: dojo/data/Item
308                        // newChildrenList: dojo/data/Item[]
309                        // tags:
310                        //              callback
311                },
312
313                onDelete: function(/*dojo/data/Item*/ /*===== item =====*/){
314                        // summary:
315                        //              Callback when an item has been deleted.
316                        // description:
317                        //              Note that there will also be an onChildrenChange() callback for the parent
318                        //              of this item.
319                        // tags:
320                        //              callback
321                },
322
323                // =======================================================================
324                // Events from data store
325
326                onNewItem: function(/* dojo/data/Item */ item, /* Object */ parentInfo){
327                        // summary:
328                        //              Handler for when new items appear in the store, either from a drop operation
329                        //              or some other way.   Updates the tree view (if necessary).
330                        // description:
331                        //              If the new item is a child of an existing item,
332                        //              calls onChildrenChange() with the new list of children
333                        //              for that existing item.
334                        //
335                        // tags:
336                        //              extension
337
338                        // We only care about the new item if it has a parent that corresponds to a TreeNode
339                        // we are currently displaying
340                        if(!parentInfo){
341                                return;
342                        }
343
344                        // Call onChildrenChange() on parent (ie, existing) item with new list of children
345                        // In the common case, the new list of children is simply parentInfo.newValue or
346                        // [ parentInfo.newValue ], although if items in the store has multiple
347                        // child attributes (see `childrenAttr`), then it's a superset of parentInfo.newValue,
348                        // so call getChildren() to be sure to get right answer.
349                        this.getChildren(parentInfo.item, lang.hitch(this, function(children){
350                                this.onChildrenChange(parentInfo.item, children);
351                        }));
352                },
353
354                onDeleteItem: function(/*Object*/ item){
355                        // summary:
356                        //              Handler for delete notifications from underlying store
357                        this.onDelete(item);
358                },
359
360                onSetItem: function(item, attribute /*===== , oldValue, newValue =====*/){
361                        // summary:
362                        //              Updates the tree view according to changes in the data store.
363                        // description:
364                        //              Handles updates to an item's children by calling onChildrenChange(), and
365                        //              other updates to an item by calling onChange().
366                        //
367                        //              See `onNewItem` for more details on handling updates to an item's children.
368                        // item: Item
369                        // attribute: attribute-name-string
370                        // oldValue: Object|Array
371                        // newValue: Object|Array
372                        // tags:
373                        //              extension
374
375                        if(array.indexOf(this.childrenAttrs, attribute) != -1){
376                                // item's children list changed
377                                this.getChildren(item, lang.hitch(this, function(children){
378                                        // See comments in onNewItem() about calling getChildren()
379                                        this.onChildrenChange(item, children);
380                                }));
381                        }else{
382                                // item's label/icon/etc. changed.
383                                this.onChange(item);
384                        }
385                }
386        });
387});
Note: See TracBrowser for help on using the repository browser.