src/cm/media/js/lib/yui/yui3-3.15.0/build/tree/tree.js
changeset 602 e16a97fb364a
equal deleted inserted replaced
601:d334a616c023 602:e16a97fb364a
       
     1 YUI.add('tree', function (Y, NAME) {
       
     2 
       
     3 /*jshint boss:true, expr:true, onevar:false */
       
     4 
       
     5 /**
       
     6 Provides a generic tree data structure and related functionality.
       
     7 
       
     8 A tree has a root node, which may contain any number of child nodes, which may
       
     9 themselves contain child nodes, ad infinitum.
       
    10 
       
    11 Child nodes are lightweight function instances which delegate to the tree for
       
    12 all significant functionality, so trees remain performant and memory-efficient
       
    13 even with thousands and thousands of nodes.
       
    14 
       
    15 @module tree
       
    16 @main tree
       
    17 **/
       
    18 
       
    19 /**
       
    20 The `Tree` class represents a generic tree data structure. A tree has a root
       
    21 node, which may contain any number of child nodes, which may themselves contain
       
    22 child nodes, ad infinitum.
       
    23 
       
    24 This class doesn't expose any UI, but is intended to be used as a data structure
       
    25 or base class for other components. For example, the SmugMug TreeView gallery
       
    26 module extends Tree and provides a TreeView UI.
       
    27 
       
    28 @class Tree
       
    29 @param {Object} [config] Config options.
       
    30     @param {Object[]|Tree.Node[]} [config.nodes] Array of tree node config
       
    31         objects or `Tree.Node` instances to add to this tree at initialization
       
    32         time.
       
    33     @param {Object|Tree.Node} [config.rootNode] Node to use as the root node of
       
    34         this tree.
       
    35 @constructor
       
    36 @extends Base
       
    37 **/
       
    38 
       
    39 var Lang = Y.Lang,
       
    40 
       
    41     /**
       
    42     Fired when a node is added to this Tree. The `src` property will indicate
       
    43     how the node was added ("append", "insert", "prepend", etc.).
       
    44 
       
    45     @event add
       
    46     @param {Number} index Index at which the node will be added.
       
    47     @param {Tree.Node} node Node being added.
       
    48     @param {Tree.Node} parent Parent node to which the node will be added.
       
    49     @param {String} src Source of the event ("append", "insert", "prepend",
       
    50         etc.).
       
    51     @preventable _defAddFn
       
    52     **/
       
    53     EVT_ADD = 'add',
       
    54 
       
    55     /**
       
    56     Fired when this Tree is cleared.
       
    57 
       
    58     @event clear
       
    59     @param {Tree.Node} rootNode New root node of this tree (the old root node is
       
    60         always destroyed when a tree is cleared).
       
    61     @param {String} src Source of the event.
       
    62     @preventable _defClearFn
       
    63     **/
       
    64     EVT_CLEAR = 'clear',
       
    65 
       
    66     /**
       
    67     Fired when a node is removed from this Tree.
       
    68 
       
    69     @event remove
       
    70     @param {Boolean} destroy Whether or not the node will be destroyed after
       
    71         being removed from this tree.
       
    72     @param {Tree.Node} node Node being removed.
       
    73     @param {Tree.Node} parent Parent node from which the node will be removed.
       
    74     @param {String} src Source of the event.
       
    75     @preventable _defRemoveFn
       
    76     **/
       
    77     EVT_REMOVE = 'remove';
       
    78 
       
    79 var Tree = Y.Base.create('tree', Y.Base, [], {
       
    80     // -- Public Properties ----------------------------------------------------
       
    81 
       
    82     /**
       
    83     Reference to the `children` array of this Tree's `rootNode`.
       
    84 
       
    85     This is a convenience property to allow you to type `tree.children` instead
       
    86     of `tree.rootNode.children`.
       
    87 
       
    88     @property {Tree.Node[]} children
       
    89     @readOnly
       
    90     **/
       
    91 
       
    92     /**
       
    93     The `Tree.Node` class or subclass that should be used for nodes created by
       
    94     this tree.
       
    95 
       
    96     You may specify an actual class reference or a string that resolves to a
       
    97     class reference at runtime.
       
    98 
       
    99     @property {String|Tree.Node} nodeClass
       
   100     @default Y.Tree.Node
       
   101     **/
       
   102     nodeClass: Y.Tree.Node,
       
   103 
       
   104     /**
       
   105     Optional array containing one or more extension classes that should be mixed
       
   106     into the `nodeClass` when this Tree is instantiated. The resulting composed
       
   107     node class will be unique to this Tree instance and will not affect any
       
   108     other instances, nor will it modify the defined `nodeClass` itself.
       
   109 
       
   110     This provides a late-binding extension mechanism for nodes that doesn't
       
   111     require them to extend `Y.Base`, which would incur a significant performance
       
   112     hit.
       
   113 
       
   114     @property {Array} nodeExtensions
       
   115     @default []
       
   116     **/
       
   117     nodeExtensions: [],
       
   118 
       
   119     /**
       
   120     Root node of this Tree.
       
   121 
       
   122     @property {Tree.Node} rootNode
       
   123     @readOnly
       
   124     **/
       
   125 
       
   126     // -- Protected Properties -------------------------------------------------
       
   127 
       
   128     /**
       
   129     Simple way to type-check that this is a Tree instance.
       
   130 
       
   131     @property {Boolean} _isYUITree
       
   132     @default true
       
   133     @protected
       
   134     **/
       
   135     _isYUITree: true,
       
   136 
       
   137     /**
       
   138     Composed node class based on `nodeClass` that mixes in any extensions
       
   139     specified in `nodeExtensions`. If there are no extensions, this will just be
       
   140     a reference to `nodeClass`.
       
   141 
       
   142     @property {Tree.Node} _nodeClass
       
   143     @protected
       
   144     **/
       
   145 
       
   146     /**
       
   147     Mapping of node ids to node instances for nodes in this tree.
       
   148 
       
   149     @property {Object} _nodeMap
       
   150     @protected
       
   151     **/
       
   152 
       
   153     /**
       
   154     Default config object for the root node.
       
   155 
       
   156     @property {Object} _rootNodeConfig
       
   157     @protected
       
   158     **/
       
   159     _rootNodeConfig: {canHaveChildren: true},
       
   160 
       
   161     // -- Lifecycle ------------------------------------------------------------
       
   162     initializer: function (config) {
       
   163         config || (config = {});
       
   164 
       
   165         if (config.nodeClass) {
       
   166             this.nodeClass = config.nodeClass;
       
   167         }
       
   168 
       
   169         if (config.nodeExtensions) {
       
   170             this.nodeExtensions = this.nodeExtensions.concat(config.nodeExtensions);
       
   171         }
       
   172 
       
   173         /**
       
   174         Hash of published custom events.
       
   175 
       
   176         @property {Object} _published
       
   177         @default {}
       
   178         @protected
       
   179         **/
       
   180         this._published || (this._published = {});
       
   181         this._nodeMap = {};
       
   182 
       
   183         // Allow all extensions to initialize, then finish up.
       
   184         this.onceAfter('initializedChange', function () {
       
   185             this._composeNodeClass();
       
   186 
       
   187             this.clear(config.rootNode, {silent: true});
       
   188 
       
   189             if (config.nodes) {
       
   190                 this.insertNode(this.rootNode, config.nodes, {silent: true});
       
   191             }
       
   192         });
       
   193     },
       
   194 
       
   195     destructor: function () {
       
   196         this.destroyNode(this.rootNode, {silent: true});
       
   197 
       
   198         this.children   = null;
       
   199         this.rootNode   = null;
       
   200         this._nodeClass = null;
       
   201         this._nodeMap   = null;
       
   202         this._published = null;
       
   203     },
       
   204 
       
   205     // -- Public Methods -------------------------------------------------------
       
   206 
       
   207     /**
       
   208     Appends a node or array of nodes as the last child of the specified parent
       
   209     node.
       
   210 
       
   211     If a node being appended is from another tree, it and all its children will
       
   212     be removed from that tree and moved to this one.
       
   213 
       
   214     @method appendNode
       
   215     @param {Tree.Node} parent Parent node.
       
   216     @param {Object|Object[]|Tree.Node|Tree.Node[]} node Child node, node config
       
   217         object, array of child nodes, or array of node config objects to append
       
   218         to the given parent. Node config objects will automatically be converted
       
   219         into node instances.
       
   220     @param {Object} [options] Options.
       
   221         @param {Boolean} [options.silent=false] If `true`, the `add` event will
       
   222             be suppressed.
       
   223     @return {Tree.Node|Tree.Node[]} Node or array of nodes that were
       
   224         appended.
       
   225     **/
       
   226     appendNode: function (parent, node, options) {
       
   227         return this.insertNode(parent, node, Y.merge(options, {
       
   228             index: parent.children.length,
       
   229             src  : 'append'
       
   230         }));
       
   231     },
       
   232 
       
   233     /**
       
   234     Clears this tree by destroying the root node and all its children. If a
       
   235     `rootNode` argument is provided, that node will become the root node of this
       
   236     tree; otherwise, a new root node will be created.
       
   237 
       
   238     @method clear
       
   239     @param {Object|Tree.Node} [rootNode] If specified, this node will be used as
       
   240         the new root node.
       
   241     @param {Object} [options] Options.
       
   242         @param {Boolean} [options.silent=false] If `true`, the `clear` event
       
   243             will be suppressed.
       
   244         @param {String} [options.src] Source of the change, to be passed along
       
   245             to the event facade of the resulting event. This can be used to
       
   246             distinguish between changes triggered by a user and changes
       
   247             triggered programmatically, for example.
       
   248     @chainable
       
   249     **/
       
   250     clear: function (rootNode, options) {
       
   251         return this._fireTreeEvent(EVT_CLEAR, {
       
   252             rootNode: this.createNode(rootNode || this._rootNodeConfig),
       
   253             src     : options && options.src
       
   254         }, {
       
   255             defaultFn: this._defClearFn,
       
   256             silent   : options && options.silent
       
   257         });
       
   258     },
       
   259 
       
   260     /**
       
   261     Creates and returns a new `Tree.Node` instance associated with (but not
       
   262     yet appended to) this tree.
       
   263 
       
   264     @method createNode
       
   265     @param {Object|Tree.Node} [config] Node configuration. If a `Tree.Node`
       
   266         instance is specified instead of a config object, that node will be
       
   267         adopted into this tree (if it doesn't already belong to this tree) and
       
   268         removed from any other tree to which it belongs.
       
   269     @return {Tree.Node|null} New node, or `null` if a node could not be created
       
   270         from the given _config_.
       
   271     **/
       
   272     createNode: function (config) {
       
   273         config || (config = {});
       
   274 
       
   275         // If `config` is already a node, just ensure it hasn't been destroyed
       
   276         // and is in the node map, then return it.
       
   277         if (config._isYUITreeNode) {
       
   278             if (config.state.destroyed) {
       
   279                 Y.error('Cannot insert a node that has already been destroyed.', null, 'tree');
       
   280                 return null;
       
   281             }
       
   282 
       
   283             this._adoptNode(config);
       
   284             return config;
       
   285         }
       
   286 
       
   287         // First, create nodes for any children of this node.
       
   288         if (config.children) {
       
   289             var children = [];
       
   290 
       
   291             for (var i = 0, len = config.children.length; i < len; i++) {
       
   292                 children.push(this.createNode(config.children[i]));
       
   293             }
       
   294 
       
   295             config = Y.merge(config, {children: children});
       
   296         }
       
   297 
       
   298         var node = new this._nodeClass(this, config);
       
   299 
       
   300         return this._nodeMap[node.id] = node;
       
   301     },
       
   302 
       
   303     /**
       
   304     Removes and destroys a node and all its child nodes. Once destroyed, a node
       
   305     is eligible for garbage collection and cannot be reused or re-added to the
       
   306     tree.
       
   307 
       
   308     @method destroyNode
       
   309     @param {Tree.Node} node Node to destroy.
       
   310     @param {Object} [options] Options.
       
   311         @param {Boolean} [options.silent=false] If `true`, `remove` events will
       
   312             be suppressed.
       
   313         @param {String} [options.src] Source of the change, to be passed along
       
   314             to the event facade of the resulting events. This can be used to
       
   315             distinguish between changes triggered by a user and changes
       
   316             triggered programmatically, for example.
       
   317     @chainable
       
   318     **/
       
   319     destroyNode: function (node, options) {
       
   320         var child, i, len;
       
   321 
       
   322         options || (options = {});
       
   323 
       
   324         for (i = 0, len = node.children.length; i < len; i++) {
       
   325             child = node.children[i];
       
   326 
       
   327             // Manually remove the child from its parent; this makes destroying
       
   328             // all children of the parent much faster since there's no splicing
       
   329             // involved.
       
   330             child.parent = null;
       
   331 
       
   332             // Destroy the child.
       
   333             this.destroyNode(child, options);
       
   334         }
       
   335 
       
   336         if (node.parent) {
       
   337             this.removeNode(node, options);
       
   338         }
       
   339 
       
   340         node.children  = [];
       
   341         node.data      = {};
       
   342         node.state     = {destroyed: true};
       
   343         node.tree      = null;
       
   344         node._indexMap = {};
       
   345 
       
   346         delete this._nodeMap[node.id];
       
   347 
       
   348         return this;
       
   349     },
       
   350 
       
   351     /**
       
   352     Removes all children from the specified node. The removed children will
       
   353     still be reusable unless the `destroy` option is truthy.
       
   354 
       
   355     @method emptyNode
       
   356     @param {Tree.Node} node Node to empty.
       
   357     @param {Object} [options] Options.
       
   358         @param {Boolean} [options.destroy=false] If `true`, the children will
       
   359             also be destroyed, which makes them available for garbage collection
       
   360             and means they can't be reused.
       
   361         @param {Boolean} [options.silent=false] If `true`, `remove` events will
       
   362             be suppressed.
       
   363         @param {String} [options.src] Source of the change, to be passed along
       
   364             to the event facade of the resulting events. This can be used to
       
   365             distinguish between changes triggered by a user and changes
       
   366             triggered programmatically, for example.
       
   367     @return {Tree.Node[]} Array of removed child nodes.
       
   368     **/
       
   369     emptyNode: function (node, options) {
       
   370         var children = node.children,
       
   371             removed  = [];
       
   372 
       
   373         for (var i = children.length - 1; i > -1; --i) {
       
   374             removed[i] = this.removeNode(children[i], options);
       
   375         }
       
   376 
       
   377         return removed;
       
   378     },
       
   379 
       
   380     /**
       
   381     Performs a depth-first traversal of _node_, passing it and each of its
       
   382     descendants to the specified _callback_, and returning the first node for
       
   383     which the callback returns a truthy value.
       
   384 
       
   385     Traversal will stop as soon as a truthy value is returned from the callback.
       
   386 
       
   387     See `traverseNode()` for more details on how depth-first traversal works.
       
   388 
       
   389     @method findNode
       
   390     @param {Tree.Node} node Node to traverse.
       
   391     @param {Object} [options] Options.
       
   392         @param {Number} [options.depth] Depth limit. If specified, descendants
       
   393             will only be traversed to this depth before backtracking and moving
       
   394             on.
       
   395     @param {Function} callback Callback function to call with the traversed
       
   396         node and each of its descendants. If this function returns a truthy
       
   397         value, traversal will be stopped and the current node will be returned.
       
   398 
       
   399         @param {Tree.Node} callback.node Node being traversed.
       
   400 
       
   401     @param {Object} [thisObj] `this` object to use when executing _callback_.
       
   402     @return {Tree.Node|null} Returns the first node for which the _callback_
       
   403         returns a truthy value, or `null` if the callback never returns a truthy
       
   404         value.
       
   405     **/
       
   406     findNode: function (node, options, callback, thisObj) {
       
   407         var match = null;
       
   408 
       
   409         // Allow callback as second argument.
       
   410         if (typeof options === 'function') {
       
   411             thisObj  = callback;
       
   412             callback = options;
       
   413             options  = {};
       
   414         }
       
   415 
       
   416         this.traverseNode(node, options, function (descendant) {
       
   417             if (callback.call(thisObj, descendant)) {
       
   418                 match = descendant;
       
   419                 return Tree.STOP_TRAVERSAL;
       
   420             }
       
   421         });
       
   422 
       
   423         return match;
       
   424     },
       
   425 
       
   426     /**
       
   427     Returns the tree node with the specified id, or `undefined` if the node
       
   428     doesn't exist in this tree.
       
   429 
       
   430     @method getNodeById
       
   431     @param {String} id Node id.
       
   432     @return {Tree.Node} Node, or `undefined` if not found.
       
   433     **/
       
   434     getNodeById: function (id) {
       
   435         return this._nodeMap[id];
       
   436     },
       
   437 
       
   438     /**
       
   439     Inserts a node or array of nodes at the specified index under the given
       
   440     parent node, or appends them to the parent if no index is specified.
       
   441 
       
   442     If a node being inserted is from another tree, it and all its children will
       
   443     be removed from that tree and moved to this one.
       
   444 
       
   445     @method insertNode
       
   446     @param {Tree.Node} parent Parent node.
       
   447     @param {Object|Object[]|Tree.Node|Tree.Node[]} node Child node, node config
       
   448         object, array of child nodes, or array of node config objects to insert
       
   449         under the given parent. Node config objects will automatically be
       
   450         converted into node instances.
       
   451 
       
   452     @param {Object} [options] Options.
       
   453         @param {Number} [options.index] Index at which to insert the child node.
       
   454             If not specified, the node will be appended as the last child of the
       
   455             parent.
       
   456         @param {Boolean} [options.silent=false] If `true`, the `add` event will
       
   457             be suppressed.
       
   458         @param {String} [options.src='insert'] Source of the change, to be
       
   459             passed along to the event facade of the resulting event. This can be
       
   460             used to distinguish between changes triggered by a user and changes
       
   461             triggered programmatically, for example.
       
   462 
       
   463     @return {Tree.Node|Tree.Node[]} Node or array of nodes that were inserted.
       
   464     **/
       
   465     insertNode: function (parent, node, options) {
       
   466         options || (options = {});
       
   467         parent  || (parent = this.rootNode);
       
   468 
       
   469         // If `node` is an array, recurse to insert each node it contains.
       
   470         //
       
   471         // Note: If you're getting an exception here because `node` is null when
       
   472         // you've passed in a reference to some other node's `children` array,
       
   473         // that's happening because nodes must be removed from their current
       
   474         // parent before being added to the new one, and the `children` array is
       
   475         // being modified while the nodes are inserted.
       
   476         //
       
   477         // Solution: pass a copy of the other node's `children` array instead of
       
   478         // the original. Doing the copy operation here would have a negative
       
   479         // impact on performance, so you're on your own since this is such a
       
   480         // rare edge case.
       
   481         if ('length' in node && Lang.isArray(node)) {
       
   482             var hasIndex      = 'index' in options,
       
   483                 insertedNodes = [],
       
   484                 insertedNode;
       
   485 
       
   486             for (var i = 0, len = node.length; i < len; i++) {
       
   487                 insertedNode = this.insertNode(parent, node[i], options);
       
   488 
       
   489                 if (insertedNode) {
       
   490                     insertedNodes.push(insertedNode);
       
   491 
       
   492                     if (hasIndex) {
       
   493                         options.index += 1;
       
   494                     }
       
   495                 }
       
   496             }
       
   497 
       
   498             return insertedNodes;
       
   499         }
       
   500 
       
   501         node = this.createNode(node);
       
   502 
       
   503         if (node) {
       
   504             var index = options.index;
       
   505 
       
   506             if (typeof index === 'undefined') {
       
   507                 index = this._getDefaultNodeIndex(parent, node, options);
       
   508             }
       
   509 
       
   510             this._fireTreeEvent(EVT_ADD, {
       
   511                 index : index,
       
   512                 node  : node,
       
   513                 parent: parent,
       
   514                 src   : options.src || 'insert'
       
   515             }, {
       
   516                 defaultFn: this._defAddFn,
       
   517                 silent   : options.silent
       
   518             });
       
   519         }
       
   520 
       
   521         return node;
       
   522     },
       
   523 
       
   524     /**
       
   525     Prepends a node or array of nodes at the beginning of the specified parent
       
   526     node.
       
   527 
       
   528     If a node being prepended is from another tree, it and all its children will
       
   529     be removed from that tree and moved to this one.
       
   530 
       
   531     @method prependNode
       
   532     @param {Tree.Node} parent Parent node.
       
   533     @param {Object|Object[]|Tree.Node|Tree.Node[]} node Child node,
       
   534         node config object, array of child nodes, or array of node config
       
   535         objects to prepend to the given parent. Node config objects will
       
   536         automatically be converted into node instances.
       
   537     @param {Object} [options] Options.
       
   538         @param {Boolean} [options.silent=false] If `true`, the `add` event will
       
   539             be suppressed.
       
   540     @return {Tree.Node|Tree.Node[]} Node or array of nodes that were
       
   541         prepended.
       
   542     **/
       
   543     prependNode: function (parent, node, options) {
       
   544         return this.insertNode(parent, node, Y.merge(options, {
       
   545             index: 0,
       
   546             src  : 'prepend'
       
   547         }));
       
   548     },
       
   549 
       
   550     /**
       
   551     Removes the specified node from its parent node. The removed node will still
       
   552     be reusable unless the `destroy` option is truthy.
       
   553 
       
   554     @method removeNode
       
   555     @param {Tree.Node} node Node to remove.
       
   556     @param {Object} [options] Options.
       
   557         @param {Boolean} [options.destroy=false] If `true`, the node and all its
       
   558             children will also be destroyed, which makes them available for
       
   559             garbage collection and means they can't be reused.
       
   560         @param {Boolean} [options.silent=false] If `true`, the `remove` event
       
   561             will be suppressed.
       
   562         @param {String} [options.src] Source of the change, to be passed along
       
   563             to the event facade of the resulting event. This can be used to
       
   564             distinguish between changes triggered by a user and changes
       
   565             triggered programmatically, for example.
       
   566     @return {Tree.Node} Node that was removed.
       
   567     **/
       
   568     removeNode: function (node, options) {
       
   569         options || (options = {});
       
   570 
       
   571         this._fireTreeEvent(EVT_REMOVE, {
       
   572             destroy: !!options.destroy,
       
   573             node   : node,
       
   574             parent : node.parent,
       
   575             src    : options.src || 'remove'
       
   576         }, {
       
   577             defaultFn: this._defRemoveFn,
       
   578             silent   : options.silent
       
   579         });
       
   580 
       
   581         return node;
       
   582     },
       
   583 
       
   584     /**
       
   585     Returns the total number of nodes in this tree, at all levels.
       
   586 
       
   587     Use `rootNode.children.length` to get only the number of top-level nodes.
       
   588 
       
   589     @method size
       
   590     @return {Number} Total number of nodes in this tree.
       
   591     **/
       
   592     size: function () {
       
   593         return this.rootNode.size() + 1;
       
   594     },
       
   595 
       
   596     /**
       
   597     Serializes this tree to an object suitable for use in JSON.
       
   598 
       
   599     @method toJSON
       
   600     @return {Object} Serialized tree object.
       
   601     **/
       
   602     toJSON: function () {
       
   603         return this.rootNode.toJSON();
       
   604     },
       
   605 
       
   606     /**
       
   607     Performs a depth-first traversal of _node_, passing it and each of its
       
   608     descendants to the specified _callback_.
       
   609 
       
   610     If the callback function returns `Tree.STOP_TRAVERSAL`, traversal will be
       
   611     stopped immediately. Otherwise, it will continue until the deepest
       
   612     descendant of _node_ has been traversed, or until each branch has been
       
   613     traversed to the optional maximum depth limit.
       
   614 
       
   615     Since traversal is depth-first, that means nodes are traversed like this:
       
   616 
       
   617                 1
       
   618               / | \
       
   619              2  8  9
       
   620             / \     \
       
   621            3   7    10
       
   622          / | \      / \
       
   623         4  5  6    11 12
       
   624 
       
   625     @method traverseNode
       
   626     @param {Tree.Node} node Node to traverse.
       
   627     @param {Object} [options] Options.
       
   628         @param {Number} [options.depth] Depth limit. If specified, descendants
       
   629             will only be traversed to this depth before backtracking and moving
       
   630             on.
       
   631     @param {Function} callback Callback function to call with the traversed
       
   632         node and each of its descendants.
       
   633 
       
   634         @param {Tree.Node} callback.node Node being traversed.
       
   635 
       
   636     @param {Object} [thisObj] `this` object to use when executing _callback_.
       
   637     @return {Mixed} Returns `Tree.STOP_TRAVERSAL` if traversal was stopped;
       
   638         otherwise returns `undefined`.
       
   639     **/
       
   640     traverseNode: function (node, options, callback, thisObj) {
       
   641         if (node.state.destroyed) {
       
   642             Y.error('Cannot traverse a node that has been destroyed.', null, 'tree');
       
   643             return;
       
   644         }
       
   645 
       
   646         // Allow callback as second argument.
       
   647         if (typeof options === 'function') {
       
   648             thisObj  = callback;
       
   649             callback = options;
       
   650             options  = {};
       
   651         }
       
   652 
       
   653         options || (options = {});
       
   654 
       
   655         var stop      = Tree.STOP_TRAVERSAL,
       
   656             unlimited = typeof options.depth === 'undefined';
       
   657 
       
   658         if (callback.call(thisObj, node) === stop) {
       
   659             return stop;
       
   660         }
       
   661 
       
   662         var children = node.children;
       
   663 
       
   664         if (unlimited || options.depth > 0) {
       
   665             var childOptions = unlimited ? options : {depth: options.depth - 1};
       
   666 
       
   667             for (var i = 0, len = children.length; i < len; i++) {
       
   668                 if (this.traverseNode(children[i], childOptions, callback, thisObj) === stop) {
       
   669                     return stop;
       
   670                 }
       
   671             }
       
   672         }
       
   673     },
       
   674 
       
   675     // -- Protected Methods ----------------------------------------------------
       
   676 
       
   677     /**
       
   678     Moves the specified node and all its children from another tree to this
       
   679     tree.
       
   680 
       
   681     @method _adoptNode
       
   682     @param {Tree.Node} node Node to adopt.
       
   683     @param {Object} [options] Options to pass along to `removeNode()`.
       
   684     @protected
       
   685     **/
       
   686     _adoptNode: function (node, options) {
       
   687         var oldTree = node.tree;
       
   688 
       
   689         if (oldTree === this) {
       
   690             return;
       
   691         }
       
   692 
       
   693         for (var i = 0, len = node.children.length; i < len; i++) {
       
   694             this._adoptNode(node.children[i], {silent: true});
       
   695         }
       
   696 
       
   697         oldTree.removeNode(node, options);
       
   698         delete oldTree._nodeMap[node.id];
       
   699 
       
   700         // If this node isn't an instance of this tree's composed _nodeClass,
       
   701         // then we need to recreate it to avoid potentially breaking things in
       
   702         // really weird ways.
       
   703         if (!(node instanceof this._nodeClass)
       
   704                 || oldTree._nodeClass !== this._nodeClass) {
       
   705 
       
   706             node = this.createNode(node.toJSON());
       
   707         }
       
   708 
       
   709         node.tree = this;
       
   710         this._nodeMap[node.id] = node;
       
   711     },
       
   712 
       
   713     /**
       
   714     Composes a custom late-bound tree node class (if necessary) based on the
       
   715     classes specified in this Tree's `nodeClass` and `nodeExtensions`
       
   716     properties.
       
   717 
       
   718     The composed class is stored in this Tree's `_nodeClass` property. If
       
   719     composition wasn't necessary, then `_nodeClass` will just be a reference to
       
   720     `nodeClass`.
       
   721 
       
   722     @method _composeNodeClass
       
   723     @protected
       
   724     **/
       
   725     _composeNodeClass: function () {
       
   726         var nodeClass      = this.nodeClass,
       
   727             nodeExtensions = this.nodeExtensions,
       
   728             composedClass;
       
   729 
       
   730         if (typeof nodeClass === 'string') {
       
   731             // Look for a namespaced node class on `Y`.
       
   732             nodeClass = Y.Object.getValue(Y, nodeClass.split('.'));
       
   733 
       
   734             if (nodeClass) {
       
   735                 this.nodeClass = nodeClass;
       
   736             } else {
       
   737                 Y.error('Node class not found: ' + nodeClass, null, 'tree');
       
   738                 return;
       
   739             }
       
   740         }
       
   741 
       
   742         if (!nodeExtensions.length) {
       
   743             this._nodeClass = nodeClass;
       
   744             return;
       
   745         }
       
   746 
       
   747         // Compose a new class by mixing extensions into nodeClass.
       
   748         composedClass = function () {
       
   749             var extensions = composedClass._nodeExtensions;
       
   750 
       
   751             nodeClass.apply(this, arguments);
       
   752 
       
   753             for (var i = 0, len = extensions.length; i < len; i++) {
       
   754                 extensions[i].apply(this, arguments);
       
   755             }
       
   756         };
       
   757 
       
   758         Y.extend(composedClass, nodeClass);
       
   759 
       
   760         for (var i = 0, len = nodeExtensions.length; i < len; i++) {
       
   761             Y.mix(composedClass.prototype, nodeExtensions[i].prototype, true);
       
   762         }
       
   763 
       
   764         composedClass._nodeExtensions = nodeExtensions;
       
   765         this._nodeClass = composedClass;
       
   766     },
       
   767 
       
   768     /**
       
   769     Utility method for lazily publishing and firing events.
       
   770 
       
   771     @method _fireTreeEvent
       
   772     @param {String} name Event name to fire.
       
   773     @param {Object} facade Event facade.
       
   774     @param {Object} [options] Options.
       
   775         @param {Function} [options.defaultFn] Default handler for this event.
       
   776         @param {Boolean} [options.silent=false] Whether the default handler
       
   777             should be executed directly without actually firing the event.
       
   778     @chainable
       
   779     @protected
       
   780     **/
       
   781     _fireTreeEvent: function (name, facade, options) {
       
   782         if (options && options.silent) {
       
   783             if (options.defaultFn) {
       
   784                 facade.silent = true; // intentionally modifying the facade
       
   785                 options.defaultFn.call(this, facade);
       
   786             }
       
   787         } else {
       
   788             if (options && options.defaultFn && !this._published[name]) {
       
   789                 this._published[name] = this.publish(name, {
       
   790                     defaultFn: options.defaultFn
       
   791                 });
       
   792             }
       
   793 
       
   794             this.fire(name, facade);
       
   795         }
       
   796 
       
   797         return this;
       
   798     },
       
   799 
       
   800     /**
       
   801     Returns the default insertion index that should be used when _node_ is
       
   802     inserted as a child of _parent_ without an explicit index.
       
   803 
       
   804     The primary purpose of this method is to serve as a hook point for
       
   805     extensions and plugins that need to customize insertion order.
       
   806 
       
   807     @method _getDefaultNodeIndex
       
   808     @param {Tree.Node} parent Parent node.
       
   809     @param {Tree.Node} node Node being inserted.
       
   810     @param {Object} [options] Options passed to `insertNode()`.
       
   811     @return {Number} Index at which _node_ should be inserted into _parent_'s
       
   812         `children` array.
       
   813     @protected
       
   814     **/
       
   815     _getDefaultNodeIndex: function (parent/*, node, options*/) {
       
   816         return parent.children.length;
       
   817     },
       
   818 
       
   819     /**
       
   820     Removes the specified node from its parent node if it has one.
       
   821 
       
   822     @method _removeNodeFromParent
       
   823     @param {Tree.Node} node Node to remove.
       
   824     @protected
       
   825     **/
       
   826     _removeNodeFromParent: function (node) {
       
   827         var parent = node.parent,
       
   828             index;
       
   829 
       
   830         if (parent) {
       
   831             index = parent.indexOf(node);
       
   832 
       
   833             if (index > -1) {
       
   834                 var children = parent.children;
       
   835 
       
   836                 if (index === children.length - 1) {
       
   837                     children.pop();
       
   838                 } else {
       
   839                     children.splice(index, 1);
       
   840                     parent._isIndexStale = true;
       
   841                 }
       
   842 
       
   843                 node.parent = null;
       
   844             }
       
   845         }
       
   846     },
       
   847 
       
   848     // -- Default Event Handlers -----------------------------------------------
       
   849     _defAddFn: function (e) {
       
   850         var index  = e.index,
       
   851             node   = e.node,
       
   852             parent = e.parent,
       
   853             oldIndex;
       
   854 
       
   855         // Remove the node from its existing parent if it has one.
       
   856         if (node.parent) {
       
   857             // If the node's existing parent is the same parent it's being
       
   858             // inserted into, adjust the index to avoid an off-by-one error.
       
   859             if (node.parent === parent) {
       
   860                 oldIndex = parent.indexOf(node);
       
   861 
       
   862                 if (oldIndex === index) {
       
   863                     // Old index is the same as the new index, so just don't do
       
   864                     // anything.
       
   865                     return;
       
   866                 } else if (oldIndex < index) {
       
   867                     // Removing the node from its old index will affect the new
       
   868                     // index, so decrement the new index by one.
       
   869                     index -= 1;
       
   870                 }
       
   871             }
       
   872 
       
   873             this.removeNode(node, {
       
   874                 silent: e.silent,
       
   875                 src   : 'add'
       
   876             });
       
   877         }
       
   878 
       
   879         // Add the node to its new parent at the desired index.
       
   880         node.parent = parent;
       
   881         parent.children.splice(index, 0, node);
       
   882 
       
   883         parent.canHaveChildren = true;
       
   884         parent._isIndexStale   = true;
       
   885     },
       
   886 
       
   887     _defClearFn: function (e) {
       
   888         var newRootNode = e.rootNode;
       
   889 
       
   890         if (this.rootNode) {
       
   891             this.destroyNode(this.rootNode, {silent: true});
       
   892         }
       
   893 
       
   894         this._nodeMap = {};
       
   895         this._nodeMap[newRootNode.id] = newRootNode;
       
   896 
       
   897         this.rootNode = newRootNode;
       
   898         this.children = newRootNode.children;
       
   899     },
       
   900 
       
   901     _defRemoveFn: function (e) {
       
   902         var node = e.node;
       
   903 
       
   904         if (e.destroy) {
       
   905             this.destroyNode(node, {silent: true});
       
   906         } else if (e.parent) {
       
   907             this._removeNodeFromParent(node);
       
   908         } else if (this.rootNode === node) {
       
   909             // Guess we'll need a new root node!
       
   910             this.rootNode = this.createNode(this._rootNodeConfig);
       
   911             this.children = this.rootNode.children;
       
   912         }
       
   913     }
       
   914 }, {
       
   915     /**
       
   916     Return this value from a `Tree#traverseNode()` or `Tree.Node#traverse()`
       
   917     callback to immediately stop traversal.
       
   918 
       
   919     @property STOP_TRAVERSAL
       
   920     @static
       
   921     **/
       
   922     STOP_TRAVERSAL: {}
       
   923 });
       
   924 
       
   925 Y.Tree = Y.mix(Tree, Y.Tree);
       
   926 
       
   927 
       
   928 }, '@VERSION@', {"requires": ["base-build", "tree-node"]});