feat(drag-drop): allow connecting containers via string ids, attaching data to drop instances and consolidate global event listeners (#12315)

* Adds the ability to pass in strings to `connectedTo` when connecting multiple containers together. Previously we only accepted `CdkDrop` instances which can be inconvenient to pass while inside an `ngFor`.
* Adds the ability to attach extra data to a `CdkDrag`, similarly to `CdkDrop`. This is mostly for consistency and convenience.
* Introduces the `CdkDragDropRegistry` which is used to keep track of the active `CdkDrag` and `CdkDrop` instances. It also manages all of the events on the `document` in order to ensure that we only have one of each event at a given time. This will allow us to handle dragging multiple items at the same time, eventually.
This commit is contained in:
Kristiyan Kostadinov
2018-07-25 22:57:57 +02:00
committed by mmalerba
parent 6aa1a55ba7
commit 5502e6f5d6
6 changed files with 510 additions and 94 deletions
@@ -0,0 +1,174 @@
import {QueryList, ViewChildren, Component} from '@angular/core';
import {fakeAsync, TestBed, ComponentFixture, inject} from '@angular/core/testing';
import {
createMouseEvent,
dispatchMouseEvent,
createTouchEvent,
dispatchTouchEvent,
} from '@angular/cdk/testing';
import {CdkDragDropRegistry} from './drag-drop-registry';
import {DragDropModule} from './drag-drop-module';
import {CdkDrag} from './drag';
import {CdkDrop} from './drop';
describe('DragDropRegistry', () => {
let fixture: ComponentFixture<SimpleDropZone>;
let testComponent: SimpleDropZone;
let registry: CdkDragDropRegistry;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [DragDropModule],
declarations: [SimpleDropZone],
}).compileComponents();
fixture = TestBed.createComponent(SimpleDropZone);
testComponent = fixture.componentInstance;
fixture.detectChanges();
inject([CdkDragDropRegistry], (c: CdkDragDropRegistry) => {
registry = c;
})();
}));
afterEach(() => {
registry.ngOnDestroy();
});
it('should be able to start dragging an item', () => {
const firstItem = testComponent.dragItems.first;
expect(registry.isDragging(firstItem)).toBe(false);
registry.startDragging(firstItem, createMouseEvent('mousedown'));
expect(registry.isDragging(firstItem)).toBe(true);
});
it('should be able to stop dragging an item', () => {
const firstItem = testComponent.dragItems.first;
registry.startDragging(firstItem, createMouseEvent('mousedown'));
expect(registry.isDragging(firstItem)).toBe(true);
registry.stopDragging(firstItem);
expect(registry.isDragging(firstItem)).toBe(false);
});
it('should stop dragging an item if it is removed', () => {
const firstItem = testComponent.dragItems.first;
registry.startDragging(firstItem, createMouseEvent('mousedown'));
expect(registry.isDragging(firstItem)).toBe(true);
registry.remove(firstItem);
expect(registry.isDragging(firstItem)).toBe(false);
});
it('should dispatch `mousemove` events after starting to drag via the mouse', () => {
const spy = jasmine.createSpy('pointerMove spy');
const subscription = registry.pointerMove.subscribe(spy);
registry.startDragging(testComponent.dragItems.first, createMouseEvent('mousedown'));
dispatchMouseEvent(document, 'mousemove');
expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
});
it('should dispatch `touchmove` events after starting to drag via touch', () => {
const spy = jasmine.createSpy('pointerMove spy');
const subscription = registry.pointerMove.subscribe(spy);
registry.startDragging(testComponent.dragItems.first,
createTouchEvent('touchstart') as TouchEvent);
dispatchTouchEvent(document, 'touchmove');
expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
});
it('should dispatch `mouseup` events after ending the drag via the mouse', () => {
const spy = jasmine.createSpy('pointerUp spy');
const subscription = registry.pointerUp.subscribe(spy);
registry.startDragging(testComponent.dragItems.first, createMouseEvent('mousedown'));
dispatchMouseEvent(document, 'mouseup');
expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
});
it('should dispatch `touchend` events after ending the drag via touch', () => {
const spy = jasmine.createSpy('pointerUp spy');
const subscription = registry.pointerUp.subscribe(spy);
registry.startDragging(testComponent.dragItems.first,
createTouchEvent('touchstart') as TouchEvent);
dispatchTouchEvent(document, 'touchend');
expect(spy).toHaveBeenCalled();
subscription.unsubscribe();
});
it('should complete the pointer event streams on destroy', () => {
const pointerUpSpy = jasmine.createSpy('pointerUp complete spy');
const pointerMoveSpy = jasmine.createSpy('pointerMove complete spy');
const pointerUpSubscription = registry.pointerUp.subscribe(undefined, undefined, pointerUpSpy);
const pointerMoveSubscription =
registry.pointerMove.subscribe(undefined, undefined, pointerMoveSpy);
registry.ngOnDestroy();
expect(pointerUpSpy).toHaveBeenCalled();
expect(pointerMoveSpy).toHaveBeenCalled();
pointerUpSubscription.unsubscribe();
pointerMoveSubscription.unsubscribe();
});
it('should not throw when trying to register the same container again', () => {
expect(() => registry.register(testComponent.dropInstances.first)).not.toThrow();
});
it('should throw when trying to register a different container with the same id', () => {
expect(() => {
testComponent.showDuplicateContainer = true;
fixture.detectChanges();
}).toThrowError(/Drop instance with id \"items\" has already been registered/);
});
it('should be able to get a drop container by its id', () => {
expect(registry.getDropContainer('items')).toBe(testComponent.dropInstances.first);
expect(registry.getDropContainer('does-not-exist')).toBeFalsy();
});
it('should not prevent the default `touchmove` actions when nothing is being dragged', () => {
expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented).toBe(false);
});
it('should prevent the default `touchmove` action when an item is being dragged', () => {
registry.startDragging(testComponent.dragItems.first,
createTouchEvent('touchstart') as TouchEvent);
expect(dispatchTouchEvent(document, 'touchmove').defaultPrevented).toBe(true);
});
});
@Component({
template: `
<cdk-drop id="items" [data]="items">
<div *ngFor="let item of items" cdkDrag>{{item}}</div>
</cdk-drop>
<cdk-drop id="items" *ngIf="showDuplicateContainer"></cdk-drop>
`
})
export class SimpleDropZone {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
@ViewChildren(CdkDrop) dropInstances: QueryList<CdkDrop>;
items = ['Zero', 'One', 'Two', 'Three'];
showDuplicateContainer = false;
}
@@ -0,0 +1,183 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injectable, NgZone, OnDestroy, Inject} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {supportsPassiveEventListeners} from '@angular/cdk/platform';
import {Subject} from 'rxjs';
import {CdkDrop} from './drop';
import {CdkDrag} from './drag';
/** Event options that can be used to bind an active event. */
const activeEventOptions = supportsPassiveEventListeners() ? {passive: false} : false;
/** Handler for a pointer event callback. */
type PointerEventHandler = (event: TouchEvent | MouseEvent) => void;
/**
* Service that keeps track of all the `CdkDrag` and `CdkDrop` instances, and
* manages global event listeners on the `document`.
* @docs-private
*/
@Injectable({providedIn: 'root'})
export class CdkDragDropRegistry implements OnDestroy {
private _document: Document;
/** Registered `CdkDrop` instances. */
private _dropInstances = new Set<CdkDrop>();
/** Registered `CdkDrag` instances. */
private _dragInstances = new Set<CdkDrag>();
/** `CdkDrag` instances that are currently being dragged. */
private _activeDragInstances = new Set<CdkDrag>();
/** Keeps track of the event listeners that we've bound to the `document`. */
private _globalListeners = new Map<string, {handler: PointerEventHandler, options?: any}>();
/**
* Emits the `touchmove` or `mousemove` events that are dispatched
* while the user is dragging a `CdkDrag` instance.
*/
readonly pointerMove: Subject<TouchEvent | MouseEvent> = new Subject<TouchEvent | MouseEvent>();
/**
* Emits the `touchend` or `mouseup` events that are dispatched
* while the user is dragging a `CdkDrag` instance.
*/
readonly pointerUp: Subject<TouchEvent | MouseEvent> = new Subject<TouchEvent | MouseEvent>();
constructor(
private _ngZone: NgZone,
@Inject(DOCUMENT) _document: any) {
this._document = _document;
}
/** Adds a `CdkDrop` instance to the registry. */
register(drop: CdkDrop);
/** Adds a `CdkDrag` instance to the registry. */
register(drag: CdkDrag);
register(instance: CdkDrop | CdkDrag) {
if (instance instanceof CdkDrop) {
if (!this._dropInstances.has(instance)) {
if (this.getDropContainer(instance.id)) {
throw Error(`Drop instance with id "${instance.id}" has already been registered.`);
}
this._dropInstances.add(instance);
}
} else {
this._dragInstances.add(instance);
if (this._dragInstances.size === 1) {
this._ngZone.runOutsideAngular(() => {
// The event handler has to be explicitly active, because
// newer browsers make it passive by default.
this._document.addEventListener('touchmove', this._preventScrollListener,
activeEventOptions);
});
}
}
}
/** Removes a `CdkDrop` instance from the registry. */
remove(drop: CdkDrop);
/** Removes a `CdkDrag` instance from the registry. */
remove(drag: CdkDrag);
remove(instance: CdkDrop | CdkDrag) {
if (instance instanceof CdkDrop) {
this._dropInstances.delete(instance);
} else {
this._dragInstances.delete(instance);
this.stopDragging(instance);
if (this._dragInstances.size === 0) {
this._document.removeEventListener('touchmove', this._preventScrollListener,
activeEventOptions as any);
}
}
}
/**
* Starts the dragging sequence for a drag instance.
* @param drag Drag instance which is being dragged.
* @param event Event that initiated the dragging.
*/
startDragging(drag: CdkDrag, event: TouchEvent | MouseEvent) {
this._activeDragInstances.add(drag);
if (this._activeDragInstances.size === 1) {
const isTouchEvent = event.type.startsWith('touch');
const moveEvent = isTouchEvent ? 'touchmove' : 'mousemove';
const upEvent = isTouchEvent ? 'touchend' : 'mouseup';
// We explicitly bind __active__ listeners here, because newer browsers will default to
// passive ones for `mousemove` and `touchmove`. The events need to be active, because we
// use `preventDefault` to prevent the page from scrolling while the user is dragging.
this._globalListeners
.set(moveEvent, {handler: e => this.pointerMove.next(e), options: activeEventOptions})
.set(upEvent, {handler: e => this.pointerUp.next(e)})
.forEach((config, name) => {
this._ngZone.runOutsideAngular(() => {
this._document.addEventListener(name, config.handler, config.options);
});
});
}
}
/** Stops dragging a `CdkDrag` instance. */
stopDragging(drag: CdkDrag) {
this._activeDragInstances.delete(drag);
if (this._activeDragInstances.size === 0) {
this._clearGlobalListeners();
}
}
/** Gets whether a `CdkDrag` instance is currently being dragged. */
isDragging(drag: CdkDrag) {
return this._activeDragInstances.has(drag);
}
/** Gets a `CdkDrop` instance by its id. */
getDropContainer<T = any>(id: string): CdkDrop<T> | undefined {
return Array.from(this._dropInstances).find(instance => instance.id === id);
}
ngOnDestroy() {
this._dragInstances.forEach(instance => this.remove(instance));
this._dropInstances.forEach(instance => this.remove(instance));
this._clearGlobalListeners();
this.pointerMove.complete();
this.pointerUp.complete();
}
/**
* Listener used to prevent `touchmove` events while the element is being dragged.
* This gets bound once, ahead of time, because WebKit won't preventDefault on a
* dynamically-added `touchmove` listener. See https://bugs.webkit.org/show_bug.cgi?id=184250.
*/
private _preventScrollListener = (event: TouchEvent) => {
if (this._activeDragInstances.size) {
event.preventDefault();
}
}
/** Clears out the global event listeners from the `document`. */
private _clearGlobalListeners() {
this._globalListeners.forEach((config, name) => {
this._document.removeEventListener(name, config.handler, config.options);
});
this._globalListeners.clear();
}
}
@@ -265,6 +265,26 @@ describe('CdkDrag', () => {
expect(fixture.componentInstance.dropInstance.data).toBe(fixture.componentInstance.items);
});
it('should be able to attach data to a drag item', () => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
expect(fixture.componentInstance.dragItems.first.data)
.toBe(fixture.componentInstance.items[0]);
});
it('should be able to overwrite the drop zone id', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.componentInstance.dropZoneId = 'custom-id';
fixture.detectChanges();
const drop = fixture.componentInstance.dropInstance;
expect(drop.id).toBe('custom-id');
expect(drop.element.nativeElement.getAttribute('id')).toBe('custom-id');
}));
it('should toggle a class when the user starts dragging an item', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
@@ -787,6 +807,40 @@ describe('CdkDrag', () => {
expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled();
}));
it('should assign a default id on each drop zone', fakeAsync(() => {
const fixture = createComponent(ConnectedDropZones);
fixture.detectChanges();
expect(fixture.componentInstance.dropInstances.toArray().every(dropZone => {
return !!dropZone.id && !!dropZone.element.nativeElement.getAttribute('id');
})).toBe(true);
}));
it('should be able to connect two drop zones by id', fakeAsync(() => {
const fixture = createComponent(ConnectedDropZones);
fixture.detectChanges();
const dropZones = fixture.componentInstance.dropInstances.toArray();
dropZones[0].id = 'todo';
dropZones[1].id = 'done';
dropZones[0].connectedTo = ['done'];
dropZones[1].connectedTo = ['todo'];
fixture.detectChanges();
const groups = fixture.componentInstance.groupedDragItems;
const element = groups[0][1].element.nativeElement;
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
expect(dropZones[0].element.nativeElement.contains(element)).toBe(true);
dragElementViaMouse(fixture, element, targetRect.left + 1, targetRect.top + 1);
flush();
fixture.detectChanges();
expect(dropZones[1].element.nativeElement.contains(element)).toBe(true);
}));
});
});
@@ -866,11 +920,13 @@ export class StandaloneDraggableWithMultipleHandles {
template: `
<cdk-drop
style="display: block; width: 100px; background: pink;"
[id]="dropZoneId"
[data]="items"
(dropped)="droppedSpy($event)">
<div
*ngFor="let item of items"
cdkDrag
[data]="item"
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">{{item}}</div>
</cdk-drop>
`
@@ -879,6 +935,7 @@ export class DraggableInDropZone {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
@ViewChild(CdkDrop) dropInstance: CdkDrop;
items = ['Zero', 'One', 'Two', 'Three'];
dropZoneId = 'items';
droppedSpy = jasmine.createSpy('dropped spy').and.callFake((event: CdkDragDrop<string[]>) => {
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
});
+55 -80
View File
@@ -7,35 +7,35 @@
*/
import {
Directive,
ContentChild,
Inject,
Optional,
ElementRef,
AfterContentInit,
NgZone,
SkipSelf,
OnDestroy,
Output,
EventEmitter,
ViewContainerRef,
EmbeddedViewRef,
ContentChildren,
Directive,
ElementRef,
EmbeddedViewRef,
EventEmitter,
Inject,
Input,
NgZone,
OnDestroy,
Optional,
Output,
QueryList,
SkipSelf,
ViewContainerRef,
} from '@angular/core';
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 {supportsPassiveEventListeners} from '@angular/cdk/platform';
import {CdkDragStart, CdkDragEnd, CdkDragExit, CdkDragEnter, CdkDragDrop} from './drag-events';
import {CdkDragPreview} from './drag-preview';
import {CdkDragPlaceholder} from './drag-placeholder';
import {ViewportRuler} from '@angular/cdk/overlay';
import {CdkDragDropRegistry} from './drag-drop-registry';
import {Subject, merge} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
/** Event options that can be used to bind an active event. */
const activeEventOptions = supportsPassiveEventListeners() ? {passive: false} : false;
// TODO: add auto-scrolling functionality.
// TODO: add an API for moving a draggable up/down the
// list programmatically. Useful for keyboard controls.
@@ -49,8 +49,9 @@ const activeEventOptions = supportsPassiveEventListeners() ? {passive: false} :
'(touchstart)': '_startDragging($event)',
}
})
export class CdkDrag implements AfterContentInit, OnDestroy {
export class CdkDrag<T = any> implements OnDestroy {
private _document: Document;
private _destroyed = new Subject<void>();
/** Element displayed next to the user's pointer while the element is dragged. */
private _preview: HTMLElement;
@@ -81,9 +82,6 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
/** CSS `transform` that is applied to the element while it's being dragged. */
private _activeTransform: Point = {x: 0, y: 0};
/** Whether the element is being dragged. */
_isDragging = false;
/** Whether the element has moved since the user started dragging it. */
private _hasMoved = false;
@@ -104,20 +102,26 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
*/
@ContentChild(CdkDragPlaceholder) _placeholderTemplate: CdkDragPlaceholder;
/** Arbitrary data to attach to this drag instance. */
@Input() data: T;
/** Emits when the user starts dragging the item. */
@Output('cdkDragStarted') started = new EventEmitter<CdkDragStart>();
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();
/** Emits when the user stops dragging an item in the container. */
@Output('cdkDragEnded') ended = new EventEmitter<CdkDragEnd>();
@Output('cdkDragEnded') ended: EventEmitter<CdkDragEnd> = new EventEmitter<CdkDragEnd>();
/** Emits when the user has moved the item into a new container. */
@Output('cdkDragEntered') entered = new EventEmitter<CdkDragEnter<any>>();
@Output('cdkDragEntered') entered: EventEmitter<CdkDragEnter<any>> =
new EventEmitter<CdkDragEnter<any>>();
/** Emits when the user removes the item its container by dragging it into another container. */
@Output('cdkDragExited') exited = new EventEmitter<CdkDragExit<any>>();
@Output('cdkDragExited') exited: EventEmitter<CdkDragExit<any>> =
new EventEmitter<CdkDragExit<any>>();
/** Emits when the user drops the item inside a container. */
@Output('cdkDragDropped') dropped = new EventEmitter<CdkDragDrop<any>>();
@Output('cdkDragDropped') dropped: EventEmitter<CdkDragDrop<any>> =
new EventEmitter<CdkDragDrop<any>>();
constructor(
/** Element that the draggable is attached to. */
@@ -128,8 +132,10 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
private _ngZone: NgZone,
private _viewContainerRef: ViewContainerRef,
private _viewportRuler: ViewportRuler,
private _dragDropRegistry: CdkDragDropRegistry,
@Optional() private _dir: Directionality) {
this._document = document;
_dragDropRegistry.register(this);
}
/**
@@ -140,27 +146,21 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
return this._placeholder;
}
ngAfterContentInit() {
// WebKit won't preventDefault on a dynamically-added `touchmove` listener, which means that
// we need to add one ahead of time. See https://bugs.webkit.org/show_bug.cgi?id=184250.
// TODO: move into a central registry.
this._ngZone.runOutsideAngular(() => {
this._document.addEventListener('touchmove', this._preventScrollListener, activeEventOptions);
});
}
ngOnDestroy() {
this._removeDocumentEvents();
this._destroyPreview();
this._destroyPlaceholder();
this._document.removeEventListener('touchmove', this._preventScrollListener,
activeEventOptions as any);
if (this._isDragging) {
// Do this check before removing from the registry since it'll
// stop being considered as dragged once it is removed.
if (this._dragDropRegistry.isDragging(this)) {
// Since we move out the element to the end of the body while it's being
// dragged, we have to make sure that it's removed if it gets destroyed.
this._removeElement(this.element.nativeElement);
}
this._dragDropRegistry.remove(this);
this._destroyed.next();
this._destroyed.complete();
}
/** Starts the dragging sequence. */
@@ -184,11 +184,21 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
/** Handler for when the pointer is pressed down on the element or the handle. */
private _pointerDown = (referenceElement: ElementRef<HTMLElement>,
event: MouseEvent | TouchEvent) => {
if (this._isDragging) {
if (this._dragDropRegistry.isDragging(this)) {
return;
}
this._isDragging = true;
const endedOrDestroyed = merge(this.ended, this._destroyed);
this._dragDropRegistry.pointerMove
.pipe(takeUntil(endedOrDestroyed))
.subscribe(this._pointerMove);
this._dragDropRegistry.pointerUp
.pipe(takeUntil(endedOrDestroyed))
.subscribe(this._pointerUp);
this._dragDropRegistry.startDragging(this, event);
this._initialContainer = this.dropContainer;
this._scrollPosition = this._viewportRuler.getViewportScrollPosition();
@@ -197,7 +207,6 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
this._pickupPositionInElement = this._previewTemplate ? {x: 0, y: 0} :
this._getPointerPositionInElement(referenceElement, event);
this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
this._registerMoveListeners(event);
// Emit the event on the item before the one on the container.
this.started.emit({source: this});
@@ -219,7 +228,9 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
/** Handler that is invoked when the user moves their pointer after they've initiated a drag. */
private _pointerMove = (event: MouseEvent | TouchEvent) => {
if (!this._isDragging) {
// TODO: this should start dragging after a certain threshold,
// otherwise we risk interfering with clicks on the element.
if (!this._dragDropRegistry.isDragging(this)) {
return;
}
@@ -239,12 +250,11 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
/** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */
private _pointerUp = () => {
if (!this._isDragging) {
if (!this._dragDropRegistry.isDragging(this)) {
return;
}
this._removeDocumentEvents();
this._isDragging = false;
this._dragDropRegistry.stopDragging(this);
if (!this.dropContainer) {
// Convert the active transform into a passive one. This means that next time
@@ -481,16 +491,6 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
}
}
/** Removes the global event listeners that were bound by this draggable. */
private _removeDocumentEvents() {
this._document.removeEventListener('mousemove', this._pointerMove,
activeEventOptions as any);
this._document.removeEventListener('touchmove', this._pointerMove,
activeEventOptions as any);
this._document.removeEventListener('mouseup', this._pointerUp);
this._document.removeEventListener('touchend', this._pointerUp);
}
/** Determines the point of the page that was touched by the user. */
private _getPointerPositionOnPage(event: MouseEvent | TouchEvent): Point {
const point = this._isTouchEvent(event) ? event.touches[0] : event;
@@ -501,13 +501,6 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
};
}
/** Listener used to prevent `touchmove` events while the element is being dragged. */
private _preventScrollListener = (event: TouchEvent) => {
if (this._isDragging) {
event.preventDefault();
}
}
/** Determines whether an event is a touch event. */
private _isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
return event.type.startsWith('touch');
@@ -539,24 +532,6 @@ export class CdkDrag implements AfterContentInit, OnDestroy {
this._placeholder = this._placeholderRef = null!;
}
/**
* Registers global event listeners that are used for moving the element.
* @param event Event that initiated the dragging.
*/
private _registerMoveListeners(event: MouseEvent | TouchEvent) {
this._ngZone.runOutsideAngular(() => {
const isTouchEvent = this._isTouchEvent(event);
// We explicitly bind __active__ listeners here, because newer browsers
// will default to passive ones for `mousemove` and `touchmove`.
// TODO: this should be bound in `mousemove` and after a certain threshold,
// otherwise it'll interfere with clicks on the element.
this._document.addEventListener(isTouchEvent ? 'touchmove' : 'mousemove', this._pointerMove,
activeEventOptions);
this._document.addEventListener(isTouchEvent ? 'touchend' : 'mouseup', this._pointerUp);
});
}
/** Gets the `transition-duration` of an element in milliseconds. */
private _getTransitionDurationInMs(element: HTMLElement): number {
const rawDuration = getComputedStyle(element).getPropertyValue('transition-duration');
+40 -14
View File
@@ -7,20 +7,26 @@
*/
import {
ChangeDetectionStrategy,
Component,
ContentChildren,
ElementRef,
EventEmitter,
forwardRef,
Input,
ViewEncapsulation,
ChangeDetectionStrategy,
OnDestroy,
OnInit,
Output,
EventEmitter,
ElementRef,
QueryList,
ViewEncapsulation,
} from '@angular/core';
import {CdkDrag} from './drag';
import {CdkDragExit, CdkDragEnter, CdkDragDrop} from './drag-events';
import {CDK_DROP_CONTAINER} from './drop-container';
import {CdkDragDropRegistry} from './drag-drop-registry';
/** Counter used to generate unique ids for drop zones. */
let _uniqueIdCounter = 0;
/** Container that wraps a set of draggable items. */
@Component({
@@ -36,40 +42,58 @@ import {CDK_DROP_CONTAINER} from './drop-container';
],
host: {
'class': 'cdk-drop',
'[id]': 'id',
'[class.cdk-drop-dragging]': '_dragging'
}
})
export class CdkDrop<T = any> {
export class CdkDrop<T = any> implements OnInit, OnDestroy {
/** Draggable items in the container. */
@ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList<CdkDrag>;
/**
* Other draggable containers that this container is connected
* to and into which the container's items can be transferred.
* Other draggable containers that this container is connected to and into which the
* container's items can be transferred. Can either be references to other drop containers,
* or their unique IDs.
*/
@Input() connectedTo: CdkDrop[] = [];
@Input() connectedTo: (CdkDrop | string)[] = [];
/** Arbitrary data to attach to all events emitted by this container. */
/** Arbitrary data to attach to this container. */
@Input() data: T;
/** Direction in which the list is oriented. */
@Input() orientation: 'horizontal' | 'vertical' = 'vertical';
/**
* Unique ID for the drop zone. Can be used as a reference
* in the `connectedTo` of another `CdkDrop`.
*/
@Input() id: string = `cdk-drop-${_uniqueIdCounter++}`;
/** Emits when the user drops an item inside the container. */
@Output() dropped = new EventEmitter<CdkDragDrop<T, any>>();
@Output() dropped: EventEmitter<CdkDragDrop<T, any>> = new EventEmitter<CdkDragDrop<T, any>>();
/**
* Emits when the user has moved a new drag item into this container.
*/
@Output() entered = new EventEmitter<CdkDragEnter<T>>();
@Output() entered: EventEmitter<CdkDragEnter<T>> = new EventEmitter<CdkDragEnter<T>>();
/**
* Emits when the user removes an item from the container
* by dragging it into another container.
*/
@Output() exited = new EventEmitter<CdkDragExit<T>>();
@Output() exited: EventEmitter<CdkDragExit<T>> = new EventEmitter<CdkDragExit<T>>();
constructor(public element: ElementRef<HTMLElement>) {}
constructor(
public element: ElementRef<HTMLElement>,
private _dragDropRegistry: CdkDragDropRegistry) {}
ngOnInit() {
this._dragDropRegistry.register(this);
}
ngOnDestroy() {
this._dragDropRegistry.remove(this);
}
/** Whether an item in the container is being dragged. */
_dragging = false;
@@ -157,7 +181,7 @@ export class CdkDrop<T = any> {
// Don't take the element of a dragged item as a reference,
// because it has been moved down to the end of the body.
const element = (newPosition && !newPosition.drag._isDragging) ?
const element = (newPosition && !this._dragDropRegistry.isDragging(newPosition.drag)) ?
newPosition.drag.element.nativeElement : null;
const next = element ? element!.nextSibling : null;
const parent = element ? element.parentElement! : this.element.nativeElement;
@@ -195,6 +219,8 @@ export class CdkDrop<T = any> {
// TODO: add filter here that ensures that the current container isn't being passed to itself.
this._positionCache.siblings = this.connectedTo
.map(drop => typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop)
.filter(Boolean)
.map(drop => ({drop, clientRect: drop.element.nativeElement.getBoundingClientRect()}));
}
@@ -15,3 +15,4 @@ export * from './drag-utils';
export * from './drag-preview';
export * from './drag-placeholder';
export * from './drag-drop-module';
export * from './drag-drop-registry';