source: Dev/branches/rest-dojo-ui/client/dijit/tree/TreeStoreModel.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 13.0 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/json", // json.stringify
6        "dojo/_base/lang" // lang.hitch
7], function(array, aspect, declare, json, lang){
8
9        // module:
10        //              dijit/tree/TreeStoreModel
11        // summary:
12        //              Implements dijit.Tree.model connecting to a dojo.data store with a single
13        //              root item.
14
15        return declare("dijit.tree.TreeStoreModel", null, {
16                // summary:
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.
20
21                // store: dojo.data.Store
22                //              Underlying store
23                store: null,
24
25                // childrenAttrs: String[]
26                //              One or more attribute names (attributes in the dojo.data item) that specify that item's children
27                childrenAttrs: ["children"],
28
29                // newItemIdAttr: String
30                //              Name of attribute in the Object passed to newItem() that specifies the id.
31                //
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).
35                //
36                //              Setting this to null or "" will make every drop create a new item.
37                newItemIdAttr: "id",
38
39                // labelAttr: String
40                //              If specified, get label for tree node from this attribute, rather
41                //              than by calling store.getLabel()
42                labelAttr: "",
43
44                // root: [readonly] dojo.data.Item
45                //              Pointer to the root item (read only, not a parameter)
46                root: null,
47
48                // query: anything
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
51                //              to root item.
52                // example:
53                //      |       {id:'ROOT'}
54                query: null,
55
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,
64
65                constructor: function(/* Object */ args){
66                        // summary:
67                        //              Passed the arguments listed above (store, etc)
68                        // tags:
69                        //              private
70
71                        lang.mixin(this, args);
72
73                        this.connects = [];
74
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");
78                        }
79
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)
86                                ]);
87                        }
88                },
89
90                destroy: function(){
91                        var h;
92                        while(h = this.connects.pop()){ h.remove(); }
93                        // TODO: should cancel any in-progress processing of getRoot(), getChildren()
94                },
95
96                // =======================================================================
97                // Methods for traversing hierarchy
98
99                getRoot: function(onItem, onError){
100                        // summary:
101                        //              Calls onItem with the root item for the tree, possibly a fabricated item.
102                        //              Calls onError on error.
103                        if(this.root){
104                                onItem(this.root);
105                        }else{
106                                this.store.fetch({
107                                        query: this.query,
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");
112                                                }
113                                                this.root = items[0];
114                                                onItem(this.root);
115                                        }),
116                                        onError: onError
117                                });
118                        }
119                },
120
121                mayHaveChildren: function(/*dojo.data.Item*/ item){
122                        // summary:
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);
129                        }, this);
130                },
131
132                getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){
133                        // summary:
134                        //              Calls onComplete() with array of child items of given parent item, all loaded.
135
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
140                                // child item)
141                                var getChildren = lang.hitch(this, arguments.callee);
142                                store.loadItem({
143                                        item: parentItem,
144                                        onItem: function(parentItem){
145                                                getChildren(parentItem, onComplete, onError);
146                                        },
147                                        onError: onError
148                                });
149                                return;
150                        }
151                        // get children of specified item
152                        var childItems = [];
153                        for(var i=0; i<this.childrenAttrs.length; i++){
154                                var vals = store.getValues(parentItem, this.childrenAttrs[i]);
155                                childItems = childItems.concat(vals);
156                        }
157
158                        // count how many items need to be loaded
159                        var _waitCount = 0;
160                        if(!this.deferItemLoadingUntilExpand){
161                                array.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } });
162                        }
163
164                        if(_waitCount == 0){
165                                // all items are already loaded (or we aren't loading them).  proceed...
166                                onComplete(childItems);
167                        }else{
168                                // still waiting for some or all of the items to load
169                                array.forEach(childItems, function(item, idx){
170                                        if(!store.isItemLoaded(item)){
171                                                store.loadItem({
172                                                        item: 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);
178                                                                }
179                                                        },
180                                                        onError: onError
181                                                });
182                                        }
183                                });
184                        }
185                },
186
187                // =======================================================================
188                // Inspecting items
189
190                isItem: function(/* anything */ something){
191                        return this.store.isItem(something);    // Boolean
192                },
193
194                fetchItemByIdentity: function(/* object */ keywordArgs){
195                        this.store.fetchItemByIdentity(keywordArgs);
196                },
197
198                getIdentity: function(/* item */ item){
199                        return this.store.getIdentity(item);    // Object
200                },
201
202                getLabel: function(/*dojo.data.Item*/ item){
203                        // summary:
204                        //              Get the label for an item
205                        if(this.labelAttr){
206                                return this.store.getValue(item,this.labelAttr);        // String
207                        }else{
208                                return this.store.getLabel(item);       // String
209                        }
210                },
211
212                // =======================================================================
213                // Write interface
214
215                newItem: function(/* dojo.dnd.Item */ args, /*Item*/ parent, /*int?*/ insertIndex){
216                        // summary:
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.
219                        // description:
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.
223
224                        var pInfo = {parent: parent, attribute: this.childrenAttrs[0]}, LnewItem;
225
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){
229                                        if(item){
230                                                // There's already a matching item in store, use it
231                                                this.pasteItem(item, null, parent, true, insertIndex);
232                                        }else{
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);
238                                                }
239                                        }
240                                }});
241                        }else{
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);
247                                }
248                        }
249                },
250
251                pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){
252                        // summary:
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
257
258                        // remove child from source item, and record the attribute that child occurred in
259                        if(oldParentItem){
260                                array.forEach(this.childrenAttrs, function(attr){
261                                        if(store.containsValue(oldParentItem, attr, childItem)){
262                                                if(!bCopy){
263                                                        var values = array.filter(store.getValues(oldParentItem, attr), function(x){
264                                                                return x != childItem;
265                                                        });
266                                                        store.setValues(oldParentItem, attr, values);
267                                                }
268                                                parentAttr = attr;
269                                        }
270                                });
271                        }
272
273                        // modify target item's children attribute to include this item
274                        if(newParentItem){
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);
280                                }else{
281                                        store.setValues(newParentItem, parentAttr,
282                                                store.getValues(newParentItem, parentAttr).concat(childItem));
283                                }
284                        }
285                },
286
287                // =======================================================================
288                // Callbacks
289
290                onChange: function(/*dojo.data.Item*/ /*===== item =====*/){
291                        // summary:
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.
296                        // tags:
297                        //              callback
298                },
299
300                onChildrenChange: function(/*===== parent, newChildrenList =====*/){
301                        // summary:
302                        //              Callback to do notifications about new, updated, or deleted items.
303                        // parent: dojo.data.Item
304                        // newChildrenList: dojo.data.Item[]
305                        // tags:
306                        //              callback
307                },
308
309                onDelete: function(/*dojo.data.Item*/ /*===== item =====*/){
310                        // summary:
311                        //              Callback when an item has been deleted.
312                        // description:
313                        //              Note that there will also be an onChildrenChange() callback for the parent
314                        //              of this item.
315                        // tags:
316                        //              callback
317                },
318
319                // =======================================================================
320                // Events from data store
321
322                onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){
323                        // summary:
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).
326                        // description:
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.
330                        //
331                        // tags:
332                        //              extension
333
334                        // We only care about the new item if it has a parent that corresponds to a TreeNode
335                        // we are currently displaying
336                        if(!parentInfo){
337                                return;
338                        }
339
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);
347                        }));
348                },
349
350                onDeleteItem: function(/*Object*/ item){
351                        // summary:
352                        //              Handler for delete notifications from underlying store
353                        this.onDelete(item);
354                },
355
356                onSetItem: function(item, attribute /*===== , oldValue, newValue =====*/){
357                        // summary:
358                        //              Updates the tree view according to changes in the data store.
359                        // description:
360                        //              Handles updates to an item's children by calling onChildrenChange(), and
361                        //              other updates to an item by calling onChange().
362                        //
363                        //              See `onNewItem` for more details on handling updates to an item's children.
364                        // item: Item
365                        // attribute: attribute-name-string
366                        // oldValue: object | array
367                        // newValue: object | array
368                        // tags:
369                        //              extension
370
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);
376                                }));
377                        }else{
378                                // item's label/icon/etc. changed.
379                                this.onChange(item);
380                        }
381                }
382        });
383});
Note: See TracBrowser for help on using the repository browser.