From 5502e6f5d61e4f6b012498bbb341375b3fa0127d Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 25 Jul 2018 22:57:57 +0200 Subject: [PATCH] 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. --- .../drag-drop/drag-drop-registry.spec.ts | 174 +++++++++++++++++ .../drag-drop/drag-drop-registry.ts | 183 ++++++++++++++++++ src/cdk-experimental/drag-drop/drag.spec.ts | 57 ++++++ src/cdk-experimental/drag-drop/drag.ts | 135 ++++++------- src/cdk-experimental/drag-drop/drop.ts | 54 ++++-- src/cdk-experimental/drag-drop/public-api.ts | 1 + 6 files changed, 510 insertions(+), 94 deletions(-) create mode 100644 src/cdk-experimental/drag-drop/drag-drop-registry.spec.ts create mode 100644 src/cdk-experimental/drag-drop/drag-drop-registry.ts diff --git a/src/cdk-experimental/drag-drop/drag-drop-registry.spec.ts b/src/cdk-experimental/drag-drop/drag-drop-registry.spec.ts new file mode 100644 index 00000000000..6a6918baa4f --- /dev/null +++ b/src/cdk-experimental/drag-drop/drag-drop-registry.spec.ts @@ -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; + 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: ` + +
{{item}}
+
+ + + ` +}) +export class SimpleDropZone { + @ViewChildren(CdkDrag) dragItems: QueryList; + @ViewChildren(CdkDrop) dropInstances: QueryList; + items = ['Zero', 'One', 'Two', 'Three']; + showDuplicateContainer = false; +} diff --git a/src/cdk-experimental/drag-drop/drag-drop-registry.ts b/src/cdk-experimental/drag-drop/drag-drop-registry.ts new file mode 100644 index 00000000000..147c457f2cb --- /dev/null +++ b/src/cdk-experimental/drag-drop/drag-drop-registry.ts @@ -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(); + + /** Registered `CdkDrag` instances. */ + private _dragInstances = new Set(); + + /** `CdkDrag` instances that are currently being dragged. */ + private _activeDragInstances = new Set(); + + /** Keeps track of the event listeners that we've bound to the `document`. */ + private _globalListeners = new Map(); + + /** + * Emits the `touchmove` or `mousemove` events that are dispatched + * while the user is dragging a `CdkDrag` instance. + */ + readonly pointerMove: Subject = new Subject(); + + /** + * Emits the `touchend` or `mouseup` events that are dispatched + * while the user is dragging a `CdkDrag` instance. + */ + readonly pointerUp: Subject = new Subject(); + + 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(id: string): CdkDrop | 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(); + } +} diff --git a/src/cdk-experimental/drag-drop/drag.spec.ts b/src/cdk-experimental/drag-drop/drag.spec.ts index 1f160d3c4a5..d8c70c0ebb3 100644 --- a/src/cdk-experimental/drag-drop/drag.spec.ts +++ b/src/cdk-experimental/drag-drop/drag.spec.ts @@ -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: `
{{item}}
` @@ -879,6 +935,7 @@ export class DraggableInDropZone { @ViewChildren(CdkDrag) dragItems: QueryList; @ViewChild(CdkDrop) dropInstance: CdkDrop; items = ['Zero', 'One', 'Two', 'Three']; + dropZoneId = 'items'; droppedSpy = jasmine.createSpy('dropped spy').and.callFake((event: CdkDragDrop) => { moveItemInArray(this.items, event.previousIndex, event.currentIndex); }); diff --git a/src/cdk-experimental/drag-drop/drag.ts b/src/cdk-experimental/drag-drop/drag.ts index 021ddd82d8c..533b249c102 100644 --- a/src/cdk-experimental/drag-drop/drag.ts +++ b/src/cdk-experimental/drag-drop/drag.ts @@ -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 implements OnDestroy { private _document: Document; + private _destroyed = new Subject(); /** 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(); + @Output('cdkDragStarted') started: EventEmitter = new EventEmitter(); /** Emits when the user stops dragging an item in the container. */ - @Output('cdkDragEnded') ended = new EventEmitter(); + @Output('cdkDragEnded') ended: EventEmitter = new EventEmitter(); /** Emits when the user has moved the item into a new container. */ - @Output('cdkDragEntered') entered = new EventEmitter>(); + @Output('cdkDragEntered') entered: EventEmitter> = + new EventEmitter>(); /** Emits when the user removes the item its container by dragging it into another container. */ - @Output('cdkDragExited') exited = new EventEmitter>(); + @Output('cdkDragExited') exited: EventEmitter> = + new EventEmitter>(); /** Emits when the user drops the item inside a container. */ - @Output('cdkDragDropped') dropped = new EventEmitter>(); + @Output('cdkDragDropped') dropped: EventEmitter> = + new EventEmitter>(); 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, 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'); diff --git a/src/cdk-experimental/drag-drop/drop.ts b/src/cdk-experimental/drag-drop/drop.ts index b38d50ea958..5e4468b5919 100644 --- a/src/cdk-experimental/drag-drop/drop.ts +++ b/src/cdk-experimental/drag-drop/drop.ts @@ -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 { +export class CdkDrop implements OnInit, OnDestroy { /** Draggable items in the container. */ @ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList; /** - * 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>(); + @Output() dropped: EventEmitter> = new EventEmitter>(); /** * Emits when the user has moved a new drag item into this container. */ - @Output() entered = new EventEmitter>(); + @Output() entered: EventEmitter> = new EventEmitter>(); /** * Emits when the user removes an item from the container * by dragging it into another container. */ - @Output() exited = new EventEmitter>(); + @Output() exited: EventEmitter> = new EventEmitter>(); - constructor(public element: ElementRef) {} + constructor( + public element: ElementRef, + 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 { // 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 { // 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()})); } diff --git a/src/cdk-experimental/drag-drop/public-api.ts b/src/cdk-experimental/drag-drop/public-api.ts index c58388211f5..d0c8a3035c7 100644 --- a/src/cdk-experimental/drag-drop/public-api.ts +++ b/src/cdk-experimental/drag-drop/public-api.ts @@ -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';