]> git.pond.sub.org Git - eow/blob - static/dojo-release-1.1.1/dijit/Tree.js
Make prompt stand out some more
[eow] / static / dojo-release-1.1.1 / dijit / Tree.js
1 if(!dojo._hasResource["dijit.Tree"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2 dojo._hasResource["dijit.Tree"] = true;
3 dojo.provide("dijit.Tree");
4
5 dojo.require("dojo.fx");
6
7 dojo.require("dijit._Widget");
8 dojo.require("dijit._Templated");
9 dojo.require("dijit._Container");
10 dojo.require("dojo.cookie");
11
12 dojo.declare(
13         "dijit._TreeNode",
14         [dijit._Widget, dijit._Templated, dijit._Container, dijit._Contained],
15 {
16         // summary
17         //              Single node within a tree
18
19         // item: dojo.data.Item
20         //              the dojo.data entry this tree represents
21         item: null,     
22
23         isTreeNode: true,
24
25         // label: String
26         //              Text of this tree node
27         label: "",
28         
29         isExpandable: null, // show expando node
30         
31         isExpanded: false,
32
33         // state: String
34         //              dynamic loading-related stuff.
35         //              When an empty folder node appears, it is "UNCHECKED" first,
36         //              then after dojo.data query it becomes "LOADING" and, finally "LOADED"   
37         state: "UNCHECKED",
38         
39         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",          
40
41         postCreate: function(){
42                 // set label, escaping special characters
43                 this.setLabelNode(this.label);
44
45                 // set expand icon for leaf
46                 this._setExpando();
47
48                 // set icon and label class based on item
49                 this._updateItemClasses(this.item);
50
51                 if(this.isExpandable){
52                         dijit.setWaiState(this.labelNode, "expanded", this.isExpanded);
53                 }
54         },
55
56         markProcessing: function(){
57                 // summary: visually denote that tree is loading data, etc.
58                 this.state = "LOADING";
59                 this._setExpando(true); 
60         },
61
62         unmarkProcessing: function(){
63                 // summary: clear markup from markProcessing() call
64                 this._setExpando(false);        
65         },
66
67         _updateItemClasses: function(item){
68                 // summary: set appropriate CSS classes for icon and label dom node (used to allow for item updates to change respective CSS)
69                 var tree = this.tree, model = tree.model;
70                 if(tree._v10Compat && item === model.root){
71                         // For back-compat with 1.0, need to use null to specify root item (TODO: remove in 2.0)
72                         item = null;
73                 }
74                 this.iconNode.className = "dijitInline dijitTreeIcon " + tree.getIconClass(item, this.isExpanded);
75                 this.labelNode.className = "dijitTreeLabel " + tree.getLabelClass(item, this.isExpanded);
76         },
77
78         _updateLayout: function(){
79                 // summary: set appropriate CSS classes for this.domNode
80                 var parent = this.getParent();
81                 if(!parent || parent.rowNode.style.display == "none"){
82                         /* if we are hiding the root node then make every first level child look like a root node */
83                         dojo.addClass(this.domNode, "dijitTreeIsRoot");
84                 }else{
85                         dojo.toggleClass(this.domNode, "dijitTreeIsLast", !this.getNextSibling());
86                 }
87         },
88
89         _setExpando: function(/*Boolean*/ processing){
90                 // summary: set the right image for the expando node
91
92                 // apply the appropriate class to the expando node
93                 var styles = ["dijitTreeExpandoLoading", "dijitTreeExpandoOpened",
94                         "dijitTreeExpandoClosed", "dijitTreeExpandoLeaf"];
95                 var idx = processing ? 0 : (this.isExpandable ? (this.isExpanded ? 1 : 2) : 3);
96                 dojo.forEach(styles,
97                         function(s){
98                                 dojo.removeClass(this.expandoNode, s);
99                         }, this
100                 );
101                 dojo.addClass(this.expandoNode, styles[idx]);
102
103                 // provide a non-image based indicator for images-off mode
104                 this.expandoNodeText.innerHTML =
105                         processing ? "*" :
106                                 (this.isExpandable ?
107                                         (this.isExpanded ? "-" : "+") : "*");
108         },      
109
110         expand: function(){
111                 // summary: show my children
112                 if(this.isExpanded){ return; }
113                 // cancel in progress collapse operation
114                 if(this._wipeOut.status() == "playing"){
115                         this._wipeOut.stop();
116                 }
117
118                 this.isExpanded = true;
119                 dijit.setWaiState(this.labelNode, "expanded", "true");
120                 dijit.setWaiRole(this.containerNode, "group");
121                 this.contentNode.className = "dijitTreeContent dijitTreeContentExpanded";
122                 this._setExpando();
123                 this._updateItemClasses(this.item);
124
125                 this._wipeIn.play();
126         },
127
128         collapse: function(){                                   
129                 if(!this.isExpanded){ return; }
130
131                 // cancel in progress expand operation
132                 if(this._wipeIn.status() == "playing"){
133                         this._wipeIn.stop();
134                 }
135
136                 this.isExpanded = false;
137                 dijit.setWaiState(this.labelNode, "expanded", "false");
138                 this.contentNode.className = "dijitTreeContent";
139                 this._setExpando();
140                 this._updateItemClasses(this.item);
141
142                 this._wipeOut.play();
143         },
144
145         setLabelNode: function(label){
146                 this.labelNode.innerHTML="";
147                 this.labelNode.appendChild(dojo.doc.createTextNode(label));
148         },
149
150         setChildItems: function(/* Object[] */ items){
151                 // summary:
152                 //              Sets the child items of this node, removing/adding nodes
153                 //              from current children to match specified items[] array.
154
155                 var tree = this.tree,
156                         model = tree.model;
157
158                 // Orphan all my existing children.
159                 // If items contains some of the same items as before then we will reattach them.
160                 // Don't call this.removeChild() because that will collapse the tree etc.
161                 this.getChildren().forEach(function(child){
162                         dijit._Container.prototype.removeChild.call(this, child);
163                 }, this);
164
165                 this.state = "LOADED";
166
167                 if(items && items.length > 0){
168                         this.isExpandable = true;
169                         if(!this.containerNode){ // maybe this node was unfolderized and still has container
170                                 this.containerNode = this.tree.containerNodeTemplate.cloneNode(true);
171                                 this.domNode.appendChild(this.containerNode);
172                         }
173
174                         // Create _TreeNode widget for each specified tree node, unless one already
175                         // exists and isn't being used (presumably it's from a DnD move and was recently
176                         // released
177                         dojo.forEach(items, function(item){
178                                 var id = model.getIdentity(item),
179                                         existingNode = tree._itemNodeMap[id],
180                                         node = 
181                                                 ( existingNode && !existingNode.getParent() ) ?
182                                                 existingNode :
183                                                 new dijit._TreeNode({
184                                                         item: item,
185                                                         tree: tree,
186                                                         isExpandable: model.mayHaveChildren(item),
187                                                         label: tree.getLabel(item)
188                                                 });
189                                 this.addChild(node);
190                                 // note: this won't work if there are two nodes for one item (multi-parented items); will be fixed later
191                                 tree._itemNodeMap[id] = node;
192                                 if(this.tree.persist){
193                                         if(tree._openedItemIds[id]){
194                                                 tree._expandNode(node);
195                                         }
196                                 }
197                         }, this);
198
199                         // note that updateLayout() needs to be called on each child after
200                         // _all_ the children exist
201                         dojo.forEach(this.getChildren(), function(child, idx){
202                                 child._updateLayout();
203                         });
204                 }else{
205                         this.isExpandable=false;
206                 }
207
208                 if(this._setExpando){
209                         // change expando to/from dot or + icon, as appropriate
210                         this._setExpando(false);
211                 }
212
213                 // On initial tree show, put focus on either the root node of the tree,
214                 // or the first child, if the root node is hidden
215                 if(!this.parent){
216                         var fc = this.tree.showRoot ? this : this.getChildren()[0],
217                                 tabnode = fc ? fc.labelNode : this.domNode;
218                         tabnode.setAttribute("tabIndex", "0");
219                 }
220
221                 // create animations for showing/hiding the children (if children exist)
222                 if(this.containerNode && !this._wipeIn){
223                         this._wipeIn = dojo.fx.wipeIn({node: this.containerNode, duration: 150});
224                         this._wipeOut = dojo.fx.wipeOut({node: this.containerNode, duration: 150});
225                 }
226         },
227
228         removeChild: function(/* treeNode */ node){
229                 this.inherited(arguments);
230
231                 var children = this.getChildren();              
232                 if(children.length == 0){
233                         this.isExpandable = false;
234                         this.collapse();
235                 }
236
237                 dojo.forEach(children, function(child){
238                                 child._updateLayout();
239                 });
240         },
241
242         makeExpandable: function(){
243                 //summary
244                 //              if this node wasn't already showing the expando node,
245                 //              turn it into one and call _setExpando()
246                 this.isExpandable = true;
247                 this._setExpando(false);
248         },
249
250         _onNodeFocus: function(evt){
251                 var node = dijit.getEnclosingWidget(evt.target);
252                 this.tree._onTreeFocus(node);
253         }
254 });
255
256 dojo.declare(
257         "dijit.Tree",
258         [dijit._Widget, dijit._Templated],
259 {
260         // summary
261         //      This widget displays hierarchical data from a store.  A query is specified
262         //      to get the "top level children" from a data store, and then those items are
263         //      queried for their children and so on (but lazily, as the user clicks the expand node).
264         //
265         //      Thus in the default mode of operation this widget is technically a forest, not a tree,
266         //      in that there can be multiple "top level children".  However, if you specify label,
267         //      then a special top level node (not corresponding to any item in the datastore) is
268         //      created, to father all the top level children.
269
270         // store: String||dojo.data.Store
271         //      The store to get data to display in the tree.
272         //      May remove for 2.0 in favor of "model".
273         store: null,
274
275         // model: dijit.Tree.model
276         //      Alternate interface from store to access data (and changes to data) in the tree
277         model: null,
278
279         // query: anything
280         //      Specifies datastore query to return the root item for the tree.
281         //
282         //      Deprecated functionality: if the query returns multiple items, the tree is given
283         //      a fake root node (not corresponding to any item in the data store), 
284         //      whose children are the items that match this query.
285         //
286         //      The root node is shown or hidden based on whether a label is specified.
287         //
288         //      Having a query return multiple items is deprecated.
289         //      If your store doesn't have a root item, wrap the store with
290         //      dijit.tree.ForestStoreModel, and specify model=myModel
291         //
292         // example:
293         //              {type:'continent'}
294         query: null,
295
296         // label: String
297         //      Deprecated.  Use dijit.tree.ForestStoreModel directly instead.
298         //      Used in conjunction with query parameter.
299         //      If a query is specified (rather than a root node id), and a label is also specified,
300         //      then a fake root node is created and displayed, with this label.
301         label: "",
302
303         // showRoot: Boolean
304         //      Should the root node be displayed, or hidden?
305         showRoot: true,
306
307         // childrenAttr: String[]
308         //              one ore more attributes that holds children of a tree node
309         childrenAttr: ["children"],
310
311         // openOnClick: Boolean
312         //              If true, clicking a folder node's label will open it, rather than calling onClick()
313         openOnClick: false,
314
315         templateString:"<div class=\"dijitTreeContainer\" waiRole=\"tree\"\n\tdojoAttachEvent=\"onclick:_onClick,onkeypress:_onKeyPress\">\n</div>\n",          
316
317         isExpandable: true,
318
319         isTree: true,
320
321         // persist: Boolean
322         //      enables/disables use of cookies for state saving.
323         persist: true,
324         
325         // dndController: String
326         //      class name to use as as the dnd controller
327         dndController: null,
328
329         //parameters to pull off of the tree and pass on to the dndController as its params
330         dndParams: ["onDndDrop","itemCreator","onDndCancel","checkAcceptance", "checkItemAcceptance"],
331
332         //declare the above items so they can be pulled from the tree's markup
333         onDndDrop:null,
334         itemCreator:null,
335         onDndCancel:null,
336         checkAcceptance:null,   
337         checkItemAcceptance:null,
338
339         _publish: function(/*String*/ topicName, /*Object*/ message){
340                 // summary:
341                 //              Publish a message for this widget/topic
342                 dojo.publish(this.id, [dojo.mixin({tree: this, event: topicName}, message||{})]);
343         },
344
345         postMixInProperties: function(){
346                 this.tree = this;
347
348                 this._itemNodeMap={};
349
350                 if(!this.cookieName){
351                         this.cookieName = this.id + "SaveStateCookie";
352                 }
353         },
354
355         postCreate: function(){
356                 // load in which nodes should be opened automatically
357                 if(this.persist){
358                         var cookie = dojo.cookie(this.cookieName);
359                         this._openedItemIds = {};
360                         if(cookie){
361                                 dojo.forEach(cookie.split(','), function(item){
362                                         this._openedItemIds[item] = true;
363                                 }, this);
364                         }
365                 }
366                 
367                 // make template for container node (we will clone this and insert it into
368                 // any nodes that have children)
369                 var div = dojo.doc.createElement('div');
370                 div.style.display = 'none';
371                 div.className = "dijitTreeContainer";   
372                 dijit.setWaiRole(div, "presentation");
373                 this.containerNodeTemplate = div;
374
375                 // Create glue between store and Tree, if not specified directly by user
376                 if(!this.model){
377                         this._store2model();
378                 }
379
380                 // monitor changes to items
381                 this.connect(this.model, "onChange", "_onItemChange");
382                 this.connect(this.model, "onChildrenChange", "_onItemChildrenChange");
383                 // TODO: monitor item deletes so we don't end up w/orphaned nodes?
384
385                 this._load();
386
387                 this.inherited("postCreate", arguments);
388
389                 if(this.dndController){
390                         if(dojo.isString(this.dndController)){
391                                 this.dndController= dojo.getObject(this.dndController);
392                         }       
393                         var params={};
394                         for (var i=0; i<this.dndParams.length;i++){
395                                 if(this[this.dndParams[i]]){
396                                         params[this.dndParams[i]]=this[this.dndParams[i]];
397                                 }
398                         }
399                         this.dndController= new this.dndController(this, params);
400                 }
401         },
402
403         _store2model: function(){
404                 // summary: user specified a store&query rather than model, so create model from store/query
405                 this._v10Compat = true;
406                 dojo.deprecated("Tree: from version 2.0, should specify a model object rather than a store/query");
407
408                 var modelParams = {
409                         id: this.id + "_ForestStoreModel",
410                         store: this.store,
411                         query: this.query,
412                         childrenAttrs: this.childrenAttr
413                 };
414
415                 // Only override the model's mayHaveChildren() method if the user has specified an override
416                 if(this.params.mayHaveChildren){
417                         modelParams.mayHaveChildren = dojo.hitch(this, "mayHaveChildren");
418                 }
419                                         
420                 if(this.params.getItemChildren){
421                         modelParams.getChildren = dojo.hitch(this, function(item, onComplete, onError){
422                                 this.getItemChildren((this._v10Compat && item === this.model.root) ? null : item, onComplete, onError);
423                         });
424                 }
425                 this.model = new dijit.tree.ForestStoreModel(modelParams);
426                 
427                 // For backwards compatibility, the visibility of the root node is controlled by
428                 // whether or not the user has specified a label
429                 this.showRoot = Boolean(this.label);
430         },
431
432         _load: function(){
433                 // summary: initial load of the tree
434                 // load root node (possibly hidden) and it's children
435                 this.model.getRoot(
436                         dojo.hitch(this, function(item){
437                                 var rn = this.rootNode = new dijit._TreeNode({
438                                         item: item,
439                                         tree: this,
440                                         isExpandable: true,
441                                         label: this.label || this.getLabel(item)
442                                 });
443                                 if(!this.showRoot){
444                                         rn.rowNode.style.display="none";
445                                 }
446                                 this.domNode.appendChild(rn.domNode);
447                                 this._itemNodeMap[this.model.getIdentity(item)] = rn;
448
449                                 rn._updateLayout();             // sets "dijitTreeIsRoot" CSS classname
450
451                                 // load top level children
452                                 this._expandNode(rn);
453                         }),
454                         function(err){
455                                 console.error(this, ": error loading root: ", err);
456                         }
457                 );
458         },
459
460         ////////////// Data store related functions //////////////////////
461         // These just get passed to the model; they are here for back-compat
462
463         mayHaveChildren: function(/*dojo.data.Item*/ item){
464                 // summary
465                 //              User overridable function to tell if an item has or may have children.
466                 //              Controls whether or not +/- expando icon is shown.
467                 //              (For efficiency reasons we may not want to check if an element actually
468                 //              has children until user clicks the expando node)
469         },
470
471         getItemChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete){
472                 // summary
473                 //              User overridable function that return array of child items of given parent item,
474                 //              or if parentItem==null then return top items in tree
475         },
476
477         ///////////////////////////////////////////////////////
478         // Functions for converting an item to a TreeNode
479         getLabel: function(/*dojo.data.Item*/ item){
480                 // summary: user overridable function to get the label for a tree node (given the item)
481                 return this.model.getLabel(item);       // String
482         },
483
484         getIconClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){
485                 // summary: user overridable function to return CSS class name to display icon
486                 return (!item || this.model.mayHaveChildren(item)) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "dijitLeaf"
487         },
488
489         getLabelClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){
490                 // summary: user overridable function to return CSS class name to display label
491         },
492
493         /////////// Keyboard and Mouse handlers ////////////////////
494
495         _onKeyPress: function(/*Event*/ e){
496                 // summary: translates keypress events into commands for the controller
497                 if(e.altKey){ return; }
498                 var treeNode = dijit.getEnclosingWidget(e.target);
499                 if(!treeNode){ return; }
500
501                 // Note: On IE e.keyCode is not 0 for printables so check e.charCode.
502                 // In dojo charCode is universally 0 for non-printables.
503                 if(e.charCode){  // handle printables (letter navigation)
504                         // Check for key navigation.
505                         var navKey = e.charCode;
506                         if(!e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey){
507                                 navKey = (String.fromCharCode(navKey)).toLowerCase();
508                                 this._onLetterKeyNav( { node: treeNode, key: navKey } );
509                                 dojo.stopEvent(e);
510                         }
511                 }else{  // handle non-printables (arrow keys)
512                         var map = this._keyHandlerMap;
513                         if(!map){
514                                 // setup table mapping keys to events
515                                 map = {};
516                                 map[dojo.keys.ENTER]="_onEnterKey";
517                                 map[this.isLeftToRight() ? dojo.keys.LEFT_ARROW : dojo.keys.RIGHT_ARROW]="_onLeftArrow";
518                                 map[this.isLeftToRight() ? dojo.keys.RIGHT_ARROW : dojo.keys.LEFT_ARROW]="_onRightArrow";
519                                 map[dojo.keys.UP_ARROW]="_onUpArrow";
520                                 map[dojo.keys.DOWN_ARROW]="_onDownArrow";
521                                 map[dojo.keys.HOME]="_onHomeKey";
522                                 map[dojo.keys.END]="_onEndKey";
523                                 this._keyHandlerMap = map;
524                         }
525                         if(this._keyHandlerMap[e.keyCode]){
526                                 this[this._keyHandlerMap[e.keyCode]]( { node: treeNode, item: treeNode.item } );        
527                                 dojo.stopEvent(e);
528                         }
529                 }
530         },
531
532         _onEnterKey: function(/*Object*/ message){
533                 this._publish("execute", { item: message.item, node: message.node} );
534                 this.onClick(message.item, message.node);
535         },
536
537         _onDownArrow: function(/*Object*/ message){
538                 // summary: down arrow pressed; get next visible node, set focus there
539                 var node = this._getNextNode(message.node);
540                 if(node && node.isTreeNode){
541                         this.focusNode(node);
542                 }       
543         },
544
545         _onUpArrow: function(/*Object*/ message){
546                 // summary: up arrow pressed; move to previous visible node
547
548                 var node = message.node;
549
550                 // if younger siblings          
551                 var previousSibling = node.getPreviousSibling();
552                 if(previousSibling){
553                         node = previousSibling;
554                         // if the previous node is expanded, dive in deep
555                         while(node.isExpandable && node.isExpanded && node.hasChildren()){
556                                 // move to the last child
557                                 var children = node.getChildren();
558                                 node = children[children.length-1];
559                         }
560                 }else{
561                         // if this is the first child, return the parent
562                         // unless the parent is the root of a tree with a hidden root
563                         var parent = node.getParent();
564                         if(!(!this.showRoot && parent === this.rootNode)){
565                                 node = parent;
566                         }
567                 }
568
569                 if(node && node.isTreeNode){
570                         this.focusNode(node);
571                 }
572         },
573
574         _onRightArrow: function(/*Object*/ message){
575                 // summary: right arrow pressed; go to child node
576                 var node = message.node;
577
578                 // if not expanded, expand, else move to 1st child
579                 if(node.isExpandable && !node.isExpanded){
580                         this._expandNode(node);
581                 }else if(node.hasChildren()){
582                         node = node.getChildren()[0];
583                         if(node && node.isTreeNode){
584                                 this.focusNode(node);
585                         }
586                 }
587         },
588
589         _onLeftArrow: function(/*Object*/ message){
590                 // summary:
591                 //              Left arrow pressed.
592                 //              If not collapsed, collapse, else move to parent.
593
594                 var node = message.node;
595
596                 if(node.isExpandable && node.isExpanded){
597                         this._collapseNode(node);
598                 }else{
599                         node = node.getParent();
600                         if(node && node.isTreeNode){
601                                 this.focusNode(node);
602                         }
603                 }
604         },
605
606         _onHomeKey: function(){
607                 // summary: home pressed; get first visible node, set focus there
608                 var node = this._getRootOrFirstNode();
609                 if(node){
610                         this.focusNode(node);
611                 }
612         },
613
614         _onEndKey: function(/*Object*/ message){
615                 // summary: end pressed; go to last visible node
616
617                 var node = this;
618                 while(node.isExpanded){
619                         var c = node.getChildren();
620                         node = c[c.length - 1];
621                 }
622
623                 if(node && node.isTreeNode){
624                         this.focusNode(node);
625                 }
626         },
627
628         _onLetterKeyNav: function(message){
629                 // summary: letter key pressed; search for node starting with first char = key
630                 var node = startNode = message.node,
631                         key = message.key;
632                 do{
633                         node = this._getNextNode(node);
634                         //check for last node, jump to first node if necessary
635                         if(!node){
636                                 node = this._getRootOrFirstNode();
637                         }
638                 }while(node !== startNode && (node.label.charAt(0).toLowerCase() != key));
639                 if(node && node.isTreeNode){
640                         // no need to set focus if back where we started
641                         if(node !== startNode){
642                                 this.focusNode(node);
643                         }
644                 }
645         },
646
647         _onClick: function(/*Event*/ e){
648                 // summary: translates click events into commands for the controller to process
649                 var domElement = e.target;
650
651                 // find node
652                 var nodeWidget = dijit.getEnclosingWidget(domElement);  
653                 if(!nodeWidget || !nodeWidget.isTreeNode){
654                         return;
655                 }
656
657                 if( (this.openOnClick && nodeWidget.isExpandable) ||
658                         (domElement == nodeWidget.expandoNode || domElement == nodeWidget.expandoNodeText) ){
659                         // expando node was clicked, or label of a folder node was clicked; open it
660                         if(nodeWidget.isExpandable){
661                                 this._onExpandoClick({node:nodeWidget});
662                         }
663                 }else{
664                         this._publish("execute", { item: nodeWidget.item, node: nodeWidget} );
665                         this.onClick(nodeWidget.item, nodeWidget);
666                         this.focusNode(nodeWidget);
667                 }
668                 dojo.stopEvent(e);
669         },
670
671         _onExpandoClick: function(/*Object*/ message){
672                 // summary: user clicked the +/- icon; expand or collapse my children.
673                 var node = message.node;
674                 
675                 // If we are collapsing, we might be hiding the currently focused node.
676                 // Also, clicking the expando node might have erased focus from the current node.
677                 // For simplicity's sake just focus on the node with the expando.
678                 this.focusNode(node);
679
680                 if(node.isExpanded){
681                         this._collapseNode(node);
682                 }else{
683                         this._expandNode(node);
684                 }
685         },
686
687         onClick: function(/* dojo.data */ item, /*TreeNode*/ node){
688                 // summary: user overridable function for executing a tree item
689         },
690
691         _getNextNode: function(node){
692                 // summary: get next visible node
693
694                 if(node.isExpandable && node.isExpanded && node.hasChildren()){
695                         // if this is an expanded node, get the first child
696                         return node.getChildren()[0];           // _TreeNode    
697                 }else{
698                         // find a parent node with a sibling
699                         while(node && node.isTreeNode){
700                                 var returnNode = node.getNextSibling();
701                                 if(returnNode){
702                                         return returnNode;              // _TreeNode
703                                 }
704                                 node = node.getParent();
705                         }
706                         return null;
707                 }
708         },
709
710         _getRootOrFirstNode: function(){
711                 // summary: get first visible node
712                 return this.showRoot ? this.rootNode : this.rootNode.getChildren()[0];
713         },
714
715         _collapseNode: function(/*_TreeNode*/ node){
716                 // summary: called when the user has requested to collapse the node
717
718                 if(node.isExpandable){
719                         if(node.state == "LOADING"){
720                                 // ignore clicks while we are in the process of loading data
721                                 return;
722                         }
723
724                         node.collapse();
725                         if(this.persist && node.item){
726                                 delete this._openedItemIds[this.model.getIdentity(node.item)];
727                                 this._saveState();
728                         }
729                 }
730         },
731
732         _expandNode: function(/*_TreeNode*/ node){
733                 // summary: called when the user has requested to expand the node
734
735                 if(!node.isExpandable){
736                         return;
737                 }
738
739                 var model = this.model,
740                         item = node.item;
741
742                 switch(node.state){
743                         case "LOADING":
744                                 // ignore clicks while we are in the process of loading data
745                                 return;
746
747                         case "UNCHECKED":
748                                 // need to load all the children, and then expand
749                                 node.markProcessing();
750                                 var _this = this;
751                                 model.getChildren(item, function(items){
752                                                 node.unmarkProcessing();
753                                                 node.setChildItems(items);
754                                                 _this._expandNode(node);
755                                         },
756                                         function(err){
757                                                 console.error(_this, ": error loading root children: ", err);
758                                         });
759                                 break;
760
761                         default:
762                                 // data is already loaded; just proceed
763                                 node.expand();
764                                 if(this.persist && item){
765                                         this._openedItemIds[model.getIdentity(item)] = true;
766                                         this._saveState();
767                                 }
768                 }
769         },
770
771         ////////////////// Miscellaneous functions ////////////////
772
773         blurNode: function(){
774                 // summary
775                 //      Removes focus from the currently focused node (which must be visible).
776                 //      Usually not called directly (just call focusNode() on another node instead)
777                 var node = this.lastFocused;
778                 if(!node){ return; }
779                 var labelNode = node.labelNode;
780                 dojo.removeClass(labelNode, "dijitTreeLabelFocused");
781                 labelNode.setAttribute("tabIndex", "-1");
782                 dijit.setWaiState(labelNode, "selected", false);
783                 this.lastFocused = null;
784         },
785
786         focusNode: function(/* _tree.Node */ node){
787                 // summary
788                 //      Focus on the specified node (which must be visible)
789
790                 // set focus so that the label will be voiced using screen readers
791                 node.labelNode.focus();
792         },
793
794         _onBlur: function(){
795                 // summary:
796                 //              We've moved away from the whole tree.  The currently "focused" node
797                 //              (see focusNode above) should remain as the lastFocused node so we can
798                 //              tab back into the tree.  Just change CSS to get rid of the dotted border
799                 //              until that time
800
801                 this.inherited(arguments);
802                 if(this.lastFocused){
803                         var labelNode = this.lastFocused.labelNode;
804                         dojo.removeClass(labelNode, "dijitTreeLabelFocused");   
805                 }
806         },
807
808         _onTreeFocus: function(/*Widget*/ node){
809                 // summary:
810                 //              called from onFocus handler of treeitem labelNode to set styles, wai state and tabindex
811                 //              for currently focused treeitem.
812                 
813                 if (node){
814                         if(node != this.lastFocused){
815                                 this.blurNode();
816                         }
817                         var labelNode = node.labelNode;
818                         // set tabIndex so that the tab key can find this node
819                         labelNode.setAttribute("tabIndex", "0");
820                         dijit.setWaiState(labelNode, "selected", true);
821                         dojo.addClass(labelNode, "dijitTreeLabelFocused");
822                         this.lastFocused = node;
823                 }
824         },
825
826         //////////////// Events from the model //////////////////////////
827         
828         _onItemDelete: function(/*Object*/ item){
829                 //summary: delete event from the store
830                 // TODO: currently this isn't called, and technically doesn't need to be,
831                 // but it would help with garbage collection
832
833                 var identity = this.model.getIdentity(item);
834                 var node = this._itemNodeMap[identity];
835
836                 if(node){
837                         var parent = node.getParent();
838                         if(parent){
839                                 // if node has not already been orphaned from a _onSetItem(parent, "children", ..) call...
840                                 parent.removeChild(node);
841                         }
842                         delete this._itemNodeMap[identity];
843                         node.destroyRecursive();
844                 }
845         },
846
847         _onItemChange: function(/*Item*/ item){
848                 //summary: set data event on an item in the store
849                 var model = this.model,
850                         identity = model.getIdentity(item),
851                         node = this._itemNodeMap[identity];
852
853                 if(node){
854                         node.setLabelNode(this.getLabel(item));
855                         node._updateItemClasses(item);
856                 }
857         },
858
859         _onItemChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){
860                 //summary: set data event on an item in the store
861                 var model = this.model,
862                         identity = model.getIdentity(parent),
863                         parentNode = this._itemNodeMap[identity];
864
865                 if(parentNode){
866                         parentNode.setChildItems(newChildrenList);
867                 }
868         },
869
870         /////////////// Miscellaneous funcs
871         
872         _saveState: function(){
873                 //summary: create and save a cookie with the currently expanded nodes identifiers
874                 if(!this.persist){
875                         return;
876                 }
877                 var ary = [];
878                 for(var id in this._openedItemIds){
879                         ary.push(id);
880                 }
881                 dojo.cookie(this.cookieName, ary.join(","));
882         },
883
884         destroy: function(){
885                 if(this.rootNode){
886                         this.rootNode.destroyRecursive();
887                 }
888                 this.rootNode = null;
889                 this.inherited(arguments);
890         },
891         
892         destroyRecursive: function(){
893                 // A tree is treated as a leaf, not as a node with children (like a grid),
894                 // but defining destroyRecursive for back-compat.
895                 this.destroy();
896         }
897 });
898
899
900 dojo.declare(
901         "dijit.tree.TreeStoreModel",
902         null,
903 {
904         // summary
905         //              Implements dijit.Tree.model connecting to a store with a single
906         //              root item.  Any methods passed into the constructor will override
907         //              the ones defined here.
908
909         // store: dojo.data.Store
910         //              Underlying store
911         store: null,
912
913         // childrenAttrs: String[]
914         //              one ore more attributes that holds children of a tree node
915         childrenAttrs: ["children"],
916         
917         // root: dojo.data.Item
918         //              Pointer to the root item (read only, not a parameter)
919         root: null,
920
921         // query: anything
922         //              Specifies datastore query to return the root item for the tree.
923         //              Must only return a single item.   Alternately can just pass in pointer
924         //              to root item.
925         // example:
926         //              {id:'ROOT'}
927         query: null,
928
929         constructor: function(/* Object */ args){
930                 // summary: passed the arguments listed above (store, etc)
931                 dojo.mixin(this, args);
932
933                 this.connects = [];
934
935                 var store = this.store;
936                 if(!store.getFeatures()['dojo.data.api.Identity']){
937                         throw new Error("dijit.Tree: store must support dojo.data.Identity");                   
938                 }
939
940                 // if the store supports Notification, subscribe to the notification events
941                 if(store.getFeatures()['dojo.data.api.Notification']){
942                         this.connects = this.connects.concat([
943                                 dojo.connect(store, "onNew", this, "_onNewItem"),
944                                 dojo.connect(store, "onDelete", this, "_onDeleteItem"),
945                                 dojo.connect(store, "onSet", this, "_onSetItem")
946                         ]);
947                 }
948         },
949
950         destroy: function(){
951                 dojo.forEach(this.connects, dojo.disconnect);
952         },
953
954         // =======================================================================
955         // Methods for traversing hierarchy
956
957         getRoot: function(onItem, onError){
958                 // summary:
959                 //              Calls onItem with the root item for the tree, possibly a fabricated item.
960                 //              Calls onError on error.
961                 if(this.root){
962                         onItem(this.root);
963                 }else{
964                         this.store.fetch({
965                                 query: this.query,
966                                 onComplete: dojo.hitch(this, function(items){
967                                         if(items.length != 1){
968                                                 throw new Error(this.declaredClass + ": query " + query + " returned " + items.length +
969                                                         " items, but must return exactly one item");
970                                         }
971                                         this.root = items[0];
972                                         onItem(this.root);
973                                 }),
974                                 onError: onError
975                         });
976                 }
977         },
978
979         mayHaveChildren: function(/*dojo.data.Item*/ item){
980                 // summary
981                 //              Tells if an item has or may have children.  Implementing logic here
982                 //              avoids showing +/- expando icon for nodes that we know don't have children.
983                 //              (For efficiency reasons we may not want to check if an element actually
984                 //              has children until user clicks the expando node)
985                 return dojo.some(this.childrenAttrs, function(attr){
986                         return this.store.hasAttribute(item, attr);
987                 }, this);
988         },
989
990         getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete, /*function*/ onError){
991                 // summary
992                 //              Calls onComplete() with array of child items of given parent item, all loaded.
993
994                 var store = this.store;
995
996                 // get children of specified item
997                 var childItems = [];
998                 for (var i=0; i<this.childrenAttrs.length; i++){
999                         var vals = store.getValues(parentItem, this.childrenAttrs[i]);
1000                         childItems = childItems.concat(vals);
1001                 }
1002
1003                 // count how many items need to be loaded
1004                 var _waitCount = 0;
1005                 dojo.forEach(childItems, function(item){ if(!store.isItemLoaded(item)){ _waitCount++; } });
1006
1007                 if(_waitCount == 0){
1008                         // all items are already loaded.  proceed...
1009                         onComplete(childItems);
1010                 }else{
1011                         // still waiting for some or all of the items to load
1012                         var onItem = function onItem(item){
1013                                 if(--_waitCount == 0){
1014                                         // all nodes have been loaded, send them to the tree
1015                                         onComplete(childItems);
1016                                 }
1017                         }
1018                         dojo.forEach(childItems, function(item){
1019                                 if(!store.isItemLoaded(item)){
1020                                         store.loadItem({
1021                                                 item: item,
1022                                                 onItem: onItem,
1023                                                 onError: onError
1024                                         });
1025                                 }
1026                         });
1027                 }
1028         },
1029
1030         // =======================================================================
1031         // Inspecting items
1032
1033         getIdentity: function(/* item */ item){
1034                 return this.store.getIdentity(item);    // Object
1035         },
1036
1037         getLabel: function(/*dojo.data.Item*/ item){
1038                 // summary: get the label for an item
1039                 return this.store.getLabel(item);       // String
1040         },
1041
1042         // =======================================================================
1043         // Write interface
1044
1045         newItem: function(/* Object? */ args, /*Item*/ parent){
1046                 // summary
1047                 //              Creates a new item.   See dojo.data.api.Write for details on args.
1048                 //              Used in drag & drop when item from external source dropped onto tree.
1049                 var pInfo = {parent: parent, attribute: this.childrenAttrs[0]};
1050                 return this.store.newItem(args, pInfo);
1051         },
1052
1053         pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy){
1054                 // summary
1055                 //              Move or copy an item from one parent item to another.
1056                 //              Used in drag & drop
1057                 var store = this.store,
1058                         parentAttr = this.childrenAttrs[0];     // name of "children" attr in parent item
1059
1060                 // remove child from source item, and record the attributee that child occurred in      
1061                 if(oldParentItem){
1062                         dojo.forEach(this.childrenAttrs, function(attr){
1063                                 if(store.containsValue(oldParentItem, attr, childItem)){
1064                                         if(!bCopy){
1065                                                 var values = dojo.filter(store.getValues(oldParentItem, attr), function(x){
1066                                                         return x != childItem;
1067                                                 });
1068                                                 store.setValues(oldParentItem, attr, values);
1069                                         }
1070                                         parentAttr = attr;
1071                                 }
1072                         });
1073                 }
1074
1075                 // modify target item's children attribute to include this item
1076                 if(newParentItem){
1077                         store.setValues(newParentItem, parentAttr,
1078                                 store.getValues(newParentItem, parentAttr).concat(childItem));
1079                 }
1080         },
1081
1082         // =======================================================================
1083         // Callbacks
1084         
1085         onChange: function(/*dojo.data.Item*/ item){
1086                 // summary
1087                 //              Callback whenever an item has changed, so that Tree
1088                 //              can update the label, icon, etc.   Note that changes
1089                 //              to an item's children or parent(s) will trigger an
1090                 //              onChildrenChange() so you can ignore those changes here.
1091         },
1092
1093         onChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){
1094                 // summary
1095                 //              Callback to do notifications about new, updated, or deleted items.
1096         },
1097
1098         // =======================================================================
1099         ///Events from data store
1100
1101         _onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){
1102                 // summary: handler for when new items appear in the store.
1103
1104                 //      In this case there's no correspond onSet() call on the parent of this
1105                 //      item, so need to get the new children list of the parent manually somehow.
1106                 if(!parentInfo){
1107                         return;
1108                 }
1109                 this.getChildren(parentInfo.item, dojo.hitch(this, function(children){
1110                         // NOTE: maybe can be optimized since parentInfo contains the new and old attribute value
1111                         this.onChildrenChange(parentInfo.item, children);
1112                 }));
1113         },
1114         
1115         _onDeleteItem: function(/*Object*/ item){
1116                 // summary: handler for delete notifications from underlying store
1117         },
1118
1119         _onSetItem: function(/* item */ item, 
1120                                         /* attribute-name-string */ attribute, 
1121                                         /* object | array */ oldValue,
1122                                         /* object | array */ newValue){
1123                 //summary: set data event on an item in the store
1124         
1125                 if(dojo.indexOf(this.childrenAttrs, attribute) != -1){
1126                         // item's children list changed
1127                         this.getChildren(item, dojo.hitch(this, function(children){
1128                                 // NOTE: maybe can be optimized since parentInfo contains the new and old attribute value
1129                                 this.onChildrenChange(item, children);
1130                         }));
1131                 }else{
1132                         // item's label/icon/etc. changed.
1133                         this.onChange(item);
1134                 }
1135         }
1136 });
1137
1138 dojo.declare("dijit.tree.ForestStoreModel", dijit.tree.TreeStoreModel, {
1139         // summary
1140         //              Interface between Tree and a dojo.store that doesn't have a root item, ie,
1141         //              has multiple "top level" items.
1142         //
1143         // description
1144         //              Use this class to wrap a dojo.store, making all the items matching the specified query
1145         //              appear as children of a fabricated "root item".  If no query is specified then all the
1146         //              items returned by fetch() on the underlying store become children of the root item.
1147         //              It allows dijit.Tree to assume a single root item, even if the store doesn't have one.
1148
1149         // Parameters to constructor
1150
1151         // rootId: String
1152         //      ID of fabricated root item
1153         rootId: "$root$",
1154
1155         // rootLabel: String
1156         //      Label of fabricated root item
1157         rootLabel: "ROOT",
1158
1159         // query: String
1160         //      Specifies the set of children of the root item.
1161         // example:
1162         //              {type:'continent'}
1163         query: null,
1164
1165         // End of parameters to constructor
1166
1167         constructor: function(params){
1168                 // Make dummy root item
1169                 this.root = {
1170                         store: this,
1171                         root: true,
1172                         id: params.rootId,
1173                         label: params.rootLabel,
1174                         children: params.rootChildren   // optional param
1175                 };
1176         },
1177
1178         // =======================================================================
1179         // Methods for traversing hierarchy
1180
1181         mayHaveChildren: function(/*dojo.data.Item*/ item){
1182                 // summary
1183                 //              Tells if an item has or may have children.  Implementing logic here
1184                 //              avoids showing +/- expando icon for nodes that we know don't have children.
1185                 //              (For efficiency reasons we may not want to check if an element actually
1186                 //              has children until user clicks the expando node)
1187                 return item === this.root || this.inherited(arguments);
1188         },
1189
1190         getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ callback, /*function*/ onError){
1191                 // summary
1192                 //              Calls onComplete() with array of child items of given parent item, all loaded.
1193                 if(parentItem === this.root){
1194                         if(this.root.children){
1195                                 // already loaded, just return
1196                                 callback(this.root.children);
1197                         }else{
1198                                 this.store.fetch({
1199                                         query: this.query,
1200                                         onComplete: dojo.hitch(this, function(items){
1201                                                 this.root.children = items;
1202                                                 callback(items);
1203                                         }),
1204                                         onError: onError
1205                                 });
1206                         }
1207                 }else{
1208                         this.inherited(arguments);
1209                 }
1210         },
1211
1212         // =======================================================================
1213         // Inspecting items
1214
1215         getIdentity: function(/* item */ item){
1216                 return (item === this.root) ? this.root.id : this.inherited(arguments);
1217         },
1218
1219         getLabel: function(/* item */ item){
1220                 return  (item === this.root) ? this.root.label : this.inherited(arguments);
1221         },
1222
1223         // =======================================================================
1224         // Write interface
1225
1226         newItem: function(/* Object? */ args, /*Item*/ parent){
1227                 // summary
1228                 //              Creates a new item.   See dojo.data.api.Write for details on args.
1229                 //              Used in drag & drop when item from external source dropped onto tree.
1230                 if(parent===this.root){
1231                         this.onNewRootItem(args);
1232                         return this.store.newItem(args);
1233                 }else{
1234                         return this.inherited(arguments);
1235                 }
1236         },
1237  
1238         onNewRootItem: function(args){
1239                 // summary:
1240                 //              User can override this method to modify a new element that's being
1241                 //              added to the root of the tree, for example to add a flag like root=true
1242         },
1243
1244         pasteItem: function(/*Item*/ childItem, /*Item*/ oldParentItem, /*Item*/ newParentItem, /*Boolean*/ bCopy){
1245                 // summary
1246                 //              Move or copy an item from one parent item to another.
1247                 //              Used in drag & drop
1248                 if(oldParentItem === this.root){
1249                         if(!bCopy){
1250                                 // It's onLeaveRoot()'s responsibility to modify the item so it no longer matches
1251                                 // this.query... thus triggering an onChildrenChange() event to notify the Tree
1252                                 // that this element is no longer a child of the root node
1253                                 this.onLeaveRoot(childItem);
1254                         }
1255                 }
1256                 dijit.tree.TreeStoreModel.prototype.pasteItem.call(this, childItem,
1257                         oldParentItem === this.root ? null : oldParentItem,
1258                         newParentItem === this.root ? null : newParentItem
1259                 );
1260                 if(newParentItem === this.root){
1261                         // It's onAddToRoot()'s responsibility to modify the item so it matches
1262                         // this.query... thus triggering an onChildrenChange() event to notify the Tree
1263                         // that this element is now a child of the root node
1264                         this.onAddToRoot(childItem);
1265                 }
1266         },
1267
1268         // =======================================================================
1269         // Callbacks
1270         
1271         onAddToRoot: function(/* item */ item){
1272                 // summary
1273                 //              Called when item added to root of tree; user must override
1274                 //              to modify the item so that it matches the query for top level items
1275                 // example
1276                 //      |       store.setValue(item, "root", true);
1277                 console.log(this, ": item ", item, " added to root");
1278         },
1279
1280         onLeaveRoot: function(/* item */ item){
1281                 // summary
1282                 //              Called when item removed from root of tree; user must override
1283                 //              to modify the item so it doesn't match the query for top level items
1284                 // example
1285                 //      |       store.unsetAttribute(item, "root");
1286                 console.log(this, ": item ", item, " removed from root");
1287         },
1288         
1289         // =======================================================================
1290         // Events from data store
1291
1292         _requeryTop: function(){
1293                 // reruns the query for the children of the root node,
1294                 // sending out an onSet notification if those children have changed
1295                 var _this = this,
1296                         oldChildren = this.root.children;
1297                 this.store.fetch({
1298                         query: this.query,
1299                         onComplete: function(newChildren){
1300                                 _this.root.children = newChildren;
1301
1302                                 // If the list of children or the order of children has changed...      
1303                                 if(oldChildren.length != newChildren.length ||
1304                                         dojo.some(oldChildren, function(item, idx){ return newChildren[idx] != item;})){
1305                                         _this.onChildrenChange(_this.root, newChildren);
1306                                 }
1307                         }
1308                 });
1309         },
1310
1311         _onNewItem: function(/* dojo.data.Item */ item, /* Object */ parentInfo){
1312                 // summary: handler for when new items appear in the store.
1313
1314                 //              In theory, any new item could be a top level item.
1315                 //              Do the safe but inefficient thing by requerying the top
1316                 //              level items.   User can override this function to do something
1317                 //              more efficient.
1318                 this._requeryTop();
1319
1320                 this.inherited(arguments);
1321         },
1322
1323         _onDeleteItem: function(/*Object*/ item){
1324                 // summary: handler for delete notifications from underlying store
1325
1326                 // check if this was a child of root, and if so send notification that root's children
1327                 // have changed
1328                 if(dojo.indexOf(this.root.children, item) != -1){
1329                         this._requeryTop();
1330                 }
1331
1332                 this.inherited(arguments);
1333         }
1334 });
1335
1336 }