Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
            child;
689
 
690
        if (oldTree === this) {
691
            return;
692
        }
693
 
694
        for (var i = 0, len = node.children.length; i < len; i++) {
695
            child = node.children[i];
696
 
697
            child.parent = null; // Prevents the child from being removed from
698
                                 // its parent during the adoption.
699
 
700
            this._adoptNode(child, {silent: true});
701
            child.parent = node;
702
        }
703
 
704
        if (node.parent) {
705
            oldTree.removeNode(node, options);
706
        }
707
 
708
        delete oldTree._nodeMap[node.id];
709
 
710
        // If this node isn't an instance of this tree's composed _nodeClass,
711
        // then we need to recreate it to avoid potentially breaking things in
712
        // really weird ways.
713
        if (!(node instanceof this._nodeClass)
714
                || oldTree._nodeClass !== this._nodeClass) {
715
 
716
            node = this.createNode(node.toJSON());
717
        }
718
 
719
        node.tree = this;
720
        node._isIndexStale = true;
721
 
722
        this._nodeMap[node.id] = node;
723
    },
724
 
725
    /**
726
    Composes a custom late-bound tree node class (if necessary) based on the
727
    classes specified in this Tree's `nodeClass` and `nodeExtensions`
728
    properties.
729
 
730
    The composed class is stored in this Tree's `_nodeClass` property. If
731
    composition wasn't necessary, then `_nodeClass` will just be a reference to
732
    `nodeClass`.
733
 
734
    @method _composeNodeClass
735
    @protected
736
    **/
737
    _composeNodeClass: function () {
738
        var nodeClass      = this.nodeClass,
739
            nodeExtensions = this.nodeExtensions,
740
            composedClass;
741
 
742
        if (typeof nodeClass === 'string') {
743
            // Look for a namespaced node class on `Y`.
744
            nodeClass = Y.Object.getValue(Y, nodeClass.split('.'));
745
 
746
            if (nodeClass) {
747
                this.nodeClass = nodeClass;
748
            } else {
749
                Y.error('Node class not found: ' + nodeClass, null, 'tree');
750
                return;
751
            }
752
        }
753
 
754
        if (!nodeExtensions.length) {
755
            this._nodeClass = nodeClass;
756
            return;
757
        }
758
 
759
        // Compose a new class by mixing extensions into nodeClass.
760
        composedClass = function () {
761
            var extensions = composedClass._nodeExtensions;
762
 
763
            nodeClass.apply(this, arguments);
764
 
765
            for (var i = 0, len = extensions.length; i < len; i++) {
766
                extensions[i].apply(this, arguments);
767
            }
768
        };
769
 
770
        Y.extend(composedClass, nodeClass);
771
 
772
        for (var i = 0, len = nodeExtensions.length; i < len; i++) {
773
            Y.mix(composedClass.prototype, nodeExtensions[i].prototype, true);
774
        }
775
 
776
        composedClass._nodeExtensions = nodeExtensions;
777
        this._nodeClass = composedClass;
778
    },
779
 
780
    /**
781
    Utility method for lazily publishing and firing events.
782
 
783
    @method _fireTreeEvent
784
    @param {String} name Event name to fire.
785
    @param {Object} facade Event facade.
786
    @param {Object} [options] Options.
787
        @param {Function} [options.defaultFn] Default handler for this event.
788
        @param {Boolean} [options.silent=false] Whether the default handler
789
            should be executed directly without actually firing the event.
790
    @chainable
791
    @protected
792
    **/
793
    _fireTreeEvent: function (name, facade, options) {
794
        if (options && options.silent) {
795
            if (options.defaultFn) {
796
                facade.silent = true; // intentionally modifying the facade
797
                options.defaultFn.call(this, facade);
798
            }
799
        } else {
800
            if (options && options.defaultFn && !this._published[name]) {
801
                this._published[name] = this.publish(name, {
802
                    defaultFn: options.defaultFn
803
                });
804
            }
805
 
806
            this.fire(name, facade);
807
        }
808
 
809
        return this;
810
    },
811
 
