if (!dojo._hasResource["dijit.Tree"]) { // _hasResource checks added by build.
// Do not use _hasResource directly in
// your code.
dojo._hasResource["dijit.Tree"] = true;
dojo.provide("dijit.Tree");
dojo.require("dojo.fx");
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");
dojo.require("dijit._Container");
dojo.require("dojo.cookie");
dojo.declare("dijit._TreeNode", [dijit._Widget, dijit._Templated,
dijit._Container, dijit._Contained], {
// summary
// Single node within a tree
// item: dojo.data.Item
// the dojo.data entry this tree represents
item : null,
isTreeNode : true,
// label: String
// Text of this tree node
label : "",
isExpandable : null, // show expando node
isExpanded : false,
// state: String
// dynamic loading-related stuff.
// When an empty folder node appears, it is "UNCHECKED" first,
// then after dojo.data query it becomes "LOADING" and, finally
// "LOADED"
state : "UNCHECKED",
templateString : "
\n",
postCreate : function() {
// set label, escaping special characters
this.setLabelNode(this.label);
// set expand icon for leaf
this._setExpando();
// set icon and label class based on item
this._updateItemClasses(this.item);
if (this.isExpandable) {
dijit.setWaiState(this.labelNode, "expanded",
this.isExpanded);
}
},
markProcessing : function() {
// summary: visually denote that tree is loading data, etc.
this.state = "LOADING";
this._setExpando(true);
},
unmarkProcessing : function() {
// summary: clear markup from markProcessing() call
this._setExpando(false);
},
_updateItemClasses : function(item) {
// summary: set appropriate CSS classes for item (used to
// allow for item updates to change respective CSS)
this.iconNode.className = "dijitInline dijitTreeIcon "
+ this.tree.getIconClass(item);
this.labelNode.className = "dijitTreeLabel "
+ this.tree.getLabelClass(item);
},
_updateLayout : function() {
// summary: set appropriate CSS classes for this.domNode
var parent = this.getParent();
if (parent && parent.isTree && parent._hideRoot) {
/*
* if we are hiding the root node then make every first
* level child look like a root node
*/
dojo.addClass(this.domNode, "dijitTreeIsRoot");
} else {
dojo.toggleClass(this.domNode, "dijitTreeIsLast", !this
.getNextSibling());
}
},
_setExpando : function(/* Boolean */processing) {
// summary: set the right image for the expando node
// apply the appropriate class to the expando node
var styles = ["dijitTreeExpandoLoading",
"dijitTreeExpandoOpened", "dijitTreeExpandoClosed",
"dijitTreeExpandoLeaf"];
var idx = processing ? 0 : (this.isExpandable
? (this.isExpanded ? 1 : 2)
: 3);
dojo.forEach(styles, function(s) {
dojo.removeClass(this.expandoNode, s);
}, this);
dojo.addClass(this.expandoNode, styles[idx]);
// provide a non-image based indicator for images-off mode
this.expandoNodeText.innerHTML = processing
? "*"
: (this.isExpandable
? (this.isExpanded ? "-" : "+")
: "*");
},
expand : function() {
// summary: show my children
if (this.isExpanded) {
return;
}
// cancel in progress collapse operation
if (this._wipeOut.status() == "playing") {
this._wipeOut.stop();
}
this.isExpanded = true;
dijit.setWaiState(this.labelNode, "expanded", "true");
dijit.setWaiRole(this.containerNode, "group");
this._setExpando();
this._wipeIn.play();
},
collapse : function() {
if (!this.isExpanded) {
return;
}
// cancel in progress expand operation
if (this._wipeIn.status() == "playing") {
this._wipeIn.stop();
}
this.isExpanded = false;
dijit.setWaiState(this.labelNode, "expanded", "false");
this._setExpando();
this._wipeOut.play();
},
setLabelNode : function(label) {
this.labelNode.innerHTML = "";
this.labelNode.appendChild(document.createTextNode(label));
},
_setChildren : function(/* Object[] */childrenArray) {
// summary:
// Sets the children of this node.
// Sets this.isExpandable based on whether or not there are
// children
// Takes array of objects like: {label: ...} (_TreeNode
// options basically)
// See parameters of _TreeNode for details.
this.destroyDescendants();
this.state = "LOADED";
var nodeMap = {};
if (childrenArray && childrenArray.length > 0) {
this.isExpandable = true;
if (!this.containerNode) { // maybe this node was
// unfolderized and still
// has container
this.containerNode = this.tree.containerNodeTemplate
.cloneNode(true);
this.domNode.appendChild(this.containerNode);
}
// Create _TreeNode widget for each specified tree node
dojo.forEach(childrenArray, function(childParams) {
var child = new dijit._TreeNode(dojo.mixin(
{
tree : this.tree,
label : this.tree
.getLabel(childParams.item)
}, childParams));
this.addChild(child);
var identity = this.tree.store
.getIdentity(childParams.item);
nodeMap[identity] = child;
if (this.tree.persist) {
if (this.tree._openedItemIds[identity]) {
this.tree._expandNode(child);
}
}
}, this);
// note that updateLayout() needs to be called on each
// child after
// _all_ the children exist
dojo.forEach(this.getChildren(), function(child, idx) {
child._updateLayout();
});
} else {
this.isExpandable = false;
}
if (this._setExpando) {
// change expando to/form dot or + icon, as appropriate
this._setExpando(false);
}
if (this.isTree && this._hideRoot) {
// put first child in tab index if one exists.
var fc = this.getChildren()[0];
var tabnode = fc ? fc.labelNode : this.domNode;
tabnode.setAttribute("tabIndex", "0");
}
// create animations for showing/hiding the children (if
// children exist)
if (this.containerNode && !this._wipeIn) {
this._wipeIn = dojo.fx.wipeIn({
node : this.containerNode,
duration : 150
});
this._wipeOut = dojo.fx.wipeOut({
node : this.containerNode,
duration : 150
});
}
return nodeMap;
},
_addChildren : function(/* object[] */childrenArray) {
// summary:
// adds the children to this node.
// Takes array of objects like: {label: ...} (_TreeNode
// options basically)
// See parameters of _TreeNode for details.
var nodeMap = {};
if (childrenArray && childrenArray.length > 0) {
dojo.forEach(childrenArray, function(childParams) {
var child = new dijit._TreeNode(dojo.mixin({
tree : this.tree,
label : this.tree
.getLabel(childParams.item)
}, childParams));
this.addChild(child);
nodeMap[this.tree.store
.getIdentity(childParams.item)] = child;
}, this);
dojo.forEach(this.getChildren(), function(child, idx) {
child._updateLayout();
});
}
return nodeMap;
},
deleteNode : function(/* treeNode */node) {
node.destroy();
var children = this.getChildren();
if (children.length == 0) {
this.isExpandable = false;
this.collapse();
}
dojo.forEach(children, function(child) {
child._updateLayout();
});
},
makeExpandable : function() {
// summary
// if this node wasn't already showing the expando node,
// turn it into one and call _setExpando()
this.isExpandable = true;
this._setExpando(false);
}
});
dojo.declare("dijit.Tree", dijit._TreeNode, {
// summary
// This widget displays hierarchical data from a store. A query is
// specified
// to get the "top level children" from a data store, and then those
// items are
// queried for their children and so on (but lazily, as the user clicks
// the expand node).
//
// Thus in the default mode of operation this widget is technically a
// forest, not a tree,
// in that there can be multiple "top level children". However, if you
// specify label,
// then a special top level node (not corresponding to any item in the
// datastore) is
// created, to father all the top level children.
// store: String||dojo.data.Store
// The store to get data to display in the tree
store : null,
// query: String
// query to get top level node(s) of tree (ex: {type:'continent'})
query : null,
// childrenAttr: String
// one ore more attributes that holds children of a tree node
childrenAttr : ["children"],
templateString : "\n",
isExpandable : true,
isTree : true,
// persist: Boolean
// enables/disables use of cookies for state saving.
persist : true,
// dndController: String
// class name to use as as the dnd controller
dndController : null,
// parameters to pull off of the tree and pass on to the dndController
// as its params
dndParams : ["onDndDrop", "itemCreator", "onDndCancel",
"checkAcceptance", "checkItemAcceptance"],
// declare the above items so they can be pulled from the tree's markup
onDndDrop : null,
itemCreator : null,
onDndCancel : null,
checkAcceptance : null,
checkItemAcceptance : null,
_publish : function(/* String */topicName, /* Object */message) {
// summary:
// Publish a message for this widget/topic
dojo.publish(this.id, [dojo.mixin({
tree : this,
event : topicName
}, message || {})]);
},
postMixInProperties : function() {
this.tree = this;
this.lastFocused = this.labelNode;
this._itemNodeMap = {};
this._hideRoot = !this.label;
if (!this.store.getFeatures()['dojo.data.api.Identity']) {
throw new Error("dijit.tree requires access to a store supporting the dojo.data Identity api");
}
if (!this.cookieName) {
this.cookieName = this.id + "SaveStateCookie";
}
// if the store supports Notification, subscribe to the notification
// events
if (this.store.getFeatures()['dojo.data.api.Notification']) {
this.connect(this.store, "onNew", "_onNewItem");
this.connect(this.store, "onDelete", "_onDeleteItem");
this.connect(this.store, "onSet", "_onSetItem");
}
},
postCreate : function() {
// load in which nodes should be opened automatically
if (this.persist) {
var cookie = dojo.cookie(this.cookieName);
this._openedItemIds = {};
if (cookie) {
dojo.forEach(cookie.split(','), function(item) {
this._openedItemIds[item] = true;
}, this);
}
}
// make template for container node (we will clone this and insert
// it into
// any nodes that have children)
var div = document.createElement('div');
div.style.display = 'none';
div.className = "dijitTreeContainer";
dijit.setWaiRole(div, "presentation");
this.containerNodeTemplate = div;
if (this._hideRoot) {
this.rowNode.style.display = "none";
}
this.inherited("postCreate", arguments);
// load top level children
this._expandNode(this);
if (this.dndController) {
if (dojo.isString(this.dndController)) {
this.dndController = dojo.getObject(this.dndController);
}
var params = {};
for (var i = 0; i < this.dndParams.length; i++) {
if (this[this.dndParams[i]]) {
params[this.dndParams[i]] = this[this.dndParams[i]];
}
}
this.dndController = new this.dndController(this, params);
}
this.connect(this.domNode, dojo.isIE ? "onactivate" : "onfocus",
"_onTreeFocus");
},
// //////////// Data store related functions //////////////////////
mayHaveChildren : function(/* dojo.data.Item */item) {
// summary
// User overridable function to tell if an item has or may have
// children.
// Controls whether or not +/- expando icon is shown.
// (For efficiency reasons we may not want to check if an element
// has
// children until user clicks the expando node)
return dojo.some(this.childrenAttr, function(attr) {
return this.store.hasAttribute(item, attr);
}, this);
},
getItemChildren : function(/* dojo.data.Item */parentItem, /* function(items) */
onComplete) {
// summary
// User overridable function that return array of child items of
// given parent item,
// or if parentItem==null then return top items in tree
var store = this.store;
if (parentItem == null) {
// get top level nodes
store.fetch({
query : this.query,
onComplete : onComplete
});
} else {
// get children of specified node
var childItems = [];
for (var i = 0; i < this.childrenAttr.length; i++) {
childItems = childItems.concat(store.getValues(parentItem,
this.childrenAttr[i]));
}
// count how many items need to be loaded
var _waitCount = 0;
dojo.forEach(childItems, function(item) {
if (!store.isItemLoaded(item)) {
_waitCount++;
}
});
if (_waitCount == 0) {
// all items are already loaded. proceed..
onComplete(childItems);
} else {
// still waiting for some or all of the items to load
function onItem(item) {
if (--_waitCount == 0) {
// all nodes have been loaded, send them to the tree
onComplete(childItems);
}
}
dojo.forEach(childItems, function(item) {
if (!store.isItemLoaded(item)) {
store.loadItem({
item : item,
onItem : onItem
});
}
});
}
}
},
getItemParentIdentity : function(/* dojo.data.Item */item, /* Object */
parentInfo) {
// summary
// User overridable function, to return id of parent (or null if top
// level).
// It's called with args from dojo.store.onNew
return this.store.getIdentity(parentInfo.item); // String
},
getLabel : function(/* dojo.data.Item */item) {
// summary: user overridable function to get the label for a tree
// node (given the item)
return this.store.getLabel(item); // String
},
getIconClass : function(/* dojo.data.Item */item) {
// summary: user overridable function to return CSS class name to
// display icon
},
getLabelClass : function(/* dojo.data.Item */item) {
// summary: user overridable function to return CSS class name to
// display label
},
_onLoadAllItems : function(/* _TreeNode */node, /* dojo.data.Item[] */
items) {
// sumary: callback when all the children of a given node have been
// loaded
var childParams = dojo.map(items, function(item) {
return {
item : item,
isExpandable : this.mayHaveChildren(item)
};
}, this);
dojo.mixin(this._itemNodeMap, node._setChildren(childParams));
this._expandNode(node);
},
// ///////// Keyboard and Mouse handlers ////////////////////
_onKeyPress : function(/* Event */e) {
// summary: translates keypress events into commands for the
// controller
if (e.altKey) {
return;
}
var treeNode = dijit.getEnclosingWidget(e.target);
if (!treeNode) {
return;
}
// Note: On IE e.keyCode is not 0 for printables so check
// e.charCode.
// In dojo charCode is universally 0 for non-printables.
if (e.charCode) { // handle printables (letter navigation)
// Check for key navigation.
var navKey = e.charCode;
if (!e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
navKey = (String.fromCharCode(navKey)).toLowerCase();
this._onLetterKeyNav({
node : treeNode,
key : navKey
});
dojo.stopEvent(e);
}
} else { // handle non-printables (arrow keys)
var map = this._keyHandlerMap;
if (!map) {
// setup table mapping keys to events
map = {};
map[dojo.keys.ENTER] = "_onEnterKey";
map[dojo.keys.LEFT_ARROW] = "_onLeftArrow";
map[dojo.keys.RIGHT_ARROW] = "_onRightArrow";
map[dojo.keys.UP_ARROW] = "_onUpArrow";
map[dojo.keys.DOWN_ARROW] = "_onDownArrow";
map[dojo.keys.HOME] = "_onHomeKey";
map[dojo.keys.END] = "_onEndKey";
this._keyHandlerMap = map;
}
if (this._keyHandlerMap[e.keyCode]) {
this[this._keyHandlerMap[e.keyCode]]({
node : treeNode,
item : treeNode.item
});
dojo.stopEvent(e);
}
}
},
_onEnterKey : function(/* Object */message) {
this._publish("execute", {
item : message.item,
node : message.node
});
this.onClick(message.item, message.node);
},
_onDownArrow : function(/* Object */message) {
// summary: down arrow pressed; get next visible node, set focus
// there
var returnNode = this._navToNextNode(message.node);
if (returnNode && returnNode.isTreeNode) {
returnNode.tree.focusNode(returnNode);
return returnNode;
}
},
_onUpArrow : function(/* Object */message) {
// summary: up arrow pressed; move to previous visible node
var nodeWidget = message.node;
var returnWidget = nodeWidget;
// if younger siblings
var previousSibling = nodeWidget.getPreviousSibling();
if (previousSibling) {
nodeWidget = previousSibling;
// if the previous nodeWidget is expanded, dive in deep
while (nodeWidget.isExpandable && nodeWidget.isExpanded
&& nodeWidget.hasChildren()) {
returnWidget = nodeWidget;
// move to the last child
var children = nodeWidget.getChildren();
nodeWidget = children[children.length - 1];
}
} else {
// if this is the first child, return the parent
// unless the parent is the root of a tree with a hidden root
var parent = nodeWidget.getParent();
if (!(this._hideRoot && parent === this)) {
nodeWidget = parent;
}
}
if (nodeWidget && nodeWidget.isTreeNode) {
returnWidget = nodeWidget;
}
if (returnWidget && returnWidget.isTreeNode) {
returnWidget.tree.focusNode(returnWidget);
return returnWidget;
}
},
_onRightArrow : function(/* Object */message) {
// summary: right arrow pressed; go to child node
var nodeWidget = message.node;
var returnWidget = nodeWidget;
// if not expanded, expand, else move to 1st child
if (nodeWidget.isExpandable && !nodeWidget.isExpanded) {
this._expandNode(nodeWidget);
} else if (nodeWidget.hasChildren()) {
nodeWidget = nodeWidget.getChildren()[0];
}
if (nodeWidget && nodeWidget.isTreeNode) {
returnWidget = nodeWidget;
}
if (returnWidget && returnWidget.isTreeNode) {
returnWidget.tree.focusNode(returnWidget);
return returnWidget;
}
},
_onLeftArrow : function(/* Object */message) {
// summary: left arrow pressed; go to parent
var node = message.node;
var returnWidget = node;
// if not collapsed, collapse, else move to parent
if (node.isExpandable && node.isExpanded) {
this._collapseNode(node);
} else {
node = node.getParent();
}
if (node && node.isTreeNode) {
returnWidget = node;
}
if (returnWidget && returnWidget.isTreeNode) {
returnWidget.tree.focusNode(returnWidget);
return returnWidget;
}
},
_onHomeKey : function() {
// summary: home pressed; get first visible node, set focus there
var returnNode = this._navToRootOrFirstNode();
if (returnNode) {
returnNode.tree.focusNode(returnNode);
return returnNode;
}
},
_onEndKey : function(/* Object */message) {
// summary: end pressed; go to last visible node
var returnWidget = message.node.tree;
var lastChild = returnWidget;
while (lastChild.isExpanded) {
var c = lastChild.getChildren();
lastChild = c[c.length - 1];
if (lastChild.isTreeNode) {
returnWidget = lastChild;
}
}
if (returnWidget && returnWidget.isTreeNode) {
returnWidget.tree.focusNode(returnWidget);
return returnWidget;
}
},
_onLetterKeyNav : function(message) {
// summary: letter key pressed; search for node starting with first
// char = key
var node = startNode = message.node;
var key = message.key;
do {
node = this._navToNextNode(node);
// check for last node, jump to first node if necessary
if (!node) {
node = this._navToRootOrFirstNode();
}
} while (node !== startNode
&& (node.label.charAt(0).toLowerCase() != key));
if (node && node.isTreeNode) {
// no need to set focus if back where we started
if (node !== startNode) {
node.tree.focusNode(node);
}
return node;
}
},
_onClick : function(/* Event */e) {
// summary: translates click events into commands for the controller
// to process
var domElement = e.target;
// find node
var nodeWidget = dijit.getEnclosingWidget(domElement);
if (!nodeWidget || !nodeWidget.isTreeNode) {
return;
}
if (domElement == nodeWidget.expandoNode
|| domElement == nodeWidget.expandoNodeText) {
// expando node was clicked
if (nodeWidget.isExpandable) {
this._onExpandoClick({
node : nodeWidget
});
}
} else {
this._publish("execute", {
item : nodeWidget.item,
node : nodeWidget
});
this.onClick(nodeWidget.item, nodeWidget);
this.focusNode(nodeWidget);
}
dojo.stopEvent(e);
},
_onExpandoClick : function(/* Object */message) {
// summary: user clicked the +/- icon; expand or collapse my
// children.
var node = message.node;
if (node.isExpanded) {
this._collapseNode(node);
} else {
this._expandNode(node);
}
},
onClick : function(/* dojo.data */item, /* TreeNode */node) {
// summary: user overridable function for executing a tree item
},
_navToNextNode : function(node) {
// summary: get next visible node
var returnNode;
// if this is an expanded node, get the first child
if (node.isExpandable && node.isExpanded && node.hasChildren()) {
returnNode = node.getChildren()[0];
} else {
// find a parent node with a sibling
while (node && node.isTreeNode) {
returnNode = node.getNextSibling();
if (returnNode) {
break;
}
node = node.getParent();
}
}
return returnNode;
},
_navToRootOrFirstNode : function() {
// summary: get first visible node
if (!this._hideRoot) {
return this;
} else {
var returnNode = this.getChildren()[0];
if (returnNode && returnNode.isTreeNode) {
return returnNode;
}
}
},
_collapseNode : function(/* _TreeNode */node) {
// summary: called when the user has requested to collapse the node
if (node.isExpandable) {
if (node.state == "LOADING") {
// ignore clicks while we are in the process of loading data
return;
}
if (this.lastFocused) {
// are we collapsing a descendant with focus?
if (dojo.isDescendant(this.lastFocused.domNode,
node.domNode)) {
this.focusNode(node);
} else {
// clicking the expando node might have erased focus
// from
// the current item; restore it
this.focusNode(this.lastFocused);
}
}
node.collapse();
if (this.persist && node.item) {
delete this._openedItemIds[this.store
.getIdentity(node.item)];
this._saveState();
}
}
},
_expandNode : function(/* _TreeNode */node) {
// summary: called when the user has requested to expand the node
// clicking the expando node might have erased focus from the
// current item; restore it
var t = node.tree;
if (t.lastFocused) {
t.focusNode(t.lastFocused);
}
if (!node.isExpandable) {
return;
}
var store = this.store;
var getValue = this.store.getValue;
switch (node.state) {
case "LOADING" :
// ignore clicks while we are in the process of loading data
return;
case "UNCHECKED" :
// need to load all the children, and then expand
node.markProcessing();
var _this = this;
var onComplete = function(childItems) {
node.unmarkProcessing();
_this._onLoadAllItems(node, childItems);
};
this.getItemChildren(node.item, onComplete);
break;
default :
// data is already loaded; just proceed
if (node.expand) { // top level Tree doesn't have expand()
// method
node.expand();
if (this.persist && node.item) {
this._openedItemIds[this.store
.getIdentity(node.item)] = true;
this._saveState();
}
}
break;
}
},
// //////////////// Miscellaneous functions ////////////////
blurNode : function() {
// summary
// Removes focus from the currently focused node (which must be
// visible).
// Usually not called directly (just call focusNode() on another
// node instead)
var node = this.lastFocused;
if (!node) {
return;
}
var labelNode = node.labelNode;
dojo.removeClass(labelNode, "dijitTreeLabelFocused");
labelNode.setAttribute("tabIndex", "-1");
this.lastFocused = null;
},
focusNode : function(/* _tree.Node */node) {
// summary
// Focus on the specified node (which must be visible)
// set focus so that the label will be voiced using screen readers
node.labelNode.focus();
},
_onBlur : function() {
// summary:
// We've moved away from the whole tree. The currently "focused"
// node
// (see focusNode above) should remain as the lastFocused node so we
// can
// tab back into the tree. Just change CSS to get rid of the dotted
// border
// until that time
if (this.lastFocused) {
var labelNode = this.lastFocused.labelNode;
dojo.removeClass(labelNode, "dijitTreeLabelFocused");
}
},
_onTreeFocus : function(evt) {
var node = dijit.getEnclosingWidget(evt.target);
if (node != this.lastFocused) {
this.blurNode();
}
var labelNode = node.labelNode;
// set tabIndex so that the tab key can find this node
labelNode.setAttribute("tabIndex", "0");
dojo.addClass(labelNode, "dijitTreeLabelFocused");
this.lastFocused = node;
},
// ////////////// Events from data store //////////////////////////
_onNewItem : function(/* Object */item, parentInfo) {
// summary: callback when new item has been added to the store.
var loadNewItem; // should new item be displayed in tree?
if (parentInfo) {
var parent = this._itemNodeMap[this.getItemParentIdentity(item,
parentInfo)];
// If new item's parent item not in tree view yet, can safely
// ignore.
// Also, if a query of specified parent wouldn't return this
// item, then ignore.
if (!parent
|| dojo
.indexOf(this.childrenAttr,
parentInfo.attribute) == -1) {
return;
}
}
var childParams = {
item : item,
isExpandable : this.mayHaveChildren(item)
};
if (parent) {
if (!parent.isExpandable) {
parent.makeExpandable();
}
if (parent.state == "LOADED" || parent.isExpanded) {
var childrenMap = parent._addChildren([childParams]);
}
} else {
// top level node
var childrenMap = this._addChildren([childParams]);
}
if (childrenMap) {
dojo.mixin(this._itemNodeMap, childrenMap);
// this._itemNodeMap[this.store.getIdentity(item)]=child;
}
},
_onDeleteItem : function(/* Object */item) {
// summary: delete event from the store
// since the object has just been deleted, we need to
// use the name directly
var identity = this.store.getIdentity(item);
var node = this._itemNodeMap[identity];
if (node) {
var parent = node.getParent();
parent.deleteNode(node);
this._itemNodeMap[identity] = null;
}
},
_onSetItem : function(/* Object */item) {
// summary: set data event on an item in the store
var identity = this.store.getIdentity(item);
node = this._itemNodeMap[identity];
if (node) {
node.setLabelNode(this.getLabel(item));
node._updateItemClasses(item);
}
},
_saveState : function() {
// summary: create and save a cookie with the currently expanded
// nodes identifiers
if (!this.persist) {
return;
}
var ary = [];
for (var id in this._openedItemIds) {
ary.push(id);
}
dojo.cookie(this.cookieName, ary.join(","));
}
});
}