ag-grid-enterprise
Version:
ag-Grid Enterprise Features
483 lines (382 loc) • 16.5 kB
text/typescript
import {
Utils,
Bean,
IRangeController,
Autowired,
GridCellDef,
LoggerFactory,
GridPanel,
IRowModel,
EventService,
ColumnController,
RowRenderer,
FocusedCellController,
MouseEventService,
Logger,
RangeSelection,
PostConstruct,
Events,
GridCell,
AddRangeSelectionParams,
GridRow,
Column,
GridOptionsWrapper,
RangeSelectionChangedEvent,
ColumnApi,
GridApi,
CellNavigationService,
_,
Constants
} from "ag-grid-community";
export class RangeController implements IRangeController {
private loggerFactory: LoggerFactory;
private rowModel: IRowModel;
private eventService: EventService;
private columnController: ColumnController;
private rowRenderer: RowRenderer;
private focusedCellController: FocusedCellController;
private mouseEventService: MouseEventService;
private gridOptionsWrapper: GridOptionsWrapper;
private columnApi: ColumnApi;
private gridApi: GridApi;
private cellNavigationService: CellNavigationService;
private logger: Logger;
private gridPanel: GridPanel;
private cellRanges: RangeSelection[];
private activeRange: RangeSelection;
private lastMouseEvent: MouseEvent;
private bodyScrollListener = this.onBodyScroll.bind(this);
private dragging = false;
private autoScrollService: AutoScrollService;
public registerGridComp(gridPanel: GridPanel): void {
this.gridPanel = gridPanel;
this.autoScrollService = new AutoScrollService(this.gridPanel, this.gridOptionsWrapper);
}
private init(): void {
this.logger = this.loggerFactory.create('RangeController');
this.eventService.addEventListener(Events.EVENT_COLUMN_EVERYTHING_CHANGED, this.clearSelection.bind(this));
this.eventService.addEventListener(Events.EVENT_COLUMN_GROUP_OPENED, this.clearSelection.bind(this));
this.eventService.addEventListener(Events.EVENT_COLUMN_MOVED, this.clearSelection.bind(this));
this.eventService.addEventListener(Events.EVENT_COLUMN_PINNED, this.clearSelection.bind(this));
this.eventService.addEventListener(Events.EVENT_COLUMN_ROW_GROUP_CHANGED, this.clearSelection.bind(this));
this.eventService.addEventListener(Events.EVENT_COLUMN_VISIBLE, this.clearSelection.bind(this));
}
public setRangeToCell(cell: GridCell, appendRange = false): void {
if (!this.gridOptionsWrapper.isEnableRangeSelection()) { return; }
const columns = this.updateSelectedColumns(cell.column, cell.column);
if (!columns) { return; }
const gridCellDef = <GridCellDef> {rowIndex: cell.rowIndex, floating: cell.floating, column: cell.column};
const newRange = {
start: new GridCell(gridCellDef),
end: new GridCell(gridCellDef),
columns: columns
};
// if not appending, then clear previous range selections
if (!appendRange || _.missing(this.cellRanges)) {
this.cellRanges = [];
}
this.cellRanges.push(newRange);
this.activeRange = null;
this.dispatchChangedEvent(true, false);
}
public extendRangeToCell(toCell: GridCell): void {
let lastRange = _.existsAndNotEmpty(this.cellRanges) ? this.cellRanges[this.cellRanges.length - 1] : null;
let startCell = lastRange ? lastRange.start : toCell;
this.setRange({
rowStart: startCell.rowIndex,
floatingStart: startCell.floating,
rowEnd: toCell.rowIndex,
floatingEnd: toCell.floating,
columnStart: startCell.column,
columnEnd: toCell.column
});
}
// returns true if successful, false if not successful
public extendRangeInDirection(startCell: GridCell, key: number): boolean {
let oneRangeExists = _.exists(this.cellRanges) || this.cellRanges.length === 1;
let previousSelectionStart = oneRangeExists ? this.cellRanges[0].start : null;
let takeEndFromPreviousSelection = startCell.equals(previousSelectionStart);
let previousEndCell = takeEndFromPreviousSelection ? this.cellRanges[0].end : startCell;
let newEndCell = this.cellNavigationService.getNextCellToFocus(key, previousEndCell);
// if user is at end of grid, so no cell to extend to, we return false
if (!newEndCell) {
return false;
}
this.setRange({
rowStart: startCell.rowIndex,
floatingStart: startCell.floating,
rowEnd: newEndCell.rowIndex,
floatingEnd: newEndCell.floating,
columnStart: startCell.column,
columnEnd: newEndCell.column
});
return true;
}
public setRange(rangeSelection: AddRangeSelectionParams): void {
if (!this.gridOptionsWrapper.isEnableRangeSelection()) { return; }
this.cellRanges = [];
this.addRange(rangeSelection);
}
public addRange(rangeSelection: AddRangeSelectionParams): void {
if (!this.gridOptionsWrapper.isEnableRangeSelection()) { return; }
let columnStart = this.columnController.getColumnWithValidation(rangeSelection.columnStart);
let columnEnd = this.columnController.getPrimaryColumn(rangeSelection.columnEnd);
if (!columnStart || !columnEnd) { return; }
let columns = this.updateSelectedColumns(columnStart, columnEnd);
if (!columns) { return; }
let startGridCellDef = <GridCellDef> {column: columnStart, rowIndex: rangeSelection.rowStart, floating: rangeSelection.floatingStart};
let endGridCellDef = <GridCellDef> {column: columnEnd, rowIndex: rangeSelection.rowEnd, floating: rangeSelection.floatingEnd};
let newRange = <RangeSelection> {
start: new GridCell(startGridCellDef),
end: new GridCell(endGridCellDef),
columns: columns
};
if (!this.cellRanges) {
this.cellRanges = [];
}
this.cellRanges.push(newRange);
this.dispatchChangedEvent(true, false);
}
public getCellRanges(): RangeSelection[] {
return this.cellRanges;
}
public isEmpty(): boolean {
return Utils.missingOrEmpty(this.cellRanges);
}
public isMoreThanOneCell(): boolean {
if (Utils.missingOrEmpty(this.cellRanges)) {
return false;
} else {
if (this.cellRanges.length>1) {
return true;
} else {
let onlyRange = this.cellRanges[0];
let onlyOneCellInRange =
onlyRange.start.column === onlyRange.end.column &&
onlyRange.start.rowIndex === onlyRange.end.rowIndex;
return !onlyOneCellInRange;
}
}
}
public clearSelection(): void {
if (Utils.missing(this.cellRanges)) {
return;
}
this.activeRange = null;
this.cellRanges = null;
this.dispatchChangedEvent(true, false);
}
// as the user is dragging outside of the panel, the div starts to scroll, which in turn
// means we are selection more (or less) cells, but the mouse isn't moving, so we recalculate
// the selection my mimicking a new mouse event
private onBodyScroll(): void {
this.onDragging(this.lastMouseEvent);
}
public isCellInAnyRange(cell: GridCell): boolean {
return this.getCellRangeCount(cell) > 0;
}
private isCellInSpecificRange(cell: GridCell, range: RangeSelection): boolean {
let columnInRange = range.columns.indexOf(cell.column) >= 0;
let rowInRange = this.isRowInRange(cell.rowIndex, cell.floating, range);
return columnInRange && rowInRange;
}
// returns the number of ranges this cell is in
public getCellRangeCount(cell: GridCell): number {
if (Utils.missingOrEmpty(this.cellRanges)) {
return 0;
}
let matchingCount = 0;
this.cellRanges.forEach( (cellRange: RangeSelection) => {
if (this.isCellInSpecificRange(cell, cellRange)) {
matchingCount++;
}
});
return matchingCount;
}
private isRowInRange(rowIndex: number, floating: string, cellRange: RangeSelection): boolean {
let row1 = new GridRow(cellRange.start.rowIndex, cellRange.start.floating);
let row2 = new GridRow(cellRange.end.rowIndex, cellRange.end.floating);
let firstRow = row1.before(row2) ? row1 : row2;
let lastRow = row1.before(row2) ? row2 : row1;
let thisRow = new GridRow(rowIndex, floating);
if (thisRow.equals(firstRow) || thisRow.equals(lastRow)) {
return true;
} else {
let afterFirstRow = !thisRow.before(firstRow);
let beforeLastRow = thisRow.before(lastRow);
return afterFirstRow && beforeLastRow;
}
}
public onDragStart(mouseEvent: MouseEvent): void {
if (!this.gridOptionsWrapper.isEnableRangeSelection()) { return; }
// ctrlKey for windows, metaKey for Apple
const multiKeyPressed = mouseEvent.ctrlKey || mouseEvent.metaKey;
const allowMulti = !this.gridOptionsWrapper.isSuppressMultiRangeSelection();
const multiSelectKeyPressed = allowMulti ? multiKeyPressed : false;
const missingRanges = Utils.missing(this.cellRanges);
let cell = this.mouseEventService.getGridCellForEvent(mouseEvent);
if (Utils.missing(cell)) {
// if drag wasn't on cell, then do nothing, including do not set dragging=true,
// (which them means onDragging and onDragStop do nothing)
return;
}
const len = missingRanges ? 0 : this.cellRanges.length;
if (missingRanges || !multiSelectKeyPressed) {
this.cellRanges = [];
} else if (!this.activeRange && len && this.isCellInSpecificRange(cell, this.cellRanges[len - 1])) {
this.activeRange = this.activeRange = this.cellRanges[len - 1];
}
if (!this.activeRange) {
this.createNewActiveRange(cell);
}
this.gridPanel.addScrollEventListener(this.bodyScrollListener);
this.dragging = true;
this.lastMouseEvent = mouseEvent;
this.selectionChanged(false, true);
}
private createNewActiveRange(cell: GridCell): void {
let gridCellDef = <GridCellDef> {column: cell.column, rowIndex: cell.rowIndex, floating: cell.floating};
this.activeRange = {
start: new GridCell(gridCellDef),
end: new GridCell(gridCellDef),
columns: [cell.column]
};
this.cellRanges.push(this.activeRange);
}
private selectionChanged(finished: boolean, started: boolean): void {
this.activeRange.columns = this.updateSelectedColumns(this.activeRange.start.column, this.activeRange.end.column);
this.dispatchChangedEvent(finished, started);
}
private dispatchChangedEvent(finished: boolean, started: boolean): void {
let event: RangeSelectionChangedEvent = {
type: Events.EVENT_RANGE_SELECTION_CHANGED,
api: this.gridApi,
columnApi: this.columnApi,
finished: finished,
started: started
};
this.eventService.dispatchEvent(event);
}
public onDragStop(): void {
if (!this.dragging) {
return;
}
this.autoScrollService.ensureCleared();
this.gridPanel.removeScrollEventListener(this.bodyScrollListener);
this.lastMouseEvent = null;
this.dragging = false;
this.dispatchChangedEvent(true, false);
}
public onDragging(mouseEvent: MouseEvent): void {
if (!this.dragging || !this.activeRange) {
return;
}
this.lastMouseEvent = mouseEvent;
this.autoScrollService.check(mouseEvent);
let cell = this.mouseEventService.getGridCellForEvent(mouseEvent);
if (Utils.missing(cell)) {
return;
}
let columnChanged = false;
if (cell.column !== this.activeRange.end.column) {
this.activeRange.end.column = cell.column;
columnChanged = true;
}
let rowChanged = false;
if (cell.rowIndex!==this.activeRange.end.rowIndex || cell.floating!==this.activeRange.end.floating) {
this.activeRange.end.rowIndex = cell.rowIndex;
this.activeRange.end.floating = cell.floating;
rowChanged = true;
}
if (columnChanged || rowChanged) {
this.selectionChanged(false, false);
}
}
private updateSelectedColumns(columnFrom: Column, columnTo: Column): Column[] {
let allColumns = this.columnController.getAllDisplayedColumns();
let fromIndex = allColumns.indexOf(columnFrom);
let toIndex = allColumns.indexOf(columnTo);
if (fromIndex < 0) {
console.warn('ag-Grid: column ' + columnFrom.getId() + ' is not visible');
return null;
}
if (toIndex < 0) {
console.warn('ag-Grid: column ' + columnTo.getId() + ' is not visible');
return null;
}
let firstIndex = Math.min(fromIndex, toIndex);
let lastIndex = Math.max(fromIndex, toIndex);
let columns: Column[] = [];
for (let i = firstIndex; i<=lastIndex; i++) {
columns.push(allColumns[i]);
}
return columns;
}
}
class AutoScrollService {
private tickingInterval: number = null;
private tickLeft: boolean;
private tickRight: boolean;
private tickUp: boolean;
private tickDown: boolean;
private gridPanel: GridPanel;
private gridOptionsWrapper: GridOptionsWrapper;
private tickCount: number;
constructor(gridPanel: GridPanel, gridOptionsWrapper: GridOptionsWrapper) {
this.gridPanel = gridPanel;
this.gridOptionsWrapper = gridOptionsWrapper;
}
public check(mouseEvent: MouseEvent): void {
// we don't do ticking if grid is auto height
if (this.gridOptionsWrapper.getDomLayout()!==Constants.DOM_LAYOUT_NORMAL) { return; }
let rect: ClientRect = this.gridPanel.getBodyClientRect();
this.tickLeft = mouseEvent.clientX < (rect.left + 20);
this.tickRight = mouseEvent.clientX > (rect.right - 20);
this.tickUp = mouseEvent.clientY < (rect.top + 20);
this.tickDown = mouseEvent.clientY > (rect.bottom - 20);
if (this.tickLeft || this.tickRight || this.tickUp || this.tickDown) {
this.ensureTickingStarted();
} else {
this.ensureCleared();
}
}
private ensureTickingStarted(): void {
if (this.tickingInterval===null) {
this.tickingInterval = setInterval(this.doTick.bind(this), 100);
this.tickCount = 0;
}
}
private doTick(): void {
this.tickCount++;
let vScrollPosition = this.gridPanel.getVScrollPosition();
let hScrollPosition = this.gridPanel.getHScrollPosition();
let tickAmount: number;
if (this.tickCount > 20) {
tickAmount = 200;
} else if (this.tickCount > 10) {
tickAmount = 80;
} else {
tickAmount = 40;
}
if (this.tickUp) {
this.gridPanel.setVerticalScrollPosition(vScrollPosition.top - tickAmount);
}
if (this.tickDown) {
this.gridPanel.setVerticalScrollPosition(vScrollPosition.top + tickAmount);
}
if (this.tickLeft) {
this.gridPanel.setHorizontalScrollPosition(hScrollPosition.left - tickAmount);
}
if (this.tickRight) {
this.gridPanel.setHorizontalScrollPosition(hScrollPosition.left + tickAmount);
}
}
public ensureCleared(): void {
if (this.tickingInterval) {
clearInterval(this.tickingInterval);
this.tickingInterval = null;
}
}
}