import {Utils as _} from "../utils"; import {GridOptionsWrapper} from "../gridOptionsWrapper"; import {ColumnApi, ColumnController} from "../columnController/columnController"; import {RowRenderer, RefreshViewParams} from "../rendering/rowRenderer"; import {BorderLayout} from "../layout/borderLayout"; import {Logger, LoggerFactory} from "../logger"; import {Bean, Qualifier, Autowired, PostConstruct, Optional, PreDestroy, Context} from "../context/context"; import {EventService} from "../eventService"; import {BodyHeightChangedEvent, BodyScrollEvent, Events} from "../events"; import {DragService, DragListenerParams} from "../dragAndDrop/dragService"; import {IRangeController} from "../interfaces/iRangeController"; import {Constants, KeyboardBinding, KeyboardBindingGroup} from "../constants"; import {SelectionController} from "../selectionController"; import {CsvCreator} from "../csvCreator"; import {MouseEventService} from "./mouseEventService"; import {IClipboardService} from "../interfaces/iClipboardService"; import {FocusedCellController} from "../focusedCellController"; import {IContextMenuFactory} from "../interfaces/iContextMenuFactory"; import {SetScrollsVisibleParams, ScrollVisibleService} from "./scrollVisibleService"; import {BeanStub} from "../context/beanStub"; import {IFrameworkFactory} from "../interfaces/iFrameworkFactory"; import {Column} from "../entities/column"; import {RowContainerComponent} from "../rendering/rowContainerComponent"; import {GridCell} from "../entities/gridCell"; import {RowNode} from "../entities/rowNode"; import {PaginationProxy} from "../rowModels/paginationProxy"; import {PopupEditorWrapper} from "../rendering/cellEditors/popupEditorWrapper"; import {AlignedGridsService} from "../alignedGridsService"; import {PinnedRowModel} from "../rowModels/pinnedRowModel"; import {GridApi} from "../gridApi"; import {AnimationFrameService} from "../misc/animationFrameService"; import {RowComp} from "../rendering/rowComp"; // in the html below, it is important that there are no white space between some of the divs, as if there is white space, // it won't render correctly in safari, as safari renders white space as a gap const HEADER_SNIPPET = '
'+ '' + '' + ''+ '' + '
'; const FLOATING_TOP_SNIPPET = ''; const FLOATING_BOTTOM_SNIPPET = ''; const BODY_SNIPPET = ''; // the difference between the 'normal' and 'full height' template is the order of the floating and body, // for normal, the floating top and bottom go in first as they are fixed position, // for auto-height, the body is in the middle of the top and bottom as they are just normally laid out const GRID_PANEL_NORMAL_TEMPLATE = '
'+ HEADER_SNIPPET + FLOATING_TOP_SNIPPET + FLOATING_BOTTOM_SNIPPET + BODY_SNIPPET + '
'; const GRID_PANEL_AUTO_HEIGHT_TEMPLATE = '
'+ HEADER_SNIPPET + FLOATING_TOP_SNIPPET + BODY_SNIPPET + FLOATING_BOTTOM_SNIPPET + '
'; // the template for for-print is much easier than that others, as it doesn't have any pinned areas // or scrollable areas (so no viewports). const GRID_PANEL_FOR_PRINT_TEMPLATE = '
'+ // header '
'+ // floating '
'+ // body '
'+ // floating bottom '
'+ '
'; // wrapping in outer div, and wrapper, is needed to center the loading icon // The idea for centering came from here: http://www.vanseodesign.com/css/vertical-centering/ const OVERLAY_TEMPLATE = ''; const LOADING_OVERLAY_TEMPLATE = '[LOADING...]'; const NO_ROWS_TO_SHOW_OVERLAY_TEMPLATE = '[NO_ROWS_TO_SHOW]'; export interface RowContainerComponents { fullWidth: RowContainerComponent; body: RowContainerComponent; pinnedLeft: RowContainerComponent; pinnedRight: RowContainerComponent; floatingTop: RowContainerComponent; floatingTopPinnedLeft: RowContainerComponent; floatingTopPinnedRight: RowContainerComponent; floatingTopFullWidth: RowContainerComponent; floatingBottom: RowContainerComponent; floatingBottomPinnedLeft: RowContainerComponent; floatingBottomPinnedRight: RowContainerComponent; floatingBottomFullWith: RowContainerComponent; } @Bean('gridPanel') export class GridPanel extends BeanStub { @Autowired('alignedGridsService') private alignedGridsService: AlignedGridsService; @Autowired('gridOptionsWrapper') private gridOptionsWrapper: GridOptionsWrapper; @Autowired('columnController') private columnController: ColumnController; @Autowired('rowRenderer') private rowRenderer: RowRenderer; @Autowired('pinnedRowModel') private pinnedRowModel: PinnedRowModel; @Autowired('eventService') private eventService: EventService; @Autowired('context') private context: Context; @Autowired('animationFrameService') private animationFrameService: AnimationFrameService; @Autowired('paginationProxy') private paginationProxy: PaginationProxy; @Autowired('columnApi') private columnApi: ColumnApi; @Autowired('gridApi') private gridApi: GridApi; @Optional('rangeController') private rangeController: IRangeController; @Autowired('dragService') private dragService: DragService; @Autowired('selectionController') private selectionController: SelectionController; @Optional('clipboardService') private clipboardService: IClipboardService; @Autowired('csvCreator') private csvCreator: CsvCreator; @Autowired('mouseEventService') private mouseEventService: MouseEventService; @Autowired('focusedCellController') private focusedCellController: FocusedCellController; @Autowired('$scope') private $scope: any; @Autowired('scrollVisibleService') private scrollVisibleService: ScrollVisibleService; @Optional('contextMenuFactory') private contextMenuFactory: IContextMenuFactory; @Autowired('frameworkFactory') private frameworkFactory: IFrameworkFactory; private layout: BorderLayout; private logger: Logger; private eBodyViewport: HTMLElement; private eRoot: HTMLElement; private eBody: HTMLElement; private rowContainerComponents: RowContainerComponents; private eBodyContainer: HTMLElement; private ePinnedLeftColsContainer: HTMLElement; private ePinnedRightColsContainer: HTMLElement; private eFullWidthCellViewport: HTMLElement; private eFullWidthCellContainer: HTMLElement; private ePinnedLeftColsViewport: HTMLElement; private ePinnedRightColsViewport: HTMLElement; private eBodyViewportWrapper: HTMLElement; private eHeaderContainer: HTMLElement; private eHeaderOverlay: HTMLElement; private ePinnedLeftHeader: HTMLElement; private ePinnedRightHeader: HTMLElement; private eHeader: HTMLElement; private eHeaderViewport: HTMLElement; private eFloatingTop: HTMLElement; private ePinnedLeftFloatingTop: HTMLElement; private ePinnedRightFloatingTop: HTMLElement; private eFloatingTopContainer: HTMLElement; private eFloatingTopViewport: HTMLElement; private eFloatingTopFullWidthCellContainer: HTMLElement; private eFloatingBottom: HTMLElement; private ePinnedLeftFloatingBottom: HTMLElement; private ePinnedRightFloatingBottom: HTMLElement; private eFloatingBottomContainer: HTMLElement; private eFloatingBottomViewport: HTMLElement; private eFloatingBottomFullWidthCellContainer: HTMLElement; private eAllCellContainers: HTMLElement[]; private scrollLeft = -1; private nextScrollLeft = -1; private scrollTop = -1; private nextScrollTop = -1; private verticalRedrawNeeded = false; private bodyHeight: number; // properties we use a lot, so keep reference private enableRtl: boolean; private forPrint: boolean; private autoHeight: boolean; private scrollWidth: number; // used to track if pinned panels are showing, so we can turn them off if not private pinningRight: boolean; private pinningLeft: boolean; private useAnimationFrame: boolean; public agWire(@Qualifier('loggerFactory') loggerFactory: LoggerFactory) { this.logger = loggerFactory.create('GridPanel'); // makes code below more readable if we pull 'forPrint' out this.forPrint = this.gridOptionsWrapper.isForPrint(); this.autoHeight = this.gridOptionsWrapper.isAutoHeight(); this.scrollWidth = this.gridOptionsWrapper.getScrollbarWidth(); this.enableRtl = this.gridOptionsWrapper.isEnableRtl(); this.loadTemplate(); this.findElements(); } public getVerticalPixelRange(): any { let container: HTMLElement = this.getPrimaryScrollViewport(); let result = { top: container.scrollTop, bottom: container.scrollTop + container.offsetHeight }; return result; } // we override this, as the base class is missing the annotation @PreDestroy public destroy() { super.destroy(); } private onRowDataChanged(): void { this.showOrHideOverlay(); } private showOrHideOverlay(): void { if (this.paginationProxy.isEmpty() && !this.gridOptionsWrapper.isSuppressNoRowsOverlay()) { this.showNoRowsOverlay(); } else { this.hideOverlay(); } } public getLayout(): BorderLayout { return this.layout; } @PostConstruct private init() { this.useAnimationFrame = !this.gridOptionsWrapper.isSuppressAnimationFrame(); this.addEventListeners(); this.addDragListeners(); this.layout = new BorderLayout({ overlays: { loading: _.loadTemplate(this.createLoadingOverlayTemplate()), noRows: _.loadTemplate(this.createNoRowsOverlayTemplate()) }, center: this.eRoot, dontFill: this.forPrint, fillHorizontalOnly: this.autoHeight, name: 'eGridPanel' }); this.layout.addSizeChangeListener(this.setBodyAndHeaderHeights.bind(this)); this.layout.addSizeChangeListener(this.setLeftAndRightBounds.bind(this)); this.addScrollListener(); if (this.gridOptionsWrapper.isSuppressHorizontalScroll()) { this.eBodyViewport.style.overflowX = 'hidden'; } if (this.gridOptionsWrapper.isRowModelDefault() && !this.gridOptionsWrapper.getRowData()) { this.showLoadingOverlay(); } this.setPinnedContainersVisible(); this.setBodyAndHeaderHeights(); this.disableBrowserDragging(); this.addShortcutKeyListeners(); this.addMouseListeners(); this.addKeyboardEvents(); this.addBodyViewportListener(); this.addStopEditingWhenGridLosesFocus(); if (this.$scope) { this.addAngularApplyCheck(); } this.onDisplayedColumnsWidthChanged(); } private addStopEditingWhenGridLosesFocus(): void { if (this.gridOptionsWrapper.isStopEditingWhenGridLosesFocus()) { this.addDestroyableEventListener(this.eBody, 'focusout', (event: FocusEvent)=> { // this is the element the focus is moving to let elementWithFocus = event.relatedTarget; // see if the element the focus is going to is part of the grid let clickInsideGrid = false; let pointer: any = elementWithFocus; while (_.exists(pointer) && !clickInsideGrid) { let isPopup = !!this.gridOptionsWrapper.getDomData(pointer, PopupEditorWrapper.DOM_KEY_POPUP_EDITOR_WRAPPER); let isBody = this.eBody == pointer; clickInsideGrid = isPopup || isBody; pointer = pointer.parentNode; } if (!clickInsideGrid) { this.rowRenderer.stopEditing(); } }) } } private addAngularApplyCheck(): void { // this makes sure if we queue up requests, we only execute oe let applyTriggered = false; let listener = ()=> { // only need to do one apply at a time if (applyTriggered) { return; } applyTriggered = true; // mark 'need apply' to true setTimeout( ()=> { applyTriggered = false; this.$scope.$apply(); }, 0); }; // these are the events we need to do an apply after - these are the ones that can end up // with columns added or removed this.addDestroyableEventListener(this.eventService, Events.EVENT_DISPLAYED_COLUMNS_CHANGED, listener); this.addDestroyableEventListener(this.eventService, Events.EVENT_VIRTUAL_COLUMNS_CHANGED, listener); } // if we do not do this, then the user can select a pic in the grid (eg an image in a custom cell renderer) // and then that will start the browser native drag n' drop, which messes up with our own drag and drop. private disableBrowserDragging(): void { this.eRoot.addEventListener('dragstart', (event: MouseEvent)=> { if (event.target instanceof HTMLImageElement) { event.preventDefault(); return false; } }); } private addEventListeners(): void { this.addDestroyableEventListener(this.eventService, Events.EVENT_DISPLAYED_COLUMNS_CHANGED, this.onDisplayedColumnsChanged.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_DISPLAYED_COLUMNS_WIDTH_CHANGED, this.onDisplayedColumnsWidthChanged.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_SCROLL_VISIBILITY_CHANGED, this.onScrollVisibilityChanged.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_PINNED_ROW_DATA_CHANGED, this.setBodyAndHeaderHeights.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_ROW_DATA_CHANGED, this.onRowDataChanged.bind(this)); this.addDestroyableEventListener(this.eventService, Events.EVENT_ROW_DATA_UPDATED, this.onRowDataChanged.bind(this)); this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_HEADER_HEIGHT, this.setBodyAndHeaderHeights.bind(this)); this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_PIVOT_HEADER_HEIGHT, this.setBodyAndHeaderHeights.bind(this)); this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_GROUP_HEADER_HEIGHT, this.setBodyAndHeaderHeights.bind(this)); this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_PIVOT_GROUP_HEADER_HEIGHT, this.setBodyAndHeaderHeights.bind(this)); this.addDestroyableEventListener(this.gridOptionsWrapper, GridOptionsWrapper.PROP_FLOATING_FILTERS_HEIGHT, this.setBodyAndHeaderHeights.bind(this)); } private addDragListeners(): void { if (this.forPrint // no range select when doing 'for print' || !this.gridOptionsWrapper.isEnableRangeSelection() // no range selection if no property || _.missing(this.rangeController)) { // no range selection if not enterprise version return; } let containers = [this.ePinnedLeftColsContainer, this.ePinnedRightColsContainer, this.eBodyContainer, this.eFloatingTop, this.eFloatingBottom]; containers.forEach(container => { let params: DragListenerParams = { dragStartPixels: 0, eElement: container, onDragStart: this.rangeController.onDragStart.bind(this.rangeController), onDragStop: this.rangeController.onDragStop.bind(this.rangeController), onDragging: this.rangeController.onDragging.bind(this.rangeController) }; this.dragService.addDragSource(params); this.addDestroyFunc( ()=> this.dragService.removeDragSource(params) ); }); } private addMouseListeners(): void { let eventNames = ['click','mousedown','dblclick','contextmenu','mouseover','mouseout']; eventNames.forEach( eventName => { let listener = this.processMouseEvent.bind(this, eventName); this.eAllCellContainers.forEach( container => this.addDestroyableEventListener(container, eventName, listener) ); }); } private addKeyboardEvents(): void { let eventNames = ['keydown','keypress']; eventNames.forEach( eventName => { let listener = this.processKeyboardEvent.bind(this, eventName); this.eAllCellContainers.forEach( container => { this.addDestroyableEventListener(container, eventName, listener); }); }); } private addBodyViewportListener(): void { // we never add this when doing 'forPrint' if (this.gridOptionsWrapper.isForPrint()) { return; } // we want to listen for clicks directly on the eBodyViewport, so the user has a way of showing // the context menu if no rows are displayed, or user simply clicks outside of a cell let listener = (mouseEvent: MouseEvent) => { let target = _.getTarget(mouseEvent); if (target===this.eBodyViewport || target===this.ePinnedLeftColsViewport || target===this.ePinnedRightColsViewport) { // show it this.onContextMenu(mouseEvent); this.preventDefaultOnContextMenu(mouseEvent); } }; //For some reason listening only to this.eBody doesnt work... Maybe because the event is consumed somewhere else? //In any case, not expending much time on this, if anyome comes accross this and knows how to make this work with //one listener please go ahead and change it... this.addDestroyableEventListener(this.eBodyViewport, 'contextmenu', listener); this.addDestroyableEventListener(this.ePinnedRightColsViewport, 'contextmenu', listener); this.addDestroyableEventListener(this.ePinnedLeftColsViewport, 'contextmenu', listener); } private getRowForEvent(event: MouseEvent | KeyboardEvent): RowComp { let sourceElement = _.getTarget(event); while (sourceElement) { let renderedRow = this.gridOptionsWrapper.getDomData(sourceElement, RowComp.DOM_DATA_KEY_RENDERED_ROW); if (renderedRow) { return renderedRow; } sourceElement = sourceElement.parentElement; } return null; } private processKeyboardEvent(eventName: string, keyboardEvent: KeyboardEvent): void { let renderedCell = this.mouseEventService.getRenderedCellForEvent(keyboardEvent); if (!renderedCell) { return; } switch (eventName) { case 'keydown': let pageScrollingKeys: KeyboardBindingGroup[] = [ Constants.DIAGONAL_SCROLL_KEYS, Constants.HORIZONTAL_SCROLL_KEYS, Constants.VERTICAL_SCROLL_KEYS ]; let result: TestKeyboardBindingGroupsResult = testKeyboardBindingGroups(pageScrollingKeys, keyboardEvent); if (result){ this.handlePageScrollingKey(result.trappedKeyboardBindingGroup.id, result.trappedKeyboardBinding.id, keyboardEvent); } else { renderedCell.onKeyDown(keyboardEvent); } break; case 'keypress': renderedCell.onKeyPress(keyboardEvent); break; } } private handlePageScrollingKey(pagingKeyGroup:string, pagingKey:string, keyboardEvent:KeyboardEvent): void{ switch (pagingKeyGroup){ case Constants.DIAGONAL_SCROLL_KEYS_ID: this.pageDiagonally(pagingKey); break; case Constants.VERTICAL_SCROLL_KEYS_ID: this.pageVertically(pagingKey); break; case Constants.HORIZONTAL_SCROLL_KEYS_ID: this.pageHorizontally(pagingKey); break; } //*************************************************************************** //Stop event defaults and propagation keyboardEvent.preventDefault(); } //Either CTL LEFT/RIGHT private pageHorizontally(pagingKey:string): void{ //*************************************************************************** //column to select let allColumns: Column[] = this.columnController.getAllDisplayedColumns(); let columnToSelect : Column = pagingKey === Constants.KEY_CTRL_LEFT_NAME ? allColumns[0]: allColumns[allColumns.length - 1]; let horizontalScroll: HorizontalScroll = { type: ScrollType.HORIZONTAL, columnToScrollTo: columnToSelect, columnToFocus: columnToSelect }; this.performScroll(horizontalScroll); } // Either HOME OR END private pageDiagonally_new(pagingKey:string): void { let homeKey = pagingKey === Constants.KEY_PAGE_HOME_NAME; let allColumns: Column[] = this.columnController.getAllDisplayedColumns(); let columnToSelect = homeKey ? allColumns[0] : allColumns[allColumns.length - 1]; let rowIndexToScrollTo = homeKey ? 0 : this.paginationProxy.getPageLastRow(); this.ensureColumnVisible(columnToSelect); this.ensureIndexVisible(rowIndexToScrollTo); // make sure the cell is rendered, needed if we are to focus this.animationFrameService.flushAllFrames(); this.focusedCellController.setFocusedCell(rowIndexToScrollTo, columnToSelect, null, true); } private pageDiagonally(pagingKey:string): void { //*************************************************************************** //where to place the newly selected cell cursor after the scroll let pageSize: number = this.getPrimaryScrollViewport().offsetHeight; let selectionTopDelta : number = pagingKey === Constants.KEY_PAGE_HOME_NAME ? 0: pageSize; //*************************************************************************** //where to scroll to let rowIndexToScrollTo = pagingKey === Constants.KEY_PAGE_HOME_NAME ? 0: this.paginationProxy.getPageLastRow(); let rowToScrollTo: RowNode = this.paginationProxy.getRow(rowIndexToScrollTo); //*************************************************************************** //column to select let allColumns: Column[] = this.columnController.getAllDisplayedColumns(); let columnToSelect : Column = pagingKey === Constants.KEY_PAGE_HOME_NAME ? allColumns[0]: allColumns[allColumns.length - 1]; let diagonalScroll: DiagonalScroll = { focusedRowTopDelta: selectionTopDelta, type: ScrollType.DIAGONAL, rowToScrollTo: rowToScrollTo, columnToScrollTo: columnToSelect }; this.performScroll(diagonalScroll); } //EITHER CTRL UP/DOWN or PAGE UP/DOWN private pageVertically (pagingKey:string): void{ if (pagingKey === Constants.KEY_CTRL_UP_NAME){ this.performScroll({ rowToScrollTo: this.paginationProxy.getRow(0), focusedRowTopDelta: 0, type: ScrollType.VERTICAL } as VerticalScroll); return; } if (pagingKey === Constants.KEY_CTRL_DOWN_NAME){ this.performScroll({ rowToScrollTo: this.paginationProxy.getRow(this.paginationProxy.getPageLastRow()), focusedRowTopDelta: this.getPrimaryScrollViewport().offsetHeight, type: ScrollType.VERTICAL } as VerticalScroll); return; } //*********PAGING KEYS****************************************************** //*************************************************************************** //where to place the newly selected cell cursor after the scroll // before we move the scroll // a) find the top position of the current selected cell // b) find what is the delta of that compared to the current scroll let focusedCell : GridCell = this.focusedCellController.getFocusedCell(); let focusedRowNode = this.paginationProxy.getRow(focusedCell.rowIndex); let focusedAbsoluteTop = focusedRowNode.rowTop; let selectionTopDelta = (focusedAbsoluteTop - this.getPrimaryScrollViewport().scrollTop) - this.paginationProxy.getPixelOffset(); //*************************************************************************** //how much to scroll: // a) One entire page from or to // b) the top of the first row in the current view // c) then find what is the row that would appear the first one in the screen and adjust it to its top pos // this will avoid having half printed rows at the top let currentPageTopmostPixel = this.getPrimaryScrollViewport().scrollTop; let currentPageTopRow = this.paginationProxy.getRowIndexAtPixel(currentPageTopmostPixel + this.paginationProxy.getPixelOffset()); let currentPageTopmostRow = this.paginationProxy.getRow(currentPageTopRow); let viewportSize: number = this.getPrimaryScrollViewport().offsetHeight; let maxPageSize: number = this.paginationProxy.getCurrentPageHeight(); let pageSize: number = maxPageSize < viewportSize ? maxPageSize : viewportSize; let currentTopmostRowBottom = currentPageTopmostRow.rowTop + currentPageTopmostRow.rowHeight; let toScrollUnadjusted = pagingKey == Constants.KEY_PAGE_DOWN_NAME ? pageSize + currentTopmostRowBottom : currentTopmostRowBottom - pageSize; let nextScreenTopmostRow = this.paginationProxy.getRow(this.paginationProxy.getRowIndexAtPixel(toScrollUnadjusted)); let verticalScroll : VerticalScroll = { rowToScrollTo: nextScreenTopmostRow, focusedRowTopDelta: selectionTopDelta, type: ScrollType.VERTICAL }; this.performScroll(verticalScroll); } // gets called by rowRenderer when new data loaded, as it will want to scroll // to the top public scrollToTop(): void { if (!this.forPrint) { this.getPrimaryScrollViewport().scrollTop = 0; } } //Performs any scroll private performScroll(scroll: Scroll) { let verticalScroll: VerticalScroll; let diagonalScroll: DiagonalScroll; let horizontalScroll: HorizontalScroll; let focusedCellBeforeScrolling : GridCell = this.focusedCellController.getFocusedCell(); //*************************************************************************** // Scroll screen let newScrollTop:number; switch (scroll.type){ case ScrollType.VERTICAL: verticalScroll = scroll; this.ensureIndexVisible(verticalScroll.rowToScrollTo.rowIndex); newScrollTop = verticalScroll.rowToScrollTo.rowTop - this.paginationProxy.getPixelOffset(); this.getPrimaryScrollViewport().scrollTop = newScrollTop; break; case ScrollType.DIAGONAL: diagonalScroll = scroll; this.ensureIndexVisible(diagonalScroll.rowToScrollTo.rowIndex); newScrollTop = diagonalScroll.rowToScrollTo.rowTop - this.paginationProxy.getPixelOffset(); this.getPrimaryScrollViewport().scrollTop = newScrollTop; this.getPrimaryScrollViewport().scrollLeft = diagonalScroll.columnToScrollTo.getLeft(); break; case ScrollType.HORIZONTAL: horizontalScroll = scroll; this.getPrimaryScrollViewport().scrollLeft = horizontalScroll.columnToScrollTo.getLeft(); break; } //*************************************************************************** // This is needed so that when we try to focus on the cell is actually rendered. let refreshViewParams: RefreshViewParams = { onlyBody: true, suppressKeepFocus: true }; this.rowRenderer.redrawAfterModelUpdate(refreshViewParams); //*************************************************************************** // New focused cell let focusedRowIndex: number; let focusedColumn: Column; switch (scroll.type){ case ScrollType.VERTICAL: focusedRowIndex = this.paginationProxy.getRowIndexAtPixel(newScrollTop + this.paginationProxy.getPixelOffset() + verticalScroll.focusedRowTopDelta); focusedColumn = focusedCellBeforeScrolling.column; break; case ScrollType.DIAGONAL: focusedRowIndex = this.paginationProxy.getRowIndexAtPixel(newScrollTop + this.paginationProxy.getPixelOffset() + diagonalScroll.focusedRowTopDelta); focusedColumn = diagonalScroll.columnToScrollTo; break; case ScrollType.HORIZONTAL: focusedRowIndex = focusedCellBeforeScrolling.rowIndex; focusedColumn = horizontalScroll.columnToScrollTo; break; } this.focusedCellController.setFocusedCell( focusedRowIndex, focusedColumn, null, true ); } private processMouseEvent(eventName: string, mouseEvent: MouseEvent): void { let cellComp = this.mouseEventService.getRenderedCellForEvent(mouseEvent); if (cellComp) { cellComp.onMouseEvent(eventName, mouseEvent); } let rowComp: RowComp = this.getRowForEvent(mouseEvent); if (rowComp) { rowComp.onMouseEvent(eventName, mouseEvent); } this.preventDefaultOnContextMenu(mouseEvent); } private onContextMenu(mouseEvent: MouseEvent): void { // to allow us to debug in chrome, we ignore the event if ctrl is pressed. // not everyone wants this, so first 'if' below allows to turn this hack off. if (!this.gridOptionsWrapper.isAllowContextMenuWithControlKey()) { // then do the check if (mouseEvent.ctrlKey || mouseEvent.metaKey) { return; } } if (this.contextMenuFactory && !this.gridOptionsWrapper.isSuppressContextMenu()) { this.contextMenuFactory.showMenu(null, null, null, mouseEvent); mouseEvent.preventDefault(); } } private preventDefaultOnContextMenu(mouseEvent: MouseEvent): void { // if we don't do this, then middle click will never result in a 'click' event, as 'mousedown' // will be consumed by the browser to mean 'scroll' (as you can scroll with the middle mouse // button in the browser). so this property allows the user to receive middle button clicks if // they want. if (this.gridOptionsWrapper.isSuppressMiddleClickScrolls() && mouseEvent.which === 2) { mouseEvent.preventDefault(); } } private addShortcutKeyListeners(): void { this.eAllCellContainers.forEach( (container)=> { container.addEventListener('keydown', (event: KeyboardEvent)=> { // if the cell the event came from is editing, then we do not // want to do the default shortcut keys, otherwise the editor // (eg a text field) would not be able to do the normal cut/copy/paste let renderedCell = this.mouseEventService.getRenderedCellForEvent(event); if (renderedCell && renderedCell.isEditing()) { return; } // for copy / paste, we don't want to execute when the event // was from a child grid (happens in master detail) if (!this.mouseEventService.isEventFromThisGrid(event)) { return; } if (event.ctrlKey || event.metaKey) { switch (event.which) { case Constants.KEY_A: return this.onCtrlAndA(event); case Constants.KEY_C: return this.onCtrlAndC(event); case Constants.KEY_V: return this.onCtrlAndV(event); case Constants.KEY_D: return this.onCtrlAndD(event); } } }); }) } private onCtrlAndA(event: KeyboardEvent): boolean { if (this.rangeController && this.paginationProxy.isRowsToRender()) { let rowEnd: number; let floatingStart: string; let floatingEnd: string; if (this.pinnedRowModel.isEmpty(Constants.PINNED_TOP)) { floatingStart = null; } else { floatingStart = Constants.PINNED_TOP; } if (this.pinnedRowModel.isEmpty(Constants.PINNED_BOTTOM)) { floatingEnd = null; rowEnd = this.paginationProxy.getTotalRowCount() - 1; } else { floatingEnd = Constants.PINNED_BOTTOM; rowEnd = this.pinnedRowModel.getPinnedBottomRowData().length = 1; } let allDisplayedColumns = this.columnController.getAllDisplayedColumns(); if (_.missingOrEmpty(allDisplayedColumns)) { return; } this.rangeController.setRange({ rowStart: 0, floatingStart: floatingStart, rowEnd: rowEnd, floatingEnd: floatingEnd, columnStart: allDisplayedColumns[0], columnEnd: allDisplayedColumns[allDisplayedColumns.length-1] }); } event.preventDefault(); return false; } private onCtrlAndC(event: KeyboardEvent): boolean { if (!this.clipboardService) { return; } let focusedCell = this.focusedCellController.getFocusedCell(); this.clipboardService.copyToClipboard(); event.preventDefault(); // the copy operation results in loosing focus on the cell, // because of the trickery the copy logic uses with a temporary // widget. so we set it back again. if (focusedCell) { this.focusedCellController.setFocusedCell(focusedCell.rowIndex, focusedCell.column, focusedCell.floating, true); } return false; } private onCtrlAndV(event: KeyboardEvent): boolean { if (!this.rangeController) { return; } this.clipboardService.pasteFromClipboard(); return false; } private onCtrlAndD(event: KeyboardEvent): boolean { if (!this.clipboardService) { return; } this.clipboardService.copyRangeDown(); event.preventDefault(); return false; } private createOverlayTemplate(name: string, defaultTemplate: string, userProvidedTemplate: string): string { let template = OVERLAY_TEMPLATE .replace('[OVERLAY_NAME]', name); if (userProvidedTemplate) { template = template.replace('[OVERLAY_TEMPLATE]', userProvidedTemplate); } else { template = template.replace('[OVERLAY_TEMPLATE]', defaultTemplate); } return template; } private createLoadingOverlayTemplate(): string { let userProvidedTemplate = this.gridOptionsWrapper.getOverlayLoadingTemplate(); let templateNotLocalised = this.createOverlayTemplate( 'loading', LOADING_OVERLAY_TEMPLATE, userProvidedTemplate); let localeTextFunc = this.gridOptionsWrapper.getLocaleTextFunc(); let templateLocalised = templateNotLocalised.replace('[LOADING...]', localeTextFunc('loadingOoo', 'Loading...')); return templateLocalised; } private createNoRowsOverlayTemplate(): string { let userProvidedTemplate = this.gridOptionsWrapper.getOverlayNoRowsTemplate(); let templateNotLocalised = this.createOverlayTemplate( 'no-rows', NO_ROWS_TO_SHOW_OVERLAY_TEMPLATE, userProvidedTemplate); let localeTextFunc = this.gridOptionsWrapper.getLocaleTextFunc(); let templateLocalised = templateNotLocalised.replace('[NO_ROWS_TO_SHOW]', localeTextFunc('noRowsToShow', 'No Rows To Show')); return templateLocalised; } // Valid values for position are bottom, middle and top // position should be {'top','middle','bottom', or undefined/null}. // if undefined/null, then the grid will to the minimal amount of scrolling, // eg if grid needs to scroll up, it scrolls until row is on top, // if grid needs to scroll down, it scrolls until row is on bottom, // if row is already in view, grid does not scroll public ensureIndexVisible(index: any, position?: string) { // if for print, everything is always visible if (this.gridOptionsWrapper.isForPrint()) { return; } this.logger.log('ensureIndexVisible: ' + index); let rowCount = this.paginationProxy.getTotalRowCount(); if (typeof index !== 'number' || index < 0 || index >= rowCount) { console.warn('invalid row index for ensureIndexVisible: ' + index); return; } this.paginationProxy.goToPageWithIndex(index); let nodeAtIndex = this.paginationProxy.getRow(index); let pixelOffset = this.paginationProxy.getPixelOffset(); let rowTopPixel = nodeAtIndex.rowTop - pixelOffset; let rowBottomPixel = rowTopPixel + nodeAtIndex.rowHeight; let vRange = this.getVerticalPixelRange(); let vRangeTop = vRange.top; let vRangeBottom = vRange.bottom; let scrollShowing = this.isHorizontalScrollShowing(); if (scrollShowing) { vRangeBottom -= this.scrollWidth; } let rowToHighlightHeight: number = rowBottomPixel - rowTopPixel; let viewportHeight = vRangeBottom - vRangeTop; let halfScreenHeight: number = (viewportHeight /2) + (rowToHighlightHeight / 2); let eViewportToScroll = this.getPrimaryScrollViewport(); let newScrollPosition: number; switch (position) { case 'top': newScrollPosition = rowTopPixel; break; case 'bottom': newScrollPosition = rowBottomPixel - viewportHeight; break; case 'middle': newScrollPosition = rowTopPixel + halfScreenHeight; // The if/else logic here protects us from over scrolling // ie: Trying to scroll past the row (ie ensureNodeVisible (0, 'middle')) newScrollPosition = newScrollPosition > rowTopPixel ? rowTopPixel : newScrollPosition; break; default: newScrollPosition = rowTopPixel; let viewportScrolledPastRow = vRangeTop > rowTopPixel; let viewportScrolledBeforeRow = vRangeBottom < rowBottomPixel; if (viewportScrolledPastRow) { // if row is before, scroll up with row at top newScrollPosition = rowTopPixel; } else if (viewportScrolledBeforeRow) { // if row is below, scroll down with row at bottom let viewportHeight = vRangeBottom - vRangeTop; newScrollPosition = rowBottomPixel - viewportHeight; } else { // row already in view, and top/middle/bottom not specified, so do nothing. newScrollPosition = null; } break; } // this means the row is already in view, and we don't need to scroll if (newScrollPosition===null) { return; } eViewportToScroll.scrollTop = newScrollPosition; this.rowRenderer.redrawAfterScroll(); } private getPrimaryScrollViewport(): HTMLElement { if (this.enableRtl && this.columnController.isPinningLeft()) { return this.ePinnedLeftColsViewport; } else if (!this.enableRtl && this.columnController.isPinningRight()) { return this.ePinnedRightColsViewport; } else { return this.eBodyViewport; } } // + moveColumnController public getCenterWidth(): number { return this.eBodyViewport.clientWidth; } private isHorizontalScrollShowing(): boolean { let result = _.isHorizontalScrollShowing(this.eBodyViewport); return result; } private isVerticalScrollShowing(): boolean { if (this.columnController.isPinningRight()) { return _.isVerticalScrollShowing(this.ePinnedRightColsViewport); } else { return _.isVerticalScrollShowing(this.eBodyViewport); } } private isBodyVerticalScrollShowing(): boolean { // if the scroll is on the pinned panel, then it is never in the center panel. // if LRT, then pinning right means scroll NOT on center if (!this.enableRtl && this.columnController.isPinningRight()) { return false; } // if RTL, then pinning left means scroll NOT on center if (this.enableRtl && this.columnController.isPinningLeft()) { return false; } return _.isVerticalScrollShowing(this.eBodyViewport); } // gets called every 500 ms. we use this to set padding on right pinned column public periodicallyCheck(): void { if (this.forPrint) { return; } this.setBottomPaddingOnPinnedRight(); this.setMarginOnFullWidthCellContainer(); this.setScrollShowing(); } private setScrollShowing(): void { let params: SetScrollsVisibleParams = { vBody: false, hBody: false, vPinnedLeft: false, vPinnedRight: false }; if (this.enableRtl) { if (this.columnController.isPinningLeft()) { params.vPinnedLeft = this.forPrint ? false : _.isVerticalScrollShowing(this.ePinnedLeftColsViewport); } else { params.vBody = _.isVerticalScrollShowing(this.eBodyViewport); } } else { if (this.columnController.isPinningRight()) { params.vPinnedRight = this.forPrint ? false : _.isVerticalScrollShowing(this.ePinnedRightColsViewport); } else { params.vBody = _.isVerticalScrollShowing(this.eBodyViewport); } } params.hBody = _.isHorizontalScrollShowing(this.eBodyViewport); this.scrollVisibleService.setScrollsVisible(params); } // the pinned container needs extra space at the bottom, some blank space, otherwise when // vertically scrolled all the way down, the last row will be hidden behind the scrolls. // this extra padding allows the last row to be lifted above the bottom scrollbar. private setBottomPaddingOnPinnedRight(): void { if (this.forPrint) { return; } if (this.columnController.isPinningRight()) { let bodyHorizontalScrollShowing = this.eBodyViewport.clientWidth < this.eBodyViewport.scrollWidth; if (bodyHorizontalScrollShowing) { this.ePinnedRightColsContainer.style.marginBottom = this.scrollWidth + 'px'; } else { this.ePinnedRightColsContainer.style.marginBottom = ''; } } } private setMarginOnFullWidthCellContainer(): void { if (this.forPrint) { return; } // if either right or bottom scrollbars are showing, we need to make sure the // fullWidthCell panel isn't covering the scrollbars. originally i tried to do this using // margin, but the overflow was not getting clipped and going into the margin, // so used border instead. dunno why it works, trial and error found the solution. if (this.enableRtl) { if (this.isVerticalScrollShowing()) { this.eFullWidthCellViewport.style.borderLeft = this.scrollWidth + 'px solid transparent'; } else { this.eFullWidthCellViewport.style.borderLeft = ''; } } else { if (this.isVerticalScrollShowing()) { this.eFullWidthCellViewport.style.borderRight = this.scrollWidth + 'px solid transparent'; } else { this.eFullWidthCellViewport.style.borderRight = ''; } } if (this.isHorizontalScrollShowing()) { this.eFullWidthCellViewport.style.borderBottom = this.scrollWidth + 'px solid transparent'; } else { this.eFullWidthCellViewport.style.borderBottom = ''; } } public ensureColumnVisible(key: any) { // if for print, everything is always visible if (this.gridOptionsWrapper.isForPrint()) { return; } let column = this.columnController.getGridColumn(key); if (!column) { return; } if (column.isPinned()) { console.warn('calling ensureIndexVisible on a '+column.getPinned()+' pinned column doesn\'t make sense for column ' + column.getColId()); return; } if (!this.columnController.isColumnDisplayed(column)) { console.warn('column is not currently visible'); return; } let colLeftPixel = column.getLeft(); let colRightPixel = colLeftPixel + column.getActualWidth(); let viewportWidth = this.eBodyViewport.clientWidth; let scrollPosition = this.getBodyViewportScrollLeft(); let bodyWidth = this.columnController.getBodyContainerWidth(); let viewportLeftPixel: number; let viewportRightPixel: number; // the logic of working out left and right viewport px is both here and in the ColumnController, // need to refactor it out to one place if (this.enableRtl) { viewportLeftPixel = bodyWidth - scrollPosition - viewportWidth; viewportRightPixel = bodyWidth - scrollPosition; } else { viewportLeftPixel = scrollPosition; viewportRightPixel = viewportWidth + scrollPosition; } let viewportScrolledPastCol = viewportLeftPixel > colLeftPixel; let viewportScrolledBeforeCol = viewportRightPixel < colRightPixel; if (viewportScrolledPastCol) { // if viewport's left side is after col's left side, scroll right to pull col into viewport at left if (this.enableRtl) { let newScrollPosition = bodyWidth - viewportWidth - colLeftPixel; this.setBodyViewportScrollLeft(newScrollPosition); } else { this.setBodyViewportScrollLeft(colLeftPixel); } } else if (viewportScrolledBeforeCol) { // if viewport's right side is before col's right side, scroll left to pull col into viewport at right if (this.enableRtl) { let newScrollPosition = bodyWidth - colRightPixel; this.setBodyViewportScrollLeft(newScrollPosition); } else { let newScrollPosition = colRightPixel - viewportWidth; this.setBodyViewportScrollLeft(newScrollPosition); } } else { // otherwise, col is already in view, so do nothing } // this will happen anyway, as the move will cause a 'scroll' event on the body, however // it is possible that the ensureColumnVisible method is called from within ag-Grid and // the caller will need to have the columns rendered to continue, which will be before // the event has been worked on (which is the case for cell navigation). this.setLeftAndRightBounds(); } public showLoadingOverlay(): void { if (!this.gridOptionsWrapper.isSuppressLoadingOverlay()) { this.layout.showOverlay('loading'); } } public showNoRowsOverlay(): void { if (!this.gridOptionsWrapper.isSuppressNoRowsOverlay()) { this.layout.showOverlay('noRows'); } } public hideOverlay(): void { this.layout.hideOverlay(); } private getWidthForSizeColsToFit() { let availableWidth = this.eBody.clientWidth; // if pinning right, then the scroll bar can show, however for some reason // it overlays the grid and doesn't take space. so we are only interested // in the body scroll showing. let removeVerticalScrollWidth = this.isVerticalScrollShowing(); if (removeVerticalScrollWidth) { availableWidth -= this.scrollWidth; } return availableWidth; } // method will call itself if no available width. this covers if the grid // isn't visible, but is just about to be visible. public sizeColumnsToFit(nextTimeout?: number) { let availableWidth = this.getWidthForSizeColsToFit(); if (availableWidth>0) { this.columnController.sizeColumnsToFit(availableWidth); } else { if (nextTimeout===undefined) { setTimeout( ()=> { this.sizeColumnsToFit(100); }, 0); } else if (nextTimeout===100) { setTimeout( ()=> { this.sizeColumnsToFit(-1); }, 100); } else { console.log('ag-Grid: tried to call sizeColumnsToFit() but the grid is coming back with ' + 'zero width, maybe the grid is not visible yet on the screen?'); } } } public getBodyContainer(): HTMLElement { return this.eBodyContainer; } public getDropTargetBodyContainers(): HTMLElement[] { if (this.forPrint) { return [this.eBodyContainer, this.eFloatingTopContainer, this.eFloatingBottomContainer]; } else { return [this.eBodyViewport, this.eFloatingTopViewport, this.eFloatingBottomViewport]; } } public getBodyViewport() { return this.eBodyViewport; } public getDropTargetLeftContainers(): HTMLElement[] { if (this.forPrint) { return []; } else { return [this.ePinnedLeftColsViewport, this.ePinnedLeftFloatingBottom, this.ePinnedLeftFloatingTop]; } } public getDropTargetPinnedRightContainers(): HTMLElement[] { if (this.forPrint) { return []; } else { return [this.ePinnedRightColsViewport, this.ePinnedRightFloatingBottom, this.ePinnedRightFloatingTop]; } } public getHeaderContainer() { return this.eHeaderContainer; } public getHeaderOverlay() { return this.eHeaderOverlay; } public getRoot() { return this.eRoot; } public getPinnedLeftHeader() { return this.ePinnedLeftHeader; } public getPinnedRightHeader() { return this.ePinnedRightHeader; } private queryHtmlElement(selector: string): HTMLElement { return this.eRoot.querySelector(selector); } private loadTemplate(): void { // the template we use is different when doing 'for print' let template: string; if (this.forPrint) { template = GRID_PANEL_FOR_PRINT_TEMPLATE; } else if (this.autoHeight) { template = GRID_PANEL_AUTO_HEIGHT_TEMPLATE; } else { template = GRID_PANEL_NORMAL_TEMPLATE; } this.eRoot = _.loadTemplate(template); } private findElements() { if (this.forPrint) { this.eHeaderContainer = this.queryHtmlElement('.ag-header-container'); this.eBodyContainer = this.queryHtmlElement('.ag-body-container'); this.eFloatingTopContainer = this.queryHtmlElement('.ag-floating-top-container'); this.eFloatingBottomContainer = this.queryHtmlElement('.ag-floating-bottom-container'); this.eAllCellContainers = [this.eBodyContainer, this.eFloatingTopContainer, this.eFloatingBottomContainer]; let containers: RowContainerComponents = { body: new RowContainerComponent( {eContainer: this.eBodyContainer} ), fullWidth: null, pinnedLeft: null, pinnedRight: null, floatingTop: new RowContainerComponent( {eContainer: this.eFloatingTopContainer} ), floatingTopPinnedLeft: null, floatingTopPinnedRight: null, floatingTopFullWidth: null, floatingBottom: new RowContainerComponent( {eContainer: this.eFloatingBottomContainer} ), floatingBottomPinnedLeft: null, floatingBottomPinnedRight: null, floatingBottomFullWith: null }; this.rowContainerComponents = containers; // when doing forPrint, we don't have any fullWidth containers, instead we add directly to the main // containers. this works in forPrint only as there are no pinned columns (no need for fullWidth to // span pinned columns) and the rows are already the full width of the grid (the reason for fullWidth) containers.fullWidth = containers.body; containers.floatingBottomFullWith = containers.floatingBottom; containers.floatingTopFullWidth = containers.floatingTop; } else { this.eBody = this.queryHtmlElement('.ag-body'); this.eBodyContainer = this.queryHtmlElement('.ag-body-container'); this.eBodyViewport = this.queryHtmlElement('.ag-body-viewport'); this.eBodyViewportWrapper = this.queryHtmlElement('.ag-body-viewport-wrapper'); this.eFullWidthCellContainer = this.queryHtmlElement('.ag-full-width-container'); this.eFullWidthCellViewport = this.queryHtmlElement('.ag-full-width-viewport'); this.ePinnedLeftColsContainer = this.queryHtmlElement('.ag-pinned-left-cols-container'); this.ePinnedRightColsContainer = this.queryHtmlElement('.ag-pinned-right-cols-container'); this.ePinnedLeftColsViewport = this.queryHtmlElement('.ag-pinned-left-cols-viewport'); this.ePinnedRightColsViewport = this.queryHtmlElement('.ag-pinned-right-cols-viewport'); this.ePinnedLeftHeader = this.queryHtmlElement('.ag-pinned-left-header'); this.ePinnedRightHeader = this.queryHtmlElement('.ag-pinned-right-header'); this.eHeader = this.queryHtmlElement('.ag-header'); this.eHeaderContainer = this.queryHtmlElement('.ag-header-container'); this.eHeaderOverlay = this.queryHtmlElement('.ag-header-overlay'); this.eHeaderViewport = this.queryHtmlElement('.ag-header-viewport'); this.eFloatingTop = this.queryHtmlElement('.ag-floating-top'); this.ePinnedLeftFloatingTop = this.queryHtmlElement('.ag-pinned-left-floating-top'); this.ePinnedRightFloatingTop = this.queryHtmlElement('.ag-pinned-right-floating-top'); this.eFloatingTopContainer = this.queryHtmlElement('.ag-floating-top-container'); this.eFloatingTopViewport = this.queryHtmlElement('.ag-floating-top-viewport'); this.eFloatingTopFullWidthCellContainer = this.queryHtmlElement('.ag-floating-top-full-width-container'); this.eFloatingBottom = this.queryHtmlElement('.ag-floating-bottom'); this.ePinnedLeftFloatingBottom = this.queryHtmlElement('.ag-pinned-left-floating-bottom'); this.ePinnedRightFloatingBottom = this.queryHtmlElement('.ag-pinned-right-floating-bottom'); this.eFloatingBottomContainer = this.queryHtmlElement('.ag-floating-bottom-container'); this.eFloatingBottomViewport = this.queryHtmlElement('.ag-floating-bottom-viewport'); this.eFloatingBottomFullWidthCellContainer = this.queryHtmlElement('.ag-floating-bottom-full-width-container'); this.eAllCellContainers = [ this.ePinnedLeftColsContainer, this.ePinnedRightColsContainer, this.eBodyContainer, this.eFloatingTop, this.eFloatingBottom, this.eFullWidthCellContainer]; this.rowContainerComponents = { body: new RowContainerComponent({eContainer: this.eBodyContainer, eViewport: this.eBodyViewport}), fullWidth: new RowContainerComponent({eContainer: this.eFullWidthCellContainer, hideWhenNoChildren: true, eViewport: this.eFullWidthCellViewport}), pinnedLeft: new RowContainerComponent({eContainer: this.ePinnedLeftColsContainer, eViewport: this.ePinnedLeftColsViewport}), pinnedRight: new RowContainerComponent({eContainer: this.ePinnedRightColsContainer, eViewport: this.ePinnedRightColsViewport}), floatingTop: new RowContainerComponent({eContainer: this.eFloatingTopContainer}), floatingTopPinnedLeft: new RowContainerComponent({eContainer: this.ePinnedLeftFloatingTop}), floatingTopPinnedRight: new RowContainerComponent({eContainer: this.ePinnedRightFloatingTop}), floatingTopFullWidth: new RowContainerComponent({eContainer: this.eFloatingTopFullWidthCellContainer, hideWhenNoChildren: true}), floatingBottom: new RowContainerComponent({eContainer: this.eFloatingBottomContainer}), floatingBottomPinnedLeft: new RowContainerComponent({eContainer: this.ePinnedLeftFloatingBottom}), floatingBottomPinnedRight: new RowContainerComponent({eContainer: this.ePinnedRightFloatingBottom}), floatingBottomFullWith: new RowContainerComponent({eContainer: this.eFloatingBottomFullWidthCellContainer, hideWhenNoChildren: true}), }; this.addMouseWheelEventListeners(); } _.iterateObject(this.rowContainerComponents, (key: string, container: RowContainerComponent)=> { if (container) { this.context.wireBean(container); } }); } public getRowContainers(): RowContainerComponents { return this.rowContainerComponents; } private addMouseWheelEventListeners(): void { // IE9, Chrome, Safari, Opera use 'mousewheel', Firefox uses 'DOMMouseScroll' this.addDestroyableEventListener(this.eBodyViewport, 'mousewheel', this.centerMouseWheelListener.bind(this)); this.addDestroyableEventListener(this.eBodyViewport, 'DOMMouseScroll', this.centerMouseWheelListener.bind(this)); if (this.enableRtl) { this.addDestroyableEventListener(this.ePinnedRightColsViewport, 'mousewheel', this.genericMouseWheelListener.bind(this)); this.addDestroyableEventListener(this.ePinnedRightColsViewport, 'DOMMouseScroll', this.genericMouseWheelListener.bind(this)); } else { this.addDestroyableEventListener(this.ePinnedLeftColsViewport, 'mousewheel', this.genericMouseWheelListener.bind(this)); this.addDestroyableEventListener(this.ePinnedLeftColsViewport, 'DOMMouseScroll', this.genericMouseWheelListener.bind(this)); } } public getHeaderViewport(): HTMLElement { return this.eHeaderViewport; } private centerMouseWheelListener(event: any): boolean { // we are only interested in mimicking the mouse wheel if we are not scrolling on the middle, // otherwise the body has scrolls and the mouse wheel works for free let bodyVScrollShowing = this.isBodyVerticalScrollActive(); if (!bodyVScrollShowing) { let targetPanel = this.enableRtl ? this.ePinnedLeftColsViewport : this.ePinnedRightColsViewport; return this.generalMouseWheelListener(event, targetPanel); } } // used for listening to mouse wheel events on 1) left pinned and also the 2) fullWidthCell components. // the fullWidthCell listener is added in renderedRow, hence public. public genericMouseWheelListener(event: any): boolean { let targetPanel: HTMLElement; let bodyVScrollActive = this.isBodyVerticalScrollActive(); if (bodyVScrollActive) { targetPanel = this.eBodyViewport; } else { targetPanel = this.enableRtl ? this.ePinnedLeftColsViewport : this.ePinnedRightColsViewport; } return this.generalMouseWheelListener(event, targetPanel); } private generalMouseWheelListener(event: any, targetPanel: HTMLElement): boolean { let wheelEvent = _.normalizeWheel(event); // we need to detect in which direction scroll is happening to allow trackpads scroll horizontally // horizontal scroll if (Math.abs(wheelEvent.pixelX) > Math.abs(wheelEvent.pixelY)) { let newLeftPosition = this.eBodyViewport.scrollLeft + wheelEvent.pixelX; this.eBodyViewport.scrollLeft = newLeftPosition; } // vertical scroll else { let newTopPosition = targetPanel.scrollTop + wheelEvent.pixelY; targetPanel.scrollTop = newTopPosition; } // allow the option to pass mouse wheel events to the browser // https://github.com/ag-grid/ag-grid/issues/800 // in the future, this should be tied in with 'forPrint' option, or have an option 'no vertical scrolls' if (!this.gridOptionsWrapper.isSuppressPreventDefaultOnMouseWheel()) { // if we don't prevent default, then the whole browser will scroll also as well as the grid event.preventDefault(); } return false; } public onDisplayedColumnsChanged(): void { this.setPinnedContainersVisible(); this.setBodyAndHeaderHeights(); this.setLeftAndRightBounds(); } private onDisplayedColumnsWidthChanged(): void { this.setWidthsOfContainers(); this.setLeftAndRightBounds(); if (this.enableRtl) { // because RTL is all backwards, a change in the width of the row // can cause a change in the scroll position, without a scroll event, // because the scroll position in RTL is a function that depends on // the width. to be convinced of this, take out this line, enable RTL, // scroll all the way to the left and then resize a column this.horizontallyScrollHeaderCenterAndFloatingCenter(); } } private onScrollVisibilityChanged(): void { this.setWidthsOfContainers(); } private setWidthsOfContainers(): void { let mainRowWidth = this.columnController.getBodyContainerWidth() + 'px'; this.eBodyContainer.style.width = mainRowWidth; if (this.forPrint) { // pinned col doesn't exist when doing forPrint return; } this.eFloatingBottomContainer.style.width = mainRowWidth; this.eFloatingTopContainer.style.width = mainRowWidth; this.setPinnedLeftWidth(); this.setPinnedRightWidth(); } private setPinnedLeftWidth(): void { let pinnedLeftWidth = this.scrollVisibleService.getPinnedLeftWidth() + 'px'; let pinnedLeftWidthWithScroll = this.scrollVisibleService.getPinnedLeftWithScrollWidth() + 'px'; this.ePinnedLeftColsViewport.style.width = pinnedLeftWidthWithScroll; this.eBodyViewportWrapper.style.marginLeft = pinnedLeftWidthWithScroll; this.ePinnedLeftFloatingBottom.style.width = pinnedLeftWidthWithScroll; this.ePinnedLeftFloatingTop.style.width = pinnedLeftWidthWithScroll; this.ePinnedLeftColsContainer.style.width = pinnedLeftWidth; } private setPinnedRightWidth(): void { let pinnedRightWidth = this.scrollVisibleService.getPinnedRightWidth() + 'px'; let pinnedRightWidthWithScroll = this.scrollVisibleService.getPinnedRightWithScrollWidth() + 'px'; this.ePinnedRightColsViewport.style.width = pinnedRightWidthWithScroll; this.eBodyViewportWrapper.style.marginRight = pinnedRightWidthWithScroll; this.ePinnedRightFloatingBottom.style.width = pinnedRightWidthWithScroll; this.ePinnedRightFloatingTop.style.width = pinnedRightWidthWithScroll; this.ePinnedRightColsContainer.style.width = pinnedRightWidth; } private setPinnedContainersVisible() { // no need to do this if not using scrolls if (this.forPrint) { return; } let changeDetected = false; // if we are v scrolling, then one of these will have the scroll position. // we us this inside the if(changedDetected), so we don't always use it, however // it is changed when we make a pinned panel not visible, so we have to check it // before we change display on the pinned panels let scrollTop = Math.max( this.eBodyViewport.scrollTop, this.ePinnedLeftColsViewport.scrollTop, this.ePinnedRightColsViewport.scrollTop); let showLeftPinned = this.columnController.isPinningLeft(); if (showLeftPinned !== this.pinningLeft) { this.pinningLeft = showLeftPinned; this.ePinnedLeftHeader.style.display = showLeftPinned ? 'inline-block' : 'none'; this.ePinnedLeftColsViewport.style.display = showLeftPinned ? 'inline' : 'none'; changeDetected = true; } let showRightPinned = this.columnController.isPinningRight(); if (showRightPinned !== this.pinningRight) { this.pinningRight = showRightPinned; this.ePinnedRightHeader.style.display = showRightPinned ? 'inline-block' : 'none'; this.ePinnedRightColsViewport.style.display = showRightPinned ? 'inline' : 'none'; changeDetected = true; } if (changeDetected) { let bodyVScrollActive = this.isBodyVerticalScrollActive(); this.eBodyViewport.style.overflowY = bodyVScrollActive ? 'auto' : 'hidden'; // the body either uses it's scroll (when scrolling) or it's style.top // (when following the scroll of a pinned section), so we need to set it // back when changing from one to the other if (bodyVScrollActive) { this.setFakeScroll(this.eBodyContainer, 0); // this.eBodyContainer.style.top = '0px'; } else { this.eBodyViewport.scrollTop = 0; } // when changing the primary scroll viewport, we copy over the scroll position, // eg if body was getting scrolled and we were at position 100px, then we start // pinning and pinned viewport is now the primary, we need to set it to 100px let primaryScrollViewport = this.getPrimaryScrollViewport(); primaryScrollViewport.scrollTop = scrollTop; // this adjusts the scroll position of all the faking panels. they should already // be correct except body which has potentially just turned to be fake. this.fakeVerticalScroll(scrollTop); } } // init, layoutChanged, floatingDataChanged, headerHeightChanged public setBodyAndHeaderHeights(): void { if (this.forPrint) { // if doing 'for print' or 'auto height', then the header and footers are laid // out naturally by the browser. it's whatever height that's needed to fit. return; } let heightOfContainer = this.layout.getCentreHeight(); if (!heightOfContainer) { return; } let headerRowCount = this.columnController.getHeaderRowCount(); let totalHeaderHeight: number; let numberOfFloating = 0; let groupHeight:number; let headerHeight:number; if (!this.columnController.isPivotMode()){ _.removeCssClass(this.eHeader, 'ag-pivot-on'); _.addCssClass(this.eHeader, 'ag-pivot-off'); if (this.gridOptionsWrapper.isFloatingFilter()){ headerRowCount ++; } numberOfFloating = (this.gridOptionsWrapper.isFloatingFilter()) ? 1 : 0; groupHeight = this.gridOptionsWrapper.getGroupHeaderHeight(); headerHeight = this.gridOptionsWrapper.getHeaderHeight(); } else { _.removeCssClass(this.eHeader, 'ag-pivot-off'); _.addCssClass(this.eHeader, 'ag-pivot-on'); numberOfFloating = 0; groupHeight = this.gridOptionsWrapper.getPivotGroupHeaderHeight(); headerHeight = this.gridOptionsWrapper.getPivotHeaderHeight(); } let numberOfNonGroups = 1 + numberOfFloating; let numberOfGroups = headerRowCount - numberOfNonGroups; totalHeaderHeight = numberOfFloating * this.gridOptionsWrapper.getFloatingFiltersHeight(); totalHeaderHeight += numberOfGroups * groupHeight; totalHeaderHeight += headerHeight; this.eHeader.style['height'] = totalHeaderHeight + 'px'; // if we are doing auto-height, we only size the header, we don't size the // other parts as we use the normal browser layout for that if (this.autoHeight) { return; } // padding top covers the header and the pinned rows on top let floatingTopHeight = this.pinnedRowModel.getPinnedTopTotalHeight(); let paddingTop = totalHeaderHeight + floatingTopHeight; // bottom is just the bottom pinned rows let floatingBottomHeight = this.pinnedRowModel.getPinnedBottomTotalHeight(); let floatingBottomTop = heightOfContainer - floatingBottomHeight; let bodyHeight = heightOfContainer - totalHeaderHeight - floatingBottomHeight - floatingTopHeight; this.eBody.style.top = paddingTop + 'px'; this.eBody.style.height = bodyHeight + 'px'; this.eFloatingTop.style.top = totalHeaderHeight + 'px'; this.eFloatingTop.style.height = floatingTopHeight + 'px'; this.eFloatingBottom.style.height = floatingBottomHeight + 'px'; this.eFloatingBottom.style.top = floatingBottomTop + 'px'; this.ePinnedLeftColsViewport.style.height = bodyHeight + 'px'; this.ePinnedRightColsViewport.style.height = bodyHeight + 'px'; // bodyHeight property is used by pagination service, that may change number of rows // in this page based on the height of the grid if (this.bodyHeight !== bodyHeight) { this.bodyHeight = bodyHeight; let event: BodyHeightChangedEvent = { type: Events.EVENT_BODY_HEIGHT_CHANGED, api: this.gridApi, columnApi: this.columnApi }; this.eventService.dispatchEvent(event); } } public getBodyHeight(): number { return this.bodyHeight; } public setHorizontalScrollPosition(hScrollPosition: number): void { this.eBodyViewport.scrollLeft = hScrollPosition; // we need to manually do the event handling (rather than wait for the event) // for the alignedGridsService, as if we don't, the aligned grid service gets // notified async, and then it's 'consuming' flag doesn't get used right, and // we can end up with an infinite loop if (this.nextScrollLeft !== hScrollPosition) { this.nextScrollLeft = hScrollPosition; this.doHorizontalScroll(); } } // tries to scroll by pixels, but returns what the result actually was public scrollHorizontally(pixels: number): number { let oldScrollPosition = this.eBodyViewport.scrollLeft; this.setHorizontalScrollPosition(oldScrollPosition + pixels); let newScrollPosition = this.eBodyViewport.scrollLeft; return newScrollPosition - oldScrollPosition; } private addScrollListener() { // if printing, then no scrolling, so no point in listening for scroll events if (this.forPrint) { return; } this.addDestroyableEventListener(this.eBodyViewport, 'scroll', this.onBodyScroll.bind(this)); // below we add two things: // pinnedScrollListener -> when pinned panel with scrollbar gets scrolled, it updates body and other pinned // suppressScroll -> stops scrolling when pinned panel was moved - which can only happen when user is navigating // in the pinned container, as the pinned col should never scroll. so we rollback the scroll on the pinned. let onPinnedLeftVerticalScroll = this.onVerticalScroll.bind(this, this.ePinnedLeftColsViewport); let onPinnedRightVerticalScroll = this.onVerticalScroll.bind(this, this.ePinnedRightColsViewport); if (this.enableRtl) { this.addDestroyableEventListener(this.ePinnedLeftColsViewport, 'scroll', onPinnedLeftVerticalScroll); let suppressRightScroll = () => this.ePinnedRightColsViewport.scrollTop = 0; this.addDestroyableEventListener(this.ePinnedRightColsViewport, 'scroll', suppressRightScroll); } else { this.addDestroyableEventListener(this.ePinnedRightColsViewport, 'scroll', onPinnedRightVerticalScroll); let suppressLeftScroll = () => this.ePinnedLeftColsViewport.scrollTop = 0; this.addDestroyableEventListener(this.ePinnedLeftColsViewport, 'scroll', suppressLeftScroll); } let suppressCenterScroll = () => { if (this.getPrimaryScrollViewport()!==this.eBodyViewport) { this.eBodyViewport.scrollTop = 0; } }; this.addDestroyableEventListener(this.eBodyViewport, 'scroll', suppressCenterScroll); this.addIEPinFix(onPinnedRightVerticalScroll, onPinnedLeftVerticalScroll); } private onBodyScroll(): void { this.onBodyHorizontalScroll(); this.onBodyVerticalScroll(); } private onBodyHorizontalScroll(): void { let scrollLeft = this.eBodyViewport.scrollLeft; if (this.nextScrollLeft !== scrollLeft) { this.nextScrollLeft = scrollLeft; if (this.useAnimationFrame) { this.animationFrameService.schedule(); } else { this.doHorizontalScroll(); } } } private doHorizontalScroll(): void { this.scrollLeft = this.nextScrollLeft; let event: BodyScrollEvent = { type: Events.EVENT_BODY_SCROLL, api: this.gridApi, columnApi: this.columnApi, direction: 'horizontal', left: this.scrollLeft, top: this.scrollTop }; this.eventService.dispatchEvent(event); this.horizontallyScrollHeaderCenterAndFloatingCenter(); this.setLeftAndRightBounds(); // this.alignedGridsService.fireHorizontalScrollEvent(this.scrollLeft); } private onBodyVerticalScroll(): void { let bodyVScrollActive = this.isBodyVerticalScrollActive(); if (bodyVScrollActive) { this.onVerticalScroll(this.eBodyViewport); } } private onVerticalScroll(sourceElement: HTMLElement): void { let scrollTop = sourceElement.scrollTop; if (this.useAnimationFrame) { if (this.nextScrollTop !== scrollTop) { this.nextScrollTop = scrollTop; this.animationFrameService.schedule(); } } else { if (scrollTop !== this.scrollTop) { this.scrollTop = scrollTop; this.fakeVerticalScroll(scrollTop); this.redrawRowsAfterScroll(); } } } public executeFrame(): boolean { if (this.scrollLeft !== this.nextScrollLeft) { this.doHorizontalScroll(); return true; } else if (this.scrollTop !== this.nextScrollTop) { this.scrollTop = this.nextScrollTop; this.fakeVerticalScroll(this.scrollTop); this.verticalRedrawNeeded = true; return true; } else if (this.verticalRedrawNeeded) { this.redrawRowsAfterScroll(); this.verticalRedrawNeeded = false; return true; } else { return false; } } private redrawRowsAfterScroll(): void { let event: BodyScrollEvent = { type: Events.EVENT_BODY_SCROLL, direction: 'vertical', api: this.gridApi, columnApi: this.columnApi, left: this.scrollLeft, top: this.scrollTop }; this.eventService.dispatchEvent(event); this.rowRenderer.redrawAfterScroll(); } // if LTR, we hide body scroll if pinning right (as scroll is in right pinned), // if RTL, we hide body scroll if pinning left (as scroll is in left pinned) private isBodyVerticalScrollActive(): boolean { let pinningRight = this.columnController.isPinningRight(); let pinningLeft = this.columnController.isPinningLeft(); let centerHasScroll = this.enableRtl ? !pinningLeft : !pinningRight; return centerHasScroll; } // this bit is a fix / hack for IE due to this: // https://www.ag-grid.com/forum/showthread.php?tid=4303 // it gets the left panel to reposition itself after a model change private addIEPinFix(onPinnedRightScroll: Function, onPinnedLeftScroll: Function): void { let listener = () => { if (this.columnController.isPinningRight()) { setTimeout( ()=> { if (this.enableRtl) { onPinnedLeftScroll(); } else { onPinnedRightScroll(); } }, 0); } }; this.addDestroyableEventListener(this.eventService, Events.EVENT_MODEL_UPDATED, listener); } // this gets called whenever a change in the viewport, so we can inform column controller it has to work // out the virtual columns again. gets called from following locations: // + ensureColVisible, scroll, init, layoutChanged, displayedColumnsChanged private setLeftAndRightBounds(): void { if (this.gridOptionsWrapper.isForPrint()) { return; } let scrollWidth = this.eBodyViewport.clientWidth; let scrollPosition = this.getBodyViewportScrollLeft(); this.columnController.setVirtualViewportPosition(scrollWidth, scrollPosition); } public getBodyViewportScrollLeft(): number { if (this.forPrint) { return 0; } // we defer to a util, as how you calculated scrollLeft when doing RTL depends on the browser return _.getScrollLeft(this.eBodyViewport, this.enableRtl); } public setBodyViewportScrollLeft(value: number): void { if (this.forPrint) { return; } // we defer to a util, as how you calculated scrollLeft when doing RTL depends on the browser _.setScrollLeft(this.eBodyViewport, value, this.enableRtl); } public horizontallyScrollHeaderCenterAndFloatingCenter(): void { let scrollLeft = this.getBodyViewportScrollLeft(); let offset = this.enableRtl ? scrollLeft : -scrollLeft; this.eHeaderContainer.style.left = offset + 'px'; this.eFloatingBottomContainer.style.left = offset + 'px'; this.eFloatingTopContainer.style.left = offset + 'px'; } // we say fake scroll as only one panel (left, right or body) has scrolls, // the other panels mimic the scroll by getting it's top position updated. private fakeVerticalScroll(position: number): void { if (this.enableRtl) { // RTL // if pinning left, then body scroll is faking let pinningLeft = this.columnController.isPinningLeft(); if (pinningLeft) { this.setFakeScroll(this.eBodyContainer, position); } // right is always faking this.setFakeScroll(this.ePinnedRightColsContainer, position); } else { // LTR // if pinning right, then body scroll is faking let pinningRight = this.columnController.isPinningRight(); if (pinningRight) { this.setFakeScroll(this.eBodyContainer, position); } // left is always faking this.setFakeScroll(this.ePinnedLeftColsContainer, position); } // always scroll fullWidth container, as this is never responsible for a scroll this.setFakeScroll(this.eFullWidthCellContainer, position); } private setFakeScroll(eContainer: HTMLElement, pixels: number): void { eContainer.style.top = -pixels + 'px'; // eContainer.style.transform = `translateY(${-pixels}px)`; } public addScrollEventListener(listener: ()=>void): void { this.eBodyViewport.addEventListener('scroll', listener); } public removeScrollEventListener(listener: ()=>void): void { this.eBodyViewport.removeEventListener('scroll', listener); } } enum ScrollType { HORIZONTAL, VERTICAL, DIAGONAL } interface Scroll { type: ScrollType } interface VerticalScroll extends Scroll{ rowToScrollTo : RowNode focusedRowTopDelta: number } interface HorizontalScroll extends Scroll { columnToScrollTo: Column, columnToFocus: Column, } interface DiagonalScroll extends Scroll { rowToScrollTo : RowNode, focusedRowTopDelta: number, columnToScrollTo: Column } export interface TestKeyboardBindingGroupsResult { trappedKeyboardBinding: KeyboardBinding, trappedKeyboardBindingGroup: KeyboardBindingGroup } function testKeyboardBindingGroups (keyboardBindingGroups:KeyboardBindingGroup[], event:KeyboardEvent): TestKeyboardBindingGroupsResult{ for (let i=0; i