diff options
Diffstat (limited to 'includes/js/dijit/Tree.js')
-rw-r--r-- | includes/js/dijit/Tree.js | 1336 |
1 files changed, 1336 insertions, 0 deletions
diff --git a/includes/js/dijit/Tree.js b/includes/js/dijit/Tree.js new file mode 100644 index 0000000..fc9be8b --- /dev/null +++ b/includes/js/dijit/Tree.js @@ -0,0 +1,1336 @@ +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:"<div class=\"dijitTreeNode\" waiRole=\"presentation\"\n\t><div dojoAttachPoint=\"rowNode\" waiRole=\"presentation\"\n\t\t><span dojoAttachPoint=\"expandoNode\" class=\"dijitTreeExpando\" waiRole=\"presentation\"\n\t\t></span\n\t\t><span dojoAttachPoint=\"expandoNodeText\" class=\"dijitExpandoText\" waiRole=\"presentation\"\n\t\t></span\n\t\t><div dojoAttachPoint=\"contentNode\" class=\"dijitTreeContent\" waiRole=\"presentation\">\n\t\t\t<div dojoAttachPoint=\"iconNode\" class=\"dijitInline dijitTreeIcon\" waiRole=\"presentation\"></div>\n\t\t\t<span dojoAttachPoint=\"labelNode\" class=\"dijitTreeLabel\" wairole=\"treeitem\" tabindex=\"-1\" waiState=\"selected-false\" dojoAttachEvent=\"onfocus:_onNodeFocus\"></span>\n\t\t</div\n\t></div>\n</div>\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 icon and label dom node (used to allow for item updates to change respective CSS) + 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; + } + this.iconNode.className = "dijitInline dijitTreeIcon " + tree.getIconClass(item, this.isExpanded); + this.labelNode.className = "dijitTreeLabel " + tree.getLabelClass(item, this.isExpanded); + }, + + _updateLayout: function(){ + // summary: set appropriate CSS classes for this.domNode + 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 + + // 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.contentNode.className = "dijitTreeContent dijitTreeContentExpanded"; + this._setExpando(); + this._updateItemClasses(this.item); + + 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.contentNode.className = "dijitTreeContent"; + this._setExpando(); + this._updateItemClasses(this.item); + + this._wipeOut.play(); + }, + + setLabelNode: function(label){ + this.labelNode.innerHTML=""; + this.labelNode.appendChild(dojo.doc.createTextNode(label)); + }, + + 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; + 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, 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 : + new dijit._TreeNode({ + item: item, + tree: tree, + isExpandable: model.mayHaveChildren(item), + label: tree.getLabel(item) + }); + 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.persist){ + if(tree._openedItemIds[id]){ + 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, put focus on either the root node of the tree, + // or the first child, if the root node is hidden + if(!this.parent){ + var fc = this.tree.showRoot ? this : this.getChildren()[0], + 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}); + } + }, + + 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() + this.isExpandable = true; + this._setExpando(false); + }, + + _onNodeFocus: function(evt){ + var node = dijit.getEnclosingWidget(evt.target); + this.tree._onTreeFocus(node); + } +}); + +dojo.declare( + "dijit.Tree", + [dijit._Widget, dijit._Templated], +{ + // 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. + // May remove for 2.0 in favor of "model". + store: null, + + // model: dijit.Tree.model + // Alternate interface from store to access data (and changes to data) in the tree + model: null, + + // query: anything + // Specifies datastore query to return the root item for the tree. + // + // Deprecated functionality: if the query returns multiple items, the tree is given + // a fake root node (not corresponding to any item in the data store), + // whose children are the items that match this query. + // + // The root node is shown or hidden based on whether a label is specified. + // + // Having a query return multiple items is deprecated. + // If your store doesn't have a root item, wrap the store with + // dijit.tree.ForestStoreModel, and specify model=myModel + // + // example: + // {type:'continent'} + query: null, + + // label: 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: Boolean + // Should the root node be displayed, or hidden? + showRoot: true, + + // childrenAttr: String[] + // 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, + + templateString:"<div class=\"dijitTreeContainer\" waiRole=\"tree\"\n\tdojoAttachEvent=\"onclick:_onClick,onkeypress:_onKeyPress\">\n</div>\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._itemNodeMap={}; + + if(!this.cookieName){ + this.cookieName = this.id + "SaveStateCookie"; + } + }, + + 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 = dojo.doc.createElement('div'); + div.style.display = 'none'; + div.className = "dijitTreeContainer"; + dijit.setWaiRole(div, "presentation"); + this.containerNodeTemplate = div; + + // 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"); + // TODO: monitor item deletes so we don't end up w/orphaned nodes? + + this._load(); + + this.inherited("postCreate", arguments); + + 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); + } + }, + + _store2model: function(){ + // summary: user specified a store&query rather than model, so create model from store/query + this._v10Compat = true; + dojo.deprecated("Tree: from version 2.0, should specify a model object rather than a store/query"); + + var modelParams = { + id: this.id + "_ForestStoreModel", + store: this.store, + query: this.query, + childrenAttrs: this.childrenAttr + }; + + // Only override the model's mayHaveChildren() method if the user has specified an override + if(this.params.mayHaveChildren){ + modelParams.mayHaveChildren = dojo.hitch(this, "mayHaveChildren"); + } + + if(this.params.getItemChildren){ + modelParams.getChildren = dojo.hitch(this, function(item, onComplete, onError){ + this.getItemChildren((this._v10Compat && item === this.model.root) ? null : item, onComplete, onError); + }); + } + this.model = new dijit.tree.ForestStoreModel(modelParams); + + // For backwards compatibility, the visibility of the root node is controlled by + // whether or not the user has specified a label + this.showRoot = Boolean(this.label); + }, + + _load: function(){ + // summary: initial load of the tree + // load root node (possibly hidden) and it's children + this.model.getRoot( + dojo.hitch(this, function(item){ + var rn = this.rootNode = new dijit._TreeNode({ + item: item, + tree: this, + isExpandable: true, + label: this.label || this.getLabel(item) + }); + if(!this.showRoot){ + rn.rowNode.style.display="none"; + } + this.domNode.appendChild(rn.domNode); + this._itemNodeMap[this.model.getIdentity(item)] = rn; + + rn._updateLayout(); // sets "dijitTreeIsRoot" CSS classname + + // load top level children + this._expandNode(rn); + }), + function(err){ + console.error(this, ": error loading root: ", err); + } + ); + }, + + ////////////// Data store related functions ////////////////////// + // These just get passed to the model; they are here for back-compat + + 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 actually + // has children until user clicks the expando node) + }, + + 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 + }, + + /////////////////////////////////////////////////////// + // Functions for converting an item to a TreeNode + getLabel: function(/*dojo.data.Item*/ item){ + // summary: user overridable function to get the label for a tree node (given the item) + return this.model.getLabel(item); // String + }, + + getIconClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){ + // summary: user overridable function to return CSS class name to display icon + return (!item || this.model.mayHaveChildren(item)) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "dijitLeaf" + }, + + getLabelClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){ + // summary: user overridable function to return CSS class name to display label + }, + + /////////// 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[this.isLeftToRight() ? dojo.keys.LEFT_ARROW : dojo.keys.RIGHT_ARROW]="_onLeftArrow"; + map[this.isLeftToRight() ? dojo.keys.RIGHT_ARROW : dojo.keys.LEFT_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 node = this._getNextNode(message.node); + if(node && node.isTreeNode){ + this.focusNode(node); + } + }, + + _onUpArrow: function(/*Object*/ message){ + // summary: up arrow pressed; move to previous visible node + + var node = message.node; + + // if younger siblings + var previousSibling = node.getPreviousSibling(); + if(previousSibling){ + node = previousSibling; + // if the previous node is expanded, dive in deep + while(node.isExpandable && node.isExpanded && node.hasChildren()){ + // move to the last child + var children = node.getChildren(); + node = 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 = node.getParent(); + if(!(!this.showRoot && parent === this.rootNode)){ + node = parent; + } + } + + if(node && node.isTreeNode){ + this.focusNode(node); + } + }, + + _onRightArrow: function(/*Object*/ message){ + // summary: right arrow pressed; go to child node + var node = message.node; + + // if not expanded, expand, else move to 1st child + if(node.isExpandable && !node.isExpanded){ + this._expandNode(node); + }else if(node.hasChildren()){ + node = node.getChildren()[0]; + if(node && node.isTreeNode){ + this.focusNode(node); + } + } + }, + + _onLeftArrow: function(/*Object*/ message){ + // summary: + // Left arrow pressed. + // If not collapsed, collapse, else move to parent. + + var node = message.node; + + if(node.isExpandable && node.isExpanded){ + this._collapseNode(node); + }else{ + node = node.getParent(); + if(node && node.isTreeNode){ + this.focusNode(node); + } + } + }, + + _onHomeKey: function(){ + // summary: home pressed; get first visible node, set focus there + var node = this._getRootOrFirstNode(); + if(node){ + this.focusNode(node); + } + }, + + _onEndKey: function(/*Object*/ message){ + // summary: end pressed; go to last visible node + + var node = this; + while(node.isExpanded){ + var c = node.getChildren(); + node = c[c.length - 1]; + } + + if(node && node.isTreeNode){ + this.focusNode(node); + } + }, + + _onLetterKeyNav: function(message){ + // summary: letter key pressed; search for node starting with first char = key + var node = startNode = message.node, + key = message.key; + do{ + node = this._getNextNode(node); + //check for last node, jump to first node if necessary + if(!node){ + node = this._getRootOrFirstNode(); + } + }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){ + this.focusNode(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( (this.openOnClick && nodeWidget.isExpandable) || + (domElement == nodeWidget.expandoNode || domElement == nodeWidget.expandoNodeText) ){ + // expando node was clicked, or label of a folder node was clicked; open it + 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 we are collapsing, we might be hiding the currently focused node. + // Also, clicking the expando node might have erased focus from the current node. + // For simplicity's sake just focus on the node with the expando. + this.focusNode(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 + }, + + _getNextNode: function(node){ + // summary: get next visible node + + if(node.isExpandable && node.isExpanded && node.hasChildren()){ + // if this is an expanded node, get the first child + return node.getChildren()[0]; // _TreeNode + }else{ + // find a parent node with a sibling + while(node && node.isTreeNode){ + var returnNode = node.getNextSibling(); + if(returnNode){ + return returnNode; // _TreeNode + } + node = node.getParent(); + } + return null; + } + }, + + _getRootOrFirstNode: function(){ + // summary: get first visible node + return this.showRoot ? this.rootNode : this.rootNode.getChildren()[0]; + }, + + _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; + } + + node.collapse(); + if(this.persist && node.item){ + delete this._openedItemIds[this.model.getIdentity(node.item)]; + this._saveState(); + } + } + }, + + _expandNode: function(/*_TreeNode*/ node){ + // summary: called when the user has requested to expand the node + + if(!node.isExpandable){ + return; + } + + var model = this.model, + item = node.item; + + 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; + model.getChildren(item, function(items){ + node.unmarkProcessing(); + node.setChildItems(items); + _this._expandNode(node); + }, + function(err){ + console.error(_this, ": error loading root children: ", err); + }); + break; + + default: + // data is already loaded; just proceed + node.expand(); + if(this.persist && item){ + this._openedItemIds[model.getIdentity(item)] = true; + this._saveState(); + } + } + }, + + ////////////////// 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"); + dijit.setWaiState(labelNode, "selected", false); + 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 + + this.inherited(arguments); + if(this.lastFocused){ + var labelNode = this.lastFocused.labelNode; + dojo.removeClass(labelNode, "dijitTreeLabelFocused"); + } + }, + + _onTreeFocus: function(/*Widget*/ node){ + // summary: + // called from onFocus handler of treeitem labelNode to set styles, wai state and tabindex + // for currently focused treeitem. + + if (node){ + 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"); + dijit.setWaiState(labelNode, "selected", true); + dojo.addClass(labelNode, "dijitTreeLabelFocused"); + this.lastFocused = node; + } + }, + + //////////////// Events from the model ////////////////////////// + + _onItemDelete: function(/*Object*/ item){ + //summary: delete event from the store + // TODO: currently this isn't called, and technically doesn't need to be, + // but it would help with garbage collection + + var identity = this.model.getIdentity(item); + var node = this._itemNodeMap[identity]; + + if(node){ + var parent = node.getParent(); + if(parent){ + // if node has not already been orphaned from a _onSetItem(parent, "children", ..) call... + parent.removeChild(node); + } + delete this._itemNodeMap[identity]; + node.destroyRecursive(); + } + }, + + _onItemChange: function(/*Item*/ item){ + //summary: set data event on an item in the store + var model = this.model, + identity = model.getIdentity(item), + node = this._itemNodeMap[identity]; + + if(node){ + node.setLabelNode(this.getLabel(item)); + node._updateItemClasses(item); + } + }, + + _onItemChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){ + //summary: set data event on an item in the store + var model = this.model, + identity = model.getIdentity(parent), + parentNode = this._itemNodeMap[identity]; + + if(parentNode){ + parentNode.setChildItems(newChildrenList); + } + }, + + /////////////// Miscellaneous funcs + + _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(",")); + }, + + destroy: function(){ + if(this.rootNode){ + this.rootNode.destroyRecursive(); + } + this.rootNode = null; + this.inherited(arguments); + }, + + destroyRecursive: function(){ + // A tree is treated as a leaf, not as a node with children (like a grid), + // but defining destroyRecursive for back-compat. + this.destroy(); + } +}); + + +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 ore more attributes that holds children of a tree node + childrenAttrs: ["children"], + + // root: 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, + + constructor: function(/* Object */ args){ + // summary: passed the arguments listed above (store, etc) + 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); + }, + + // ======================================================================= + // 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 " + 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; + + // 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; + 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 + var onItem = 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, + onError: onError + }); + } + }); + } + }, + + // ======================================================================= + // Inspecting items + + getIdentity: function(/* item */ item){ + return this.store.getIdentity(item); // Object + }, + + getLabel: function(/*dojo.data.Item*/ item){ + // summary: get the label for an item + return this.store.getLabel(item); // String + }, + + // ======================================================================= + // Write interface + + newItem: function(/* Object? */ args, /*Item*/ parent){ + // 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. + var pInfo = {parent: parent, attribute: this.childrenAttrs[0]}; + return this.store.newItem(args, pInfo); + }, + + pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy){ + // 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 attributee 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){ + 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. + }, + + onChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){ + // summary + // Callback to do notifications about new, updated, or deleted items. + }, + + // ======================================================================= + ///Events from data store + + _onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){ + // summary: handler for when new items appear in the store. + + // In this case there's no correspond onSet() call on the parent of this + // item, so need to get the new children list of the parent manually somehow. + if(!parentInfo){ + return; + } + this.getChildren(parentInfo.item, dojo.hitch(this, function(children){ + // NOTE: maybe can be optimized since parentInfo contains the new and old attribute value + this.onChildrenChange(parentInfo.item, children); + })); + }, + + _onDeleteItem: function(/*Object*/ item){ + // summary: handler for delete notifications from underlying store + }, + + _onSetItem: function(/* item */ item, + /* attribute-name-string */ attribute, + /* object | array */ oldValue, + /* object | array */ newValue){ + //summary: set data event on an item in the store + + if(dojo.indexOf(this.childrenAttrs, attribute) != -1){ + // item's children list changed + this.getChildren(item, dojo.hitch(this, function(children){ + // NOTE: maybe can be optimized since parentInfo contains the new and old attribute value + this.onChildrenChange(item, children); + })); + }else{ + // item's label/icon/etc. changed. + this.onChange(item); + } + } +}); + +dojo.declare("dijit.tree.ForestStoreModel", dijit.tree.TreeStoreModel, { + // summary + // Interface between Tree and a dojo.store that doesn't have a root item, ie, + // has multiple "top level" items. + // + // description + // Use this class to wrap a dojo.store, making all the items matching the specified query + // appear as children of a fabricated "root item". If no query is specified then all the + // items returned by fetch() on the underlying store become children of the root item. + // It allows dijit.Tree to assume a single root item, even if the store doesn't have one. + + // Parameters to constructor + + // rootId: String + // ID of fabricated root item + rootId: "$root$", + + // rootLabel: String + // Label of fabricated root item + rootLabel: "ROOT", + + // query: String + // Specifies the set of children of the root item. + // example: + // {type:'continent'} + query: null, + + // End of parameters to constructor + + constructor: function(params){ + // Make dummy root item + this.root = { + store: this, + root: true, + id: params.rootId, + label: params.rootLabel, + children: params.rootChildren // optional param + }; + }, + + // ======================================================================= + // Methods for traversing hierarchy + + 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 item === this.root || this.inherited(arguments); + }, + + getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ callback, /*function*/ onError){ + // summary + // Calls onComplete() with array of child items of given parent item, all loaded. + if(parentItem === this.root){ + if(this.root.children){ + // already loaded, just return + callback(this.root.children); + }else{ + this.store.fetch({ + query: this.query, + onComplete: dojo.hitch(this, function(items){ + this.root.children = items; + callback(items); + }), + onError: onError + }); + } + }else{ + this.inherited(arguments); + } + }, + + // ======================================================================= + // Inspecting items + + getIdentity: function(/* item */ item){ + return (item === this.root) ? this.root.id : this.inherited(arguments); + }, + + getLabel: function(/* item */ item){ + return (item === this.root) ? this.root.label : this.inherited(arguments); + }, + + // ======================================================================= + // Write interface + + newItem: function(/* Object? */ args, /*Item*/ parent){ + // 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. + if(parent===this.root){ + this.onNewRootItem(args); + return this.store.newItem(args); + }else{ + return this.inherited(arguments); + } + }, + + onNewRootItem: function(args){ + // summary: + // User can override this method to modify a new element that's being + // added to the root of the tree, for example to add a flag like root=true + }, + + pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy){ + // summary + // Move or copy an item from one parent item to another. + // Used in drag & drop + if(oldParentItem === this.root){ + if(!bCopy){ + // It's onLeaveRoot()'s responsibility to modify the item so it no longer matches + // this.query... thus triggering an onChildrenChange() event to notify the Tree + // that this element is no longer a child of the root node + this.onLeaveRoot(childItem); + } + } + dijit.tree.TreeStoreModel.prototype.pasteItem.call(this, childItem, + oldParentItem === this.root ? null : oldParentItem, + newParentItem === this.root ? null : newParentItem + ); + if(newParentItem === this.root){ + // It's onAddToRoot()'s responsibility to modify the item so it matches + // this.query... thus triggering an onChildrenChange() event to notify the Tree + // that this element is now a child of the root node + this.onAddToRoot(childItem); + } + }, + + // ======================================================================= + // Callbacks + + onAddToRoot: function(/* item */ item){ + // summary + // Called when item added to root of tree; user must override + // to modify the item so that it matches the query for top level items + // example + // | store.setValue(item, "root", true); + console.log(this, ": item ", item, " added to root"); + }, + + onLeaveRoot: function(/* item */ item){ + // summary + // Called when item removed from root of tree; user must override + // to modify the item so it doesn't match the query for top level items + // example + // | store.unsetAttribute(item, "root"); + console.log(this, ": item ", item, " removed from root"); + }, + + // ======================================================================= + // Events from data store + + _requeryTop: function(){ + // reruns the query for the children of the root node, + // sending out an onSet notification if those children have changed + var _this = this, + oldChildren = this.root.children; + this.store.fetch({ + query: this.query, + onComplete: function(newChildren){ + _this.root.children = newChildren; + + // If the list of children or the order of children has changed... + if(oldChildren.length != newChildren.length || + dojo.some(oldChildren, function(item, idx){ return newChildren[idx] != item;})){ + _this.onChildrenChange(_this.root, newChildren); + } + } + }); + }, + + _onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){ + // summary: handler for when new items appear in the store. + + // In theory, any new item could be a top level item. + // Do the safe but inefficient thing by requerying the top + // level items. User can override this function to do something + // more efficient. + this._requeryTop(); + + this.inherited(arguments); + }, + + _onDeleteItem: function(/*Object*/ item){ + // summary: handler for delete notifications from underlying store + + // check if this was a child of root, and if so send notification that root's children + // have changed + if(dojo.indexOf(this.root.children, item) != -1){ + this._requeryTop(); + } + + this.inherited(arguments); + } +}); + +} |