');
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.$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
|