]> git.wh0rd.org - tt-rss.git/blob - lib/dijit/tree/TreeStoreModel.js.uncompressed.js
update dojo to 1.7.3
[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/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 });