TreeStoreModel.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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. // module:
  9. // dijit/tree/TreeStoreModel
  10. // summary:
  11. // Implements dijit.Tree.model connecting to a dojo.data store with a single
  12. // root item.
  13. return declare("dijit.tree.TreeStoreModel", null, {
  14. // summary:
  15. // Implements dijit.Tree.model connecting to a dojo.data store with a single
  16. // root item. Any methods passed into the constructor will override
  17. // the ones defined here.
  18. // store: dojo.data.Store
  19. // Underlying store
  20. store: null,
  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. // newItemIdAttr: String
  25. // Name of attribute in the Object passed to newItem() that specifies the id.
  26. //
  27. // If newItemIdAttr is set then it's used when newItem() is called to see if an
  28. // item with the same id already exists, and if so just links to the old item
  29. // (so that the old item ends up with two parents).
  30. //
  31. // Setting this to null or "" will make every drop create a new item.
  32. newItemIdAttr: "id",
  33. // labelAttr: String
  34. // If specified, get label for tree node from this attribute, rather
  35. // than by calling store.getLabel()
  36. labelAttr: "",
  37. // root: [readonly] dojo.data.Item
  38. // Pointer to the root item (read only, not a parameter)
  39. root: null,
  40. // query: anything
  41. // Specifies datastore query to return the root item for the tree.
  42. // Must only return a single item. Alternately can just pass in pointer
  43. // to root item.
  44. // example:
  45. // | {id:'ROOT'}
  46. query: null,
  47. // deferItemLoadingUntilExpand: Boolean
  48. // Setting this to true will cause the TreeStoreModel to defer calling loadItem on nodes
  49. // until they are expanded. This allows for lazying loading where only one
  50. // loadItem (and generally one network call, consequently) per expansion
  51. // (rather than one for each child).
  52. // This relies on partial loading of the children items; each children item of a
  53. // fully loaded item should contain the label and info about having children.
  54. deferItemLoadingUntilExpand: false,
  55. constructor: function(/* Object */ args){
  56. // summary:
  57. // Passed the arguments listed above (store, etc)
  58. // tags:
  59. // private
  60. lang.mixin(this, args);
  61. this.connects = [];
  62. var store = this.store;
  63. if(!store.getFeatures()['dojo.data.api.Identity']){
  64. throw new Error("dijit.Tree: store must support dojo.data.Identity");
  65. }
  66. // if the store supports Notification, subscribe to the notification events
  67. if(store.getFeatures()['dojo.data.api.Notification']){
  68. this.connects = this.connects.concat([
  69. aspect.after(store, "onNew", lang.hitch(this, "onNewItem"), true),
  70. aspect.after(store, "onDelete", lang.hitch(this, "onDeleteItem"), true),
  71. aspect.after(store, "onSet", lang.hitch(this, "onSetItem"), true)
  72. ]);
  73. }
  74. },
  75. destroy: function(){
  76. var h;
  77. while(h = this.connects.pop()){ h.remove(); }
  78. // TODO: should cancel any in-progress processing of getRoot(), getChildren()
  79. },
  80. // =======================================================================
  81. // Methods for traversing hierarchy
  82. getRoot: function(onItem, onError){
  83. // summary:
  84. // Calls onItem with the root item for the tree, possibly a fabricated item.
  85. // Calls onError on error.
  86. if(this.root){
  87. onItem(this.root);
  88. }else{
  89. this.store.fetch({
  90. query: this.query,
  91. onComplete: lang.hitch(this, function(items){
  92. if(items.length != 1){
  93. throw new Error(this.declaredClass + ": query " + json.stringify(this.query) + " returned " + items.length +
  94. " items, but must return exactly one item");
  95. }
  96. this.root = items[0];
  97. onItem(this.root);
  98. }),
  99. onError: onError
  100. });
  101. }
  102. },
  103. mayHaveChildren: function(/*dojo.data.Item*/ item){
  104. // summary:
  105. // Tells if an item has or may have children. Implementing logic here
  106. // avoids showing +/- expando icon for nodes that we know don't have children.
  107. // (For efficiency reasons we may not want to check if an element actually
  108. // has children until user clicks the expando node)
  109. return array.some(this.childrenAttrs, function(attr){
  110. return this.store.hasAttribute(item, attr);
  111. }, this);
  112. },
  113. getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){
  114. // summary:
  115. // Calls onComplete() with array of child items of given parent item, all loaded.
  116. var store = this.store;
  117. if(!store.isItemLoaded(parentItem)){
  118. // The parent is not loaded yet, we must be in deferItemLoadingUntilExpand
  119. // mode, so we will load it and just return the children (without loading each
  120. // child item)
  121. var getChildren = lang.hitch(this, arguments.callee);
  122. store.loadItem({
  123. item: parentItem,
  124. onItem: function(parentItem){
  125. getChildren(parentItem, onComplete, onError);
  126. },
  127. onError: onError
  128. });
  129. return;
  130. }
  131. // get children of specified item
  132. var childItems = [];
  133. for(var i=0; i<this.childrenAttrs.length; i++){
  134. var vals = store.getValues(parentItem, this.childrenAttrs[i]);
  135. childItems = childItems.concat(vals);
  136. }
  137. // count how many items need to be loaded
  138. var _waitCount = 0;
  139. if(!this.deferItemLoadingUntilExpand){
  140. array.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } });
  141. }
  142. if(_waitCount == 0){
  143. // all items are already loaded (or we aren't loading them). proceed...
  144. onComplete(childItems);
  145. }else{
  146. // still waiting for some or all of the items to load
  147. array.forEach(childItems, function(item, idx){
  148. if(!store.isItemLoaded(item)){
  149. store.loadItem({
  150. item: item,
  151. onItem: function(item){
  152. childItems[idx] = item;
  153. if(--_waitCount == 0){
  154. // all nodes have been loaded, send them to the tree
  155. onComplete(childItems);
  156. }
  157. },
  158. onError: onError
  159. });
  160. }
  161. });
  162. }
  163. },
  164. // =======================================================================
  165. // Inspecting items
  166. isItem: function(/* anything */ something){
  167. return this.store.isItem(something); // Boolean
  168. },
  169. fetchItemByIdentity: function(/* object */ keywordArgs){
  170. this.store.fetchItemByIdentity(keywordArgs);
  171. },
  172. getIdentity: function(/* item */ item){
  173. return this.store.getIdentity(item); // Object
  174. },
  175. getLabel: function(/*dojo.data.Item*/ item){
  176. // summary:
  177. // Get the label for an item
  178. if(this.labelAttr){
  179. return this.store.getValue(item,this.labelAttr); // String
  180. }else{
  181. return this.store.getLabel(item); // String
  182. }
  183. },
  184. // =======================================================================
  185. // Write interface
  186. newItem: function(/* dojo.dnd.Item */ args, /*Item*/ parent, /*int?*/ insertIndex){
  187. // summary:
  188. // Creates a new item. See `dojo.data.api.Write` for details on args.
  189. // Used in drag & drop when item from external source dropped onto tree.
  190. // description:
  191. // Developers will need to override this method if new items get added
  192. // to parents with multiple children attributes, in order to define which
  193. // children attribute points to the new item.
  194. var pInfo = {parent: parent, attribute: this.childrenAttrs[0]}, LnewItem;
  195. if(this.newItemIdAttr && args[this.newItemIdAttr]){
  196. // Maybe there's already a corresponding item in the store; if so, reuse it.
  197. this.fetchItemByIdentity({identity: args[this.newItemIdAttr], scope: this, onItem: function(item){
  198. if(item){
  199. // There's already a matching item in store, use it
  200. this.pasteItem(item, null, parent, true, insertIndex);
  201. }else{
  202. // Create new item in the tree, based on the drag source.
  203. LnewItem=this.store.newItem(args, pInfo);
  204. if(LnewItem && (insertIndex!=undefined)){
  205. // Move new item to desired position
  206. this.pasteItem(LnewItem, parent, parent, false, insertIndex);
  207. }
  208. }
  209. }});
  210. }else{
  211. // [as far as we know] there is no id so we must assume this is a new item
  212. LnewItem=this.store.newItem(args, pInfo);
  213. if(LnewItem && (insertIndex!=undefined)){
  214. // Move new item to desired position
  215. this.pasteItem(LnewItem, parent, parent, false, insertIndex);
  216. }
  217. }
  218. },
  219. pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){
  220. // summary:
  221. // Move or copy an item from one parent item to another.
  222. // Used in drag & drop
  223. var store = this.store,
  224. parentAttr = this.childrenAttrs[0]; // name of "children" attr in parent item
  225. // remove child from source item, and record the attribute that child occurred in
  226. if(oldParentItem){
  227. array.forEach(this.childrenAttrs, function(attr){
  228. if(store.containsValue(oldParentItem, attr, childItem)){
  229. if(!bCopy){
  230. var values = array.filter(store.getValues(oldParentItem, attr), function(x){
  231. return x != childItem;
  232. });
  233. store.setValues(oldParentItem, attr, values);
  234. }
  235. parentAttr = attr;
  236. }
  237. });
  238. }
  239. // modify target item's children attribute to include this item
  240. if(newParentItem){
  241. if(typeof insertIndex == "number"){
  242. // call slice() to avoid modifying the original array, confusing the data store
  243. var childItems = store.getValues(newParentItem, parentAttr).slice();
  244. childItems.splice(insertIndex, 0, childItem);
  245. store.setValues(newParentItem, parentAttr, childItems);
  246. }else{
  247. store.setValues(newParentItem, parentAttr,
  248. store.getValues(newParentItem, parentAttr).concat(childItem));
  249. }
  250. }
  251. },
  252. // =======================================================================
  253. // Callbacks
  254. onChange: function(/*dojo.data.Item*/ /*===== item =====*/){
  255. // summary:
  256. // Callback whenever an item has changed, so that Tree
  257. // can update the label, icon, etc. Note that changes
  258. // to an item's children or parent(s) will trigger an
  259. // onChildrenChange() so you can ignore those changes here.
  260. // tags:
  261. // callback
  262. },
  263. onChildrenChange: function(/*===== parent, newChildrenList =====*/){
  264. // summary:
  265. // Callback to do notifications about new, updated, or deleted items.
  266. // parent: dojo.data.Item
  267. // newChildrenList: dojo.data.Item[]
  268. // tags:
  269. // callback
  270. },
  271. onDelete: function(/*dojo.data.Item*/ /*===== item =====*/){
  272. // summary:
  273. // Callback when an item has been deleted.
  274. // description:
  275. // Note that there will also be an onChildrenChange() callback for the parent
  276. // of this item.
  277. // tags:
  278. // callback
  279. },
  280. // =======================================================================
  281. // Events from data store
  282. onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){
  283. // summary:
  284. // Handler for when new items appear in the store, either from a drop operation
  285. // or some other way. Updates the tree view (if necessary).
  286. // description:
  287. // If the new item is a child of an existing item,
  288. // calls onChildrenChange() with the new list of children
  289. // for that existing item.
  290. //
  291. // tags:
  292. // extension
  293. // We only care about the new item if it has a parent that corresponds to a TreeNode
  294. // we are currently displaying
  295. if(!parentInfo){
  296. return;
  297. }
  298. // Call onChildrenChange() on parent (ie, existing) item with new list of children
  299. // In the common case, the new list of children is simply parentInfo.newValue or
  300. // [ parentInfo.newValue ], although if items in the store has multiple
  301. // child attributes (see `childrenAttr`), then it's a superset of parentInfo.newValue,
  302. // so call getChildren() to be sure to get right answer.
  303. this.getChildren(parentInfo.item, lang.hitch(this, function(children){
  304. this.onChildrenChange(parentInfo.item, children);
  305. }));
  306. },
  307. onDeleteItem: function(/*Object*/ item){
  308. // summary:
  309. // Handler for delete notifications from underlying store
  310. this.onDelete(item);
  311. },
  312. onSetItem: function(item, attribute /*===== , oldValue, newValue =====*/){
  313. // summary:
  314. // Updates the tree view according to changes in the data store.
  315. // description:
  316. // Handles updates to an item's children by calling onChildrenChange(), and
  317. // other updates to an item by calling onChange().
  318. //
  319. // See `onNewItem` for more details on handling updates to an item's children.
  320. // item: Item
  321. // attribute: attribute-name-string
  322. // oldValue: object | array
  323. // newValue: object | array
  324. // tags:
  325. // extension
  326. if(array.indexOf(this.childrenAttrs, attribute) != -1){
  327. // item's children list changed
  328. this.getChildren(item, lang.hitch(this, function(children){
  329. // See comments in onNewItem() about calling getChildren()
  330. this.onChildrenChange(item, children);
  331. }));
  332. }else{
  333. // item's label/icon/etc. changed.
  334. this.onChange(item);
  335. }
  336. }
  337. });
  338. });