2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
8 YUI.add('tree', function (Y, NAME) {
10 /*jshint boss:true, expr:true, onevar:false */
13 Provides a generic tree data structure and related functionality.
15 A tree has a root node, which may contain any number of child nodes, which may
16 themselves contain child nodes, ad infinitum.
18 Child nodes are lightweight function instances which delegate to the tree for
19 all significant functionality, so trees remain performant and memory-efficient
20 even with thousands and thousands of nodes.
27 The `Tree` class represents a generic tree data structure. A tree has a root
28 node, which may contain any number of child nodes, which may themselves contain
29 child nodes, ad infinitum.
31 This class doesn't expose any UI, but is intended to be used as a data structure
32 or base class for other components. For example, the SmugMug TreeView gallery
33 module extends Tree and provides a TreeView UI.
36 @param {Object} [config] Config options.
37 @param {Object[]|Tree.Node[]} [config.nodes] Array of tree node config
38 objects or `Tree.Node` instances to add to this tree at initialization
40 @param {Object|Tree.Node} [config.rootNode] Node to use as the root node of
49 Fired when a node is added to this Tree. The `src` property will indicate
50 how the node was added ("append", "insert", "prepend", etc.).
53 @param {Number} index Index at which the node will be added.
54 @param {Tree.Node} node Node being added.
55 @param {Tree.Node} parent Parent node to which the node will be added.
56 @param {String} src Source of the event ("append", "insert", "prepend",
58 @preventable _defAddFn
63 Fired when this Tree is cleared.
66 @param {Tree.Node} rootNode New root node of this tree (the old root node is
67 always destroyed when a tree is cleared).
68 @param {String} src Source of the event.
69 @preventable _defClearFn
74 Fired when a node is removed from this Tree.
77 @param {Boolean} destroy Whether or not the node will be destroyed after
78 being removed from this tree.
79 @param {Tree.Node} node Node being removed.
80 @param {Tree.Node} parent Parent node from which the node will be removed.
81 @param {String} src Source of the event.
82 @preventable _defRemoveFn
84 EVT_REMOVE = 'remove';
86 var Tree = Y.Base.create('tree', Y.Base, [], {
87 // -- Public Properties ----------------------------------------------------
90 Reference to the `children` array of this Tree's `rootNode`.
92 This is a convenience property to allow you to type `tree.children` instead
93 of `tree.rootNode.children`.
95 @property {Tree.Node[]} children
100 The `Tree.Node` class or subclass that should be used for nodes created by
103 You may specify an actual class reference or a string that resolves to a
104 class reference at runtime.
106 @property {String|Tree.Node} nodeClass
109 nodeClass: Y.Tree.Node,
112 Optional array containing one or more extension classes that should be mixed
113 into the `nodeClass` when this Tree is instantiated. The resulting composed
114 node class will be unique to this Tree instance and will not affect any
115 other instances, nor will it modify the defined `nodeClass` itself.
117 This provides a late-binding extension mechanism for nodes that doesn't
118 require them to extend `Y.Base`, which would incur a significant performance
121 @property {Array} nodeExtensions
127 Root node of this Tree.
129 @property {Tree.Node} rootNode
133 // -- Protected Properties -------------------------------------------------
136 Simple way to type-check that this is a Tree instance.
138 @property {Boolean} _isYUITree
145 Composed node class based on `nodeClass` that mixes in any extensions
146 specified in `nodeExtensions`. If there are no extensions, this will just be
147 a reference to `nodeClass`.
149 @property {Tree.Node} _nodeClass
154 Mapping of node ids to node instances for nodes in this tree.
156 @property {Object} _nodeMap
161 Default config object for the root node.
163 @property {Object} _rootNodeConfig
166 _rootNodeConfig: {canHaveChildren: true},
168 // -- Lifecycle ------------------------------------------------------------
169 initializer: function (config) {
170 config || (config = {});
172 if (config.nodeClass) {
173 this.nodeClass = config.nodeClass;
176 if (config.nodeExtensions) {
177 this.nodeExtensions = this.nodeExtensions.concat(config.nodeExtensions);
181 Hash of published custom events.
183 @property {Object} _published
187 this._published || (this._published = {});
190 // Allow all extensions to initialize, then finish up.
191 this.onceAfter('initializedChange', function () {
192 this._composeNodeClass();
194 this.clear(config.rootNode, {silent: true});
197 this.insertNode(this.rootNode, config.nodes, {silent: true});
202 destructor: function () {
203 this.destroyNode(this.rootNode, {silent: true});
205 this.children = null;
206 this.rootNode = null;
207 this._nodeClass = null;
208 this._nodeMap = null;
209 this._published = null;
212 // -- Public Methods -------------------------------------------------------
215 Appends a node or array of nodes as the last child of the specified parent
218 If a node being appended is from another tree, it and all its children will
219 be removed from that tree and moved to this one.
222 @param {Tree.Node} parent Parent node.
223 @param {Object|Object[]|Tree.Node|Tree.Node[]} node Child node, node config
224 object, array of child nodes, or array of node config objects to append
225 to the given parent. Node config objects will automatically be converted
227 @param {Object} [options] Options.
228 @param {Boolean} [options.silent=false] If `true`, the `add` event will
230 @return {Tree.Node|Tree.Node[]} Node or array of nodes that were
233 appendNode: function (parent, node, options) {
234 return this.insertNode(parent, node, Y.merge(options, {
235 index: parent.children.length,
241 Clears this tree by destroying the root node and all its children. If a
242 `rootNode` argument is provided, that node will become the root node of this
243 tree; otherwise, a new root node will be created.
246 @param {Object|Tree.Node} [rootNode] If specified, this node will be used as
248 @param {Object} [options] Options.
249 @param {Boolean} [options.silent=false] If `true`, the `clear` event
251 @param {String} [options.src] Source of the change, to be passed along
252 to the event facade of the resulting event. This can be used to
253 distinguish between changes triggered by a user and changes
254 triggered programmatically, for example.
257 clear: function (rootNode, options) {
258 return this._fireTreeEvent(EVT_CLEAR, {
259 rootNode: this.createNode(rootNode || this._rootNodeConfig),
260 src : options && options.src
262 defaultFn: this._defClearFn,
263 silent : options && options.silent
268 Creates and returns a new `Tree.Node` instance associated with (but not
269 yet appended to) this tree.
272 @param {Object|Tree.Node} [config] Node configuration. If a `Tree.Node`
273 instance is specified instead of a config object, that node will be
274 adopted into this tree (if it doesn't already belong to this tree) and
275 removed from any other tree to which it belongs.
276 @return {Tree.Node|null} New node, or `null` if a node could not be created
277 from the given _config_.
279 createNode: function (config) {
280 config || (config = {});
282 // If `config` is already a node, just ensure it hasn't been destroyed
283 // and is in the node map, then return it.
284 if (config._isYUITreeNode) {
285 if (config.state.destroyed) {
286 Y.error('Cannot insert a node that has already been destroyed.', null, 'tree');
290 this._adoptNode(config);
294 // First, create nodes for any children of this node.
295 if (config.children) {
298 for (var i = 0, len = config.children.length; i < len; i++) {
299 children.push(this.createNode(config.children[i]));
302 config = Y.merge(config, {children: children});
305 var node = new this._nodeClass(this, config);
307 return this._nodeMap[node.id] = node;
311 Removes and destroys a node and all its child nodes. Once destroyed, a node
312 is eligible for garbage collection and cannot be reused or re-added to the
316 @param {Tree.Node} node Node to destroy.
317 @param {Object} [options] Options.
318 @param {Boolean} [options.silent=false] If `true`, `remove` events will
320 @param {String} [options.src] Source of the change, to be passed along
321 to the event facade of the resulting events. This can be used to
322 distinguish between changes triggered by a user and changes
323 triggered programmatically, for example.
326 destroyNode: function (node, options) {
329 options || (options = {});
331 for (i = 0, len = node.children.length; i < len; i++) {
332 child = node.children[i];
334 // Manually remove the child from its parent; this makes destroying
335 // all children of the parent much faster since there's no splicing
339 // Destroy the child.
340 this.destroyNode(child, options);
344 this.removeNode(node, options);
349 node.state = {destroyed: true};
353 delete this._nodeMap[node.id];
359 Removes all children from the specified node. The removed children will
360 still be reusable unless the `destroy` option is truthy.
363 @param {Tree.Node} node Node to empty.
364 @param {Object} [options] Options.
365 @param {Boolean} [options.destroy=false] If `true`, the children will
366 also be destroyed, which makes them available for garbage collection
367 and means they can't be reused.
368 @param {Boolean} [options.silent=false] If `true`, `remove` events will
370 @param {String} [options.src] Source of the change, to be passed along
371 to the event facade of the resulting events. This can be used to
372 distinguish between changes triggered by a user and changes
373 triggered programmatically, for example.
374 @return {Tree.Node[]} Array of removed child nodes.
376 emptyNode: function (node, options) {
377 var children = node.children,
380 for (var i = children.length - 1; i > -1; --i) {
381 removed[i] = this.removeNode(children[i], options);
388 Performs a depth-first traversal of _node_, passing it and each of its
389 descendants to the specified _callback_, and returning the first node for
390 which the callback returns a truthy value.
392 Traversal will stop as soon as a truthy value is returned from the callback.
394 See `traverseNode()` for more details on how depth-first traversal works.
397 @param {Tree.Node} node Node to traverse.
398 @param {Object} [options] Options.
399 @param {Number} [options.depth] Depth limit. If specified, descendants
400 will only be traversed to this depth before backtracking and moving
402 @param {Function} callback Callback function to call with the traversed
403 node and each of its descendants. If this function returns a truthy
404 value, traversal will be stopped and the current node will be returned.
406 @param {Tree.Node} callback.node Node being traversed.
408 @param {Object} [thisObj] `this` object to use when executing _callback_.
409 @return {Tree.Node|null} Returns the first node for which the _callback_
410 returns a truthy value, or `null` if the callback never returns a truthy
413 findNode: function (node, options, callback, thisObj) {
416 // Allow callback as second argument.
417 if (typeof options === 'function') {
423 this.traverseNode(node, options, function (descendant) {
424 if (callback.call(thisObj, descendant)) {
426 return Tree.STOP_TRAVERSAL;
434 Returns the tree node with the specified id, or `undefined` if the node
435 doesn't exist in this tree.
438 @param {String} id Node id.
439 @return {Tree.Node} Node, or `undefined` if not found.
441 getNodeById: function (id) {
442 return this._nodeMap[id];
446 Inserts a node or array of nodes at the specified index under the given
447 parent node, or appends them to the parent if no index is specified.
449 If a node being inserted is from another tree, it and all its children will
450 be removed from that tree and moved to this one.
453 @param {Tree.Node} parent Parent node.
454 @param {Object|Object[]|Tree.Node|Tree.Node[]} node Child node, node config
455 object, array of child nodes, or array of node config objects to insert
456 under the given parent. Node config objects will automatically be
457 converted into node instances.
459 @param {Object} [options] Options.
460 @param {Number} [options.index] Index at which to insert the child node.
461 If not specified, the node will be appended as the last child of the
463 @param {Boolean} [options.silent=false] If `true`, the `add` event will
465 @param {String} [options.src='insert'] Source of the change, to be
466 passed along to the event facade of the resulting event. This can be
467 used to distinguish between changes triggered by a user and changes
468 triggered programmatically, for example.
470 @return {Tree.Node|Tree.Node[]} Node or array of nodes that were inserted.
472 insertNode: function (parent, node, options) {
473 options || (options = {});
474 parent || (parent = this.rootNode);
476 // If `node` is an array, recurse to insert each node it contains.
478 // Note: If you're getting an exception here because `node` is null when
479 // you've passed in a reference to some other node's `children` array,
480 // that's happening because nodes must be removed from their current
481 // parent before being added to the new one, and the `children` array is
482 // being modified while the nodes are inserted.
484 // Solution: pass a copy of the other node's `children` array instead of
485 // the original. Doing the copy operation here would have a negative
486 // impact on performance, so you're on your own since this is such a
488 if ('length' in node && Lang.isArray(node)) {
489 var hasIndex = 'index' in options,
493 for (var i = 0, len = node.length; i < len; i++) {
494 insertedNode = this.insertNode(parent, node[i], options);
497 insertedNodes.push(insertedNode);
505 return insertedNodes;
508 node = this.createNode(node);
511 var index = options.index;
513 if (typeof index === 'undefined') {
514 index = this._getDefaultNodeIndex(parent, node, options);
517 this._fireTreeEvent(EVT_ADD, {
521 src : options.src || 'insert'
523 defaultFn: this._defAddFn,
524 silent : options.silent
532 Prepends a node or array of nodes at the beginning of the specified parent
535 If a node being prepended is from another tree, it and all its children will
536 be removed from that tree and moved to this one.
539 @param {Tree.Node} parent Parent node.
540 @param {Object|Object[]|Tree.Node|Tree.Node[]} node Child node,
541 node config object, array of child nodes, or array of node config
542 objects to prepend to the given parent. Node config objects will
543 automatically be converted into node instances.
544 @param {Object} [options] Options.
545 @param {Boolean} [options.silent=false] If `true`, the `add` event will
547 @return {Tree.Node|Tree.Node[]} Node or array of nodes that were
550 prependNode: function (parent, node, options) {
551 return this.insertNode(parent, node, Y.merge(options, {
558 Removes the specified node from its parent node. The removed node will still
559 be reusable unless the `destroy` option is truthy.
562 @param {Tree.Node} node Node to remove.
563 @param {Object} [options] Options.
564 @param {Boolean} [options.destroy=false] If `true`, the node and all its
565 children will also be destroyed, which makes them available for
566 garbage collection and means they can't be reused.
567 @param {Boolean} [options.silent=false] If `true`, the `remove` event
569 @param {String} [options.src] Source of the change, to be passed along
570 to the event facade of the resulting event. This can be used to
571 distinguish between changes triggered by a user and changes
572 triggered programmatically, for example.
573 @return {Tree.Node} Node that was removed.
575 removeNode: function (node, options) {
576 options || (options = {});
578 this._fireTreeEvent(EVT_REMOVE, {
579 destroy: !!options.destroy,
581 parent : node.parent,
582 src : options.src || 'remove'
584 defaultFn: this._defRemoveFn,
585 silent : options.silent
592 Returns the total number of nodes in this tree, at all levels.
594 Use `rootNode.children.length` to get only the number of top-level nodes.
597 @return {Number} Total number of nodes in this tree.
600 return this.rootNode.size() + 1;
604 Serializes this tree to an object suitable for use in JSON.
607 @return {Object} Serialized tree object.
609 toJSON: function () {
610 return this.rootNode.toJSON();
614 Performs a depth-first traversal of _node_, passing it and each of its
615 descendants to the specified _callback_.
617 If the callback function returns `Tree.STOP_TRAVERSAL`, traversal will be
618 stopped immediately. Otherwise, it will continue until the deepest
619 descendant of _node_ has been traversed, or until each branch has been
620 traversed to the optional maximum depth limit.
622 Since traversal is depth-first, that means nodes are traversed like this:
633 @param {Tree.Node} node Node to traverse.
634 @param {Object} [options] Options.
635 @param {Number} [options.depth] Depth limit. If specified, descendants
636 will only be traversed to this depth before backtracking and moving
638 @param {Function} callback Callback function to call with the traversed
639 node and each of its descendants.
641 @param {Tree.Node} callback.node Node being traversed.
643 @param {Object} [thisObj] `this` object to use when executing _callback_.
644 @return {Mixed} Returns `Tree.STOP_TRAVERSAL` if traversal was stopped;
645 otherwise returns `undefined`.
647 traverseNode: function (node, options, callback, thisObj) {
648 if (node.state.destroyed) {
649 Y.error('Cannot traverse a node that has been destroyed.', null, 'tree');
653 // Allow callback as second argument.
654 if (typeof options === 'function') {
660 options || (options = {});
662 var stop = Tree.STOP_TRAVERSAL,
663 unlimited = typeof options.depth === 'undefined';
665 if (callback.call(thisObj, node) === stop) {
669 var children = node.children;
671 if (unlimited || options.depth > 0) {
672 var childOptions = unlimited ? options : {depth: options.depth - 1};
674 for (var i = 0, len = children.length; i < len; i++) {
675 if (this.traverseNode(children[i], childOptions, callback, thisObj) === stop) {
682 // -- Protected Methods ----------------------------------------------------
685 Moves the specified node and all its children from another tree to this
689 @param {Tree.Node} node Node to adopt.
690 @param {Object} [options] Options to pass along to `removeNode()`.
693 _adoptNode: function (node, options) {
694 var oldTree = node.tree;
696 if (oldTree === this) {
700 for (var i = 0, len = node.children.length; i < len; i++) {
701 this._adoptNode(node.children[i], {silent: true});
704 oldTree.removeNode(node, options);
705 delete oldTree._nodeMap[node.id];
707 // If this node isn't an instance of this tree's composed _nodeClass,
708 // then we need to recreate it to avoid potentially breaking things in
709 // really weird ways.
710 if (!(node instanceof this._nodeClass)
711 || oldTree._nodeClass !== this._nodeClass) {
713 node = this.createNode(node.toJSON());
717 this._nodeMap[node.id] = node;
721 Composes a custom late-bound tree node class (if necessary) based on the
722 classes specified in this Tree's `nodeClass` and `nodeExtensions`
725 The composed class is stored in this Tree's `_nodeClass` property. If
726 composition wasn't necessary, then `_nodeClass` will just be a reference to
729 @method _composeNodeClass
732 _composeNodeClass: function () {
733 var nodeClass = this.nodeClass,
734 nodeExtensions = this.nodeExtensions,
737 if (typeof nodeClass === 'string') {
738 // Look for a namespaced node class on `Y`.
739 nodeClass = Y.Object.getValue(Y, nodeClass.split('.'));
742 this.nodeClass = nodeClass;
744 Y.error('Node class not found: ' + nodeClass, null, 'tree');
749 if (!nodeExtensions.length) {
750 this._nodeClass = nodeClass;
754 // Compose a new class by mixing extensions into nodeClass.
755 composedClass = function () {
756 var extensions = composedClass._nodeExtensions;
758 nodeClass.apply(this, arguments);
760 for (var i = 0, len = extensions.length; i < len; i++) {
761 extensions[i].apply(this, arguments);
765 Y.extend(composedClass, nodeClass);
767 for (var i = 0, len = nodeExtensions.length; i < len; i++) {
768 Y.mix(composedClass.prototype, nodeExtensions[i].prototype, true);
771 composedClass._nodeExtensions = nodeExtensions;
772 this._nodeClass = composedClass;
776 Utility method for lazily publishing and firing events.
778 @method _fireTreeEvent
779 @param {String} name Event name to fire.
780 @param {Object} facade Event facade.
781 @param {Object} [options] Options.
782 @param {Function} [options.defaultFn] Default handler for this event.
783 @param {Boolean} [options.silent=false] Whether the default handler
784 should be executed directly without actually firing the event.
788 _fireTreeEvent: function (name, facade, options) {
789 if (options && options.silent) {
790 if (options.defaultFn) {
791 facade.silent = true; // intentionally modifying the facade
792 options.defaultFn.call(this, facade);
795 if (options && options.defaultFn && !this._published[name]) {
796 this._published[name] = this.publish(name, {
797 defaultFn: options.defaultFn
801 this.fire(name, facade);
808 Returns the default insertion index that should be used when _node_ is
809 inserted as a child of _parent_ without an explicit index.
811 The primary purpose of this method is to serve as a hook point for
812 extensions and plugins that need to customize insertion order.
814 @method _getDefaultNodeIndex
815 @param {Tree.Node} parent Parent node.
816 @param {Tree.Node} node Node being inserted.
817 @param {Object} [options] Options passed to `insertNode()`.
818 @return {Number} Index at which _node_ should be inserted into _parent_'s
822 _getDefaultNodeIndex: function (parent/*, node, options*/) {
823 return parent.children.length;
827 Removes the specified node from its parent node if it has one.
829 @method _removeNodeFromParent
830 @param {Tree.Node} node Node to remove.
833 _removeNodeFromParent: function (node) {
834 var parent = node.parent,
838 index = parent.indexOf(node);
841 var children = parent.children;
843 if (index === children.length - 1) {
846 children.splice(index, 1);
847 parent._isIndexStale = true;
855 // -- Default Event Handlers -----------------------------------------------
856 _defAddFn: function (e) {
862 // Remove the node from its existing parent if it has one.
864 // If the node's existing parent is the same parent it's being
865 // inserted into, adjust the index to avoid an off-by-one error.
866 if (node.parent === parent) {
867 oldIndex = parent.indexOf(node);
869 if (oldIndex === index) {
870 // Old index is the same as the new index, so just don't do
873 } else if (oldIndex < index) {
874 // Removing the node from its old index will affect the new
875 // index, so decrement the new index by one.
880 this.removeNode(node, {
886 // Add the node to its new parent at the desired index.
887 node.parent = parent;
888 parent.children.splice(index, 0, node);
890 parent.canHaveChildren = true;
891 parent._isIndexStale = true;
894 _defClearFn: function (e) {
895 var newRootNode = e.rootNode;
898 this.destroyNode(this.rootNode, {silent: true});
902 this._nodeMap[newRootNode.id] = newRootNode;
904 this.rootNode = newRootNode;
905 this.children = newRootNode.children;
908 _defRemoveFn: function (e) {
912 this.destroyNode(node, {silent: true});
913 } else if (e.parent) {
914 this._removeNodeFromParent(node);
915 } else if (this.rootNode === node) {
916 // Guess we'll need a new root node!
917 this.rootNode = this.createNode(this._rootNodeConfig);
918 this.children = this.rootNode.children;
923 Return this value from a `Tree#traverseNode()` or `Tree.Node#traverse()`
924 callback to immediately stop traversal.
926 @property STOP_TRAVERSAL
932 Y.Tree = Y.mix(Tree, Y.Tree);
935 }, '3.13.0', {"requires": ["base-build", "tree-node"]});