ag-grid
Version:
Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components
1,118 lines (922 loc) • 65.4 kB
text/typescript
import {Utils as _} from "../utils";
import { observeResize } from "../resizeObserver";
import {GridOptionsWrapper} from "../gridOptionsWrapper";
import {ColumnController} from "../columnController/columnController";
import {ColumnApi} from "../columnController/columnApi";
import {RowRenderer} from "../rendering/rowRenderer";
import {
Bean,
Autowired,
PostConstruct,
Optional,
PreDestroy,
Context,
PreConstruct
} 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} 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 {IFrameworkFactory} from "../interfaces/iFrameworkFactory";
import {Column} from "../entities/column";
import {RowContainerComponent} from "../rendering/rowContainerComponent";
import {RowNode} from "../entities/rowNode";
import {PaginationAutoPageSizeService, 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";
import {NavigationService} from "./navigationService";
import {CellComp} from "../rendering/cellComp";
import {ValueService} from "../valueService/valueService";
import {LongTapEvent, TouchListener} from "../widgets/touchListener";
import {ComponentRecipes} from "../components/framework/componentRecipes";
import {DragAndDropService} from "../dragAndDrop/dragAndDropService";
import {RowDragFeature} from "./rowDragFeature";
import {HeightScaler} from "../rendering/heightScaler";
import {IOverlayWrapperComp} from "../rendering/overlays/overlayWrapperComponent";
import {Component} from "../widgets/component";
import {AutoHeightCalculator} from "../rendering/autoHeightCalculator";
import {ColumnAnimationService} from "../rendering/columnAnimationService";
import {AutoWidthCalculator} from "../rendering/autoWidthCalculator";
import {Beans} from "../rendering/beans";
import {RefSelector} from "../widgets/componentAnnotations";
import {HeaderRootComp} from "../headerRendering/headerRootComp";
// 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 GRID_PANEL_NORMAL_TEMPLATE =
`<div class="ag-root ag-font-style" role="grid">
<ag-header-root ref="headerRoot"></ag-header-root>
<div class="ag-floating-top" ref="eTop" role="presentation">
<div class="ag-pinned-left-floating-top" ref="eLeftTop" role="presentation"></div>
<div class="ag-floating-top-viewport" ref="eTopViewport" role="presentation">
<div class="ag-floating-top-container" ref="eTopContainer" role="presentation"></div>
</div>
<div class="ag-pinned-right-floating-top" ref="eRightTop" role="presentation"></div>
<div class="ag-floating-top-full-width-container" ref="eTopFullWidthContainer" role="presentation"></div>
</div>
<div class="ag-body" ref="eBody" role="presentation">
<div class="ag-pinned-left-cols-viewport-wrapper" ref="eLeftViewportWrapper" role="presentation">
<div class="ag-pinned-left-cols-viewport" ref="eLeftViewport" role="presentation">
<div class="ag-pinned-left-cols-container" ref="eLeftContainer" role="presentation"></div>
</div>
</div>
<div class="ag-body-viewport-wrapper" role="presentation">
<div class="ag-body-viewport" ref="eBodyViewport" role="presentation">
<div class="ag-body-container" ref="eBodyContainer" role="presentation"></div>
</div>
</div>
<div class="ag-pinned-right-cols-viewport-wrapper" ref="eRightViewportWrapper" role="presentation">
<div class="ag-pinned-right-cols-viewport" ref="eRightViewport" role="presentation">
<div class="ag-pinned-right-cols-container" ref="eRightContainer" role="presentation"></div>
</div>
</div>
<div class="ag-full-width-viewport-wrapper" ref="eFullWidthViewportWrapper" role="presentation">
<div class="ag-full-width-viewport" ref="eFullWidthViewport" role="presentation">
<div class="ag-full-width-container" ref="eFullWidthContainer" role="presentation"></div>
</div>
</div>
</div>
<div class="ag-floating-bottom" ref="eBottom" role="presentation">
<div class="ag-pinned-left-floating-bottom" ref="eLeftBottom" role="presentation"></div>
<div class="ag-floating-bottom-viewport" ref="eBottomViewport" role="presentation">
<div class="ag-floating-bottom-container" ref="eBottomContainer" role="presentation"></div>
</div>
<div class="ag-pinned-right-floating-bottom" ref="eRightBottom" role="presentation"></div>
<div class="ag-floating-bottom-full-width-container" ref="eBottomFullWidthContainer" role="presentation"></div>
</div>
<div class="ag-overlay" ref="eOverlay"></div>
</div>`;
export type RowContainerComponentNames =
'fullWidth' |
'body' |
'pinnedLeft' |
'pinnedRight' |
'floatingTop' |
'floatingTopPinnedLeft' |
'floatingTopPinnedRight' |
'floatingTopFullWidth' |
'floatingBottom' |
'floatingBottomPinnedLeft' |
'floatingBottomPinnedRight' |
'floatingBottomFullWith';
export type RowContainerComponents = { [K in RowContainerComponentNames]: RowContainerComponent };
export class GridPanel extends Component {
@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('navigationService') private navigationService: NavigationService;
@Autowired('autoHeightCalculator') private autoHeightCalculator: AutoHeightCalculator;
@Autowired('columnAnimationService') private columnAnimationService: ColumnAnimationService;
@Autowired('autoWidthCalculator') private autoWidthCalculator: AutoWidthCalculator;
@Autowired('paginationAutoPageSizeService') private paginationAutoPageSizeService: PaginationAutoPageSizeService;
@Autowired('beans') private beans: Beans;
@Autowired('paginationProxy') private paginationProxy: PaginationProxy;
@Autowired('columnApi') private columnApi: ColumnApi;
@Autowired('gridApi') private gridApi: GridApi;
@Autowired('dragService') private dragService: DragService;
@Autowired('selectionController') private selectionController: SelectionController;
@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;
@Autowired('frameworkFactory') private frameworkFactory: IFrameworkFactory;
@Autowired('valueService') private valueService: ValueService;
@Autowired('componentRecipes') private componentRecipes: ComponentRecipes;
@Autowired('dragAndDropService') private dragAndDropService: DragAndDropService;
@Autowired('heightScaler') private heightScaler: HeightScaler;
@Autowired('enterprise') private enterprise: boolean;
@Optional('rangeController') private rangeController: IRangeController;
@Optional('contextMenuFactory') private contextMenuFactory: IContextMenuFactory;
@Optional('clipboardService') private clipboardService: IClipboardService;
@RefSelector('eBody') private eBody: HTMLElement;
@RefSelector('eBodyViewport') private eBodyViewport: HTMLElement;
@RefSelector('eBodyContainer') private eBodyContainer: HTMLElement;
@RefSelector('eLeftContainer') private eLeftContainer: HTMLElement;
@RefSelector('eRightContainer') private eRightContainer: HTMLElement;
@RefSelector('eFullWidthViewportWrapper') private eFullWidthViewportWrapper: HTMLElement;
@RefSelector('eFullWidthViewport') private eFullWidthViewport: HTMLElement;
@RefSelector('eFullWidthContainer') private eFullWidthContainer: HTMLElement;
@RefSelector('eLeftViewport') private eLeftViewport: HTMLElement;
@RefSelector('eLeftViewportWrapper') private eLeftViewportWrapper: HTMLElement;
@RefSelector('eRightViewport') private eRightViewport: HTMLElement;
@RefSelector('eRightViewportWrapper') private eRightViewportWrapper: HTMLElement;
@RefSelector('eTop') private eTop: HTMLElement;
@RefSelector('eLeftTop') private eLeftTop: HTMLElement;
@RefSelector('eRightTop') private eRightTop: HTMLElement;
@RefSelector('eTopContainer') private eTopContainer: HTMLElement;
@RefSelector('eTopViewport') private eTopViewport: HTMLElement;
@RefSelector('eTopFullWidthContainer') private eTopFullWidthContainer: HTMLElement;
@RefSelector('eBottom') private eBottom: HTMLElement;
@RefSelector('eLeftBottom') private eLeftBottom: HTMLElement;
@RefSelector('eRightBottom') private eRightBottom: HTMLElement;
@RefSelector('eBottomContainer') private eBottomContainer: HTMLElement;
@RefSelector('eBottomViewport') private eBottomViewport: HTMLElement;
@RefSelector('eBottomFullWidthContainer') private eBottomFullWidthContainer: HTMLElement;
@RefSelector('headerRoot') headerRootComp: HeaderRootComp;
private rowContainerComponents: RowContainerComponents;
private eAllCellContainers: HTMLElement[];
private eOverlay: 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 scrollWidth: number;
private scrollClipWidth: 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;
private overlayWrapper: IOverlayWrapperComp;
private lastVScrollElement: HTMLElement;
private lastVScrollTime: number;
constructor() {
super(GRID_PANEL_NORMAL_TEMPLATE);
}
public getVScrollPosition(): {top: number, bottom: number} {
let result = {
top: this.eBodyViewport.scrollTop,
bottom: this.eBodyViewport.scrollTop + this.eBodyViewport.offsetHeight
};
return result;
}
// used by range controller
public getHScrollPosition(): {left: number, right: number} {
let result = {
left: this.eBodyViewport.scrollLeft,
right: this.eBodyViewport.scrollTop + this.eBodyViewport.offsetWidth
};
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();
}
}
private onNewColumnsLoaded(): void {
// hide overlay if columns and rows exist, this can happen if columns are loaded after data.
// this problem exists before of the race condition between the services (column controller in this case)
// and the view (grid panel). if the model beans were all initialised first, and then the view beans second,
// this race condition would not happen.
if (this.columnController.isReady() && !this.paginationProxy.isEmpty()) {
this.hideOverlay();
}
}
@PostConstruct
private init() {
this.instantiate(this.context);
// makes code below more readable if we pull 'forPrint' out
this.scrollWidth = this.gridOptionsWrapper.getScrollbarWidth();
this.enableRtl = this.gridOptionsWrapper.isEnableRtl();
this.useAnimationFrame = !this.gridOptionsWrapper.isSuppressAnimationFrame();
// if the browser is Windows based, then the scrollbars take up space, and we clip by
// the width of the scrollbar. however if the scroll bars do not take up space (iOS)
// then they overlay on top of the div, so we clip some extra blank space instead.
this.scrollClipWidth = this.scrollWidth > 0 ? this.scrollWidth : 20;
// all of these element have different CSS when layout changes
this.gridOptionsWrapper.addLayoutElement(this.getGui());
this.gridOptionsWrapper.addLayoutElement(this.eBody);
this.gridOptionsWrapper.addLayoutElement(this.eBodyViewport);
this.gridOptionsWrapper.addLayoutElement(this.eTopViewport);
this.gridOptionsWrapper.addLayoutElement(this.eBodyContainer);
this.suppressScrollOnFloatingRow();
this.setupRowAnimationCssClass();
this.buildRowContainerComponents();
this.addEventListeners();
this.addDragListeners();
this.addScrollListener();
if (this.gridOptionsWrapper.isSuppressHorizontalScroll()) {
this.eBodyViewport.style.overflowX = 'hidden';
}
this.setupOverlay();
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();
this.mockContextMenuForIPad();
this.addRowDragListener();
if (this.$scope) {
this.addAngularApplyCheck();
}
this.onDisplayedColumnsWidthChanged();
// this.addWindowResizeListener();
this.gridApi.registerGridComp(this);
this.alignedGridsService.registerGridComp(this);
this.headerRootComp.registerGridComp(this);
this.animationFrameService.registerGridComp(this);
this.navigationService.registerGridComp(this);
this.heightScaler.registerGridComp(this);
this.autoHeightCalculator.registerGridComp(this);
this.columnAnimationService.registerGridComp(this);
this.autoWidthCalculator.registerGridComp(this);
this.paginationAutoPageSizeService.registerGridComp(this);
this.beans.registerGridComp(this);
this.rowRenderer.registerGridComp(this);
if (this.rangeController) {
this.rangeController.registerGridComp(this);
}
const unsubscribeFromResize = observeResize(this.eBodyViewport, this.onBodyViewportResized.bind(this) );
this.addDestroyFunc(() => unsubscribeFromResize() );
}
private onBodyViewportResized(): void {
this.checkViewportAndScrolls();
}
// used by ColumnAnimationService
public setColumnMovingCss(moving: boolean): void {
this.addOrRemoveCssClass('ag-column-moving', moving);
}
private setupOverlay(): void {
this.overlayWrapper = this.componentRecipes.newOverlayWrapperComponent();
this.eOverlay = this.queryForHtmlElement('[ref="eOverlay"]');
this.overlayWrapper.hideOverlay(this.eOverlay);
}
private addRowDragListener(): void {
let rowDragFeature = new RowDragFeature(this.eBody, this);
this.context.wireBean(rowDragFeature);
this.dragAndDropService.addDropTarget(rowDragFeature);
}
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.getGui().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_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.eventService, Events.EVENT_NEW_COLUMNS_LOADED, this.onNewColumnsLoaded.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.gridOptionsWrapper.isEnableRangeSelection() // no range selection if no property
|| _.missing(this.rangeController)) { // no range selection if not enterprise version
return;
}
let containers = [this.eLeftContainer, this.eRightContainer, this.eBodyContainer,
this.eTop, this.eBottom];
containers.forEach(container => {
let params: DragListenerParams = {
eElement: container,
onDragStart: this.rangeController.onDragStart.bind(this.rangeController),
onDragStop: this.rangeController.onDragStop.bind(this.rangeController),
onDragging: this.rangeController.onDragging.bind(this.rangeController),
// for range selection by dragging the mouse, we want to ignore the event if shift key is pressed,
// as shift key click is another type of range selection
skipMouseEvent: mouseEvent => mouseEvent.shiftKey
};
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 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.eLeftViewport || target===this.eRightViewport) {
// show it
this.onContextMenu(mouseEvent, null, null, null, null);
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.eRightViewport, 'contextmenu', listener);
this.addDestroyableEventListener(this.eLeftViewport, 'contextmenu', listener);
}
// + rangeController
public getBodyClientRect(): ClientRect {
if (this.eBody) {
return this.eBody.getBoundingClientRect();
}
}
private getRowForEvent(event: Event): 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':
// first see if it's a scroll key, page up / down, home / end etc
let wasScrollKey = this.navigationService.handlePageScrollingKey(keyboardEvent);
// if not a scroll key, then we pass onto cell
if (!wasScrollKey) {
renderedCell.onKeyDown(keyboardEvent);
}
break;
case 'keypress':
renderedCell.onKeyPress(keyboardEvent);
break;
}
}
// gets called by rowRenderer when new data loaded, as it will want to scroll to the top
public scrollToTop(): void {
this.eBodyViewport.scrollTop = 0;
}
private processMouseEvent(eventName: string, mouseEvent: MouseEvent): void {
if (!this.mouseEventService.isEventFromThisGrid(mouseEvent)) { return; }
if (_.isStopPropagationForAgGrid(mouseEvent)) { return; }
let rowComp = this.getRowForEvent(mouseEvent);
let cellComp = this.mouseEventService.getRenderedCellForEvent(mouseEvent);
if (eventName === "contextmenu") {
this.handleContextMenuMouseEvent(mouseEvent, null, rowComp, cellComp);
} else {
if (cellComp) { cellComp.onMouseEvent(eventName, mouseEvent); }
if (rowComp) { rowComp.onMouseEvent(eventName, mouseEvent); }
}
this.preventDefaultOnContextMenu(mouseEvent);
}
private mockContextMenuForIPad(): void {
// we do NOT want this when not in ipad, otherwise we will be doing
if (!_.isUserAgentIPad()) {return;}
this.eAllCellContainers.forEach( container => {
let touchListener = new TouchListener(container);
let longTapListener = (event: LongTapEvent)=> {
let rowComp = this.getRowForEvent(event.touchEvent);
let cellComp = this.mouseEventService.getRenderedCellForEvent(event.touchEvent);
this.handleContextMenuMouseEvent(null, event.touchEvent, rowComp, cellComp);
};
this.addDestroyableEventListener(touchListener, TouchListener.EVENT_LONG_TAP, longTapListener);
this.addDestroyFunc( ()=> touchListener.destroy() );
});
}
private handleContextMenuMouseEvent(mouseEvent: MouseEvent, touchEvent: TouchEvent, rowComp: RowComp, cellComp: CellComp) {
let rowNode = rowComp ? rowComp.getRowNode() : null;
let column = cellComp ? cellComp.getColumn() : null;
let value = null;
if (column) {
let event = mouseEvent ? mouseEvent : touchEvent;
cellComp.dispatchCellContextMenuEvent(event);
value = this.valueService.getValue(column, rowNode);
}
this.onContextMenu(mouseEvent, touchEvent, rowNode, column, value);
}
private onContextMenu(mouseEvent: MouseEvent, touchEvent: TouchEvent, rowNode: RowNode, column: Column, value: any): 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 && (mouseEvent.ctrlKey || mouseEvent.metaKey)) {
return;
}
}
if (this.contextMenuFactory && !this.gridOptionsWrapper.isSuppressContextMenu()) {
let eventOrTouch: (MouseEvent | Touch) = mouseEvent ? mouseEvent : touchEvent.touches[0];
this.contextMenuFactory.showMenu(rowNode, column, value, eventOrTouch);
let event = mouseEvent ? mouseEvent : touchEvent;
event.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.enterprise) { return; }
if (this.gridOptionsWrapper.isSuppressClipboardPaste()) { return; }
this.clipboardService.pasteFromClipboard();
return false;
}
private onCtrlAndD(event: KeyboardEvent): boolean {
if (!this.enterprise) { return; }
this.clipboardService.copyRangeDown();
event.preventDefault();
return false;
}
// 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 or auto height, everything is always visible
if (this.gridOptionsWrapper.isGridAutoHeight()) { return; }
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 rowNode = this.paginationProxy.getRow(index);
let paginationOffset = this.paginationProxy.getPixelOffset();
let rowTopPixel = rowNode.rowTop - paginationOffset;
let rowBottomPixel = rowTopPixel + rowNode.rowHeight;
let scrollPosition = this.getVScrollPosition();
let heightOffset = this.heightScaler.getOffset();
let vScrollTop = scrollPosition.top + heightOffset;
let vScrollBottom = scrollPosition.bottom + heightOffset;
if (this.isHorizontalScrollShowing()) {
vScrollBottom -= this.scrollWidth;
}
let viewportHeight = vScrollBottom - vScrollTop;
let newScrollPosition: number = null;
// work out the pixels for top, middle and bottom up front,
// make the if/else below easier to read
let pxTop = this.heightScaler.getScrollPositionForPixel(rowTopPixel);
let pxBottom = this.heightScaler.getScrollPositionForPixel(rowBottomPixel - viewportHeight);
let pxMiddle = (pxTop + pxBottom) / 2;
// make sure if middle, the row is not outside the top of the grid
if (pxMiddle > rowTopPixel) {
pxMiddle = rowTopPixel;
}
let rowBelowViewport = vScrollTop > rowTopPixel;
let rowAboveViewport = vScrollBottom < rowBottomPixel;
if (position==='top') {
newScrollPosition = pxTop;
} else if (position==='bottom') {
newScrollPosition = pxBottom;
} else if (position==='middle') {
newScrollPosition = pxMiddle;
} else if (rowBelowViewport) {
// if row is before, scroll up with row at top
newScrollPosition = pxTop;
} else if (rowAboveViewport) {
// if row is below, scroll down with row at bottom
newScrollPosition = pxBottom;
}
if (newScrollPosition!==null) {
this.eBodyViewport.scrollTop = newScrollPosition;
this.rowRenderer.redrawAfterScroll();
}
}
// + moveColumnController
public getCenterWidth(): number {
return this.eBodyViewport.clientWidth;
}
public isHorizontalScrollShowing(): boolean {
return _.isHorizontalScrollShowing(this.eBodyViewport);
}
private isVerticalScrollShowing(): boolean {
return _.isVerticalScrollShowing(this.eBodyViewport);
}
// gets called every time the viewport size changes. we use this to check visibility of scrollbars
// in the grid panel, and also to check size and position of viewport for row and column virtualisation.
public checkViewportAndScrolls(): void {
// results in updating anything that depends on scroll showing
this.updateScrollVisibleService();
// fires event if height changes, used by PaginationService, HeightScalerService, RowRenderer
this.checkBodyHeight();
// check for virtual columns for ColumnController
this.onHorizontalViewportChanged();
this.setPinnedLeftWidth();
this.setPinnedRightWidth();
this.setBottomPaddingOnPinned();
this.hideVerticalScrollOnCenter();
this.hideFullWidthViewportScrollbars();
}
private updateScrollVisibleService(): void {
let params: SetScrollsVisibleParams = {
bodyHorizontalScrollShowing: false,
leftVerticalScrollShowing: false,
rightVerticalScrollShowing: false
};
if (this.enableRtl && this.columnController.isPinningLeft()) {
params.leftVerticalScrollShowing = _.isVerticalScrollShowing(this.eLeftViewport);
}
if (!this.enableRtl && this.columnController.isPinningRight()) {
params.rightVerticalScrollShowing = _.isVerticalScrollShowing(this.eRightViewport);
}
params.bodyHorizontalScrollShowing = this.isHorizontalScrollShowing();
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 setBottomPaddingOnPinned(): void {
// no need for padding if the scrollbars are not taking up any space
if (this.scrollWidth<=0) { return; }
if (this.isHorizontalScrollShowing()) {
this.eRightContainer.style.marginBottom = this.scrollWidth + 'px';
this.eLeftContainer.style.marginBottom = this.scrollWidth + 'px';
} else {
this.eRightContainer.style.marginBottom = '';
this.eLeftContainer.style.marginBottom = '';
}
}
private hideFullWidthViewportScrollbars(): void {
// if browser does not have scrollbars that take up space (eg iOS) then we don't need
// to adjust the sizes of the container for scrollbars
// if (this.scrollWidth <= 0) { return; }
let scrollWidthPx = this.scrollClipWidth > 0 ? this.scrollWidth + 'px' : '';
// if horizontal scroll is showing, we add padding to bottom so
// fullWidth container is not spreading over the scroll
this.eFullWidthViewportWrapper.style.paddingBottom = this.isHorizontalScrollShowing() ? scrollWidthPx : '';
// if vertical scroll is showing on full width viewport, then we clip it away, otherwise
// it competes with the main vertical scroll. this is done by getting the viewport to be
// bigger than the wrapper, the wrapper then ends up clipping the viewport.
let takeOutVScroll = this.isVerticalScrollShowing();
if (this.enableRtl) {
this.eFullWidthViewportWrapper.style.marginLeft = takeOutVScroll ? scrollWidthPx : '';
this.eFullWidthViewport.style.marginLeft = takeOutVScroll ? ('-' + scrollWidthPx) : '';
} else {
this.eFullWidthViewportWrapper.style.width = takeOutVScroll ? `calc(100% - ${scrollWidthPx})` : '';
this.eFullWidthViewport.style.width = takeOutVScroll ? `calc(100% + ${scrollWidthPx})` : '';
}
}
public ensureColumnVisible(key: any) {
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;
let colToSmallForViewport = viewportWidth < column.getActualWidth();
let alignColToLeft = viewportScrolledPastCol || colToSmallForViewport;
let alignColToRight = viewportScrolledBeforeCol;
if (alignColToLeft) {
// if viewport's left side is after col's left side, scroll left to pull col into viewport at left
if (this.enableRtl) {
let newScrollPosition = bodyWidth - viewportWidth - colLeftPixel;
this.setBodyViewportScrollLeft(newScrollPosition);
} else {
this.setBodyViewportScrollLeft(colLeftPixel);
}
} else if (alignColToRight) {
// if viewport's right side is before col's right side, scroll right 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.onHorizontalViewportChanged();
}
public showLoadingOverlay() {
if (!this.gridOptionsWrapper.isSuppressLoadingOverlay()) {
this.overlayWrapper.showLoadingOverlay(this.eOverlay);
}
}
public showNoRowsOverlay() {
if (!this.gridOptionsWrapper.isSuppressNoRowsOverlay()) {
this.overlayWrapper.showNoRowsOverlay(this.eOverlay);
}
}
public hideOverlay() {
this.overlayWrapper.hideOverlay(this.eOverlay);
}
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, "sizeColumnsToFit");
} else {
if (nextTimeout===undefined) {
setTimeout( ()=> {
this.sizeColumnsToFit(100);
}, 0);
} else if (nextTimeout===100) {
setTimeout( ()=> {
this.sizeColumnsToFit(500);
}, 100);
} else if (nextTimeout===500) {
setTimeout( ()=> {
this.sizeColumnsToFit(-1);
}, 500);
} 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[] {
return [this.eBodyViewport, this.eTopViewport, this.eBottomViewport];
}
public getDropTargetLeftContainers(): HTMLElement[] {
return [this.eLeftViewport, this.eLeftBottom, this.eLeftTop];
}
public getDropTargetRightContainers(): HTMLElement[] {
return [this.eRightViewport, this.eRightBottom, this.eRightTop];
}
private buildRowContainerComponents() {
this.eAllCellContainers = [
this.eLeftContainer, this.eRightContainer, this.eBodyContainer,
this.eTop, this.eBottom, this.eFullWidthContainer];
this.rowContainerComponents = {
body: new RowContainerComponent({eContainer: this.eBodyContainer, eViewport: this.eBodyViewport}),
fullWidth: new RowContainerComponent({eContainer: this.eFullWidthContainer, hideWhenNoChildren: true, eViewport: this.eFullWidthViewport}),
pinnedLeft: new RowContainerComponent({eContainer: this.eLeftContainer, eViewport: this.eLeftViewport}),
pinnedRight: new RowContainerComponent({eContainer: this.eRightContainer, eViewport: this.eRightViewport}),
floatingTop: new RowContainerComponent({eContainer: this.eTopContainer}),
floatingTopPinnedLeft: new RowContainerComponent({eContainer: this.eLeftTop}),
floatingTopPinnedRight: new RowContainerComponent({eContainer: this.eRightTop}),
floatingTopFullWidth: new RowContainerComponent({eContainer: this.eTopFullWidthContainer, hideWhenNoChildren: true}),
floatingBottom: new RowContainerComponent({eContainer: this.eBottomContainer}),
floatingBottomPinnedLeft: new RowContainerComponent({eContainer: this.eLeftBottom}),
floatingBottomPinnedRight: new RowContainerComponent({eContainer: this.eRightBottom}),
floatingBottomFullWith: new RowContainerComponent({eContainer: this.eBottomFullWidthContainer, hideWhenNoChildren: true}),
};
_.iterateObject(this.rowContainerComponents, (key: string, container: RowContainerComponent)=> {
if (container) {
this.context.wireBean(container);
}
});
}
private setupRowAnimationCssClass(): void {
let listener = () => {
// we don't want to use row animation if scaling, as rows jump strangely as you scroll,
// when scaling and doing row animation.
let animateRows = this.gridOptionsWrapper.isAnimateRows() && !this.heightScaler.isScaling();
_.addOrRemoveCssClass(this.eBody, 'ag-row-animation', animateRows);
_.addOrRemoveCssClass(this.eBody, 'ag-row-no-animation', !animateRows);
};
listener();
this.addDestroyableEventListener(this.eventService, Events.EVENT_HEIGHT_SCALE_CHANGED, listener);
}
// when editing a pinned row, if the cell is half outside the scrollable area, the browser can
// scroll the column into view. we do not want this, the pinned sections should never scroll.
// so we listen to scrolls on these containers and reset the scroll if we find one.
private suppressScrollOnFloatingRow(): void {
let resetTopScroll = () => this.eTopViewport.scrollLeft = 0;
let resetBottomScroll = () => this.eTopViewport.scrollLeft = 0;
this.addDestroyableEventListener(this.eTopViewport, 'scroll', resetTopScroll);
this.addDestroyableEventListener(this.eBottomViewport, 'scroll', resetBottomScroll);
}
public getRowContainers(): RowContainerComponents {
return this.rowContainerComponents;
}
public onDisplayedColumnsChanged(): void {
this.setPinnedContainersVisible();
this.setBodyAndHeaderHeights();
this.onHorizontalViewportChanged();
}
private onDisplayedColumnsWidthChanged(): void {
this.setWidthsOfContainers();
this.onHorizontalViewportChanged();
if (this.enableRtl) {
// because RTL is all backwards, a change in the width of the row
// can cause a change in the scroll position, with