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.
298 lines
11 KiB
298 lines
11 KiB
define([ |
|
"dojo/_base/array", // array.filter array.forEach array.indexOf array.some |
|
"dojo/aspect", // aspect.before, aspect.after |
|
"dojo/_base/declare", // declare |
|
"dojo/Deferred", |
|
"dojo/_base/lang", // lang.hitch |
|
"dojo/when", |
|
"../Destroyable" |
|
], function(array, aspect, declare, Deferred, lang, when, Destroyable){ |
|
|
|
// module: |
|
// dijit/tree/ObjectStoreModel |
|
|
|
return declare("dijit.tree.ObjectStoreModel", Destroyable, { |
|
// summary: |
|
// Implements dijit/tree/model connecting dijit/Tree to a dojo/store/api/Store that implements |
|
// getChildren(). |
|
// |
|
// If getChildren() returns an array with an observe() method, then it will be leveraged to reflect |
|
// store updates to the tree. So, this class will work best when: |
|
// |
|
// 1. the store implements dojo/store/Observable |
|
// 2. getChildren() is implemented as a query to the server (i.e. it calls store.query()) |
|
// |
|
// Drag and Drop: To support drag and drop, besides implementing getChildren() |
|
// and dojo/store/Observable, the store must support the parent option to put(). |
|
// And in order to have child elements ordered according to how the user dropped them, |
|
// put() must support the before option. |
|
|
|
// store: dojo/store/api/Store |
|
// Underlying store |
|
store: null, |
|
|
|
// labelAttr: String |
|
// Get label for tree node from this attribute |
|
labelAttr: "name", |
|
|
|
// labelType: [const] String |
|
// Specifies how to interpret the labelAttr in the data store items. |
|
// Can be "html" or "text". |
|
labelType: "text", |
|
|
|
// root: [readonly] Object |
|
// Pointer to the root item from the dojo/store/api/Store (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, |
|
|
|
constructor: function(/* Object */ args){ |
|
// summary: |
|
// Passed the arguments listed above (store, etc) |
|
// tags: |
|
// private |
|
|
|
lang.mixin(this, args); |
|
|
|
// Map from id of each parent node to array of its children, or to Promise for that array of children. |
|
this.childrenCache = {}; |
|
}, |
|
|
|
// ======================================================================= |
|
// 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{ |
|
var res = this.store.query(this.query); |
|
if(res.then){ |
|
this.own(res); // in case app calls destroy() before query completes |
|
} |
|
|
|
when(res, |
|
lang.hitch(this, function(items){ |
|
//console.log("queried root: ", res); |
|
if(items.length != 1){ |
|
throw new Error("dijit.tree.ObjectStoreModel: root query returned " + items.length + |
|
" items, but must return exactly one"); |
|
} |
|
this.root = items[0]; |
|
onItem(this.root); |
|
|
|
// Setup listener to detect if root item changes |
|
if(res.observe){ |
|
res.observe(lang.hitch(this, function(obj){ |
|
// Presumably removedFrom == insertedInto == 1, and this call indicates item has changed. |
|
//console.log("root changed: ", obj); |
|
this.onChange(obj); |
|
}), true); // true to listen for updates to obj |
|
} |
|
}), |
|
onError |
|
); |
|
} |
|
}, |
|
|
|
mayHaveChildren: function(/*===== item =====*/){ |
|
// summary: |
|
// Tells if an item has or might have children. Implementing logic here |
|
// avoids showing +/- expando icon for nodes that we know won't have children. |
|
// (For efficiency reasons we may not want to check if an element actually |
|
// has children until user clicks the expando node). |
|
// |
|
// Application code should override this method based on the data, for example |
|
// it could be `return item.leaf == true;`. |
|
// |
|
// Note that mayHaveChildren() must return true for an item if it could possibly |
|
// have children in the future, due to drag-an-drop or some other data store update. |
|
// Also note that it may return true if it's just too expensive to check during tree |
|
// creation whether or not the item has children. |
|
// item: Object |
|
// Item from the dojo/store |
|
return true; |
|
}, |
|
|
|
getChildren: function(/*Object*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){ |
|
// summary: |
|
// Calls onComplete() with array of child items of given parent item. |
|
// parentItem: |
|
// Item from the dojo/store |
|
|
|
// TODO: |
|
// For 2.0, change getChildren(), getRoot(), etc. to return a cancelable promise, rather than taking |
|
// onComplete() and onError() callbacks. Also, probably get rid of the caching. |
|
// |
|
// But be careful if we continue to maintain ObjectStoreModel as a separate class |
|
// from Tree, because in that case ObjectStoreModel can be shared by two trees, and destroying one tree |
|
// should not interfere with an in-progress getChildren() call from another tree. Also, need to make |
|
// sure that multiple calls to getChildren() for the same parentItem don't trigger duplicate calls |
|
// to onChildrenChange() and onChange(). |
|
// |
|
// I think for 2.0 though that ObjectStoreModel should be rolled into Tree itself. |
|
|
|
var id = this.store.getIdentity(parentItem); |
|
|
|
if(this.childrenCache[id]){ |
|
// If this.childrenCache[id] is defined, then it always has the latest list of children |
|
// (like a live collection), so just return it. |
|
when(this.childrenCache[id], onComplete, onError); |
|
return; |
|
} |
|
|
|
// Query the store. |
|
// Cache result so that we can close the query on destroy(), and to avoid setting up multiple observers |
|
// when getChildren() is called multiple times for the same parent. |
|
// The only problem is that getChildren() on non-Observable stores may return a stale value. |
|
var res = this.childrenCache[id] = this.store.getChildren(parentItem); |
|
if(res.then){ |
|
this.own(res); // in case app calls destroy() before query completes |
|
} |
|
|
|
// Setup observer in case children list changes, or the item(s) in the children list are updated. |
|
if(res.observe){ |
|
this.own(res.observe(lang.hitch(this, function(obj, removedFrom, insertedInto){ |
|
//console.log("observe on children of ", id, ": ", obj, removedFrom, insertedInto); |
|
|
|
// If removedFrom == insertedInto, this call indicates that the item has changed. |
|
// Even if removedFrom != insertedInto, the item may have changed. |
|
this.onChange(obj); |
|
|
|
if(removedFrom != insertedInto){ |
|
// Indicates an item was added, removed, or re-parented. The children[] array (returned from |
|
// res.then(...)) has already been updated (like a live collection), so just use it. |
|
when(res, lang.hitch(this, "onChildrenChange", parentItem)); |
|
} |
|
}), true)); // true means to notify on item changes |
|
} |
|
|
|
// User callback |
|
when(res, onComplete, onError); |
|
}, |
|
|
|
// ======================================================================= |
|
// Inspecting items |
|
|
|
isItem: function(/*===== something =====*/){ |
|
return true; // Boolean |
|
}, |
|
|
|
getIdentity: function(/* item */ item){ |
|
return this.store.getIdentity(item); // Object |
|
}, |
|
|
|
getLabel: function(/*dojo/data/Item*/ item){ |
|
// summary: |
|
// Get the label for an item |
|
return item[this.labelAttr]; // String |
|
}, |
|
|
|
// ======================================================================= |
|
// Write interface, for DnD |
|
|
|
newItem: function(/* dijit/tree/dndSource.__Item */ args, /*Item*/ parent, /*int?*/ insertIndex, /*Item*/ before){ |
|
// 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. |
|
|
|
return this.store.put(args, { |
|
parent: parent, |
|
before: before |
|
}); |
|
}, |
|
|
|
pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, |
|
/*Boolean*/ bCopy, /*int?*/ insertIndex, /*Item*/ before){ |
|
// summary: |
|
// Move or copy an item from one parent item to another. |
|
// Used in drag & drop. |
|
|
|
var d = new Deferred(); |
|
|
|
if(oldParentItem === newParentItem && !bCopy && !before){ |
|
// Avoid problem when items visually disappear when dropped onto their parent. |
|
// Happens because the (no-op) store.put() call doesn't generate any notification |
|
// that the childItem was added/moved. |
|
d.resolve(true); |
|
return d; |
|
} |
|
|
|
if(oldParentItem && !bCopy){ |
|
// In order for DnD moves to work correctly, childItem needs to be orphaned from oldParentItem |
|
// before being adopted by newParentItem. That way, the TreeNode is moved rather than |
|
// an additional TreeNode being created, and the old TreeNode subsequently being deleted. |
|
// The latter loses information such as selection and opened/closed children TreeNodes. |
|
// Unfortunately simply calling this.store.put() will send notifications in a random order, based |
|
// on when the TreeNodes in question originally appeared, and not based on the drag-from |
|
// TreeNode vs. the drop-onto TreeNode. |
|
|
|
this.getChildren(oldParentItem, lang.hitch(this, function(oldParentChildren){ |
|
oldParentChildren = [].concat(oldParentChildren); // concat to make copy |
|
var index = array.indexOf(oldParentChildren, childItem); |
|
oldParentChildren.splice(index, 1); |
|
this.onChildrenChange(oldParentItem, oldParentChildren); |
|
|
|
d.resolve(this.store.put(childItem, { |
|
overwrite: true, |
|
parent: newParentItem, |
|
oldParent: oldParentItem, |
|
before: before, |
|
isCopy: false |
|
})); |
|
})); |
|
}else{ |
|
d.resolve(this.store.put(childItem, { |
|
overwrite: true, |
|
parent: newParentItem, |
|
oldParent: oldParentItem, |
|
before: before, |
|
isCopy: true |
|
})); |
|
} |
|
|
|
return d; |
|
}, |
|
|
|
// ======================================================================= |
|
// 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: Object[] |
|
// Items from the store |
|
// tags: |
|
// callback |
|
}, |
|
|
|
onDelete: function(/*dojo/data/Item*/ /*===== item =====*/){ |
|
// summary: |
|
// Callback when an item has been deleted. |
|
// Actually we have no way of knowing this with the new dojo.store API, |
|
// so this method is never called (but it's left here since Tree connects |
|
// to it). |
|
// tags: |
|
// callback |
|
} |
|
}); |
|
});
|
|
|