UNPKG

17.4 kBPlain TextView Raw
1import {Logger, LoggerFactory} from "../logger";
2import {Qualifier, PostConstruct, Bean, Autowired, PreDestroy} from "../context/context";
3import {Column} from "../entities/column";
4import {Utils as _} from "../utils";
5import {GridOptionsWrapper} from "../gridOptionsWrapper";
6import {DragService, DragListenerParams} from "./dragService";
7import {ColumnController} from "../columnController/columnController";
8import {Environment} from "../environment";
9import {RowNode} from "../entities/rowNode";
10
11export enum DragSourceType { ToolPanel, HeaderCell, RowDrag }
12
13export interface DragItem {
14 // if moving a row, the, this contains the row node
15 rowNode?: RowNode;
16 // if moving columns, this contains the columns and the visible state
17 columns?: Column[];
18 visibleState?: {[key: string]: boolean};
19}
20
21export interface DragSource {
22 /** So the drop target knows what type of event it is, useful for columns,
23 * we we re-ordering or moving dropping from toolPanel */
24 type: DragSourceType;
25 /** Element which, when dragged, will kick off the DnD process */
26 eElement: HTMLElement;
27 /** If eElement is dragged, then the dragItem is the object that gets passed around. */
28 dragItemCallback: () => DragItem;
29 /** This name appears in the ghost icon when dragging */
30 dragItemName: string;
31 /** The drop target associated with this dragSource. So when dragging starts, this target does not get
32 * onDragEnter event. */
33 dragSourceDropTarget?: DropTarget;
34 /** After how many pixels of dragging should the drag operation start. Default is 4px. */
35 dragStartPixels?: number;
36 /** Callback for drag started */
37 dragStarted?: ()=>void;
38 /** Callback for drag stopped */
39 dragStopped?: ()=>void;
40}
41
42export interface DropTarget {
43 /** The main container that will get the drop. */
44 getContainer(): HTMLElement;
45 /** If any secondary containers. For example when moving columns in ag-Grid, we listen for drops
46 * in the header as well as the body (main rows and pinned rows) of the grid. */
47 getSecondaryContainers?(): HTMLElement[];
48 /** Icon to show when drag is over*/
49 getIconName?(): string;
50
51 isInterestedIn(type: DragSourceType): boolean;
52
53 /** Callback for when drag enters */
54 onDragEnter?(params: DraggingEvent): void;
55 /** Callback for when drag leaves */
56 onDragLeave?(params: DraggingEvent): void;
57 /** Callback for when dragging */
58 onDragging?(params: DraggingEvent): void;
59 /** Callback for when drag stops */
60 onDragStop?(params: DraggingEvent): void;
61}
62
63export enum VDirection {Up, Down}
64
65export enum HDirection {Left, Right}
66
67export interface DraggingEvent {
68 event: MouseEvent;
69 x: number;
70 y: number;
71 vDirection: VDirection;
72 hDirection: HDirection;
73 dragSource: DragSource;
74 dragItem: DragItem;
75 fromNudge: boolean;
76}
77
78@Bean('dragAndDropService')
79export class DragAndDropService {
80
81 @Autowired('gridOptionsWrapper') private gridOptionsWrapper: GridOptionsWrapper;
82 @Autowired('dragService') private dragService: DragService;
83 @Autowired('environment') private environment: Environment;
84 @Autowired('columnController') private columnController: ColumnController;
85
86 public static ICON_PINNED = 'pinned';
87 public static ICON_ADD = 'add';
88 public static ICON_MOVE = 'move';
89 public static ICON_LEFT = 'left';
90 public static ICON_RIGHT = 'right';
91 public static ICON_GROUP = 'group';
92 public static ICON_AGGREGATE = 'aggregate';
93 public static ICON_PIVOT = 'pivot';
94 public static ICON_NOT_ALLOWED = 'notAllowed';
95
96 public static GHOST_TEMPLATE =
97 '<div class="ag-dnd-ghost">' +
98 ' <span class="ag-dnd-ghost-icon ag-shake-left-to-right"></span>' +
99 ' <div class="ag-dnd-ghost-label">' +
100 ' </div>' +
101 '</div>';
102
103 private logger: Logger;
104
105 private dragSourceAndParamsList: {params: DragListenerParams, dragSource: DragSource}[] = [];
106
107 private dragItem: DragItem;
108 private eventLastTime: MouseEvent;
109 private dragSource: DragSource;
110 private dragging: boolean;
111
112 private eGhost: HTMLElement;
113 private eGhostParent: HTMLElement;
114 private eGhostIcon: HTMLElement;
115
116 private dropTargets: DropTarget[] = [];
117 private lastDropTarget: DropTarget;
118
119 private ePinnedIcon: HTMLElement;
120 private ePlusIcon: HTMLElement;
121 private eHiddenIcon: HTMLElement;
122 private eMoveIcon: HTMLElement;
123 private eLeftIcon: HTMLElement;
124 private eRightIcon: HTMLElement;
125 private eGroupIcon: HTMLElement;
126 private eAggregateIcon: HTMLElement;
127 private ePivotIcon: HTMLElement;
128 private eDropNotAllowedIcon: HTMLElement;
129
130 @PostConstruct
131 private init(): void {
132 this.ePinnedIcon = _.createIcon('columnMovePin', this.gridOptionsWrapper, null);
133 this.ePlusIcon = _.createIcon('columnMoveAdd', this.gridOptionsWrapper, null);
134 this.eHiddenIcon = _.createIcon('columnMoveHide', this.gridOptionsWrapper, null);
135 this.eMoveIcon = _.createIcon('columnMoveMove', this.gridOptionsWrapper, null);
136 this.eLeftIcon = _.createIcon('columnMoveLeft', this.gridOptionsWrapper, null);
137 this.eRightIcon = _.createIcon('columnMoveRight', this.gridOptionsWrapper, null);
138 this.eGroupIcon = _.createIcon('columnMoveGroup', this.gridOptionsWrapper, null);
139 this.eAggregateIcon = _.createIcon('columnMoveValue', this.gridOptionsWrapper, null);
140 this.ePivotIcon = _.createIcon('columnMovePivot', this.gridOptionsWrapper, null);
141 this.eDropNotAllowedIcon = _.createIcon('dropNotAllowed', this.gridOptionsWrapper, null);
142 }
143
144 private setBeans(@Qualifier('loggerFactory') loggerFactory: LoggerFactory) {
145 this.logger = loggerFactory.create('OldToolPanelDragAndDropService');
146 }
147
148 private getStringType(type: DragSourceType): string {
149 switch (type) {
150 case DragSourceType.RowDrag: return 'row';
151 case DragSourceType.HeaderCell: return 'headerCell';
152 case DragSourceType.ToolPanel: return 'toolPanel';
153 default:
154 console.warn(`ag-Grid: bug - unknown drag type ${type}`);
155 return null;
156 }
157 }
158
159 public addDragSource(dragSource: DragSource, allowTouch = false): void {
160 let params: DragListenerParams = {
161 eElement: dragSource.eElement,
162 dragStartPixels: dragSource.dragStartPixels,
163 onDragStart: this.onDragStart.bind(this, dragSource),
164 onDragStop: this.onDragStop.bind(this),
165 onDragging: this.onDragging.bind(this)
166 };
167
168 this.dragSourceAndParamsList.push({params: params, dragSource: dragSource});
169
170 this.dragService.addDragSource(params, allowTouch);
171 }
172
173 public removeDragSource(dragSource: DragSource): void {
174 let sourceAndParams = _.find(this.dragSourceAndParamsList, item => item.dragSource === dragSource);
175 if (sourceAndParams) {
176 this.dragService.removeDragSource(sourceAndParams.params);
177 _.removeFromArray(this.dragSourceAndParamsList, sourceAndParams);
178 }
179 }
180
181 @PreDestroy
182 private destroy(): void {
183 this.dragSourceAndParamsList.forEach( sourceAndParams => {
184 this.dragService.removeDragSource(sourceAndParams.params);
185 });
186 this.dragSourceAndParamsList.length = 0;
187 }
188
189 public nudge(): void {
190 if (this.dragging) {
191 this.onDragging(this.eventLastTime, true);
192 }
193 }
194
195 private onDragStart(dragSource: DragSource, mouseEvent: MouseEvent): void {
196 this.dragging = true;
197 this.dragSource = dragSource;
198 this.eventLastTime = mouseEvent;
199 this.dragItem = this.dragSource.dragItemCallback();
200 this.lastDropTarget = this.dragSource.dragSourceDropTarget;
201
202 if (this.dragSource.dragStarted) {
203 this.dragSource.dragStarted();
204 }
205
206 this.createGhost();
207 }
208
209 private onDragStop(mouseEvent: MouseEvent): void {
210 this.eventLastTime = null;
211 this.dragging = false;
212
213 if (this.dragSource.dragStopped) {
214 this.dragSource.dragStopped();
215 }
216 if (this.lastDropTarget && this.lastDropTarget.onDragStop) {
217 let draggingEvent = this.createDropTargetEvent(this.lastDropTarget, mouseEvent, null, null, false);
218 this.lastDropTarget.onDragStop(draggingEvent);
219 }
220 this.lastDropTarget = null;
221 this.dragItem = null;
222 this.removeGhost();
223 }
224
225 private onDragging(mouseEvent: MouseEvent, fromNudge: boolean): void {
226
227 let hDirection = this.workOutHDirection(mouseEvent);
228 let vDirection = this.workOutVDirection(mouseEvent);
229
230 this.eventLastTime = mouseEvent;
231
232 this.positionGhost(mouseEvent);
233
234 // check if mouseEvent intersects with any of the drop targets
235 let dropTarget = _.find(this.dropTargets, this.isMouseOnDropTarget.bind(this, mouseEvent));
236
237 if (dropTarget!==this.lastDropTarget) {
238 this.leaveLastTargetIfExists(mouseEvent, hDirection, vDirection, fromNudge);
239 this.enterDragTargetIfExists(dropTarget, mouseEvent, hDirection, vDirection, fromNudge);
240 this.lastDropTarget = dropTarget;
241 } else if (dropTarget) {
242 let draggingEvent = this.createDropTargetEvent(dropTarget, mouseEvent, hDirection, vDirection, fromNudge);
243 dropTarget.onDragging(draggingEvent);
244 }
245 }
246
247 private enterDragTargetIfExists(dropTarget: DropTarget, mouseEvent: MouseEvent, hDirection: HDirection, vDirection: VDirection, fromNudge: boolean): void {
248 if (!dropTarget) { return; }
249
250 let dragEnterEvent = this.createDropTargetEvent(dropTarget, mouseEvent, hDirection, vDirection, fromNudge);
251 dropTarget.onDragEnter(dragEnterEvent);
252 this.setGhostIcon(dropTarget.getIconName ? dropTarget.getIconName() : null);
253 }
254
255 private leaveLastTargetIfExists(mouseEvent: MouseEvent, hDirection: HDirection, vDirection: VDirection, fromNudge: boolean): void {
256 if (!this.lastDropTarget) { return; }
257
258 let dragLeaveEvent = this.createDropTargetEvent(this.lastDropTarget, mouseEvent, hDirection, vDirection, fromNudge);
259 this.lastDropTarget.onDragLeave(dragLeaveEvent);
260 this.setGhostIcon(null);
261 }
262
263 private getAllContainersFromDropTarget(dropTarget: DropTarget): HTMLElement[] {
264 let containers = [dropTarget.getContainer()];
265 let secondaryContainers = dropTarget.getSecondaryContainers ? dropTarget.getSecondaryContainers() : null;
266 if (secondaryContainers) {
267 containers = containers.concat(secondaryContainers);
268 }
269 return containers;
270 }
271
272 // checks if the mouse is on the drop target. it checks eContainer and eSecondaryContainers
273 private isMouseOnDropTarget(mouseEvent: MouseEvent, dropTarget: DropTarget): boolean {
274 let allContainers = this.getAllContainersFromDropTarget(dropTarget);
275
276 let mouseOverTarget: boolean = false;
277 allContainers.forEach( (eContainer: HTMLElement) => {
278 if (!eContainer) { return; } // secondary can be missing
279 let rect = eContainer.getBoundingClientRect();
280
281 // if element is not visible, then width and height are zero
282 if (rect.width===0 || rect.height===0) {
283 return;
284 }
285 let horizontalFit = mouseEvent.clientX >= rect.left && mouseEvent.clientX <= rect.right;
286 let verticalFit = mouseEvent.clientY >= rect.top && mouseEvent.clientY <= rect.bottom;
287
288 //console.log(`rect.width = ${rect.width} || rect.height = ${rect.height} ## verticalFit = ${verticalFit}, horizontalFit = ${horizontalFit}, `);
289
290 if (horizontalFit && verticalFit) {
291 mouseOverTarget = true;
292 }
293 });
294
295 if (mouseOverTarget) {
296 let mouseOverTargetAndInterested = dropTarget.isInterestedIn(this.dragSource.type);
297 return mouseOverTargetAndInterested;
298 } else {
299 return false;
300 }
301 }
302
303 public addDropTarget(dropTarget: DropTarget) {
304 this.dropTargets.push(dropTarget);
305 }
306
307 public workOutHDirection(event: MouseEvent): HDirection {
308 if (this.eventLastTime.clientX > event.clientX) {
309 return HDirection.Left;
310 } else if (this.eventLastTime.clientX < event.clientX) {
311 return HDirection.Right;
312 } else {
313 return null;
314 }
315 }
316
317 public workOutVDirection(event: MouseEvent): VDirection {
318 if (this.eventLastTime.clientY > event.clientY) {
319 return VDirection.Up;
320 } else if (this.eventLastTime.clientY < event.clientY) {
321 return VDirection.Down;
322 } else {
323 return null;
324 }
325 }
326
327 public createDropTargetEvent(dropTarget: DropTarget, event: MouseEvent, hDirection: HDirection, vDirection: VDirection, fromNudge: boolean): DraggingEvent {
328
329 // localise x and y to the target component
330 let rect = dropTarget.getContainer().getBoundingClientRect();
331 let x = event.clientX - rect.left;
332 let y = event.clientY - rect.top;
333
334 let dropTargetEvent: DraggingEvent = {
335 event: event,
336 x: x,
337 y: y,
338 vDirection: vDirection,
339 hDirection: hDirection,
340 dragSource: this.dragSource,
341 fromNudge: fromNudge,
342 dragItem: this.dragItem
343 };
344
345 return dropTargetEvent;
346 }
347
348 private positionGhost(event: MouseEvent): void {
349 let ghostRect = this.eGhost.getBoundingClientRect();
350 let ghostHeight = ghostRect.height;
351 // for some reason, without the '-2', it still overlapped by 1 or 2 pixels, which
352 // then brought in scrollbars to the browser. no idea why, but putting in -2 here
353 // works around it which is good enough for me.
354 let browserWidth = _.getBodyWidth() - 2;
355 let browserHeight = _.getBodyHeight() - 2;
356
357 // put ghost vertically in middle of cursor
358 let top = event.pageY - (ghostHeight / 2);
359 // horizontally, place cursor just right of icon
360 let left = event.pageX - 30;
361
362 let usrDocument = this.gridOptionsWrapper.getDocument();
363
364 let windowScrollY = window.pageYOffset || usrDocument.documentElement.scrollTop;
365 let windowScrollX = window.pageXOffset || usrDocument.documentElement.scrollLeft;
366
367 // check ghost is not positioned outside of the browser
368 if (browserWidth>0) {
369 if ( (left + this.eGhost.clientWidth) > (browserWidth + windowScrollX) ) {
370 left = browserWidth + windowScrollX - this.eGhost.clientWidth;
371 }
372 }
373 if (left < 0) {
374 left = 0;
375 }
376 if (browserHeight>0) {
377 if ( (top + this.eGhost.clientHeight) > (browserHeight + windowScrollY) ) {
378 top = browserHeight + windowScrollY - this.eGhost.clientHeight;
379 }
380 }
381 if (top < 0) {
382 top = 0;
383 }
384
385 this.eGhost.style.left = left + 'px';
386 this.eGhost.style.top = top + 'px';
387 }
388
389 private removeGhost(): void {
390 if (this.eGhost && this.eGhostParent) {
391 this.eGhostParent.removeChild(this.eGhost);
392 }
393 this.eGhost = null;
394 }
395
396 private createGhost(): void {
397 this.eGhost = _.loadTemplate(DragAndDropService.GHOST_TEMPLATE);
398 _.addCssClass(this.eGhost, this.environment.getTheme());
399 this.eGhostIcon = <HTMLElement> this.eGhost.querySelector('.ag-dnd-ghost-icon');
400
401 this.setGhostIcon(null);
402
403 let eText = <HTMLElement> this.eGhost.querySelector('.ag-dnd-ghost-label');
404 eText.innerHTML = this.dragSource.dragItemName;
405
406 this.eGhost.style.height = '25px';
407
408 this.eGhost.style.top = '20px';
409 this.eGhost.style.left = '20px';
410
411 let usrDocument = this.gridOptionsWrapper.getDocument();
412 this.eGhostParent = <HTMLElement> usrDocument.querySelector('body');
413 if (!this.eGhostParent) {
414 console.warn('ag-Grid: could not find document body, it is needed for dragging columns');
415 } else {
416 this.eGhostParent.appendChild(this.eGhost);
417 }
418
419 }
420
421 public setGhostIcon(iconName: string, shake = false): void {
422 _.removeAllChildren(this.eGhostIcon);
423 let eIcon: HTMLElement;
424 switch (iconName) {
425 case DragAndDropService.ICON_ADD: eIcon = this.ePlusIcon; break;
426 case DragAndDropService.ICON_PINNED: eIcon = this.ePinnedIcon; break;
427 case DragAndDropService.ICON_MOVE: eIcon = this.eMoveIcon; break;
428 case DragAndDropService.ICON_LEFT: eIcon = this.eLeftIcon; break;
429 case DragAndDropService.ICON_RIGHT: eIcon = this.eRightIcon; break;
430 case DragAndDropService.ICON_GROUP: eIcon = this.eGroupIcon; break;
431 case DragAndDropService.ICON_AGGREGATE: eIcon = this.eAggregateIcon; break;
432 case DragAndDropService.ICON_PIVOT: eIcon = this.ePivotIcon; break;
433 case DragAndDropService.ICON_NOT_ALLOWED: eIcon = this.eDropNotAllowedIcon; break;
434 default: eIcon = this.eHiddenIcon; break;
435 }
436 this.eGhostIcon.appendChild(eIcon);
437 _.addOrRemoveCssClass(this.eGhostIcon, 'ag-shake-left-to-right', shake);
438 }
439
440}