From 6d54d7f5f150cc8f0bfb54cc3e10dc4ee9fd01c3 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 8 May 2019 19:52:20 -0400 Subject: [PATCH] feat(drag-drop): include dragged distance in events Adds the distance that the user has dragged in the `ended`, `moved` and `drop` events. This makes cases like resize handles to be implemented more conveniently. --- src/cdk/drag-drop/directives/drag.spec.ts | 116 ++++++++++++++++++---- src/cdk/drag-drop/directives/drag.ts | 10 +- src/cdk/drag-drop/directives/drop-list.ts | 3 +- src/cdk/drag-drop/drag-events.ts | 6 ++ src/cdk/drag-drop/drag-ref.ts | 40 ++++++-- src/cdk/drag-drop/drop-list-ref.ts | 12 ++- tools/public_api_guard/cdk/drag-drop.d.ts | 18 +++- 7 files changed, 171 insertions(+), 34 deletions(-) diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 91b39b786566..b0a16f34c78b 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -356,7 +356,31 @@ describe('CdkDrag', () => { // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will // go into an infinite loop trying to stringify the event, if the test fails. - expect(event).toEqual({source: fixture.componentInstance.dragInstance}); + expect(event).toEqual({ + source: fixture.componentInstance.dragInstance, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} + }); + })); + + it('should include the drag distance in the ended event', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 25, 30); + let event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; + + expect(event).toEqual({ + source: jasmine.anything(), + distance: {x: 25, y: 30} + }); + + dragElementViaMouse(fixture, fixture.componentInstance.dragElement.nativeElement, 40, 50); + event = fixture.componentInstance.endedSpy.calls.mostRecent().args[0]; + + expect(event).toEqual({ + source: jasmine.anything(), + distance: {x: 40, y: 50} + }); })); it('should emit when the user is moving the drag element', () => { @@ -844,6 +868,30 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); })); + it('should include the dragged distance as the user is dragging', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const spy = jasmine.createSpy('moved spy'); + const subscription = fixture.componentInstance.dragInstance.moved.subscribe(spy); + + startDraggingViaMouse(fixture, dragElement); + + dispatchMouseEvent(document, 'mousemove', 50, 100); + fixture.detectChanges(); + + let event = spy.calls.mostRecent().args[0]; + expect(event.distance).toEqual({x: 50, y: 100}); + + dispatchMouseEvent(document, 'mousemove', 75, 50); + fixture.detectChanges(); + + event = spy.calls.mostRecent().args[0]; + expect(event.distance).toEqual({x: 75, y: 50}); + + subscription.unsubscribe(); + })); + }); describe('draggable with a handle', () => { @@ -1149,7 +1197,8 @@ describe('CdkDrag', () => { item: firstItem, container: fixture.componentInstance.dropInstance, previousContainer: fixture.componentInstance.dropInstance, - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -1176,6 +1225,24 @@ describe('CdkDrag', () => { expect(event.isPointerOverContainer).toBe(true); })); + it('should expose the drag distance when an item is dropped', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + const dragItems = fixture.componentInstance.dragItems; + const firstItem = dragItems.first; + + dragElementViaMouse(fixture, firstItem.element.nativeElement, 50, 60); + flush(); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + + const event: CdkDragDrop = + fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + expect(event.distance).toEqual({x: 50, y: 60}); + })); + it('should expose whether an item was dropped outside of a container', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); @@ -1256,7 +1323,8 @@ describe('CdkDrag', () => { item: firstItem, container: fixture.componentInstance.dropInstance, previousContainer: fixture.componentInstance.dropInstance, - isPointerOverContainer: false + isPointerOverContainer: false, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -1314,7 +1382,8 @@ describe('CdkDrag', () => { item: firstItem, container: fixture.componentInstance.dropInstance, previousContainer: fixture.componentInstance.dropInstance, - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -1354,7 +1423,8 @@ describe('CdkDrag', () => { item: firstItem, container: fixture.componentInstance.dropInstance, previousContainer: fixture.componentInstance.dropInstance, - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -1390,7 +1460,8 @@ describe('CdkDrag', () => { item: firstItem, container: fixture.componentInstance.dropInstance, previousContainer: fixture.componentInstance.dropInstance, - isPointerOverContainer: false + isPointerOverContainer: false, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -2508,7 +2579,8 @@ describe('CdkDrag', () => { item: firstItem, container: dropInstance, previousContainer: dropInstance, - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) @@ -2543,7 +2615,8 @@ describe('CdkDrag', () => { item, container: fixture.componentInstance.dropInstances.toArray()[1], previousContainer: fixture.componentInstance.dropInstances.first, - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); })); @@ -2645,7 +2718,8 @@ describe('CdkDrag', () => { item: groups[0][1], container: dropInstances[1], previousContainer: dropInstances[0], - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); })); @@ -2675,7 +2749,8 @@ describe('CdkDrag', () => { item: groups[0][1], container: dropInstances[0], previousContainer: dropInstances[0], - isPointerOverContainer: false + isPointerOverContainer: false, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); })); @@ -2705,7 +2780,8 @@ describe('CdkDrag', () => { item: groups[0][1], container: dropInstances[0], previousContainer: dropInstances[0], - isPointerOverContainer: false + isPointerOverContainer: false, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); })); @@ -2828,7 +2904,8 @@ describe('CdkDrag', () => { item: groups[0][1], container: dropInstances[1], previousContainer: dropInstances[0], - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); })); @@ -2854,7 +2931,8 @@ describe('CdkDrag', () => { item: groups[0][1], container: dropInstances[1], previousContainer: dropInstances[0], - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); })); @@ -2885,7 +2963,8 @@ describe('CdkDrag', () => { item: groups[0][1], container: dropInstances[1], previousContainer: dropInstances[0], - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); })); @@ -2920,7 +2999,8 @@ describe('CdkDrag', () => { item, container: fixture.componentInstance.dropInstances.toArray()[1], previousContainer: fixture.componentInstance.dropInstances.first, - isPointerOverContainer: true + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); expect(dropContainers[0].contains(item.element.nativeElement)).toBe(true, @@ -3016,7 +3096,8 @@ describe('CdkDrag', () => { item: groups[0][1], container: dropInstances[0], previousContainer: dropInstances[0], - isPointerOverContainer: false + isPointerOverContainer: false, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} }); })); @@ -3136,7 +3217,8 @@ describe('CdkDrag', () => { item: groups[0][1], container: dropInstances[2], previousContainer: dropInstances[0], - isPointerOverContainer: false + isPointerOverContainer: false, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)} })); })); diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 1633718feb15..2fd627e7c8c6 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -170,7 +170,8 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { source: this, pointerPosition: movedEvent.pointerPosition, event: movedEvent.event, - delta: movedEvent.delta + delta: movedEvent.delta, + distance: movedEvent.distance }))).subscribe(observer); return () => { @@ -344,8 +345,8 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { this.released.emit({source: this}); }); - ref.ended.subscribe(() => { - this.ended.emit({source: this}); + ref.ended.subscribe(event => { + this.ended.emit({source: this, distance: event.distance}); // Since all of these events run outside of change detection, // we need to ensure that everything is marked correctly. @@ -373,7 +374,8 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { previousContainer: event.previousContainer.data, container: event.container.data, isPointerOverContainer: event.isPointerOverContainer, - item: this + item: this, + distance: event.distance }); }); } diff --git a/src/cdk/drag-drop/directives/drop-list.ts b/src/cdk/drag-drop/directives/drop-list.ts index e9aa212f43a2..f7349cca3682 100644 --- a/src/cdk/drag-drop/directives/drop-list.ts +++ b/src/cdk/drag-drop/directives/drop-list.ts @@ -341,7 +341,8 @@ export class CdkDropList implements CdkDropListContainer, AfterContentI previousContainer: event.previousContainer.data, container: event.container.data, item: event.item.data, - isPointerOverContainer: event.isPointerOverContainer + isPointerOverContainer: event.isPointerOverContainer, + distance: event.distance }); // Mark for check since all of these events run outside of change diff --git a/src/cdk/drag-drop/drag-events.ts b/src/cdk/drag-drop/drag-events.ts index a613dfe01543..d6e3a5f7901a 100644 --- a/src/cdk/drag-drop/drag-events.ts +++ b/src/cdk/drag-drop/drag-events.ts @@ -25,6 +25,8 @@ export interface CdkDragRelease { export interface CdkDragEnd { /** Draggable that emitted the event. */ source: CdkDrag; + /** Distance in pixels that the user has dragged since the drag sequence started. */ + distance: {x: number, y: number}; } /** Event emitted when the user moves an item into a new drop container. */ @@ -61,6 +63,8 @@ export interface CdkDragDrop { previousContainer: CdkDropList; /** Whether the user's pointer was over the container when the item was dropped. */ isPointerOverContainer: boolean; + /** Distance in pixels that the user has dragged since the drag sequence started. */ + distance: {x: number, y: number}; } /** Event emitted as the user is dragging a draggable item. */ @@ -71,6 +75,8 @@ export interface CdkDragMove { pointerPosition: {x: number, y: number}; /** Native event that is causing the dragging. */ event: MouseEvent | TouchEvent; + /** Distance in pixels that the user has dragged since the drag sequence started. */ + distance: {x: number, y: number}; /** * Indicates the direction in which the user is dragging the element along each axis. * `1` means that the position is increasing (e.g. the user is moving to the right or downwards), diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 341a1c27b784..ebe67ff91acf 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -127,6 +127,7 @@ export class DragRef { source: DragRef; pointerPosition: {x: number, y: number}; event: MouseEvent | TouchEvent; + distance: Point; delta: {x: -1 | 0 | 1, y: -1 | 0 | 1}; }>(); @@ -233,7 +234,7 @@ export class DragRef { released = new Subject<{source: DragRef}>(); /** Emits when the user stops dragging an item in the container. */ - ended = new Subject<{source: DragRef}>(); + ended = new Subject<{source: DragRef, distance: Point}>(); /** Emits when the user has moved the item into a new container. */ entered = new Subject<{container: DropListRef, item: DragRef}>(); @@ -248,6 +249,7 @@ export class DragRef { item: DragRef; container: DropListRef; previousContainer: DropListRef; + distance: Point; isPointerOverContainer: boolean; }>(); @@ -259,6 +261,7 @@ export class DragRef { source: DragRef; pointerPosition: {x: number, y: number}; event: MouseEvent | TouchEvent; + distance: Point; delta: {x: -1 | 0 | 1, y: -1 | 0 | 1}; }> = new Observable((observer: Observer) => { const subscription = this._moveEvents.subscribe(observer); @@ -569,6 +572,7 @@ export class DragRef { source: this, pointerPosition: constrainedPointerPosition, event, + distance: this._getDragDistance(constrainedPointerPosition), delta: this._pointerDirectionDelta }); }); @@ -604,7 +608,12 @@ export class DragRef { // to the new passive transform. this._passiveTransform.x = this._activeTransform.x; this._passiveTransform.y = this._activeTransform.y; - this._ngZone.run(() => this.ended.next({source: this})); + this._ngZone.run(() => { + this.ended.next({ + source: this, + distance: this._getDragDistance(this._getPointerPositionOnPage(event)) + }); + }); this._dragDropRegistry.stopDragging(this); return; } @@ -731,19 +740,22 @@ export class DragRef { this._ngZone.run(() => { const container = this._dropContainer!; const currentIndex = container.getItemIndex(this); - const {x, y} = this._getPointerPositionOnPage(event); - const isPointerOverContainer = container._isOverContainer(x, y); + const pointerPosition = this._getPointerPositionOnPage(event); + const distance = this._getDragDistance(this._getPointerPositionOnPage(event)); + const isPointerOverContainer = container._isOverContainer( + pointerPosition.x, pointerPosition.y); - this.ended.next({source: this}); + this.ended.next({source: this, distance}); this.dropped.next({ item: this, currentIndex, previousIndex: this._initialContainer.getItemIndex(this), container: container, previousContainer: this._initialContainer, - isPointerOverContainer + isPointerOverContainer, + distance }); - container.drop(this, currentIndex, this._initialContainer, isPointerOverContainer); + container.drop(this, currentIndex, this._initialContainer, isPointerOverContainer, distance); this._dropContainer = this._initialContainer; }); } @@ -1022,6 +1034,20 @@ export class DragRef { this._rootElement.style.transform = this._initialTransform ? transform + ' ' + this._initialTransform : transform; } + + /** + * Gets the distance that the user has dragged during the current drag sequence. + * @param currentPosition Current position of the user's pointer. + */ + private _getDragDistance(currentPosition: Point): Point { + const pickupPosition = this._pickupPositionOnPage; + + if (pickupPosition) { + return {x: currentPosition.x - pickupPosition.x, y: currentPosition.y - pickupPosition.y}; + } + + return {x: 0, y: 0}; + } } /** Point on the page or within an element. */ diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index 6bb790d0d09c..2627728f37db 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -11,7 +11,7 @@ import {DragDropRegistry} from './drag-drop-registry'; import {Direction} from '@angular/cdk/bidi'; import {Subject} from 'rxjs'; import {moveItemInArray} from './drag-utils'; -import {DragRefInternal as DragRef} from './drag-ref'; +import {DragRefInternal as DragRef, Point} from './drag-ref'; /** Counter used to generate unique ids for drop refs. */ @@ -96,7 +96,8 @@ export class DropListRef { previousIndex: number, container: DropListRef, previousContainer: DropListRef, - isPointerOverContainer: boolean + isPointerOverContainer: boolean, + distance: Point; }>(); /** Emits as the user is swapping items while actively dragging. */ @@ -255,9 +256,11 @@ export class DropListRef { * @param previousContainer Container from which the item got dragged in. * @param isPointerOverContainer Whether the user's pointer was over the * container when the item was dropped. + * @param distance Distance the user has dragged since the start of the dragging sequence. + * @breaking-change 9.0.0 `distance` parameter to become required. */ drop(item: DragRef, currentIndex: number, previousContainer: DropListRef, - isPointerOverContainer: boolean): void { + isPointerOverContainer: boolean, distance: Point = {x: 0, y: 0}): void { this._reset(); this.dropped.next({ item, @@ -265,7 +268,8 @@ export class DropListRef { previousIndex: previousContainer.getItemIndex(item), container: this, previousContainer, - isPointerOverContainer + isPointerOverContainer, + distance }); } diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index e94a7fafda05..894e4132dc99 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -52,6 +52,10 @@ export interface CdkDragConfig extends DragRefConfig { export interface CdkDragDrop { container: CdkDropList; currentIndex: number; + distance: { + x: number; + y: number; + }; isPointerOverContainer: boolean; item: CdkDrag; previousContainer: CdkDropList; @@ -59,6 +63,10 @@ export interface CdkDragDrop { } export interface CdkDragEnd { + distance: { + x: number; + y: number; + }; source: CdkDrag; } @@ -86,6 +94,10 @@ export interface CdkDragMove { x: -1 | 0 | 1; y: -1 | 0 | 1; }; + distance: { + x: number; + y: number; + }; event: MouseEvent | TouchEvent; pointerPosition: { x: number; @@ -221,10 +233,12 @@ export declare class DragRef { item: DragRef; container: DropListRef; previousContainer: DropListRef; + distance: Point; isPointerOverContainer: boolean; }>; ended: Subject<{ source: DragRef; + distance: Point; }>; entered: Subject<{ container: DropListRef; @@ -242,6 +256,7 @@ export declare class DragRef { y: number; }; event: MouseEvent | TouchEvent; + distance: Point; delta: { x: -1 | 0 | 1; y: -1 | 0 | 1; @@ -288,6 +303,7 @@ export declare class DropListRef { container: DropListRef; previousContainer: DropListRef; isPointerOverContainer: boolean; + distance: Point; }>; readonly element: HTMLElement; enterPredicate: (drag: DragRef, drop: DropListRef) => boolean; @@ -320,7 +336,7 @@ export declare class DropListRef { _stopReceiving(sibling: DropListRef): void; connectedTo(connectedTo: DropListRef[]): this; dispose(): void; - drop(item: DragRef, currentIndex: number, previousContainer: DropListRef, isPointerOverContainer: boolean): void; + drop(item: DragRef, currentIndex: number, previousContainer: DropListRef, isPointerOverContainer: boolean, distance?: Point): void; enter(item: DragRef, pointerX: number, pointerY: number): void; exit(item: DragRef): void; getItemIndex(item: DragRef): number;