'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2016, 2019 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['../../../lib/@waca/core-client/js/core-client/ui/core/View', '../../../lib/@waca/core-client/js/core-client/utils/UniqueId', '../../../lib/@waca/core-client/js/core-client/utils/BrowserUtils', 'underscore', 'jquery', './GridDataProvider', '../../../util/KeyCodes'], function (BaseView, UniqueId, BrowserUtils, _, $, DataProvider, KeyCodes) { 'use strict'; var cbGetScrolledFastPinnedCornerCellIdx = function cbGetScrolledFastPinnedCornerCellIdx(tdIndex, fixedColumns, fixedRows) { //Corner cells positioning in rows are static (never change in tables). When fast horizontal scrolling had //taken place and when there is need to resize or update after data change, do not move the corner cells to a another location //within a row return fixedColumns && fixedRows && tdIndex < fixedColumns ? tdIndex : null; }; var ATTRIBUTES = { COLUMN_INDEX: 'col', ROW_INDEX: 'row' }; var View = null; /** * Class representing a grid. * @extends {lib/@waca/core-client/js/core-client/ui/core/View} */ View = BaseView.extend({ events: { 'keydown': 'onKeyDown' }, /** * @constructs * Initialize the class. Takes in an options object that gives some configuration details to the grid class. The options object's * parameters are listed below: * * @param {object} options: An options object * @param {array} dataProvider: the data provider object * @param {boolean} isGroupEnabled: * @param {number} minCellWidth: minimum width of each grid cell when empty text in pixels * @param {number} minCellHeight: minimum height of each grid cell when empty text in pixels * @param {number} height: 100, * @param {function} formatCell: an optional function that takes 4 parameters and will be called per grid cell/DOM node to format that cell. * Function declaration should be something like: function formatCell(td, value, rowIndex, colIndex) where * @param [object] td: cell node * @param [string] value: the value of the cell * @param [number] rowIndex: number representing the row index. 0-indexed * @param [ number] colIndex: number representing the column index. 0-indexed * @param {function} getMoreData: optional function that will be called if we scroll towards the bottom of the grid and there are less than or * equal to 5 rows left to render. Useful if there is additional data to be fetched from the server that should be rendered. * @param {function} onStartScroll: * @param {function} onFinishScroll: * @param {object} el: * @param {number} fixedRows: indicates how many rows starting from row 0 to pin to the grid (eg. 1 to pin the headers row) * @param {number} fixedColumns: indicates how many columns starting from col 0 to pin to the grid (eg. 1 to pin the headers column) */ init: function init() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; View.inherited('init', this, arguments); this.dataProvider = options.dataProvider || new DataProvider(options.data); this.offsetCount = 1; //Right now, only autoResizeHorizontal is used - but want to keep the //autoResizeVertical logic in case it is used in the future this.autoResizeHorizontal = options.autoResizeHorizontal || options.autoResize; this.autoResizeVertical = options.autoResizeVertical || options.autoResize; this.isRendered = false; // Minimum width and height allowed for the cell // when we have an empty text. Large text will push // the size of the cell. this.minCellWidth = options.minCellWidth || 150; this.minCellHeight = options.minCellHeight || 30; // Used to enable grouping of cells. this.isGroupEnabled = options.isGroupEnabled || false; this.options = options; this.translateX = 0; this.translateY = 0; this.verticalScrollConfig = { isVerticalScrolling: true, minSizeProp: 'min-height', defaultSize: function () { return this.minCellHeight; }.bind(this), sizeFunction: function sizeFunction($node) { return $node.outerHeight(); }, paddingProp: 'padding-top', translate: function (value) { this.translateX = value; this.$virtualTable.css('padding-top', value + 'px'); this.$rowHeadersTable.css('margin-top', value - this.$scrollableArea.scrollTop() + 'px'); }.bind(this), getTranslateValue: function () { return this.translateX; }.bind(this), scrolledRowIdx: 0, //The row (TR Row) that was scrolled to, renderedCount: 0, scrollFunction: 'scrollTop', isOutOfView: function (offset) { if (offset > 0) { return this.isOutOfViewFromTop(this.$firstRow()); } else { return this.isOutOfViewFromBottom(this.$lastRow()); } }.bind(this) }; if (BrowserUtils.isIE11()) { //Work around a bug where the jQuery size functions can return incorrect values for these nodes in IE11 using jQuery 3.3.1. this.verticalScrollConfig.sizeFunction = function ($node) { return $node[0].offsetHeight; }; } this.verticalScrollConfig.getScrolledIdx = function () { return this.scrolledRowIdx; }.bind(this.verticalScrollConfig); this.verticalScrollConfig.setScrolledIdx = function (value) { this.scrolledRowIdx = value; }.bind(this.verticalScrollConfig); this.verticalScrollConfig.recalculateRenderingIndexes = function () { if (this.count - this.scrolledRowIdx < this.renderedCount) { this.scrolledRowIdx = Math.max(0, this.count - this.renderedCount); } }.bind(this.verticalScrollConfig); this.createCellStat(this.verticalScrollConfig); this.horizontalScrollConfig = { isVerticalScrolling: false, minSizeProp: 'min-width', sizeFunction: function sizeFunction($node) { return $node.outerWidth(); }, defaultSize: function () { return this.minCellWidth; }.bind(this), translate: function (value) { this.translateY = value; this.$virtualTable.css('padding-left', value + 'px'); this.$columnHeadersTable.css('margin-left', value - this.$scrollableArea.scrollLeft() + 'px'); }.bind(this), getTranslateValue: function () { return this.translateY; }.bind(this), scrolledColumnIdx: 0, //The column (TD Cell) that was scrolled to, renderedCount: 0, scrollFunction: 'scrollLeft', isOutOfView: function (offset) { if (offset > 0) { return this.isOutOfViewFromLeft(this.$firstRowFirstCell()); } else { return this.isOutOfViewFromRight(this.$firstRowLastCell()); } }.bind(this) }; if (BrowserUtils.isIE11()) { //Work around a bug where the jQuery size functions can return incorrect values for these nodes in IE11 using jQuery 3.3.1. this.horizontalScrollConfig.sizeFunction = function ($node) { return $node[0].offsetWidth; }; } this.horizontalScrollConfig.getScrolledIdx = function () { return this.scrolledColumnIdx; }.bind(this.horizontalScrollConfig); this.horizontalScrollConfig.setScrolledIdx = function (value) { this.scrolledColumnIdx = value; }.bind(this.horizontalScrollConfig); this.horizontalScrollConfig.recalculateRenderingIndexes = function () { if (this.count - this.scrolledColumnIdx < this.renderedCount) { this.scrolledColumnIdx = Math.max(0, this.count - this.renderedCount); } }.bind(this.horizontalScrollConfig); this.createCellStat(this.horizontalScrollConfig); this.dataProvider = options.dataProvider || new DataProvider(options.data); this._setProvideDataCount(); this._setFixedRowsAndColumns({ fixedRows: options.fixedRows, fixedColumns: options.fixedColumns }); }, /** * Dump the grid data and size. * Used to generate testcase for the unit test * * @returns */ _getGridState: function _getGridState() { var state = {}; state.height = this.$scrollableArea.height(); state.width = this.$scrollableArea.width(); state.minCellWidth = this.minCellWidth; state.minCellHeight = this.minCellHeight; state.rows = this.dataProvider.getNumRows(); state.columns = this.dataProvider.getNumColumns(); state.renderedRows = this.verticalScrollConfig.renderedCount; state.renderedColumns = this.horizontalScrollConfig.renderedCount; state.scrollTop = this.$scrollableArea.scrollTop(); state.scrollLeft = this.$scrollableArea.scrollLeft(); state.data = []; var i, j, row; for (i = 0; i < this.dataProvider.getNumRows(); i++) { row = []; state.data.push(row); for (j = 0; j < this.dataProvider.getNumColumns(); j++) { var value = this.dataProvider.getValue(i, j); var cellValue; if (this.options.setValue) { var $cell = $('
'); this.options.setValue($cell, value, i, j); cellValue = $cell.text(); } else if (_.isObject(value)) { cellValue = value.value; } else { cellValue = value || ''; } row.push(cellValue); } } return JSON.stringify(state); }, /** * Update the dataprovider with a new one, and call onDataChange for a rerender * @param {object} dataProvider - the new dataProvider used by the grid * @param {object} options - an options object which contains fixedRows and fixedColumns * @public */ updateDataProvider: function updateDataProvider(dataProvider, options) { this.dataProvider = dataProvider; this.onDataChange({ fixedRows: options.fixedRows, fixedColumns: options.fixedColumns }); }, /** * Notify the grid whenever the data provider is modified. This will update the number of rows/columns, * number of fixed rows/columns. Takes an options object: * isNewDataAvailable flag (boolean) used to keep track whether or not we're * appending data to the dataprovider or completely changing the data. If we're merely appending, then we should keep existing cell * stats and handle the grid scrolling to keep the right positioning for the grid. Else we can create new cell stats. * @param {object} options * @public */ onDataChange: function onDataChange() { var _this = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var isForceRedraw = this._setProvideDataCount(); // Reapply the pinned rows/columns this._setFixedRowsAndColumns(options); // If this parameter is passed in as true, then we assumed that we were appending data to the dataprovider. // In this case we'd like to keep our position in the grid so scrolling vertically will appear seamless. For now // we have to do this.scrollTopOffset - 1 pixel because if you scroll down fast enough, scrolling will hang at the bottom // of the grid when we fetch additional data (async operation) since handleOffset() will set this.scrollTopOffset to be equal to // the top offset of the scrollable area. By subtracting a single pixel, this is inconspicuous to the eye and essentially forces // a recalculation of the scrolling. if (options.isNewDataAvailable) { this.scrollTopOffset -= 1; this.handleOffset(); return Promise.resolve(this.isRendered); } else { //The data has changed. Force a rerender return this.resolveDelayedPromises(options).then(function () { _this.reRenderAfterDataChange(isForceRedraw, options); return _this.isRendered; }); } }, reRenderAfterDataChange: function reRenderAfterDataChange(isForceRedraw, options) { var _this2 = this; //silent scroll is used to handle the case where a filter was being set //and we didnt want it to trigger a scroll event as this hid the the flyout //for the filter this.silentScroll = true; if (isForceRedraw) { this.createCellStat(this.verticalScrollConfig); this.createCellStat(this.horizontalScrollConfig); } this._recalculateRenderingIndexes(); if (isForceRedraw || !this.isRendered) { // redraw the table cells this.prepareTableForRendering(); } options.forceScrollUpdate = true; this.updateTables(options); setTimeout(function () { _this2.silentScroll = false; }, 500); }, updateTables: function updateTables() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; this.updateTableValues(); //Recalculate stat size statistics after all values have been updated in the UI this._reCalcTableStats(); //Now resize the tables with new stats so all tables line up this.updateTableSize(); this.updateScroll(options.forceScrollUpdate); this.autoFitContentIfNeeded(); }, resolveDelayedPromises: function resolveDelayedPromises() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; return options.onDelayedUpdateReady ? options.onDelayedUpdateReady() : Promise.resolve(); }, /** * A cell width/height can change after all values are rendered in the grid. When this happens it can cause the tables not to line up. * This function fixes this by resetting the column header cells to initial minimum size, recalculate all table stats then resize the table headers so that * they line up with the virtual table cells */ _reCalcTableStats: function _reCalcTableStats() { this._resetColumnHeaderTableMinCellWidth(); this._createCellStats(); this.recordTableStats(this.$table); this.recordTableStats(this.$columnHeadersTable); this.recordTableStats(this.$rowHeadersTable); this.recordTableStats(this.$cornerTable); this.resizeTableIfNeeded(this.$table); this.resizeTableIfNeeded(this.$columnHeadersTable); this.resizeTableIfNeeded(this.$rowHeadersTable); this.resizeTableIfNeeded(this.$cornerTable); }, /** * Reset column header cells to initial minimum sizes * */ _resetColumnHeaderTableMinCellWidth: function _resetColumnHeaderTableMinCellWidth() { var $columnHeaderRows = this.$columnHeadersTable.find('tr'); var $tr = void 0, $columns = void 0, $td = void 0; for (var index = 0; index < this.fixedRows; ++index) { $tr = $columnHeaderRows.eq(index); $columns = $tr.find('td'); for (var i = 0; i < $columns.length; i++) { $td = $columns.eq(i); this.setCellMinWidth($td); } } }, /** * Make sure that the current padding is not too big. * * If we are re-rendering after an initial render and we have scrolled and then the grid changes size, * then we need to update the padding in case it is now too big after resizing the table * @private */ _recalculateTablePositionForInitialRendering: function _recalculateTablePositionForInitialRendering() { var padding = this.$scrollableArea[this.verticalScrollConfig.scrollFunction](); var scrollInfo = this.getScrollIncrement(padding, this.verticalScrollConfig); this.verticalScrollConfig.setScrolledIdx(scrollInfo.steps); this.verticalScrollConfig.translate(scrollInfo.padding); padding = this.$scrollableArea[this.horizontalScrollConfig.scrollFunction](); scrollInfo = this.getScrollIncrement(padding, this.horizontalScrollConfig); this.horizontalScrollConfig.setScrolledIdx(scrollInfo.steps); this.horizontalScrollConfig.translate(scrollInfo.padding); }, /** * Recalculate the rendering indexes * If the rendering index will only render less than what we are currently rendering, * then we need to asjust the index to render more * */ _recalculateRenderingIndexes: function _recalculateRenderingIndexes() { this.verticalScrollConfig.recalculateRenderingIndexes(); this.horizontalScrollConfig.recalculateRenderingIndexes(); }, _setProvideDataCount: function _setProvideDataCount() { var isCountChanged = false; // Get the new data count if (this.verticalScrollConfig.count !== this.dataProvider.getNumRows()) { this.verticalScrollConfig.count = this.dataProvider.getNumRows(); isCountChanged = true; } if (this.horizontalScrollConfig.count !== this.dataProvider.getNumColumns()) { this.horizontalScrollConfig.count = this.dataProvider.getNumColumns(); isCountChanged = true; } return isCountChanged; }, _setFixedRowsAndColumns: function _setFixedRowsAndColumns(options) { this.verticalScrollConfig.count = this.dataProvider.getNumRows(); this.horizontalScrollConfig.count = this.dataProvider.getNumColumns(); this.fixedRows = !_.isUndefined(options.fixedRows) ? options.fixedRows : this.fixedRows; this.fixedColumns = !_.isUndefined(options.fixedColumns) ? options.fixedColumns : this.fixedColumns; }, /** * Recalculate positioning for the grid table and then rerender * @public */ onResize: function onResize() { if (this.$scrollableArea.scrollTop() === 0 && this.$scrollableArea.scrollLeft() === 0 && (this.scrollTopOffset !== 0 || this.scrollLeftOffset !== 0)) { // We lost the scrolling state. We need to restore it. This might happen if the node was removed from the dom and restored this.resetScroll(); } //reset the cell stats if we have maximized the content so that they don't affect the new rendering // We will only clear the affected orientation if (this.autoResizeVerticalEnabled) { this.createCellStat(this.verticalScrollConfig); } if (this.autoResizeHorizontalEnabled) { this.createCellStat(this.horizontalScrollConfig); } // Adjust the padding in case we are at the end of the scrolling and we are showing less than what we can after the resize. this._recalculateTablePositionForInitialRendering(); // re-render return this.render(); }, /** * Returns an object detailing some information about the cells * @private */ createCellStat: function createCellStat(config) { config.stats = { map: {}, totalRecordedSize: 0, count: 0, minSize: config.defaultSize(), maxSize: config.defaultSize(), averageSize: config.defaultSize(), totalSize: config.count * config.defaultSize() }; }, _createCellStats: function _createCellStats() { this.createCellStat(this.verticalScrollConfig); this.createCellStat(this.horizontalScrollConfig); }, cacheMostUsedValues: function cacheMostUsedValues() { // Cache the most common used elements so that we // can easily have access to them this.$cells = this.$table.find('td'); this.$rows = this.$table.find('tr'); this.$firstRow = function () { return this.$table.find('tr:first'); }.bind(this); this.$lastRow = function () { return this.$table.find('tr:last'); }.bind(this); this.$firstRowFirstCell = function () { return this.$firstRow().find('td:first'); }.bind(this); this.$firstRowLastCell = function () { return this.$firstRow().find('td:last'); }.bind(this); this.horizontalScrollConfig.$firstCell = function () { return this.$firstRow().find('td:first'); }.bind(this); //this.$firstRowFirstCell; this.horizontalScrollConfig.$lastCell = function () { return this.$firstRow().find('td:last'); }.bind(this); //this.$firstRowLastCell; this.verticalScrollConfig.$firstCell = function () { return this.$table.find('tr:first'); }.bind(this); //this.$firstRow; this.verticalScrollConfig.$lastCell = function () { return this.$table.find('tr:last'); }.bind(this); // this.$lastRow; }, /** * Render the grid table * @public */ render: function render(options) { var _this3 = this; if (!this.$table) { this.$table = $('
'); if (this.options.label) { this.$table.attr('aria-label', this.options.label); } this.$columnHeadersTable = $('
'); this.$rowHeadersTable = $('
'); this.$cornerTable = $('
'); this.$scrollableArea = $('
'); this.$scrollableArea.attr('id', UniqueId.get('scrollableArea_')); this.$virtualTable = $('
'); this.$virtualTable.attr('id', UniqueId.get('virtualTable_')); this.$virtualTable.append(this.$table); this.$scrollableArea.append(this.$virtualTable); this.$headerContainer = $('
'); this.$headerContainer.attr('id', UniqueId.get('headerContainer_')); this.$headerContainer.append(this.$columnHeadersTable); if (this.fixedColumns && this.fixedColumns > 0) { this.bHasCornerTable = true; this.$headerContainer.append(this.$rowHeadersTable); this.$headerContainer.append(this.$cornerTable); } this.$contentWrapper = $('
'); this.$contentWrapper.attr('id', UniqueId.get('contentWrapper_')); this.$contentWrapper.append(this.$scrollableArea); this.$contentWrapper.append(this.$headerContainer); this.$el.append(this.$contentWrapper); this.scrollTopOffset = 0; this.scrollLeftOffset = 0; this.registerScrollEvents(); } return this.resolveDelayedPromises(options).then(function () { _this3.prepareTableForRendering(); if (_this3.isRendered) { _this3.updateTables(options); } return _this3.isRendered; }); }, prepareTableForRendering: function prepareTableForRendering() { this.$scrollableArea.css('overflow-x', 'auto'); this.$scrollableArea.css('overflow-y', 'auto'); this.$headerContainer.height(this.$scrollableArea[0].clientHeight); this.$headerContainer.width(this.$scrollableArea[0].clientWidth); this._clearTables(); if (!this.dataProvider.getNumRows()) { this.isRendered = false; return; } this.isRendered = this.renderTable(); if (this.isRendered) { this.renderPinnedColumnHeaders(); if (this.fixedColumns && this.fixedColumns > 0) { this.renderPinnedRowHeaders(); this.renderPinnedCorner(); } } this.cacheMostUsedValues(); }, _clearTables: function _clearTables() { this.$table.empty(); this.$columnHeadersTable.empty(); this.$rowHeadersTable.empty(); this.$cornerTable.empty(); }, autoFitContentIfNeeded: function autoFitContentIfNeeded() { if (this.autoResizeHorizontal || this.autoResizeVertical) { this.autoResizeHorizontalEnabled = false; this.autoResizeVerticalEnabled = false; var extra, nodes, index, i; if (this.autoResizeHorizontal && this.$virtualTable.outerWidth() <= this.$el.innerWidth()) { var width = this.$scrollableArea[0].clientWidth / this.horizontalScrollConfig.count - 2; extra = 0; nodes = []; for (index in this.horizontalScrollConfig.stats.map) { if (this.horizontalScrollConfig.stats.map.hasOwnProperty(index)) { if (this.horizontalScrollConfig.stats.map[index] > width) { extra += this.horizontalScrollConfig.stats.map[index] - width; } else { nodes.push(this.$el.find('td[col="' + index + '"]')); } } } width -= extra / nodes.length; for (i = 0; i < nodes.length; i++) { nodes[i].css('min-width', width + 'px'); } this.$virtualTable.css(this.horizontalScrollConfig.minSizeProp, '0px'); this.$scrollableArea.css('overflow-x', 'visible'); this.autoResizeHorizontalEnabled = true; } if (this.autoResizeVertical && this.$virtualTable.outerHeight() <= this.$el.innerHeight()) { var height = this.$scrollableArea[0].clientHeight / this.verticalScrollConfig.count - 2; extra = 0; nodes = []; for (index in this.verticalScrollConfig.stats.map) { if (this.verticalScrollConfig.stats.map[index] > height) { extra += this.verticalScrollConfig.stats.map[index] - height; } else { nodes.push(this.$el.find('td[row="' + index + '"]')); } } height -= extra / nodes.length; for (i = 0; i < nodes.length; i++) { nodes[i].css('height', height + 'px'); } this.$virtualTable.css(this.verticalScrollConfig.minSizeProp, '0px'); this.$scrollableArea.css('overflow-y', 'visible'); this.autoResizeVerticalEnabled = true; } if (this.autoResizeVerticalEnabled || this.autoResizeHorizontalEnabled) { this.recordTableStats(this.$table); this.resizeTableIfNeeded(this.$columnHeadersTable); this.resizeTableIfNeeded(this.$rowHeadersTable); } } }, registerScrollEvents: function registerScrollEvents() { this.$scrollableArea.on('scroll.virtualScroll', this.onScroll.bind(this)); this.$headerContainer.on('wheel', this.onWheel.bind(this)); }, onWheel: function onWheel(event) { var scrollTop = this.$scrollableArea.scrollTop(); if (event.originalEvent.deltaY < 0) { this.$scrollableArea.scrollTop(this.$scrollableArea.scrollTop() - 90); } else if (event.originalEvent.deltaY > 0) { this.$scrollableArea.scrollTop(this.$scrollableArea.scrollTop() + 90); } if (event.originalEvent.deltaX < 0) { this.$scrollableArea.scrollLeft(this.$scrollableArea.scrollLeft() - 90); } else if (event.originalEvent.deltaX > 0) { this.$scrollableArea.scrollLeft(this.$scrollableArea.scrollLeft() + 90); } if (scrollTop !== this.$scrollableArea.scrollTop()) { event.preventDefault(); } }, remove: function remove() { if (this.scrollTimer) { clearTimeout(this.scrollTimer); } this.$scrollableArea && this.$scrollableArea.remove(); this.$headerContainer && this.$headerContainer.remove(); if (this.$contentWrapper) { this.$contentWrapper.remove(); this.$contentWrapper = null; } this.dataProvider = null; View.inherited('remove', this, arguments); }, /** * scroll handler. Called when the user scrolls the * virtual table */ onScroll: function onScroll() { var isScrollStarted = false; if (this.scrollTimer) { clearTimeout(this.scrollTimer); isScrollStarted = true; } // Notify the start scroll handler if (!isScrollStarted && this.options.onStartScroll && !this.silentScroll) { this.options.onStartScroll(); } this.scrollTimer = setTimeout(function () { this.scrollTimer = null; if (this.options.onFinishScroll && !this.silentScroll) { this.options.onFinishScroll(); } this._setFocusAfterScroll(); }.bind(this), 800); this.updateScroll(); }, /** * Update the positions of the pinned headers. Also, if there are 5 or fewer rowers left to render, determine whether or not we * need to fetch more data from our server by checking if we passed in a callback function inside our options object in init(). If * so, make the call to fetch more data, wait for the response, and then update position and offsets. * @private */ updateScroll: function updateScroll(isForceUpdate) { var scrollTopOffset = this.$scrollableArea.scrollTop(); var scrollLeftOffset = this.$scrollableArea.scrollLeft(); this.updatePinnedHeaderPosition(scrollTopOffset, scrollLeftOffset); var scrolledIdx = this.verticalScrollConfig.getScrolledIdx(); var numberOfDataRowsInGrid = this.options.getMoreData && this.verticalScrollConfig.count - (scrolledIdx + this.verticalScrollConfig.renderedCount); if (numberOfDataRowsInGrid !== false && numberOfDataRowsInGrid >= 0 && numberOfDataRowsInGrid < 5 && this.scrollTopOffset <= scrollTopOffset) { this.options.getMoreData(); } this.handleOffset(scrollTopOffset, scrollLeftOffset, isForceUpdate); }, resetScroll: function resetScroll() { this.$scrollableArea.scrollTop(this.scrollTopOffset); this.$scrollableArea.scrollLeft(this.scrollLeftOffset); }, /** * Handles the scrolling if we have scrolled horizontally or vertically by changing the vertical/horizontal offsets. * @private */ handleOffset: function handleOffset(scrollTop, scrollLeft, isForceUpdate) { var scrollTopOffset = scrollTop || this.$scrollableArea.scrollTop(); var scrollLeftOffset = scrollLeft || this.$scrollableArea.scrollLeft(); if (this.scrollTopOffset !== scrollTopOffset || isForceUpdate) { this.handleScroll(scrollTopOffset - this.scrollTopOffset, this.verticalScrollConfig, isForceUpdate); } if (this.scrollLeftOffset !== scrollLeftOffset || isForceUpdate) { this.handleScroll(scrollLeftOffset - this.scrollLeftOffset, this.horizontalScrollConfig, isForceUpdate); } // update the current scrolltop and scrollLeft // values. // Don't use the variables above // (scrollTopOffset/scrollLeftOffset), the scroll // values might have changed while processing the // scroll event this.scrollTopOffset = this.$scrollableArea.scrollTop(); this.scrollLeftOffset = this.$scrollableArea.scrollLeft(); }, /** * Remove any colspan/rowspan that was set on the cell * when the grouping is enabled This method will save * the current state of the grouping so it can be * restored using the restoreGrouping method */ removeGrouping: function removeGrouping() { if (!this.isGroupEnabled) { return; } this.$cells.each(function () { // we use font-size set to 0px to hide the text to avoid the height/width impact on the cell var $td = $(this); var display = $td.css('display'); var fontSize = $td.css('font-size'); if (display === 'none') { $td.css({ 'display': '', 'visibility': 'hidden', 'font-size': '0px' }); } if (fontSize) { $td.attr({ '_fontSize': fontSize }); } var colspan = $td.attr('colspan'); $td.attr({ '_colspan': null }); if (colspan) { $td.attr({ 'colspan': null, '_colspan': colspan }); $td.css({ 'font-size': '0px' }); } var rowspan = $td.attr('rowspan'); $td.attr({ '_rowspan': null }); if (rowspan) { $td.attr({ 'rowspan': null, '_rowspan': rowspan }); $td.css({ 'font-size': '0px' }); } }); }, /** * Restore any grouping by adding colspan/rowspan to the * cells. This method is supposed to restore the * grouping that was removed using the removeGrouping * method * @private */ restoreGrouping: function restoreGrouping() { if (!this.isGroupEnabled) { return; } this.$cells.each(function () { var $td = $(this); var display = $td.css('visibility'); var fontSize = $td.attr('_fontSize'); //when restore happens, we used the saved state of the fontSize to set the font-size value if (display === 'hidden') { $td.css({ 'display': 'none', 'visibility': '', 'font-size': fontSize ? fontSize : '' }); $td.attr({ '_fontSize': null }); } var colspan = $td.attr('_colspan'); if (colspan) { $td.attr({ '_colspan': null, 'colspan': colspan, '_fontSize': null }); $td.css({ 'font-size': fontSize ? fontSize : '' }); } var rowspan = $td.attr('_rowspan'); if (rowspan) { $td.attr({ '_rowspan': null, 'rowspan': rowspan, '_fontSize': null }); $td.css({ 'font-size': fontSize ? fontSize : '' }); } }); }, /** * Offset is positive when moving right and negative * when moving left. * * @param offset */ handleScroll: function handleScroll(offset, config, isForceUpdate) { if (!this.dataProvider.getNumRows()) { return; } // We need to temporarily remove the group to // determine the cell width so that we can properly // do the proper horizontal scrolling this.removeGrouping(); var shifted = false; // Calculate how many columns we are shifting based on the current scroll position // if the increment steps is different than the current index we are rendering, then we need to redraw var increment = this.getScrollIncrement(this.$scrollableArea[config.scrollFunction](), config); if (increment.steps !== config.getScrolledIdx() || isForceUpdate) { shifted = this.redrawTable(increment.steps, increment.padding, config); } if (!shifted) { // If nothing has changed, we restore the // grouping. Otherwise the grouping would be // properly rendered when we shift the values this.restoreGrouping(); } else if (this.options.onAfterGridUpdate) { // Cells have been updated as a result of a scroll. Notify the callback if it exists this.options.onAfterGridUpdate(); } }, redrawTable: function redrawTable(newScrolledToRowOrColumnIndex, paddingOffset, config) { this.updateTablePadding(paddingOffset, config); var redrawn = this.updateIndexAndValues(newScrolledToRowOrColumnIndex, config); this.validateTablePadding(paddingOffset, config); return redrawn; }, updateIndexAndValues: function updateIndexAndValues(newScrolledToRowOrColumnIndex, config) { if (newScrolledToRowOrColumnIndex < 0 || newScrolledToRowOrColumnIndex + config.renderedCount > config.count) { return false; } var previousScrolledIdx = config.getScrolledIdx(); config.setScrolledIdx(newScrolledToRowOrColumnIndex); this.updateTableValues({ previousScrolledIdx: previousScrolledIdx, isVerticalScrolling: config.isVerticalScrolling }); return true; }, resizeTableIfNeeded: function resizeTableIfNeeded($table) { //update the size of the columns in the header here if they differ from stats var $rows = $table.find('tr'); for (var i = 0; i < $rows.length; i++) { var $tr = $rows.eq(i); var $columns = $tr.find('td'); for (var j = 0; j < $columns.length; j++) { var $td = $columns.eq(j); if ($td.hasClass('corner')) { if ($td.is(':visible')) { this.resizeCorner($td); } } else { this.updateCellSize($td); } } } this.resizeCornerTable(); }, /** in the column header table, must size the corner width independently when multiple row nesting is involved since it arbitrarily chooses * the 2nd cell in the first array to be the visible cell and sizes it according to that cell stat when it should * in fact be sized based on the width of the 1st and 2nd(0th and 1st) cell width stat. * @param {object} td - the cell * @private */ resizeCorner: function resizeCorner($td) { var colspan = parseInt($td.attr('colspan'), 10); var rowspan = parseInt($td.attr('rowspan'), 10); var i; var width = 0; var height = 0; for (i = 0; i < this.fixedColumns; i++) { width += this.horizontalScrollConfig.stats.map[i]; } for (i = 0; i < this.fixedRows; i++) { height += this.verticalScrollConfig.stats.map[i]; } //prevents the corner from becoming the size of more cells than are visible after scrolling to the right if (colspan !== this.fixedColumns) { for (i = 0; i < this.fixedColumns - colspan; i++) { width -= this.horizontalScrollConfig.stats.map[i]; } } if (rowspan !== this.fixedRows) { for (i = 0; i < this.fixedRows - rowspan; i++) { height -= this.verticalScrollConfig.stats.map[i]; } } $td.css('min-width', width + 'px'); $td.css('height', height + 'px'); }, /** * Resize the corner table to match the stats set by _resizeCorner * @private */ resizeCornerTable: function resizeCornerTable() { // we only care about the corner table if we have a pinned header if (this.fixedColumns > 0) { var width = 0; var height = 0; for (var i = 0; i < this.fixedColumns; i++) { width += this.horizontalScrollConfig.stats.map[i]; } for (var k = 0; k < this.fixedRows; k++) { height += this.verticalScrollConfig.stats.map[k]; } this.$corner.css({ 'min-width': width }); this.$corner.css({ 'height': height }); } }, /** * Update the positions of the pinned column/row headers tables * @param {number} scrollTopOffset - the offset we should pad to the top * @param {number} scrollLeftOffset - the offset we should pad to te left * @private */ updatePinnedHeaderPosition: function updatePinnedHeaderPosition() /*scrollTopOffset, scrollLeftOffset*/{ this.$columnHeadersTable.css('margin-left', this.horizontalScrollConfig.getTranslateValue() - this.$scrollableArea.scrollLeft() + 'px'); this.$rowHeadersTable.css('margin-top', this.verticalScrollConfig.getTranslateValue() - this.$scrollableArea.scrollTop() + 'px'); }, /** * based on the cells that are currently visible, update the minimum widths and heights for the columns and rows, respectively. * @param {object} table - the table * @private */ recordTableStats: function recordTableStats($table) { var $rows = $table.find('tr'); for (var index = $rows.length - 1; index >= 0; index--) { var $tr = $rows.eq(index); var $columns = $tr.find('td:visible'); var $td; // set the values and the formats for (var i = 0; i < $columns.length; i++) { $td = $columns.eq(i); var colspan = parseInt($td.attr('colspan'), 10) || 1; var rowspan = parseInt($td.attr('rowspan'), 10) || 1; //only record the width of cells that have col span 1 not including corners that span multiple columns if (colspan === 1 && (!$td.hasClass('corner') || this.fixedColumns === 1)) { var col = $td.attr(ATTRIBUTES.COLUMN_INDEX); this.recordCellStats($td, this.horizontalScrollConfig, col); } if (rowspan === 1 && (!$td.hasClass('corner') || this.fixedRows === 1)) { var row = $td.attr(ATTRIBUTES.ROW_INDEX); this.recordCellStats($td, this.verticalScrollConfig, row); } } } }, /** * adjust the size of the virtual table * @private */ updateTableSize: function updateTableSize() { this.$virtualTable.css(this.horizontalScrollConfig.minSizeProp, this.horizontalScrollConfig.stats.totalSize + 'px'); this.$virtualTable.css(this.verticalScrollConfig.minSizeProp, this.verticalScrollConfig.stats.totalSize + 'px'); }, /** * Updates the table padding in the vertical or horizontal directionshi * @param {number} newPadding - the new padding for the table * @param {config} config - the vertical/horizontal config * @private */ updateTablePadding: function updateTablePadding(newPadding, config) { // adjust the padding to simulate the scroll var padding = newPadding; config.translate(padding); }, validateTablePadding: function validateTablePadding(newPadding, config) { this.updateTableSize(); var previousScrolledIdx = void 0; if (newPadding <= 0) { newPadding = 0; if (config.getScrolledIdx() !== 0) { previousScrolledIdx = config.getScrolledIdx(); config.setScrolledIdx(0); this.updateTableValues({ previousScrolledIdx: config.getScrolledIdx(), isVerticalScrolling: config.isVerticalScrolling }); } config.translate(newPadding); } else if (newPadding > this.$scrollableArea[config.scrollFunction]()) { newPadding = this.$scrollableArea[config.scrollFunction](); var scrollInfo = this.getScrollIncrement(newPadding, config); if (config.getScrolledIdx() !== scrollInfo.steps) { previousScrolledIdx = config.getScrolledIdx(); config.setScrolledIdx(scrollInfo.steps); this.updateTableValues({ previousScrolledIdx: previousScrolledIdx, isVerticalScrolling: config.isVerticalScrolling }); } config.translate(newPadding); } // The virtual table is too big or too small(some // cell sizes have changed).. adjust it. var expectedTableSize = config.sizeFunction(this.$table) + newPadding; var currentTableSize = config.sizeFunction(this.$virtualTable); if (config.getScrolledIdx() + config.renderedCount === config.count && expectedTableSize !== currentTableSize) { config.stats.totalSize -= currentTableSize - expectedTableSize; this.updateTableSize(); } }, /** * @private */ updateTableValues: function updateTableValues() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var previousScrolledIdx = options.previousScrolledIdx; var isVerticalScrolling = options.isVerticalScrolling; if (this.dataProvider.getNumRows() && !this.partialValuesUpdate(previousScrolledIdx, isVerticalScrolling)) { this.allValuesUpdate(); } }, /** * @private */ updateDataVertically: function updateDataVertically(previousScrolledIdx, delta) { this.resetTableGroups(this.$table); this._updateTableScrolledVertically({ $table: this.$table, previousScrolledIdx: previousScrolledIdx, delta: delta, handleGrouping: true }); this.updateTableGroups(this.$table); //Remove a row and placing it at top or end of the table at the current scroll position. // Also, updating the table groups might change the current scroll position // We need to restore the scroll position var scrollTop = this.$scrollableArea.scrollTop(); this.$scrollableArea.scrollTop(scrollTop); this.recordTableStats(this.$table); this.resizeTableIfNeeded(this.$columnHeadersTable); this.resizeTableIfNeeded(this.$rowHeadersTable); }, /** * @private */ updateRowHeaderVertically: function updateRowHeaderVertically(previousScrolledIdx, delta) { this.resetTableGroups(this.$rowHeadersTable); this._updateTableScrolledVertically({ $table: this.$rowHeadersTable, previousScrolledIdx: previousScrolledIdx, delta: delta, handleGrouping: true, isRowHeader: true }); this.updateTableGroups(this.$rowHeadersTable); this.recordTableStats(this.$rowHeadersTable); }, /** * * Do a partial update of the values in the given $rows array. * If we are not changing all values, we simply move the rows/columns and keep the unchanged cells. This is done for better performance. * * @param previousScrolledIdx - old scrolled row or columnd index location before the updated * @param isVerticalScrolling - true if scrolling vertically else false * @param $rows * @returns {Boolean} */ partialValuesUpdate: function partialValuesUpdate(previousScrolledIdx, isVerticalScrolling) { var _this4 = this; var scrolledVerticalIdx = this.verticalScrollConfig.getScrolledIdx(); var index = isVerticalScrolling ? scrolledVerticalIdx : this.horizontalScrollConfig.getScrolledIdx(); var diff = Math.abs(previousScrolledIdx - index); var $columnHeaderRows = this.$columnHeadersTable.find('tr'); var $tableRows = this.$table.find('tr'); var scrollVertical = isVerticalScrolling && diff < this.verticalScrollConfig.renderedCount; var scrollHorizontal = !isVerticalScrolling && diff < this.horizontalScrollConfig.renderedCount; if (scrollVertical) { var delta = this.fixedRows && this.fixedRows > 0 ? this.fixedRows : 0; //updateRowHeaders this.updateRowHeaderVertically(previousScrolledIdx, delta); //update data this.updateDataVertically(previousScrolledIdx, delta); this.resizeTableIfNeeded(this.$columnHeadersTable); this.resizeTableIfNeeded(this.$rowHeadersTable); } else if (scrollHorizontal) { this.resetTableGroups(this.$columnHeadersTable); var _delta = this.fixedColumns && this.fixedColumns > 0 ? this.fixedColumns : 0; var getTableRowIndex = function getTableRowIndex(rowIndex) { return scrolledVerticalIdx + rowIndex; }; var options = { previousScrolledIdx: previousScrolledIdx, delta: _delta, cbUpdateCell: function cbUpdateCell($td, isRowHeaders, rowIndex, tdIndex) { rowIndex = isRowHeaders ? rowIndex : getTableRowIndex(rowIndex); if (previousScrolledIdx < _this4.horizontalScrollConfig.getScrolledIdx()) { tdIndex = _this4.horizontalScrollConfig.renderedCount - tdIndex - 1; } else { tdIndex = tdIndex + _delta; } _this4.updateCell($td, rowIndex, tdIndex); }, isRowHeaders: true }; //update column headers this._updateColumnHeadersWhenScrolleFastdHorizontally($columnHeaderRows, options); this.updateTableGroups(this.$columnHeadersTable); this.recordTableStats(this.$columnHeadersTable); //update data this.resetTableGroups(this.$table); options.isRowHeaders = false; this._updateColumnHeadersWhenScrolleFastdHorizontally($tableRows, options); this.updateTableGroups(this.$table); this.recordTableStats(this.$table); this.resizeTableIfNeeded(this.$columnHeadersTable); } if (scrollVertical || scrollHorizontal) { this.cacheMostUsedValues(); } return scrollVertical || scrollHorizontal; }, /** * Updates the table cell column headers after scrolled fast horizontally * * @param {object} $rows - the jQuery rows object * @param {object} options - options to update the table cells position and their contents within an table row */ _updateColumnHeadersWhenScrolleFastdHorizontally: function _updateColumnHeadersWhenScrolleFastdHorizontally($rows) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if ($rows instanceof $) { var $td = void 0; var i = void 0; var j = void 0; var isRowHeaders = options.isRowHeaders; var delta = options.delta; var previousScrolledIdx = options.previousScrolledIdx; for (i = 0; i < $rows.length; i++) { var $row = $rows.eq(i); var $tds = $row.children(); if (previousScrolledIdx < this.horizontalScrollConfig.getScrolledIdx()) { for (j = this.horizontalScrollConfig.getScrolledIdx() - previousScrolledIdx - 1; j >= 0; j--) { $td = $tds.eq(j + delta); $td.appendTo($row); options.cbUpdateCell($td, isRowHeaders, i, j); } } else { var $tdAfter = this.fixedColumns && this.fixedColumns > 0 ? $tds.eq(this.fixedColumns - 1) : null; for (j = previousScrolledIdx - this.horizontalScrollConfig.getScrolledIdx() - 1; j >= 0; j--) { $td = $tds.eq($tds.length - j - 1); if ($tdAfter) { $td.insertAfter($tdAfter); } else { $td.prependTo($row); } options.cbUpdateCell($td, isRowHeaders, i, j); } } } } }, /** * Update all the values in the given $rows array. * * @param $rows */ allValuesUpdate: function allValuesUpdate() { var _this5 = this; var _cbGetScrolledPinnedRowHeaders = function _cbGetScrolledPinnedRowHeaders(rowIndex) { //row headers are pinned, should not be moved when scrolled vertically return _this5.fixedRows && rowIndex < _this5.fixedRows ? rowIndex : rowIndex + _this5.verticalScrollConfig.getScrolledIdx(); }; //update column headers var $columnHeaderRows = this.$columnHeadersTable.find('tr'); var $tr; for (var index = this.fixedRows - 1; index >= 0; index--) { $tr = $columnHeaderRows.eq(index); this.updateRow({ $tr: $tr, rowIndex: index, handleGrouping: true, isRowHeader: false, cbGetScrolledFastPinnedCornerCellIdx: cbGetScrolledFastPinnedCornerCellIdx }); } this.recordTableStats(this.$columnHeadersTable); //update row headers var $rowHeaderRows = this.$rowHeadersTable.find('tr'); for (index = $rowHeaderRows.length - 1; index >= 0; index--) { $tr = $rowHeaderRows.eq(index); this.updateRow({ $tr: $tr, rowIndex: _cbGetScrolledPinnedRowHeaders(index), handleGrouping: true, isRowHeader: true }); } this.recordTableStats(this.$rowHeadersTable); //update corner table var $cornerTableRows = this.$cornerTable.find('tr'); for (index = this.fixedRows - 1; index >= 0; index--) { $tr = $cornerTableRows.eq(index); this.updateRow({ $tr: $tr, rowIndex: index, handleGrouping: true, isRowHeader: false, cbGetScrolledFastPinnedCornerCellIdx: cbGetScrolledFastPinnedCornerCellIdx }); } this.recordTableStats(this.$cornerTable); //update data var $tableRows = this.$table.find('tr'); for (index = $tableRows.length - 1; index >= 0; index--) { $tr = $tableRows.eq(index); this.updateRow({ $tr: $tr, rowIndex: _cbGetScrolledPinnedRowHeaders(index), handleGrouping: true, isRowHeader: false, cbGetScrolledFastPinnedCornerCellIdx: cbGetScrolledFastPinnedCornerCellIdx }); } this.recordTableStats(this.$table); this.resizeTableIfNeeded(this.$columnHeadersTable); this.resizeTableIfNeeded(this.$rowHeadersTable); }, /** * Updates the passed in row from the table * @param {object} tr - the row * @param {number} rowIndex - the rowIndex of the cell * @param {boolean} handleGrouping * @param {boolean} isRowHeader * @param {function} cbGetScrolledFastPinnedCornerCellIdx - optional callback to get the correct corner cell colunm index within a row * @private */ updateRow: function updateRow() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var $tr = options.$tr; var rowIndex = options.rowIndex; var handleGrouping = options.handleGrouping; var isRowHeader = options.isRowHeader; var cbGetScrolledFastPinnedCornerCellIdx = options.cbGetScrolledFastPinnedCornerCellIdx; var $columns = $tr.find('td'); var $td; $tr.attr(ATTRIBUTES.ROW_INDEX, rowIndex); if (this.options.formatRow) { this.options.formatRow($tr, rowIndex); } // set the values and the formats for (var i = 0; i < $columns.length; i++) { $td = $columns.eq(i); this.updateCell($td, rowIndex, i, handleGrouping, isRowHeader, cbGetScrolledFastPinnedCornerCellIdx); } }, /** * Updates and, if necessary, formats the passed in cell with new styles and text. Called whenever we need to render the cell, * eg. when we scroll/resize, etc * @param {object} td - a jQuery object representing the cell * @param {number} rowIndex - the rowIndex of the cell * @param {number} tdIndex - the column index of the cell. If the grid has been scrolled horizontally, we must manually calculate this value * @param {boolean} handleGrouping * @param {boolean} isRowHeader * @param {function} cbGetScrolledFastPinnedCornerCellIdx - optional callback to get the correct corner cell colunm index within a row * @private */ updateCell: function updateCell($td, rowIndex, tdIndex, handleGrouping, isRowHeader, cbGetScrolledFastPinnedCornerCellIdx) { var colIndex = cbGetScrolledFastPinnedCornerCellIdx ? cbGetScrolledFastPinnedCornerCellIdx(tdIndex, this.fixedColumns, this.fixedRows) : null; if (!_.isNumber(colIndex)) { colIndex = isRowHeader ? tdIndex : this.horizontalScrollConfig.getScrolledIdx() + tdIndex; } var value = this.dataProvider.getValue(rowIndex, colIndex); $td.attr(ATTRIBUTES.COLUMN_INDEX, colIndex); $td.attr('row', rowIndex); this.setCellMinWidth($td); $td.attr('data-group', null); if (handleGrouping) { this.resetGroupSettings($td); } if (this.options.setValue) { this.options.setValue($td, value, rowIndex, colIndex); } else { var cellValue; if (_.isObject(value)) { cellValue = value.value; } else { cellValue = value || ''; } $td.attr('aria-label', cellValue); $td.text(cellValue); } if (this.options.formatCell) { this.options.formatCell($td, value, rowIndex, colIndex); } this.updateCellSize($td); if (handleGrouping) { this.renderGroup($td, tdIndex); } }, /** * Updates the cell value with value when GridDataProvider cell value changes * @param {number} rowIndex - the rowIndex of the cell * @param {number} colIndex - the column index of the cell. * @param {number} value - the new value to replace the old one * @private */ onCellDataChange: function onCellDataChange(rowIndex, colIndex, value) { var $td = this.$table.find('td[' + ATTRIBUTES.ROW_INDEX + '="' + rowIndex + '"][' + ATTRIBUTES.COLUMN_INDEX + '="' + colIndex + '"]'); if ($td && $td.length && $td.is(':visible') && this.options.setValue) { this.options.setValue($td, value, rowIndex, colIndex); } }, /** * Updates the cell size based on the largest width/height of a cell in the same row/column * @param {object} td - a jQuery object represeting the table cell * @private */ updateCellSize: function updateCellSize($td) { var colIndex = $td.attr(ATTRIBUTES.COLUMN_INDEX); var rowIndex = $td.attr(ATTRIBUTES.ROW_INDEX); var width = this.horizontalScrollConfig.stats.map[colIndex]; if (width) { //var borderleftSize = parseInt($td.css('border-left-width'), 10); $td.css('min-width', width + 'px'); } var height = this.verticalScrollConfig.stats.map[rowIndex]; if (height) { // var borderTopSize = parseInt($td).css('border-top-width'), 10); $td.css('height', height + 'px'); } }, /** * @private */ resetTableGroups: function resetTableGroups($table) { var $rows = $table.find('tr'); for (var index = $rows.length - 1; index >= 0; index--) { var $tr = $rows.eq(index); var $columns = $tr.find('td'); var $td; // set the values and the formats for (var i = 0; i < $columns.length; i++) { $td = $columns.eq(i); this.resetGroupSettings($td); } } }, /** * @private */ updateTableGroups: function updateTableGroups($table) { var $rows = $table.find('tr'); for (var index = $rows.length - 1; index >= 0; index--) { var $tr = $rows.eq(index); var $columns = $tr.find('td'); var $td; // set the values and the formats for (var i = 0; i < $columns.length; i++) { $td = $columns.eq(i); this.renderGroup($td, i); } } }, /** * @private */ resetGroupSettings: function resetGroupSettings($td) { if (this.isGroupEnabled) { var fontSize = $td.attr('_fontSize') ? $td.attr('_fontSize') : $td.css('font-size'); $td.attr({ 'colspan': null, 'rowspan': null, '_colspan': null, '_rowspan': null, '_fontSize': null }); $td.css({ 'display': '', 'visibility': '', 'font-size': fontSize }); } }, /** * @private */ renderGroup: function renderGroup($td, i) { if (this.isGroupEnabled) { var group = $td.attr('data-group'); if (group) { var $prev = $td.prev(); var prevGroup = $prev.attr('data-group'); if (prevGroup === group) { var colspan = $prev.attr('colspan'); $td.attr('colspan', 1 + parseInt(colspan, 10)); $td.attr('rowspan', $prev.attr('rowspan')); $prev.css('display', 'none'); } else { $td.attr('colspan', '1'); } var $next = $td.parent().next().children().eq(i); prevGroup = $next.attr('data-group'); if (prevGroup === group) { var rowspan = $next.attr('rowspan'); $td.attr('rowspan', 1 + parseInt(rowspan, 10)); $td.attr('colspan', $next.attr('colspan')); $next.css('display', 'none'); } else { $td.attr('rowspan', '1'); } } } }, /** * @private */ recordCellStats: function recordCellStats($td, config, index) { var width = this.getCellSize($td, config); var minWidth = config.stats.map[index] || 0; if (width > minWidth) { this.addCellStat(config.stats, width, index, config.count); return width; } }, /** * Returns the size of a cell * @param {object} td - the cell * @param {object} config - the horizontal/vertical config object for the cell * @private */ getCellSize: function getCellSize($td, config) { return config.sizeFunction($td); }, /** * @private */ getScrollIncrement: function getScrollIncrement(scrollOffset, config) { var index = 0; var stats = config.stats; var stopAt = config.count; var currentOffset = scrollOffset; var rendered = config.renderedCount; var step; var steps = 0; var padding = 0; while (index < stopAt - rendered && currentOffset > 0) { step = stats.map[index]; step = step ? step : stats.maxSize; currentOffset -= step; index++; if (currentOffset >= 0) { padding += step; steps++; } } return { steps: steps, padding: padding }; }, /** * @private */ addCellStat: function addCellStat(stat, size, index, cellCount) { var current = stat.map[index]; if (current && size <= current) { return; } stat.map[index] = size; stat.totalRecordedSize += size; if (current) { stat.totalRecordedSize -= current; } else { stat.count++; } stat.minSize = stat.minSize ? Math.min(stat.minSize, size) : size; stat.maxSize = Math.max(stat.maxSize || 0, size); stat.totalSize = stat.totalRecordedSize + (cellCount - stat.count) * stat.maxSize; stat.averageSize = stat.totalSize / cellCount; }, /** * Renders the table row by row * @private */ renderTable: function renderTable() { var currentRow = this.verticalScrollConfig.getScrolledIdx(); var $tr = null; var bottomOffset = 0; this.horizontalScrollConfig.renderedCount = 0; this.verticalScrollConfig.renderedCount = 0; if (this.$el.is(':hidden')) { return false; } do { $tr = this.appendRow(this.$table, currentRow, false, this.horizontalScrollConfig.renderedCount); if ($tr) { if ($tr.is(':hidden')) { return false; } this.verticalScrollConfig.renderedCount++; if (!this.horizontalScrollConfig.renderedCount) { this.horizontalScrollConfig.renderedCount = $tr.children().length; } currentRow++; if (this.isOutOfViewFromBottom($tr)) { bottomOffset++; } if (bottomOffset === this.offsetCount) { break; } } } while ($tr); return true; }, /** * Renders the pinned column headers * @private */ renderPinnedColumnHeaders: function renderPinnedColumnHeaders() { for (var i = 0; i < this.fixedRows; i++) { this.appendRow(this.$columnHeadersTable, i, false, this.horizontalScrollConfig.renderedCount); } }, /** * Renders the pineed row headers * @private */ renderPinnedRowHeaders: function renderPinnedRowHeaders() { var $tr = void 0; var scrolledVerticalIdx = this.verticalScrollConfig.getScrolledIdx(); for (var i = scrolledVerticalIdx; i < scrolledVerticalIdx + this.verticalScrollConfig.renderedCount; i++) { if (i === this.verticalScrollConfig.count) { break; } $tr = $(''); this.$rowHeadersTable.append($tr); $tr.attr(ATTRIBUTES.ROW_INDEX, i); for (var j = 0; j < this.fixedColumns; j++) { this.appendCell($tr, i, j, false); } } }, /** * Renders the pinned corners * @private */ renderPinnedCorner: function renderPinnedCorner() { this.$corner = $('' + this.dataProvider.getValue(0, 0).value + ''); this.$cornerTable.append($('')); this.$cornerTable.find('tr:first').append(this.$corner); }, /** * Append/Prepend a row to the table, and then append/create all the cells for that row * @param {object} table - the grid table * @param {number} rowIndex - a number representing the row index * @param {boolean} isPrepend - a boolean indicating whether to append or prepend the row to the table * @param {number} renderColumnCount - number indicating how many columns are rendered to the DOM * @private */ appendRow: function appendRow($table, rowIndex, isPrepend, renderColumnCount) { var $tr = null; if (rowIndex < this.verticalScrollConfig.count) { $tr = $(''); if (isPrepend) { $table.prepend($tr); } else { $table.append($tr); } $tr.attr(ATTRIBUTES.ROW_INDEX, rowIndex); var $td; var colIndex = this.horizontalScrollConfig.getScrolledIdx(); var rightOffset = 0; for (var i = colIndex; i < this.horizontalScrollConfig.count; i++) { $td = this.appendCell($tr, rowIndex, i); if (!renderColumnCount) { if (this.isOutOfViewFromRight($td)) { rightOffset++; } if (rightOffset === this.offsetCount) { break; } } else { if (renderColumnCount === i - this.horizontalScrollConfig.getScrolledIdx() + 1) { break; } } } } return $tr; }, /** * Create the actual cell, and then set the cell's minimum width * @private */ createCell: function createCell() { var $td = $(''); this.setCellMinWidth($td); return $td; }, /** * Set's the cell's minimum width in pixels * @param {object} td - the cell * @private */ setCellMinWidth: function setCellMinWidth($td) { $td.css({ 'min-width': this.minCellWidth + 'px', 'height': this.minCellHeight + 'px' }); }, /** * Create the actual cell, set the cell's minimum width, and then append/prepend it to the table * @param {object} tr - the row in the table to append/prepend the cell to * @param {number} rowIndex - number representing the rowIndex of the cell * @param {number} colIndex - number representing the colIndex of the cell * @param {boolean} isPrepend - a boolean representing whether to prepend the cell to the given row, or to append it to the row * @private */ appendCell: function appendCell($tr, rowIndex, colIndex, isPrepend) { var $td = this.createCell(rowIndex, colIndex); if (isPrepend) { $tr.prepend($td); } else { $tr.append($td); } return $td; }, _getPositionFromTop: function _getPositionFromTop($el) { var top = 0; $el.prevAll().each(function () { top += $(this).outerHeight(); }); return top; }, _getPositionFromLeft: function _getPositionFromLeft($el) { var left = 0; $el.prevAll().each(function () { left += $(this).outerWidth(); }); return left; }, /** * Function that returns whether or not the element is out of view from the top * @param {object} el - a jQuery object * @private */ isOutOfViewFromTop: function isOutOfViewFromTop($el) { return this._getPositionFromTop($el) + $el.outerHeight() < 0; }, /** * Function that returns whether or not the element is out of view from the bottom * @param {object} el - a jQuery object * @private */ isOutOfViewFromBottom: function isOutOfViewFromBottom($el) { return this._getPositionFromTop($el) > this.$scrollableArea.height(); }, /** * Function that returns whether or not the element is out of view from the right * @param {object} el - a jQuery object * @private */ isOutOfViewFromRight: function isOutOfViewFromRight($el) { return this._getPositionFromLeft($el) > this.$scrollableArea.width(); }, /** * Function that returns whether or not the element is out of view from the left * @param {object} el - a jQuery object * @private */ isOutOfViewFromLeft: function isOutOfViewFromLeft($el) { return this._getPositionFromLeft($el) + $el.outerWidth() < 0; }, getRowValues: function getRowValues(rowIndex) { var rowData = []; var nodes = this.getVisibleRowNodes(rowIndex); for (var i = 0; i < nodes.length; i++) { var $cell = $(nodes[i]); rowData.push($cell.text()); } return rowData; }, //Returns an array of tds for the given row index. If there is grouping, it returns //the visible td (which may be on a different row) getVisibleRowNodes: function getVisibleRowNodes(rowIndex) { var nodes = this.$rows.find('[' + ATTRIBUTES.ROW_INDEX + '="' + rowIndex + '"]'); for (var i = 0; i < nodes.length; i++) { var $cell = $(nodes[i]); if (!$cell.is(':visible')) { var datagroup = $cell.attr('data-group'); if (!datagroup || !datagroup.length) { continue; } var element = this._getPreviousClosestDatagroupElement($cell, datagroup); nodes.splice(i, 1, element[0]); } } return nodes; }, getDisplayedRows: function getDisplayedRows() { var a_rowCells = []; this.$rows.each(function (idx, row) { a_rowCells.push($(row).find('td')); }.bind(this)); return a_rowCells; }, /** return a visible element that matches the given datagroup in the previous rows. * @private */ _getPreviousClosestDatagroupElement: function _getPreviousClosestDatagroupElement($srcElement, datagroup) { var allPreviousRows = $srcElement.parent().prevAll('tr'); if (!allPreviousRows.length) { return null; } var prevClosestVisibleDatagroupElement = allPreviousRows.find('td[data-group="' + datagroup + '"]:visible'); if (!prevClosestVisibleDatagroupElement.length) { return null; } return prevClosestVisibleDatagroupElement.last(); }, _targetIsOnTheFirstRowAfterFixedRowHeadersTable: function _targetIsOnTheFirstRowAfterFixedRowHeadersTable($target) { return Number($target.attr(ATTRIBUTES.ROW_INDEX)) === this.fixedRows; }, _moveUpToColumnHeadersTableOrCornerTable: function _moveUpToColumnHeadersTableOrCornerTable($target) { if ($target.closest('table.gridRowHeaders').length) { this.setInitialFocus(); return; } var position = this._getCurrentTargetPosition($target); var columnPosition = position.column + 1; var newRowPosition = position.row; var previousRowElement = this.$columnHeadersTable.find('tr:nth-child(' + newRowPosition + ') td:nth-child(' + columnPosition + ')'); if (previousRowElement.length) { this._setFocus(previousRowElement, 'up'); } }, _moveUp: function _moveUp($target) { //:nth-child() is 1-based so need to add 1 var columnIdx = $target.index() + 1; var previousRow = $target.parent().prev('tr'); if (!previousRow.length) { var bTargetIsAtTheTopOfGrid = Number($target.attr(ATTRIBUTES.ROW_INDEX)) === 0; if (bTargetIsAtTheTopOfGrid) { return; } var index = this.verticalScrollConfig.getScrolledIdx() - 1; this._addColumnOrRowAndSetFocus(this.verticalScrollConfig, index, $target, this._moveUp.bind(this)); return; } if (this._targetIsOnTheFirstRowAfterFixedRowHeadersTable($target)) { this._moveUpToColumnHeadersTableOrCornerTable($target); return; } var previousRowElement = previousRow.find('td:nth-child(' + columnIdx + ')'); if (previousRowElement.length > 0 && previousRowElement.is(':visible')) { this._setFocus(previousRowElement, 'up'); return; } //previous row element that is in the same column is not visible, see if it is in a different column var newDatagroup = previousRowElement.attr('data-group'); //get next visible item in same newDatagroup in same row var visibleDatagroupItem = previousRowElement.nextAll('td[data-group="' + newDatagroup + '"]:visible:first'); if (visibleDatagroupItem && visibleDatagroupItem.length) { visibleDatagroupItem.focus(); return; } // no visible item found in same row, try previous rows previousRowElement = this._getPreviousClosestDatagroupElement(previousRowElement, newDatagroup); if (previousRowElement && previousRowElement.length) { this._setFocus(previousRowElement, 'up'); } }, _moveDownToGridFromColumnHeaders: function _moveDownToGridFromColumnHeaders($target, columnIdx) { //to get to the next row, add 1 and another 1 as row is 0 based but nth-child is 1-based var rowIdx = $target.parent().index() + 2; var nextRowElement = this.$table.find('tr:nth-child(' + rowIdx + ') td:nth-child(' + columnIdx + ')'); if (nextRowElement.length) { nextRowElement.focus(); } }, _moveDown: function _moveDown($target) { var columnIdx = $target.index() + 1; var colSpan = Number($target.attr('colSpan')); if (colSpan > 1) { //if colspan is > 1, then on the next row, we want to focus on the first element in the span columnIdx = columnIdx - colSpan + 1; } var oldDatagroup = $target.attr('data-group'); var nextRows = $target.parent().nextAll('tr'); if (!nextRows.length) { var bTargetIsOnColumnHeaderTable = $target.closest('table.gridColumnHeaders').length > 0; if (bTargetIsOnColumnHeaderTable) { this._moveDownToGridFromColumnHeaders($target, columnIdx); return; } //the corner table cell's row attribute does not accurately reflect the var bTargetIsOnCornerTable = $target.closest('table.cornerTable').length > 0; if (bTargetIsOnCornerTable) { //setup the nextRows for moving from cornerTable to rowHeadersTable nextRows = this.$rowHeadersTable.find('tr'); } } //handles moving down on nested row header var nextRowElement = nextRows.find('td[data-group!="' + oldDatagroup + '"]:nth-child(' + columnIdx + ')').first(); var newDatagroup; if (nextRowElement.length) { if (nextRowElement.is(':visible')) { nextRowElement.focus(); return; } newDatagroup = nextRowElement.attr('data-group'); } //handles moving down on nested column headers //next row element in the same column idx is not visible, see if it is in a different column var sameDatagropuItemInRow = nextRowElement.nextAll('td[data-group="' + newDatagroup + '"]:visible:first'); if (sameDatagropuItemInRow && sameDatagropuItemInRow.length > 0) { sameDatagropuItemInRow.focus(); return; } //if there is no more visible item, check if all the rows had been retrieved var rowIndex = Number(nextRowElement.attr(ATTRIBUTES.ROW_INDEX)); var totalNumberOfRows = this.verticalScrollConfig.count; if (rowIndex < totalNumberOfRows) { return; } //if we get here, it means that the new data-group is not fully rendered so get more rows var index = this.verticalScrollConfig.getScrolledIdx() + 1; this._addColumnOrRowAndSetFocus(this.verticalScrollConfig, index, $target, this._moveDown.bind(this)); }, _moveRightFromCornerTableToColumnHeader: function _moveRightFromCornerTableToColumnHeader($target, oldDatagroup) { var firstCellElement = this.$columnHeadersTable.find('tr:visible:first td:first'); var nextElement = firstCellElement.nextAll('td[data-group!="' + oldDatagroup + '"]:visible:first'); if (nextElement.length) { nextElement.focus(); } }, _getCurrentTargetPosition: function _getCurrentTargetPosition($target) { var colPosition = $target.index(); var rowPosition = $target.parent().index(); return { 'column': colPosition, 'row': rowPosition }; }, _moveRightFromRowHeadersToGrid: function _moveRightFromRowHeadersToGrid($target) { var position = this._getCurrentTargetPosition($target); var columnIdx = position.column + 2; var rowIdx = position.row + 1; var nextColumnElement = this.$table.find('tr:nth-child(' + rowIdx + ') td:nth-child(' + columnIdx + ')'); if (nextColumnElement.length && nextColumnElement.is(':visible')) { nextColumnElement.focus(); return; } var datagroup = nextColumnElement.attr('data-group'); nextColumnElement.find('td[data-group="' + datagroup + '"]:visible:first'); if (nextColumnElement.length) { nextColumnElement.focus(); } }, _targetIsOnLastColumnInRowHeadersTable: function _targetIsOnLastColumnInRowHeadersTable($target) { if (!$target.closest('table.gridRowHeaders').length) { return false; } var columnPosition = Number($target.attr(ATTRIBUTES.COLUMN_INDEX)) + 1; return columnPosition === this.fixedColumns; }, _moveRight: function _moveRight($target) { var bTargetIsOnCornerTable = $target.closest('table.cornerTable').length > 0; var oldDatagroup = $target.attr('data-group'); if (bTargetIsOnCornerTable) { this._moveRightFromCornerTableToColumnHeader($target, oldDatagroup); return; } var nextElement; if (this._targetIsOnLastColumnInRowHeadersTable($target)) { this._moveRightFromRowHeadersToGrid($target); return; } nextElement = $target.nextAll('td:visible:first'); if (nextElement.length) { nextElement.focus(); return; } //There are no visible item to focus on, so check if there's a different data-group following $target var nextGroupDataElement = $target.nextAll('td[data-group!="' + oldDatagroup + '"]:first'); if (!nextGroupDataElement.length) { var colIndex = Number($target.attr(ATTRIBUTES.COLUMN_INDEX)) + 1; var totalNumberOfColumns = this.horizontalScrollConfig.count; if (colIndex === totalNumberOfColumns) { return; } //There isn't a different datagroup and no visible element, so the current datagroup must be partially rendered, //so get more data var index = this.horizontalScrollConfig.getScrolledIdx() + 1; this._addColumnOrRowAndSetFocus(this.horizontalScrollConfig, index, $target, this._moveRight.bind(this)); return; } var newDatagroup = nextGroupDataElement.attr('data-group'); nextElement = this._getPreviousClosestDatagroupElement(nextGroupDataElement, newDatagroup); if (nextElement && nextElement.length) { nextElement.focus(); return; } nextElement = this.$rowHeadersTable.nextAll('td[data-group!="' + oldDatagroup + '"]:first'); if (nextElement.length) { nextElement.focus(); } }, _addColumnOrRowAndSetFocus: function _addColumnOrRowAndSetFocus(config, index, $target, setFocusFunction) { if (typeof setFocusFunction !== 'function') { return; } if (!this.updateIndexAndValues(index, config)) { return; } var colIdx = $target.attr(ATTRIBUTES.COLUMN_INDEX); var rowIdx = $target.attr(ATTRIBUTES.ROW_INDEX); //find the target node in the updated table var table = $target.closest('table'); var currentElement = table.find('td[col="' + colIdx + '"][row="' + rowIdx + '"]'); if (!currentElement.length) { return; } setFocusFunction(currentElement); }, _moveLeftToRowHeadersTable: function _moveLeftToRowHeadersTable($target) { var position = this._getCurrentTargetPosition($target); var rowPosition = position.row + 1; //nth-child is 1-based var newColumnPosition = position.column; // column preceding var previousColumnElement = this.$rowHeadersTable.find('tr:nth-child(' + rowPosition + ') td:nth-child(' + newColumnPosition + ')'); previousColumnElement.focus(); }, _moveLeftToRowHeadersOrCornerTable: function _moveLeftToRowHeadersOrCornerTable($target) { var bTargetIsOnColumnHeaderTable = $target.closest('table.gridColumnHeaders').length > 0; if (bTargetIsOnColumnHeaderTable) { this.setInitialFocus(); return; } this._moveLeftToRowHeadersTable($target); }, _targetIsOnColumnOrDataGroupBeforeRowHeadersTable: function _targetIsOnColumnOrDataGroupBeforeRowHeadersTable($target) { var colSpan = $target.attr('colspan'); if (colSpan) { colSpan = Number(colSpan); } var columnIndex = $target.index(); var columnIdxToUse; if (colSpan && colSpan > 1) { columnIdxToUse = columnIndex - colSpan + 1; } else { columnIdxToUse = columnIndex; } return this.fixedColumns && columnIdxToUse === this.fixedColumns; }, _moveLeft: function _moveLeft($target) { var bTargetIsOnFirstColumn = Number($target.attr(ATTRIBUTES.COLUMN_INDEX)) === 0; if (bTargetIsOnFirstColumn) { return; } if (this._targetIsOnColumnOrDataGroupBeforeRowHeadersTable($target)) { this._moveLeftToRowHeadersOrCornerTable($target); return; } var previousElement = $target.prevAll('td:visible:first'); if (previousElement.length) { this._setFocus(previousElement, 'left'); return; } //There are no visible element to focus on, so check if there is a different datagroup preceding $target var oldDatagroup = $target.attr('data-group'); var previousGroupDataElement = $target.prevAll('td[data-group!="' + oldDatagroup + '"]:first'); if (!previousGroupDataElement.length) { var colIndex = Number($target.attr(ATTRIBUTES.COLUMN_INDEX)); if (colIndex === 0) { return; } // there isn't a different datagroup, therefore, the current datagroup must be only partially rendered // so get more data this._addColumnOrRowAndSetFocus(this.horizontalScrollConfig, this.horizontalScrollConfig.getScrolledIdx() - 1, $target, this._moveLeft.bind(this)); return; } // There is a different datagroup but since there is no visible element on the same row, it means that the visible element must be on one of the // previous rows var newDatagroup = previousGroupDataElement.attr('data-group'); previousElement = this._getPreviousClosestDatagroupElement(previousGroupDataElement, newDatagroup); if (previousElement && previousElement.length > 0) { this._setFocus(previousElement, 'left'); return; } }, _setFocus: function _setFocus($nodeToFocus, moveDirection) { $nodeToFocus.focus(); var nodeToFocusRect = $nodeToFocus[0].getBoundingClientRect(); switch (moveDirection) { case 'left': { var rowHeadersTableRect = this.$rowHeadersTable[0].getBoundingClientRect(); if (nodeToFocusRect.left + 1 > rowHeadersTableRect.right) { return; } var leftScrollAmount = rowHeadersTableRect.width - (nodeToFocusRect.left - rowHeadersTableRect.left); this.$scrollableArea.scrollLeft(this.$scrollableArea.scrollLeft() - leftScrollAmount); break; } case 'up': { var columnHeadersTable = this.$columnHeadersTable[0].getBoundingClientRect(); if (nodeToFocusRect.top + 1 > columnHeadersTable.bottom) { return; } var topScrollAmount = columnHeadersTable.height - (nodeToFocusRect.top - columnHeadersTable.top); this.$scrollableArea.scrollTop(this.$scrollableArea.scrollTop() - topScrollAmount); break; } } }, _moveToFirstRow: function _moveToFirstRow() { this.setInitialFocus(); }, _moveToLastRow: function _moveToLastRow() { this.$table.find('tr:visible:last td:visible:last').focus(); }, onKeyDown: function onKeyDown(event) { var $target = $(event.target); var bStopPropagation = false; switch (event.keyCode) { case KeyCodes.KEY_UP: { bStopPropagation = true; this._moveUp($target); break; } case KeyCodes.KEY_DOWN: { bStopPropagation = true; this._moveDown($target); break; } case KeyCodes.KEY_RIGHT: { bStopPropagation = true; this._moveRight($target); break; } case KeyCodes.KEY_LEFT: { bStopPropagation = true; this._moveLeft($target); break; } case KeyCodes.KEY_h: case KeyCodes.KEY_HOME: { if (!event.ctrlKey) { return; } bStopPropagation = true; this._moveToFirstRow(); break; } case KeyCodes.KEY_e: case KeyCodes.KEY_END: { if (!event.ctrlKey) { return; } bStopPropagation = true; this._moveToLastRow(); break; } } if (bStopPropagation) { this._savedKeyboardNavigationInfo = { 'keycode': event.keyCode, '$target': $target }; event.preventDefault(); event.stopPropagation(); } }, setInitialFocus: function setInitialFocus() { var startingTable = this.bHasCornerTable ? this.$cornerTable : this.$columnHeadersTable; var initialNode = startingTable.find('tr:visible:first td:visible:first'); if (initialNode.length) { initialNode.focus(); } }, _setFocusAfterScroll: function _setFocusAfterScroll() { if (!this._savedKeyboardNavigationInfo) { return; } switch (this._savedKeyboardNavigationInfo.keycode) { case KeyCodes.KEY_UP: this._moveUp(this._savedKeyboardNavigationInfo.$target); break; case KeyCodes.KEY_DOWN: this._moveDown(this._savedKeyboardNavigationInfo.$target); break; case KeyCodes.KEY_RIGHT: this._moveRight(this._savedKeyboardNavigationInfo.$target); break; case KeyCodes.KEY_LEFT: this._moveLeft(this._savedKeyboardNavigationInfo.$target); break; case KeyCodes.KEY_e: case KeyCodes.KEY_END: this._moveToLastRow(); break; case KeyCodes.KEY_h: case KeyCodes.KEY_HOME: this._moveToFirstRow(); break; } }, /** * Update the specified table when scrollled vertically * * @param {object} options - the options to scroll the table vertically * */ _updateTableScrolledVertically: function _updateTableScrolledVertically() { var _this6 = this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var $table = options.$table; var previousScrolledIdx = options.previousScrolledIdx; var delta = options.delta; var handleGrouping = options.handleGrouping || false; var isRowHeader = options.isRowHeader || false; var scrolledVerticalIdx = this.verticalScrollConfig.getScrolledIdx(); var scrollDown = previousScrolledIdx < scrolledVerticalIdx; var $rows = $table.find('tr'); var $trAfter = delta > 0 ? $rows.eq(delta - 1) : null; var count = scrollDown ? scrolledVerticalIdx - previousScrolledIdx - 1 : previousScrolledIdx - scrolledVerticalIdx - 1; var $row = void 0; var rowContext = void 0; var _getRowUpdateContext = function _getRowUpdateContext($row, idx) { return { $tr: $row, rowIndex: scrollDown ? _this6.verticalScrollConfig.renderedCount + scrolledVerticalIdx - 1 - idx : idx + scrolledVerticalIdx + delta, handleGrouping: handleGrouping, isRowHeader: isRowHeader, cbGetScrolledFastPinnedCornerCellIdx: cbGetScrolledFastPinnedCornerCellIdx }; }; for (var idx = count; idx >= 0; idx--) { rowContext = null; if (scrollDown) { $row = $rows.eq(idx + delta); $row.appendTo($rows.parent()); } else { $row = $rows.eq($rows.length - idx - 1); if ($trAfter) { $row.insertAfter($trAfter); } else { $row.prependTo($rows.parent()); } } rowContext = _getRowUpdateContext($row, idx); this.updateRow(rowContext); } } }); View.getColumnPosition = function ($tdNode) { return Number($tdNode.attr(ATTRIBUTES.COLUMN_INDEX)); }; View.getRowPosition = function ($tdNode) { return Number($tdNode.attr(ATTRIBUTES.ROW_INDEX)); }; return View; }); //# sourceMappingURL=Grid.js.map