]> git.wh0rd.org - tt-rss.git/blob - lib/dijit/tree/TreeStoreModel.js.uncompressed.js
upgrade dojo to 1.8.3 (refs #570)
[tt-rss.git] / lib / dijit / tree / TreeStoreModel.js.uncompressed.js
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){
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 this.store.fetchItemByIdentity(keywordArgs);
192 },
193
194 getIdentity: function(/* item */ item){
195 return this.store.getIdentity(item); // Object
196 },
197
198 getLabel: function(/*dojo/data/Item*/ item){
199 // summary:
200 // Get the label for an item
201 if(this.labelAttr){
202 return this.store.getValue(item,this.labelAttr); // String
203 }else{
204 return this.store.getLabel(item); // String
205 }
206 },
207
208 // =======================================================================
209 // Write interface
210
211 newItem: function(/* dijit/tree/dndSource.__Item */ args, /*dojo/data/api/Item*/ parent, /*int?*/ insertIndex){
212 // summary:
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.
215 // description:
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.
219
220 var pInfo = {parent: parent, attribute: this.childrenAttrs[0]}, LnewItem;
221
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){
225 if(item){
226 // There's already a matching item in store, use it
227 this.pasteItem(item, null, parent, true, insertIndex);
228 }else{
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);
234 }
235 }
236 }});
237 }else{
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);
243 }
244 }
245 },
246
247 pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){
248 // summary:
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
253
254 // remove child from source item, and record the attribute that child occurred in
255 if(oldParentItem){
256 array.forEach(this.childrenAttrs, function(attr){
257 if(store.containsValue(oldParentItem, attr, childItem)){
258 if(!bCopy){
259 var values = array.filter(store.getValues(oldParentItem, attr), function(x){
260 return x != childItem;
261 });
262 store.setValues(oldParentItem, attr, values);
263 }
264 parentAttr = attr;
265 }
266 });
267 }
268
269 // modify target item's children attribute to include this item
270 if(newParentItem){
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);
276 }else{
277 store.setValues(newParentItem, parentAttr,
278 store.getValues(newParentItem, parentAttr).concat(childItem));
279 }
280 }
281 },
282
283 // =======================================================================
284 // Callbacks
285
286 onChange: function(/*dojo/data/Item*/ /*===== item =====*/){
287 // summary:
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.
292 // tags:
293 // callback
294 },
295
296 onChildrenChange: function(/*===== parent, newChildrenList =====*/){
297 // summary:
298 // Callback to do notifications about new, updated, or deleted items.
299 // parent: dojo/data/Item
300 // newChildrenList: dojo/data/Item[]
301 // tags:
302 // callback
303 },
304
305 onDelete: function(/*dojo/data/Item*/ /*===== item =====*/){
306 // summary:
307 // Callback when an item has been deleted.
308 // description:
309 // Note that there will also be an onChildrenChange() callback for the parent
310 // of this item.
311 // tags:
312 // callback
313 },
314
315 // =======================================================================
316 // Events from data store
317
318 onNewItem: function(/* dojo/data/Item */ item, /* Object */ parentInfo){
319 // summary:
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).
322 // description:
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.
326 //
327 // tags:
328 // extension
329
330 // We only care about the new item if it has a parent that corresponds to a TreeNode
331 // we are currently displaying
332 if(!parentInfo){
333 return;
334 }
335
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);
343 }));
344 },
345
346 onDeleteItem: function(/*Object*/ item){
347 // summary:
348 // Handler for delete notifications from underlying store
349 this.onDelete(item);
350 },
351
352 onSetItem: function(item, attribute /*===== , oldValue, newValue =====*/){
353 // summary:
354 // Updates the tree view according to changes in the data store.
355 // description:
356 // Handles updates to an item's children by calling onChildrenChange(), and
357 // other updates to an item by calling onChange().
358 //
359 // See `onNewItem` for more details on handling updates to an item's children.
360 // item: Item
361 // attribute: attribute-name-string
362 // oldValue: Object|Array
363 // newValue: Object|Array
364 // tags:
365 // extension
366
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);
372 }));
373 }else{
374 // item's label/icon/etc. changed.
375 this.onChange(item);
376 }
377 }
378 });
379 });