TreeStoreModel.js.uncompressed.js 13 KB

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