ttrss/lib/dijit/tree/TreeStoreModel.js

382 lines
13 KiB
JavaScript

/*
Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved.
Available via Academic Free License >= 2.1 OR the modified BSD license.
see: http://dojotoolkit.org/license for details
*/
if(!dojo._hasResource["dijit.tree.TreeStoreModel"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dijit.tree.TreeStoreModel"] = true;
dojo.provide("dijit.tree.TreeStoreModel");
dojo.declare(
"dijit.tree.TreeStoreModel",
null,
{
// summary:
// Implements dijit.Tree.model connecting to a store with a single
// root item. Any methods passed into the constructor will override
// the ones defined here.
// store: dojo.data.Store
// 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
dojo.mixin(this, args);
this.connects = [];
var store = this.store;
if(!store.getFeatures()['dojo.data.api.Identity']){
throw new Error("dijit.Tree: 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([
dojo.connect(store, "onNew", this, "onNewItem"),
dojo.connect(store, "onDelete", this, "onDeleteItem"),
dojo.connect(store, "onSet", this, "onSetItem")
]);
}
},
destroy: function(){
dojo.forEach(this.connects, dojo.disconnect);
// 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: dojo.hitch(this, function(items){
if(items.length != 1){
throw new Error(this.declaredClass + ": query " + dojo.toJson(this.query) + " returned " + items.length +
" items, but must return exactly one item");
}
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 dojo.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 = dojo.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){
dojo.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
dojo.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){
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(/* dojo.dnd.Item */ args, /*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){
dojo.forEach(this.childrenAttrs, function(attr){
if(store.containsValue(oldParentItem, attr, childItem)){
if(!bCopy){
var values = dojo.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(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){
// summary:
// Callback to do notifications about new, updated, or deleted items.
// tags:
// callback
},
onDelete: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){
// 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, dojo.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 */ item,
/* attribute-name-string */ attribute,
/* object | array */ oldValue,
/* object | array */ 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.
// tags:
// extension
if(dojo.indexOf(this.childrenAttrs, attribute) != -1){
// item's children list changed
this.getChildren(item, dojo.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);
}
}
});
}