ag-grid
Version:
Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components
400 lines (327 loc) • 17.7 kB
text/typescript
import {Autowired, PostConstruct} from "../context/context";
import {Logger, LoggerFactory} from "../logger";
import {ColumnController} from "../columnController/columnController";
import {Column} from "../entities/column";
import {Utils as _} from "../utils";
import {
DragAndDropService, DraggingEvent, DragItem, DragSourceType,
HDirection
} from "../dragAndDrop/dragAndDropService";
import {GridPanel} from "../gridPanel/gridPanel";
import {GridOptionsWrapper} from "../gridOptionsWrapper";
import {DropListener} from "./bodyDropTarget";
import {ColumnEventType} from "../events";
export class MoveColumnController implements DropListener {
private loggerFactory: LoggerFactory;
private columnController: ColumnController;
private dragAndDropService: DragAndDropService;
private gridOptionsWrapper: GridOptionsWrapper;
private gridPanel: GridPanel;
private needToMoveLeft = false;
private needToMoveRight = false;
private movingIntervalId: number;
private intervalCount: number;
private logger: Logger;
private pinned: string;
private centerContainer: boolean;
private lastDraggingEvent: DraggingEvent;
// this counts how long the user has been trying to scroll by dragging and failing,
// if they fail x amount of times, then the column will get pinned. this is what gives
// the 'hold and pin' functionality
private failedMoveAttempts: number;
private eContainer: HTMLElement;
constructor(pinned: string, eContainer: HTMLElement) {
this.pinned = pinned;
this.eContainer = eContainer;
this.centerContainer = !_.exists(pinned);
}
public registerGridComp(gridPanel: GridPanel): void {
this.gridPanel = gridPanel;
}
public init(): void {
this.logger = this.loggerFactory.create('MoveColumnController');
}
public getIconName(): string {
return this.pinned ? DragAndDropService.ICON_PINNED : DragAndDropService.ICON_MOVE;
}
public onDragEnter(draggingEvent: DraggingEvent): void {
// we do dummy drag, so make sure column appears in the right location when first placed
let columns = draggingEvent.dragItem.columns;
let dragCameFromToolPanel = draggingEvent.dragSource.type===DragSourceType.ToolPanel;
if (dragCameFromToolPanel) {
// the if statement doesn't work if drag leaves grid, then enters again
this.setColumnsVisible(columns, true, "uiColumnDragged");
} else {
// restore previous state of visible columns upon re-entering. this means if the user drags
// a group out, and then drags the group back in, only columns that were originally visible
// will be visible again. otherwise a group with three columns (but only two visible) could
// be dragged out, then when it's dragged in again, all three are visible. this stops that.
let visibleState = draggingEvent.dragItem.visibleState;
let visibleColumns: Column[] = columns.filter(column => visibleState[column.getId()] );
this.setColumnsVisible(visibleColumns, true, "uiColumnDragged");
}
this.setColumnsPinned(columns, this.pinned, "uiColumnDragged");
this.onDragging(draggingEvent, true);
}
public onDragLeave(draggingEvent: DraggingEvent): void {
let hideColumnOnExit = !this.gridOptionsWrapper.isSuppressDragLeaveHidesColumns() && !draggingEvent.fromNudge;
if (hideColumnOnExit) {
let dragItem = draggingEvent.dragSource.dragItemCallback();
let columns = dragItem.columns;
this.setColumnsVisible(columns, false, "uiColumnDragged");
}
this.ensureIntervalCleared();
}
public setColumnsVisible(columns: Column[], visible: boolean, source: ColumnEventType = "api") {
if (columns) {
let allowedCols = columns.filter( c => !c.isLockVisible() );
this.columnController.setColumnsVisible(allowedCols, visible, source);
}
}
public setColumnsPinned(columns: Column[], pinned: string, source: ColumnEventType = "api") {
if (columns) {
let allowedCols = columns.filter( c => !c.isLockPinned() );
this.columnController.setColumnsPinned(allowedCols, pinned, source);
}
}
public onDragStop(): void {
this.ensureIntervalCleared();
}
private normaliseX(x: number): number {
// flip the coordinate if doing RTL
let flipHorizontallyForRtl = this.gridOptionsWrapper.isEnableRtl();
if (flipHorizontallyForRtl) {
let clientWidth = this.eContainer.clientWidth;
x = clientWidth - x;
}
// adjust for scroll only if centre container (the pinned containers dont scroll)
let adjustForScroll = this.centerContainer;
if (adjustForScroll) {
x += this.gridPanel.getBodyViewportScrollLeft();
}
return x;
}
private checkCenterForScrolling(xAdjustedForScroll: number): void {
if (this.centerContainer) {
// scroll if the mouse has gone outside the grid (or just outside the scrollable part if pinning)
// putting in 50 buffer, so even if user gets to edge of grid, a scroll will happen
let firstVisiblePixel = this.gridPanel.getBodyViewportScrollLeft();
let lastVisiblePixel = firstVisiblePixel + this.gridPanel.getCenterWidth();
if (this.gridOptionsWrapper.isEnableRtl()) {
this.needToMoveRight = xAdjustedForScroll < (firstVisiblePixel + 50);
this.needToMoveLeft = xAdjustedForScroll > (lastVisiblePixel - 50);
} else {
this.needToMoveLeft = xAdjustedForScroll < (firstVisiblePixel + 50);
this.needToMoveRight = xAdjustedForScroll > (lastVisiblePixel - 50);
}
if (this.needToMoveLeft || this.needToMoveRight) {
this.ensureIntervalStarted();
} else {
this.ensureIntervalCleared();
}
}
}
public onDragging(draggingEvent: DraggingEvent, fromEnter = false): void {
this.lastDraggingEvent = draggingEvent;
// if moving up or down (ie not left or right) then do nothing
if (_.missing(draggingEvent.hDirection)) {
return;
}
let xNormalised = this.normaliseX(draggingEvent.x);
// if the user is dragging into the panel, ie coming from the side panel into the main grid,
// we don't want to scroll the grid this time, it would appear like the table is jumping
// each time a column is dragged in.
if (!fromEnter) {
this.checkCenterForScrolling(xNormalised);
}
let hDirectionNormalised = this.normaliseDirection(draggingEvent.hDirection);
let dragSourceType: DragSourceType = draggingEvent.dragSource.type;
let columnsToMove = draggingEvent.dragSource.dragItemCallback().columns;
columnsToMove = columnsToMove.filter( col => {
if (col.isLockPinned()) {
// if locked return true only if both col and container are same pin type.
// double equals (==) here on purpose so that null==undefined is true (for not pinned options)
return col.getPinned() == this.pinned;
} else {
// if not pin locked, then always allowed to be in this container
return true;
}
});
this.attemptMoveColumns(dragSourceType, columnsToMove, hDirectionNormalised, xNormalised, fromEnter);
}
private normaliseDirection(hDirection: HDirection): HDirection {
if (this.gridOptionsWrapper.isEnableRtl()) {
switch (hDirection) {
case HDirection.Left: return HDirection.Right;
case HDirection.Right: return HDirection.Left;
default: console.error(`ag-Grid: Unknown direction ${hDirection}`);
}
} else {
return hDirection;
}
}
// returns the index of the first column in the list ONLY if the cols are all beside
// each other. if the cols are not beside each other, then returns null
private calculateOldIndex(movingCols: Column[]): number {
let gridCols: Column[] = this.columnController.getAllGridColumns();
let indexes: number[] = [];
movingCols.forEach( col => indexes.push(gridCols.indexOf(col)));
_.sortNumberArray(indexes);
let firstIndex = indexes[0];
let lastIndex = indexes[indexes.length-1];
let spread = lastIndex - firstIndex;
let gapsExist = spread !== indexes.length - 1;
return gapsExist ? null : firstIndex;
}
private attemptMoveColumns(dragSourceType: DragSourceType, allMovingColumns: Column[], hDirection: HDirection, xAdjusted: number, fromEnter: boolean): void {
let draggingLeft = hDirection === HDirection.Left;
let draggingRight = hDirection === HDirection.Right;
let validMoves: number[] = this.calculateValidMoves(allMovingColumns, draggingRight, xAdjusted);
// if cols are not adjacent, then this returns null. when moving, we constrain the direction of the move
// (ie left or right) to the mouse direction. however
let oldIndex = this.calculateOldIndex(allMovingColumns);
// fromEnter = false;
for (let i = 0; i<validMoves.length; i++) {
let newIndex: number = validMoves[i];
// the two check below stop an error when the user grabs a group my a middle column, then
// it is possible the mouse pointer is to the right of a column while been dragged left.
// so we need to make sure that the mouse pointer is actually left of the left most column
// if moving left, and right of the right most column if moving right
// we check 'fromEnter' below so we move the column to the new spot if the mouse is coming from
// outside the grid, eg if the column is moving from side panel, mouse is moving left, then we should
// place the column to the RHS even if the mouse is moving left and the column is already on
// the LHS. otherwise we stick to the rule described above.
let constrainDirection = oldIndex !== null && !fromEnter;
// don't consider 'fromEnter' when dragging header cells, otherwise group can jump to opposite direction of drag
if(dragSourceType == DragSourceType.HeaderCell) {
constrainDirection = oldIndex !== null;
}
if (constrainDirection) {
// only allow left drag if this column is moving left
if (draggingLeft && newIndex>=oldIndex) { continue; }
// only allow right drag if this column is moving right
if (draggingRight && newIndex<=oldIndex) { continue; }
}
if (!this.columnController.doesMovePassRules(allMovingColumns, newIndex)) {
continue;
}
this.columnController.moveColumns(allMovingColumns, newIndex, "uiColumnDragged");
// important to return here, so once we do the first valid move, we don't try do any more
return;
}
}
private calculateValidMoves(movingCols: Column[], draggingRight: boolean, x: number): number[] {
// this is the list of cols on the screen, so it's these we use when comparing the x mouse position
let allDisplayedCols = this.columnController.getDisplayedColumns(this.pinned);
// but this list is the list of all cols, when we move a col it's the index within this list that gets used,
// so the result we return has to be and index location for this list
let allGridCols = this.columnController.getAllGridColumns();
let colIsMovingFunc = (col: Column) => movingCols.indexOf(col) >= 0;
let colIsNotMovingFunc = (col: Column) => movingCols.indexOf(col) < 0;
let movingDisplayedCols = allDisplayedCols.filter(colIsMovingFunc);
let otherDisplayedCols = allDisplayedCols.filter(colIsNotMovingFunc);
let otherGridCols = allGridCols.filter(colIsNotMovingFunc);
// work out how many DISPLAYED columns fit before the 'x' position. this gives us the displayIndex.
// for example, if cols are a,b,c,d and we find a,b fit before 'x', then we want to place the moving
// col between b and c (so that it is under the mouse position).
let displayIndex = 0;
let availableWidth = x;
// if we are dragging right, then the columns will be to the left of the mouse, so we also want to
// include the width of the moving columns
if (draggingRight) {
let widthOfMovingDisplayedCols = 0;
movingDisplayedCols.forEach( col => widthOfMovingDisplayedCols += col.getActualWidth() );
availableWidth -= widthOfMovingDisplayedCols;
}
// now count how many of the displayed columns will fit to the left
for (let i = 0; i < otherDisplayedCols.length; i++) {
let col = otherDisplayedCols[i];
availableWidth -= col.getActualWidth();
if (availableWidth < 0) { break; }
displayIndex++;
}
// trial and error, if going right, we adjust by one, i didn't manage to quantify why, but it works
if (draggingRight) {
displayIndex++;
}
// the display index is with respect to all the showing columns, however when we move, it's with
// respect to all grid columns, so we need to translate from display index to grid index
let gridColIndex: number;
if (displayIndex > 0) {
let leftColumn = otherDisplayedCols[displayIndex-1];
gridColIndex = otherGridCols.indexOf(leftColumn) + 1;
} else {
gridColIndex = 0;
}
let validMoves = [gridColIndex];
// add in all adjacent empty columns as other valid moves. this allows us to try putting the new
// column in any place of a hidden column, to try different combinations so that we don't break
// married children. in other words, maybe the new index breaks a group, but only because some
// columns are hidden, maybe we can reshuffle the hidden columns to find a place that works.
let nextCol = allGridCols[gridColIndex];
while (_.exists(nextCol) && this.isColumnHidden(allDisplayedCols, nextCol)) {
gridColIndex++;
validMoves.push(gridColIndex);
nextCol = allGridCols[gridColIndex];
}
return validMoves;
}
// isHidden takes into account visible=false and group=closed, ie it is not displayed
private isColumnHidden(displayedColumns: Column[], col: Column) {
return displayedColumns.indexOf(col)<0;
}
private ensureIntervalStarted(): void {
if (!this.movingIntervalId) {
this.intervalCount = 0;
this.failedMoveAttempts = 0;
this.movingIntervalId = setInterval(this.moveInterval.bind(this), 100);
if (this.needToMoveLeft) {
this.dragAndDropService.setGhostIcon(DragAndDropService.ICON_LEFT, true);
} else {
this.dragAndDropService.setGhostIcon(DragAndDropService.ICON_RIGHT, true);
}
}
}
private ensureIntervalCleared(): void {
if (this.moveInterval) {
clearInterval(this.movingIntervalId);
this.movingIntervalId = null;
this.dragAndDropService.setGhostIcon(DragAndDropService.ICON_MOVE);
}
}
private moveInterval(): void {
// the amounts we move get bigger at each interval, so the speed accelerates, starting a bit slow
// and getting faster. this is to give smoother user experience. we max at 100px to limit the speed.
let pixelsToMove: number;
this.intervalCount++;
pixelsToMove = 10 + (this.intervalCount * 5);
if (pixelsToMove > 100) {
pixelsToMove = 100;
}
let pixelsMoved: number;
if (this.needToMoveLeft) {
pixelsMoved = this.gridPanel.scrollHorizontally(-pixelsToMove);
} else if (this.needToMoveRight) {
pixelsMoved = this.gridPanel.scrollHorizontally(pixelsToMove);
}
if (pixelsMoved !== 0) {
this.onDragging(this.lastDraggingEvent);
this.failedMoveAttempts = 0;
} else {
// we count the failed move attempts. if we fail to move 7 times, then we pin the column.
// this is how we achieve pining by dragging the column to the edge of the grid.
this.failedMoveAttempts++;
let columns = this.lastDraggingEvent.dragItem.columns;
let columnsThatCanPin = columns.filter( c => !c.isLockPinned() );
if (columnsThatCanPin.length > 0) {
this.dragAndDropService.setGhostIcon(DragAndDropService.ICON_PINNED);
if (this.failedMoveAttempts > 7) {
let pinType = this.needToMoveLeft ? Column.PINNED_LEFT : Column.PINNED_RIGHT;
this.setColumnsPinned(columnsThatCanPin, pinType, "uiColumnDragged");
this.dragAndDropService.nudge();
}
}
}
}
}