UNPKG

ag-grid

Version:

Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components

1,176 lines (972 loc) 46.7 kB
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"; @Bean("rowRenderer") export class RowRenderer extends BeanStub { @Autowired("paginationProxy") private paginationProxy: PaginationProxy; @Autowired("columnController") private columnController: ColumnController; @Autowired("gridOptionsWrapper") private gridOptionsWrapper: GridOptionsWrapper; @Autowired("gridCore") private gridCore: GridCore; @Autowired("$scope") private $scope: any; @Autowired("expressionService") private expressionService: ExpressionService; @Autowired("templateService") private templateService: TemplateService; @Autowired("valueService") private valueService: ValueService; @Autowired("eventService") private eventService: EventService; @Autowired("pinnedRowModel") private pinnedRowModel: PinnedRowModel; @Autowired("context") private context: Context; @Autowired("loggerFactory") private loggerFactory: LoggerFactory; @Autowired("focusedCellController") private focusedCellController: FocusedCellController; @Autowired("cellNavigationService") private cellNavigationService: CellNavigationService; @Autowired("columnApi") private columnApi: ColumnApi; @Autowired("gridApi") private gridApi: GridApi; @Autowired("beans") private beans: Beans; @Autowired("heightScaler") private heightScaler: HeightScaler; @Autowired("animationFrameService") private animationFrameService: AnimationFrameService; @Optional("rangeController") 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(@Qualifier("loggerFactory") 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); } } @PreDestroy 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; }