Skip to content

Commit b7b74bf

Browse files
committed
feat(drag-drop): add move event
This is something that came up during a discussion in #8963. Adds the `cdkDragMoved` event which will emit as an item is being dragged. Also adds some extra precautions to make sure that we're not doing extra work unless the consumer opted into the event.
1 parent c5cfede commit b7b74bf

File tree

3 files changed

+115
-15
lines changed

3 files changed

+115
-15
lines changed

src/cdk-experimental/drag-drop/drag-events.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,13 @@ export interface CdkDragDrop<T, O = T> {
5555
/** Container from which the item was picked up. Can be the same as the `container`. */
5656
previousContainer: CdkDropContainer<O>;
5757
}
58+
59+
/** Event emitted as the user is dragging a draggable item. */
60+
export interface CdkDragMove<T = any> {
61+
/** Item that is being dragged. */
62+
source: CdkDrag<T>;
63+
/** Position of the user's pointer on the page. */
64+
pointerPosition: {x: number, y: number};
65+
/** Native event that is causing the dragging. */
66+
event: MouseEvent | TouchEvent;
67+
}

src/cdk-experimental/drag-drop/drag.spec.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import {
2+
AfterViewInit,
23
Component,
4+
ElementRef,
5+
NgZone,
6+
Provider,
7+
QueryList,
38
Type,
49
ViewChild,
5-
ElementRef,
610
ViewChildren,
7-
QueryList,
8-
AfterViewInit,
9-
Provider,
1011
ViewEncapsulation,
1112
} from '@angular/core';
1213
import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing';
@@ -201,6 +202,50 @@ describe('CdkDrag', () => {
201202
// go into an infinite loop trying to stringify the event, if the test fails.
202203
expect(event).toEqual({source: fixture.componentInstance.dragInstance});
203204
}));
205+
206+
it('should emit when the user is moving the drag element', () => {
207+
const fixture = createComponent(StandaloneDraggable);
208+
fixture.detectChanges();
209+
210+
const spy = jasmine.createSpy('move spy');
211+
const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy);
212+
213+
dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10);
214+
expect(spy).toHaveBeenCalledTimes(1);
215+
216+
dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 10, 20);
217+
expect(spy).toHaveBeenCalledTimes(2);
218+
219+
subscription.unsubscribe();
220+
});
221+
222+
it('should emit to `moved` inside the NgZone', () => {
223+
const fixture = createComponent(StandaloneDraggable);
224+
fixture.detectChanges();
225+
226+
const spy = jasmine.createSpy('move spy');
227+
const subscription = fixture.componentInstance.dragInstance.moved
228+
.subscribe(() => spy(NgZone.isInAngularZone()));
229+
230+
dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 10, 20);
231+
expect(spy).toHaveBeenCalledWith(true);
232+
233+
subscription.unsubscribe();
234+
});
235+
236+
it('should complete the `moved` stream on destroy', () => {
237+
const fixture = createComponent(StandaloneDraggable);
238+
fixture.detectChanges();
239+
240+
const spy = jasmine.createSpy('move spy');
241+
const subscription = fixture.componentInstance.dragInstance.moved
242+
.subscribe(undefined, undefined, spy);
243+
244+
fixture.destroy();
245+
expect(spy).toHaveBeenCalled();
246+
subscription.unsubscribe();
247+
});
248+
204249
});
205250

