Skip to content

Commit

Permalink
feat(drag-drop): add move event (#12641)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto authored and mmalerba committed Aug 20, 2018
1 parent a19c60c commit c7d34be
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 15 deletions.
10 changes: 10 additions & 0 deletions src/cdk-experimental/drag-drop/drag-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,13 @@ export interface CdkDragDrop<T, O = T> {
/** Container from which the item was picked up. Can be the same as the `container`. */
previousContainer: CdkDropContainer<O>;
}

/** Event emitted as the user is dragging a draggable item. */
export interface CdkDragMove<T = any> {
/** Item that is being dragged. */
source: CdkDrag<T>;
/** Position of the user's pointer on the page. */
pointerPosition: {x: number, y: number};
/** Native event that is causing the dragging. */
event: MouseEvent | TouchEvent;
}
53 changes: 49 additions & 4 deletions src/cdk-experimental/drag-drop/drag.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down
67 changes: 56 additions & 11 deletions src/cdk-experimental/drag-drop/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -97,6 +104,15 @@ export class CdkDrag<T = any> 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<CdkDragMove<T>>();

/**
* 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<CdkDragHandle>;

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

/**
* 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<CdkDragMove<T>> = 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<HTMLElement>,
Expand Down Expand Up @@ -166,6 +196,7 @@ export class CdkDrag<T = any> implements OnDestroy {

this._nextSibling = null;
this._dragDropRegistry.removeDragItem(this);
this._moveEvents.complete();
this._destroyed.next();
this._destroyed.complete();
}
Expand Down Expand Up @@ -245,15 +276,31 @@ export class CdkDrag<T = any> 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. */
Expand Down Expand Up @@ -314,19 +361,17 @@ export class CdkDrag<T = any> 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);
});
Expand Down

0 comments on commit c7d34be

Please sign in to comment.