UNPKG

17.7 kBPlain TextView Raw
1import {Autowired, PostConstruct} from "../context/context";
2import {Logger, LoggerFactory} from "../logger";
3import {ColumnController} from "../columnController/columnController";
4import {Column} from "../entities/column";
5import {Utils as _} from "../utils";
6import {
7 DragAndDropService, DraggingEvent, DragItem, DragSourceType,
8 HDirection
9} from "../dragAndDrop/dragAndDropService";
10import {GridPanel} from "../gridPanel/gridPanel";
11import {GridOptionsWrapper} from "../gridOptionsWrapper";
12import {DropListener} from "./bodyDropTarget";
13import {ColumnEventType} from "../events";
14
15export class MoveColumnController implements DropListener {
16
17 @Autowired('loggerFactory') private loggerFactory: LoggerFactory;
18 @Autowired('columnController') private columnController: ColumnController;
19 @Autowired('dragAndDropService') private dragAndDropService: DragAndDropService;
20 @Autowired('gridOptionsWrapper') private gridOptionsWrapper: GridOptionsWrapper;
21
22 private gridPanel: GridPanel;
23
24 private needToMoveLeft = false;
25 private needToMoveRight = false;
26 private movingIntervalId: number;
27 private intervalCount: number;
28
29 private logger: Logger;
30 private pinned: string;
31 private centerContainer: boolean;
32
33 private lastDraggingEvent: DraggingEvent;
34
35 // this counts how long the user has been trying to scroll by dragging and failing,
36 // if they fail x amount of times, then the column will get pinned. this is what gives
37 // the 'hold and pin' functionality
38 private failedMoveAttempts: number;
39
40 private eContainer: HTMLElement;
41
42 constructor(pinned: string, eContainer: HTMLElement) {
43 this.pinned = pinned;
44 this.eContainer = eContainer;
45 this.centerContainer = !_.exists(pinned);
46 }
47
48 public registerGridComp(gridPanel: GridPanel): void {
49 this.gridPanel = gridPanel;
50 }
51
52 @PostConstruct
53 public init(): void {
54 this.logger = this.loggerFactory.create('MoveColumnController');
55 }
56
57 public getIconName(): string {
58 return this.pinned ? DragAndDropService.ICON_PINNED : DragAndDropService.ICON_MOVE;
59 }
60
61 public onDragEnter(draggingEvent: DraggingEvent): void {
62 // we do dummy drag, so make sure column appears in the right location when first placed
63
64 let columns = draggingEvent.dragItem.columns;
65 let dragCameFromToolPanel = draggingEvent.dragSource.type===DragSourceType.ToolPanel;
66 if (dragCameFromToolPanel) {
67 // the if statement doesn't work if drag leaves grid, then enters again
68 this.setColumnsVisible(columns, true, "uiColumnDragged");
69 } else {
70 // restore previous state of visible columns upon re-entering. this means if the user drags
71 // a group out, and then drags the group back in, only columns that were originally visible
72 // will be visible again. otherwise a group with three columns (but only two visible) could
73 // be dragged out, then when it's dragged in again, all three are visible. this stops that.
74 let visibleState = draggingEvent.dragItem.visibleState;
75 let visibleColumns: Column[] = columns.filter(column => visibleState[column.getId()] );
76 this.setColumnsVisible(visibleColumns, true, "uiColumnDragged");
77 }
78
79 this.setColumnsPinned(columns, this.pinned, "uiColumnDragged");
80 this.onDragging(draggingEvent, true);
81 }
82
83 public onDragLeave(draggingEvent: DraggingEvent): void {
84 let hideColumnOnExit = !this.gridOptionsWrapper.isSuppressDragLeaveHidesColumns() && !draggingEvent.fromNudge;
85 if (hideColumnOnExit) {
86 let dragItem = draggingEvent.dragSource.dragItemCallback();
87 let columns = dragItem.columns;
88 this.setColumnsVisible(columns, false, "uiColumnDragged");
89 }
90 this.ensureIntervalCleared();
91 }
92
93 public setColumnsVisible(columns: Column[], visible: boolean, source: ColumnEventType = "api") {
94 if (columns) {
95 let allowedCols = columns.filter( c => !c.isLockVisible() );
96 this.columnController.setColumnsVisible(allowedCols, visible, source);
97 }
98 }
99
100 public setColumnsPinned(columns: Column[], pinned: string, source: ColumnEventType = "api") {
101 if (columns) {
102 let allowedCols = columns.filter( c => !c.isLockPinned() );
103 this.columnController.setColumnsPinned(allowedCols, pinned, source);
104 }
105 }
106
107 public onDragStop(): void {
108 this.ensureIntervalCleared();
109 }
110
111 private normaliseX(x: number): number {
112
113 // flip the coordinate if doing RTL
114 let flipHorizontallyForRtl = this.gridOptionsWrapper.isEnableRtl();
115 if (flipHorizontallyForRtl) {
116 let clientWidth = this.eContainer.clientWidth;
117 x = clientWidth - x;
118 }
119
120 // adjust for scroll only if centre container (the pinned containers dont scroll)
121 let adjustForScroll = this.centerContainer;
122 if (adjustForScroll) {
123 x += this.gridPanel.getBodyViewportScrollLeft();
124 }
125
126 return x;
127 }
128
129 private checkCenterForScrolling(xAdjustedForScroll: number): void {
130 if (this.centerContainer) {
131 // scroll if the mouse has gone outside the grid (or just outside the scrollable part if pinning)
132 // putting in 50 buffer, so even if user gets to edge of grid, a scroll will happen
133 let firstVisiblePixel = this.gridPanel.getBodyViewportScrollLeft();
134 let lastVisiblePixel = firstVisiblePixel + this.gridPanel.getCenterWidth();
135
136 if (this.gridOptionsWrapper.isEnableRtl()) {
137 this.needToMoveRight = xAdjustedForScroll < (firstVisiblePixel + 50);
138 this.needToMoveLeft = xAdjustedForScroll > (lastVisiblePixel - 50);
139 } else {
140 this.needToMoveLeft = xAdjustedForScroll < (firstVisiblePixel + 50);
141 this.needToMoveRight = xAdjustedForScroll > (lastVisiblePixel - 50);
142 }
143
144 if (this.needToMoveLeft || this.needToMoveRight) {
145 this.ensureIntervalStarted();
146 } else {
147 this.ensureIntervalCleared();
148 }
149 }
150 }
151
152 public onDragging(draggingEvent: DraggingEvent, fromEnter = false): void {
153
154 this.lastDraggingEvent = draggingEvent;
155
156 // if moving up or down (ie not left or right) then do nothing
157 if (_.missing(draggingEvent.hDirection)) {
158 return;
159 }
160
161 let xNormalised = this.normaliseX(draggingEvent.x);
162
163 // if the user is dragging into the panel, ie coming from the side panel into the main grid,
164 // we don't want to scroll the grid this time, it would appear like the table is jumping
165 // each time a column is dragged in.
166 if (!fromEnter) {
167 this.checkCenterForScrolling(xNormalised);
168 }
169
170 let hDirectionNormalised = this.normaliseDirection(draggingEvent.hDirection);
171
172 let dragSourceType: DragSourceType = draggingEvent.dragSource.type;
173 let columnsToMove = draggingEvent.dragSource.dragItemCallback().columns;
174
175 columnsToMove = columnsToMove.filter( col => {
176 if (col.isLockPinned()) {
177 // if locked return true only if both col and container are same pin type.
178 // double equals (==) here on purpose so that null==undefined is true (for not pinned options)
179 return col.getPinned() == this.pinned;
180 } else {
181 // if not pin locked, then always allowed to be in this container
182 return true;
183 }
184 });
185
186 this.attemptMoveColumns(dragSourceType, columnsToMove, hDirectionNormalised, xNormalised, fromEnter);
187 }
188
189 private normaliseDirection(hDirection: HDirection): HDirection {
190 if (this.gridOptionsWrapper.isEnableRtl()) {
191 switch (hDirection) {
192 case HDirection.Left: return HDirection.Right;
193 case HDirection.Right: return HDirection.Left;
194 default: console.error(`ag-Grid: Unknown direction ${hDirection}`);
195 }
196 } else {
197 return hDirection;
198 }
199 }
200
201 // returns the index of the first column in the list ONLY if the cols are all beside
202 // each other. if the cols are not beside each other, then returns null
203 private calculateOldIndex(movingCols: Column[]): number {
204 let gridCols: Column[] = this.columnController.getAllGridColumns();
205 let indexes: number[] = [];
206 movingCols.forEach( col => indexes.push(gridCols.indexOf(col)));
207 _.sortNumberArray(indexes);
208 let firstIndex = indexes[0];
209 let lastIndex = indexes[indexes.length-1];
210 let spread = lastIndex - firstIndex;
211 let gapsExist = spread !== indexes.length - 1;
212 return gapsExist ? null : firstIndex;
213 }
214
215 private attemptMoveColumns(dragSourceType: DragSourceType, allMovingColumns: Column[], hDirection: HDirection, xAdjusted: number, fromEnter: boolean): void {
216
217 let draggingLeft = hDirection === HDirection.Left;
218 let draggingRight = hDirection === HDirection.Right;
219
220 let validMoves: number[] = this.calculateValidMoves(allMovingColumns, draggingRight, xAdjusted);
221
222 // if cols are not adjacent, then this returns null. when moving, we constrain the direction of the move
223 // (ie left or right) to the mouse direction. however
224 let oldIndex = this.calculateOldIndex(allMovingColumns);
225
226 // fromEnter = false;
227
228 for (let i = 0; i<validMoves.length; i++) {
229 let newIndex: number = validMoves[i];
230
231 // the two check below stop an error when the user grabs a group my a middle column, then
232 // it is possible the mouse pointer is to the right of a column while been dragged left.
233 // so we need to make sure that the mouse pointer is actually left of the left most column
234 // if moving left, and right of the right most column if moving right
235
236 // we check 'fromEnter' below so we move the column to the new spot if the mouse is coming from
237 // outside the grid, eg if the column is moving from side panel, mouse is moving left, then we should
238 // place the column to the RHS even if the mouse is moving left and the column is already on
239 // the LHS. otherwise we stick to the rule described above.
240
241 let constrainDirection = oldIndex !== null && !fromEnter;
242
243 // don't consider 'fromEnter' when dragging header cells, otherwise group can jump to opposite direction of drag
244 if(dragSourceType == DragSourceType.HeaderCell) {
245 constrainDirection = oldIndex !== null;
246 }
247
248 if (constrainDirection) {
249 // only allow left drag if this column is moving left
250 if (draggingLeft && newIndex>=oldIndex) { continue; }
251
252 // only allow right drag if this column is moving right
253 if (draggingRight && newIndex<=oldIndex) { continue; }
254 }
255
256 if (!this.columnController.doesMovePassRules(allMovingColumns, newIndex)) {
257 continue;
258 }
259
260 this.columnController.moveColumns(allMovingColumns, newIndex, "uiColumnDragged");
261
262 // important to return here, so once we do the first valid move, we don't try do any more
263 return;
264 }
265 }
266
267 private calculateValidMoves(movingCols: Column[], draggingRight: boolean, x: number): number[] {
268
269 // this is the list of cols on the screen, so it's these we use when comparing the x mouse position
270 let allDisplayedCols = this.columnController.getDisplayedColumns(this.pinned);
271 // but this list is the list of all cols, when we move a col it's the index within this list that gets used,
272 // so the result we return has to be and index location for this list
273 let allGridCols = this.columnController.getAllGridColumns();
274
275 let colIsMovingFunc = (col: Column) => movingCols.indexOf(col) >= 0;
276 let colIsNotMovingFunc = (col: Column) => movingCols.indexOf(col) < 0;
277
278 let movingDisplayedCols = allDisplayedCols.filter(colIsMovingFunc);
279 let otherDisplayedCols = allDisplayedCols.filter(colIsNotMovingFunc);
280 let otherGridCols = allGridCols.filter(colIsNotMovingFunc);
281
282 // work out how many DISPLAYED columns fit before the 'x' position. this gives us the displayIndex.
283 // for example, if cols are a,b,c,d and we find a,b fit before 'x', then we want to place the moving
284 // col between b and c (so that it is under the mouse position).
285 let displayIndex = 0;
286 let availableWidth = x;
287
288 // if we are dragging right, then the columns will be to the left of the mouse, so we also want to
289 // include the width of the moving columns
290 if (draggingRight) {
291 let widthOfMovingDisplayedCols = 0;
292 movingDisplayedCols.forEach( col => widthOfMovingDisplayedCols += col.getActualWidth() );
293 availableWidth -= widthOfMovingDisplayedCols;
294 }
295
296 // now count how many of the displayed columns will fit to the left
297 for (let i = 0; i < otherDisplayedCols.length; i++) {
298 let col = otherDisplayedCols[i];
299 availableWidth -= col.getActualWidth();
300 if (availableWidth < 0) { break; }
301 displayIndex++;
302 }
303
304 // trial and error, if going right, we adjust by one, i didn't manage to quantify why, but it works
305 if (draggingRight) {
306 displayIndex++;
307 }
308
309 // the display index is with respect to all the showing columns, however when we move, it's with
310 // respect to all grid columns, so we need to translate from display index to grid index
311
312 let gridColIndex: number;
313 if (displayIndex > 0) {
314 let leftColumn = otherDisplayedCols[displayIndex-1];
315 gridColIndex = otherGridCols.indexOf(leftColumn) + 1;
316 } else {
317 gridColIndex = 0;
318 }
319
320 let validMoves = [gridColIndex];
321
322 // add in all adjacent empty columns as other valid moves. this allows us to try putting the new
323 // column in any place of a hidden column, to try different combinations so that we don't break
324 // married children. in other words, maybe the new index breaks a group, but only because some
325 // columns are hidden, maybe we can reshuffle the hidden columns to find a place that works.
326 let nextCol = allGridCols[gridColIndex];
327 while (_.exists(nextCol) && this.isColumnHidden(allDisplayedCols, nextCol)) {
328 gridColIndex++;
329 validMoves.push(gridColIndex);
330 nextCol = allGridCols[gridColIndex];
331 }
332
333 return validMoves;
334 }
335
336 // isHidden takes into account visible=false and group=closed, ie it is not displayed
337 private isColumnHidden(displayedColumns: Column[], col: Column) {
338 return displayedColumns.indexOf(col)<0;
339 }
340
341 private ensureIntervalStarted(): void {
342 if (!this.movingIntervalId) {
343 this.intervalCount = 0;
344 this.failedMoveAttempts = 0;
345 this.movingIntervalId = setInterval(this.moveInterval.bind(this), 100);
346 if (this.needToMoveLeft) {
347 this.dragAndDropService.setGhostIcon(DragAndDropService.ICON_LEFT, true);
348 } else {
349 this.dragAndDropService.setGhostIcon(DragAndDropService.ICON_RIGHT, true);
350 }
351 }
352 }
353
354 private ensureIntervalCleared(): void {
355 if (this.moveInterval) {
356 clearInterval(this.movingIntervalId);
357 this.movingIntervalId = null;
358 this.dragAndDropService.setGhostIcon(DragAndDropService.ICON_MOVE);
359 }
360 }
361
362 private moveInterval(): void {
363 // the amounts we move get bigger at each interval, so the speed accelerates, starting a bit slow
364 // and getting faster. this is to give smoother user experience. we max at 100px to limit the speed.
365 let pixelsToMove: number;
366 this.intervalCount++;
367 pixelsToMove = 10 + (this.intervalCount * 5);
368 if (pixelsToMove > 100) {
369 pixelsToMove = 100;
370 }
371
372 let pixelsMoved: number;
373 if (this.needToMoveLeft) {
374 pixelsMoved = this.gridPanel.scrollHorizontally(-pixelsToMove);
375 } else if (this.needToMoveRight) {
376 pixelsMoved = this.gridPanel.scrollHorizontally(pixelsToMove);
377 }
378
379 if (pixelsMoved !== 0) {
380 this.onDragging(this.lastDraggingEvent);
381 this.failedMoveAttempts = 0;
382 } else {
383 // we count the failed move attempts. if we fail to move 7 times, then we pin the column.
384 // this is how we achieve pining by dragging the column to the edge of the grid.
385 this.failedMoveAttempts++;
386
387 let columns = this.lastDraggingEvent.dragItem.columns;
388 let columnsThatCanPin = columns.filter( c => !c.isLockPinned() );
389
390 if (columnsThatCanPin.length > 0) {
391 this.dragAndDropService.setGhostIcon(DragAndDropService.ICON_PINNED);
392 if (this.failedMoveAttempts > 7) {
393 let pinType = this.needToMoveLeft ? Column.PINNED_LEFT : Column.PINNED_RIGHT;
394 this.setColumnsPinned(columnsThatCanPin, pinType, "uiColumnDragged");
395 this.dragAndDropService.nudge();
396 }
397 }
398 }
399 }
400}
\No newline at end of file