UNPKG

ag-grid

Version:

Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components

400 lines (327 loc) 17.7 kB
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 { @Autowired('loggerFactory') private loggerFactory: LoggerFactory; @Autowired('columnController') private columnController: ColumnController; @Autowired('dragAndDropService') private dragAndDropService: DragAndDropService; @Autowired('gridOptionsWrapper') 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; } @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.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(); } } } } }