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"; export class MoveColumnController { @Autowired('loggerFactory') private loggerFactory: LoggerFactory; @Autowired('columnController') private columnController: ColumnController; @Autowired('gridPanel') private gridPanel: GridPanel; @Autowired('dragAndDropService') private dragAndDropService: DragAndDropService; @Autowired('gridOptionsWrapper') private gridOptionsWrapper: GridOptionsWrapper; 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; public constructor(pinned: string, eContainer: HTMLElement) { this.pinned = pinned; this.eContainer = eContainer; this.centerContainer = !_.exists(pinned); } @PostConstruct 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.columnController.setColumnsVisible(columns, true); } else { // restore previous state of visible columns upon re-entering let visibleState = draggingEvent.dragItem.visibleState; let visibleColumns: Column[] = columns.filter(column => visibleState[column.getId()] ); this.columnController.setColumnsVisible(visibleColumns, true); } this.columnController.setColumnsPinned(columns, this.pinned); 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.columnController.setColumnsVisible(columns, false); } this.ensureIntervalCleared(); } 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; 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=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); // 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 { 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 { this.failedMoveAttempts++; this.dragAndDropService.setGhostIcon(DragAndDropService.ICON_PINNED); if (this.failedMoveAttempts > 7) { let columns = this.lastDraggingEvent.dragItem.columns; let pinType = this.needToMoveLeft ? Column.PINNED_LEFT : Column.PINNED_RIGHT; this.columnController.setColumnsPinned(columns, pinType); this.dragAndDropService.nudge(); } } } }