You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
387 lines
13 KiB
387 lines
13 KiB
define([ |
|
"dojo/_base/array", // array.filter array.forEach array.indexOf array.some |
|
"dojo/aspect", // aspect.after |
|
"dojo/_base/declare", // declare |
|
"dojo/_base/lang" // lang.hitch |
|
], function(array, aspect, declare, lang){ |
|
|
|
// module: |
|
// dijit/tree/TreeStoreModel |
|
|
|
return declare("dijit.tree.TreeStoreModel", null, { |
|
// summary: |
|
// Implements dijit/Tree/model connecting to a dojo.data store with a single |
|
// root item. Any methods passed into the constructor will override |
|
// the ones defined here. |
|
|
|
// store: dojo/data/api/Read |
|
// Underlying store |
|
store: null, |
|
|
|
// childrenAttrs: String[] |
|
// One or more attribute names (attributes in the dojo.data item) that specify that item's children |
|
childrenAttrs: ["children"], |
|
|
|
// newItemIdAttr: String |
|
// Name of attribute in the Object passed to newItem() that specifies the id. |
|
// |
|
// If newItemIdAttr is set then it's used when newItem() is called to see if an |
|
// item with the same id already exists, and if so just links to the old item |
|
// (so that the old item ends up with two parents). |
|
// |
|
// Setting this to null or "" will make every drop create a new item. |
|
newItemIdAttr: "id", |
|
|
|
// labelAttr: String |
|
// If specified, get label for tree node from this attribute, rather |
|
// than by calling store.getLabel() |
|
labelAttr: "", |
|
|
|
// root: [readonly] dojo/data/Item |
|
// Pointer to the root item (read only, not a parameter) |
|
root: null, |
|
|
|
// query: anything |
|
// Specifies datastore query to return the root item for the tree. |
|
// Must only return a single item. Alternately can just pass in pointer |
|
// to root item. |
|
// example: |
|
// | {id:'ROOT'} |
|
query: null, |
|
|
|
// deferItemLoadingUntilExpand: Boolean |
|
// Setting this to true will cause the TreeStoreModel to defer calling loadItem on nodes |
|
// until they are expanded. This allows for lazying loading where only one |
|
// loadItem (and generally one network call, consequently) per expansion |
|
// (rather than one for each child). |
|
// This relies on partial loading of the children items; each children item of a |
|
// fully loaded item should contain the label and info about having children. |
|
deferItemLoadingUntilExpand: false, |
|
|
|
constructor: function(/* Object */ args){ |
|
// summary: |
|
// Passed the arguments listed above (store, etc) |
|
// tags: |
|
// private |
|
|
|
lang.mixin(this, args); |
|
|
|
this.connects = []; |
|
|
|
var store = this.store; |
|
if(!store.getFeatures()['dojo.data.api.Identity']){ |
|
throw new Error("dijit.tree.TreeStoreModel: store must support dojo.data.Identity"); |
|
} |
|
|
|
// if the store supports Notification, subscribe to the notification events |
|
if(store.getFeatures()['dojo.data.api.Notification']){ |
|
this.connects = this.connects.concat([ |
|
aspect.after(store, "onNew", lang.hitch(this, "onNewItem"), true), |
|
aspect.after(store, "onDelete", lang.hitch(this, "onDeleteItem"), true), |
|
aspect.after(store, "onSet", lang.hitch(this, "onSetItem"), true) |
|
]); |
|
} |
|
}, |
|
|
|
destroy: function(){ |
|
var h; |
|
while(h = this.connects.pop()){ h.remove(); } |
|
// TODO: should cancel any in-progress processing of getRoot(), getChildren() |
|
}, |
|
|
|
// ======================================================================= |
|
// Methods for traversing hierarchy |
|
|
|
getRoot: function(onItem, onError){ |
|
// summary: |
|
// Calls onItem with the root item for the tree, possibly a fabricated item. |
|
// Calls onError on error. |
|
if(this.root){ |
|
onItem(this.root); |
|
}else{ |
|
this.store.fetch({ |
|
query: this.query, |
|
onComplete: lang.hitch(this, function(items){ |
|
if(items.length != 1){ |
|
throw new Error("dijit.tree.TreeStoreModel: root query returned " + items.length + |
|
" items, but must return exactly one"); |
|
} |
|
this.root = items[0]; |
|
onItem(this.root); |
|
}), |
|
onError: onError |
|
}); |
|
} |
|
}, |
|
|
|
mayHaveChildren: function(/*dojo/data/Item*/ item){ |
|
// summary: |
|
// Tells if an item has or may have children. Implementing logic here |
|
// avoids showing +/- expando icon for nodes that we know don't have children. |
|
// (For efficiency reasons we may not want to check if an element actually |
|
// has children until user clicks the expando node) |
|
return array.some(this.childrenAttrs, function(attr){ |
|
return this.store.hasAttribute(item, attr); |
|
}, this); |
|
}, |
|
|
|
getChildren: function(/*dojo/data/Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){ |
|
// summary: |
|
// Calls onComplete() with array of child items of given parent item, all loaded. |
|
|
|
var store = this.store; |
|
if(!store.isItemLoaded(parentItem)){ |
|
// The parent is not loaded yet, we must be in deferItemLoadingUntilExpand |
|
// mode, so we will load it and just return the children (without loading each |
|
// child item) |
|
var getChildren = lang.hitch(this, arguments.callee); |
|
store.loadItem({ |
|
item: parentItem, |
|
onItem: function(parentItem){ |
|
getChildren(parentItem, onComplete, onError); |
|
}, |
|
onError: onError |
|
}); |
|
return; |
|
} |
|
// get children of specified item |
|
var childItems = []; |
|
for(var i=0; i<this.childrenAttrs.length; i++){ |
|
var vals = store.getValues(parentItem, this.childrenAttrs[i]); |
|
childItems = childItems.concat(vals); |
|
} |
|
|
|
// count how many items need to be loaded |
|
var _waitCount = 0; |
|
if(!this.deferItemLoadingUntilExpand){ |
|
array.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } }); |
|
} |
|
|
|
if(_waitCount == 0){ |
|
// all items are already loaded (or we aren't loading them). proceed... |
|
onComplete(childItems); |
|
}else{ |
|
// still waiting for some or all of the items to load |
|
array.forEach(childItems, function(item, idx){ |
|
if(!store.isItemLoaded(item)){ |
|
store.loadItem({ |
|
item: item, |
|
onItem: function(item){ |
|
childItems[idx] = item; |
|
if(--_waitCount == 0){ |
|
// all nodes have been loaded, send them to the tree |
|
onComplete(childItems); |
|
} |
|
}, |
|
onError: onError |
|
}); |
|
} |
|
}); |
|
} |
|
}, |
|
|
|
// ======================================================================= |
|
// Inspecting items |
|
|
|
isItem: function(/* anything */ something){ |
|
return this.store.isItem(something); // Boolean |
|
}, |
|
|
|
fetchItemByIdentity: function(/* object */ keywordArgs){ |
|
// summary: |
|
// Given the identity of an item, this method returns the item that has |
|
// that identity through the onItem callback. Conforming implementations |
|
// should return null if there is no item with the given identity. |
|
// Implementations of fetchItemByIdentity() may sometimes return an item |
|
// from a local cache and may sometimes fetch an item from a remote server. |
|
// tags: |
|
// extension |
|
this.store.fetchItemByIdentity(keywordArgs); |
|
}, |
|
|
|
getIdentity: function(/* item */ item){ |
|
return this.store.getIdentity(item); // Object |
|
}, |
|
|
|
getLabel: function(/*dojo/data/Item*/ item){ |
|
// summary: |
|
// Get the label for an item |
|
if(this.labelAttr){ |
|
return this.store.getValue(item,this.labelAttr); // String |
|
}else{ |
|
return this.store.getLabel(item); // String |
|
} |
|
}, |
|
|
|
// ======================================================================= |
|
// Write interface |
|
|
|
newItem: function(/* dijit/tree/dndSource.__Item */ args, /*dojo/data/api/Item*/ parent, /*int?*/ insertIndex){ |
|
// summary: |
|
// Creates a new item. See `dojo/data/api/Write` for details on args. |
|
// Used in drag & drop when item from external source dropped onto tree. |
|
// description: |
|
// Developers will need to override this method if new items get added |
|
// to parents with multiple children attributes, in order to define which |
|
// children attribute points to the new item. |
|
|
|
var pInfo = {parent: parent, attribute: this.childrenAttrs[0]}, LnewItem; |
|
|
|
if(this.newItemIdAttr && args[this.newItemIdAttr]){ |
|
// Maybe there's already a corresponding item in the store; if so, reuse it. |
|
this.fetchItemByIdentity({identity: args[this.newItemIdAttr], scope: this, onItem: function(item){ |
|
if(item){ |
|
// There's already a matching item in store, use it |
|
this.pasteItem(item, null, parent, true, insertIndex); |
|
}else{ |
|
// Create new item in the tree, based on the drag source. |
|
LnewItem=this.store.newItem(args, pInfo); |
|
if(LnewItem && (insertIndex!=undefined)){ |
|
// Move new item to desired position |
|
this.pasteItem(LnewItem, parent, parent, false, insertIndex); |
|
} |
|
} |
|
}}); |
|
}else{ |
|
// [as far as we know] there is no id so we must assume this is a new item |
|
LnewItem=this.store.newItem(args, pInfo); |
|
if(LnewItem && (insertIndex!=undefined)){ |
|
// Move new item to desired position |
|
this.pasteItem(LnewItem, parent, parent, false, insertIndex); |
|
} |
|
} |
|
}, |
|
|
|
pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy, /*int?*/ insertIndex){ |
|
// summary: |
|
// Move or copy an item from one parent item to another. |
|
// Used in drag & drop |
|
var store = this.store, |
|
parentAttr = this.childrenAttrs[0]; // name of "children" attr in parent item |
|
|
|
// remove child from source item, and record the attribute that child occurred in |
|
if(oldParentItem){ |
|
array.forEach(this.childrenAttrs, function(attr){ |
|
if(store.containsValue(oldParentItem, attr, childItem)){ |
|
if(!bCopy){ |
|
var values = array.filter(store.getValues(oldParentItem, attr), function(x){ |
|
return x != childItem; |
|
}); |
|
store.setValues(oldParentItem, attr, values); |
|
} |
|
parentAttr = attr; |
|
} |
|
}); |
|
} |
|
|
|
// modify target item's children attribute to include this item |
|
if(newParentItem){ |
|
if(typeof insertIndex == "number"){ |
|
// call slice() to avoid modifying the original array, confusing the data store |
|
var childItems = store.getValues(newParentItem, parentAttr).slice(); |
|
childItems.splice(insertIndex, 0, childItem); |
|
store.setValues(newParentItem, parentAttr, childItems); |
|
}else{ |
|
store.setValues(newParentItem, parentAttr, |
|
store.getValues(newParentItem, parentAttr).concat(childItem)); |
|
} |
|
} |
|
}, |
|
|
|
// ======================================================================= |
|
// Callbacks |
|
|
|
onChange: function(/*dojo/data/Item*/ /*===== item =====*/){ |
|
// summary: |
|
// Callback whenever an item has changed, so that Tree |
|
// can update the label, icon, etc. Note that changes |
|
// to an item's children or parent(s) will trigger an |
|
// onChildrenChange() so you can ignore those changes here. |
|
// tags: |
|
// callback |
|
}, |
|
|
|
onChildrenChange: function(/*===== parent, newChildrenList =====*/){ |
|
// summary: |
|
// Callback to do notifications about new, updated, or deleted items. |
|
// parent: dojo/data/Item |
|
// newChildrenList: dojo/data/Item[] |
|
// tags: |
|
// callback |
|
}, |
|
|
|
onDelete: function(/*dojo/data/Item*/ /*===== item =====*/){ |
|
// summary: |
|
// Callback when an item has been deleted. |
|
// description: |
|
// Note that there will also be an onChildrenChange() callback for the parent |
|
// of this item. |
|
// tags: |
|
// callback |
|
}, |
|
|
|
// ======================================================================= |
|
// Events from data store |
|
|
|
onNewItem: function(/* dojo/data/Item */ item, /* Object */ parentInfo){ |
|
// summary: |
|
// Handler for when new items appear in the store, either from a drop operation |
|
// or some other way. Updates the tree view (if necessary). |
|
// description: |
|
// If the new item is a child of an existing item, |
|
// calls onChildrenChange() with the new list of children |
|
// for that existing item. |
|
// |
|
// tags: |
|
// extension |
|
|
|
// We only care about the new item if it has a parent that corresponds to a TreeNode |
|
// we are currently displaying |
|
if(!parentInfo){ |
|
return; |
|
} |
|
|
|
// Call onChildrenChange() on parent (ie, existing) item with new list of children |
|
// In the common case, the new list of children is simply parentInfo.newValue or |
|
// [ parentInfo.newValue ], although if items in the store has multiple |
|
// child attributes (see `childrenAttr`), then it's a superset of parentInfo.newValue, |
|
// so call getChildren() to be sure to get right answer. |
|
this.getChildren(parentInfo.item, lang.hitch(this, function(children){ |
|
this.onChildrenChange(parentInfo.item, children); |
|
})); |
|
}, |
|
|
|
onDeleteItem: function(/*Object*/ item){ |
|
// summary: |
|
// Handler for delete notifications from underlying store |
|
this.onDelete(item); |
|
}, |
|
|
|
onSetItem: function(item, attribute /*===== , oldValue, newValue =====*/){ |
|
// summary: |
|
// Updates the tree view according to changes in the data store. |
|
// description: |
|
// Handles updates to an item's children by calling onChildrenChange(), and |
|
// other updates to an item by calling onChange(). |
|
// |
|
// See `onNewItem` for more details on handling updates to an item's children. |
|
// item: Item |
|
// attribute: attribute-name-string |
|
// oldValue: Object|Array |
|
// newValue: Object|Array |
|
// tags: |
|
// extension |
|
|
|
if(array.indexOf(this.childrenAttrs, attribute) != -1){ |
|
// item's children list changed |
|
this.getChildren(item, lang.hitch(this, function(children){ |
|
// See comments in onNewItem() about calling getChildren() |
|
this.onChildrenChange(item, children); |
|
})); |
|
}else{ |
|
// item's label/icon/etc. changed. |
|
this.onChange(item); |
|
} |
|
} |
|
}); |
|
});
|
|
|