Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('datatable-scroll', function (Y, NAME) {
2
 
3
/**
4
Adds the ability to make the table rows scrollable while preserving the header
5
placement.
6
 
7
@module datatable-scroll
8
@for DataTable
9
@since 3.5.0
10
**/
11
var YLang = Y.Lang,
12
    isString = YLang.isString,
13
    isNumber = YLang.isNumber,
14
    isArray  = YLang.isArray,
15
 
16
    Scrollable;
17
 
18
// Returns the numeric value portion of the computed style, defaulting to 0
19
function styleDim(node, style) {
20
    return parseInt(node.getComputedStyle(style), 10) || 0;
21
}
22
 
23
/**
24
_API docs for this extension are included in the DataTable class._
25
 
26
Adds the ability to make the table rows scrollable while preserving the header
27
placement.
28
 
29
There are two types of scrolling, horizontal (x) and vertical (y).  Horizontal
30
scrolling is achieved by wrapping the entire table in a scrollable container.
31
Vertical scrolling is achieved by splitting the table headers and data into two
32
separate tables, the latter of which is wrapped in a vertically scrolling
33
container.  In this case, column widths of header cells and data cells are kept
34
in sync programmatically.
35
 
36
Since the split table synchronization can be costly at runtime, the split is only
37
done if the data in the table stretches beyond the configured `height` value.
38
 
39
To activate or deactivate scrolling, set the `scrollable` attribute to one of
40
the following values:
41
 
42
 * `false` - (default) Scrolling is disabled.
43
 * `true` or 'xy' - If `height` is set, vertical scrolling will be activated, if
44
            `width` is set, horizontal scrolling will be activated.
45
 * 'x' - Activate horizontal scrolling only. Requires the `width` attribute is
46
         also set.
47
 * 'y' - Activate vertical scrolling only. Requires the `height` attribute is
48
         also set.
49
 
50
@class DataTable.Scrollable
51
@for DataTable
52
@since 3.5.0
53
**/
54
Y.DataTable.Scrollable = Scrollable = function () {};
55
 
56
Scrollable.ATTRS = {
57
    /**
58
    Activates or deactivates scrolling in the table.  Acceptable values are:
59
 
60
     * `false` - (default) Scrolling is disabled.
61
     * `true` or 'xy' - If `height` is set, vertical scrolling will be
62
       activated, if `width` is set, horizontal scrolling will be activated.
63
     * 'x' - Activate horizontal scrolling only. Requires the `width` attribute
64
       is also set.
65
     * 'y' - Activate vertical scrolling only. Requires the `height` attribute
66
       is also set.
67
 
68
    @attribute scrollable
69
    @type {String|Boolean}
70
    @value false
71
    @since 3.5.0
72
    **/
73
    scrollable: {
74
        value: false,
75
        setter: '_setScrollable'
76
    }
77
};
78
 
79
Y.mix(Scrollable.prototype, {
80
 
81
    /**
82
    Scrolls a given row or cell into view if the table is scrolling.  Pass the
83
    `clientId` of a Model from the DataTable's `data` ModelList or its row
84
    index to scroll to a row or a [row index, column index] array to scroll to
85
    a cell.  Alternately, to scroll to any element contained within the table's
86
    scrolling areas, pass its ID, or the Node itself (though you could just as
87
    well call `node.scrollIntoView()` yourself, but hey, whatever).
88
 
89
    @method scrollTo
90
    @param {String|Number|Number[]|Node} id A row clientId, row index, cell
91
            coordinate array, id string, or Node
92
    @return {DataTable}
93
    @chainable
94
    @since 3.5.0
95
    **/
96
    scrollTo: function (id) {
97
        var target;
98
 
99
        if (id && this._tbodyNode && (this._yScrollNode || this._xScrollNode)) {
100
            if (isArray(id)) {
101
                target = this.getCell(id);
102
            } else if (isNumber(id)) {
103
                target = this.getRow(id);
104
            } else if (isString(id)) {
105
                target = this._tbodyNode.one('#' + id);
106
            } else if (id._node &&
107
                    // TODO: ancestor(yScrollNode, xScrollNode)
108
                    id.ancestor('.yui3-datatable') === this.get('boundingBox')) {
109
                target = id;
110
            }
111
 
112
            if(target) {
113
                target.scrollIntoView();
114
            }
115
        }
116
 
117
        return this;
118
    },
119
 
120
    //--------------------------------------------------------------------------
121
    // Protected properties and methods
122
    //--------------------------------------------------------------------------
123
 
124
    /**
125
    Template for the `<table>` that is used to fix the caption in place when
126
    the table is horizontally scrolling.
127
 
128
    @property _CAPTION_TABLE_TEMPLATE
129
    @type {String}
130
    @value '<table class="{className}" role="presentation"></table>'
131
    @protected
132
    @since 3.5.0
133
    **/
134
    _CAPTION_TABLE_TEMPLATE: '<table class="{className}" role="presentation"></table>',
135
 
136
    /**
137
    Template used to create sizable element liners around header content to
138
    synchronize fixed header column widths.
139
 
140
    @property _SCROLL_LINER_TEMPLATE
141
    @type {String}
142
    @value '<div class="{className}"></div>'
143
    @protected
144
    @since 3.5.0
145
    **/
146
    _SCROLL_LINER_TEMPLATE: '<div class="{className}"></div>',
147
 
148
    /**
149
    Template for the virtual scrollbar needed in "y" and "xy" scrolling setups.
150
 
151
    @property _SCROLLBAR_TEMPLATE
152
    @type {String}
153
    @value '<div class="{className}"><div></div></div>'
154
    @protected
155
    @since 3.5.0
156
    **/
157
    _SCROLLBAR_TEMPLATE: '<div class="{className}"><div></div></div>',
158
 
159
    /**
160
    Template for the `<div>` that is used to contain the table when the table is
161
    horizontally scrolling.
162
 
163
    @property _X_SCROLLER_TEMPLATE
164
    @type {String}
165
    @value '<div class="{className}"></div>'
166
    @protected
167
    @since 3.5.0
168
    **/
169
    _X_SCROLLER_TEMPLATE: '<div class="{className}"></div>',
170
 
171
    /**
172
    Template for the `<table>` used to contain the fixed column headers for
173
    vertically scrolling tables.
174
 
175
    @property _Y_SCROLL_HEADER_TEMPLATE
176
    @type {String}
177
    @value '<table cellspacing="0" role="presentation" aria-hidden="true" class="{className}"></table>'
178
    @protected
179
    @since 3.5.0
180
    **/
181
    _Y_SCROLL_HEADER_TEMPLATE: '<table cellspacing="0" aria-hidden="true" class="{className}"></table>',
182
 
183
    /**
184
    Template for the `<div>` that is used to contain the rows when the table is
185
    vertically scrolling.
186
 
187
    @property _Y_SCROLLER_TEMPLATE
188
    @type {String}
189
    @value '<div class="{className}"><div class="{scrollerClassName}"></div></div>'
190
    @protected
191
    @since 3.5.0
192
    **/
193
    _Y_SCROLLER_TEMPLATE: '<div class="{className}"><div class="{scrollerClassName}"></div></div>',
194
 
195
    /**
196
    Adds padding to the last cells in the fixed header for vertically scrolling
197
    tables.  This padding is equal in width to the scrollbar, so can't be
198
    relegated to a stylesheet.
199
 
200
    @method _addScrollbarPadding
201
    @protected
202
    @since 3.5.0
203
    **/
204
    _addScrollbarPadding: function () {
205
        var fixedHeader = this._yScrollHeader,
206
            headerClass = '.' + this.getClassName('header'),
207
            scrollbarWidth, rows, header, i, len;
208
 
209
        if (fixedHeader) {
210
            scrollbarWidth = Y.DOM.getScrollbarWidth() + 'px';
211
            rows = fixedHeader.all('tr');
212
 
213
            for (i = 0, len = rows.size(); i < len; i += +header.get('rowSpan')) {
214
                header = rows.item(i).all(headerClass).pop();
215
                header.setStyle('paddingRight', scrollbarWidth);
216
            }
217
        }
218
    },
219
 
220
    /**
221
    Reacts to changes in the `scrollable` attribute by updating the `_xScroll`
222
    and `_yScroll` properties and syncing the scrolling structure accordingly.
223
 
224
    @method _afterScrollableChange
225
    @param {EventFacade} e The relevant change event (ignored)
226
    @protected
227
    @since 3.5.0
228
    **/
229
    _afterScrollableChange: function () {
230
        var scroller = this._xScrollNode;
231
 
232
        if (this._xScroll && scroller) {
233
            if (this._yScroll && !this._yScrollNode) {
234
                scroller.setStyle('paddingRight',
235
                    Y.DOM.getScrollbarWidth() + 'px');
236
            } else if (!this._yScroll && this._yScrollNode) {
237
                scroller.setStyle('paddingRight', '');
238
            }
239
        }
240
 
241
        this._syncScrollUI();
242
    },
243
 
244
    /**
245
    Reacts to changes in the `caption` attribute by adding, removing, or
246
    syncing the caption table when the table is set to scroll.
247
 
248
    @method _afterScrollCaptionChange
249
    @param {EventFacade} e The relevant change event (ignored)
250
    @protected
251
    @since 3.5.0
252
    **/
253
    _afterScrollCaptionChange: function () {
254
        if (this._xScroll || this._yScroll) {
255
            this._syncScrollUI();
256
        }
257
    },
258
 
259
    /**
260
    Reacts to changes in the `columns` attribute of vertically scrolling tables
261
    by refreshing the fixed headers, scroll container, and virtual scrollbar
262
    position.
263
 
264
    @method _afterScrollColumnsChange
265
    @param {EventFacade} e The relevant change event (ignored)
266
    @protected
267
    @since 3.5.0
268
    **/
269
    _afterScrollColumnsChange: function () {
270
        if (this._xScroll || this._yScroll) {
271
            if (this._yScroll && this._yScrollHeader) {
272
                this._syncScrollHeaders();
273
            }
274
 
275
            this._syncScrollUI();
276
        }
277
    },
278
 
279
    /**
280
    Reacts to changes in vertically scrolling table's `data` ModelList by
281
    synchronizing the fixed column header widths and virtual scrollbar height.
282
 
283
    @method _afterScrollDataChange
284
    @param {EventFacade} e The relevant change event (ignored)
285
    @protected
286
    @since 3.5.0
287
    **/
288
    _afterScrollDataChange: function () {
289
        if (this._xScroll || this._yScroll) {
290
            this._syncScrollUI();
291
        }
292
    },
293
 
294
    /**
295
    Reacts to changes in the `height` attribute of vertically scrolling tables
296
    by updating the height of the `<div>` wrapping the data table and the
297
    virtual scrollbar.  If `scrollable` was set to "y" or "xy" but lacking a
298
    declared `height` until the received change, `_syncScrollUI` is called to
299
    create the fixed headers etc.
300
 
301
    @method _afterScrollHeightChange
302
    @param {EventFacade} e The relevant change event (ignored)
303
    @protected
304
    @since 3.5.0
305
    **/
306
    _afterScrollHeightChange: function () {
307
        if (this._yScroll) {
308
            this._syncScrollUI();
309
        }
310
    },
311
 
312
    /* (not an API doc comment on purpose)
313
    Reacts to the sort event (if the table is also sortable) by updating the
314
    fixed header classes to match the data table's headers.
315
 
316
    THIS IS A HACK that will be removed immediately after the 3.5.0 release.
317
    If you're reading this and the current version is greater than 3.5.0, I
318
    should be publicly scolded.
319
    */
320
    _afterScrollSort: function () {
321
        var headers, headerClass;
322
 
323
        if (this._yScroll && this._yScrollHeader) {
324
            headerClass = '.' + this.getClassName('header');
325
            headers = this._theadNode.all(headerClass);
326
 
327
            this._yScrollHeader.all(headerClass).each(function (header, i) {
328
                header.set('className', headers.item(i).get('className'));
329
            });
330
        }
331
    },
332
 
333
    /**
334
    Reacts to changes in the width of scrolling tables by expanding the width of
335
    the `<div>` wrapping the data table for horizontally scrolling tables or
336
    upding the position of the virtual scrollbar for vertically scrolling
337
    tables.
338
 
339
    @method _afterScrollWidthChange
340
    @param {EventFacade} e The relevant change event (ignored)
341
    @protected
342
    @since 3.5.0
343
    **/
344
    _afterScrollWidthChange: function () {
345
        if (this._xScroll || this._yScroll) {
346
            this._syncScrollUI();
347
        }
348
    },
349
 
350
    /**
351
    Binds virtual scrollbar interaction to the `_yScrollNode`'s `scrollTop` and
352
    vice versa.
353
 
354
    @method _bindScrollbar
355
    @protected
356
    @since 3.5.0
357
    **/
358
    _bindScrollbar: function () {
359
        var scrollbar = this._scrollbarNode,
360
            scroller  = this._yScrollNode;
361
 
362
        if (scrollbar && scroller && !this._scrollbarEventHandle) {
363
            this._scrollbarEventHandle = new Y.Event.Handle([
364
                scrollbar.on('scroll', this._syncScrollPosition, this),
365
                scroller.on('scroll', this._syncScrollPosition, this)
366
            ]);
367
        }
368
    },
369
 
370
    /**
371
    Binds to the window resize event to update the vertical scrolling table
372
    headers and wrapper `<div>` dimensions.
373
 
374
    @method _bindScrollResize
375
    @protected
376
    @since 3.5.0
377
    **/
378
    _bindScrollResize: function () {
379
        if (!this._scrollResizeHandle) {
380
            // TODO: sync header widths and scrollbar position.  If the height
381
            // of the headers has changed, update the scrollbar dims as well.
382
            this._scrollResizeHandle = Y.on('resize',
383
                this._syncScrollUI, null, this);
384
        }
385
    },
386
 
387
    /**
388
    Attaches internal subscriptions to keep the scrolling structure up to date
389
    with changes in the table's `data`, `columns`, `caption`, or `height`.  The
390
    `width` is taken care of already.
391
 
392
    This executes after the table's native `bindUI` method.
393
 
394
    @method _bindScrollUI
395
    @protected
396
    @since 3.5.0
397
    **/
398
    _bindScrollUI: function () {
399
        this.after({
400
            columnsChange: Y.bind('_afterScrollColumnsChange', this),
401
            heightChange : Y.bind('_afterScrollHeightChange', this),
402
            widthChange  : Y.bind('_afterScrollWidthChange', this),
403
            captionChange: Y.bind('_afterScrollCaptionChange', this),
404
            scrollableChange: Y.bind('_afterScrollableChange', this),
405
            // FIXME: this is a last minute hack to work around the fact that
406
            // DT doesn't use a tableView to render table content that can be
407
            // replaced with a scrolling table view.  This must be removed asap!
408
            sort         : Y.bind('_afterScrollSort', this)
409
        });
410
 
411
        this.after(['dataChange', '*:add', '*:remove', '*:reset', '*:change'],
412
            Y.bind('_afterScrollDataChange', this));
413
    },
414
 
415
    /**
416
    Clears the lock and timer used to manage synchronizing the scroll position
417
    between the vertical scroll container and the virtual scrollbar.
418
 
419
    @method _clearScrollLock
420
    @protected
421
    @since 3.5.0
422
    **/
423
    _clearScrollLock: function () {
424
        if (this._scrollLock) {
425
            this._scrollLock.cancel();
426
            delete this._scrollLock;
427
        }
428
    },
429
 
430
    /**
431
    Creates a virtual scrollbar from the `_SCROLLBAR_TEMPLATE`, assigning it to
432
    the `_scrollbarNode` property.
433
 
434
    @method _createScrollbar
435
    @return {Node} The created Node
436
    @protected
437
    @since 3.5.0
438
    **/
439
    _createScrollbar: function () {
440
        var scrollbar = this._scrollbarNode;
441
 
442
        if (!scrollbar) {
443
            scrollbar = this._scrollbarNode = Y.Node.create(
444
                Y.Lang.sub(this._SCROLLBAR_TEMPLATE, {
445
                    className: this.getClassName('scrollbar')
446
                }));
447
 
448
            // IE 6-10 require the scrolled area to be visible (at least 1px)
449
            // or they don't respond to clicking on the scrollbar rail or arrows
450
            scrollbar.setStyle('width', (Y.DOM.getScrollbarWidth() + 1) + 'px');
451
        }
452
 
453
        return scrollbar;
454
    },
455
 
456
    /**
457
    Creates a separate table to contain the caption when the table is
458
    configured to scroll vertically or horizontally.
459
 
460
    @method _createScrollCaptionTable
461
    @return {Node} The created Node
462
    @protected
463
    @since 3.5.0
464
    **/
465
    _createScrollCaptionTable: function () {
466
        if (!this._captionTable) {
467
            this._captionTable = Y.Node.create(
468
                Y.Lang.sub(this._CAPTION_TABLE_TEMPLATE, {
469
                    className: this.getClassName('caption', 'table')
470
                }));
471
 
472
            this._captionTable.empty();
473
        }
474
 
475
        return this._captionTable;
476
    },
477
 
478
    /**
479
    Populates the `_xScrollNode` property by creating the `<div>` Node described
480
    by the `_X_SCROLLER_TEMPLATE`.
481
 
482
    @method _createXScrollNode
483
    @return {Node} The created Node
484
    @protected
485
    @since 3.5.0
486
    **/
487
    _createXScrollNode: function () {
488
        if (!this._xScrollNode) {
489
            this._xScrollNode = Y.Node.create(
490
                Y.Lang.sub(this._X_SCROLLER_TEMPLATE, {
491
                    className: this.getClassName('x','scroller')
492
                }));
493
        }
494
 
495
        return this._xScrollNode;
496
    },
497
 
498
    /**
499
    Populates the `_yScrollHeader` property by creating the `<table>` Node
500
    described by the `_Y_SCROLL_HEADER_TEMPLATE`.
501
 
502
    @method _createYScrollHeader
503
    @return {Node} The created Node
504
    @protected
505
    @since 3.5.0
506
    **/
507
    _createYScrollHeader: function () {
508
        var fixedHeader = this._yScrollHeader;
509
 
510
        if (!fixedHeader) {
511
            fixedHeader = this._yScrollHeader = Y.Node.create(
512
                Y.Lang.sub(this._Y_SCROLL_HEADER_TEMPLATE, {
513
                    className: this.getClassName('scroll','columns')
514
                }));
515
        }
516
 
517
        return fixedHeader;
518
    },
519
 
520
    /**
521
    Populates the `_yScrollNode` property by creating the `<div>` Node described
522
    by the `_Y_SCROLLER_TEMPLATE`.
523
 
524
    @method _createYScrollNode
525
    @return {Node} The created Node
526
    @protected
527
    @since 3.5.0
528
    **/
529
    _createYScrollNode: function () {
530
        var scrollerClass;
531
 
532
        if (!this._yScrollNode) {
533
            scrollerClass = this.getClassName('y', 'scroller');
534
 
535
            this._yScrollContainer = Y.Node.create(
536
                Y.Lang.sub(this._Y_SCROLLER_TEMPLATE, {
537
                    className: this.getClassName('y','scroller','container'),
538
                    scrollerClassName: scrollerClass
539
                }));
540
 
541
            this._yScrollNode = this._yScrollContainer
542
                .one('.' + scrollerClass);
543
        }
544
 
545
        return this._yScrollContainer;
546
    },
547
 
548
    /**
549
    Removes the nodes used to create horizontal and vertical scrolling and
550
    rejoins the caption to the main table if needed.
551
 
552
    @method _disableScrolling
553
    @protected
554
    @since 3.5.0
555
    **/
556
    _disableScrolling: function () {
557
        this._removeScrollCaptionTable();
558
        this._disableXScrolling();
559
        this._disableYScrolling();
560
        this._unbindScrollResize();
561
 
562
        this._uiSetWidth(this.get('width'));
563
    },
564
 
565
    /**
566
    Removes the nodes used to allow horizontal scrolling.
567
 
568
    @method _disableXScrolling
569
    @protected
570
    @since 3.5.0
571
    **/
572
    _disableXScrolling: function () {
573
        this._removeXScrollNode();
574
    },
575
 
576
    /**
577
    Removes the nodes used to allow vertical scrolling.
578
 
579
    @method _disableYScrolling
580
    @protected
581
    @since 3.5.0
582
    **/
583
    _disableYScrolling: function () {
584
        this._removeYScrollHeader();
585
        this._removeYScrollNode();
586
        this._removeYScrollContainer();
587
        this._removeScrollbar();
588
    },
589
 
590
    /**
591
    Cleans up external event subscriptions.
592
 
593
    @method destructor
594
    @protected
595
    @since 3.5.0
596
    **/
597
    destructor: function () {
598
        this._unbindScrollbar();
599
        this._unbindScrollResize();
600
        this._clearScrollLock();
601
    },
602
 
603
    /**
604
    Sets up event handlers and AOP advice methods to bind the DataTable's natural
605
    behaviors with the scrolling APIs and state.
606
 
607
    @method initializer
608
    @param {Object} config The config object passed to the constructor (ignored)
609
    @protected
610
    @since 3.5.0
611
    **/
612
    initializer: function () {
613
        this._setScrollProperties();
614
 
615
        this.after(['scrollableChange', 'heightChange', 'widthChange'],
616
            this._setScrollProperties);
617
 
618
        this.after('renderView', Y.bind('_syncScrollUI', this));
619
 
620
        Y.Do.after(this._bindScrollUI, this, 'bindUI');
621
    },
622
 
623
    /**
624
    Removes the table used to house the caption when the table is scrolling.
625
 
626
    @method _removeScrollCaptionTable
627
    @protected
628
    @since 3.5.0
629
    **/
630
    _removeScrollCaptionTable: function () {
631
        if (this._captionTable) {
632
            if (this._captionNode) {
633
                this._tableNode.prepend(this._captionNode);
634
            }
635
 
636
            this._captionTable.remove().destroy(true);
637
 
638
            delete this._captionTable;
639
        }
640
    },
641
 
642
    /**
643
    Removes the `<div>` wrapper used to contain the data table when the table
644
    is horizontally scrolling.
645
 
646
    @method _removeXScrollNode
647
    @protected
648
    @since 3.5.0
649
    **/
650
    _removeXScrollNode: function () {
651
        var scroller = this._xScrollNode;
652
 
653
        if (scroller) {
654
            scroller.replace(scroller.get('childNodes').toFrag());
655
            scroller.remove().destroy(true);
656
 
657
            delete this._xScrollNode;
658
        }
659
    },
660
 
661
    /**
662
    Removes the `<div>` wrapper used to contain the data table and fixed header
663
    when the table is vertically scrolling.
664
 
665
    @method _removeYScrollContainer
666
    @protected
667
    @since 3.5.0
668
    **/
669
    _removeYScrollContainer: function () {
670
        var scroller = this._yScrollContainer;
671
 
672
        if (scroller) {
673
            scroller.replace(scroller.get('childNodes').toFrag());
674
            scroller.remove().destroy(true);
675
 
676
            delete this._yScrollContainer;
677
        }
678
    },
679
 
680
    /**
681
    Removes the `<table>` used to contain the fixed column headers when the
682
    table is vertically scrolling.
683
 
684
    @method _removeYScrollHeader
685
    @protected
686
    @since 3.5.0
687
    **/
688
    _removeYScrollHeader: function () {
689
        if (this._yScrollHeader) {
690
            this._yScrollHeader.remove().destroy(true);
691
 
692
            delete this._yScrollHeader;
693
        }
694
    },
695
 
696
    /**
697
    Removes the `<div>` wrapper used to contain the data table when the table
698
    is vertically scrolling.
699
 
700
    @method _removeYScrollNode
701
    @protected
702
    @since 3.5.0
703
    **/
704
    _removeYScrollNode: function () {
705
        var scroller = this._yScrollNode;
706
 
707
        if (scroller) {
708
            scroller.replace(scroller.get('childNodes').toFrag());
709
            scroller.remove().destroy(true);
710
 
711
            delete this._yScrollNode;
712
        }
713
    },
714
 
715
    /**
716
    Removes the virtual scrollbar used by scrolling tables.
717
 
718
    @method _removeScrollbar
719
    @protected
720
    @since 3.5.0
721
    **/
722
    _removeScrollbar: function () {
723
        if (this._scrollbarNode) {
724
            this._scrollbarNode.remove().destroy(true);
725
 
726
            delete this._scrollbarNode;
727
        }
728
        if (this._scrollbarEventHandle) {
729
            this._scrollbarEventHandle.detach();
730
 
731
            delete this._scrollbarEventHandle;
732
        }
733
    },
734
 
735
    /**
736
    Accepts (case insensitive) values "x", "y", "xy", `true`, and `false`.
737
    `true` is translated to "xy" and upper case values are converted to lower
738
    case.  All other values are invalid.
739
 
740
    @method _setScrollable
741
    @param {String|Boolean} val Incoming value for the `scrollable` attribute
742
    @return {String}
743
    @protected
744
    @since 3.5.0
745
    **/
746
    _setScrollable: function (val) {
747
        if (val === true) {
748
            val = 'xy';
749
        }
750
 
751
        if (isString(val)) {
752
            val = val.toLowerCase();
753
        }
754
 
755
        return (val === false || val === 'y' || val === 'x' || val === 'xy') ?
756
            val :
757
            Y.Attribute.INVALID_VALUE;
758
    },
759
 
760
    /**
761
    Assigns the `_xScroll` and `_yScroll` properties to true if an
762
    appropriate value is set in the `scrollable` attribute and the `height`
763
    and/or `width` is set.
764
 
765
    @method _setScrollProperties
766
    @protected
767
    @since 3.5.0
768
    **/
769
    _setScrollProperties: function () {
770
        var scrollable = this.get('scrollable') || '',
771
            width      = this.get('width'),
772
            height     = this.get('height');
773
 
774
        this._xScroll = width  && scrollable.indexOf('x') > -1;
775
        this._yScroll = height && scrollable.indexOf('y') > -1;
776
    },
777
 
778
    /**
779
    Keeps the virtual scrollbar and the scrolling `<div>` wrapper around the
780
    data table in vertically scrolling tables in sync.
781
 
782
    @method _syncScrollPosition
783
    @param {DOMEventFacade} e The scroll event
784
    @protected
785
    @since 3.5.0
786
    **/
787
    _syncScrollPosition: function (e) {
788
        var scrollbar = this._scrollbarNode,
789
            scroller  = this._yScrollNode,
790
            source    = e.currentTarget,
791
            other;
792
 
793
        if (scrollbar && scroller) {
794
            if (this._scrollLock && this._scrollLock.source !== source) {
795
                return;
796
            }
797
 
798
            this._clearScrollLock();
799
            this._scrollLock = Y.later(300, this, this._clearScrollLock);
800
            this._scrollLock.source = source;
801
 
802
            other = (source === scrollbar) ? scroller : scrollbar;
803
            other.set('scrollTop', source.get('scrollTop'));
804
        }
805
    },
806
 
807
    /**
808
    Splits the caption from the data `<table>` if the table is configured to
809
    scroll.  If not, rejoins the caption to the data `<table>` if it needs to
810
    be.
811
 
812
    @method _syncScrollCaptionUI
813
    @protected
814
    @since 3.5.0
815
    **/
816
    _syncScrollCaptionUI: function () {
817
        var caption      = this._captionNode,
818
            table        = this._tableNode,
819
            captionTable = this._captionTable,
820
            id;
821
 
822
        if (caption) {
823
            id = caption.getAttribute('id');
824
 
825
            if (!captionTable) {
826
                captionTable = this._createScrollCaptionTable();
827
 
828
                this.get('contentBox').prepend(captionTable);
829
            }
830
 
831
            if (!caption.get('parentNode').compareTo(captionTable)) {
832
                captionTable.empty().insert(caption);
833
 
834
                if (!id) {
835
                    id = Y.stamp(caption);
836
                    caption.setAttribute('id', id);
837
                }
838
 
839
                table.setAttribute('aria-describedby', id);
840
            }
841
        } else if (captionTable) {
842
            this._removeScrollCaptionTable();
843
        }
844
    },
845
 
846
    /**
847
    Assigns widths to the fixed header columns to match the columns in the data
848
    table.
849
 
850
    @method _syncScrollColumnWidths
851
    @protected
852
    @since 3.5.0
853
    **/
854
    _syncScrollColumnWidths: function () {
855
        var widths = [];
856
 
857
        if (this._theadNode && this._yScrollHeader) {
858
            // Capture dims and assign widths in two passes to avoid reflows for
859
            // each access of clientWidth/getComputedStyle
860
            this._theadNode.all('.' + this.getClassName('header'))
861
                .each(function (header) {
862
                    widths.push(
863
                        // FIXME: IE returns the col.style.width from
864
                        // getComputedStyle even if the column has been
865
                        // compressed below that width, so it must use
866
                        // clientWidth. FF requires getComputedStyle because it
867
                        // uses fractional widths that round up to an overall
868
                        // cell/table width 1px greater than the data table's
869
                        // cell/table width, resulting in misaligned columns or
870
                        // fixed header bleed through. I can't think of a
871
                        // *reasonable* way to capture the correct width without
872
                        // a sniff.  Math.min(cW - p, getCS(w)) was imperfect
873
                        // and punished all browsers, anyway.
874
                        (Y.UA.ie && Y.UA.ie < 8) ?
875
                            (header.get('clientWidth') -
876
                             styleDim(header, 'paddingLeft') -
877
                             styleDim(header, 'paddingRight')) + 'px' :
878
                            header.getComputedStyle('width'));
879
            });
880
 
881
            this._yScrollHeader.all('.' + this.getClassName('scroll', 'liner'))
882
                .each(function (liner, i) {
883
                    liner.setStyle('width', widths[i]);
884
                });
885
        }
886
    },
887
 
888
    /**
889
    Creates matching headers in the fixed header table for vertically scrolling
890
    tables and synchronizes the column widths.
891
 
892
    @method _syncScrollHeaders
893
    @protected
894
    @since 3.5.0
895
    **/
896
    _syncScrollHeaders: function () {
897
        var fixedHeader   = this._yScrollHeader,
898
            linerTemplate = this._SCROLL_LINER_TEMPLATE,
899
            linerClass    = this.getClassName('scroll', 'liner'),
900
            headerClass   = this.getClassName('header'),
901
            headers       = this._theadNode.all('.' + headerClass);
902
 
903
        if (this._theadNode && fixedHeader) {
904
            fixedHeader.empty().appendChild(
905
                this._theadNode.cloneNode(true));
906
 
907
            // Prevent duplicate IDs and assign ARIA attributes to hide
908
            // from screen readers
909
            fixedHeader.all('[id]').removeAttribute('id');
910
 
911
            fixedHeader.all('.' + headerClass).each(function (header, i) {
912
                var liner = Y.Node.create(Y.Lang.sub(linerTemplate, {
913
                            className: linerClass
914
                        })),
915
                    refHeader = headers.item(i);
916
 
917
                // Can't assign via skin css because sort (and potentially
918
                // others) might override the padding values.
919
                liner.setStyle('padding',
920
                    refHeader.getComputedStyle('paddingTop') + ' ' +
921
                    refHeader.getComputedStyle('paddingRight') + ' ' +
922
                    refHeader.getComputedStyle('paddingBottom') + ' ' +
923
                    refHeader.getComputedStyle('paddingLeft'));
924
 
925
                liner.appendChild(header.get('childNodes').toFrag());
926
 
927
                header.appendChild(liner);
928
            }, this);
929
 
930
            this._syncScrollColumnWidths();
931
 
932
            this._addScrollbarPadding();
933
        }
934
    },
935
 
936
    /**
937
    Wraps the table for X and Y scrolling, if necessary, if the `scrollable`
938
    attribute is set.  Synchronizes dimensions and DOM placement of all
939
    scrolling related nodes.
940
 
941
    @method _syncScrollUI
942
    @protected
943
    @since 3.5.0
944
    **/
945
    _syncScrollUI: function () {
946
        var x = this._xScroll,
947
            y = this._yScroll,
948
            xScroller  = this._xScrollNode,
949
            yScroller  = this._yScrollNode,
950
            scrollLeft = xScroller && xScroller.get('scrollLeft'),
951
            scrollTop  = yScroller && yScroller.get('scrollTop');
952
 
953
        this._uiSetScrollable();
954
 
955
        // TODO: Probably should split this up into syncX, syncY, and syncXY
956
        if (x || y) {
957
            if ((this.get('width') || '').slice(-1) === '%') {
958
                this._bindScrollResize();
959
            } else {
960
                this._unbindScrollResize();
961
            }
962
 
963
            this._syncScrollCaptionUI();
964
        } else {
965
            this._disableScrolling();
966
        }
967
 
968
        if (this._yScrollHeader) {
969
            this._yScrollHeader.setStyle('display', 'none');
970
        }
971
 
972
        if (x) {
973
            if (!y) {
974
                this._disableYScrolling();
975
            }
976
 
977
            this._syncXScrollUI(y);
978
        }
979
 
980
        if (y) {
981
            if (!x) {
982
                this._disableXScrolling();
983
            }
984
 
985
            this._syncYScrollUI(x);
986
        }
987
 
988
        // Restore scroll position
989
        if (scrollLeft && this._xScrollNode) {
990
            this._xScrollNode.set('scrollLeft', scrollLeft);
991
        }
992
        if (scrollTop && this._yScrollNode) {
993
            this._yScrollNode.set('scrollTop', scrollTop);
994
        }
995
    },
996
 
997
    /**
998
    Wraps the table in a scrolling `<div>` of the configured width for "x"
999
    scrolling.
1000
 
1001
    @method _syncXScrollUI
1002
    @param {Boolean} xy True if the table is configured with scrollable ="xy"
1003
    @protected
1004
    @since 3.5.0
1005
    **/
1006
    _syncXScrollUI: function (xy) {
1007
        var scroller     = this._xScrollNode,
1008
            yScroller    = this._yScrollContainer,
1009
            table        = this._tableNode,
1010
            width        = this.get('width'),
1011
            bbWidth      = this.get('boundingBox').get('offsetWidth'),
1012
            scrollbarWidth = Y.DOM.getScrollbarWidth(),
1013
            borderWidth, tableWidth;
1014
 
1015
        if (!scroller) {
1016
            scroller = this._createXScrollNode();
1017
 
1018
            // Not using table.wrap() because IE went all crazy, wrapping the
1019
            // table in the last td in the table itself.
1020
            (yScroller || table).replace(scroller).appendTo(scroller);
1021
        }
1022
 
1023
        // Can't use offsetHeight - clientHeight because IE6 returns
1024
        // clientHeight of 0 intially.
1025
        borderWidth = styleDim(scroller, 'borderLeftWidth') +
1026
                      styleDim(scroller, 'borderRightWidth');
1027
 
1028
        scroller.setStyle('width', '');
1029
        this._uiSetDim('width', '');
1030
        if (xy && this._yScrollContainer) {
1031
            this._yScrollContainer.setStyle('width', '');
1032
        }
1033
 
1034
        // Lock the table's unconstrained width to avoid configured column
1035
        // widths being ignored
1036
        if (Y.UA.ie && Y.UA.ie < 8) {
1037
            // Have to assign a style and trigger a reflow to allow the
1038
            // subsequent clearing of width + reflow to expand the table to
1039
            // natural width in IE 6
1040
            table.setStyle('width', width);
1041
            table.get('offsetWidth');
1042
        }
1043
        table.setStyle('width', '');
1044
        tableWidth = table.get('offsetWidth');
1045
        table.setStyle('width', tableWidth + 'px');
1046
 
1047
        this._uiSetDim('width', width);
1048
 
1049
        // Can't use 100% width because the borders add additional width
1050
        // TODO: Cache the border widths, though it won't prevent a reflow
1051
        scroller.setStyle('width', (bbWidth - borderWidth) + 'px');
1052
 
1053
        // expand the table to fill the assigned width if it doesn't
1054
        // already overflow the configured width
1055
        if ((scroller.get('offsetWidth') - borderWidth) > tableWidth) {
1056
            // Assumes the wrapped table doesn't have borders
1057
            if (xy) {
1058
                table.setStyle('width', (scroller.get('offsetWidth') -
1059
                     borderWidth - scrollbarWidth) + 'px');
1060
            } else {
1061
                table.setStyle('width', '100%');
1062
            }
1063
        }
1064
    },
1065
 
1066
    /**
1067
    Wraps the table in a scrolling `<div>` of the configured height (accounting
1068
    for the caption if there is one) if "y" scrolling is enabled.  Otherwise,
1069
    unwraps the table if necessary.
1070
 
1071
    @method _syncYScrollUI
1072
    @param {Boolean} xy True if the table is configured with scrollable = "xy"
1073
    @protected
1074
    @since 3.5.0
1075
    **/
1076
    _syncYScrollUI: function (xy) {
1077
        var yScroller    = this._yScrollContainer,
1078
            yScrollNode  = this._yScrollNode,
1079
            xScroller    = this._xScrollNode,
1080
            fixedHeader  = this._yScrollHeader,
1081
            scrollbar    = this._scrollbarNode,
1082
            table        = this._tableNode,
1083
            thead        = this._theadNode,
1084
            captionTable = this._captionTable,
1085
            boundingBox  = this.get('boundingBox'),
1086
            contentBox   = this.get('contentBox'),
1087
            width        = this.get('width'),
1088
            height       = boundingBox.get('offsetHeight'),
1089
            scrollbarWidth = Y.DOM.getScrollbarWidth(),
1090
            outerScroller;
1091
 
1092
        if (captionTable && !xy) {
1093
            captionTable.setStyle('width', width || '100%');
1094
        }
1095
 
1096
        if (!yScroller) {
1097
            yScroller = this._createYScrollNode();
1098
 
1099
            yScrollNode = this._yScrollNode;
1100
 
1101
            table.replace(yScroller).appendTo(yScrollNode);
1102
        }
1103
 
1104
        outerScroller = xy ? xScroller : yScroller;
1105
 
1106
        if (!xy) {
1107
            table.setStyle('width', '');
1108
        }
1109
 
1110
        // Set the scroller height
1111
        if (xy) {
1112
            // Account for the horizontal scrollbar in the overall height
1113
            height -= scrollbarWidth;
1114
        }
1115
 
1116
        yScrollNode.setStyle('height',
1117
            (height - outerScroller.get('offsetTop') -
1118
            // because IE6 is returning clientHeight 0 initially
1119
            styleDim(outerScroller, 'borderTopWidth') -
1120
            styleDim(outerScroller, 'borderBottomWidth')) + 'px');
1121
 
1122
        // Set the scroller width
1123
        if (xy) {
1124
            // For xy scrolling tables, the table should expand freely within
1125
            // the x scroller
1126
            yScroller.setStyle('width',
1127
                (table.get('offsetWidth') + scrollbarWidth) + 'px');
1128
        } else {
1129
            this._uiSetYScrollWidth(width);
1130
        }
1131
 
1132
        if (captionTable && !xy) {
1133
            captionTable.setStyle('width', yScroller.get('offsetWidth') + 'px');
1134
        }
1135
 
1136
        // Allow headerless scrolling
1137
        if (thead && !fixedHeader) {
1138
            fixedHeader = this._createYScrollHeader();
1139
 
1140
            yScroller.prepend(fixedHeader);
1141
 
1142
            this._syncScrollHeaders();
1143
        }
1144
 
1145
        if (fixedHeader) {
1146
            this._syncScrollColumnWidths();
1147
 
1148
            fixedHeader.setStyle('display', '');
1149
            // This might need to come back if FF has issues
1150
            //fixedHeader.setStyle('width', '100%');
1151
                //(yScroller.get('clientWidth') + scrollbarWidth) + 'px');
1152
 
1153
            if (!scrollbar) {
1154
                scrollbar = this._createScrollbar();
1155
 
1156
                this._bindScrollbar();
1157
 
1158
                contentBox.prepend(scrollbar);
1159
            }
1160
 
1161
            this._uiSetScrollbarHeight();
1162
            this._uiSetScrollbarPosition(outerScroller);
1163
        }
1164
    },
1165
 
1166
    /**
1167
    Assigns the appropriate class to the `boundingBox` to identify the DataTable
1168
    as horizontally scrolling, vertically scrolling, or both (adds both classes).
1169
 
1170
    Classes added are "yui3-datatable-scrollable-x" or "...-y"
1171
 
1172
    @method _uiSetScrollable
1173
    @protected
1174
    @since 3.5.0
1175
    **/
1176
    _uiSetScrollable: function () {
1177
        this.get('boundingBox')
1178
            .toggleClass(this.getClassName('scrollable','x'), this._xScroll)
1179
            .toggleClass(this.getClassName('scrollable','y'), this._yScroll);
1180
    },
1181
 
1182
    /**
1183
    Updates the virtual scrollbar's height to avoid overlapping with the fixed
1184
    headers.
1185
 
1186
    @method _uiSetScrollbarHeight
1187
    @protected
1188
    @since 3.5.0
1189
    **/
1190
    _uiSetScrollbarHeight: function () {
1191
        var scrollbar   = this._scrollbarNode,
1192
            scroller    = this._yScrollNode,
1193
            fixedHeader = this._yScrollHeader;
1194
 
1195
        if (scrollbar && scroller && fixedHeader) {
1196
            scrollbar.get('firstChild').setStyle('height',
1197
                this._tbodyNode.get('scrollHeight') + 'px');
1198
 
1199
            scrollbar.setStyle('height',
1200
                (parseFloat(scroller.getComputedStyle('height')) -
1201
                 parseFloat(fixedHeader.getComputedStyle('height'))) + 'px');
1202
        }
1203
    },
1204
 
1205
    /**
1206
    Updates the virtual scrollbar's placement to avoid overlapping the fixed
1207
    headers or the data table.
1208
 
1209
    @method _uiSetScrollbarPosition
1210
    @param {Node} scroller Reference node to position the scrollbar over
1211
    @protected
1212
    @since 3.5.0
1213
    **/
1214
    _uiSetScrollbarPosition: function (scroller) {
1215
        var scrollbar     = this._scrollbarNode,
1216
            fixedHeader   = this._yScrollHeader;
1217
 
1218
        if (scrollbar && scroller && fixedHeader) {
1219
            scrollbar.setStyles({
1220
                // Using getCS instead of offsetHeight because FF uses
1221
                // fractional values, but reports ints to offsetHeight, so
1222
                // offsetHeight is unreliable.  It is probably fine to use
1223
                // offsetHeight in this case but this was left in place after
1224
                // fixing an off-by-1px issue in FF 10- by fixing the caption
1225
                // font style so FF picked it up.
1226
                top: (parseFloat(fixedHeader.getComputedStyle('height')) +
1227
                      styleDim(scroller, 'borderTopWidth') +
1228
                      scroller.get('offsetTop')) + 'px',
1229
 
1230
                // Minus 1 because IE 6-10 require the scrolled area to be
1231
                // visible by at least 1px or it won't respond to clicks on the
1232
                // scrollbar rail or endcap arrows.
1233
                left: (scroller.get('offsetWidth') -
1234
                       Y.DOM.getScrollbarWidth() - 1 -
1235
                       styleDim(scroller, 'borderRightWidth')) + 'px'
1236
            });
1237
        }
1238
    },
1239
 
1240
    /**
1241
    Assigns the width of the `<div>` wrapping the data table in vertically
1242
    scrolling tables.
1243
 
1244
    If the table can't compress to the specified width, the container is
1245
    expanded accordingly.
1246
 
1247
    @method _uiSetYScrollWidth
1248
    @param {String} width The CSS width to attempt to set
1249
    @protected
1250
    @since 3.5.0
1251
    **/
1252
    _uiSetYScrollWidth: function (width) {
1253
        var scroller = this._yScrollContainer,
1254
            table    = this._tableNode,
1255
            tableWidth, borderWidth, scrollerWidth, scrollbarWidth;
1256
 
1257
        if (scroller && table) {
1258
            scrollbarWidth = Y.DOM.getScrollbarWidth();
1259
 
1260
            if (width) {
1261
                // Assumes no table border
1262
                borderWidth = scroller.get('offsetWidth') -
1263
                              scroller.get('clientWidth') +
1264
                              scrollbarWidth; // added back at the end
1265
 
1266
                // The table's rendered width might be greater than the
1267
                // configured width
1268
                scroller.setStyle('width', width);
1269
 
1270
                // Have to subtract the border width from the configured width
1271
                // because the scroller's width will need to be reduced by the
1272
                // border width as well during the width reassignment below.
1273
                scrollerWidth = scroller.get('clientWidth') - borderWidth;
1274
 
1275
                // Assumes no table borders
1276
                table.setStyle('width', scrollerWidth + 'px');
1277
 
1278
                tableWidth = table.get('offsetWidth');
1279
 
1280
                // Expand the scroll node width if the table can't fit.
1281
                // Otherwise, reassign the scroller a pixel width that
1282
                // accounts for the borders.
1283
                scroller.setStyle('width',
1284
                    (tableWidth + scrollbarWidth) + 'px');
1285
            } else {
1286
                // Allow the table to expand naturally
1287
                table.setStyle('width', '');
1288
                scroller.setStyle('width', '');
1289
 
1290
                scroller.setStyle('width',
1291
                    (table.get('offsetWidth') + scrollbarWidth) + 'px');
1292
            }
1293
        }
1294
    },
1295
 
1296
    /**
1297
    Detaches the scroll event subscriptions used to maintain scroll position
1298
    parity between the scrollable `<div>` wrapper around the data table and the
1299
    virtual scrollbar for vertically scrolling tables.
1300
 
1301
    @method _unbindScrollbar
1302
    @protected
1303
    @since 3.5.0
1304
    **/
1305
    _unbindScrollbar: function () {
1306
        if (this._scrollbarEventHandle) {
1307
            this._scrollbarEventHandle.detach();
1308
        }
1309
    },
1310
 
1311
    /**
1312
    Detaches the resize event subscription used to maintain column parity for
1313
    vertically scrolling tables with percentage widths.
1314
 
1315
    @method _unbindScrollResize
1316
    @protected
1317
    @since 3.5.0
1318
    **/
1319
    _unbindScrollResize: function () {
1320
        if (this._scrollResizeHandle) {
1321
            this._scrollResizeHandle.detach();
1322
            delete this._scrollResizeHandle;
1323
        }
1324
    }
1325
 
1326
    /**
1327
    Indicates horizontal table scrolling is enabled.
1328
 
1329
    @property _xScroll
1330
    @type {Boolean}
1331
    @default undefined (not initially set)
1332
    @private
1333
    @since 3.5.0
1334
    **/
1335
    //_xScroll: null,
1336
 
1337
    /**
1338
    Indicates vertical table scrolling is enabled.
1339
 
1340
    @property _yScroll
1341
    @type {Boolean}
1342
    @default undefined (not initially set)
1343
    @private
1344
    @since 3.5.0
1345
    **/
1346
    //_yScroll: null,
1347
 
1348
    /**
1349
    Fixed column header `<table>` Node for vertical scrolling tables.
1350
 
1351
    @property _yScrollHeader
1352
    @type {Node}
1353
    @default undefined (not initially set)
1354
    @protected
1355
    @since 3.5.0
1356
    **/
1357
    //_yScrollHeader: null,
1358
 
1359
    /**
1360
    Overflow Node used to contain the data rows in a vertically scrolling table.
1361
 
1362
    @property _yScrollNode
1363
    @type {Node}
1364
    @default undefined (not initially set)
1365
    @protected
1366
    @since 3.5.0
1367
    **/
1368
    //_yScrollNode: null,
1369
 
1370
    /**
1371
    Overflow Node used to contain the table headers and data in a horizontally
1372
    scrolling table.
1373
 
1374
    @property _xScrollNode
1375
    @type {Node}
1376
    @default undefined (not initially set)
1377
    @protected
1378
    @since 3.5.0
1379
    **/
1380
    //_xScrollNode: null
1381
}, true);
1382
 
1383
Y.Base.mix(Y.DataTable, [Scrollable]);
1384
 
1385
 
1386
}, '3.18.1', {"requires": ["datatable-base", "datatable-column-widths", "dom-screen"], "skinnable": true});