src/cm/media/js/lib/yui/yui3-3.15.0/build/datatable-scroll/datatable-scroll.js
changeset 602 e16a97fb364a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cm/media/js/lib/yui/yui3-3.15.0/build/datatable-scroll/datatable-scroll.js	Mon Mar 10 15:19:48 2014 +0100
@@ -0,0 +1,1386 @@
+YUI.add('datatable-scroll', function (Y, NAME) {
+
+/**
+Adds the ability to make the table rows scrollable while preserving the header
+placement.
+
+@module datatable-scroll
+@for DataTable
+@since 3.5.0
+**/
+var YLang = Y.Lang,
+    isString = YLang.isString,
+    isNumber = YLang.isNumber,
+    isArray  = YLang.isArray,
+
+    Scrollable;
+
+// Returns the numeric value portion of the computed style, defaulting to 0
+function styleDim(node, style) {
+    return parseInt(node.getComputedStyle(style), 10) || 0;
+}
+
+/**
+_API docs for this extension are included in the DataTable class._
+
+Adds the ability to make the table rows scrollable while preserving the header
+placement.
+
+There are two types of scrolling, horizontal (x) and vertical (y).  Horizontal
+scrolling is achieved by wrapping the entire table in a scrollable container.
+Vertical scrolling is achieved by splitting the table headers and data into two
+separate tables, the latter of which is wrapped in a vertically scrolling
+container.  In this case, column widths of header cells and data cells are kept
+in sync programmatically.
+
+Since the split table synchronization can be costly at runtime, the split is only
+done if the data in the table stretches beyond the configured `height` value.
+
+To activate or deactivate scrolling, set the `scrollable` attribute to one of
+the following values:
+
+ * `false` - (default) Scrolling is disabled.
+ * `true` or 'xy' - If `height` is set, vertical scrolling will be activated, if
+            `width` is set, horizontal scrolling will be activated.
+ * 'x' - Activate horizontal scrolling only. Requires the `width` attribute is
+         also set.
+ * 'y' - Activate vertical scrolling only. Requires the `height` attribute is
+         also set.
+
+@class DataTable.Scrollable
+@for DataTable
+@since 3.5.0
+**/
+Y.DataTable.Scrollable = Scrollable = function () {};
+
+Scrollable.ATTRS = {
+    /**
+    Activates or deactivates scrolling in the table.  Acceptable values are:
+
+     * `false` - (default) Scrolling is disabled.
+     * `true` or 'xy' - If `height` is set, vertical scrolling will be
+       activated, if `width` is set, horizontal scrolling will be activated.
+     * 'x' - Activate horizontal scrolling only. Requires the `width` attribute
+       is also set.
+     * 'y' - Activate vertical scrolling only. Requires the `height` attribute
+       is also set.
+
+    @attribute scrollable
+    @type {String|Boolean}
+    @value false
+    @since 3.5.0
+    **/
+    scrollable: {
+        value: false,
+        setter: '_setScrollable'
+    }
+};
+
+Y.mix(Scrollable.prototype, {
+
+    /**
+    Scrolls a given row or cell into view if the table is scrolling.  Pass the
+    `clientId` of a Model from the DataTable's `data` ModelList or its row
+    index to scroll to a row or a [row index, column index] array to scroll to
+    a cell.  Alternately, to scroll to any element contained within the table's
+    scrolling areas, pass its ID, or the Node itself (though you could just as
+    well call `node.scrollIntoView()` yourself, but hey, whatever).
+
+    @method scrollTo
+    @param {String|Number|Number[]|Node} id A row clientId, row index, cell
+            coordinate array, id string, or Node
+    @return {DataTable}
+    @chainable
+    @since 3.5.0
+    **/
+    scrollTo: function (id) {
+        var target;
+
+        if (id && this._tbodyNode && (this._yScrollNode || this._xScrollNode)) {
+            if (isArray(id)) {
+                target = this.getCell(id);
+            } else if (isNumber(id)) {
+                target = this.getRow(id);
+            } else if (isString(id)) {
+                target = this._tbodyNode.one('#' + id);
+            } else if (id instanceof Y.Node &&
+                    // TODO: ancestor(yScrollNode, xScrollNode)
+                    id.ancestor('.yui3-datatable') === this.get('boundingBox')) {
+                target = id;
+            }
+
+            if(target) {
+                target.scrollIntoView();
+            }
+        }
+
+        return this;
+    },
+
+    //--------------------------------------------------------------------------
+    // Protected properties and methods
+    //--------------------------------------------------------------------------
+
+    /**
+    Template for the `<table>` that is used to fix the caption in place when
+    the table is horizontally scrolling.
+
+    @property _CAPTION_TABLE_TEMPLATE
+    @type {String}
+    @value '<table class="{className}" role="presentation"></table>'
+    @protected
+    @since 3.5.0
+    **/
+    _CAPTION_TABLE_TEMPLATE: '<table class="{className}" role="presentation"></table>',
+
+    /**
+    Template used to create sizable element liners around header content to
+    synchronize fixed header column widths.
+
+    @property _SCROLL_LINER_TEMPLATE
+    @type {String}
+    @value '<div class="{className}"></div>'
+    @protected
+    @since 3.5.0
+    **/
+    _SCROLL_LINER_TEMPLATE: '<div class="{className}"></div>',
+
+    /**
+    Template for the virtual scrollbar needed in "y" and "xy" scrolling setups.
+
+    @property _SCROLLBAR_TEMPLATE
+    @type {String}
+    @value '<div class="{className}"><div></div></div>'
+    @protected
+    @since 3.5.0
+    **/
+    _SCROLLBAR_TEMPLATE: '<div class="{className}"><div></div></div>',
+
+    /**
+    Template for the `<div>` that is used to contain the table when the table is
+    horizontally scrolling.
+
+    @property _X_SCROLLER_TEMPLATE
+    @type {String}
+    @value '<div class="{className}"></div>'
+    @protected
+    @since 3.5.0
+    **/
+    _X_SCROLLER_TEMPLATE: '<div class="{className}"></div>',
+
+    /**
+    Template for the `<table>` used to contain the fixed column headers for
+    vertically scrolling tables.
+
+    @property _Y_SCROLL_HEADER_TEMPLATE
+    @type {String}
+    @value '<table cellspacing="0" role="presentation" aria-hidden="true" class="{className}"></table>'
+    @protected
+    @since 3.5.0
+    **/
+    _Y_SCROLL_HEADER_TEMPLATE: '<table cellspacing="0" aria-hidden="true" class="{className}"></table>',
+
+    /**
+    Template for the `<div>` that is used to contain the rows when the table is
+    vertically scrolling.
+
+    @property _Y_SCROLLER_TEMPLATE
+    @type {String}
+    @value '<div class="{className}"><div class="{scrollerClassName}"></div></div>'
+    @protected
+    @since 3.5.0
+    **/
+    _Y_SCROLLER_TEMPLATE: '<div class="{className}"><div class="{scrollerClassName}"></div></div>',
+
+    /**
+    Adds padding to the last cells in the fixed header for vertically scrolling
+    tables.  This padding is equal in width to the scrollbar, so can't be
+    relegated to a stylesheet.
+
+    @method _addScrollbarPadding
+    @protected
+    @since 3.5.0
+    **/
+    _addScrollbarPadding: function () {
+        var fixedHeader = this._yScrollHeader,
+            headerClass = '.' + this.getClassName('header'),
+            scrollbarWidth, rows, header, i, len;
+
+        if (fixedHeader) {
+            scrollbarWidth = Y.DOM.getScrollbarWidth() + 'px';
+            rows = fixedHeader.all('tr');
+
+            for (i = 0, len = rows.size(); i < len; i += +header.get('rowSpan')) {
+                header = rows.item(i).all(headerClass).pop();
+                header.setStyle('paddingRight', scrollbarWidth);
+            }
+        }
+    },
+
+    /**
+    Reacts to changes in the `scrollable` attribute by updating the `_xScroll`
+    and `_yScroll` properties and syncing the scrolling structure accordingly.
+
+    @method _afterScrollableChange
+    @param {EventFacade} e The relevant change event (ignored)
+    @protected
+    @since 3.5.0
+    **/
+    _afterScrollableChange: function () {
+        var scroller = this._xScrollNode;
+
+        if (this._xScroll && scroller) {
+            if (this._yScroll && !this._yScrollNode) {
+                scroller.setStyle('paddingRight',
+                    Y.DOM.getScrollbarWidth() + 'px');
+            } else if (!this._yScroll && this._yScrollNode) {
+                scroller.setStyle('paddingRight', '');
+            }
+        }
+
+        this._syncScrollUI();
+    },
+
+    /**
+    Reacts to changes in the `caption` attribute by adding, removing, or
+    syncing the caption table when the table is set to scroll.
+
+    @method _afterScrollCaptionChange
+    @param {EventFacade} e The relevant change event (ignored)
+    @protected
+    @since 3.5.0
+    **/
+    _afterScrollCaptionChange: function () {
+        if (this._xScroll || this._yScroll) {
+            this._syncScrollUI();
+        }
+    },
+
+    /**
+    Reacts to changes in the `columns` attribute of vertically scrolling tables
+    by refreshing the fixed headers, scroll container, and virtual scrollbar
+    position.
+
+    @method _afterScrollColumnsChange
+    @param {EventFacade} e The relevant change event (ignored)
+    @protected
+    @since 3.5.0
+    **/
+    _afterScrollColumnsChange: function () {
+        if (this._xScroll || this._yScroll) {
+            if (this._yScroll && this._yScrollHeader) {
+                this._syncScrollHeaders();
+            }
+
+            this._syncScrollUI();
+        }
+    },
+
+    /**
+    Reacts to changes in vertically scrolling table's `data` ModelList by
+    synchronizing the fixed column header widths and virtual scrollbar height.
+
+    @method _afterScrollDataChange
+    @param {EventFacade} e The relevant change event (ignored)
+    @protected
+    @since 3.5.0
+    **/
+    _afterScrollDataChange: function () {
+        if (this._xScroll || this._yScroll) {
+            this._syncScrollUI();
+        }
+    },
+
+    /**
+    Reacts to changes in the `height` attribute of vertically scrolling tables
+    by updating the height of the `<div>` wrapping the data table and the
+    virtual scrollbar.  If `scrollable` was set to "y" or "xy" but lacking a
+    declared `height` until the received change, `_syncScrollUI` is called to
+    create the fixed headers etc.
+
+    @method _afterScrollHeightChange
+    @param {EventFacade} e The relevant change event (ignored)
+    @protected
+    @since 3.5.0
+    **/
+    _afterScrollHeightChange: function () {
+        if (this._yScroll) {
+            this._syncScrollUI();
+        }
+    },
+
+    /* (not an API doc comment on purpose)
+    Reacts to the sort event (if the table is also sortable) by updating the
+    fixed header classes to match the data table's headers.
+
+    THIS IS A HACK that will be removed immediately after the 3.5.0 release.
+    If you're reading this and the current version is greater than 3.5.0, I
+    should be publicly scolded.
+    */
+    _afterScrollSort: function () {
+        var headers, headerClass;
+
+        if (this._yScroll && this._yScrollHeader) {
+            headerClass = '.' + this.getClassName('header');
+            headers = this._theadNode.all(headerClass);
+
+            this._yScrollHeader.all(headerClass).each(function (header, i) {
+                header.set('className', headers.item(i).get('className'));
+            });
+        }
+    },
+
+    /**
+    Reacts to changes in the width of scrolling tables by expanding the width of
+    the `<div>` wrapping the data table for horizontally scrolling tables or
+    upding the position of the virtual scrollbar for vertically scrolling
+    tables.
+
+    @method _afterScrollWidthChange
+    @param {EventFacade} e The relevant change event (ignored)
+    @protected
+    @since 3.5.0
+    **/
+    _afterScrollWidthChange: function () {
+        if (this._xScroll || this._yScroll) {
+            this._syncScrollUI();
+        }
+    },
+
+    /**
+    Binds virtual scrollbar interaction to the `_yScrollNode`'s `scrollTop` and
+    vice versa.
+
+    @method _bindScrollbar
+    @protected
+    @since 3.5.0
+    **/
+    _bindScrollbar: function () {
+        var scrollbar = this._scrollbarNode,
+            scroller  = this._yScrollNode;
+
+        if (scrollbar && scroller && !this._scrollbarEventHandle) {
+            this._scrollbarEventHandle = new Y.Event.Handle([
+                scrollbar.on('scroll', this._syncScrollPosition, this),
+                scroller.on('scroll', this._syncScrollPosition, this)
+            ]);
+        }
+    },
+
+    /**
+    Binds to the window resize event to update the vertical scrolling table
+    headers and wrapper `<div>` dimensions.
+
+    @method _bindScrollResize
+    @protected
+    @since 3.5.0
+    **/
+    _bindScrollResize: function () {
+        if (!this._scrollResizeHandle) {
+            // TODO: sync header widths and scrollbar position.  If the height
+            // of the headers has changed, update the scrollbar dims as well.
+            this._scrollResizeHandle = Y.on('resize',
+                this._syncScrollUI, null, this);
+        }
+    },
+
+    /**
+    Attaches internal subscriptions to keep the scrolling structure up to date
+    with changes in the table's `data`, `columns`, `caption`, or `height`.  The
+    `width` is taken care of already.
+
+    This executes after the table's native `bindUI` method.
+
+    @method _bindScrollUI
+    @protected
+    @since 3.5.0
+    **/
+    _bindScrollUI: function () {
+        this.after({
+            columnsChange: Y.bind('_afterScrollColumnsChange', this),
+            heightChange : Y.bind('_afterScrollHeightChange', this),
+            widthChange  : Y.bind('_afterScrollWidthChange', this),
+            captionChange: Y.bind('_afterScrollCaptionChange', this),
+            scrollableChange: Y.bind('_afterScrollableChange', this),
+            // FIXME: this is a last minute hack to work around the fact that
+            // DT doesn't use a tableView to render table content that can be
+            // replaced with a scrolling table view.  This must be removed asap!
+            sort         : Y.bind('_afterScrollSort', this)
+        });
+
+        this.after(['dataChange', '*:add', '*:remove', '*:reset', '*:change'],
+            Y.bind('_afterScrollDataChange', this));
+    },
+
+    /**
+    Clears the lock and timer used to manage synchronizing the scroll position
+    between the vertical scroll container and the virtual scrollbar.
+
+    @method _clearScrollLock
+    @protected
+    @since 3.5.0
+    **/
+    _clearScrollLock: function () {
+        if (this._scrollLock) {
+            this._scrollLock.cancel();
+            delete this._scrollLock;
+        }
+    },
+
+    /**
+    Creates a virtual scrollbar from the `_SCROLLBAR_TEMPLATE`, assigning it to
+    the `_scrollbarNode` property.
+
+    @method _createScrollbar
+    @return {Node} The created Node
+    @protected
+    @since 3.5.0
+    **/
+    _createScrollbar: function () {
+        var scrollbar = this._scrollbarNode;
+
+        if (!scrollbar) {
+            scrollbar = this._scrollbarNode = Y.Node.create(
+                Y.Lang.sub(this._SCROLLBAR_TEMPLATE, {
+                    className: this.getClassName('scrollbar')
+                }));
+
+            // IE 6-10 require the scrolled area to be visible (at least 1px)
+            // or they don't respond to clicking on the scrollbar rail or arrows
+            scrollbar.setStyle('width', (Y.DOM.getScrollbarWidth() + 1) + 'px');
+        }
+
+        return scrollbar;
+    },
+
+    /**
+    Creates a separate table to contain the caption when the table is
+    configured to scroll vertically or horizontally.
+
+    @method _createScrollCaptionTable
+    @return {Node} The created Node
+    @protected
+    @since 3.5.0
+    **/
+    _createScrollCaptionTable: function () {
+        if (!this._captionTable) {
+            this._captionTable = Y.Node.create(
+                Y.Lang.sub(this._CAPTION_TABLE_TEMPLATE, {
+                    className: this.getClassName('caption', 'table')
+                }));
+
+            this._captionTable.empty();
+        }
+
+        return this._captionTable;
+    },
+
+    /**
+    Populates the `_xScrollNode` property by creating the `<div>` Node described
+    by the `_X_SCROLLER_TEMPLATE`.
+
+    @method _createXScrollNode
+    @return {Node} The created Node
+    @protected
+    @since 3.5.0
+    **/
+    _createXScrollNode: function () {
+        if (!this._xScrollNode) {
+            this._xScrollNode = Y.Node.create(
+                Y.Lang.sub(this._X_SCROLLER_TEMPLATE, {
+                    className: this.getClassName('x','scroller')
+                }));
+        }
+
+        return this._xScrollNode;
+    },
+
+    /**
+    Populates the `_yScrollHeader` property by creating the `<table>` Node
+    described by the `_Y_SCROLL_HEADER_TEMPLATE`.
+
+    @method _createYScrollHeader
+    @return {Node} The created Node
+    @protected
+    @since 3.5.0
+    **/
+    _createYScrollHeader: function () {
+        var fixedHeader = this._yScrollHeader;
+
+        if (!fixedHeader) {
+            fixedHeader = this._yScrollHeader = Y.Node.create(
+                Y.Lang.sub(this._Y_SCROLL_HEADER_TEMPLATE, {
+                    className: this.getClassName('scroll','columns')
+                }));
+        }
+
+        return fixedHeader;
+    },
+
+    /**
+    Populates the `_yScrollNode` property by creating the `<div>` Node described
+    by the `_Y_SCROLLER_TEMPLATE`.
+
+    @method _createYScrollNode
+    @return {Node} The created Node
+    @protected
+    @since 3.5.0
+    **/
+    _createYScrollNode: function () {
+        var scrollerClass;
+
+        if (!this._yScrollNode) {
+            scrollerClass = this.getClassName('y', 'scroller');
+
+            this._yScrollContainer = Y.Node.create(
+                Y.Lang.sub(this._Y_SCROLLER_TEMPLATE, {
+                    className: this.getClassName('y','scroller','container'),
+                    scrollerClassName: scrollerClass
+                }));
+
+            this._yScrollNode = this._yScrollContainer
+                .one('.' + scrollerClass);
+        }
+
+        return this._yScrollContainer;
+    },
+
+    /**
+    Removes the nodes used to create horizontal and vertical scrolling and
+    rejoins the caption to the main table if needed.
+
+    @method _disableScrolling
+    @protected
+    @since 3.5.0
+    **/
+    _disableScrolling: function () {
+        this._removeScrollCaptionTable();
+        this._disableXScrolling();
+        this._disableYScrolling();
+        this._unbindScrollResize();
+
+        this._uiSetWidth(this.get('width'));
+    },
+
+    /**
+    Removes the nodes used to allow horizontal scrolling.
+
+    @method _disableXScrolling
+    @protected
+    @since 3.5.0
+    **/
+    _disableXScrolling: function () {
+        this._removeXScrollNode();
+    },
+
+    /**
+    Removes the nodes used to allow vertical scrolling.
+
+    @method _disableYScrolling
+    @protected
+    @since 3.5.0
+    **/
+    _disableYScrolling: function () {
+        this._removeYScrollHeader();
+        this._removeYScrollNode();
+        this._removeYScrollContainer();
+        this._removeScrollbar();
+    },
+
+    /**
+    Cleans up external event subscriptions.
+
+    @method destructor
+    @protected
+    @since 3.5.0
+    **/
+    destructor: function () {
+        this._unbindScrollbar();
+        this._unbindScrollResize();
+        this._clearScrollLock();
+    },
+
+    /**
+    Sets up event handlers and AOP advice methods to bind the DataTable's natural
+    behaviors with the scrolling APIs and state.
+
+    @method initializer
+    @param {Object} config The config object passed to the constructor (ignored)
+    @protected
+    @since 3.5.0
+    **/
+    initializer: function () {
+        this._setScrollProperties();
+
+        this.after(['scrollableChange', 'heightChange', 'widthChange'],
+            this._setScrollProperties);
+
+        this.after('renderView', Y.bind('_syncScrollUI', this));
+
+        Y.Do.after(this._bindScrollUI, this, 'bindUI');
+    },
+
+    /**
+    Removes the table used to house the caption when the table is scrolling.
+
+    @method _removeScrollCaptionTable
+    @protected
+    @since 3.5.0
+    **/
+    _removeScrollCaptionTable: function () {
+        if (this._captionTable) {
+            if (this._captionNode) {
+                this._tableNode.prepend(this._captionNode);
+            }
+
+            this._captionTable.remove().destroy(true);
+
+            delete this._captionTable;
+        }
+    },
+
+    /**
+    Removes the `<div>` wrapper used to contain the data table when the table
+    is horizontally scrolling.
+
+    @method _removeXScrollNode
+    @protected
+    @since 3.5.0
+    **/
+    _removeXScrollNode: function () {
+        var scroller = this._xScrollNode;
+
+        if (scroller) {
+            scroller.replace(scroller.get('childNodes').toFrag());
+            scroller.remove().destroy(true);
+
+            delete this._xScrollNode;
+        }
+    },
+
+    /**
+    Removes the `<div>` wrapper used to contain the data table and fixed header
+    when the table is vertically scrolling.
+
+    @method _removeYScrollContainer
+    @protected
+    @since 3.5.0
+    **/
+    _removeYScrollContainer: function () {
+        var scroller = this._yScrollContainer;
+
+        if (scroller) {
+            scroller.replace(scroller.get('childNodes').toFrag());
+            scroller.remove().destroy(true);
+
+            delete this._yScrollContainer;
+        }
+    },
+
+    /**
+    Removes the `<table>` used to contain the fixed column headers when the
+    table is vertically scrolling.
+
+    @method _removeYScrollHeader
+    @protected
+    @since 3.5.0
+    **/
+    _removeYScrollHeader: function () {
+        if (this._yScrollHeader) {
+            this._yScrollHeader.remove().destroy(true);
+
+            delete this._yScrollHeader;
+        }
+    },
+
+    /**
+    Removes the `<div>` wrapper used to contain the data table when the table
+    is vertically scrolling.
+
+    @method _removeYScrollNode
+    @protected
+    @since 3.5.0
+    **/
+    _removeYScrollNode: function () {
+        var scroller = this._yScrollNode;
+
+        if (scroller) {
+            scroller.replace(scroller.get('childNodes').toFrag());
+            scroller.remove().destroy(true);
+
+            delete this._yScrollNode;
+        }
+    },
+
+    /**
+    Removes the virtual scrollbar used by scrolling tables.
+
+    @method _removeScrollbar
+    @protected
+    @since 3.5.0
+    **/
+    _removeScrollbar: function () {
+        if (this._scrollbarNode) {
+            this._scrollbarNode.remove().destroy(true);
+
+            delete this._scrollbarNode;
+        }
+        if (this._scrollbarEventHandle) {
+            this._scrollbarEventHandle.detach();
+
+            delete this._scrollbarEventHandle;
+        }
+    },
+
+    /**
+    Accepts (case insensitive) values "x", "y", "xy", `true`, and `false`.
+    `true` is translated to "xy" and upper case values are converted to lower
+    case.  All other values are invalid.
+
+    @method _setScrollable
+    @param {String|Boolean} val Incoming value for the `scrollable` attribute
+    @return {String}
+    @protected
+    @since 3.5.0
+    **/
+    _setScrollable: function (val) {
+        if (val === true) {
+            val = 'xy';
+        }
+
+        if (isString(val)) {
+            val = val.toLowerCase();
+        }
+
+        return (val === false || val === 'y' || val === 'x' || val === 'xy') ?
+            val :
+            Y.Attribute.INVALID_VALUE;
+    },
+
+    /**
+    Assigns the `_xScroll` and `_yScroll` properties to true if an
+    appropriate value is set in the `scrollable` attribute and the `height`
+    and/or `width` is set.
+
+    @method _setScrollProperties
+    @protected
+    @since 3.5.0
+    **/
+    _setScrollProperties: function () {
+        var scrollable = this.get('scrollable') || '',
+            width      = this.get('width'),
+            height     = this.get('height');
+
+        this._xScroll = width  && scrollable.indexOf('x') > -1;
+        this._yScroll = height && scrollable.indexOf('y') > -1;
+    },
+
+    /**
+    Keeps the virtual scrollbar and the scrolling `<div>` wrapper around the
+    data table in vertically scrolling tables in sync.
+
+    @method _syncScrollPosition
+    @param {DOMEventFacade} e The scroll event
+    @protected
+    @since 3.5.0
+    **/
+    _syncScrollPosition: function (e) {
+        var scrollbar = this._scrollbarNode,
+            scroller  = this._yScrollNode,
+            source    = e.currentTarget,
+            other;
+
+        if (scrollbar && scroller) {
+            if (this._scrollLock && this._scrollLock.source !== source) {
+                return;
+            }
+
+            this._clearScrollLock();
+            this._scrollLock = Y.later(300, this, this._clearScrollLock);
+            this._scrollLock.source = source;
+
+            other = (source === scrollbar) ? scroller : scrollbar;
+            other.set('scrollTop', source.get('scrollTop'));
+        }
+    },
+
+    /**
+    Splits the caption from the data `<table>` if the table is configured to
+    scroll.  If not, rejoins the caption to the data `<table>` if it needs to
+    be.
+
+    @method _syncScrollCaptionUI
+    @protected
+    @since 3.5.0
+    **/
+    _syncScrollCaptionUI: function () {
+        var caption      = this._captionNode,
+            table        = this._tableNode,
+            captionTable = this._captionTable,
+            id;
+
+        if (caption) {
+            id = caption.getAttribute('id');
+
+            if (!captionTable) {
+                captionTable = this._createScrollCaptionTable();
+
+                this.get('contentBox').prepend(captionTable);
+            }
+
+            if (!caption.get('parentNode').compareTo(captionTable)) {
+                captionTable.empty().insert(caption);
+
+                if (!id) {
+                    id = Y.stamp(caption);
+                    caption.setAttribute('id', id);
+                }
+
+                table.setAttribute('aria-describedby', id);
+            }
+        } else if (captionTable) {
+            this._removeScrollCaptionTable();
+        }
+    },
+
+    /**
+    Assigns widths to the fixed header columns to match the columns in the data
+    table.
+
+    @method _syncScrollColumnWidths
+    @protected
+    @since 3.5.0
+    **/
+    _syncScrollColumnWidths: function () {
+        var widths = [];
+
+        if (this._theadNode && this._yScrollHeader) {
+            // Capture dims and assign widths in two passes to avoid reflows for
+            // each access of clientWidth/getComputedStyle
+            this._theadNode.all('.' + this.getClassName('header'))
+                .each(function (header) {
+                    widths.push(
+                        // FIXME: IE returns the col.style.width from
+                        // getComputedStyle even if the column has been
+                        // compressed below that width, so it must use
+                        // clientWidth. FF requires getComputedStyle because it
+                        // uses fractional widths that round up to an overall
+                        // cell/table width 1px greater than the data table's
+                        // cell/table width, resulting in misaligned columns or
+                        // fixed header bleed through. I can't think of a
+                        // *reasonable* way to capture the correct width without
+                        // a sniff.  Math.min(cW - p, getCS(w)) was imperfect
+                        // and punished all browsers, anyway.
+                        (Y.UA.ie && Y.UA.ie < 8) ?
+                            (header.get('clientWidth') -
+                             styleDim(header, 'paddingLeft') -
+                             styleDim(header, 'paddingRight')) + 'px' :
+                            header.getComputedStyle('width'));
+            });
+
+            this._yScrollHeader.all('.' + this.getClassName('scroll', 'liner'))
+                .each(function (liner, i) {
+                    liner.setStyle('width', widths[i]);
+                });
+        }
+    },
+
+    /**
+    Creates matching headers in the fixed header table for vertically scrolling
+    tables and synchronizes the column widths.
+
+    @method _syncScrollHeaders
+    @protected
+    @since 3.5.0
+    **/
+    _syncScrollHeaders: function () {
+        var fixedHeader   = this._yScrollHeader,
+            linerTemplate = this._SCROLL_LINER_TEMPLATE,
+            linerClass    = this.getClassName('scroll', 'liner'),
+            headerClass   = this.getClassName('header'),
+            headers       = this._theadNode.all('.' + headerClass);
+
+        if (this._theadNode && fixedHeader) {
+            fixedHeader.empty().appendChild(
+                this._theadNode.cloneNode(true));
+
+            // Prevent duplicate IDs and assign ARIA attributes to hide
+            // from screen readers
+            fixedHeader.all('[id]').removeAttribute('id');
+
+            fixedHeader.all('.' + headerClass).each(function (header, i) {
+                var liner = Y.Node.create(Y.Lang.sub(linerTemplate, {
+                            className: linerClass
+                        })),
+                    refHeader = headers.item(i);
+
+                // Can't assign via skin css because sort (and potentially
+                // others) might override the padding values.
+                liner.setStyle('padding',
+                    refHeader.getComputedStyle('paddingTop') + ' ' +
+                    refHeader.getComputedStyle('paddingRight') + ' ' +
+                    refHeader.getComputedStyle('paddingBottom') + ' ' +
+                    refHeader.getComputedStyle('paddingLeft'));
+
+                liner.appendChild(header.get('childNodes').toFrag());
+
+                header.appendChild(liner);
+            }, this);
+
+            this._syncScrollColumnWidths();
+
+            this._addScrollbarPadding();
+        }
+    },
+
+    /**
+    Wraps the table for X and Y scrolling, if necessary, if the `scrollable`
+    attribute is set.  Synchronizes dimensions and DOM placement of all
+    scrolling related nodes.
+
+    @method _syncScrollUI
+    @protected
+    @since 3.5.0
+    **/
+    _syncScrollUI: function () {
+        var x = this._xScroll,
+            y = this._yScroll,
+            xScroller  = this._xScrollNode,
+            yScroller  = this._yScrollNode,
+            scrollLeft = xScroller && xScroller.get('scrollLeft'),
+            scrollTop  = yScroller && yScroller.get('scrollTop');
+
+        this._uiSetScrollable();
+
+        // TODO: Probably should split this up into syncX, syncY, and syncXY
+        if (x || y) {
+            if ((this.get('width') || '').slice(-1) === '%') {
+                this._bindScrollResize();
+            } else {
+                this._unbindScrollResize();
+            }
+
+            this._syncScrollCaptionUI();
+        } else {
+            this._disableScrolling();
+        }
+
+        if (this._yScrollHeader) {
+            this._yScrollHeader.setStyle('display', 'none');
+        }
+
+        if (x) {
+            if (!y) {
+                this._disableYScrolling();
+            }
+
+            this._syncXScrollUI(y);
+        }
+
+        if (y) {
+            if (!x) {
+                this._disableXScrolling();
+            }
+
+            this._syncYScrollUI(x);
+        }
+
+        // Restore scroll position
+        if (scrollLeft && this._xScrollNode) {
+            this._xScrollNode.set('scrollLeft', scrollLeft);
+        }
+        if (scrollTop && this._yScrollNode) {
+            this._yScrollNode.set('scrollTop', scrollTop);
+        }
+    },
+
+    /**
+    Wraps the table in a scrolling `<div>` of the configured width for "x"
+    scrolling.
+
+    @method _syncXScrollUI
+    @param {Boolean} xy True if the table is configured with scrollable ="xy"
+    @protected
+    @since 3.5.0
+    **/
+    _syncXScrollUI: function (xy) {
+        var scroller     = this._xScrollNode,
+            yScroller    = this._yScrollContainer,
+            table        = this._tableNode,
+            width        = this.get('width'),
+            bbWidth      = this.get('boundingBox').get('offsetWidth'),
+            scrollbarWidth = Y.DOM.getScrollbarWidth(),
+            borderWidth, tableWidth;
+
+        if (!scroller) {
+            scroller = this._createXScrollNode();
+
+            // Not using table.wrap() because IE went all crazy, wrapping the
+            // table in the last td in the table itself.
+            (yScroller || table).replace(scroller).appendTo(scroller);
+        }
+
+        // Can't use offsetHeight - clientHeight because IE6 returns
+        // clientHeight of 0 intially.
+        borderWidth = styleDim(scroller, 'borderLeftWidth') +
+                      styleDim(scroller, 'borderRightWidth');
+
+        scroller.setStyle('width', '');
+        this._uiSetDim('width', '');
+        if (xy && this._yScrollContainer) {
+            this._yScrollContainer.setStyle('width', '');
+        }
+
+        // Lock the table's unconstrained width to avoid configured column
+        // widths being ignored
+        if (Y.UA.ie && Y.UA.ie < 8) {
+            // Have to assign a style and trigger a reflow to allow the
+            // subsequent clearing of width + reflow to expand the table to
+            // natural width in IE 6
+            table.setStyle('width', width);
+            table.get('offsetWidth');
+        }
+        table.setStyle('width', '');
+        tableWidth = table.get('offsetWidth');
+        table.setStyle('width', tableWidth + 'px');
+
+        this._uiSetDim('width', width);
+
+        // Can't use 100% width because the borders add additional width
+        // TODO: Cache the border widths, though it won't prevent a reflow
+        scroller.setStyle('width', (bbWidth - borderWidth) + 'px');
+
+        // expand the table to fill the assigned width if it doesn't
+        // already overflow the configured width
+        if ((scroller.get('offsetWidth') - borderWidth) > tableWidth) {
+            // Assumes the wrapped table doesn't have borders
+            if (xy) {
+                table.setStyle('width', (scroller.get('offsetWidth') -
+                     borderWidth - scrollbarWidth) + 'px');
+            } else {
+                table.setStyle('width', '100%');
+            }
+        }
+    },
+
+    /**
+    Wraps the table in a scrolling `<div>` of the configured height (accounting
+    for the caption if there is one) if "y" scrolling is enabled.  Otherwise,
+    unwraps the table if necessary.
+
+    @method _syncYScrollUI
+    @param {Boolean} xy True if the table is configured with scrollable = "xy"
+    @protected
+    @since 3.5.0
+    **/
+    _syncYScrollUI: function (xy) {
+        var yScroller    = this._yScrollContainer,
+            yScrollNode  = this._yScrollNode,
+            xScroller    = this._xScrollNode,
+            fixedHeader  = this._yScrollHeader,
+            scrollbar    = this._scrollbarNode,
+            table        = this._tableNode,
+            thead        = this._theadNode,
+            captionTable = this._captionTable,
+            boundingBox  = this.get('boundingBox'),
+            contentBox   = this.get('contentBox'),
+            width        = this.get('width'),
+            height       = boundingBox.get('offsetHeight'),
+            scrollbarWidth = Y.DOM.getScrollbarWidth(),
+            outerScroller;
+
+        if (captionTable && !xy) {
+            captionTable.setStyle('width', width || '100%');
+        }
+
+        if (!yScroller) {
+            yScroller = this._createYScrollNode();
+
+            yScrollNode = this._yScrollNode;
+
+            table.replace(yScroller).appendTo(yScrollNode);
+        }
+
+        outerScroller = xy ? xScroller : yScroller;
+
+        if (!xy) {
+            table.setStyle('width', '');
+        }
+
+        // Set the scroller height
+        if (xy) {
+            // Account for the horizontal scrollbar in the overall height
+            height -= scrollbarWidth;
+        }
+
+        yScrollNode.setStyle('height',
+            (height - outerScroller.get('offsetTop') -
+            // because IE6 is returning clientHeight 0 initially
+            styleDim(outerScroller, 'borderTopWidth') -
+            styleDim(outerScroller, 'borderBottomWidth')) + 'px');
+
+        // Set the scroller width
+        if (xy) {
+            // For xy scrolling tables, the table should expand freely within
+            // the x scroller
+            yScroller.setStyle('width',
+                (table.get('offsetWidth') + scrollbarWidth) + 'px');
+        } else {
+            this._uiSetYScrollWidth(width);
+        }
+
+        if (captionTable && !xy) {
+            captionTable.setStyle('width', yScroller.get('offsetWidth') + 'px');
+        }
+
+        // Allow headerless scrolling
+        if (thead && !fixedHeader) {
+            fixedHeader = this._createYScrollHeader();
+
+            yScroller.prepend(fixedHeader);
+
+            this._syncScrollHeaders();
+        }
+
+        if (fixedHeader) {
+            this._syncScrollColumnWidths();
+
+            fixedHeader.setStyle('display', '');
+            // This might need to come back if FF has issues
+            //fixedHeader.setStyle('width', '100%');
+                //(yScroller.get('clientWidth') + scrollbarWidth) + 'px');
+
+            if (!scrollbar) {
+                scrollbar = this._createScrollbar();
+
+                this._bindScrollbar();
+
+                contentBox.prepend(scrollbar);
+            }
+
+            this._uiSetScrollbarHeight();
+            this._uiSetScrollbarPosition(outerScroller);
+        }
+    },
+
+    /**
+    Assigns the appropriate class to the `boundingBox` to identify the DataTable
+    as horizontally scrolling, vertically scrolling, or both (adds both classes).
+
+    Classes added are "yui3-datatable-scrollable-x" or "...-y"
+
+    @method _uiSetScrollable
+    @protected
+    @since 3.5.0
+    **/
+    _uiSetScrollable: function () {
+        this.get('boundingBox')
+            .toggleClass(this.getClassName('scrollable','x'), this._xScroll)
+            .toggleClass(this.getClassName('scrollable','y'), this._yScroll);
+    },
+
+    /**
+    Updates the virtual scrollbar's height to avoid overlapping with the fixed
+    headers.
+
+    @method _uiSetScrollbarHeight
+    @protected
+    @since 3.5.0
+    **/
+    _uiSetScrollbarHeight: function () {
+        var scrollbar   = this._scrollbarNode,
+            scroller    = this._yScrollNode,
+            fixedHeader = this._yScrollHeader;
+
+        if (scrollbar && scroller && fixedHeader) {
+            scrollbar.get('firstChild').setStyle('height',
+                this._tbodyNode.get('scrollHeight') + 'px');
+
+            scrollbar.setStyle('height',
+                (parseFloat(scroller.getComputedStyle('height')) -
+                 parseFloat(fixedHeader.getComputedStyle('height'))) + 'px');
+        }
+    },
+
+    /**
+    Updates the virtual scrollbar's placement to avoid overlapping the fixed
+    headers or the data table.
+
+    @method _uiSetScrollbarPosition
+    @param {Node} scroller Reference node to position the scrollbar over
+    @protected
+    @since 3.5.0
+    **/
+    _uiSetScrollbarPosition: function (scroller) {
+        var scrollbar     = this._scrollbarNode,
+            fixedHeader   = this._yScrollHeader;
+
+        if (scrollbar && scroller && fixedHeader) {
+            scrollbar.setStyles({
+                // Using getCS instead of offsetHeight because FF uses
+                // fractional values, but reports ints to offsetHeight, so
+                // offsetHeight is unreliable.  It is probably fine to use
+                // offsetHeight in this case but this was left in place after
+                // fixing an off-by-1px issue in FF 10- by fixing the caption
+                // font style so FF picked it up.
+                top: (parseFloat(fixedHeader.getComputedStyle('height')) +
+                      styleDim(scroller, 'borderTopWidth') +
+                      scroller.get('offsetTop')) + 'px',
+
+                // Minus 1 because IE 6-10 require the scrolled area to be
+                // visible by at least 1px or it won't respond to clicks on the
+                // scrollbar rail or endcap arrows.
+                left: (scroller.get('offsetWidth') -
+                       Y.DOM.getScrollbarWidth() - 1 -
+                       styleDim(scroller, 'borderRightWidth')) + 'px'
+            });
+        }
+    },
+
+    /**
+    Assigns the width of the `<div>` wrapping the data table in vertically
+    scrolling tables.
+
+    If the table can't compress to the specified width, the container is
+    expanded accordingly.
+
+    @method _uiSetYScrollWidth
+    @param {String} width The CSS width to attempt to set
+    @protected
+    @since 3.5.0
+    **/
+    _uiSetYScrollWidth: function (width) {
+        var scroller = this._yScrollContainer,
+            table    = this._tableNode,
+            tableWidth, borderWidth, scrollerWidth, scrollbarWidth;
+
+        if (scroller && table) {
+            scrollbarWidth = Y.DOM.getScrollbarWidth();
+
+            if (width) {
+                // Assumes no table border
+                borderWidth = scroller.get('offsetWidth') -
+                              scroller.get('clientWidth') +
+                              scrollbarWidth; // added back at the end
+
+                // The table's rendered width might be greater than the
+                // configured width
+                scroller.setStyle('width', width);
+
+                // Have to subtract the border width from the configured width
+                // because the scroller's width will need to be reduced by the
+                // border width as well during the width reassignment below.
+                scrollerWidth = scroller.get('clientWidth') - borderWidth;
+
+                // Assumes no table borders
+                table.setStyle('width', scrollerWidth + 'px');
+
+                tableWidth = table.get('offsetWidth');
+
+                // Expand the scroll node width if the table can't fit.
+                // Otherwise, reassign the scroller a pixel width that
+                // accounts for the borders.
+                scroller.setStyle('width',
+                    (tableWidth + scrollbarWidth) + 'px');
+            } else {
+                // Allow the table to expand naturally
+                table.setStyle('width', '');
+                scroller.setStyle('width', '');
+
+                scroller.setStyle('width',
+                    (table.get('offsetWidth') + scrollbarWidth) + 'px');
+            }
+        }
+    },
+
+    /**
+    Detaches the scroll event subscriptions used to maintain scroll position
+    parity between the scrollable `<div>` wrapper around the data table and the
+    virtual scrollbar for vertically scrolling tables.
+
+    @method _unbindScrollbar
+    @protected
+    @since 3.5.0
+    **/
+    _unbindScrollbar: function () {
+        if (this._scrollbarEventHandle) {
+            this._scrollbarEventHandle.detach();
+        }
+    },
+
+    /**
+    Detaches the resize event subscription used to maintain column parity for
+    vertically scrolling tables with percentage widths.
+
+    @method _unbindScrollResize
+    @protected
+    @since 3.5.0
+    **/
+    _unbindScrollResize: function () {
+        if (this._scrollResizeHandle) {
+            this._scrollResizeHandle.detach();
+            delete this._scrollResizeHandle;
+        }
+    }
+
+    /**
+    Indicates horizontal table scrolling is enabled.
+
+    @property _xScroll
+    @type {Boolean}
+    @default undefined (not initially set)
+    @private
+    @since 3.5.0
+    **/
+    //_xScroll: null,
+
+    /**
+    Indicates vertical table scrolling is enabled.
+
+    @property _yScroll
+    @type {Boolean}
+    @default undefined (not initially set)
+    @private
+    @since 3.5.0
+    **/
+    //_yScroll: null,
+
+    /**
+    Fixed column header `<table>` Node for vertical scrolling tables.
+
+    @property _yScrollHeader
+    @type {Node}
+    @default undefined (not initially set)
+    @protected
+    @since 3.5.0
+    **/
+    //_yScrollHeader: null,
+
+    /**
+    Overflow Node used to contain the data rows in a vertically scrolling table.
+
+    @property _yScrollNode
+    @type {Node}
+    @default undefined (not initially set)
+    @protected
+    @since 3.5.0
+    **/
+    //_yScrollNode: null,
+
+    /**
+    Overflow Node used to contain the table headers and data in a horizontally
+    scrolling table.
+
+    @property _xScrollNode
+    @type {Node}
+    @default undefined (not initially set)
+    @protected
+    @since 3.5.0
+    **/
+    //_xScrollNode: null
+}, true);
+
+Y.Base.mix(Y.DataTable, [Scrollable]);
+
+
+}, '@VERSION@', {"requires": ["datatable-base", "datatable-column-widths", "dom-screen"], "skinnable": true});