diff --git a/src/cdk-experimental/drag-drop/drag-events.ts b/src/cdk-experimental/drag-drop/drag-events.ts index 7d71eb4ca38e..4ba0db09c4ff 100644 --- a/src/cdk-experimental/drag-drop/drag-events.ts +++ b/src/cdk-experimental/drag-drop/drag-events.ts @@ -55,3 +55,13 @@ export interface CdkDragDrop { /** Container from which the item was picked up. Can be the same as the `container`. */ previousContainer: CdkDropContainer; } + +/** Event emitted as the user is dragging a draggable item. */ +export interface CdkDragMove { + /** Item that is being dragged. */ + source: CdkDrag; + /** Position of the user's pointer on the page. */ + pointerPosition: {x: number, y: number}; + /** Native event that is causing the dragging. */ + event: MouseEvent | TouchEvent; +} diff --git a/src/cdk-experimental/drag-drop/drag.spec.ts b/src/cdk-experimental/drag-drop/drag.spec.ts index bee22282faaf..fadccc09d162 100644 --- a/src/cdk-experimental/drag-drop/drag.spec.ts +++ b/src/cdk-experimental/drag-drop/drag.spec.ts @@ -1,12 +1,13 @@ import { + AfterViewInit, Component, + ElementRef, + NgZone, + Provider, + QueryList, Type, ViewChild, - ElementRef, ViewChildren, - QueryList, - AfterViewInit, - Provider, ViewEncapsulation, } from '@angular/core'; import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing'; @@ -201,6 +202,50 @@ describe('CdkDrag', () => { // go into an infinite loop trying to stringify the event, if the test fails. expect(event).toEqual({source: fixture.componentInstance.dragInstance}); })); + + it('should emit when the user is moving the drag element', () => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const spy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 5, 10); + expect(spy).toHaveBeenCalledTimes(1); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 10, 20); + expect(spy).toHaveBeenCalledTimes(2); + + subscription.unsubscribe(); + }); + + it('should emit to `moved` inside the NgZone', () => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const spy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved + .subscribe(() => spy(NgZone.isInAngularZone())); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 10, 20); + expect(spy).toHaveBeenCalledWith(true); + + subscription.unsubscribe(); + }); + + it('should complete the `moved` stream on destroy', () => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const spy = jasmine.createSpy('move spy'); + const subscription = fixture.componentInstance.dragInstance.moved + .subscribe(undefined, undefined, spy); + + fixture.destroy(); + expect(spy).toHaveBeenCalled(); + subscription.unsubscribe(); + }); + }); describe('draggable with a handle', () => { diff --git a/src/cdk-experimental/drag-drop/drag.ts b/src/cdk-experimental/drag-drop/drag.ts index 0034438b3cb5..3ad3a0d648ee 100644 --- a/src/cdk-experimental/drag-drop/drag.ts +++ b/src/cdk-experimental/drag-drop/drag.ts @@ -27,12 +27,19 @@ import {DOCUMENT} from '@angular/common'; import {Directionality} from '@angular/cdk/bidi'; import {CdkDragHandle} from './drag-handle'; import {CdkDropContainer, CDK_DROP_CONTAINER} from './drop-container'; -import {CdkDragStart, CdkDragEnd, CdkDragExit, CdkDragEnter, CdkDragDrop} from './drag-events'; +import { + CdkDragStart, + CdkDragEnd, + CdkDragExit, + CdkDragEnter, + CdkDragDrop, + CdkDragMove, +} from './drag-events'; import {CdkDragPreview} from './drag-preview'; import {CdkDragPlaceholder} from './drag-placeholder'; import {ViewportRuler} from '@angular/cdk/overlay'; import {DragDropRegistry} from './drag-drop-registry'; -import {Subject, merge} from 'rxjs'; +import {Subject, merge, Observable} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; // TODO(crisbeto): add auto-scrolling functionality. @@ -97,6 +104,15 @@ export class CdkDrag implements OnDestroy { /** Cached scroll position on the page when the element was picked up. */ private _scrollPosition: {top: number, left: number}; + /** Emits when the item is being moved. */ + private _moveEvents = new Subject>(); + + /** + * Amount of subscriptions to the move event. Used to avoid + * hitting the zone if the consumer didn't subscribe to it. + */ + private _moveEventSubscriptions = 0; + /** Elements that can be used to drag the draggable item. */ @ContentChildren(CdkDragHandle) _handles: QueryList; @@ -129,6 +145,20 @@ export class CdkDrag implements OnDestroy { @Output('cdkDragDropped') dropped: EventEmitter> = new EventEmitter>(); + /** + * Emits as the user is dragging the item. Use with caution, + * because this event will fire for every pixel that the user has dragged. + */ + @Output('cdkDragMoved') moved: Observable> = Observable.create(observer => { + const subscription = this._moveEvents.subscribe(observer); + this._moveEventSubscriptions++; + + return () => { + subscription.unsubscribe(); + this._moveEventSubscriptions--; + }; + }); + constructor( /** Element that the draggable is attached to. */ public element: ElementRef, @@ -166,6 +196,7 @@ export class CdkDrag implements OnDestroy { this._nextSibling = null; this._dragDropRegistry.removeDragItem(this); + this._moveEvents.complete(); this._destroyed.next(); this._destroyed.complete(); } @@ -245,15 +276,31 @@ export class CdkDrag implements OnDestroy { this._hasMoved = true; event.preventDefault(); + const pointerPosition = this._getPointerPositionOnPage(event); + if (this.dropContainer) { - this._updateActiveDropContainer(event); + this._updateActiveDropContainer(pointerPosition); } else { const activeTransform = this._activeTransform; - const {x: pageX, y: pageY} = this._getPointerPositionOnPage(event); - activeTransform.x = pageX - this._pickupPositionOnPage.x + this._passiveTransform.x; - activeTransform.y = pageY - this._pickupPositionOnPage.y + this._passiveTransform.y; + activeTransform.x = + pointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x; + activeTransform.y = + pointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y; this._setTransform(this.element.nativeElement, activeTransform.x, activeTransform.y); } + + // Since this event gets fired for every pixel while dragging, we only + // want to fire it if the consumer opted into it. Also we have to + // re-enter the zone becaus we run all of the events on the outside. + if (this._moveEventSubscriptions > 0) { + this._ngZone.run(() => { + this._moveEvents.next({ + source: this, + pointerPosition, + event + }); + }); + } } /** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */ @@ -314,19 +361,17 @@ export class CdkDrag implements OnDestroy { * Updates the item's position in its drop container, or moves it * into a new one, depending on its current drag position. */ - private _updateActiveDropContainer(event: MouseEvent | TouchEvent) { - const {x, y} = this._getPointerPositionOnPage(event); - + private _updateActiveDropContainer({x, y}: Point) { // Drop container that draggable has been moved into. const newContainer = this.dropContainer._getSiblingContainerFromPosition(x, y); if (newContainer) { this._ngZone.run(() => { // Notify the old container that the item has left. - this.exited.emit({ item: this, container: this.dropContainer }); + this.exited.emit({item: this, container: this.dropContainer}); this.dropContainer.exit(this); // Notify the new container that the item has entered. - this.entered.emit({ item: this, container: newContainer }); + this.entered.emit({item: this, container: newContainer}); this.dropContainer = newContainer; this.dropContainer.enter(this, x, y); });