812
    /**
813
    Returns the default insertion index that should be used when _node_ is
814
    inserted as a child of _parent_ without an explicit index.
815
 
816
    The primary purpose of this method is to serve as a hook point for
817
    extensions and plugins that need to customize insertion order.
818
 
819
    @method _getDefaultNodeIndex
820
    @param {Tree.Node} parent Parent node.
821
    @param {Tree.Node} node Node being inserted.
822
    @param {Object} [options] Options passed to `insertNode()`.
823
    @return {Number} Index at which _node_ should be inserted into _parent_'s
824
        `children` array.
825
    @protected
826
    **/
827
    _getDefaultNodeIndex: function (parent/*, node, options*/) {
828
        return parent.children.length;
829
    },
830
 
831
    /**
832
    Removes the specified node from its parent node if it has one.
833
 
834
    @method _removeNodeFromParent
835
    @param {Tree.Node} node Node to remove.
836
    @protected
837
    **/
838
    _removeNodeFromParent: function (node) {
839
        var parent = node.parent,
840
            index;
841
 
842
        if (parent) {
843
            index = parent.indexOf(node);
844
 
845
            if (index > -1) {
846
                var children = parent.children;
847
 
848
                if (index === children.length - 1) {
849
                    children.pop();
850
                } else {
851
                    children.splice(index, 1);
852
                    parent._isIndexStale = true;
853
                }
854
 
855
                node.parent = null;
856
            }
857
        }
858
    },
859
 
860
    // -- Default Event Handlers -----------------------------------------------
861
    _defAddFn: function (e) {
862
        var index  = e.index,
863
            node   = e.node,
864
            parent = e.parent,
865
            oldIndex;
866
 
867
        // Remove the node from its existing parent if it has one.
868
        if (node.parent) {
869
            // If the node's existing parent is the same parent it's being
870
            // inserted into, adjust the index to avoid an off-by-one error.
871
            if (node.parent === parent) {
872
                oldIndex = parent.indexOf(node);
873
 
874
                if (oldIndex === index) {
875
                    // Old index is the same as the new index, so just don't do
876
                    // anything.
877
                    return;
878
                } else if (oldIndex < index) {
879
                    // Removing the node from its old index will affect the new
880
                    // index, so decrement the new index by one.
881
                    index -= 1;
882
                }
883
            }
884
 
885
            this.removeNode(node, {
886
                silent: e.silent,
887
                src   : 'add'
888
            });
889
        }
890
 
891
        // Add the node to its new parent at the desired index.
892
        node.parent = parent;
893
        parent.children.splice(index, 0, node);
894
 
895
        parent.canHaveChildren = true;
896
        parent._isIndexStale   = true;
897
    },
898
 
899
    _defClearFn: function (e) {
900
        var newRootNode = e.rootNode;
901
 
902
        if (this.rootNode) {
903
            this.destroyNode(this.rootNode, {silent: true});
904
        }
905
 
906
        this._nodeMap = {};
907
        this._nodeMap[newRootNode.id] = newRootNode;
908
 
909
        this.rootNode = newRootNode;
910
        this.children = newRootNode.children;
911
    },
912
 
913
    _defRemoveFn: function (e) {
914
        var node = e.node;
915
 
916
        if (e.destroy) {
917
            this.destroyNode(node, {silent: true});
918
        } else if (e.parent) {
919
            this._removeNodeFromParent(node);
920
        } else if (this.rootNode === node) {
921
            // Guess we'll need a new root node!
922
            this.rootNode = this.createNode(this._rootNodeConfig);
923
            this.children = this.rootNode.children;
924
        }
925
    }
926
}, {
927
    /**
928
    Return this value from a `Tree#traverseNode()` or `Tree.Node#traverse()`
929
    callback to immediately stop traversal.
930
 
931
    @property STOP_TRAVERSAL
932
    @static
933
    **/
934
    STOP_TRAVERSAL: {}
935
});
936
 
937
Y.Tree = Y.mix(Tree, Y.Tree);
938
 
939
 
940
}, '3.18.1', {"requires": ["base-build", "tree-node"]});