206251
describe('draggable with a handle', () => {

src/cdk-experimental/drag-drop/drag.ts

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@ import {DOCUMENT} from '@angular/common';
2727
import {Directionality} from '@angular/cdk/bidi';
2828
import {CdkDragHandle} from './drag-handle';
2929
import {CdkDropContainer, CDK_DROP_CONTAINER} from './drop-container';
30-
import {CdkDragStart, CdkDragEnd, CdkDragExit, CdkDragEnter, CdkDragDrop} from './drag-events';
30+
import {
31+
CdkDragStart,
32+
CdkDragEnd,
33+
CdkDragExit,
34+
CdkDragEnter,
35+
CdkDragDrop,
36+
CdkDragMove,
37+
} from './drag-events';
3138
import {CdkDragPreview} from './drag-preview';
3239
import {CdkDragPlaceholder} from './drag-placeholder';
3340
import {ViewportRuler} from '@angular/cdk/overlay';
3441
import {DragDropRegistry} from './drag-drop-registry';
35-
import {Subject, merge} from 'rxjs';
42+
import {Subject, merge, Observable} from 'rxjs';
3643
import {takeUntil} from 'rxjs/operators';
3744

3845
// TODO(crisbeto): add auto-scrolling functionality.
@@ -97,6 +104,15 @@ export class CdkDrag<T = any> implements OnDestroy {
97104
/** Cached scroll position on the page when the element was picked up. */
98105
private _scrollPosition: {top: number, left: number};
99106

107+
/** Emits when the item is being moved. */
108+
private _moveEvents = new Subject<CdkDragMove<T>>();
109+
110+
/**
111+
* Amount of subscriptions to the move event. Used to avoid
112+
* hitting the zone if the consumer didn't subscribe to it.
113+
*/
114+
private _moveEventSubscriptions = 0;
115+
100116
/** Elements that can be used to drag the draggable item. */
101117
@ContentChildren(CdkDragHandle) _handles: QueryList<CdkDragHandle>;
102118

@@ -129,6 +145,20 @@ export class CdkDrag<T = any> implements OnDestroy {
129145
@Output('cdkDragDropped') dropped: EventEmitter<CdkDragDrop<any>> =
130146
new EventEmitter<CdkDragDrop<any>>();
131147

148+
/**
149+
* Emits as the user is dragging the the item. Use with caution,
150+
* because this event will fire for every pixel that the user has dragged.
151+
*/
152+
@Output('cdkDragMoved') moved: Observable<CdkDragMove<T>> = Observable.create(observer => {
153+
const subscription = this._moveEvents.subscribe(observer);
154+
this._moveEventSubscriptions++;
155+
156+
return () => {
157+
subscription.unsubscribe();
158+
this._moveEventSubscriptions--;
159+
};
160+
});
161+
132162
constructor(
133163
/** Element that the draggable is attached to. */
134164
public element: ElementRef<HTMLElement>,
@@ -166,6 +196,7 @@ export class CdkDrag<T = any> implements OnDestroy {
166196

167197
this._nextSibling = null;
168198
this._dragDropRegistry.removeDragItem(this);
199+
this._moveEvents.complete();
169200
this._destroyed.next();
170201
this._destroyed.complete();
171202
}
@@ -245,15 +276,31 @@ export class CdkDrag<T = any> implements OnDestroy {
245276
this._hasMoved = true;
246277
event.preventDefault();
247278

279+
const pointerPosition = this._getPointerPositionOnPage(event);
280+
248281
if (this.dropContainer) {
249-
this._updateActiveDropContainer(event);
282+
this._updateActiveDropContainer(pointerPosition);
250283
} else {
251284
const activeTransform = this._activeTransform;
252-
const {x: pageX, y: pageY} = this._getPointerPositionOnPage(event);
253-
activeTransform.x = pageX - this._pickupPositionOnPage.x + this._passiveTransform.x;
254-
activeTransform.y = pageY - this._pickupPositionOnPage.y + this._passiveTransform.y;
285+
activeTransform.x =
286+
pointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x;
287+
activeTransform.y =
288+
pointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y;
255289
this._setTransform(this.element.nativeElement, activeTransform.x, activeTransform.y);
256290
}
291+
292+
// Since this event gets fired for every pixel while dragging, we only
293+
// want to fire it if the consumer opted into it. Also we have to
294+
// re-enter the zone becaus we run all of the events on the outside.
295+
if (this._moveEventSubscriptions > 0) {
296+
this._ngZone.run(() => {
297+
this._moveEvents.next({
298+
source: this,
299+
pointerPosition,
300+
event
301+
});
302+
});
303+
}
257304
}
258305

259306
/** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */
@@ -314,19 +361,17 @@ export class CdkDrag<T = any> implements OnDestroy {
314361
* Updates the item's position in its drop container, or moves it
315362
* into a new one, depending on its current drag position.
316363
*/
317-
private _updateActiveDropContainer(event: MouseEvent | TouchEvent) {
318-
const {x, y} = this._getPointerPositionOnPage(event);
319-
364+
private _updateActiveDropContainer({x, y}: Point) {
320365
// Drop container that draggable has been moved into.
321366
const newContainer = this.dropContainer._getSiblingContainerFromPosition(x, y);
322367

323368
if (newContainer) {
324369
this._ngZone.run(() => {
325370
// Notify the old container that the item has left.
326-
this.exited.emit({ item: this, container: this.dropContainer });
371+
this.exited.emit({item: this, container: this.dropContainer});
327372
this.dropContainer.exit(this);
328373
// Notify the new container that the item has entered.
329-
this.entered.emit({ item: this, container: newContainer });
374+
this.entered.emit({item: this, container: newContainer});
330375
this.dropContainer = newContainer;
331376
this.dropContainer.enter(this, x, y);
332377
});

0 commit comments

Comments
 (0)