ag-grid
Version:
Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components
1,176 lines (972 loc) • 46.7 kB
text/typescript
import { Utils as _ } from "../utils";
import { GridOptionsWrapper } from "../gridOptionsWrapper";
import { GridPanel, RowContainerComponents } from "../gridPanel/gridPanel";
import { ExpressionService } from "../valueService/expressionService";
import { TemplateService } from "../templateService";
import { ValueService } from "../valueService/valueService";
import { EventService } from "../eventService";
import { RowComp } from "./rowComp";
import { Column } from "../entities/column";
import { RowNode } from "../entities/rowNode";
import { Events, ModelUpdatedEvent, ViewportChangedEvent } from "../events";
import { Constants } from "../constants";
import { CellComp } from "./cellComp";
import { Autowired, Bean, Context, Optional, PostConstruct, PreDestroy, Qualifier } from "../context/context";
import { GridCore } from "../gridCore";
import { ColumnApi } from "../columnController/columnApi";
import { ColumnController } from "../columnController/columnController";
import { Logger, LoggerFactory } from "../logger";
import { FocusedCellController } from "../focusedCellController";
import { IRangeController } from "../interfaces/iRangeController";
import { CellNavigationService } from "../cellNavigationService";
import {GridCell, GridCellDef} from "../entities/gridCell";
import { NavigateToNextCellParams, TabToNextCellParams } from "../entities/gridOptions";
import { RowContainerComponent } from "./rowContainerComponent";
import { BeanStub } from "../context/beanStub";
import { PaginationProxy } from "../rowModels/paginationProxy";
import {FlashCellsParams, GetCellRendererInstancesParams, GridApi, RefreshCellsParams} from "../gridApi";
import { PinnedRowModel } from "../rowModels/pinnedRowModel";
import { Beans } from "./beans";
import { AnimationFrameService } from "../misc/animationFrameService";
import { HeightScaler } from "./heightScaler";
import {Grid} from "../grid";
import {ICellRendererComp} from "./cellRenderers/iCellRenderer";
import {ICellEditorComp} from "./cellEditors/iCellEditor";
export class RowRenderer extends BeanStub {
private paginationProxy: PaginationProxy;
private columnController: ColumnController;
private gridOptionsWrapper: GridOptionsWrapper;
private gridCore: GridCore;
private $scope: any;
private expressionService: ExpressionService;
private templateService: TemplateService;
private valueService: ValueService;
private eventService: EventService;
private pinnedRowModel: PinnedRowModel;
private context: Context;
private loggerFactory: LoggerFactory;
private focusedCellController: FocusedCellController;
private cellNavigationService: CellNavigationService;
private columnApi: ColumnApi;
private gridApi: GridApi;
private beans: Beans;
private heightScaler: HeightScaler;
private animationFrameService: AnimationFrameService;
private rangeController: IRangeController;
private gridPanel: GridPanel;
private firstRenderedRow: number;
private lastRenderedRow: number;
// map of row ids to row objects. keeps track of which elements
// are rendered for which rows in the dom.
private rowCompsByIndex: { [key: string]: RowComp } = {};
private floatingTopRowComps: RowComp[] = [];
private floatingBottomRowComps: RowComp[] = [];
private rowContainers: RowContainerComponents;
private pinningLeft: boolean;
private pinningRight: boolean;
// we only allow one refresh at a time, otherwise the internal memory structure here
// will get messed up. this can happen if the user has a cellRenderer, and inside the
// renderer they call an API method that results in another pass of the refresh,
// then it will be trying to draw rows in the middle of a refresh.
private refreshInProgress = false;
private logger: Logger;
public agWire( loggerFactory: LoggerFactory) {
this.logger = loggerFactory.create("RowRenderer");
}
public registerGridComp(gridPanel: GridPanel): void {
this.gridPanel = gridPanel;
this.rowContainers = this.gridPanel.getRowContainers();
this.addDestroyableEventListener(this.eventService, Events.EVENT_PAGINATION_CHANGED, this.onPageLoaded.bind(this));
this.addDestroyableEventListener(this.eventService, Events.EVENT_PINNED_ROW_DATA_CHANGED, this.onPinnedRowDataChanged.bind(this));
this.addDestroyableEventListener(this.eventService, Events.EVENT_DISPLAYED_COLUMNS_CHANGED, this.onDisplayedColumnsChanged.bind(this));
this.addDestroyableEventListener(this.eventService, Events.EVENT_BODY_SCROLL, this.redrawAfterScroll.bind(this));
this.addDestroyableEventListener(this.eventService, Events.EVENT_BODY_HEIGHT_CHANGED, this.redrawAfterScroll.bind(this));
this.redrawAfterModelUpdate();
}
private onPageLoaded(refreshEvent?: ModelUpdatedEvent): void {
if (_.missing(refreshEvent)) {
refreshEvent = {
type: Events.EVENT_MODEL_UPDATED,
api: this.gridApi,
columnApi: this.columnApi,
animate: false,
keepRenderedRows: false,
newData: false,
newPage: false
};
}
this.onModelUpdated(refreshEvent);
}
public getAllCellsForColumn(column: Column): HTMLElement[] {
let eCells: HTMLElement[] = [];
_.iterateObject(this.rowCompsByIndex, callback);
_.iterateObject(this.floatingBottomRowComps, callback);
_.iterateObject(this.floatingTopRowComps, callback);
function callback(key: any, rowComp: RowComp) {
let eCell = rowComp.getCellForCol(column);
if (eCell) {
eCells.push(eCell);
}
}
return eCells;
}
public refreshFloatingRowComps(): void {
this.refreshFloatingRows(
this.floatingTopRowComps,
this.pinnedRowModel.getPinnedTopRowData(),
this.rowContainers.floatingTopPinnedLeft,
this.rowContainers.floatingTopPinnedRight,
this.rowContainers.floatingTop,
this.rowContainers.floatingTopFullWidth
);
this.refreshFloatingRows(
this.floatingBottomRowComps,
this.pinnedRowModel.getPinnedBottomRowData(),
this.rowContainers.floatingBottomPinnedLeft,
this.rowContainers.floatingBottomPinnedRight,
this.rowContainers.floatingBottom,
this.rowContainers.floatingBottomFullWith
);
}
private refreshFloatingRows(
rowComps: RowComp[],
rowNodes: RowNode[],
pinnedLeftContainerComp: RowContainerComponent,
pinnedRightContainerComp: RowContainerComponent,
bodyContainerComp: RowContainerComponent,
fullWidthContainerComp: RowContainerComponent
): void {
rowComps.forEach((row: RowComp) => {
row.destroy();
});
rowComps.length = 0;
if (rowNodes) {
rowNodes.forEach((node: RowNode) => {
let rowComp = new RowComp(
this.$scope,
bodyContainerComp,
pinnedLeftContainerComp,
pinnedRightContainerComp,
fullWidthContainerComp,
node,
this.beans,
false,
false
);
rowComp.init();
rowComps.push(rowComp);
});
}
this.flushContainers(rowComps);
}
private onPinnedRowDataChanged(): void {
// recycling rows in order to ensure cell editing is not cancelled
let params: RefreshViewParams = {
recycleRows: true
};
this.redrawAfterModelUpdate(params);
}
private onModelUpdated(refreshEvent: ModelUpdatedEvent): void {
let params: RefreshViewParams = {
recycleRows: refreshEvent.keepRenderedRows,
animate: refreshEvent.animate,
newData: refreshEvent.newData,
newPage: refreshEvent.newPage,
// because this is a model updated event (not pinned rows), we
// can skip updating the pinned rows. this is needed so that if user
// is doing transaction updates, the pinned rows are not getting constantly
// trashed - or editing cells in pinned rows are not refreshed and put into read mode
onlyBody: true
};
this.redrawAfterModelUpdate(params);
}
// if the row nodes are not rendered, no index is returned
private getRenderedIndexesForRowNodes(rowNodes: RowNode[]): string[] {
let result: string[] = [];
if (_.missing(rowNodes)) {
return result;
}
_.iterateObject(this.rowCompsByIndex, (index: string, renderedRow: RowComp) => {
let rowNode = renderedRow.getRowNode();
if (rowNodes.indexOf(rowNode) >= 0) {
result.push(index);
}
});
return result;
}
public redrawRows(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
let indexesToRemove = this.getRenderedIndexesForRowNodes(rowNodes);
// remove the rows
this.removeRowComps(indexesToRemove);
// add draw them again
this.redrawAfterModelUpdate({
recycleRows: true
});
}
private getCellToRestoreFocusToAfterRefresh(params: RefreshViewParams): GridCell {
let focusedCell = params.suppressKeepFocus ? null : this.focusedCellController.getFocusCellToUseAfterRefresh();
if (_.missing(focusedCell)) {
return null;
}
// if the dom is not actually focused on a cell, then we don't try to refocus. the problem this
// solves is with editing - if the user is editing, eg focus is on a text field, and not on the
// cell itself, then the cell can be registered as having focus, however it's the text field that
// has the focus and not the cell div. therefore, when the refresh is finished, the grid will focus
// the cell, and not the textfield. that means if the user is in a text field, and the grid refreshes,
// the focus is lost from the text field. we do not want this.
let activeElement = document.activeElement;
let domData = this.gridOptionsWrapper.getDomData(activeElement, CellComp.DOM_DATA_KEY_CELL_COMP);
let elementIsNotACellDev = _.missing(domData);
if (elementIsNotACellDev) {
return null;
}
return focusedCell;
}
// gets called after changes to the model.
public redrawAfterModelUpdate(params: RefreshViewParams = {}): void {
this.getLockOnRefresh();
let focusedCell: GridCell = this.getCellToRestoreFocusToAfterRefresh(params);
this.sizeContainerToPageHeight();
this.scrollToTopIfNewData(params);
let recycleRows = params.recycleRows;
let animate = params.animate && this.gridOptionsWrapper.isAnimateRows();
let rowsToRecycle: { [key: string]: RowComp } = this.binRowComps(recycleRows);
this.redraw(rowsToRecycle, animate);
if (!params.onlyBody) {
this.refreshFloatingRowComps();
}
this.restoreFocusedCell(focusedCell);
this.releaseLockOnRefresh();
}
private scrollToTopIfNewData(params: RefreshViewParams): void {
let scrollToTop = params.newData || params.newPage;
let suppressScrollToTop = this.gridOptionsWrapper.isSuppressScrollOnNewData();
if (scrollToTop && !suppressScrollToTop) {
this.gridPanel.scrollToTop();
}
}
private sizeContainerToPageHeight(): void {
let containerHeight = this.paginationProxy.getCurrentPageHeight();
// we need at least 1 pixel for the horizontal scroll to work. so if there are now rows,
// we still want the scroll to be present, otherwise there would be no way to access the columns
// on the RHS - and if that was where the filter was that cause no rows to be presented, there
// is no way to remove the filter.
if (containerHeight === 0) {
containerHeight = 1;
}
this.heightScaler.setModelHeight(containerHeight);
let realHeight = this.heightScaler.getUiContainerHeight();
this.rowContainers.body.setHeight(realHeight);
this.rowContainers.fullWidth.setHeight(realHeight);
this.rowContainers.pinnedLeft.setHeight(realHeight);
this.rowContainers.pinnedRight.setHeight(realHeight);
}
private getLockOnRefresh(): void {
if (this.refreshInProgress) {
throw new Error(
"ag-Grid: cannot get grid to draw rows when it is in the middle of drawing rows. " +
"Your code probably called a grid API method while the grid was in the render stage. To overcome " +
"this, put the API call into a timeout, eg instead of api.refreshView(), " +
"call setTimeout(function(){api.refreshView(),0}). To see what part of your code " +
"that caused the refresh check this stacktrace."
);
}
this.refreshInProgress = true;
}
private releaseLockOnRefresh(): void {
this.refreshInProgress = false;
}
// sets the focus to the provided cell, if the cell is provided. this way, the user can call refresh without
// worry about the focus been lost. this is important when the user is using keyboard navigation to do edits
// and the cellEditor is calling 'refresh' to get other cells to update (as other cells might depend on the
// edited cell).
private restoreFocusedCell(gridCell: GridCell): void {
if (gridCell) {
this.focusedCellController.setFocusedCell(gridCell.rowIndex, gridCell.column, gridCell.floating, true);
}
}
public stopEditing(cancel: boolean = false) {
this.forEachRowComp((key: string, rowComp: RowComp) => {
rowComp.stopEditing(cancel);
});
}
public forEachCellComp(callback: (cellComp: CellComp) => void): void {
this.forEachRowComp( (key: string, rowComp: RowComp) => rowComp.forEachCellComp(callback));
}
private forEachRowComp(callback: (key: string, rowComp: RowComp) => void): void {
_.iterateObject(this.rowCompsByIndex, callback);
_.iterateObject(this.floatingTopRowComps, callback);
_.iterateObject(this.floatingBottomRowComps, callback);
}
public addRenderedRowListener(eventName: string, rowIndex: number, callback: Function): void {
let rowComp = this.rowCompsByIndex[rowIndex];
if (rowComp) {
rowComp.addEventListener(eventName, callback);
}
}
public flashCells(params: FlashCellsParams = {}): void {
this.forEachCellCompFiltered(params.rowNodes, params.columns, cellComp => cellComp.flashCell());
}
public refreshCells(params: RefreshCellsParams = {}): void {
let refreshCellParams = {
forceRefresh: params.force,
newData: false
};
this.forEachCellCompFiltered(params.rowNodes, params.columns, cellComp => cellComp.refreshCell(refreshCellParams));
}
public getCellRendererInstances(params: GetCellRendererInstancesParams): ICellRendererComp[] {
let res: ICellRendererComp[] = [];
this.forEachCellCompFiltered(params.rowNodes, params.columns, cellComp => {
let cellRenderer = cellComp.getCellRenderer();
if (cellRenderer) {
res.push(cellRenderer);
}
} );
return res;
}
public getCellEditorInstances(params: GetCellRendererInstancesParams): ICellEditorComp[] {
let res: ICellEditorComp[] = [];
this.forEachCellCompFiltered(params.rowNodes, params.columns, cellComp => {
let cellEditor = cellComp.getCellEditor();
if (cellEditor) {
res.push(cellEditor);
}
} );
return res;
}
public getEditingCells(): GridCellDef[] {
let res: GridCellDef[] = [];
this.forEachCellComp( cellComp => {
if (cellComp.isEditing()) {
let gridCellDef: GridCellDef = cellComp.getGridCell().getGridCellDef();
res.push(gridCellDef);
}
});
return res;
}
// calls the callback for each cellComp that match the provided rowNodes and columns. eg if one row node
// and two columns provided, that identifies 4 cells, so callback gets called 4 times, once for each cell.
private forEachCellCompFiltered(rowNodes: RowNode[], columns: (string | Column)[], callback: (cellComp: CellComp) => void): void {
let rowIdsMap: any;
if (_.exists(rowNodes)) {
rowIdsMap = {
top: {},
bottom: {},
normal: {}
};
rowNodes.forEach(rowNode => {
if (rowNode.rowPinned === Constants.PINNED_TOP) {
rowIdsMap.top[rowNode.id] = true;
} else if (rowNode.rowPinned === Constants.PINNED_BOTTOM) {
rowIdsMap.bottom[rowNode.id] = true;
} else {
rowIdsMap.normal[rowNode.id] = true;
}
});
}
let colIdsMap: any;
if (_.exists(columns)) {
colIdsMap = {};
columns.forEach((colKey: string | Column) => {
let column: Column = this.columnController.getGridColumn(colKey);
if (_.exists(column)) {
colIdsMap[column.getId()] = true;
}
});
}
let processRow = (rowComp: RowComp) => {
let rowNode: RowNode = rowComp.getRowNode();
let id = rowNode.id;
let floating = rowNode.rowPinned;
// skip this row if it is missing from the provided list
if (_.exists(rowIdsMap)) {
if (floating === Constants.PINNED_BOTTOM) {
if (!rowIdsMap.bottom[id]) {
return;
}
} else if (floating === Constants.PINNED_TOP) {
if (!rowIdsMap.top[id]) {
return;
}
} else {
if (!rowIdsMap.normal[id]) {
return;
}
}
}
rowComp.forEachCellComp(cellComp => {
let colId: string = cellComp.getColumn().getId();
let excludeColFromRefresh = colIdsMap && !colIdsMap[colId];
if (excludeColFromRefresh) {
return;
}
callback(cellComp);
});
};
_.iterateObject(this.rowCompsByIndex, (index: string, rowComp: RowComp) => {
processRow(rowComp);
});
if (this.floatingTopRowComps) {
this.floatingTopRowComps.forEach(processRow);
}
if (this.floatingBottomRowComps) {
this.floatingBottomRowComps.forEach(processRow);
}
}
public destroy() {
super.destroy();
let rowIndexesToRemove = Object.keys(this.rowCompsByIndex);
this.removeRowComps(rowIndexesToRemove);
}
private binRowComps(recycleRows: boolean): { [key: string]: RowComp } {
let indexesToRemove: string[];
let rowsToRecycle: { [key: string]: RowComp } = {};
if (recycleRows) {
indexesToRemove = [];
_.iterateObject(this.rowCompsByIndex, (index: string, rowComp: RowComp) => {
let rowNode = rowComp.getRowNode();
if (_.exists(rowNode.id)) {
rowsToRecycle[rowNode.id] = rowComp;
delete this.rowCompsByIndex[index];
} else {
indexesToRemove.push(index);
}
});
} else {
indexesToRemove = Object.keys(this.rowCompsByIndex);
}
this.removeRowComps(indexesToRemove);
return rowsToRecycle;
}
// takes array of row indexes
private removeRowComps(rowsToRemove: any[]) {
// if no fromIndex then set to -1, which will refresh everything
// let realFromIndex = -1;
rowsToRemove.forEach(indexToRemove => {
let renderedRow = this.rowCompsByIndex[indexToRemove];
renderedRow.destroy();
delete this.rowCompsByIndex[indexToRemove];
});
}
// gets called when rows don't change, but viewport does, so after:
// 1) height of grid body changes, ie number of displayed rows has changed
// 2) grid scrolled to new position
// 3) ensure index visible (which is a scroll)
public redrawAfterScroll() {
this.getLockOnRefresh();
this.redraw(null, false, true);
this.releaseLockOnRefresh();
}
private removeRowCompsNotToDraw(indexesToDraw: number[]): void {
// for speedy lookup, dump into map
let indexesToDrawMap: { [index: string]: boolean } = {};
indexesToDraw.forEach(index => (indexesToDrawMap[index] = true));
let existingIndexes = Object.keys(this.rowCompsByIndex);
let indexesNotToDraw: string[] = _.filter(existingIndexes, index => !indexesToDrawMap[index]);
this.removeRowComps(indexesNotToDraw);
}
private calculateIndexesToDraw(): number[] {
// all in all indexes in the viewport
let indexesToDraw = _.createArrayOfNumbers(this.firstRenderedRow, this.lastRenderedRow);
// add in indexes of rows we want to keep, because they are currently editing
_.iterateObject(this.rowCompsByIndex, (indexStr: string, rowComp: RowComp) => {
let index = Number(indexStr);
if (index < this.firstRenderedRow || index > this.lastRenderedRow) {
if (this.keepRowBecauseEditing(rowComp)) {
indexesToDraw.push(index);
}
}
});
indexesToDraw.sort((a: number, b: number) => a - b);
return indexesToDraw;
}
private redraw(rowsToRecycle?: { [key: string]: RowComp }, animate = false, afterScroll = false) {
this.heightScaler.update();
this.workOutFirstAndLastRowsToRender();
// the row can already exist and be in the following:
// rowsToRecycle -> if model change, then the index may be different, however row may
// exist here from previous time (mapped by id).
// this.rowCompsByIndex -> if just a scroll, then this will contain what is currently in the viewport
// this is all the indexes we want, including those that already exist, so this method
// will end up going through each index and drawing only if the row doesn't already exist
let indexesToDraw = this.calculateIndexesToDraw();
this.removeRowCompsNotToDraw(indexesToDraw);
// add in new rows
let nextVmTurnFunctions: Function[] = [];
let rowComps: RowComp[] = [];
indexesToDraw.forEach(rowIndex => {
let rowComp = this.createOrUpdateRowComp(rowIndex, rowsToRecycle, animate, afterScroll);
if (_.exists(rowComp)) {
rowComps.push(rowComp);
_.pushAll(nextVmTurnFunctions, rowComp.getAndClearNextVMTurnFunctions());
}
});
this.flushContainers(rowComps);
_.executeNextVMTurn(nextVmTurnFunctions);
if (afterScroll && !this.gridOptionsWrapper.isSuppressAnimationFrame()) {
this.beans.taskQueue.addP2Task(this.destroyRowComps.bind(this, rowsToRecycle, animate));
} else {
this.destroyRowComps(rowsToRecycle, animate);
}
this.checkAngularCompile();
}
private flushContainers(rowComps: RowComp[]): void {
_.iterateObject(this.rowContainers, (key: string, rowContainerComp: RowContainerComponent) => {
if (rowContainerComp) {
rowContainerComp.flushRowTemplates();
}
});
rowComps.forEach(rowComp => rowComp.afterFlush());
}
private onDisplayedColumnsChanged(): void {
let pinningLeft = this.columnController.isPinningLeft();
let pinningRight = this.columnController.isPinningRight();
let atLeastOneChanged = this.pinningLeft !== pinningLeft || pinningRight !== this.pinningRight;
if (atLeastOneChanged) {
this.pinningLeft = pinningLeft;
this.pinningRight = pinningRight;
if (this.gridOptionsWrapper.isEmbedFullWidthRows()) {
this.redrawFullWidthEmbeddedRows();
}
}
}
// when embedding, what gets showed in each section depends on what is pinned. eg if embedding group expand / collapse,
// then it should go into the pinned left area if pinning left, or the center area if not pinning.
private redrawFullWidthEmbeddedRows(): void {
// if either of the pinned panels has shown / hidden, then need to redraw the fullWidth bits when
// embedded, as what appears in each section depends on whether we are pinned or not
let rowsToRemove: string[] = [];
_.iterateObject(this.rowCompsByIndex, (id: string, rowComp: RowComp) => {
if (rowComp.isFullWidth()) {
let rowIndex = rowComp.getRowNode().rowIndex;
rowsToRemove.push(rowIndex.toString());
}
});
this.refreshFloatingRowComps();
this.removeRowComps(rowsToRemove);
this.redrawAfterScroll();
}
private createOrUpdateRowComp(rowIndex: number, rowsToRecycle: { [key: string]: RowComp }, animate: boolean, afterScroll: boolean): RowComp {
let rowNode: RowNode;
let rowComp: RowComp = this.rowCompsByIndex[rowIndex];
// if no row comp, see if we can get it from the previous rowComps
if (!rowComp) {
rowNode = this.paginationProxy.getRow(rowIndex);
if (_.exists(rowNode) && _.exists(rowsToRecycle) && rowsToRecycle[rowNode.id]) {
rowComp = rowsToRecycle[rowNode.id];
rowsToRecycle[rowNode.id] = null;
}
}
let creatingNewRowComp = !rowComp;
if (creatingNewRowComp) {
// create a new one
if (!rowNode) {
rowNode = this.paginationProxy.getRow(rowIndex);
}
if (_.exists(rowNode)) {
rowComp = this.createRowComp(rowNode, animate, afterScroll);
} else {
// this should never happen - if somehow we are trying to create
// a row for a rowNode that does not exist.
return;
}
} else {
// ensure row comp is in right position in DOM
rowComp.ensureDomOrder();
}
this.rowCompsByIndex[rowIndex] = rowComp;
return rowComp;
}
private destroyRowComps(rowCompsMap: { [key: string]: RowComp }, animate: boolean): void {
let delayedFuncs: Function[] = [];
_.iterateObject(rowCompsMap, (nodeId: string, rowComp: RowComp) => {
// if row was used, then it's null
if (!rowComp) {
return;
}
rowComp.destroy(animate);
_.pushAll(delayedFuncs, rowComp.getAndClearDelayedDestroyFunctions());
});
_.executeInAWhile(delayedFuncs);
}
private checkAngularCompile(): void {
// 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(() => {
this.$scope.$apply();
}, 0);
}
}
private workOutFirstAndLastRowsToRender(): void {
let newFirst: number;
let newLast: number;
if (!this.paginationProxy.isRowsToRender()) {
newFirst = 0;
newLast = -1; // setting to -1 means nothing in range
} else {
let pageFirstRow = this.paginationProxy.getPageFirstRow();
let pageLastRow = this.paginationProxy.getPageLastRow();
let pixelOffset = this.paginationProxy ? this.paginationProxy.getPixelOffset() : 0;
let heightOffset = this.heightScaler.getOffset();
let bodyVRange = this.gridPanel.getVScrollPosition();
let topPixel = bodyVRange.top;
let bottomPixel = bodyVRange.bottom;
let realPixelTop = topPixel + pixelOffset + heightOffset;
let realPixelBottom = bottomPixel + pixelOffset + heightOffset;
let first = this.paginationProxy.getRowIndexAtPixel(realPixelTop);
let last = this.paginationProxy.getRowIndexAtPixel(realPixelBottom);
//add in buffer
let buffer = this.gridOptionsWrapper.getRowBuffer();
first = first - buffer;
last = last + buffer;
// adjust, in case buffer extended actual size
if (first < pageFirstRow) {
first = pageFirstRow;
}
if (last > pageLastRow) {
last = pageLastRow;
}
newFirst = first;
newLast = last;
}
let firstDiffers = newFirst !== this.firstRenderedRow;
let lastDiffers = newLast !== this.lastRenderedRow;
if (firstDiffers || lastDiffers) {
this.firstRenderedRow = newFirst;
this.lastRenderedRow = newLast;
let event: ViewportChangedEvent = {
type: Events.EVENT_VIEWPORT_CHANGED,
firstRow: newFirst,
lastRow: newLast,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
}
}
public getFirstVirtualRenderedRow() {
return this.firstRenderedRow;
}
public getLastVirtualRenderedRow() {
return this.lastRenderedRow;
}
// check that none of the rows to remove are editing or focused as:
// a) if editing, we want to keep them, otherwise the user will loose the context of the edit,
// eg user starts editing, enters some text, then scrolls down and then up, next time row rendered
// the edit is reset - so we want to keep it rendered.
// b) if focused, we want ot keep keyboard focus, so if user ctrl+c, it goes to clipboard,
// otherwise the user can range select and drag (with focus cell going out of the viewport)
// and then ctrl+c, nothing will happen if cell is removed from dom.
private keepRowBecauseEditing(rowComp: RowComp): boolean {
let REMOVE_ROW: boolean = false;
let KEEP_ROW: boolean = true;
let rowNode = rowComp.getRowNode();
let rowHasFocus = this.focusedCellController.isRowNodeFocused(rowNode);
let rowIsEditing = rowComp.isEditing();
let mightWantToKeepRow = rowHasFocus || rowIsEditing;
// if we deffo don't want to keep it,
if (!mightWantToKeepRow) {
return REMOVE_ROW;
}
// editing row, only remove if it is no longer rendered, eg filtered out or new data set.
// the reason we want to keep is if user is scrolling up and down, we don't want to loose
// the context of the editing in process.
let rowNodePresent = this.paginationProxy.isRowPresent(rowNode);
return rowNodePresent ? KEEP_ROW : REMOVE_ROW;
}
private createRowComp(rowNode: RowNode, animate: boolean, afterScroll: boolean): RowComp {
let useAnimationFrameForCreate = afterScroll && !this.gridOptionsWrapper.isSuppressAnimationFrame();
let rowComp = new RowComp(
this.$scope,
this.rowContainers.body,
this.rowContainers.pinnedLeft,
this.rowContainers.pinnedRight,
this.rowContainers.fullWidth,
rowNode,
this.beans,
animate,
useAnimationFrameForCreate
);
rowComp.init();
return rowComp;
}
public getRenderedNodes() {
let renderedRows = this.rowCompsByIndex;
return Object.keys(renderedRows).map(key => {
return renderedRows[key].getRowNode();
});
}
// 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(event: KeyboardEvent, key: number, previousCell: GridCell, allowUserOverride: boolean) {
let nextCell = previousCell;
// we keep searching for a next cell until we find one. this is how the group rows get skipped
while (true) {
nextCell = this.cellNavigationService.getNextCellToFocus(key, nextCell);
if (_.missing(nextCell)) {
break;
}
let skipGroupRows = this.gridOptionsWrapper.isGroupUseEntireRow();
if (skipGroupRows) {
let rowNode = this.paginationProxy.getRow(nextCell.rowIndex);
if (!rowNode.group) {
break;
}
} else {
break;
}
}
// allow user to override what cell to go to next. when doing normal cell navigation (with keys)
// we allow this, however if processing 'enter after edit' we don't allow override
if (allowUserOverride) {
let userFunc = this.gridOptionsWrapper.getNavigateToNextCellFunc();
if (_.exists(userFunc)) {
let params = <NavigateToNextCellParams>{
key: key,
previousCellDef: previousCell,
nextCellDef: nextCell ? nextCell.getGridCellDef() : null,
event: event
};
let nextCellDef = userFunc(params);
if (_.exists(nextCellDef)) {
nextCell = new GridCell(nextCellDef);
} else {
nextCell = null;
}
}
}
// no next cell means we have reached a grid boundary, eg left, right, top or bottom of grid
if (!nextCell) {
return;
}
this.ensureCellVisible(nextCell);
this.focusedCellController.setFocusedCell(nextCell.rowIndex, nextCell.column, nextCell.floating, true);
if (this.rangeController) {
let gridCell = new GridCell({ rowIndex: nextCell.rowIndex, floating: nextCell.floating, column: nextCell.column });
this.rangeController.setRangeToCell(gridCell);
}
}
public ensureCellVisible(gridCell: GridCell): void {
// this scrolls the row into view
if (_.missing(gridCell.floating)) {
this.gridPanel.ensureIndexVisible(gridCell.rowIndex);
}
if (!gridCell.column.isPinned()) {
this.gridPanel.ensureColumnVisible(gridCell.column);
}
// need to nudge the scrolls for the floating items. otherwise when we set focus on a non-visible
// floating cell, the scrolls get out of sync
this.gridPanel.horizontallyScrollHeaderCenterAndFloatingCenter();
// need to flush frames, to make sure the correct cells are rendered
this.animationFrameService.flushAllFrames();
}
public startEditingCell(gridCell: GridCell, keyPress: number, charPress: string): void {
let cell = this.getComponentForCell(gridCell);
if (cell) {
cell.startRowOrCellEdit(keyPress, charPress);
}
}
private getComponentForCell(gridCell: GridCell): CellComp {
let rowComponent: RowComp;
switch (gridCell.floating) {
case Constants.PINNED_TOP:
rowComponent = this.floatingTopRowComps[gridCell.rowIndex];
break;
case Constants.PINNED_BOTTOM:
rowComponent = this.floatingBottomRowComps[gridCell.rowIndex];
break;
default:
rowComponent = this.rowCompsByIndex[gridCell.rowIndex];
break;
}
if (!rowComponent) {
return null;
}
let cellComponent: CellComp = rowComponent.getRenderedCellForColumn(gridCell.column);
return cellComponent;
}
public onTabKeyDown(previousRenderedCell: CellComp, keyboardEvent: KeyboardEvent): void {
let backwards = keyboardEvent.shiftKey;
let success = this.moveToCellAfter(previousRenderedCell, backwards);
if (success) {
keyboardEvent.preventDefault();
}
}
public tabToNextCell(backwards: boolean): boolean {
let focusedCell = this.focusedCellController.getFocusedCell();
// if no focus, then cannot navigate
if (_.missing(focusedCell)) {
return false;
}
let renderedCell = this.getComponentForCell(focusedCell);
// if cell is not rendered, means user has scrolled away from the cell
if (_.missing(renderedCell)) {
return false;
}
let result = this.moveToCellAfter(renderedCell, backwards);
return result;
}
private moveToCellAfter(previousRenderedCell: CellComp, backwards: boolean): boolean {
let editing = previousRenderedCell.isEditing();
let res: boolean;
if (editing) {
if (this.gridOptionsWrapper.isFullRowEdit()) {
res = this.moveToNextEditingRow(previousRenderedCell, backwards);
} else {
res = this.moveToNextEditingCell(previousRenderedCell, backwards);
}
} else {
res = this.moveToNextCellNotEditing(previousRenderedCell, backwards);
}
return res;
}
private moveToNextEditingCell(previousRenderedCell: CellComp, backwards: boolean): boolean {
let gridCell = previousRenderedCell.getGridCell();
// need to do this before getting next cell to edit, in case the next cell
// has editable function (eg colDef.editable=func() ) and it depends on the
// result of this cell, so need to save updates from the first edit, in case
// the value is referenced in the function.
previousRenderedCell.stopEditing();
// find the next cell to start editing
let nextRenderedCell = this.findNextCellToFocusOn(gridCell, backwards, true);
let foundCell = _.exists(nextRenderedCell);
// only prevent default if we found a cell. so if user is on last cell and hits tab, then we default
// to the normal tabbing so user can exit the grid.
if (foundCell) {
nextRenderedCell.startEditingIfEnabled(null, null, true);
nextRenderedCell.focusCell(false);
}
return foundCell;
}
private moveToNextEditingRow(previousRenderedCell: CellComp, backwards: boolean): boolean {
let gridCell = previousRenderedCell.getGridCell();
// find the next cell to start editing
let nextRenderedCell = this.findNextCellToFocusOn(gridCell, backwards, true);
let foundCell = _.exists(nextRenderedCell);
// only prevent default if we found a cell. so if user is on last cell and hits tab, then we default
// to the normal tabbing so user can exit the grid.
if (foundCell) {
this.moveEditToNextCellOrRow(previousRenderedCell, nextRenderedCell);
}
return foundCell;
}
private moveToNextCellNotEditing(previousRenderedCell: CellComp, backwards: boolean): boolean {
let gridCell = previousRenderedCell.getGridCell();
// find the next cell to start editing
let nextRenderedCell = this.findNextCellToFocusOn(gridCell, backwards, false);
let foundCell = _.exists(nextRenderedCell);
// only prevent default if we found a cell. so if user is on last cell and hits tab, then we default
// to the normal tabbing so user can exit the grid.
if (foundCell) {
nextRenderedCell.focusCell(true);
}
return foundCell;
}
private moveEditToNextCellOrRow(previousRenderedCell: CellComp, nextRenderedCell: CellComp): void {
let pGridCell = previousRenderedCell.getGridCell();
let nGridCell = nextRenderedCell.getGridCell();
let rowsMatch = pGridCell.rowIndex === nGridCell.rowIndex && pGridCell.floating === nGridCell.floating;
if (rowsMatch) {
// same row, so we don't start / stop editing, we just move the focus along
previousRenderedCell.setFocusOutOnEditor();
nextRenderedCell.setFocusInOnEditor();
} else {
let pRow = previousRenderedCell.getRenderedRow();
let nRow = nextRenderedCell.getRenderedRow();
previousRenderedCell.setFocusOutOnEditor();
pRow.stopEditing();
nRow.startRowEditing();
nextRenderedCell.setFocusInOnEditor();
}
nextRenderedCell.focusCell();
}
// called by the cell, when tab is pressed while editing.
// @return: RenderedCell when navigation successful, otherwise null
private findNextCellToFocusOn(gridCell: GridCell, backwards: boolean, startEditing: boolean): CellComp {
let nextCell: GridCell = gridCell;
while (true) {
nextCell = this.cellNavigationService.getNextTabbedCell(nextCell, backwards);
// allow user to override what cell to go to next
let userFunc = this.gridOptionsWrapper.getTabToNextCellFunc();
if (_.exists(userFunc)) {
let params = <TabToNextCellParams>{
backwards: backwards,
editing: startEditing,
previousCellDef: gridCell.getGridCellDef(),
nextCellDef: nextCell ? nextCell.getGridCellDef() : null
};
let nextCellDef = userFunc(params);
if (_.exists(nextCellDef)) {
nextCell = new GridCell(nextCellDef);
} else {
nextCell = null;
}
}
// if no 'next cell', means we have got to last cell of grid, so nothing to move to,
// so bottom right cell going forwards, or top left going backwards
if (!nextCell) {
return null;
}
// if editing, but cell not editable, skip cell. we do this before we do all of
// the 'ensure index visible' and 'flush all frames', otherwise if we are skipping
// a bunch of cells (eg 10 rows) then all the work on ensuring cell visible is useless
// (except for the last one) which causes grid to stall for a while.
if (startEditing) {
let rowNode = this.paginationProxy.getRow(nextCell.rowIndex);
let cellIsEditable = nextCell.column.isCellEditable(rowNode);
if (!cellIsEditable) { continue; }
}
// this scrolls the row into view
let cellIsNotFloating = _.missing(nextCell.floating);
if (cellIsNotFloating) {
this.gridPanel.ensureIndexVisible(nextCell.rowIndex);
}
// pinned columns don't scroll, so no need to ensure index visible
if (!nextCell.column.isPinned()) {
this.gridPanel.ensureColumnVisible(nextCell.column);
}
// need to nudge the scrolls for the floating items. otherwise when we set focus on a non-visible
// floating cell, the scrolls get out of sync
this.gridPanel.horizontallyScrollHeaderCenterAndFloatingCenter();
// get the grid panel to flush all animation frames - otherwise the call below to get the cellComp
// could fail, if we just scrolled the grid (to make a cell visible) and the rendering hasn't finished.
this.animationFrameService.flushAllFrames();
// we have to call this after ensureColumnVisible - otherwise it could be a virtual column
// or row that is not currently in view, hence the renderedCell would not exist
let nextCellComp = this.getComponentForCell(nextCell);
// if next cell is fullWidth row, then no rendered cell,
// as fullWidth rows have no cells, so we skip it
if (_.missing(nextCellComp)) {
continue;
}
if (nextCellComp.isSuppressNavigable()) {
continue;
}
// by default, when we click a cell, it gets selected into a range, so to keep keyboard navigation
// consistent, we set into range here also.
if (this.rangeController) {
let gridCell = new GridCell({ rowIndex: nextCell.rowIndex, floating: nextCell.floating, column: nextCell.column });
this.rangeController.setRangeToCell(gridCell);
}
// we successfully tabbed onto a grid cell, so return true
return nextCellComp;
}
}
}
export interface RefreshViewParams {
recycleRows?: boolean;
animate?: boolean;
suppressKeepFocus?: boolean;
onlyBody?: boolean;
// when new data, grid scrolls back to top
newData?: boolean;
newPage?: boolean;
}