ag-grid
Version:
Advanced Javascript Datagrid. Supports raw Javascript, AngularJS 1.x, AngularJS 2.0 and Web Components
591 lines (502 loc) • 25.4 kB
text/typescript
/// <reference path="../utils.ts" />
/// <reference path="../constants.ts" />
/// <reference path="renderedRow.ts" />
/// <reference path="../cellRenderers/groupCellRendererFactory.ts" />
module ag.grid {
var _ = Utils;
export class RowRenderer {
private columnModel: any;
private gridOptionsWrapper: GridOptionsWrapper;
private angularGrid: Grid;
private selectionRendererFactory: SelectionRendererFactory;
private gridPanel: GridPanel;
private $compile: any;
private $scope: any;
private selectionController: SelectionController;
private expressionService: ExpressionService;
private templateService: TemplateService;
private cellRendererMap: {[key: string]: any};
private rowModel: any;
private firstVirtualRenderedRow: number;
private lastVirtualRenderedRow: number;
private focusedCell: any;
private valueService: ValueService;
private eventService: EventService;
private renderedRows: {[key: string]: RenderedRow};
private renderedTopFloatingRows: RenderedRow[] = [];
private renderedBottomFloatingRows: RenderedRow[] = [];
private eAllBodyContainers: HTMLElement[];
private eAllPinnedContainers: HTMLElement[];
private eBodyContainer: HTMLElement;
private eBodyViewport: HTMLElement;
private ePinnedColsContainer: HTMLElement;
private eFloatingTopContainer: HTMLElement;
private eFloatingTopPinnedContainer: HTMLElement;
private eFloatingBottomContainer: HTMLElement;
private eFloatingBottomPinnedContainer: HTMLElement;
private eParentsOfRows: HTMLElement[];
public init(columnModel: any, gridOptionsWrapper: GridOptionsWrapper, gridPanel: GridPanel,
angularGrid: Grid, selectionRendererFactory: SelectionRendererFactory, $compile: any, $scope: any,
selectionController: SelectionController, expressionService: ExpressionService,
templateService: TemplateService, valueService: ValueService, eventService: EventService) {
this.columnModel = columnModel;
this.gridOptionsWrapper = gridOptionsWrapper;
this.angularGrid = angularGrid;
this.selectionRendererFactory = selectionRendererFactory;
this.gridPanel = gridPanel;
this.$compile = $compile;
this.$scope = $scope;
this.selectionController = selectionController;
this.expressionService = expressionService;
this.templateService = templateService;
this.valueService = valueService;
this.findAllElements(gridPanel);
this.eventService = eventService;
this.cellRendererMap = {
'group': groupCellRendererFactory(gridOptionsWrapper, selectionRendererFactory, expressionService),
'default': function(params: any) {
return params.value;
}
};
// map of row ids to row objects. keeps track of which elements
// are rendered for which rows in the dom.
this.renderedRows = {};
}
public setRowModel(rowModel: any) {
this.rowModel = rowModel;
}
public onIndividualColumnResized(column: Column) {
var newWidthPx = column.actualWidth + "px";
var selectorForAllColsInCell = ".cell-col-" + column.index;
this.eParentsOfRows.forEach( function(rowContainer: HTMLElement) {
var cellsForThisCol: NodeList = rowContainer.querySelectorAll(selectorForAllColsInCell);
for (var i = 0; i < cellsForThisCol.length; i++) {
var element = <HTMLElement> cellsForThisCol[i];
element.style.width = newWidthPx;
}
});
}
public setMainRowWidths() {
var mainRowWidth = this.columnModel.getBodyContainerWidth() + "px";
this.eAllBodyContainers.forEach( function(container: HTMLElement) {
var unpinnedRows: [any] = (<any>container).querySelectorAll(".ag-row");
for (var i = 0; i < unpinnedRows.length; i++) {
unpinnedRows[i].style.width = mainRowWidth;
}
});
}
private findAllElements(gridPanel: any) {
this.eBodyContainer = gridPanel.getBodyContainer();
this.ePinnedColsContainer = gridPanel.getPinnedColsContainer();
this.eFloatingTopContainer = gridPanel.getFloatingTopContainer();
this.eFloatingTopPinnedContainer = gridPanel.getPinnedFloatingTop();
this.eFloatingBottomContainer = gridPanel.getFloatingBottomContainer();
this.eFloatingBottomPinnedContainer = gridPanel.getPinnedFloatingBottom();
this.eBodyViewport = gridPanel.getBodyViewport();
this.eParentsOfRows = gridPanel.getRowsParent();
this.eAllBodyContainers = [this.eBodyContainer, this.eFloatingBottomContainer,
this.eFloatingTopContainer];
this.eAllPinnedContainers = [this.ePinnedColsContainer, this.eFloatingBottomPinnedContainer,
this.eFloatingTopPinnedContainer];
}
public refreshAllFloatingRows(): void {
this.refreshFloatingRows(
this.renderedTopFloatingRows,
this.gridOptionsWrapper.getFloatingTopRowData(),
this.eFloatingTopPinnedContainer,
this.eFloatingTopContainer,
true);
this.refreshFloatingRows(
this.renderedBottomFloatingRows,
this.gridOptionsWrapper.getFloatingBottomRowData(),
this.eFloatingBottomPinnedContainer,
this.eFloatingBottomContainer,
false);
}
private refreshFloatingRows(renderedRows: RenderedRow[], rowData: any[],
pinnedContainer: HTMLElement, bodyContainer: HTMLElement,
isTop: boolean): void {
renderedRows.forEach( (row: RenderedRow) => {
row.destroy();
});
renderedRows.length = 0;
// if no cols, don't draw row - can we get rid of this???
var columns = this.columnModel.getDisplayedColumns();
if (!columns || columns.length == 0) {
return;
}
// should we be storing this somewhere???
var mainRowWidth = this.columnModel.getBodyContainerWidth();
if (rowData) {
rowData.forEach( (data: any, rowIndex: number) => {
var node: RowNode = {
data: data,
floating: true,
floatingTop: isTop,
floatingBottom: !isTop
};
var renderedRow = new RenderedRow(this.gridOptionsWrapper, this.valueService, this.$scope, this.angularGrid,
this.columnModel, this.expressionService, this.cellRendererMap, this.selectionRendererFactory,
this.$compile, this.templateService, this.selectionController, this,
bodyContainer, pinnedContainer, node, rowIndex, this.eventService);
renderedRow.setMainRowWidth(mainRowWidth);
renderedRows.push(renderedRow);
})
}
}
public refreshView(refreshFromIndex?: any) {
if (!this.gridOptionsWrapper.isForPrint()) {
var rowCount = this.rowModel.getVirtualRowCount();
var containerHeight = this.gridOptionsWrapper.getRowHeight() * rowCount;
this.eBodyContainer.style.height = containerHeight + "px";
this.ePinnedColsContainer.style.height = containerHeight + "px";
}
this.refreshAllVirtualRows(refreshFromIndex);
this.refreshAllFloatingRows();
}
public softRefreshView() {
_.iterateObject(this.renderedRows, (key: any, renderedRow: RenderedRow)=> {
renderedRow.softRefresh();
});
}
public refreshRows(rowNodes: RowNode[]): void {
if (!rowNodes || rowNodes.length==0) {
return;
}
// we only need to be worried about rendered rows, as this method is
// called to whats rendered. if the row isn't rendered, we don't care
var indexesToRemove: any = [];
_.iterateObject(this.renderedRows, (key: string, renderedRow: RenderedRow)=> {
var rowNode = renderedRow.getRowNode();
if (rowNodes.indexOf(rowNode)>=0) {
indexesToRemove.push(key);
}
});
// remove the rows
this.removeVirtualRow(indexesToRemove);
// add draw them again
this.drawVirtualRows();
}
public refreshCells(rowNodes: RowNode[], colIds: string[]): void {
if (!rowNodes || rowNodes.length==0) {
return;
}
// we only need to be worried about rendered rows, as this method is
// called to whats rendered. if the row isn't rendered, we don't care
_.iterateObject(this.renderedRows, (key: string, renderedRow: RenderedRow)=> {
var rowNode = renderedRow.getRowNode();
if (rowNodes.indexOf(rowNode)>=0) {
renderedRow.refreshCells(colIds);
}
});
}
public rowDataChanged(rows: any) {
// we only need to be worried about rendered rows, as this method is
// called to whats rendered. if the row isn't rendered, we don't care
var indexesToRemove: any = [];
var renderedRows = this.renderedRows;
Object.keys(renderedRows).forEach(function (key: any) {
var renderedRow = renderedRows[key];
// see if the rendered row is in the list of rows we have to update
if (renderedRow.isDataInList(rows)) {
indexesToRemove.push(key);
}
});
// remove the rows
this.removeVirtualRow(indexesToRemove);
// add draw them again
this.drawVirtualRows();
}
private refreshAllVirtualRows(fromIndex: any) {
// remove all current virtual rows, as they have old data
var rowsToRemove = Object.keys(this.renderedRows);
this.removeVirtualRow(rowsToRemove, fromIndex);
// add in new rows
this.drawVirtualRows();
}
// public - removes the group rows and then redraws them again
public refreshGroupRows() {
// find all the group rows
var rowsToRemove: any = [];
var that = this;
Object.keys(this.renderedRows).forEach(function (key: any) {
var renderedRow = that.renderedRows[key];
if (renderedRow.isGroup()) {
rowsToRemove.push(key);
}
});
// remove the rows
this.removeVirtualRow(rowsToRemove);
// and draw them back again
this.ensureRowsRendered();
}
// takes array of row indexes
private removeVirtualRow(rowsToRemove: any, fromIndex?: any) {
var that = this;
// if no fromIndex then set to -1, which will refresh everything
var realFromIndex = (typeof fromIndex === 'number') ? fromIndex : -1;
rowsToRemove.forEach(function (indexToRemove: any) {
if (indexToRemove >= realFromIndex) {
that.unbindVirtualRow(indexToRemove);
// if the row was last to have focus, we remove the fact that it has focus
if (that.focusedCell && that.focusedCell.rowIndex == indexToRemove) {
that.focusedCell = null;
}
}
});
}
private unbindVirtualRow(indexToRemove: any) {
var renderedRow = this.renderedRows[indexToRemove];
renderedRow.destroy();
var event = {node: renderedRow.getRowNode(), rowIndex: indexToRemove};
this.eventService.dispatchEvent(Events.EVENT_VIRTUAL_ROW_REMOVED, event);
this.angularGrid.onVirtualRowRemoved(indexToRemove);
delete this.renderedRows[indexToRemove];
}
public drawVirtualRows() {
var first: any;
var last: any;
var rowCount = this.rowModel.getVirtualRowCount();
if (this.gridOptionsWrapper.isForPrint()) {
first = 0;
last = rowCount;
} else {
var topPixel = this.eBodyViewport.scrollTop;
var bottomPixel = topPixel + this.eBodyViewport.offsetHeight;
first = Math.floor(topPixel / this.gridOptionsWrapper.getRowHeight());
last = Math.floor(bottomPixel / this.gridOptionsWrapper.getRowHeight());
//add in buffer
var buffer = this.gridOptionsWrapper.getRowBuffer() || Constants.ROW_BUFFER_SIZE;
first = first - buffer;
last = last + buffer;
// adjust, in case buffer extended actual size
if (first < 0) {
first = 0;
}
if (last > rowCount - 1) {
last = rowCount - 1;
}
}
this.firstVirtualRenderedRow = first;
this.lastVirtualRenderedRow = last;
this.ensureRowsRendered();
}
public getFirstVirtualRenderedRow() {
return this.firstVirtualRenderedRow;
}
public getLastVirtualRenderedRow() {
return this.lastVirtualRenderedRow;
}
private ensureRowsRendered() {
//var start = new Date().getTime();
var mainRowWidth = this.columnModel.getBodyContainerWidth();
var that = this;
// at the end, this array will contain the items we need to remove
var rowsToRemove = Object.keys(this.renderedRows);
// add in new rows
for (var rowIndex = this.firstVirtualRenderedRow; rowIndex <= this.lastVirtualRenderedRow; rowIndex++) {
// see if item already there, and if yes, take it out of the 'to remove' array
if (rowsToRemove.indexOf(rowIndex.toString()) >= 0) {
rowsToRemove.splice(rowsToRemove.indexOf(rowIndex.toString()), 1);
continue;
}
// check this row actually exists (in case overflow buffer window exceeds real data)
var node = this.rowModel.getVirtualRow(rowIndex);
if (node) {
that.insertRow(node, rowIndex, mainRowWidth);
}
}
// at this point, everything in our 'rowsToRemove' . . .
this.removeVirtualRow(rowsToRemove);
// if we are doing angular compiling, then do digest the scope here
if (this.gridOptionsWrapper.isAngularCompileRows()) {
// we do it in a timeout, in case we are already in an apply
setTimeout(function () {
that.$scope.$apply();
}, 0);
}
//var end = new Date().getTime();
//console.log(end-start);
}
private insertRow(node: any, rowIndex: any, mainRowWidth: any) {
var columns = this.columnModel.getDisplayedColumns();
// if no cols, don't draw row
if (!columns || columns.length == 0) {
return;
}
var renderedRow = new RenderedRow(this.gridOptionsWrapper, this.valueService, this.$scope, this.angularGrid,
this.columnModel, this.expressionService, this.cellRendererMap, this.selectionRendererFactory,
this.$compile, this.templateService, this.selectionController, this,
this.eBodyContainer, this.ePinnedColsContainer, node, rowIndex, this.eventService);
renderedRow.setMainRowWidth(mainRowWidth);
this.renderedRows[rowIndex] = renderedRow;
}
public getRenderedNodes() {
var renderedRows = this.renderedRows;
return Object.keys(renderedRows).map(key => {
return renderedRows[key].getRowNode();
});
}
public getIndexOfRenderedNode(node: any): number {
var renderedRows = this.renderedRows;
var keys: string[] = Object.keys(renderedRows);
for (var i = 0; i < keys.length; i++) {
var key: string = keys[i];
if (renderedRows[key].getRowNode() === node) {
return renderedRows[key].getRowIndex();
}
}
return -1;
}
// we use index for rows, but column object for columns, as the next column (by index) might not
// be visible (header grouping) so it's not reliable, so using the column object instead.
public navigateToNextCell(key: any, rowIndex: number, column: Column) {
var cellToFocus = {rowIndex: rowIndex, column: column};
var renderedRow: RenderedRow;
var eCell: any;
// we keep searching for a next cell until we find one. this is how the group rows get skipped
while (!eCell) {
cellToFocus = this.getNextCellToFocus(key, cellToFocus);
// no next cell means we have reached a grid boundary, eg left, right, top or bottom of grid
if (!cellToFocus) {
return;
}
// see if the next cell is selectable, if yes, use it, if not, skip it
renderedRow = this.renderedRows[cellToFocus.rowIndex];
eCell = renderedRow.getCellForCol(cellToFocus.column);
}
// this scrolls the row into view
this.gridPanel.ensureIndexVisible(renderedRow.getRowIndex());
// this changes the css on the cell
this.focusCell(eCell, cellToFocus.rowIndex, cellToFocus.column.index, cellToFocus.column.colDef, true);
}
private getNextCellToFocus(key: any, lastCellToFocus: any) {
var lastRowIndex = lastCellToFocus.rowIndex;
var lastColumn = lastCellToFocus.column;
var nextRowToFocus: any;
var nextColumnToFocus: any;
switch (key) {
case Constants.KEY_UP :
// if already on top row, do nothing
if (lastRowIndex === this.firstVirtualRenderedRow) {
return null;
}
nextRowToFocus = lastRowIndex - 1;
nextColumnToFocus = lastColumn;
break;
case Constants.KEY_DOWN :
// if already on bottom, do nothing
if (lastRowIndex === this.lastVirtualRenderedRow) {
return null;
}
nextRowToFocus = lastRowIndex + 1;
nextColumnToFocus = lastColumn;
break;
case Constants.KEY_RIGHT :
var colToRight = this.columnModel.getVisibleColAfter(lastColumn);
// if already on right, do nothing
if (!colToRight) {
return null;
}
nextRowToFocus = lastRowIndex;
nextColumnToFocus = colToRight;
break;
case Constants.KEY_LEFT :
var colToLeft = this.columnModel.getVisibleColBefore(lastColumn);
// if already on left, do nothing
if (!colToLeft) {
return null;
}
nextRowToFocus = lastRowIndex;
nextColumnToFocus = colToLeft;
break;
}
return {
rowIndex: nextRowToFocus,
column: nextColumnToFocus
};
}
public onRowSelected(rowIndex: number, selected: boolean) {
if (this.renderedRows[rowIndex]) {
this.renderedRows[rowIndex].onRowSelected(selected);
}
}
// called by the renderedRow
public focusCell(eCell: any, rowIndex: number, colIndex: number, colDef: ColDef, forceBrowserFocus: any) {
// do nothing if cell selection is off
if (this.gridOptionsWrapper.isSuppressCellSelection()) {
return;
}
this.eParentsOfRows.forEach( function(rowContainer: HTMLElement) {
// remove any previous focus
_.querySelectorAll_replaceCssClass(rowContainer, '.ag-cell-focus', 'ag-cell-focus', 'ag-cell-no-focus');
var selectorForCell = '[row="' + rowIndex + '"] [col="' + colIndex + '"]';
_.querySelectorAll_replaceCssClass(rowContainer, selectorForCell, 'ag-cell-no-focus', 'ag-cell-focus');
});
this.focusedCell = {rowIndex: rowIndex, colIndex: colIndex, node: this.rowModel.getVirtualRow(rowIndex), colDef: colDef};
// this puts the browser focus on the cell (so it gets key presses)
if (forceBrowserFocus) {
eCell.focus();
}
this.eventService.dispatchEvent(Events.EVENT_CELL_FOCUSED, this.focusedCell);
}
// for API
public getFocusedCell() {
return this.focusedCell;
}
// called via API
public setFocusedCell(rowIndex: any, colIndex: any) {
var renderedRow = this.renderedRows[rowIndex];
var column = this.columnModel.getDisplayedColumns()[colIndex];
if (renderedRow && column) {
var eCell = renderedRow.getCellForCol(column);
this.focusCell(eCell, rowIndex, colIndex, column.colDef, true);
}
}
// called by the cell, when tab is pressed while editing
public startEditingNextCell(rowIndex: any, column: any, shiftKey: any) {
var firstRowToCheck = this.firstVirtualRenderedRow;
var lastRowToCheck = this.lastVirtualRenderedRow;
var currentRowIndex = rowIndex;
var visibleColumns = this.columnModel.getDisplayedColumns();
var currentCol = column;
while (true) {
var indexOfCurrentCol = visibleColumns.indexOf(currentCol);
// move backward
if (shiftKey) {
// move along to the previous cell
currentCol = visibleColumns[indexOfCurrentCol - 1];
// check if end of the row, and if so, go back a row
if (!currentCol) {
currentCol = visibleColumns[visibleColumns.length - 1];
currentRowIndex--;
}
// if got to end of rendered rows, then quit looking
if (currentRowIndex < firstRowToCheck) {
return;
}
// move forward
} else {
// move along to the next cell
currentCol = visibleColumns[indexOfCurrentCol + 1];
// check if end of the row, and if so, go forward a row
if (!currentCol) {
currentCol = visibleColumns[0];
currentRowIndex++;
}
// if got to end of rendered rows, then quit looking
if (currentRowIndex > lastRowToCheck) {
return;
}
}
var nextRenderedRow: RenderedRow = this.renderedRows[currentRowIndex];
var nextRenderedCell: RenderedCell = nextRenderedRow.getRenderedCellForColumn(currentCol);
if (nextRenderedCell.isCellEditable()) {
nextRenderedCell.startEditing();
nextRenderedCell.focusCell(false);
return;
}
}
}
}
}