mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
mmalerba
parent
6aa1a55ba7
commit
5502e6f5d6
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user