source: Dev/trunk/src/client/dijit/Tree.js @ 487

Last change on this file since 487 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

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