Skip to content

feat(drag-drop): add move event #12641

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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