dojo.provide("dijit.Tree"); dojo.require("dojo.fx"); dojo.require("dijit._Widget"); dojo.require("dijit._Templated"); dojo.require("dijit._Container"); dojo.require("dijit._Contained"); dojo.require("dojo.cookie"); dojo.declare( "dijit._TreeNode", [dijit._Widget, dijit._Templated, dijit._Container, dijit._Contained], { // summary: // Single node within a tree. This class is used internally // by Tree and should not be accessed directly. // tags: // private // item: dojo.data.Item // the dojo.data entry this tree represents item: null, // isTreeNode: [protected] Boolean // Indicates that this is a TreeNode. Used by `dijit.Tree` only, // should not be accessed directly. isTreeNode: true, // label: String // Text of this tree node label: "", // isExpandable: [private] Boolean // This node has children, so show the expando node (+ sign) isExpandable: null, // isExpanded: [readonly] Boolean // This node is currently expanded (ie, opened) isExpanded: false, // state: [private] 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", templatePath: dojo.moduleUrl("dijit", "templates/TreeNode.html"), 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); if(this == this.tree.rootNode){ dijit.setWaitState(this.tree.domNode, "expanded", this.isExpanded); } } }, _setIndentAttr: function(indent){ // summary: // Tell this node how many levels it should be indented // description: // 0 for top level nodes, 1 for their children, 2 for their // grandchildren, etc. this.indent = indent; // Math.max() is to prevent negative padding on hidden root node (when indent == -1) // 19 is the width of the expandoIcon (TODO: get this from CSS instead of hardcoding) var pixels = (Math.max(indent, 0) * 19) + "px"; dojo.style(this.domNode, "backgroundPosition", pixels + " 0px"); dojo.style(this.rowNode, dojo._isBodyLtr() ? "paddingLeft" : "paddingRight", pixels); dojo.forEach(this.getChildren(), function(child){ child.attr("indent", indent+1); }); }, markProcessing: function(){ // summary: // Visually denote that tree is loading data, etc. // tags: // private this.state = "LOADING"; this._setExpando(true); }, unmarkProcessing: function(){ // summary: // Clear markup from markProcessing() call // tags: // private this._setExpando(false); }, _updateItemClasses: function(item){ // summary: // Set appropriate CSS classes for icon and label dom node // (used to allow for item updates to change respective CSS) // tags: // private var tree = this.tree, model = tree.model; if(tree._v10Compat && item === model.root){ // For back-compat with 1.0, need to use null to specify root item (TODO: remove in 2.0) item = null; } if(this._iconClass){ dojo.removeClass(this.iconNode, this._iconClass); } this._iconClass = tree.getIconClass(item, this.isExpanded); if(this._iconClass){ dojo.addClass(this.iconNode, this._iconClass); } dojo.style(this.iconNode, tree.getIconStyle(item, this.isExpanded) || {}); if(this._labelClass){ dojo.removeClass(this.labelNode, this._labelClass); } this._labelClass = tree.getLabelClass(item, this.isExpanded); if(this._labelClass){ dojo.addClass(this.labelNode, this._labelClass); } dojo.style(this.labelNode, tree.getLabelStyle(item, this.isExpanded) || {}); }, _updateLayout: function(){ // summary: // Set appropriate CSS classes for this.domNode // tags: // private var parent = this.getParent(); if(!parent || parent.rowNode.style.display == "none"){ /* 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 // tags: // private // apply the appropriate class to the expando node var styles = ["dijitTreeExpandoLoading", "dijitTreeExpandoOpened", "dijitTreeExpandoClosed", "dijitTreeExpandoLeaf"]; var _a11yStates = ["*","-","+","*"]; 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 = _a11yStates[idx]; }, expand: function(){ // summary: // Show my children if(this.isExpanded){ return; } // cancel in progress collapse operation this._wipeOut && this._wipeOut.stop(); this.isExpanded = true; dijit.setWaiState(this.labelNode, "expanded", "true"); dijit.setWaiRole(this.containerNode, "group"); dojo.addClass(this.contentNode,'dijitTreeContentExpanded'); this._setExpando(); this._updateItemClasses(this.item); if(this == this.tree.rootNode){ dijit.setWaiState(this.tree.domNode, "expanded", "true"); } if(!this._wipeIn){ this._wipeIn = dojo.fx.wipeIn({ node: this.containerNode, duration: dijit.defaultDuration }); } this._wipeIn.play(); }, collapse: function(){ // summary: // Collapse this node (if it's expanded) if(!this.isExpanded){ return; } // cancel in progress expand operation this._wipeIn && this._wipeIn.stop(); this.isExpanded = false; dijit.setWaiState(this.labelNode, "expanded", "false"); if(this == this.tree.rootNode){ dijit.setWaiState(this.tree.domNode, "expanded", "false"); } dojo.removeClass(this.contentNode,'dijitTreeContentExpanded'); this._setExpando(); this._updateItemClasses(this.item); if(!this._wipeOut){ this._wipeOut = dojo.fx.wipeOut({ node: this.containerNode, duration: dijit.defaultDuration }); } this._wipeOut.play(); }, setLabelNode: function(label){ // summary: // Sets the label this.labelNode.innerHTML = ""; this.labelNode.appendChild(dojo.doc.createTextNode(label)); }, // indent: Integer // Levels from this node to the root node indent: 0, setChildItems: function(/* Object[] */ items){ // summary: // Sets the child items of this node, removing/adding nodes // from current children to match specified items[] array. var tree = this.tree, model = tree.model; // Orphan all my existing children. // If items contains some of the same items as before then we will reattach them. // Don't call this.removeChild() because that will collapse the tree etc. this.getChildren().forEach(function(child){ dijit._Container.prototype.removeChild.call(this, child); }, this); this.state = "LOADED"; if(items && items.length > 0){ this.isExpandable = true; // Create _TreeNode widget for each specified tree node, unless one already // exists and isn't being used (presumably it's from a DnD move and was recently // released dojo.forEach(items, function(item){ var id = model.getIdentity(item), existingNode = tree._itemNodeMap[id], node = ( existingNode && !existingNode.getParent() ) ? existingNode : this.tree._createTreeNode({ item: item, tree: tree, isExpandable: model.mayHaveChildren(item), label: tree.getLabel(item), indent: this.indent + 1 }); if(existingNode){ existingNode.attr('indent', this.indent+1); } this.addChild(node); // note: this won't work if there are two nodes for one item (multi-parented items); will be fixed later tree._itemNodeMap[id] = node; if(this.tree._state(item)){ tree._expandNode(node); } }, 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/from dot or + icon, as appropriate this._setExpando(false); } // On initial tree show, make the selected TreeNode as either the root node of the tree, // or the first child, if the root node is hidden if(this == tree.rootNode){ var fc = this.tree.showRoot ? this : this.getChildren()[0]; if(fc){ fc.setSelected(true); tree.lastFocused = fc; }else{ // fallback: no nodes in tree so focus on Tree
itself tree.domNode.setAttribute("tabIndex", "0"); } } }, removeChild: function(/* treeNode */ node){ this.inherited(arguments); 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() // TODO: hmm this isn't called from anywhere, maybe should remove it for 2.0 this.isExpandable = true; this._setExpando(false); }, _onLabelFocus: function(evt){ // summary: // Called when this node is focused (possibly programatically) // tags: // private dojo.addClass(this.labelNode, "dijitTreeLabelFocused"); this.tree._onNodeFocus(this); }, _onLabelBlur: function(evt){ // summary: // Called when focus was moved away from this node, either to // another TreeNode or away from the Tree entirely. // Note that we aren't using _onFocus/_onBlur builtin to dijit // because _onBlur() isn't called when focus is moved to my child TreeNode. // tags: // private dojo.removeClass(this.labelNode, "dijitTreeLabelFocused"); }, setSelected: function(/*Boolean*/ selected){ // summary: // A Tree has a (single) currently selected node. // Mark that this node is/isn't that currently selected node. // description: // In particular, setting a node as selected involves setting tabIndex // so that when user tabs to the tree, focus will go to that node (only). var labelNode = this.labelNode; labelNode.setAttribute("tabIndex", selected ? "0" : "-1"); dijit.setWaiState(labelNode, "selected", selected); dojo.toggleClass(this.rowNode, "dijitTreeNodeSelected", selected); }, _onMouseEnter: function(evt){ // summary: // Handler for onmouseenter event on a node // tags: // private dojo.addClass(this.rowNode, "dijitTreeNodeHover"); this.tree._onNodeMouseEnter(this, evt); }, _onMouseLeave: function(evt){ // summary: // Handler for onmouseenter event on a node // tags: // private dojo.removeClass(this.rowNode, "dijitTreeNodeHover"); this.tree._onNodeMouseLeave(this, evt); } }); dojo.declare( "dijit.Tree", [dijit._Widget, dijit._Templated], { // summary: // This widget displays hierarchical data from a store. // store: [deprecated] String||dojo.data.Store // Deprecated. Use "model" parameter instead. // The store to get data to display in the tree. store: null, // model: dijit.Tree.model // Interface to read tree data, get notifications of changes to tree data, // and for handling drop operations (i.e drag and drop onto the tree) model: null, // query: [deprecated] anything // Deprecated. User should specify query to the model directly instead. // Specifies datastore query to return the root item or top items for the tree. query: null, // label: [deprecated] String // Deprecated. Use dijit.tree.ForestStoreModel directly instead. // Used in conjunction with query parameter. // If a query is specified (rather than a root node id), and a label is also specified, // then a fake root node is created and displayed, with this label. label: "", // showRoot: [const] Boolean // Should the root node be displayed, or hidden? showRoot: true, // childrenAttr: [deprecated] String[] // Deprecated. This information should be specified in the model. // One ore more attributes that holds children of a tree node childrenAttr: ["children"], // openOnClick: Boolean // If true, clicking a folder node's label will open it, rather than calling onClick() openOnClick: false, // openOnDblClick: Boolean // If true, double-clicking a folder node's label will open it, rather than calling onDblClick() openOnDblClick: false, templatePath: dojo.moduleUrl("dijit", "templates/Tree.html"), // isExpandable: [private deprecated] Boolean // TODO: this appears to be vestigal, back from when Tree extended TreeNode. Remove. isExpandable: true, // isTree: [private deprecated] Boolean // TODO: this appears to be vestigal. Remove. isTree: true, // persist: Boolean // Enables/disables use of cookies for state saving. persist: true, // dndController: [protected] String // Class name to use as as the dnd controller. Specifying this class enables DnD. // Generally you should specify this as "dijit._tree.dndSource". dndController: null, // parameters to pull off of the tree and pass on to the dndController as its params dndParams: ["onDndDrop","itemCreator","onDndCancel","checkAcceptance", "checkItemAcceptance", "dragThreshold", "betweenThreshold"], //declare the above items so they can be pulled from the tree's markup // onDndDrop: [protected] Function // Parameter to dndController, see `dijit._tree.dndSource.onDndDrop`. // Generally this doesn't need to be set. onDndDrop: null, // itemCreator: [protected] Function // Parameter to dndController, see `dijit._tree.dndSource.itemCreator`. // Generally this doesn't need to be set. itemCreator: null, // onDndCancel: [protected] Function // Parameter to dndController, see `dijit._tree.dndSource.onDndCancel`. // Generally this doesn't need to be set. onDndCancel: null, /*===== checkAcceptance: function(source, nodes){ // summary: // Checks if the Tree itself can accept nodes from this source // source: dijit.tree._dndSource // The source which provides items // nodes: DOMNode[] // Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if // source is a dijit.Tree. // tags: // extension return true; // Boolean }, =====*/ checkAcceptance: null, /*===== checkItemAcceptance: function(target, source, position){ // summary: // Stub function to be overridden if one wants to check for the ability to drop at the node/item level // description: // In the base case, this is called to check if target can become a child of source. // When betweenThreshold is set, position="before" or "after" means that we // are asking if the source node can be dropped before/after the target node. // target: DOMNode // The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to // Use dijit.getEnclosingWidget(target) to get the TreeNode. // source: dijit._tree.dndSource // The (set of) nodes we are dropping // position: String // "over", "before", or "after" // tags: // extension return true; // Boolean }, =====*/ checkItemAcceptance: null, // dragThreshold: Integer // Number of pixels mouse moves before it's considered the start of a drag operation dragThreshold: 0, // betweenThreshold: Integer // Set to a positive value to allow drag and drop "between" nodes. // // If during DnD mouse is over a (target) node but less than betweenThreshold // pixels from the bottom edge, dropping the the dragged node will make it // the next sibling of the target node, rather than the child. // // Similarly, if mouse is over a target node but less that betweenThreshold // pixels from the top edge, dropping the dragged node will make it // the target node's previous sibling rather than the target node's child. betweenThreshold: 0, _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._itemNodeMap={}; if(!this.cookieName){ this.cookieName = this.id + "SaveStateCookie"; } // TODO: this.inherited(arguments) }, postCreate: function(){ this._initState(); // Create glue between store and Tree, if not specified directly by user if(!this.model){ this._store2model(); } // monitor changes to items this.connect(this.model, "onChange", "_onItemChange"); this.connect(this.model, "onChildrenChange", "_onItemChildrenChange"); this.connect(this.model, "onDelete", "_onItemDelete"); this._load(); this.inherited(arguments); if(this.dndController){ if(dojo.isString(this.dndController)){ this.dndController = dojo.getObject(this.dndController); } var params={}; for (var i=0; i