source: Dev/branches/rest-dojo-ui/client/dijit/Tree.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 50.0 KB
RevLine 
[256]1define([
2        "dojo/_base/array", // array.filter array.forEach array.map
3        "dojo/_base/connect",   // connect.isCopyKey()
4        "dojo/cookie", // cookie
5        "dojo/_base/declare", // declare
6        "dojo/_base/Deferred", // Deferred
7        "dojo/DeferredList", // DeferredList
8        "dojo/dom", // dom.isDescendant
9        "dojo/dom-class", // domClass.add domClass.remove domClass.replace domClass.toggle
10        "dojo/dom-geometry", // domGeometry.setMarginBox domGeometry.position
11        "dojo/dom-style",// domStyle.set
12        "dojo/_base/event", // event.stop
13        "dojo/fx", // fxUtils.wipeIn fxUtils.wipeOut
14        "dojo/_base/kernel", // kernel.deprecated
15        "dojo/keys",    // arrows etc.
16        "dojo/_base/lang", // lang.getObject lang.mixin lang.hitch
17        "dojo/topic",
18        "./focus",
19        "./registry",   // registry.getEnclosingWidget(), manager.defaultDuration
20        "./_base/manager",      // manager.getEnclosingWidget(), manager.defaultDuration
21        "./_Widget",
22        "./_TemplatedMixin",
23        "./_Container",
24        "./_Contained",
25        "./_CssStateMixin",
26        "dojo/text!./templates/TreeNode.html",
27        "dojo/text!./templates/Tree.html",
28        "./tree/TreeStoreModel",
29        "./tree/ForestStoreModel",
30        "./tree/_dndSelector"
31], function(array, connect, cookie, declare, Deferred, DeferredList,
32                        dom, domClass, domGeometry, domStyle, event, fxUtils, kernel, keys, lang, topic,
33                        focus, registry, manager, _Widget, _TemplatedMixin, _Container, _Contained, _CssStateMixin,
34                        treeNodeTemplate, treeTemplate, TreeStoreModel, ForestStoreModel, _dndSelector){
35
36/*=====
37        var _Widget = dijit._Widget;
38        var _TemplatedMixin = dijit._TemplatedMixin;
39        var _CssStateMixin = dijit._CssStateMixin;
40        var _Container = dijit._Container;
41        var _Contained = dijit._Contained;
42=====*/
43
44// module:
45//              dijit/Tree
46// summary:
47//              dijit.Tree widget, and internal dijit._TreeNode widget
48
49
50var TreeNode = declare(
51        "dijit._TreeNode",
52        [_Widget, _TemplatedMixin, _Container, _Contained, _CssStateMixin],
53{
54        // summary:
55        //              Single node within a tree.   This class is used internally
56        //              by Tree and should not be accessed directly.
57        // tags:
58        //              private
59
60        // item: [const] Item
61        //              the dojo.data entry this tree represents
62        item: null,
63
64        // isTreeNode: [protected] Boolean
65        //              Indicates that this is a TreeNode.   Used by `dijit.Tree` only,
66        //              should not be accessed directly.
67        isTreeNode: true,
68
69        // label: String
70        //              Text of this tree node
71        label: "",
72        _setLabelAttr: {node: "labelNode", type: "innerText"},
73
74        // isExpandable: [private] Boolean
75        //              This node has children, so show the expando node (+ sign)
76        isExpandable: null,
77
78        // isExpanded: [readonly] Boolean
79        //              This node is currently expanded (ie, opened)
80        isExpanded: false,
81
82        // state: [private] String
83        //              Dynamic loading-related stuff.
84        //              When an empty folder node appears, it is "UNCHECKED" first,
85        //              then after dojo.data query it becomes "LOADING" and, finally "LOADED"
86        state: "UNCHECKED",
87
88        templateString: treeNodeTemplate,
89
90        baseClass: "dijitTreeNode",
91
92        // For hover effect for tree node, and focus effect for label
93        cssStateNodes: {
94                rowNode: "dijitTreeRow",
95                labelNode: "dijitTreeLabel"
96        },
97
98        // Tooltip is defined in _WidgetBase but we need to handle the mapping to DOM here
99        _setTooltipAttr: {node: "rowNode", type: "attribute", attribute: "title"},
100
101        buildRendering: function(){
102                this.inherited(arguments);
103
104                // set expand icon for leaf
105                this._setExpando();
106
107                // set icon and label class based on item
108                this._updateItemClasses(this.item);
109
110                if(this.isExpandable){
111                        this.labelNode.setAttribute("aria-expanded", this.isExpanded);
112                }
113
114                //aria-selected should be false on all selectable elements.
115                this.setSelected(false);
116        },
117
118        _setIndentAttr: function(indent){
119                // summary:
120                //              Tell this node how many levels it should be indented
121                // description:
122                //              0 for top level nodes, 1 for their children, 2 for their
123                //              grandchildren, etc.
124
125                // Math.max() is to prevent negative padding on hidden root node (when indent == -1)
126                var pixels = (Math.max(indent, 0) * this.tree._nodePixelIndent) + "px";
127
128                domStyle.set(this.domNode, "backgroundPosition",        pixels + " 0px");
129                domStyle.set(this.rowNode, this.isLeftToRight() ? "paddingLeft" : "paddingRight", pixels);
130
131                array.forEach(this.getChildren(), function(child){
132                        child.set("indent", indent+1);
133                });
134
135                this._set("indent", indent);
136        },
137
138        markProcessing: function(){
139                // summary:
140                //              Visually denote that tree is loading data, etc.
141                // tags:
142                //              private
143                this.state = "LOADING";
144                this._setExpando(true);
145        },
146
147        unmarkProcessing: function(){
148                // summary:
149                //              Clear markup from markProcessing() call
150                // tags:
151                //              private
152                this._setExpando(false);
153        },
154
155        _updateItemClasses: function(item){
156                // summary:
157                //              Set appropriate CSS classes for icon and label dom node
158                //              (used to allow for item updates to change respective CSS)
159                // tags:
160                //              private
161                var tree = this.tree, model = tree.model;
162                if(tree._v10Compat && item === model.root){
163                        // For back-compat with 1.0, need to use null to specify root item (TODO: remove in 2.0)
164                        item = null;
165                }
166                this._applyClassAndStyle(item, "icon", "Icon");
167                this._applyClassAndStyle(item, "label", "Label");
168                this._applyClassAndStyle(item, "row", "Row");
169        },
170
171        _applyClassAndStyle: function(item, lower, upper){
172                // summary:
173                //              Set the appropriate CSS classes and styles for labels, icons and rows.
174                //
175                // item:
176                //              The data item.
177                //
178                // lower:
179                //              The lower case attribute to use, e.g. 'icon', 'label' or 'row'.
180                //
181                // upper:
182                //              The upper case attribute to use, e.g. 'Icon', 'Label' or 'Row'.
183                //
184                // tags:
185                //              private
186
187                var clsName = "_" + lower + "Class";
188                var nodeName = lower + "Node";
189                var oldCls = this[clsName];
190
191                this[clsName] = this.tree["get" + upper + "Class"](item, this.isExpanded);
192                domClass.replace(this[nodeName], this[clsName] || "", oldCls || "");
193
194                domStyle.set(this[nodeName], this.tree["get" + upper + "Style"](item, this.isExpanded) || {});
195        },
196
197        _updateLayout: function(){
198                // summary:
199                //              Set appropriate CSS classes for this.domNode
200                // tags:
201                //              private
202                var parent = this.getParent();
203                if(!parent || !parent.rowNode || parent.rowNode.style.display == "none"){
204                        /* if we are hiding the root node then make every first level child look like a root node */
205                        domClass.add(this.domNode, "dijitTreeIsRoot");
206                }else{
207                        domClass.toggle(this.domNode, "dijitTreeIsLast", !this.getNextSibling());
208                }
209        },
210
211        _setExpando: function(/*Boolean*/ processing){
212                // summary:
213                //              Set the right image for the expando node
214                // tags:
215                //              private
216
217                var styles = ["dijitTreeExpandoLoading", "dijitTreeExpandoOpened",
218                                                "dijitTreeExpandoClosed", "dijitTreeExpandoLeaf"],
219                        _a11yStates = ["*","-","+","*"],
220                        idx = processing ? 0 : (this.isExpandable ?     (this.isExpanded ? 1 : 2) : 3);
221
222                // apply the appropriate class to the expando node
223                domClass.replace(this.expandoNode, styles[idx], styles);
224
225                // provide a non-image based indicator for images-off mode
226                this.expandoNodeText.innerHTML = _a11yStates[idx];
227
228        },
229
230        expand: function(){
231                // summary:
232                //              Show my children
233                // returns:
234                //              Deferred that fires when expansion is complete
235
236                // If there's already an expand in progress or we are already expanded, just return
237                if(this._expandDeferred){
238                        return this._expandDeferred;            // dojo.Deferred
239                }
240
241                // cancel in progress collapse operation
242                this._wipeOut && this._wipeOut.stop();
243
244                // All the state information for when a node is expanded, maybe this should be
245                // set when the animation completes instead
246                this.isExpanded = true;
247                this.labelNode.setAttribute("aria-expanded", "true");
248                if(this.tree.showRoot || this !== this.tree.rootNode){
249                        this.containerNode.setAttribute("role", "group");
250                }
251                domClass.add(this.contentNode,'dijitTreeContentExpanded');
252                this._setExpando();
253                this._updateItemClasses(this.item);
254                if(this == this.tree.rootNode){
255                        this.tree.domNode.setAttribute("aria-expanded", "true");
256                }
257
258                var def,
259                        wipeIn = fxUtils.wipeIn({
260                                node: this.containerNode, duration: manager.defaultDuration,
261                                onEnd: function(){
262                                        def.callback(true);
263                                }
264                        });
265
266                // Deferred that fires when expand is complete
267                def = (this._expandDeferred = new Deferred(function(){
268                        // Canceller
269                        wipeIn.stop();
270                }));
271
272                wipeIn.play();
273
274                return def;             // dojo.Deferred
275        },
276
277        collapse: function(){
278                // summary:
279                //              Collapse this node (if it's expanded)
280
281                if(!this.isExpanded){ return; }
282
283                // cancel in progress expand operation
284                if(this._expandDeferred){
285                        this._expandDeferred.cancel();
286                        delete this._expandDeferred;
287                }
288
289                this.isExpanded = false;
290                this.labelNode.setAttribute("aria-expanded", "false");
291                if(this == this.tree.rootNode){
292                        this.tree.domNode.setAttribute("aria-expanded", "false");
293                }
294                domClass.remove(this.contentNode,'dijitTreeContentExpanded');
295                this._setExpando();
296                this._updateItemClasses(this.item);
297
298                if(!this._wipeOut){
299                        this._wipeOut = fxUtils.wipeOut({
300                                node: this.containerNode, duration: manager.defaultDuration
301                        });
302                }
303                this._wipeOut.play();
304        },
305
306        // indent: Integer
307        //              Levels from this node to the root node
308        indent: 0,
309
310        setChildItems: function(/* Object[] */ items){
311                // summary:
312                //              Sets the child items of this node, removing/adding nodes
313                //              from current children to match specified items[] array.
314                //              Also, if this.persist == true, expands any children that were previously
315                //              opened.
316                // returns:
317                //              Deferred object that fires after all previously opened children
318                //              have been expanded again (or fires instantly if there are no such children).
319
320                var tree = this.tree,
321                        model = tree.model,
322                        defs = [];      // list of deferreds that need to fire before I am complete
323
324
325                // Orphan all my existing children.
326                // If items contains some of the same items as before then we will reattach them.
327                // Don't call this.removeChild() because that will collapse the tree etc.
328                array.forEach(this.getChildren(), function(child){
329                        _Container.prototype.removeChild.call(this, child);
330                }, this);
331
332                this.state = "LOADED";
333
334                if(items && items.length > 0){
335                        this.isExpandable = true;
336
337                        // Create _TreeNode widget for each specified tree node, unless one already
338                        // exists and isn't being used (presumably it's from a DnD move and was recently
339                        // released
340                        array.forEach(items, function(item){
341                                var id = model.getIdentity(item),
342                                        existingNodes = tree._itemNodesMap[id],
343                                        node;
344                                if(existingNodes){
345                                        for(var i=0;i<existingNodes.length;i++){
346                                                if(existingNodes[i] && !existingNodes[i].getParent()){
347                                                        node = existingNodes[i];
348                                                        node.set('indent', this.indent+1);
349                                                        break;
350                                                }
351                                        }
352                                }
353                                if(!node){
354                                        node = this.tree._createTreeNode({
355                                                        item: item,
356                                                        tree: tree,
357                                                        isExpandable: model.mayHaveChildren(item),
358                                                        label: tree.getLabel(item),
359                                                        tooltip: tree.getTooltip(item),
360                                                        dir: tree.dir,
361                                                        lang: tree.lang,
362                                                        textDir: tree.textDir,
363                                                        indent: this.indent + 1
364                                                });
365                                        if(existingNodes){
366                                                existingNodes.push(node);
367                                        }else{
368                                                tree._itemNodesMap[id] = [node];
369                                        }
370                                }
371                                this.addChild(node);
372
373                                // If node was previously opened then open it again now (this may trigger
374                                // more data store accesses, recursively)
375                                if(this.tree.autoExpand || this.tree._state(node)){
376                                        defs.push(tree._expandNode(node));
377                                }
378                        }, this);
379
380                        // note that updateLayout() needs to be called on each child after
381                        // _all_ the children exist
382                        array.forEach(this.getChildren(), function(child){
383                                child._updateLayout();
384                        });
385                }else{
386                        this.isExpandable=false;
387                }
388
389                if(this._setExpando){
390                        // change expando to/from dot or + icon, as appropriate
391                        this._setExpando(false);
392                }
393
394                // Set leaf icon or folder icon, as appropriate
395                this._updateItemClasses(this.item);
396
397                // On initial tree show, make the selected TreeNode as either the root node of the tree,
398                // or the first child, if the root node is hidden
399                if(this == tree.rootNode){
400                        var fc = this.tree.showRoot ? this : this.getChildren()[0];
401                        if(fc){
402                                fc.setFocusable(true);
403                                tree.lastFocused = fc;
404                        }else{
405                                // fallback: no nodes in tree so focus on Tree <div> itself
406                                tree.domNode.setAttribute("tabIndex", "0");
407                        }
408                }
409
410                return new DeferredList(defs);  // dojo.Deferred
411        },
412
413        getTreePath: function(){
414                var node = this;
415                var path = [];
416                while(node && node !== this.tree.rootNode){
417                                path.unshift(node.item);
418                                node = node.getParent();
419                }
420                path.unshift(this.tree.rootNode.item);
421
422                return path;
423        },
424
425        getIdentity: function(){
426                return this.tree.model.getIdentity(this.item);
427        },
428
429        removeChild: function(/* treeNode */ node){
430                this.inherited(arguments);
431
432                var children = this.getChildren();
433                if(children.length == 0){
434                        this.isExpandable = false;
435                        this.collapse();
436                }
437
438                array.forEach(children, function(child){
439                                child._updateLayout();
440                });
441        },
442
443        makeExpandable: function(){
444                // summary:
445                //              if this node wasn't already showing the expando node,
446                //              turn it into one and call _setExpando()
447
448                // TODO: hmm this isn't called from anywhere, maybe should remove it for 2.0
449
450                this.isExpandable = true;
451                this._setExpando(false);
452        },
453
454        _onLabelFocus: function(){
455                // summary:
456                //              Called when this row is focused (possibly programatically)
457                //              Note that we aren't using _onFocus() builtin to dijit
458                //              because it's called when focus is moved to a descendant TreeNode.
459                // tags:
460                //              private
461                this.tree._onNodeFocus(this);
462        },
463
464        setSelected: function(/*Boolean*/ selected){
465                // summary:
466                //              A Tree has a (single) currently selected node.
467                //              Mark that this node is/isn't that currently selected node.
468                // description:
469                //              In particular, setting a node as selected involves setting tabIndex
470                //              so that when user tabs to the tree, focus will go to that node (only).
471                this.labelNode.setAttribute("aria-selected", selected);
472                domClass.toggle(this.rowNode, "dijitTreeRowSelected", selected);
473        },
474
475        setFocusable: function(/*Boolean*/ selected){
476                // summary:
477                //              A Tree has a (single) node that's focusable.
478                //              Mark that this node is/isn't that currently focsuable node.
479                // description:
480                //              In particular, setting a node as selected involves setting tabIndex
481                //              so that when user tabs to the tree, focus will go to that node (only).
482
483                this.labelNode.setAttribute("tabIndex", selected ? "0" : "-1");
484        },
485
486        _onClick: function(evt){
487                // summary:
488                //              Handler for onclick event on a node
489                // tags:
490                //              private
491                this.tree._onClick(this, evt);
492        },
493        _onDblClick: function(evt){
494                // summary:
495                //              Handler for ondblclick event on a node
496                // tags:
497                //              private
498                this.tree._onDblClick(this, evt);
499        },
500
501        _onMouseEnter: function(evt){
502                // summary:
503                //              Handler for onmouseenter event on a node
504                // tags:
505                //              private
506                this.tree._onNodeMouseEnter(this, evt);
507        },
508
509        _onMouseLeave: function(evt){
510                // summary:
511                //              Handler for onmouseenter event on a node
512                // tags:
513                //              private
514                this.tree._onNodeMouseLeave(this, evt);
515        },
516
517        _setTextDirAttr: function(textDir){
518                if(textDir &&((this.textDir != textDir) || !this._created)){
519                        this._set("textDir", textDir);
520                        this.applyTextDir(this.labelNode, this.labelNode.innerText || this.labelNode.textContent || "");
521                        array.forEach(this.getChildren(), function(childNode){
522                                childNode.set("textDir", textDir);
523                        }, this);
524                }
525        }
526});
527
528var Tree = declare("dijit.Tree", [_Widget, _TemplatedMixin], {
529        // summary:
530        //              This widget displays hierarchical data from a store.
531
532        // store: [deprecated] String||dojo.data.Store
533        //              Deprecated.  Use "model" parameter instead.
534        //              The store to get data to display in the tree.
535        store: null,
536
537        // model: dijit.Tree.model
538        //              Interface to read tree data, get notifications of changes to tree data,
539        //              and for handling drop operations (i.e drag and drop onto the tree)
540        model: null,
541
542        // query: [deprecated] anything
543        //              Deprecated.  User should specify query to the model directly instead.
544        //              Specifies datastore query to return the root item or top items for the tree.
545        query: null,
546
547        // label: [deprecated] String
548        //              Deprecated.  Use dijit.tree.ForestStoreModel directly instead.
549        //              Used in conjunction with query parameter.
550        //              If a query is specified (rather than a root node id), and a label is also specified,
551        //              then a fake root node is created and displayed, with this label.
552        label: "",
553
554        // showRoot: [const] Boolean
555        //              Should the root node be displayed, or hidden?
556        showRoot: true,
557
558        // childrenAttr: [deprecated] String[]
559        //              Deprecated.   This information should be specified in the model.
560        //              One ore more attributes that holds children of a tree node
561        childrenAttr: ["children"],
562
563        // paths: String[][] or Item[][]
564        //              Full paths from rootNode to selected nodes expressed as array of items or array of ids.
565        //              Since setting the paths may be asynchronous (because ofwaiting on dojo.data), set("paths", ...)
566        //              returns a Deferred to indicate when the set is complete.
567        paths: [],
568
569        // path: String[] or Item[]
570        //      Backward compatible singular variant of paths.
571        path: [],
572
573        // selectedItems: [readonly] Item[]
574        //              The currently selected items in this tree.
575        //              This property can only be set (via set('selectedItems', ...)) when that item is already
576        //              visible in the tree.   (I.e. the tree has already been expanded to show that node.)
577        //              Should generally use `paths` attribute to set the selected items instead.
578        selectedItems: null,
579
580        // selectedItem: [readonly] Item
581        //      Backward compatible singular variant of selectedItems.
582        selectedItem: null,
583
584        // openOnClick: Boolean
585        //              If true, clicking a folder node's label will open it, rather than calling onClick()
586        openOnClick: false,
587
588        // openOnDblClick: Boolean
589        //              If true, double-clicking a folder node's label will open it, rather than calling onDblClick()
590        openOnDblClick: false,
591
592        templateString: treeTemplate,
593
594        // persist: Boolean
595        //              Enables/disables use of cookies for state saving.
596        persist: true,
597
598        // autoExpand: Boolean
599        //              Fully expand the tree on load.   Overrides `persist`.
600        autoExpand: false,
601
602        // dndController: [protected] Function|String
603        //              Class to use as as the dnd controller.  Specifying this class enables DnD.
604        //              Generally you should specify this as dijit.tree.dndSource.
605        //      Setting of dijit.tree._dndSelector handles selection only (no actual DnD).
606        dndController: _dndSelector,
607
608        // parameters to pull off of the tree and pass on to the dndController as its params
609        dndParams: ["onDndDrop","itemCreator","onDndCancel","checkAcceptance", "checkItemAcceptance", "dragThreshold", "betweenThreshold"],
610
611        //declare the above items so they can be pulled from the tree's markup
612
613        // onDndDrop: [protected] Function
614        //              Parameter to dndController, see `dijit.tree.dndSource.onDndDrop`.
615        //              Generally this doesn't need to be set.
616        onDndDrop: null,
617
618        /*=====
619        itemCreator: function(nodes, target, source){
620                // summary:
621                //              Returns objects passed to `Tree.model.newItem()` based on DnD nodes
622                //              dropped onto the tree.   Developer must override this method to enable
623                //              dropping from external sources onto this Tree, unless the Tree.model's items
624                //              happen to look like {id: 123, name: "Apple" } with no other attributes.
625                // description:
626                //              For each node in nodes[], which came from source, create a hash of name/value
627                //              pairs to be passed to Tree.model.newItem().  Returns array of those hashes.
628                // nodes: DomNode[]
629                //              The DOMNodes dragged from the source container
630                // target: DomNode
631                //              The target TreeNode.rowNode
632                // source: dojo.dnd.Source
633                //              The source container the nodes were dragged from, perhaps another Tree or a plain dojo.dnd.Source
634                // returns: Object[]
635                //              Array of name/value hashes for each new item to be added to the Tree, like:
636                // |    [
637                // |            { id: 123, label: "apple", foo: "bar" },
638                // |            { id: 456, label: "pear", zaz: "bam" }
639                // |    ]
640                // tags:
641                //              extension
642                return [{}];
643        },
644        =====*/
645        itemCreator: null,
646
647        // onDndCancel: [protected] Function
648        //              Parameter to dndController, see `dijit.tree.dndSource.onDndCancel`.
649        //              Generally this doesn't need to be set.
650        onDndCancel: null,
651
652/*=====
653        checkAcceptance: function(source, nodes){
654                // summary:
655                //              Checks if the Tree itself can accept nodes from this source
656                // source: dijit.tree._dndSource
657                //              The source which provides items
658                // nodes: DOMNode[]
659                //              Array of DOM nodes corresponding to nodes being dropped, dijitTreeRow nodes if
660                //              source is a dijit.Tree.
661                // tags:
662                //              extension
663                return true;    // Boolean
664        },
665=====*/
666        checkAcceptance: null,
667
668/*=====
669        checkItemAcceptance: function(target, source, position){
670                // summary:
671                //              Stub function to be overridden if one wants to check for the ability to drop at the node/item level
672                // description:
673                //              In the base case, this is called to check if target can become a child of source.
674                //              When betweenThreshold is set, position="before" or "after" means that we
675                //              are asking if the source node can be dropped before/after the target node.
676                // target: DOMNode
677                //              The dijitTreeRoot DOM node inside of the TreeNode that we are dropping on to
678                //              Use dijit.getEnclosingWidget(target) to get the TreeNode.
679                // source: dijit.tree.dndSource
680                //              The (set of) nodes we are dropping
681                // position: String
682                //              "over", "before", or "after"
683                // tags:
684                //              extension
685                return true;    // Boolean
686        },
687=====*/
688        checkItemAcceptance: null,
689
690        // dragThreshold: Integer
691        //              Number of pixels mouse moves before it's considered the start of a drag operation
692        dragThreshold: 5,
693
694        // betweenThreshold: Integer
695        //              Set to a positive value to allow drag and drop "between" nodes.
696        //
697        //              If during DnD mouse is over a (target) node but less than betweenThreshold
698        //              pixels from the bottom edge, dropping the the dragged node will make it
699        //              the next sibling of the target node, rather than the child.
700        //
701        //              Similarly, if mouse is over a target node but less that betweenThreshold
702        //              pixels from the top edge, dropping the dragged node will make it
703        //              the target node's previous sibling rather than the target node's child.
704        betweenThreshold: 0,
705
706        // _nodePixelIndent: Integer
707        //              Number of pixels to indent tree nodes (relative to parent node).
708        //              Default is 19 but can be overridden by setting CSS class dijitTreeIndent
709        //              and calling resize() or startup() on tree after it's in the DOM.
710        _nodePixelIndent: 19,
711
712        _publish: function(/*String*/ topicName, /*Object*/ message){
713                // summary:
714                //              Publish a message for this widget/topic
715                topic.publish(this.id, lang.mixin({tree: this, event: topicName}, message || {}));      // publish
716        },
717
718        postMixInProperties: function(){
719                this.tree = this;
720
721                if(this.autoExpand){
722                        // There's little point in saving opened/closed state of nodes for a Tree
723                        // that initially opens all it's nodes.
724                        this.persist = false;
725                }
726
727                this._itemNodesMap={};
728
729                if(!this.cookieName && this.id){
730                        this.cookieName = this.id + "SaveStateCookie";
731                }
732
733                this._loadDeferred = new Deferred();
734
735                this.inherited(arguments);
736        },
737
738        postCreate: function(){
739                this._initState();
740
741                // Create glue between store and Tree, if not specified directly by user
742                if(!this.model){
743                        this._store2model();
744                }
745
746                // monitor changes to items
747                this.connect(this.model, "onChange", "_onItemChange");
748                this.connect(this.model, "onChildrenChange", "_onItemChildrenChange");
749                this.connect(this.model, "onDelete", "_onItemDelete");
750
751                this._load();
752
753                this.inherited(arguments);
754
755                if(this.dndController){
756                        if(lang.isString(this.dndController)){
757                                this.dndController = lang.getObject(this.dndController);
758                        }
759                        var params={};
760                        for(var i=0; i<this.dndParams.length;i++){
761                                if(this[this.dndParams[i]]){
762                                        params[this.dndParams[i]] = this[this.dndParams[i]];
763                                }
764                        }
765                        this.dndController = new this.dndController(this, params);
766                }
767        },
768
769        _store2model: function(){
770                // summary:
771                //              User specified a store&query rather than model, so create model from store/query
772                this._v10Compat = true;
773                kernel.deprecated("Tree: from version 2.0, should specify a model object rather than a store/query");
774
775                var modelParams = {
776                        id: this.id + "_ForestStoreModel",
777                        store: this.store,
778                        query: this.query,
779                        childrenAttrs: this.childrenAttr
780                };
781
782                // Only override the model's mayHaveChildren() method if the user has specified an override
783                if(this.params.mayHaveChildren){
784                        modelParams.mayHaveChildren = lang.hitch(this, "mayHaveChildren");
785                }
786
787                if(this.params.getItemChildren){
788                        modelParams.getChildren = lang.hitch(this, function(item, onComplete, onError){
789                                this.getItemChildren((this._v10Compat && item === this.model.root) ? null : item, onComplete, onError);
790                        });
791                }
792                this.model = new ForestStoreModel(modelParams);
793
794                // For backwards compatibility, the visibility of the root node is controlled by
795                // whether or not the user has specified a label
796                this.showRoot = Boolean(this.label);
797        },
798
799        onLoad: function(){
800                // summary:
801                //              Called when tree finishes loading and expanding.
802                // description:
803                //              If persist == true the loading may encompass many levels of fetches
804                //              from the data store, each asynchronous.   Waits for all to finish.
805                // tags:
806                //              callback
807        },
808
809        _load: function(){
810                // summary:
811                //              Initial load of the tree.
812                //              Load root node (possibly hidden) and it's children.
813                this.model.getRoot(
814                        lang.hitch(this, function(item){
815                                var rn = (this.rootNode = this.tree._createTreeNode({
816                                        item: item,
817                                        tree: this,
818                                        isExpandable: true,
819                                        label: this.label || this.getLabel(item),
820                                        textDir: this.textDir,
821                                        indent: this.showRoot ? 0 : -1
822                                }));
823                                if(!this.showRoot){
824                                        rn.rowNode.style.display="none";
825                                        // if root is not visible, move tree role to the invisible
826                                        // root node's containerNode, see #12135
827                                        this.domNode.setAttribute("role", "presentation");
828
829                                        rn.labelNode.setAttribute("role", "presentation");
830                                        rn.containerNode.setAttribute("role", "tree");
831                                }
832                                this.domNode.appendChild(rn.domNode);
833                                var identity = this.model.getIdentity(item);
834                                if(this._itemNodesMap[identity]){
835                                        this._itemNodesMap[identity].push(rn);
836                                }else{
837                                        this._itemNodesMap[identity] = [rn];
838                                }
839
840                                rn._updateLayout();             // sets "dijitTreeIsRoot" CSS classname
841
842                                // load top level children and then fire onLoad() event
843                                this._expandNode(rn).addCallback(lang.hitch(this, function(){
844                                        this._loadDeferred.callback(true);
845                                        this.onLoad();
846                                }));
847                        }),
848                        function(err){
849                                console.error(this, ": error loading root: ", err);
850                        }
851                );
852        },
853
854        getNodesByItem: function(/*Item or id*/ item){
855                // summary:
856                //              Returns all tree nodes that refer to an item
857                // returns:
858                //              Array of tree nodes that refer to passed item
859
860                if(!item){ return []; }
861                var identity = lang.isString(item) ? item : this.model.getIdentity(item);
862                // return a copy so widget don't get messed up by changes to returned array
863                return [].concat(this._itemNodesMap[identity]);
864        },
865
866        _setSelectedItemAttr: function(/*Item or id*/ item){
867                this.set('selectedItems', [item]);
868        },
869
870        _setSelectedItemsAttr: function(/*Items or ids*/ items){
871                // summary:
872                //              Select tree nodes related to passed items.
873                //              WARNING: if model use multi-parented items or desired tree node isn't already loaded
874                //              behavior is undefined. Use set('paths', ...) instead.
875                var tree = this;
876                this._loadDeferred.addCallback( lang.hitch(this, function(){
877                        var identities = array.map(items, function(item){
878                                return (!item || lang.isString(item)) ? item : tree.model.getIdentity(item);
879                        });
880                        var nodes = [];
881                        array.forEach(identities, function(id){
882                                nodes = nodes.concat(tree._itemNodesMap[id] || []);
883                        });
884                        this.set('selectedNodes', nodes);
885                }));
886        },
887
888        _setPathAttr: function(/*Item[] || String[]*/ path){
889                // summary:
890                //      Singular variant of _setPathsAttr
891                if(path.length){
892                        return this.set("paths", [path]);
893                }else{
894                        // Empty list is interpreted as "select nothing"
895                        return this.set("paths", []);
896                }
897        },
898
899        _setPathsAttr: function(/*Item[][] || String[][]*/ paths){
900                // summary:
901                //              Select the tree nodes identified by passed paths.
902                // paths:
903                //              Array of arrays of items or item id's
904                // returns:
905                //              Deferred to indicate when the set is complete
906                var tree = this;
907
908                // We may need to wait for some nodes to expand, so setting
909                // each path will involve a Deferred. We bring those deferreds
910                // together witha DeferredList.
911                return new DeferredList(array.map(paths, function(path){
912                        var d = new Deferred();
913
914                        // normalize path to use identity
915                        path = array.map(path, function(item){
916                                return lang.isString(item) ? item : tree.model.getIdentity(item);
917                        });
918
919                        if(path.length){
920                                // Wait for the tree to load, if it hasn't already.
921                                tree._loadDeferred.addCallback(function(){ selectPath(path, [tree.rootNode], d); });
922                        }else{
923                                d.errback("Empty path");
924                        }
925                        return d;
926                })).addCallback(setNodes);
927
928                function selectPath(path, nodes, def){
929                        // Traverse path; the next path component should be among "nodes".
930                        var nextPath = path.shift();
931                        var nextNode = array.filter(nodes, function(node){
932                                return node.getIdentity() == nextPath;
933                        })[0];
934                        if(!!nextNode){
935                                if(path.length){
936                                        tree._expandNode(nextNode).addCallback(function(){ selectPath(path, nextNode.getChildren(), def); });
937                                }else{
938                                        //Successfully reached the end of this path
939                                        def.callback(nextNode);
940                                }
941                        }else{
942                                def.errback("Could not expand path at " + nextPath);
943                        }
944                }
945
946                function setNodes(newNodes){
947                        //After all expansion is finished, set the selection to
948                        //the set of nodes successfully found.
949                        tree.set("selectedNodes", array.map(
950                                array.filter(newNodes,function(x){return x[0];}),
951                                function(x){return x[1];}));
952                }
953        },
954
955        _setSelectedNodeAttr: function(node){
956                this.set('selectedNodes', [node]);
957        },
958        _setSelectedNodesAttr: function(nodes){
959                this._loadDeferred.addCallback( lang.hitch(this, function(){
960                        this.dndController.setSelection(nodes);
961                }));
962        },
963
964
965        ////////////// Data store related functions //////////////////////
966        // These just get passed to the model; they are here for back-compat
967
968        mayHaveChildren: function(/*dojo.data.Item*/ /*===== item =====*/){
969                // summary:
970                //              Deprecated.   This should be specified on the model itself.
971                //
972                //              Overridable function to tell if an item has or may have children.
973                //              Controls whether or not +/- expando icon is shown.
974                //              (For efficiency reasons we may not want to check if an element actually
975                //              has children until user clicks the expando node)
976                // tags:
977                //              deprecated
978        },
979
980        getItemChildren: function(/*===== parentItem, onComplete =====*/){
981                // summary:
982                //              Deprecated.   This should be specified on the model itself.
983                //
984                //              Overridable function that return array of child items of given parent item,
985                //              or if parentItem==null then return top items in tree
986                // tags:
987                //              deprecated
988        },
989
990        ///////////////////////////////////////////////////////
991        // Functions for converting an item to a TreeNode
992        getLabel: function(/*dojo.data.Item*/ item){
993                // summary:
994                //              Overridable function to get the label for a tree node (given the item)
995                // tags:
996                //              extension
997                return this.model.getLabel(item);       // String
998        },
999
1000        getIconClass: function(/*dojo.data.Item*/ item, /*Boolean*/ opened){
1001                // summary:
1002                //              Overridable function to return CSS class name to display icon
1003                // tags:
1004                //              extension
1005                return (!item || this.model.mayHaveChildren(item)) ? (opened ? "dijitFolderOpened" : "dijitFolderClosed") : "dijitLeaf"
1006        },
1007
1008        getLabelClass: function(/*===== item, opened =====*/){
1009                // summary:
1010                //              Overridable function to return CSS class name to display label
1011                // item: dojo.data.Item
1012                // opened: Boolean
1013                // returns: String
1014                //              CSS class name
1015                // tags:
1016                //              extension
1017        },
1018
1019        getRowClass: function(/*===== item, opened =====*/){
1020                // summary:
1021                //              Overridable function to return CSS class name to display row
1022                // item: dojo.data.Item
1023                // opened: Boolean
1024                // returns: String
1025                //              CSS class name
1026                // tags:
1027                //              extension
1028        },
1029
1030        getIconStyle: function(/*===== item, opened =====*/){
1031                // summary:
1032                //              Overridable function to return CSS styles to display icon
1033                // item: dojo.data.Item
1034                // opened: Boolean
1035                // returns: Object
1036                //              Object suitable for input to dojo.style() like {backgroundImage: "url(...)"}
1037                // tags:
1038                //              extension
1039        },
1040
1041        getLabelStyle: function(/*===== item, opened =====*/){
1042                // summary:
1043                //              Overridable function to return CSS styles to display label
1044                // item: dojo.data.Item
1045                // opened: Boolean
1046                // returns:
1047                //              Object suitable for input to dojo.style() like {color: "red", background: "green"}
1048                // tags:
1049                //              extension
1050        },
1051
1052        getRowStyle: function(/*===== item, opened =====*/){
1053                // summary:
1054                //              Overridable function to return CSS styles to display row
1055                // item: dojo.data.Item
1056                // opened: Boolean
1057                // returns:
1058                //              Object suitable for input to dojo.style() like {background-color: "#bbb"}
1059                // tags:
1060                //              extension
1061        },
1062
1063        getTooltip: function(/*dojo.data.Item*/ /*===== item =====*/){
1064                // summary:
1065                //              Overridable function to get the tooltip for a tree node (given the item)
1066                // tags:
1067                //              extension
1068                return "";      // String
1069        },
1070
1071        /////////// Keyboard and Mouse handlers ////////////////////
1072
1073        _onKeyPress: function(/*Event*/ e){
1074                // summary:
1075                //              Translates keypress events into commands for the controller
1076                if(e.altKey){ return; }
1077                var treeNode = registry.getEnclosingWidget(e.target);
1078                if(!treeNode){ return; }
1079
1080                var key = e.charOrCode;
1081                if(typeof key == "string" && key != " "){       // handle printables (letter navigation)
1082                        // Check for key navigation.
1083                        if(!e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey){
1084                                this._onLetterKeyNav( { node: treeNode, key: key.toLowerCase() } );
1085                                event.stop(e);
1086                        }
1087                }else{  // handle non-printables (arrow keys)
1088                        // clear record of recent printables (being saved for multi-char letter navigation),
1089                        // because "a", down-arrow, "b" shouldn't search for "ab"
1090                        if(this._curSearch){
1091                                clearTimeout(this._curSearch.timer);
1092                                delete this._curSearch;
1093                        }
1094
1095                        var map = this._keyHandlerMap;
1096                        if(!map){
1097                                // setup table mapping keys to events
1098                                map = {};
1099                                map[keys.ENTER]="_onEnterKey";
1100                                //On WebKit based browsers, the combination ctrl-enter
1101                                //does not get passed through. To allow accessible
1102                                //multi-select on those browsers, the space key is
1103                                //also used for selection.
1104                                map[keys.SPACE]= map[" "] = "_onEnterKey";
1105                                map[this.isLeftToRight() ? keys.LEFT_ARROW : keys.RIGHT_ARROW]="_onLeftArrow";
1106                                map[this.isLeftToRight() ? keys.RIGHT_ARROW : keys.LEFT_ARROW]="_onRightArrow";
1107                                map[keys.UP_ARROW]="_onUpArrow";
1108                                map[keys.DOWN_ARROW]="_onDownArrow";
1109                                map[keys.HOME]="_onHomeKey";
1110                                map[keys.END]="_onEndKey";
1111                                this._keyHandlerMap = map;
1112                        }
1113                        if(this._keyHandlerMap[key]){
1114                                this[this._keyHandlerMap[key]]( { node: treeNode, item: treeNode.item, evt: e } );
1115                                event.stop(e);
1116                        }
1117                }
1118        },
1119
1120        _onEnterKey: function(/*Object*/ message){
1121                this._publish("execute", { item: message.item, node: message.node } );
1122                this.dndController.userSelect(message.node, connect.isCopyKey( message.evt ), message.evt.shiftKey);
1123                this.onClick(message.item, message.node, message.evt);
1124        },
1125
1126        _onDownArrow: function(/*Object*/ message){
1127                // summary:
1128                //              down arrow pressed; get next visible node, set focus there
1129                var node = this._getNextNode(message.node);
1130                if(node && node.isTreeNode){
1131                        this.focusNode(node);
1132                }
1133        },
1134
1135        _onUpArrow: function(/*Object*/ message){
1136                // summary:
1137                //              Up arrow pressed; move to previous visible node
1138
1139                var node = message.node;
1140
1141                // if younger siblings
1142                var previousSibling = node.getPreviousSibling();
1143                if(previousSibling){
1144                        node = previousSibling;
1145                        // if the previous node is expanded, dive in deep
1146                        while(node.isExpandable && node.isExpanded && node.hasChildren()){
1147                                // move to the last child
1148                                var children = node.getChildren();
1149                                node = children[children.length-1];
1150                        }
1151                }else{
1152                        // if this is the first child, return the parent
1153                        // unless the parent is the root of a tree with a hidden root
1154                        var parent = node.getParent();
1155                        if(!(!this.showRoot && parent === this.rootNode)){
1156                                node = parent;
1157                        }
1158                }
1159
1160                if(node && node.isTreeNode){
1161                        this.focusNode(node);
1162                }
1163        },
1164
1165        _onRightArrow: function(/*Object*/ message){
1166                // summary:
1167                //              Right arrow pressed; go to child node
1168                var node = message.node;
1169
1170                // if not expanded, expand, else move to 1st child
1171                if(node.isExpandable && !node.isExpanded){
1172                        this._expandNode(node);
1173                }else if(node.hasChildren()){
1174                        node = node.getChildren()[0];
1175                        if(node && node.isTreeNode){
1176                                this.focusNode(node);
1177                        }
1178                }
1179        },
1180
1181        _onLeftArrow: function(/*Object*/ message){
1182                // summary:
1183                //              Left arrow pressed.
1184                //              If not collapsed, collapse, else move to parent.
1185
1186                var node = message.node;
1187
1188                if(node.isExpandable && node.isExpanded){
1189                        this._collapseNode(node);
1190                }else{
1191                        var parent = node.getParent();
1192                        if(parent && parent.isTreeNode && !(!this.showRoot && parent === this.rootNode)){
1193                                this.focusNode(parent);
1194                        }
1195                }
1196        },
1197
1198        _onHomeKey: function(){
1199                // summary:
1200                //              Home key pressed; get first visible node, and set focus there
1201                var node = this._getRootOrFirstNode();
1202                if(node){
1203                        this.focusNode(node);
1204                }
1205        },
1206
1207        _onEndKey: function(){
1208                // summary:
1209                //              End key pressed; go to last visible node.
1210
1211                var node = this.rootNode;
1212                while(node.isExpanded){
1213                        var c = node.getChildren();
1214                        node = c[c.length - 1];
1215                }
1216
1217                if(node && node.isTreeNode){
1218                        this.focusNode(node);
1219                }
1220        },
1221
1222        // multiCharSearchDuration: Number
1223        //              If multiple characters are typed where each keystroke happens within
1224        //              multiCharSearchDuration of the previous keystroke,
1225        //              search for nodes matching all the keystrokes.
1226        //
1227        //              For example, typing "ab" will search for entries starting with
1228        //              "ab" unless the delay between "a" and "b" is greater than multiCharSearchDuration.
1229        multiCharSearchDuration: 250,
1230
1231        _onLetterKeyNav: function(message){
1232                // summary:
1233                //              Called when user presses a prinatable key; search for node starting with recently typed letters.
1234                // message: Object
1235                //              Like { node: TreeNode, key: 'a' } where key is the key the user pressed.
1236
1237                // Branch depending on whether this key starts a new search, or modifies an existing search
1238                var cs = this._curSearch;
1239                if(cs){
1240                        // We are continuing a search.  Ex: user has pressed 'a', and now has pressed
1241                        // 'b', so we want to search for nodes starting w/"ab".
1242                        cs.pattern = cs.pattern + message.key;
1243                        clearTimeout(cs.timer);
1244                }else{
1245                        // We are starting a new search
1246                        cs = this._curSearch = {
1247                                        pattern: message.key,
1248                                        startNode: message.node
1249                        };
1250                }
1251
1252                // set/reset timer to forget recent keystrokes
1253                var self = this;
1254                cs.timer = setTimeout(function(){
1255                        delete self._curSearch;
1256                }, this.multiCharSearchDuration);
1257
1258                // Navigate to TreeNode matching keystrokes [entered so far].
1259                var node = cs.startNode;
1260                do{
1261                        node = this._getNextNode(node);
1262                        //check for last node, jump to first node if necessary
1263                        if(!node){
1264                                node = this._getRootOrFirstNode();
1265                        }
1266                }while(node !== cs.startNode && (node.label.toLowerCase().substr(0, cs.pattern.length) != cs.pattern));
1267                if(node && node.isTreeNode){
1268                        // no need to set focus if back where we started
1269                        if(node !== cs.startNode){
1270                                this.focusNode(node);
1271                        }
1272                }
1273        },
1274
1275        isExpandoNode: function(node, widget){
1276                // summary:
1277                //              check whether a dom node is the expandoNode for a particular TreeNode widget
1278                return dom.isDescendant(node, widget.expandoNode);
1279        },
1280        _onClick: function(/*TreeNode*/ nodeWidget, /*Event*/ e){
1281                // summary:
1282                //              Translates click events into commands for the controller to process
1283
1284                var domElement = e.target,
1285                        isExpandoClick = this.isExpandoNode(domElement, nodeWidget);
1286
1287                if( (this.openOnClick && nodeWidget.isExpandable) || isExpandoClick ){
1288                        // expando node was clicked, or label of a folder node was clicked; open it
1289                        if(nodeWidget.isExpandable){
1290                                this._onExpandoClick({node:nodeWidget});
1291                        }
1292                }else{
1293                        this._publish("execute", { item: nodeWidget.item, node: nodeWidget, evt: e } );
1294                        this.onClick(nodeWidget.item, nodeWidget, e);
1295                        this.focusNode(nodeWidget);
1296                }
1297                event.stop(e);
1298        },
1299        _onDblClick: function(/*TreeNode*/ nodeWidget, /*Event*/ e){
1300                // summary:
1301                //              Translates double-click events into commands for the controller to process
1302
1303                var domElement = e.target,
1304                        isExpandoClick = (domElement == nodeWidget.expandoNode || domElement == nodeWidget.expandoNodeText);
1305
1306                if( (this.openOnDblClick && nodeWidget.isExpandable) ||isExpandoClick ){
1307                        // expando node was clicked, or label of a folder node was clicked; open it
1308                        if(nodeWidget.isExpandable){
1309                                this._onExpandoClick({node:nodeWidget});
1310                        }
1311                }else{
1312                        this._publish("execute", { item: nodeWidget.item, node: nodeWidget, evt: e } );
1313                        this.onDblClick(nodeWidget.item, nodeWidget, e);
1314                        this.focusNode(nodeWidget);
1315                }
1316                event.stop(e);
1317        },
1318
1319        _onExpandoClick: function(/*Object*/ message){
1320                // summary:
1321                //              User clicked the +/- icon; expand or collapse my children.
1322                var node = message.node;
1323
1324                // If we are collapsing, we might be hiding the currently focused node.
1325                // Also, clicking the expando node might have erased focus from the current node.
1326                // For simplicity's sake just focus on the node with the expando.
1327                this.focusNode(node);
1328
1329                if(node.isExpanded){
1330                        this._collapseNode(node);
1331                }else{
1332                        this._expandNode(node);
1333                }
1334        },
1335
1336        onClick: function(/*===== item, node, evt =====*/){
1337                // summary:
1338                //              Callback when a tree node is clicked
1339                // item: dojo.data.Item
1340                // node: TreeNode
1341                // evt: Event
1342                // tags:
1343                //              callback
1344        },
1345        onDblClick: function(/*===== item, node, evt =====*/){
1346                // summary:
1347                //              Callback when a tree node is double-clicked
1348                // item: dojo.data.Item
1349                // node: TreeNode
1350                // evt: Event
1351                // tags:
1352                //              callback
1353        },
1354        onOpen: function(/*===== item, node =====*/){
1355                // summary:
1356                //              Callback when a node is opened
1357                // item: dojo.data.Item
1358                // node: TreeNode
1359                // tags:
1360                //              callback
1361        },
1362        onClose: function(/*===== item, node =====*/){
1363                // summary:
1364                //              Callback when a node is closed
1365                // item: dojo.data.Item
1366                // node: TreeNode
1367                // tags:
1368                //              callback
1369        },
1370
1371        _getNextNode: function(node){
1372                // summary:
1373                //              Get next visible node
1374
1375                if(node.isExpandable && node.isExpanded && node.hasChildren()){
1376                        // if this is an expanded node, get the first child
1377                        return node.getChildren()[0];           // _TreeNode
1378                }else{
1379                        // find a parent node with a sibling
1380                        while(node && node.isTreeNode){
1381                                var returnNode = node.getNextSibling();
1382                                if(returnNode){
1383                                        return returnNode;              // _TreeNode
1384                                }
1385                                node = node.getParent();
1386                        }
1387                        return null;
1388                }
1389        },
1390
1391        _getRootOrFirstNode: function(){
1392                // summary:
1393                //              Get first visible node
1394                return this.showRoot ? this.rootNode : this.rootNode.getChildren()[0];
1395        },
1396
1397        _collapseNode: function(/*_TreeNode*/ node){
1398                // summary:
1399                //              Called when the user has requested to collapse the node
1400
1401                if(node._expandNodeDeferred){
1402                        delete node._expandNodeDeferred;
1403                }
1404
1405                if(node.isExpandable){
1406                        if(node.state == "LOADING"){
1407                                // ignore clicks while we are in the process of loading data
1408                                return;
1409                        }
1410
1411                        node.collapse();
1412                        this.onClose(node.item, node);
1413
1414                        this._state(node, false);
1415                }
1416        },
1417
1418        _expandNode: function(/*_TreeNode*/ node, /*Boolean?*/ recursive){
1419                // summary:
1420                //              Called when the user has requested to expand the node
1421                // recursive:
1422                //              Internal flag used when _expandNode() calls itself, don't set.
1423                // returns:
1424                //              Deferred that fires when the node is loaded and opened and (if persist=true) all it's descendants
1425                //              that were previously opened too
1426
1427                if(node._expandNodeDeferred && !recursive){
1428                        // there's already an expand in progress (or completed), so just return
1429                        return node._expandNodeDeferred;        // dojo.Deferred
1430                }
1431
1432                var model = this.model,
1433                        item = node.item,
1434                        _this = this;
1435
1436                switch(node.state){
1437                        case "UNCHECKED":
1438                                // need to load all the children, and then expand
1439                                node.markProcessing();
1440
1441                                // Setup deferred to signal when the load and expand are finished.
1442                                // Save that deferred in this._expandDeferred as a flag that operation is in progress.
1443                                var def = (node._expandNodeDeferred = new Deferred());
1444
1445                                // Get the children
1446                                model.getChildren(
1447                                        item,
1448                                        function(items){
1449                                                node.unmarkProcessing();
1450
1451                                                // Display the children and also start expanding any children that were previously expanded
1452                                                // (if this.persist == true).   The returned Deferred will fire when those expansions finish.
1453                                                var scid = node.setChildItems(items);
1454
1455                                                // Call _expandNode() again but this time it will just to do the animation (default branch).
1456                                                // The returned Deferred will fire when the animation completes.
1457                                                // TODO: seems like I can avoid recursion and just use a deferred to sequence the events?
1458                                                var ed = _this._expandNode(node, true);
1459
1460                                                // After the above two tasks (setChildItems() and recursive _expandNode()) finish,
1461                                                // signal that I am done.
1462                                                scid.addCallback(function(){
1463                                                        ed.addCallback(function(){
1464                                                                def.callback();
1465                                                        })
1466                                                });
1467                                        },
1468                                        function(err){
1469                                                console.error(_this, ": error loading root children: ", err);
1470                                        }
1471                                );
1472                                break;
1473
1474                        default:        // "LOADED"
1475                                // data is already loaded; just expand node
1476                                def = (node._expandNodeDeferred = node.expand());
1477
1478                                this.onOpen(node.item, node);
1479
1480                                this._state(node, true);
1481                }
1482
1483                return def;     // dojo.Deferred
1484        },
1485
1486        ////////////////// Miscellaneous functions ////////////////
1487
1488        focusNode: function(/* _tree.Node */ node){
1489                // summary:
1490                //              Focus on the specified node (which must be visible)
1491                // tags:
1492                //              protected
1493
1494                // set focus so that the label will be voiced using screen readers
1495                focus.focus(node.labelNode);
1496        },
1497
1498        _onNodeFocus: function(/*dijit._Widget*/ node){
1499                // summary:
1500                //              Called when a TreeNode gets focus, either by user clicking
1501                //              it, or programatically by arrow key handling code.
1502                // description:
1503                //              It marks that the current node is the selected one, and the previously
1504                //              selected node no longer is.
1505
1506                if(node && node != this.lastFocused){
1507                        if(this.lastFocused && !this.lastFocused._destroyed){
1508                                // mark that the previously focsable node is no longer focusable
1509                                this.lastFocused.setFocusable(false);
1510                        }
1511
1512                        // mark that the new node is the currently selected one
1513                        node.setFocusable(true);
1514                        this.lastFocused = node;
1515                }
1516        },
1517
1518        _onNodeMouseEnter: function(/*dijit._Widget*/ /*===== node =====*/){
1519                // summary:
1520                //              Called when mouse is over a node (onmouseenter event),
1521                //              this is monitored by the DND code
1522        },
1523
1524        _onNodeMouseLeave: function(/*dijit._Widget*/ /*===== node =====*/){
1525                // summary:
1526                //              Called when mouse leaves a node (onmouseleave event),
1527                //              this is monitored by the DND code
1528        },
1529
1530        //////////////// Events from the model //////////////////////////
1531
1532        _onItemChange: function(/*Item*/ item){
1533                // summary:
1534                //              Processes notification of a change to an item's scalar values like label
1535                var model = this.model,
1536                        identity = model.getIdentity(item),
1537                        nodes = this._itemNodesMap[identity];
1538
1539                if(nodes){
1540                        var label = this.getLabel(item),
1541                                tooltip = this.getTooltip(item);
1542                        array.forEach(nodes, function(node){
1543                                node.set({
1544                                        item: item,             // theoretically could be new JS Object representing same item
1545                                        label: label,
1546                                        tooltip: tooltip
1547                                });
1548                                node._updateItemClasses(item);
1549                        });
1550                }
1551        },
1552
1553        _onItemChildrenChange: function(/*dojo.data.Item*/ parent, /*dojo.data.Item[]*/ newChildrenList){
1554                // summary:
1555                //              Processes notification of a change to an item's children
1556                var model = this.model,
1557                        identity = model.getIdentity(parent),
1558                        parentNodes = this._itemNodesMap[identity];
1559
1560                if(parentNodes){
1561                        array.forEach(parentNodes,function(parentNode){
1562                                parentNode.setChildItems(newChildrenList);
1563                        });
1564                }
1565        },
1566
1567        _onItemDelete: function(/*Item*/ item){
1568                // summary:
1569                //              Processes notification of a deletion of an item
1570                var model = this.model,
1571                        identity = model.getIdentity(item),
1572                        nodes = this._itemNodesMap[identity];
1573
1574                if(nodes){
1575                        array.forEach(nodes,function(node){
1576                                // Remove node from set of selected nodes (if it's selected)
1577                                this.dndController.removeTreeNode(node);
1578
1579                                var parent = node.getParent();
1580                                if(parent){
1581                                        // if node has not already been orphaned from a _onSetItem(parent, "children", ..) call...
1582                                        parent.removeChild(node);
1583                                }
1584                                node.destroyRecursive();
1585                        }, this);
1586                        delete this._itemNodesMap[identity];
1587                }
1588        },
1589
1590        /////////////// Miscellaneous funcs
1591
1592        _initState: function(){
1593                // summary:
1594                //              Load in which nodes should be opened automatically
1595                this._openedNodes = {};
1596                if(this.persist && this.cookieName){
1597                        var oreo = cookie(this.cookieName);
1598                        if(oreo){
1599                                array.forEach(oreo.split(','), function(item){
1600                                        this._openedNodes[item] = true;
1601                                }, this);
1602                        }
1603                }
1604        },
1605        _state: function(node, expanded){
1606                // summary:
1607                //              Query or set expanded state for an node
1608                if(!this.persist){
1609                        return false;
1610                }
1611                var path = array.map(node.getTreePath(), function(item){
1612                                return this.model.getIdentity(item);
1613                        }, this).join("/");
1614                if(arguments.length === 1){
1615                        return this._openedNodes[path];
1616                }else{
1617                        if(expanded){
1618                                this._openedNodes[path] = true;
1619                        }else{
1620                                delete this._openedNodes[path];
1621                        }
1622                        var ary = [];
1623                        for(var id in this._openedNodes){
1624                                ary.push(id);
1625                        }
1626                        cookie(this.cookieName, ary.join(","), {expires:365});
1627                }
1628        },
1629
1630        destroy: function(){
1631                if(this._curSearch){
1632                        clearTimeout(this._curSearch.timer);
1633                        delete this._curSearch;
1634                }
1635                if(this.rootNode){
1636                        this.rootNode.destroyRecursive();
1637                }
1638                if(this.dndController && !lang.isString(this.dndController)){
1639                        this.dndController.destroy();
1640                }
1641                this.rootNode = null;
1642                this.inherited(arguments);
1643        },
1644
1645        destroyRecursive: function(){
1646                // A tree is treated as a leaf, not as a node with children (like a grid),
1647                // but defining destroyRecursive for back-compat.
1648                this.destroy();
1649        },
1650
1651        resize: function(changeSize){
1652                if(changeSize){
1653                        domGeometry.setMarginBox(this.domNode, changeSize);
1654                }
1655
1656                // The only JS sizing involved w/tree is the indentation, which is specified
1657                // in CSS and read in through this dummy indentDetector node (tree must be
1658                // visible and attached to the DOM to read this)
1659                this._nodePixelIndent = domGeometry.position(this.tree.indentDetector).w;
1660
1661                if(this.tree.rootNode){
1662                        // If tree has already loaded, then reset indent for all the nodes
1663                        this.tree.rootNode.set('indent', this.showRoot ? 0 : -1);
1664                }
1665        },
1666
1667        _createTreeNode: function(/*Object*/ args){
1668                // summary:
1669                //              creates a TreeNode
1670                // description:
1671                //              Developers can override this method to define their own TreeNode class;
1672                //              However it will probably be removed in a future release in favor of a way
1673                //              of just specifying a widget for the label, rather than one that contains
1674                //              the children too.
1675                return new TreeNode(args);
1676        },
1677
1678        _setTextDirAttr: function(textDir){
1679                if(textDir && this.textDir!= textDir){
1680                        this._set("textDir",textDir);
1681                        this.rootNode.set("textDir", textDir);
1682                }
1683        }
1684});
1685
1686Tree._TreeNode = TreeNode;      // for monkey patching
1687
1688return Tree;
1689});
Note: See TracBrowser for help on using the repository browser.