UNPKG

12.7 kBPlain TextView Raw
1import {Bean, PreDestroy, Autowired, PostConstruct, Optional} from "../context/context";
2import {LoggerFactory, Logger} from "../logger";
3import {Utils as _} from "../utils";
4import {EventService} from "../eventService";
5import {DragStartedEvent, DragStoppedEvent, Events} from "../events";
6import {GridOptionsWrapper} from "../gridOptionsWrapper";
7import {ColumnApi} from "../columnController/columnApi";
8import {GridApi} from "../gridApi";
9
10/** Adds drag listening onto an element. In ag-Grid this is used twice, first is resizing columns,
11 * second is moving the columns and column groups around (ie the 'drag' part of Drag and Drop. */
12@Bean('dragService')
13export class DragService {
14
15 @Autowired('loggerFactory') private loggerFactory: LoggerFactory;
16 @Autowired('eventService') private eventService: EventService;
17 @Autowired('gridOptionsWrapper') private gridOptionsWrapper: GridOptionsWrapper;
18 @Autowired('columnApi') private columnApi: ColumnApi;
19 @Autowired('gridApi') private gridApi: GridApi;
20
21 private currentDragParams: DragListenerParams;
22 private dragging: boolean;
23 private mouseEventLastTime: MouseEvent;
24 private mouseStartEvent: MouseEvent;
25 private touchLastTime: Touch;
26 private touchStart: Touch;
27
28 private onMouseUpListener = this.onMouseUp.bind(this);
29 private onMouseMoveListener = this.onMouseMove.bind(this);
30
31 private onTouchEndListener = this.onTouchUp.bind(this);
32 private onTouchMoveListener = this.onTouchMove.bind(this);
33
34 private logger: Logger;
35
36 private dragEndFunctions: Function[] = [];
37
38 private dragSources: DragSourceAndListener[] = [];
39
40 @PostConstruct
41 private init(): void {
42 this.logger = this.loggerFactory.create('DragService');
43 }
44
45 @PreDestroy
46 private destroy(): void {
47 this.dragSources.forEach( this.removeListener.bind(this) );
48 this.dragSources.length = 0;
49 }
50
51 private removeListener(dragSourceAndListener: DragSourceAndListener): void {
52 let element = dragSourceAndListener.dragSource.eElement;
53 let mouseDownListener = dragSourceAndListener.mouseDownListener;
54 element.removeEventListener('mousedown', mouseDownListener);
55
56 // remove touch listener only if it exists
57 if (dragSourceAndListener.touchEnabled) {
58 let touchStartListener = dragSourceAndListener.touchStartListener;
59 element.removeEventListener('touchstart', touchStartListener, <any>{passive:true});
60 }
61 }
62
63 public removeDragSource(params: DragListenerParams): void {
64 let dragSourceAndListener = _.find( this.dragSources, item => item.dragSource === params);
65
66 if (!dragSourceAndListener) { return; }
67
68 this.removeListener(dragSourceAndListener);
69 _.removeFromArray(this.dragSources, dragSourceAndListener);
70 }
71
72 private setNoSelectToBody(noSelect: boolean): void {
73 let usrDocument = this.gridOptionsWrapper.getDocument();
74 let eBody = <HTMLElement> usrDocument.querySelector('body');
75 if (_.exists(eBody)) {
76 _.addOrRemoveCssClass(eBody, 'ag-body-no-select', noSelect);
77 }
78 }
79
80 public addDragSource(params: DragListenerParams, includeTouch: boolean = false): void {
81
82 let mouseListener = this.onMouseDown.bind(this, params);
83 params.eElement.addEventListener('mousedown', mouseListener);
84
85 let touchListener: (touchEvent: TouchEvent)=>void = null;
86
87 let suppressTouch = this.gridOptionsWrapper.isSuppressTouch();
88
89 let reallyIncludeTouch = includeTouch && !suppressTouch;
90
91 if (reallyIncludeTouch) {
92 touchListener = this.onTouchStart.bind(this, params);
93 params.eElement.addEventListener('touchstart', touchListener, <any>{passive:false});
94 }
95
96 this.dragSources.push({
97 dragSource: params,
98 mouseDownListener: mouseListener,
99 touchStartListener: touchListener,
100 touchEnabled: includeTouch
101 });
102 }
103
104 // gets called whenever mouse down on any drag source
105 private onTouchStart(params: DragListenerParams, touchEvent: TouchEvent): void {
106
107 this.currentDragParams = params;
108 this.dragging = false;
109
110 let touch = touchEvent.touches[0];
111
112 this.touchLastTime = touch;
113 this.touchStart = touch;
114
115 touchEvent.preventDefault();
116
117 // we temporally add these listeners, for the duration of the drag, they
118 // are removed in touch end handling.
119 params.eElement.addEventListener('touchmove', this.onTouchMoveListener, <any>{passive:true});
120 params.eElement.addEventListener('touchend', this.onTouchEndListener, <any>{passive:true});
121 params.eElement.addEventListener('touchcancel', this.onTouchEndListener, <any>{passive:true});
122
123 this.dragEndFunctions.push( ()=> {
124 params.eElement.removeEventListener('touchmove', this.onTouchMoveListener, <any>{passive:true});
125 params.eElement.removeEventListener('touchend', this.onTouchEndListener, <any>{passive:true});
126 params.eElement.removeEventListener('touchcancel', this.onTouchEndListener, <any>{passive:true});
127 });
128
129 // see if we want to start dragging straight away
130 if (params.dragStartPixels===0) {
131 this.onCommonMove(touch, this.touchStart);
132 }
133 }
134
135 // gets called whenever mouse down on any drag source
136 private onMouseDown(params: DragListenerParams, mouseEvent: MouseEvent): void {
137
138 // we ignore when shift key is pressed. this is for the range selection, as when
139 // user shift-clicks a cell, this should not be interpreted as the start of a drag.
140 // if (mouseEvent.shiftKey) { return; }
141 if (params.skipMouseEvent) {
142 if (params.skipMouseEvent(mouseEvent)) { return; }
143 }
144
145 // if there are two elements with parent / child relationship, and both are draggable,
146 // when we drag the child, we should NOT drag the parent. an example of this is row moving
147 // and range selection - row moving should get preference when use drags the rowDrag component.
148 if ((<any>mouseEvent)._alreadyProcessedByDragService) { return; }
149 (<any>mouseEvent)._alreadyProcessedByDragService = true;
150
151 // only interested in left button clicks
152 if (mouseEvent.button!==0) { return; }
153
154 this.currentDragParams = params;
155 this.dragging = false;
156
157 this.mouseEventLastTime = mouseEvent;
158 this.mouseStartEvent = mouseEvent;
159
160 let usrDocument = this.gridOptionsWrapper.getDocument();
161
162 // we temporally add these listeners, for the duration of the drag, they
163 // are removed in mouseup handling.
164
165 usrDocument.addEventListener('mousemove', this.onMouseMoveListener);
166 usrDocument.addEventListener('mouseup', this.onMouseUpListener);
167
168 this.dragEndFunctions.push( ()=> {
169 usrDocument.removeEventListener('mousemove', this.onMouseMoveListener);
170 usrDocument.removeEventListener('mouseup', this.onMouseUpListener);
171 });
172
173 // see if we want to start dragging straight away
174 if (params.dragStartPixels===0) {
175 this.onMouseMove(mouseEvent);
176 }
177 }
178
179 // returns true if the event is close to the original event by X pixels either vertically or horizontally.
180 // we only start dragging after X pixels so this allows us to know if we should start dragging yet.
181 private isEventNearStartEvent(currentEvent: MouseEvent|Touch, startEvent: MouseEvent|Touch): boolean {
182 // by default, we wait 4 pixels before starting the drag
183 let requiredPixelDiff = _.exists(this.currentDragParams.dragStartPixels) ? this.currentDragParams.dragStartPixels : 4;
184 return _.areEventsNear(currentEvent, startEvent, requiredPixelDiff);
185 }
186
187 private getFirstActiveTouch(touchList: TouchList): Touch {
188 for (let i = 0; i<touchList.length; i++) {
189 let matches = touchList[i].identifier === this.touchStart.identifier;
190 if (matches) {
191 return touchList[i];
192 }
193 }
194
195 return null;
196 }
197
198 private onCommonMove(currentEvent: MouseEvent|Touch, startEvent: MouseEvent|Touch): void {
199
200 if (!this.dragging) {
201 // if mouse hasn't travelled from the start position enough, do nothing
202 let toEarlyToDrag = !this.dragging && this.isEventNearStartEvent(currentEvent, startEvent);
203 if (toEarlyToDrag) {
204 return;
205 } else {
206 // alert(`started`);
207 this.dragging = true;
208 let event: DragStartedEvent = {
209 type: Events.EVENT_DRAG_STARTED,
210 api: this.gridApi,
211 columnApi: this.columnApi
212 };
213 this.eventService.dispatchEvent(event);
214 this.currentDragParams.onDragStart(startEvent);
215 this.setNoSelectToBody(true);
216 }
217 }
218
219 this.currentDragParams.onDragging(currentEvent);
220 }
221
222 private onTouchMove(touchEvent: TouchEvent): void {
223
224 let touch = this.getFirstActiveTouch(touchEvent.touches);
225 if (!touch) { return; }
226
227 // this.___statusBar.setInfoText(Math.random() + ' onTouchMove preventDefault stopPropagation');
228
229 // if we don't preview default, then the browser will try and do it's own touch stuff,
230 // like do 'back button' (chrome does this) or scroll the page (eg drag column could be confused
231 // with scroll page in the app)
232 // touchEvent.preventDefault();
233
234 this.onCommonMove(touch, this.touchStart);
235 }
236
237 // only gets called after a mouse down - as this is only added after mouseDown
238 // and is removed when mouseUp happens
239 private onMouseMove(mouseEvent: MouseEvent): void {
240 this.onCommonMove(mouseEvent, this.mouseStartEvent);
241 }
242
243 public onTouchUp(touchEvent: TouchEvent): void {
244 let touch = this.getFirstActiveTouch(touchEvent.changedTouches);
245
246 // i haven't worked this out yet, but there is no matching touch
247 // when we get the touch up event. to get around this, we swap in
248 // the last touch. this is a hack to 'get it working' while we
249 // figure out what's going on, why we are not getting a touch in
250 // current event.
251 if (!touch) {
252 touch = this.touchLastTime;
253 }
254
255 // if mouse was left up before we started to move, then this is a tap.
256 // we check this before onUpCommon as onUpCommon resets the dragging
257 // let tap = !this.dragging;
258 // let tapTarget = this.currentDragParams.eElement;
259
260 this.onUpCommon(touch);
261
262 // if tap, tell user
263 // console.log(`${Math.random()} tap = ${tap}`);
264 // if (tap) {
265 // tapTarget.click();
266 // }
267 }
268
269 public onMouseUp(mouseEvent: MouseEvent): void {
270 this.onUpCommon(mouseEvent);
271 }
272
273 public onUpCommon(eventOrTouch: MouseEvent|Touch): void {
274
275 if (this.dragging) {
276 this.dragging = false;
277 this.currentDragParams.onDragStop(eventOrTouch);
278 let event: DragStoppedEvent = {
279 type: Events.EVENT_DRAG_STOPPED,
280 api: this.gridApi,
281 columnApi: this.columnApi
282 };
283 this.eventService.dispatchEvent(event);
284 }
285
286 this.setNoSelectToBody(false);
287
288 this.mouseStartEvent = null;
289 this.mouseEventLastTime = null;
290 this.touchStart = null;
291 this.touchLastTime = null;
292 this.currentDragParams = null;
293
294 this.dragEndFunctions.forEach( func => func() );
295 this.dragEndFunctions.length = 0;
296 }
297}
298
299interface DragSourceAndListener {
300 dragSource: DragListenerParams;
301 mouseDownListener: (mouseEvent: MouseEvent)=>void;
302 touchEnabled: boolean;
303 touchStartListener: (touchEvent: TouchEvent)=>void;
304}
305
306export interface DragListenerParams {
307 /** After how many pixels of dragging should the drag operation start. Default is 4px. */
308 dragStartPixels?: number;
309 /** Dom element to add the drag handling to */
310 eElement: HTMLElement;
311 /** Some places may wish to ignore certain events, eg range selection ignores shift clicks */
312 skipMouseEvent?: (mouseEvent: MouseEvent) => boolean;
313 /** Callback for drag starting */
314 onDragStart: (mouseEvent: MouseEvent|Touch) => void;
315 /** Callback for drag stopping */
316 onDragStop: (mouseEvent: MouseEvent|Touch) => void;
317 /** Callback for mouse move while dragging */
318 onDragging: (mouseEvent: MouseEvent|Touch) => void;
319}
\No newline at end of file