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