Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('gallery-sm-treeview', function (Y, NAME) {
2
 
3
var Micro = Y.Template.Micro;
4
 
5
Y.namespace('TreeView').Templates = {
6
    children: Micro.compile(
7
        '<ul class="<%= data.classNames.children %>" ' +
8
 
9
            '<% if (data.node.isRoot()) { %>' +
10
                'role="tree" tabindex="0"' +
11
            '<% } else { %>' +
12
                'role="group"' +
13
            '<% } %>' +
14
 
15
        '></ul>'
16
    ),
17
 
18
    node: Micro.compile(
19
        '<li id="<%= data.node.id %>" class="<%= data.nodeClassNames.join(" ") %>" role="treeitem" aria-labelled-by="<%= data.node.id %>-label">' +
20
            '<div class="<%= data.classNames.row %>" data-node-id="<%= data.node.id %>">' +
21
                '<span class="<%= data.classNames.indicator %>"><s></s></span>' +
22
                '<span class="<%= data.classNames.icon %>"></span>' +
23
                '<span id="<%= data.node.id %>-label" class="<%= data.classNames.label %>"><%== data.node.label %></span>' +
24
            '</div>' +
25
        '</li>'
26
    )
27
};
28
/*jshint expr:true, onevar:false */
29
 
30
/**
31
Provides the `Y.TreeView` widget.
32
 
33
@module gallery-sm-treeview
34
@main gallery-sm-treeview
35
**/
36
 
37
/**
38
TreeView widget.
39
 
40
@class TreeView
41
@constructor
42
@extends View
43
@uses Tree
44
@uses Tree.Labelable
45
@uses Tree.Openable
46
@uses Tree.Selectable
47
**/
48
 
49
var getClassName = Y.ClassNameManager.getClassName,
50
 
51
TreeView = Y.Base.create('treeView', Y.View, [
52
    Y.Tree,
53
    Y.Tree.Labelable,
54
    Y.Tree.Openable,
55
    Y.Tree.Selectable
56
], {
57
    // -- Public Properties ----------------------------------------------------
58
 
59
    /**
60
    CSS class names used by this treeview.
61
 
62
    @property {Object} classNames
63
    @param {String} canHaveChildren Class name indicating that a tree node can
64
        contain child nodes (whether or not it actually does).
65
    @param {String} children Class name for a list of child nodes.
66
    @param {String} hasChildren Class name indicating that a tree node has one
67
        or more child nodes.
68
    @param {String} icon Class name for a tree node's icon.
69
    @param {String} indicator Class name for an open/closed indicator.
70
    @param {String} label Class name for a tree node's user-visible label.
71
    @param {String} node Class name for a tree node item.
72
    @param {String} noTouch Class name added to the TreeView container when not
73
        using a touchscreen device.
74
    @param {String} open Class name indicating that a tree node is open.
75
    @param {String} row Class name for a row container encompassing the
76
        indicator and label within a tree node.
77
    @param {String} selected Class name for a tree node that's selected.
78
    @param {String} touch Class name added to the TreeView container when using
79
        a touchscreen device.
80
    @param {String} treeview Class name for the TreeView container.
81
    **/
82
    classNames: {
83
        canHaveChildren: getClassName('treeview-can-have-children'),
84
        children       : getClassName('treeview-children'),
85
        hasChildren    : getClassName('treeview-has-children'),
86
        icon           : getClassName('treeview-icon'),
87
        indicator      : getClassName('treeview-indicator'),
88
        label          : getClassName('treeview-label'),
89
        node           : getClassName('treeview-node'),
90
        noTouch        : getClassName('treeview-notouch'),
91
        open           : getClassName('treeview-open'),
92
        row            : getClassName('treeview-row'),
93
        selected       : getClassName('treeview-selected'),
94
        touch          : getClassName('treeview-touch'),
95
        treeview       : getClassName('treeview')
96
    },
97
 
98
    /**
99
    Whether or not this TreeView has been rendered.
100
 
101
    @property {Boolean} rendered
102
    @default false
103
    **/
104
    rendered: false,
105
 
106
    /**
107
    Default templates used to render this TreeView.
108
 
109
    @property {Object} templates
110
    **/
111
    templates: Y.TreeView.Templates,
112
 
113
    // -- Protected Properties -------------------------------------------------
114
 
115
    /**
116
    Simple way to type-check that this is a TreeView instance.
117
 
118
    @property {Boolean} _isYUITreeView
119
    @default true
120
    @protected
121
    **/
122
    _isYUITreeView: true,
123
 
124
    /**
125
    Cached value of the `lazyRender` attribute.
126
 
127
    @property {Boolean} _lazyRender
128
    @protected
129
    **/
130
 
131
    // -- Lifecycle Methods ----------------------------------------------------
132
 
133
    initializer: function (config) {
134
        if (config && config.templates) {
135
            this.templates = Y.merge(this.templates, config.templates);
136
        }
137
 
138
        this._renderQueue = {};
139
        this._attachTreeViewEvents();
140
    },
141
 
142
    destructor: function () {
143
        clearTimeout(this._renderTimeout);
144
        this._detachTreeViewEvents();
145
 
146
        this._renderQueue = null;
147
    },
148
 
149
    // -- Public Methods -------------------------------------------------------
150
 
151
    destroyNode: function (node, options) {
152
        node._htmlNode = null;
153
        return Y.Tree.prototype.destroyNode.call(this, node, options);
154
    },
155
 
156
    /**
157
    Returns the HTML node (as a `Y.Node` instance) associated with the specified
158
    `Tree.Node` instance, if any.
159
 
160
    @method getHTMLNode
161
    @param {Tree.Node} treeNode Tree node.
162
    @return {Node} `Y.Node` instance associated with the given tree node, or
163
        `undefined` if one was not found.
164
    **/
165
    getHTMLNode: function (treeNode) {
166
        if (!treeNode._htmlNode) {
167
            treeNode._htmlNode = this.get('container').one('#' + treeNode.id);
168
        }
169
 
170
        return treeNode._htmlNode;
171
    },
172
 
173
    /**
174
    Renders this TreeView into its container.
175
 
176
    If the container hasn't already been added to the current document, it will
177
    be appended to the `<body>` element.
178
 
179
    @method render
180
    @chainable
181
    **/
182
    render: function () {
183
        var container     = this.get('container'),
184
            isTouchDevice = 'ontouchstart' in Y.config.win;
185
 
186
        container.addClass(this.classNames.treeview);
187
        container.addClass(this.classNames[isTouchDevice ? 'touch' : 'noTouch']);
188
 
189
        this._childrenNode = this.renderChildren(this.rootNode, {
190
            container: container
191
        });
192
 
193
        if (!container.inDoc()) {
194
            Y.one('body').append(container);
195
        }
196
 
197
        this.rendered = true;
198
 
199
        return this;
200
    },
201
 
202
    /**
203
    Renders the children of the specified tree node.
204
 
205
    If a container is specified, it will be assumed to be an existing rendered
206
    tree node, and the children will be rendered (or re-rendered) inside it.
207
 
208
    @method renderChildren
209
    @param {Tree.Node} treeNode Tree node whose children should be rendered.
210
    @param {Object} [options] Options.
211
        @param {Node} [options.container] `Y.Node` instance of a container into
212
            which the children should be rendered. If the container already
213
            contains rendered children, they will be re-rendered in place.
214
    @return {Node} `Y.Node` instance containing the rendered children.
215
    **/
216
    renderChildren: function (treeNode, options) {
217
        options || (options = {});
218
 
219
        var container    = options.container,
220
            childrenNode = container && container.one('>.' + this.classNames.children),
221
            lazyRender   = this._lazyRender;
222
 
223
        if (!childrenNode) {
224
            childrenNode = Y.Node.create(this.templates.children({
225
                classNames: this.classNames,
226
                node      : treeNode,
227
                treeview  : this // not currently used, but may be useful for custom templates
228
            }));
229
        }
230
 
231
        if (treeNode.hasChildren()) {
232
            childrenNode.set('aria-expanded', treeNode.isOpen());
233
 
234
            for (var i = 0, len = treeNode.children.length; i < len; i++) {
235
                var child = treeNode.children[i];
236
 
237
                this.renderNode(child, {
238
                    container     : childrenNode,
239
                    renderChildren: !lazyRender || child.isOpen()
240
                });
241
            }
242
        }
243
 
244
        // Keep track of whether or not this node's children have been rendered
245
        // so we'll know whether we need to render them later if the node is
246
        // opened.
247
        treeNode.state.renderedChildren = true;
248
 
249
        if (container) {
250
            container.append(childrenNode);
251
        }
252
 
253
        return childrenNode;
254
    },
255
 
256
    /**
257
    Renders the specified tree node and its children (if any).
258
 
259
    If a container is specified, the rendered node will be appended to it.
260
 
261
    @method renderNode
262
    @param {Tree.Node} treeNode Tree node to render.
263
    @param {Object} [options] Options.
264
        @param {Node} [options.container] `Y.Node` instance of a container to
265
            which the rendered tree node should be appended.
266
        @param {Boolean} [options.renderChildren=false] Whether or not to render
267
            this node's children.
268
    @return {Node} `Y.Node` instance of the rendered tree node.
269
    **/
270
    renderNode: function (treeNode, options) {
271
        options || (options = {});
272
 
273
        var classNames     = this.classNames,
274
            hasChildren    = treeNode.hasChildren(),
275
            htmlNode       = treeNode._htmlNode,
276
            nodeClassNames = {},
277
            className;
278
 
279
        // Build the hash of CSS classes for this node.
280
        nodeClassNames[classNames.node]            = true;
281
        nodeClassNames[classNames.canHaveChildren] = !!treeNode.canHaveChildren;
282
        nodeClassNames[classNames.hasChildren]     = hasChildren;
283
 
284
        if (htmlNode) {
285
            // This node has already been rendered, so we just need to update
286
            // the DOM instead of re-rendering it from scratch.
287
            htmlNode.one('.' + classNames.label).setHTML(treeNode.label);
288
 
289
            for (className in nodeClassNames) {
290
                if (nodeClassNames.hasOwnProperty(className)) {
291
                    htmlNode.toggleClass(className, nodeClassNames[className]);
292
                }
293
            }
294
        } else {
295
            // This node hasn't been rendered yet, so render it from scratch.
296
            var enabledClassNames = [];
297
 
298
            for (className in nodeClassNames) {
299
                if (nodeClassNames.hasOwnProperty(className) && nodeClassNames[className]) {
300
                    enabledClassNames.push(className);
301
                }
302
            }
303
 
304
            htmlNode = treeNode._htmlNode = Y.Node.create(this.templates.node({
305
                classNames    : classNames,
306
                nodeClassNames: enabledClassNames,
307
                node          : treeNode,
308
                treeview      : this // not currently used, but may be useful for custom templates
309
            }));
310
        }
311
 
312
        this._syncNodeOpenState(treeNode, htmlNode);
313
        this._syncNodeSelectedState(treeNode, htmlNode);
314
 
315
        if (hasChildren) {
316
            if (options.renderChildren) {
317
                this.renderChildren(treeNode, {
318
                    container: htmlNode
319
                });
320
            }
321
        } else {
322
            // If children were previously rendered but this node no longer has
323
            // children, remove the empty child list.
324
            var childrenNode = htmlNode.one('>.' + classNames.children);
325
 
326
            if (childrenNode) {
327
                childrenNode.remove(true);
328
            }
329
        }
330
 
331
        treeNode.state.rendered = true;
332
 
333
        if (options.container) {
334
            options.container.append(htmlNode);
335
        }
336
 
337
        return htmlNode;
338
    },
339
 
340
    // -- Protected Methods ----------------------------------------------------
341
 
342
    _attachTreeViewEvents: function () {
343
        this._treeViewEvents || (this._treeViewEvents = []);
344
 
345
        var classNames = this.classNames,
346
            container  = this.get('container');
347
 
348
        this._treeViewEvents.push(
349
            // Custom events.
350
            this.after({
351
                add              : this._afterAdd,
352
                clear            : this._afterClear,
353
                close            : this._afterClose,
354
                multiSelectChange: this._afterTreeViewMultiSelectChange, // sheesh
355
                open             : this._afterOpen,
356
                remove           : this._afterRemove,
357
                select           : this._afterSelect,
358
                unselect         : this._afterUnselect
359
            }),
360
 
361
            // DOM events.
362
            container.on('mousedown', this._onMouseDown, this),
363
 
364
            container.delegate('click', this._onIndicatorClick,
365
                '.' + classNames.indicator, this),
366
 
367
            container.delegate('click', this._onRowClick,
368
                '.' + classNames.row, this),
369
 
370
            container.delegate('dblclick', this._onRowDoubleClick,
371
                '.' + classNames.canHaveChildren + ' > .' + classNames.row, this)
372
        );
373
    },
374
 
375
    _detachTreeViewEvents: function () {
376
        (new Y.EventHandle(this._treeViewEvents)).detach();
377
    },
378
 
379
    _processRenderQueue: function () {
380
        if (!this.rendered) {
381
            return;
382
        }
383
 
384
        var queue = this._renderQueue,
385
            node;
386
 
387
        for (var id in queue) {
388
            if (queue.hasOwnProperty(id)) {
389
                node = this.getNodeById(id);
390
 
391
                if (node) {
392
                    this.renderNode(node, queue[id]);
393
                }
394
            }
395
        }
396
 
397
        this._renderQueue = {};
398
    },
399
 
400
    _queueRender: function (node, options) {
401
        if (!this.rendered) {
402
            return;
403
        }
404
 
405
        var queue = this._renderQueue,
406
            self  = this;
407
 
408
        clearTimeout(this._renderTimeout);
409
 
410
        queue[node.id] = Y.merge(queue[node.id], options);
411
 
412
        this._renderTimeout = setTimeout(function () {
413
            self._processRenderQueue();
414
        }, 15);
415
 
416
        return this;
417
    },
418
 
419
    /**
420
    Setter for the `lazyRender` attribute.
421
 
422
    Just caches the value in a property for faster lookups.
423
 
424
    @method _setLazyRender
425
    @return {Boolean} Value.
426
    @protected
427
    **/
428
    _setLazyRender: function (value) {
429
        /*jshint boss:true */
430
        return this._lazyRender = value;
431
    },
432
 
433
    _syncNodeOpenState: function (node, htmlNode) {
434
        htmlNode || (htmlNode = this.getHTMLNode(node));
435
 
436
        if (!htmlNode) {
437
            return;
438
        }
439
 
440
        if (node.isOpen()) {
441
            htmlNode
442
                .addClass(this.classNames.open)
443
                .set('aria-expanded', true);
444
        } else {
445
            htmlNode
446
                .removeClass(this.classNames.open)
447
                .set('aria-expanded', false);
448
        }
449
    },
450
 
451
    _syncNodeSelectedState: function (node, htmlNode) {
452
        htmlNode || (htmlNode = this.getHTMLNode(node));
453
 
454
        if (!htmlNode) {
455
            return;
456
        }
457
 
458
        var multiSelect = this.get('multiSelect');
459
 
460
        if (node.isSelected()) {
461
            htmlNode.addClass(this.classNames.selected);
462
 
463
            if (multiSelect) {
464
                // It's only necessary to set aria-selected when multi-select is
465
                // enabled and focus can't be used to track the selection state.
466
                htmlNode.set('aria-selected', true);
467
            } else {
468
                htmlNode.set('tabIndex', 0);
469
            }
470
        } else {
471
            htmlNode
472
                .removeClass(this.classNames.selected)
473
                .removeAttribute('tabIndex');
474
 
475
            if (multiSelect) {
476
                htmlNode.set('aria-selected', false);
477
            }
478
        }
479
    },
480
 
481
    // -- Protected Event Handlers ---------------------------------------------
482
 
483
    _afterAdd: function (e) {
484
        // Nothing to do if the treeview hasn't been rendered yet.
485
        if (!this.rendered) {
486
            return;
487
        }
488
 
489
        var parent       = e.parent,
490
            parentIsRoot = parent.isRoot(),
491
            treeNode     = e.node,
492
 
493
            htmlChildren,
494
            htmlParent;
495
 
496
        if (parentIsRoot) {
497
            htmlChildren = this._childrenNode;
498
        } else {
499
            htmlParent   = this.getHTMLNode(parent),
500
            htmlChildren = htmlParent && htmlParent.one('>.' + this.classNames.children);
501
        }
502
 
503
        if (htmlChildren) {
504
            // Parent's children have already been rendered. Instead of
505
            // re-rendering all of them, just render the new node and insert it
506
            // at the correct position.
507
            htmlChildren.insert(this.renderNode(treeNode, {
508
                renderChildren: !this._lazyRender || treeNode.isOpen()
509
            }), e.index);
510
 
511
            // Schedule the parent node to be re-rendered in order to update its
512
            // state. This is done asynchronously and throttled in order to
513
            // avoid re-rendering the parent many times if multiple children are
514
            // added in quick succession.
515
            if (!parentIsRoot) {
516
                this._queueRender(parent);
517
            }
518
        } else if (!parentIsRoot) {
519
            // Either the parent hasn't been rendered yet, or its children
520
            // haven't been rendered yet. Schedule it to be rendered. This is
521
            // done asynchronously and throttled in order to avoid re-rendering
522
            // the parent many times if multiple children are added in quick
523
            // succession.
524
            this._queueRender(parent, {renderChildren: true});
525
        }
526
    },
527
 
528
    _afterClear: function () {
529
        // Nothing to do if the treeview hasn't been rendered yet.
530
        if (!this.rendered) {
531
            return;
532
        }
533
 
534
        clearTimeout(this._renderTimeout);
535
        this._renderQueue = {};
536
 
537
        delete this._childrenNode;
538
        this.rendered = false;
539
 
540
        this.get('container').empty();
541
        this.render();
542
    },
543
 
544
    _afterClose: function (e) {
545
        if (this.rendered) {
546
            this._syncNodeOpenState(e.node);
547
        }
548
    },
549
 
550
    _afterOpen: function (e) {
551
        if (!this.rendered) {
552
            return;
553
        }
554
 
555
        var treeNode = e.node,
556
            htmlNode = this.getHTMLNode(treeNode);
557
 
558
        // If this node's children haven't been rendered yet, render them.
559
        if (!treeNode.state.renderedChildren) {
560
            this.renderChildren(treeNode, {
561
                container: htmlNode
562
            });
563
        }
564
 
565
        this._syncNodeOpenState(treeNode, htmlNode);
566
    },
567
 
568
    _afterRemove: function (e) {
569
        if (!this.rendered) {
570
            return;
571
        }
572
 
573
        var treeNode = e.node,
574
            parent   = e.parent;
575
 
576
        // If this node is in the render queue, remove it from the queue.
577
        if (this._renderQueue[treeNode.id]) {
578
            delete this._renderQueue[treeNode.id];
579
        }
580
 
581
        // Remove DOM nodes associated with this node and any of its
582
        // descendants, and mark all nodes as unrendered so that they'll be
583
        // re-rendered if they're reinserted in the tree.
584
        var htmlNode = this.getHTMLNode(treeNode);
585
 
586
        if (htmlNode) {
587
            htmlNode
588
                .empty()
589
                .remove(true);
590
 
591
            treeNode._htmlNode = null;
592
        }
593
 
594
        if (!treeNode.state.destroyed) {
595
            treeNode.traverse(function (node) {
596
                node._htmlNode              = null;
597
                node.state.rendered         = false;
598
                node.state.renderedChildren = false;
599
            });
600
        }
601
 
602
        // Re-render the parent to update its state if this was its last child.
603
        if (parent && !parent.hasChildren()) {
604
            this.renderNode(parent);
605
        }
606
    },
607
 
608
    _afterSelect: function (e) {
609
        if (this.rendered) {
610
            this._syncNodeSelectedState(e.node);
611
        }
612
    },
613
 
614
    _afterTreeViewMultiSelectChange: function (e) {
615
        if (!this.rendered) {
616
            return;
617
        }
618
 
619
        var container = this.get('container'),
620
            rootList  = container.one('> .' + this.classNames.children),
621
            htmlNodes = container.all('.' + this.classNames.node);
622
 
623
        if (e.newVal) {
624
            rootList.set('aria-multiselectable', true);
625
            htmlNodes.set('aria-selected', false);
626
        } else {
627
            // When multiselect is disabled, aria-selected must not be set on
628
            // any nodes, since focus is used to indicate selection.
629
            rootList.removeAttribute('aria-multiselectable');
630
            htmlNodes.removeAttribute('aria-selected');
631
        }
632
    },
633
 
634
    _afterUnselect: function (e) {
635
        if (this.rendered) {
636
            this._syncNodeSelectedState(e.node);
637
        }
638
    },
639
 
640
    _onIndicatorClick: function (e) {
641
        var rowNode = e.currentTarget.ancestor('.' + this.classNames.row);
642
 
643
        // Indicator clicks shouldn't toggle selection state, so don't allow
644
        // this event to propagate to the _onRowClick() handler.
645
        e.stopImmediatePropagation();
646
 
647
        this.getNodeById(rowNode.getData('node-id')).toggleOpen();
648
    },
649
 
650
    _onMouseDown: function (e) {
651
        // This prevents the tree from momentarily grabbing focus before focus
652
        // is set on a node.
653
        e.preventDefault();
654
    },
655
 
656
    _onRowClick: function (e) {
657
        // Ignore buttons other than the left button.
658
        if (e.button > 1) {
659
            return;
660
        }
661
 
662
        var node = this.getNodeById(e.currentTarget.getData('node-id'));
663
 
664
        if (this.get('multiSelect')) {
665
            node[node.isSelected() ? 'unselect' : 'select']();
666
        } else {
667
            node.select();
668
        }
669
    },
670
 
671
    _onRowDoubleClick: function (e) {
672
        // Ignore buttons other than the left button.
673
        if (e.button > 1) {
674
            return;
675
        }
676
 
677
        this.getNodeById(e.currentTarget.getData('node-id')).toggleOpen();
678
    }
679
}, {
680
    ATTRS: {
681
        /**
682
        When `true`, a node's children won't be rendered until the first time
683
        that node is opened.
684
 
685
        This can significantly speed up the time it takes to render a large
686
        tree, but might not make sense if you're using CSS that doesn't hide the
687
        contents of closed nodes.
688
 
689
        @attribute {Boolean} lazyRender
690
        @default true
691
        **/
692
        lazyRender: {
693
            lazyAdd: false, // to ensure that the setter runs on init
694
            setter : '_setLazyRender',
695
            value  : true
696
        }
697
    }
698
});
699
 
700
Y.TreeView = Y.mix(TreeView, Y.TreeView);
701
 
702
 
703
}, 'gallery-2013.06.20-02-07', {
704
    "requires": [
705
        "base-build",
706
        "classnamemanager",
707
        "template-micro",
708
        "tree",
709
        "tree-labelable",
710
        "tree-openable",
711
        "tree-selectable",
712
        "view"
713
    ],
714
    "skinnable": true
715
});