diff --git a/src/cdk/drag-drop/drag-handle.ts b/src/cdk/drag-drop/directives/drag-handle.ts similarity index 90% rename from src/cdk/drag-drop/drag-handle.ts rename to src/cdk/drag-drop/directives/drag-handle.ts index 9d8cf3b1fd17..73b622a2b897 100644 --- a/src/cdk/drag-drop/drag-handle.ts +++ b/src/cdk/drag-drop/directives/drag-handle.ts @@ -8,8 +8,8 @@ import {Directive, ElementRef, Inject, Optional, Input} from '@angular/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {CDK_DRAG_PARENT} from './drag-parent'; -import {toggleNativeDragInteractions} from './drag-styling'; +import {CDK_DRAG_PARENT} from '../drag-parent'; +import {toggleNativeDragInteractions} from '../drag-styling'; /** Handle that can be used to drag and CdkDrag instance. */ @Directive({ diff --git a/src/cdk/drag-drop/drag-placeholder.ts b/src/cdk/drag-drop/directives/drag-placeholder.ts similarity index 100% rename from src/cdk/drag-drop/drag-placeholder.ts rename to src/cdk/drag-drop/directives/drag-placeholder.ts diff --git a/src/cdk/drag-drop/drag-preview.ts b/src/cdk/drag-drop/directives/drag-preview.ts similarity index 100% rename from src/cdk/drag-drop/drag-preview.ts rename to src/cdk/drag-drop/directives/drag-preview.ts diff --git a/src/cdk/drag-drop/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts similarity index 99% rename from src/cdk/drag-drop/drag.spec.ts rename to src/cdk/drag-drop/directives/drag.spec.ts index ba9d47e7ee01..01de1d6e92a1 100644 --- a/src/cdk/drag-drop/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -12,7 +12,7 @@ import { ChangeDetectionStrategy, } from '@angular/core'; import {TestBed, ComponentFixture, fakeAsync, flush, tick} from '@angular/core/testing'; -import {DragDropModule} from './drag-drop-module'; +import {DragDropModule} from '../drag-drop-module'; import { createMouseEvent, dispatchEvent, @@ -21,13 +21,14 @@ import { createTouchEvent, } from '@angular/cdk/testing'; import {Directionality} from '@angular/cdk/bidi'; -import {CdkDrag, CDK_DRAG_CONFIG, CdkDragConfig} from './drag'; -import {CdkDragDrop} from './drag-events'; -import {moveItemInArray} from './drag-utils'; +import {CdkDrag, CDK_DRAG_CONFIG} from './drag'; +import {CdkDragDrop} from '../drag-events'; +import {moveItemInArray} from '../drag-utils'; import {CdkDropList} from './drop-list'; import {CdkDragHandle} from './drag-handle'; import {CdkDropListGroup} from './drop-list-group'; -import {extendStyles} from './drag-styling'; +import {extendStyles} from '../drag-styling'; +import {DragRefConfig} from '../drag-ref'; const ITEM_HEIGHT = 25; const ITEM_WIDTH = 75; @@ -47,7 +48,7 @@ describe('CdkDrag', () => { // have to deal with thresholds. dragStartThreshold: dragDistance, pointerDirectionChangeThreshold: 5 - } as CdkDragConfig + } as DragRefConfig }, ...providers ], diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts new file mode 100644 index 000000000000..1db945ca0656 --- /dev/null +++ b/src/cdk/drag-drop/directives/drag.ts @@ -0,0 +1,305 @@ +/** + * @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 {Directionality} from '@angular/cdk/bidi'; +import {ViewportRuler} from '@angular/cdk/scrolling'; +import {DOCUMENT} from '@angular/common'; +import { + AfterViewInit, + ContentChild, + ContentChildren, + Directive, + ElementRef, + EventEmitter, + Inject, + InjectionToken, + Input, + NgZone, + OnDestroy, + Optional, + Output, + QueryList, + SkipSelf, + ViewContainerRef, +} from '@angular/core'; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {Observable, Subscription, Observer} from 'rxjs'; +import {startWith, take, map} from 'rxjs/operators'; +import {DragDropRegistry} from '../drag-drop-registry'; +import { + CdkDragDrop, + CdkDragEnd, + CdkDragEnter, + CdkDragExit, + CdkDragMove, + CdkDragStart, +} from '../drag-events'; +import {CdkDragHandle} from './drag-handle'; +import {CdkDragPlaceholder} from './drag-placeholder'; +import {CdkDragPreview} from './drag-preview'; +import {CDK_DROP_LIST} from '../drop-list-container'; +import {CDK_DRAG_PARENT} from '../drag-parent'; +import {DragRef, DragRefConfig} from '../drag-ref'; +import {DropListRef} from '../drop-list-ref'; +import {CdkDropListInternal as CdkDropList} from './drop-list'; + +/** Injection token that can be used to configure the behavior of `CdkDrag`. */ +export const CDK_DRAG_CONFIG = new InjectionToken('CDK_DRAG_CONFIG', { + providedIn: 'root', + factory: CDK_DRAG_CONFIG_FACTORY +}); + +/** @docs-private */ +export function CDK_DRAG_CONFIG_FACTORY(): DragRefConfig { + return {dragStartThreshold: 5, pointerDirectionChangeThreshold: 5}; +} + +/** Element that can be moved inside a CdkDropList container. */ +@Directive({ + selector: '[cdkDrag]', + exportAs: 'cdkDrag', + host: { + 'class': 'cdk-drag', + '[class.cdk-drag-dragging]': '_dragRef.isDragging()', + }, + providers: [{provide: CDK_DRAG_PARENT, useExisting: CdkDrag}] +}) +export class CdkDrag implements AfterViewInit, OnDestroy { + /** Subscription to the stream that initializes the root element. */ + private _rootElementInitSubscription = Subscription.EMPTY; + + /** Reference to the underlying drag instance. */ + _dragRef: DragRef>; + + /** Elements that can be used to drag the draggable item. */ + @ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList; + + /** Element that will be used as a template to create the draggable item's preview. */ + @ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview; + + /** Template for placeholder element rendered to show where a draggable would be dropped. */ + @ContentChild(CdkDragPlaceholder) _placeholderTemplate: CdkDragPlaceholder; + + /** Arbitrary data to attach to this drag instance. */ + @Input('cdkDragData') data: T; + + /** Locks the position of the dragged element along the specified axis. */ + @Input('cdkDragLockAxis') lockAxis: 'x' | 'y'; + + /** + * Selector that will be used to determine the root draggable element, starting from + * the `cdkDrag` element and going up the DOM. Passing an alternate root element is useful + * when trying to enable dragging on an element that you might not have access to. + */ + @Input('cdkDragRootElement') rootElementSelector: string; + + /** + * Selector that will be used to determine the element to which the draggable's position will + * be constrained. Matching starts from the element's parent and goes up the DOM until a matching + * element has been found. + */ + @Input('cdkDragBoundary') boundaryElementSelector: string; + + /** Whether starting to drag this element is disabled. */ + @Input('cdkDragDisabled') + get disabled(): boolean { + return this._disabled || (this.dropContainer && this.dropContainer.disabled); + } + set disabled(value: boolean) { + this._disabled = coerceBooleanProperty(value); + } + private _disabled = false; + + /** Emits when the user starts dragging the item. */ + @Output('cdkDragStarted') started: EventEmitter = new EventEmitter(); + + /** Emits when the user stops dragging an item in the container. */ + @Output('cdkDragEnded') ended: EventEmitter = new EventEmitter(); + + /** Emits when the user has moved the item into a new container. */ + @Output('cdkDragEntered') entered: EventEmitter> = + new EventEmitter>(); + + /** Emits when the user removes the item its container by dragging it into another container. */ + @Output('cdkDragExited') exited: EventEmitter> = + new EventEmitter>(); + + /** Emits when the user drops the item inside a container. */ + @Output('cdkDragDropped') dropped: EventEmitter> = + new EventEmitter>(); + + /** + * Emits as the user is dragging the item. Use with caution, + * because this event will fire for every pixel that the user has dragged. + */ + @Output('cdkDragMoved') moved: Observable> = + Observable.create((observer: Observer>) => { + const subscription = this._dragRef.moved.pipe(map(movedEvent => ({ + source: this, + pointerPosition: movedEvent.pointerPosition, + event: movedEvent.event, + delta: movedEvent.delta + }))).subscribe(observer); + + return () => { + subscription.unsubscribe(); + }; + }); + + constructor( + /** Element that the draggable is attached to. */ + public element: ElementRef, + /** Droppable container that the draggable is a part of. */ + @Inject(CDK_DROP_LIST) @Optional() @SkipSelf() + public dropContainer: CdkDropList, + @Inject(DOCUMENT) private _document: any, + private _ngZone: NgZone, + private _viewContainerRef: ViewContainerRef, + private _viewportRuler: ViewportRuler, + private _dragDropRegistry: DragDropRegistry, + @Inject(CDK_DRAG_CONFIG) private _config: DragRefConfig, + @Optional() private _dir: Directionality) { + + const ref = this._dragRef = new DragRef(element, this._document, this._ngZone, + this._viewContainerRef, this._viewportRuler, this._dragDropRegistry, + this._config, this.dropContainer ? this.dropContainer._dropListRef : undefined, + this._dir); + ref.data = this; + ref.beforeStarted.subscribe(() => { + if (!ref.isDragging()) { + ref.disabled = this.disabled; + ref.lockAxis = this.lockAxis; + ref.withBoundaryElement(this._getBoundaryElement()); + } + }); + this._proxyEvents(ref); + } + + /** + * Returns the element that is being used as a placeholder + * while the current element is being dragged. + */ + getPlaceholderElement(): HTMLElement { + return this._dragRef.getPlaceholderElement(); + } + + /** Returns the root draggable element. */ + getRootElement(): HTMLElement { + return this._dragRef.getRootElement(); + } + + /** Resets a standalone drag item to its initial position. */ + reset(): void { + this._dragRef.reset(); + } + + ngAfterViewInit() { + // We need to wait for the zone to stabilize, in order for the reference + // element to be in the proper place in the DOM. This is mostly relevant + // for draggable elements inside portals since they get stamped out in + // their original DOM position and then they get transferred to the portal. + this._rootElementInitSubscription = this._ngZone.onStable.asObservable() + .pipe(take(1)) + .subscribe(() => { + const rootElement = this._getRootElement(); + + if (rootElement.nodeType !== this._document.ELEMENT_NODE) { + throw Error(`cdkDrag must be attached to an element node. ` + + `Currently attached to "${rootElement.nodeName}".`); + } + + this._dragRef + .withRootElement(rootElement) + .withPlaceholderTemplate(this._placeholderTemplate) + .withPreviewTemplate(this._previewTemplate); + + this._handles.changes + .pipe(startWith(this._handles)) + .subscribe((handleList: QueryList) => { + this._dragRef.withHandles(handleList.filter(handle => handle._parentDrag === this)); + }); + }); + } + + ngOnDestroy() { + this._rootElementInitSubscription.unsubscribe(); + this._dragRef.dispose(); + } + + /** Gets the root draggable element, based on the `rootElementSelector`. */ + private _getRootElement(): HTMLElement { + const element = this.element.nativeElement; + const rootElement = this.rootElementSelector ? + getClosestMatchingAncestor(element, this.rootElementSelector) : null; + + return rootElement || element; + } + + /** Gets the boundary element, based on the `boundaryElementSelector`. */ + private _getBoundaryElement() { + const selector = this.boundaryElementSelector; + return selector ? getClosestMatchingAncestor(this.element.nativeElement, selector) : null; + } + + /** + * Proxies the events from a DragRef to events that + * match the interfaces of the CdkDrag outputs. + */ + private _proxyEvents(ref: DragRef>) { + ref.started.subscribe(() => { + this.started.emit({source: this}); + }); + + ref.ended.subscribe(() => { + this.ended.emit({source: this}); + }); + + ref.entered.subscribe(event => { + this.entered.emit({ + container: event.container.data, + item: this + }); + }); + + ref.exited.subscribe(event => { + this.exited.emit({ + container: event.container.data, + item: this + }); + }); + + ref.dropped.subscribe(event => { + this.dropped.emit({ + previousIndex: event.previousIndex, + currentIndex: event.currentIndex, + previousContainer: event.previousContainer.data, + container: event.container.data, + isPointerOverContainer: event.isPointerOverContainer, + item: this + }); + }); + } +} + +/** Gets the closest ancestor of an element that matches a selector. */ +function getClosestMatchingAncestor(element: HTMLElement, selector: string) { + let currentElement = element.parentElement as HTMLElement | null; + + while (currentElement) { + // IE doesn't support `matches` so we have to fall back to `msMatchesSelector`. + if (currentElement.matches ? currentElement.matches(selector) : + (currentElement as any).msMatchesSelector(selector)) { + return currentElement; + } + + currentElement = currentElement.parentElement; + } + + return null; +} + diff --git a/src/cdk/drag-drop/drop-list-group.ts b/src/cdk/drag-drop/directives/drop-list-group.ts similarity index 100% rename from src/cdk/drag-drop/drop-list-group.ts rename to src/cdk/drag-drop/directives/drop-list-group.ts diff --git a/src/cdk/drag-drop/directives/drop-list.ts b/src/cdk/drag-drop/directives/drop-list.ts new file mode 100644 index 000000000000..ae5aaa50a4dc --- /dev/null +++ b/src/cdk/drag-drop/directives/drop-list.ts @@ -0,0 +1,318 @@ +/** + * @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 {coerceArray, coerceBooleanProperty} from '@angular/cdk/coercion'; +import { + ContentChildren, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnDestroy, + Output, + QueryList, + Optional, + Directive, + ChangeDetectorRef, + SkipSelf, + Inject, +} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; +import {Directionality} from '@angular/cdk/bidi'; +import {CdkDrag} from './drag'; +import {DragDropRegistry} from '../drag-drop-registry'; +import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from '../drag-events'; +import {CDK_DROP_LIST_CONTAINER, CdkDropListContainer} from '../drop-list-container'; +import {CdkDropListGroup} from './drop-list-group'; +import {DropListRef} from '../drop-list-ref'; +import {DragRef} from '../drag-ref'; + +/** Counter used to generate unique ids for drop zones. */ +let _uniqueIdCounter = 0; + +/** + * Internal compile-time-only representation of a `CdkDropList`. + * Used to avoid circular import issues between the `CdkDropList` and the `CdkDrag`. + * @docs-private + */ +export interface CdkDropListInternal extends CdkDropList {} + +// @breaking-change 8.0.0 `CdkDropList` implements `CdkDropListContainer` for backwards +// compatiblity. The implements clause, as well as all the methods that it enforces can +// be removed when `CdkDropListContainer` is deleted. + +/** Container that wraps a set of draggable items. */ +@Directive({ + selector: '[cdkDropList], cdk-drop-list', + exportAs: 'cdkDropList', + providers: [ + // Prevent child drop lists from picking up the same group as their parent. + {provide: CdkDropListGroup, useValue: undefined}, + {provide: CDK_DROP_LIST_CONTAINER, useExisting: CdkDropList}, + ], + host: { + 'class': 'cdk-drop-list', + '[id]': 'id', + '[class.cdk-drop-list-dragging]': '_dropListRef.isDragging()' + } +}) +export class CdkDropList implements CdkDropListContainer, OnDestroy { + /** Keeps track of the drop lists that are currently on the page. */ + private static _dropLists: CdkDropList[] = []; + + /** Reference to the underlying drop list instance. */ + _dropListRef: DropListRef>; + + /** 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. Can either be references to other drop containers, + * or their unique IDs. + */ + @Input('cdkDropListConnectedTo') + connectedTo: (CdkDropList | string)[] | CdkDropList | string = []; + + /** Arbitrary data to attach to this container. */ + @Input('cdkDropListData') data: T; + + /** Direction in which the list is oriented. */ + @Input('cdkDropListOrientation') orientation: 'horizontal' | 'vertical' = 'vertical'; + + /** + * Unique ID for the drop zone. Can be used as a reference + * in the `connectedTo` of another `CdkDropList`. + */ + @Input() id: string = `cdk-drop-list-${_uniqueIdCounter++}`; + + /** Locks the position of the draggable elements inside the container along the specified axis. */ + @Input('cdkDropListLockAxis') lockAxis: 'x' | 'y'; + + /** Whether starting a dragging sequence from this container is disabled. */ + @Input('cdkDropListDisabled') + get disabled(): boolean { return this._dropListRef.disabled; } + set disabled(value: boolean) { + this._dropListRef.disabled = coerceBooleanProperty(value); + } + + /** + * Function that is used to determine whether an item + * is allowed to be moved into a drop container. + */ + @Input('cdkDropListEnterPredicate') + enterPredicate: (drag: CdkDrag, drop: CdkDropList) => boolean = () => true + + /** Emits when the user drops an item inside the container. */ + @Output('cdkDropListDropped') + dropped: EventEmitter> = new EventEmitter>(); + + /** + * Emits when the user has moved a new drag item into this container. + */ + @Output('cdkDropListEntered') + entered: EventEmitter> = new EventEmitter>(); + + /** + * Emits when the user removes an item from the container + * by dragging it into another container. + */ + @Output('cdkDropListExited') + exited: EventEmitter> = new EventEmitter>(); + + /** Emits as the user is swapping items while actively dragging. */ + @Output('cdkDropListSorted') + sorted: EventEmitter> = new EventEmitter>(); + + constructor( + public element: ElementRef, + dragDropRegistry: DragDropRegistry, + private _changeDetectorRef: ChangeDetectorRef, + @Optional() dir?: Directionality, + @Optional() @SkipSelf() private _group?: CdkDropListGroup, + // @breaking-change 8.0.0 `_document` parameter to be made required. + @Optional() @Inject(DOCUMENT) _document?: any) { + + + // @breaking-change 8.0.0 Remove || once `_document` parameter is required. + const ref = this._dropListRef = new DropListRef(element, dragDropRegistry, + _document || document, dir); + ref.data = this; + ref.enterPredicate = (drag: DragRef, drop: DropListRef) => { + return this.enterPredicate(drag.data, drop.data); + }; + this._syncInputs(ref); + this._proxyEvents(ref); + CdkDropList._dropLists.push(this); + + if (_group) { + _group._items.add(this); + } + } + + ngOnDestroy() { + const index = CdkDropList._dropLists.indexOf(this); + this._dropListRef.dispose(); + + if (index > -1) { + CdkDropList._dropLists.splice(index, 1); + } + + if (this._group) { + this._group._items.delete(this); + } + } + + /** Starts dragging an item. */ + start(): void { + this._dropListRef.start(); + } + + /** + * Drops an item into this container. + * @param item Item being dropped into the container. + * @param currentIndex Index at which the item should be inserted. + * @param previousContainer Container from which the item got dragged in. + * @param isPointerOverContainer Whether the user's pointer was over the + * container when the item was dropped. + */ + drop(item: CdkDrag, currentIndex: number, previousContainer: Partial, + isPointerOverContainer: boolean): void { + this._dropListRef.drop(item._dragRef, currentIndex, + (previousContainer as CdkDropList)._dropListRef, isPointerOverContainer); + } + + /** + * Emits an event to indicate that the user moved an item into the container. + * @param item Item that was moved into the container. + * @param pointerX Position of the item along the X axis. + * @param pointerY Position of the item along the Y axis. + */ + enter(item: CdkDrag, pointerX: number, pointerY: number): void { + this._dropListRef.enter(item._dragRef, pointerX, pointerY); + } + + /** + * Removes an item from the container after it was dragged into another container by the user. + * @param item Item that was dragged out. + */ + exit(item: CdkDrag): void { + this._dropListRef.exit(item._dragRef); + } + + /** + * Figures out the index of an item in the container. + * @param item Item whose index should be determined. + */ + getItemIndex(item: CdkDrag): number { + return this._dropListRef.getItemIndex(item._dragRef); + } + + /** + * Sorts an item inside the container based on its position. + * @param item Item to be sorted. + * @param pointerX Position of the item along the X axis. + * @param pointerY Position of the item along the Y axis. + * @param pointerDelta Direction in which the pointer is moving along each axis. + */ + _sortItem(item: CdkDrag, pointerX: number, pointerY: number, + pointerDelta: {x: number, y: number}): void { + return this._dropListRef._sortItem(item._dragRef, pointerX, pointerY, pointerDelta); + } + + /** + * Figures out whether an item should be moved into a sibling + * drop container, based on its current position. + * @param item Drag item that is being moved. + * @param x Position of the item along the X axis. + * @param y Position of the item along the Y axis. + */ + _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): + CdkDropListContainer | null { + const result = this._dropListRef._getSiblingContainerFromPosition(item._dragRef, x, y); + return result ? result.data : null; + } + + /** + * Checks whether the user's pointer is positioned over the container. + * @param x Pointer position along the X axis. + * @param y Pointer position along the Y axis. + */ + _isOverContainer(x: number, y: number): boolean { + return this._dropListRef._isOverContainer(x, y); + } + + /** Syncs the inputs of the CdkDropList with the options of the underlying DropListRef. */ + private _syncInputs(ref: DropListRef) { + ref.beforeStarted.subscribe(() => { + const siblings = coerceArray(this.connectedTo).map(drop => { + return typeof drop === 'string' ? + CdkDropList._dropLists.find(list => list.id === drop)! : drop; + }); + + if (this._group) { + this._group._items.forEach(drop => { + if (siblings.indexOf(drop) === -1) { + siblings.push(drop); + } + }); + } + + ref.lockAxis = this.lockAxis; + ref + .connectedTo(siblings.filter(drop => drop && drop !== this).map(list => list._dropListRef)) + .withOrientation(this.orientation) + .withItems(this._draggables.map(drag => drag._dragRef)); + }); + } + + /** + * Proxies the events from a DropListRef to events that + * match the interfaces of the CdkDropList outputs. + */ + private _proxyEvents(ref: DropListRef) { + ref.beforeStarted.subscribe(() => { + this._changeDetectorRef.markForCheck(); + }); + + ref.entered.subscribe(event => { + this.entered.emit({ + container: this, + item: event.item.data + }); + }); + + ref.exited.subscribe(event => { + this.exited.emit({ + container: this, + item: event.item.data + }); + }); + + ref.sorted.subscribe(event => { + this.sorted.emit({ + previousIndex: event.previousIndex, + currentIndex: event.currentIndex, + container: this, + item: event.item.data + }); + }); + + ref.dropped.subscribe(event => { + this.dropped.emit({ + previousIndex: event.previousIndex, + currentIndex: event.currentIndex, + previousContainer: event.previousContainer.data, + container: event.container.data, + item: event.item.data, + isPointerOverContainer: event.isPointerOverContainer + }); + }); + } + +} diff --git a/src/cdk/drag-drop/drag-drop-module.ts b/src/cdk/drag-drop/drag-drop-module.ts index b6237a9bb8ca..6ccc01f1b532 100644 --- a/src/cdk/drag-drop/drag-drop-module.ts +++ b/src/cdk/drag-drop/drag-drop-module.ts @@ -7,12 +7,12 @@ */ import {NgModule} from '@angular/core'; -import {CdkDropList} from './drop-list'; -import {CdkDropListGroup} from './drop-list-group'; -import {CdkDrag} from './drag'; -import {CdkDragHandle} from './drag-handle'; -import {CdkDragPreview} from './drag-preview'; -import {CdkDragPlaceholder} from './drag-placeholder'; +import {CdkDropList} from './directives/drop-list'; +import {CdkDropListGroup} from './directives/drop-list-group'; +import {CdkDrag} from './directives/drag'; +import {CdkDragHandle} from './directives/drag-handle'; +import {CdkDragPreview} from './directives/drag-preview'; +import {CdkDragPlaceholder} from './directives/drag-placeholder'; @NgModule({ declarations: [ diff --git a/src/cdk/drag-drop/drag-drop-registry.spec.ts b/src/cdk/drag-drop/drag-drop-registry.spec.ts index 8426b74a0879..65fa16aa7939 100644 --- a/src/cdk/drag-drop/drag-drop-registry.spec.ts +++ b/src/cdk/drag-drop/drag-drop-registry.spec.ts @@ -9,8 +9,8 @@ import { } from '@angular/cdk/testing'; import {DragDropRegistry} from './drag-drop-registry'; import {DragDropModule} from './drag-drop-module'; -import {CdkDrag} from './drag'; -import {CdkDropList} from './drop-list'; +import {CdkDrag} from './directives/drag'; +import {CdkDropList} from './directives/drop-list'; describe('DragDropRegistry', () => { let fixture: ComponentFixture; @@ -160,18 +160,6 @@ describe('DragDropRegistry', () => { expect(() => registry.registerDropContainer(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); }); diff --git a/src/cdk/drag-drop/drag-drop-registry.ts b/src/cdk/drag-drop/drag-drop-registry.ts index 38112c50ad72..54380328b3a6 100644 --- a/src/cdk/drag-drop/drag-drop-registry.ts +++ b/src/cdk/drag-drop/drag-drop-registry.ts @@ -166,7 +166,11 @@ export class DragDropRegistry implements OnDestroy { return this._activeDragInstances.has(drag); } - /** Gets a drop container by its id. */ + /** + * Gets a drop container by its id. + * @deprecated No longer being used. To be removed. + * @breaking-change 8.0.0 + */ getDropContainer(id: string): C | undefined { return Array.from(this._dropInstances).find(instance => instance.id === id); } diff --git a/src/cdk/drag-drop/drag-events.ts b/src/cdk/drag-drop/drag-events.ts index be00099243be..57ffc86e9e1e 100644 --- a/src/cdk/drag-drop/drag-events.ts +++ b/src/cdk/drag-drop/drag-events.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {CdkDrag} from './drag'; -import {CdkDropListContainer} from './drop-list-container'; +import {CdkDrag} from './directives/drag'; +import {CdkDropList} from './directives/drop-list'; /** Event emitted when the user starts dragging a draggable. */ export interface CdkDragStart { @@ -24,7 +24,7 @@ export interface CdkDragEnd { /** Event emitted when the user moves an item into a new drop container. */ export interface CdkDragEnter { /** Container into which the user has moved the item. */ - container: CdkDropListContainer; + container: CdkDropList; /** Item that was removed from the container. */ item: CdkDrag; } @@ -35,7 +35,7 @@ export interface CdkDragEnter { */ export interface CdkDragExit { /** Container from which the user has a removed an item. */ - container: CdkDropListContainer; + container: CdkDropList; /** Item that was removed from the container. */ item: CdkDrag; } @@ -50,9 +50,9 @@ export interface CdkDragDrop { /** Item that is being dropped. */ item: CdkDrag; /** Container in which the item was dropped. */ - container: CdkDropListContainer; + container: CdkDropList; /** Container from which the item was picked up. Can be the same as the `container`. */ - previousContainer: CdkDropListContainer; + previousContainer: CdkDropList; /** Whether the user's pointer was over the container when the item was dropped. */ isPointerOverContainer: boolean; } @@ -81,7 +81,7 @@ export interface CdkDragSortEvent { /** Index that the item is currently in. */ currentIndex: number; /** Container that the item belongs to. */ - container: CdkDropListContainer; + container: CdkDropList; /** Item that is being sorted. */ item: CdkDrag; } diff --git a/src/cdk/drag-drop/drag.ts b/src/cdk/drag-drop/drag-ref.ts similarity index 72% rename from src/cdk/drag-drop/drag.ts rename to src/cdk/drag-drop/drag-ref.ts index 0e3b7d951354..fd7e97a41af0 100644 --- a/src/cdk/drag-drop/drag.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -6,56 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directionality} from '@angular/cdk/bidi'; +import {EmbeddedViewRef, ElementRef, NgZone, ViewContainerRef, TemplateRef} from '@angular/core'; import {ViewportRuler} from '@angular/cdk/scrolling'; -import {DOCUMENT} from '@angular/common'; -import { - AfterViewInit, - ContentChild, - ContentChildren, - Directive, - ElementRef, - EmbeddedViewRef, - EventEmitter, - Inject, - InjectionToken, - Input, - NgZone, - OnDestroy, - Optional, - Output, - QueryList, - SkipSelf, - ViewContainerRef, -} from '@angular/core'; +import {Directionality} from '@angular/cdk/bidi'; import {normalizePassiveListenerOptions} from '@angular/cdk/platform'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {Observable, Subject, Subscription, Observer} from 'rxjs'; -import {startWith, take} from 'rxjs/operators'; +import {Subscription, Subject, Observable, Observer} from 'rxjs'; +import {DropListRefInternal as DropListRef} from './drop-list-ref'; import {DragDropRegistry} from './drag-drop-registry'; -import { - CdkDragDrop, - CdkDragEnd, - CdkDragEnter, - CdkDragExit, - CdkDragMove, - CdkDragStart, -} from './drag-events'; -import {CdkDragHandle} from './drag-handle'; -import {CdkDragPlaceholder} from './drag-placeholder'; -import {CdkDragPreview} from './drag-preview'; -import {CDK_DROP_LIST_CONTAINER, CdkDropListContainer} from './drop-list-container'; -import {getTransformTransitionDurationInMs} from './transition-duration'; import {extendStyles, toggleNativeDragInteractions} from './drag-styling'; -import {CDK_DRAG_PARENT} from './drag-parent'; - - -// TODO(crisbeto): add auto-scrolling functionality. -// TODO(crisbeto): add an API for moving a draggable up/down the -// list programmatically. Useful for keyboard controls. +import {getTransformTransitionDurationInMs} from './transition-duration'; -/** Object that can be used to configure the behavior of CdkDrag. */ -export interface CdkDragConfig { +/** Object that can be used to configure the behavior of DragRef. */ +export interface DragRefConfig { /** * Minimum amount of pixels that the user should * drag, before the CDK initiates a drag sequence. @@ -69,17 +32,6 @@ export interface CdkDragConfig { pointerDirectionChangeThreshold: number; } -/** Injection token that can be used to configure the behavior of `CdkDrag`. */ -export const CDK_DRAG_CONFIG = new InjectionToken('CDK_DRAG_CONFIG', { - providedIn: 'root', - factory: CDK_DRAG_CONFIG_FACTORY -}); - -/** @docs-private */ -export function CDK_DRAG_CONFIG_FACTORY(): CdkDragConfig { - return {dragStartThreshold: 5, pointerDirectionChangeThreshold: 5}; -} - /** Options that can be used to bind a passive event listener. */ const passiveEventListenerOptions = normalizePassiveListenerOptions({passive: true}); @@ -94,22 +46,36 @@ const activeEventListenerOptions = normalizePassiveListenerOptions({passive: fal */ const MOUSE_EVENT_IGNORE_TIME = 800; -/** Element that can be moved inside a CdkDropList container. */ -@Directive({ - selector: '[cdkDrag]', - exportAs: 'cdkDrag', - host: { - 'class': 'cdk-drag', - '[class.cdk-drag-dragging]': '_hasStartedDragging && _isDragging()', - }, - providers: [{ - provide: CDK_DRAG_PARENT, - useExisting: CdkDrag - }] -}) -export class CdkDrag implements AfterViewInit, OnDestroy { - private _document: Document; +/** + * Template that can be used to create a drag helper element (e.g. a preview or a placeholder). + */ +interface DragHelperTemplate { + templateRef: TemplateRef; + data: T; +} + +interface DragHandle { + element: ElementRef; + disabled: boolean; +} + +// TODO(crisbeto): add auto-scrolling functionality. +// TODO(crisbeto): add an API for moving a draggable up/down the +// list programmatically. Useful for keyboard controls. + +/** + * Internal compile-time-only representation of a `DragRef`. + * Used to avoid circular import issues between the `DragRef` and the `DropListRef`. + * @docs-private + */ +export interface DragRefInternal extends DragRef {} + +/** + * Reference to a draggable item. Used to manipulate or dispose of the item. + * @docs-private + */ +export class DragRef { /** Element displayed next to the user's pointer while the element is dragged. */ private _preview: HTMLElement; @@ -152,19 +118,24 @@ export class CdkDrag implements AfterViewInit, OnDestroy { * Whether the dragging sequence has been started. Doesn't * necessarily mean that the element has been moved. */ - _hasStartedDragging: boolean; + private _hasStartedDragging: boolean; /** Whether the element has moved since the user started dragging it. */ private _hasMoved: boolean; - /** Drop container in which the CdkDrag resided when dragging began. */ - private _initialContainer: CdkDropListContainer; + /** Drop container in which the DragRef resided when dragging began. */ + private _initialContainer: DropListRef; /** Cached scroll position on the page when the element was picked up. */ private _scrollPosition: {top: number, left: number}; /** Emits when the item is being moved. */ - private _moveEvents = new Subject>(); + private _moveEvents = new Subject<{ + source: DragRef; + pointerPosition: {x: number, y: number}; + event: MouseEvent | TouchEvent; + delta: {x: -1 | 0 | 1, y: -1 | 0 | 1}; + }>(); /** * Amount of subscriptions to the move event. Used to avoid @@ -178,7 +149,10 @@ export class CdkDrag implements AfterViewInit, OnDestroy { /** Pointer position at which the last change in the delta occurred. */ private _pointerPositionAtLastDirectionChange: Point; - /** Root element that will be dragged by the user. */ + /** + * Root DOM node of the drag instance. This is the element that will + * be moved around as the user is dragging. + */ private _rootElement: HTMLElement; /** Subscription to pointer movement events. */ @@ -186,6 +160,7 @@ export class CdkDrag implements AfterViewInit, OnDestroy { /** Subscription to the event that is dispatched when the user lifts their pointer. */ private _pointerUpSubscription = Subscription.EMPTY; + /** * Time at which the last touch event occurred. Used to avoid firing the same * events multiple times on touch devices where the browser will fire a fake @@ -193,11 +168,8 @@ export class CdkDrag implements AfterViewInit, OnDestroy { */ private _lastTouchEventTime: number; - /** Subscription to the stream that initializes the root element. */ - private _rootElementInitSubscription = Subscription.EMPTY; - /** Cached reference to the boundary element. */ - private _boundaryElement?: HTMLElement; + private _boundaryElement: HTMLElement | null = null; /** Cached dimensions of the preview element. */ private _previewRect?: ClientRect; @@ -205,94 +177,92 @@ export class CdkDrag implements AfterViewInit, OnDestroy { /** Cached dimensions of the boundary element. */ private _boundaryRect?: ClientRect; - /** Elements that can be used to drag the draggable item. */ - @ContentChildren(CdkDragHandle, {descendants: true}) _handles: QueryList; - /** Element that will be used as a template to create the draggable item's preview. */ - @ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview; + private _previewTemplate: DragHelperTemplate | null; /** Template for placeholder element rendered to show where a draggable would be dropped. */ - @ContentChild(CdkDragPlaceholder) _placeholderTemplate: CdkDragPlaceholder; + private _placeholderTemplate: DragHelperTemplate | null; - /** Arbitrary data to attach to this drag instance. */ - @Input('cdkDragData') data: T; + /** Elements that can be used to drag the draggable item. */ + private _handles: DragHandle[] = []; - /** Locks the position of the dragged element along the specified axis. */ - @Input('cdkDragLockAxis') lockAxis: 'x' | 'y'; + /** Whether the native interactions on the element are enabled. */ + private _nativeInteractionsEnabled = true; - /** - * Selector that will be used to determine the root draggable element, starting from - * the `cdkDrag` element and going up the DOM. Passing an alternate root element is useful - * when trying to enable dragging on an element that you might not have access to. - */ - @Input('cdkDragRootElement') rootElementSelector: string; - - /** - * Selector that will be used to determine the element to which the draggable's position will - * be constrained. Matching starts from the element's parent and goes up the DOM until a matching - * element has been found. - */ - @Input('cdkDragBoundary') boundaryElementSelector: string; + /** Axis along which dragging is locked. */ + lockAxis: 'x' | 'y'; /** Whether starting to drag this element is disabled. */ - @Input('cdkDragDisabled') get disabled(): boolean { - return this._disabled || (this.dropContainer && this.dropContainer.disabled); + return this._disabled || !!(this.dropContainer && this.dropContainer.disabled); } set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } private _disabled = false; + /** Emits as the drag sequence is being prepared. */ + beforeStarted = new Subject(); + /** Emits when the user starts dragging the item. */ - @Output('cdkDragStarted') started: EventEmitter = new EventEmitter(); + started = new Subject<{source: DragRef}>(); /** Emits when the user stops dragging an item in the container. */ - @Output('cdkDragEnded') ended: EventEmitter = new EventEmitter(); + ended = new Subject<{source: DragRef}>(); /** Emits when the user has moved the item into a new container. */ - @Output('cdkDragEntered') entered: EventEmitter> = - new EventEmitter>(); + entered = new Subject<{container: DropListRef, item: DragRef}>(); /** Emits when the user removes the item its container by dragging it into another container. */ - @Output('cdkDragExited') exited: EventEmitter> = - new EventEmitter>(); + exited = new Subject<{container: DropListRef, item: DragRef}>(); /** Emits when the user drops the item inside a container. */ - @Output('cdkDragDropped') dropped: EventEmitter> = - new EventEmitter>(); + dropped = new Subject<{ + previousIndex: number; + currentIndex: number; + item: DragRef; + container: DropListRef; + previousContainer: DropListRef; + isPointerOverContainer: boolean; + }>(); /** * Emits as the user is dragging the item. Use with caution, * because this event will fire for every pixel that the user has dragged. */ - @Output('cdkDragMoved') moved: Observable> = - Observable.create((observer: Observer>) => { - const subscription = this._moveEvents.subscribe(observer); - this._moveEventSubscriptions++; - - return () => { - subscription.unsubscribe(); - this._moveEventSubscriptions--; - }; - }); + moved: Observable<{ + source: DragRef; + pointerPosition: {x: number, y: number}; + event: MouseEvent | TouchEvent; + delta: {x: -1 | 0 | 1, y: -1 | 0 | 1}; + }> = Observable.create((observer: Observer) => { + const subscription = this._moveEvents.subscribe(observer); + this._moveEventSubscriptions++; + + return () => { + subscription.unsubscribe(); + this._moveEventSubscriptions--; + }; + }); + + /** Arbitrary data that can be attached to the drag item. */ + data: T; constructor( - /** Element that the draggable is attached to. */ - public element: ElementRef, - /** Droppable container that the draggable is a part of. */ - @Inject(CDK_DROP_LIST_CONTAINER) @Optional() @SkipSelf() - public dropContainer: CdkDropListContainer, - @Inject(DOCUMENT) document: any, + element: ElementRef | HTMLElement, + private _document: Document, private _ngZone: NgZone, private _viewContainerRef: ViewContainerRef, private _viewportRuler: ViewportRuler, - private _dragDropRegistry: DragDropRegistry, CdkDropListContainer>, - @Inject(CDK_DRAG_CONFIG) private _config: CdkDragConfig, - @Optional() private _dir: Directionality) { - this._document = document; - _dragDropRegistry.registerDragItem(this); - } + private _dragDropRegistry: DragDropRegistry, + private _config: DragRefConfig, + /** Droppable container that the draggable is a part of. */ + public dropContainer?: DropListRef, + private _dir?: Directionality) { + + this.withRootElement(element); + _dragDropRegistry.registerDragItem(this); + } /** * Returns the element that is being used as a placeholder @@ -307,177 +277,150 @@ export class CdkDrag implements AfterViewInit, OnDestroy { return this._rootElement; } - /** Resets a standalone drag item to its initial position. */ - reset(): void { - this._rootElement.style.transform = ''; - this._activeTransform = {x: 0, y: 0}; - this._passiveTransform = {x: 0, y: 0}; + /** Registers the handles that can be used to drag the element. */ + withHandles(handles: DragHandle[]): this { + // TODO(crisbeto): have this accept HTMLElement[] | ElementRef[] + this._handles = handles; + handles.forEach(handle => toggleNativeDragInteractions(handle.element.nativeElement, false)); + this._toggleNativeDragInteractions(); + return this; } - ngAfterViewInit() { - // We need to wait for the zone to stabilize, in order for the reference - // element to be in the proper place in the DOM. This is mostly relevant - // for draggable elements inside portals since they get stamped out in - // their original DOM position and then they get transferred to the portal. - this._rootElementInitSubscription = this._ngZone.onStable.asObservable() - .pipe(take(1)) - .subscribe(() => { - const rootElement = this._rootElement = this._getRootElement(); - - if (rootElement.nodeType !== this._document.ELEMENT_NODE) { - throw Error(`cdkDrag must be attached to an element node. ` + - `Currently attached to "${rootElement.nodeName}".`); - } - - rootElement.addEventListener('mousedown', this._pointerDown, activeEventListenerOptions); - rootElement.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions); - this._handles.changes.pipe(startWith(null)).subscribe(() => - toggleNativeDragInteractions(rootElement, this.getChildHandles().length > 0)); - }); + /** Registers the template that should be used for the drag preview. */ + withPreviewTemplate(template: DragHelperTemplate | null): this { + // TODO(crisbeto): have this accept a TemplateRef + this._previewTemplate = template; + return this; + } + + /** Registers the template that should be used for the drag placeholder. */ + withPlaceholderTemplate(template: DragHelperTemplate | null): this { + // TODO(crisbeto): have this accept a TemplateRef + this._placeholderTemplate = template; + return this; } - ngOnDestroy() { - // The directive might have been destroyed before the root element is initialized. - if (this._rootElement) { - this._rootElement.removeEventListener('mousedown', this._pointerDown, - activeEventListenerOptions); - this._rootElement.removeEventListener('touchstart', this._pointerDown, - passiveEventListenerOptions); - - // Do this check before removing from the registry since it'll - // stop being considered as dragged once it is removed. - if (this._isDragging()) { - // 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._rootElement); + + /** + * Sets an alternate drag root element. The root element is the element that will be moved as + * the user is dragging. Passing an alternate root element is useful when trying to enable + * dragging on an element that you might not have access to. + */ + withRootElement(rootElement: ElementRef | HTMLElement): this { + const element = rootElement instanceof ElementRef ? rootElement.nativeElement : rootElement; + + if (element !== this._rootElement) { + if (this._rootElement) { + this._removeRootElementListeners(this._rootElement); } + + element.addEventListener('mousedown', this._pointerDown, activeEventListenerOptions); + element.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions); + this._rootElement = element; + } + + return this; + } + + /** + * Element to which the draggable's position will be constrained. + */ + withBoundaryElement(boundaryElement: ElementRef | HTMLElement | null): this { + this._boundaryElement = boundaryElement instanceof ElementRef ? + boundaryElement.nativeElement : boundaryElement; + return this; + } + + /** Removes the dragging functionality from the DOM element. */ + dispose() { + this._removeRootElementListeners(this._rootElement); + + // Do this check before removing from the registry since it'll + // stop being considered as dragged once it is removed. + if (this.isDragging()) { + // 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. + removeElement(this._rootElement); } - this._rootElementInitSubscription.unsubscribe(); this._destroyPreview(); this._destroyPlaceholder(); - this._boundaryElement = this._nextSibling = null!; this._dragDropRegistry.removeDragItem(this); this._removeSubscriptions(); + this.beforeStarted.complete(); + this.started.complete(); + this.ended.complete(); + this.entered.complete(); + this.exited.complete(); + this.dropped.complete(); this._moveEvents.complete(); + this._handles = []; + this._boundaryElement = this._rootElement = this._placeholderTemplate = + this._previewTemplate = this._nextSibling = null!; } /** Checks whether the element is currently being dragged. */ - _isDragging() { - return this._dragDropRegistry.isDragging(this); + isDragging(): boolean { + return this._hasStartedDragging && this._dragDropRegistry.isDragging(this); } - /** Gets only handles that are not inside descendant `CdkDrag` instances. */ - private getChildHandles() { - return this._handles.filter(handle => handle._parentDrag === this); + /** Resets a standalone drag item to its initial position. */ + reset(): void { + this._rootElement.style.transform = ''; + this._activeTransform = {x: 0, y: 0}; + this._passiveTransform = {x: 0, y: 0}; } - /** Handler for the `mousedown`/`touchstart` events. */ - _pointerDown = (event: MouseEvent | TouchEvent) => { - const handles = this.getChildHandles(); - - // Delegate the event based on whether it started from a handle or the element itself. - if (handles.length) { - const targetHandle = handles.find(handle => { - const element = handle.element.nativeElement; - const target = event.target; - return !!target && (target === element || element.contains(target as HTMLElement)); - }); - - if (targetHandle && !targetHandle.disabled && !this.disabled) { - this._initializeDragSequence(targetHandle.element.nativeElement, event); - } - } else if (!this.disabled) { - this._initializeDragSequence(this._rootElement, event); - } + /** Unsubscribes from the global subscriptions. */ + private _removeSubscriptions() { + this._pointerMoveSubscription.unsubscribe(); + this._pointerUpSubscription.unsubscribe(); } - /** - * Sets up the different variables and subscriptions - * that will be necessary for the dragging sequence. - * @param referenceElement Element that started the drag sequence. - * @param event Browser event object that started the sequence. - */ - private _initializeDragSequence(referenceElement: HTMLElement, event: MouseEvent | TouchEvent) { - // Always stop propagation for the event that initializes - // the dragging sequence, in order to prevent it from potentially - // starting another sequence for a draggable parent somewhere up the DOM tree. - event.stopPropagation(); - - const isDragging = this._isDragging(); - const isTouchEvent = this._isTouchEvent(event); - const isAuxiliaryMouseButton = !isTouchEvent && (event as MouseEvent).button !== 0; - const isSyntheticEvent = !isTouchEvent && this._lastTouchEventTime && - this._lastTouchEventTime + MOUSE_EVENT_IGNORE_TIME > Date.now(); - - // If the event started from an element with the native HTML drag&drop, it'll interfere - // with our own dragging (e.g. `img` tags do it by default). Prevent the default action - // to stop it from happening. Note that preventing on `dragstart` also seems to work, but - // it's flaky and it fails if the user drags it away quickly. Also note that we only want - // to do this for `mousedown` since doing the same for `touchstart` will stop any `click` - // events from firing on touch devices. - if (event.target && (event.target as HTMLElement).draggable && event.type === 'mousedown') { - event.preventDefault(); + /** Destroys the preview element and its ViewRef. */ + private _destroyPreview() { + if (this._preview) { + removeElement(this._preview); } - // Abort if the user is already dragging or is using a mouse button other than the primary one. - if (isDragging || isAuxiliaryMouseButton || isSyntheticEvent) { - return; + if (this._previewRef) { + this._previewRef.destroy(); } - // Cache the previous transform amount only after the first drag sequence, because - // we don't want our own transforms to stack on top of each other. - if (this._initialTransform == null) { - this._initialTransform = this._rootElement.style.transform || ''; - } + this._preview = this._previewRef = null!; + } - this._hasStartedDragging = this._hasMoved = false; - this._initialContainer = this.dropContainer; - this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove); - this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp); - this._scrollPosition = this._viewportRuler.getViewportScrollPosition(); - this._boundaryElement = this._getBoundaryElement(); + /** Destroys the placeholder element and its ViewRef. */ + private _destroyPlaceholder() { + if (this._placeholder) { + removeElement(this._placeholder); + } - if (this._boundaryElement) { - this._boundaryRect = this._boundaryElement.getBoundingClientRect(); + if (this._placeholderRef) { + this._placeholderRef.destroy(); } - // If we have a custom preview template, the element won't be visible anyway so we avoid the - // extra `getBoundingClientRect` calls and just move the preview next to the cursor. - this._pickupPositionInElement = this._previewTemplate ? {x: 0, y: 0} : - this._getPointerPositionInElement(referenceElement, event); - const pointerPosition = this._pickupPositionOnPage = this._getPointerPositionOnPage(event); - this._pointerDirectionDelta = {x: 0, y: 0}; - this._pointerPositionAtLastDirectionChange = {x: pointerPosition.x, y: pointerPosition.y}; - this._dragDropRegistry.startDragging(this, event); + this._placeholder = this._placeholderRef = null!; } - /** Starts the dragging sequence. */ - private _startDragSequence(event: MouseEvent | TouchEvent) { - // Emit the event on the item before the one on the container. - this.started.emit({source: this}); - - if (this._isTouchEvent(event)) { - this._lastTouchEventTime = Date.now(); - } - if (this.dropContainer) { - const element = this._rootElement; - - // Grab the `nextSibling` before the preview and placeholder - // have been created so we don't get the preview by accident. - this._nextSibling = element.nextSibling; + /** Handler for the `mousedown`/`touchstart` events. */ + private _pointerDown = (event: MouseEvent | TouchEvent) => { + this.beforeStarted.next(); - const preview = this._preview = this._createPreviewElement(); - const placeholder = this._placeholder = this._createPlaceholderElement(); + // Delegate the event based on whether it started from a handle or the element itself. + if (this._handles.length) { + const targetHandle = this._handles.find(handle => { + const element = handle.element.nativeElement; + const target = event.target; + return !!target && (target === element || element.contains(target as HTMLElement)); + }); - // We move the element out at the end of the body and we make it hidden, because keeping it in - // place will throw off the consumer's `:last-child` selectors. We can't remove the element - // from the DOM completely, because iOS will stop firing all subsequent events in the chain. - element.style.display = 'none'; - this._document.body.appendChild(element.parentNode!.replaceChild(placeholder, element)); - this._document.body.appendChild(preview); - this.dropContainer.start(); + if (targetHandle && !targetHandle.disabled && !this.disabled) { + this._initializeDragSequence(targetHandle.element.nativeElement, event); + } + } else if (!this.disabled) { + this._initializeDragSequence(this._rootElement, event); } } @@ -552,7 +495,7 @@ export class CdkDrag implements AfterViewInit, OnDestroy { /** Handler that is invoked when the user lifts their pointer up, after initiating a drag. */ private _pointerUp = (event: MouseEvent | TouchEvent) => { - if (!this._isDragging()) { + if (!this.isDragging()) { return; } @@ -569,7 +512,7 @@ export class CdkDrag implements AfterViewInit, OnDestroy { // to the new passive transform. this._passiveTransform.x = this._activeTransform.x; this._passiveTransform.y = this._activeTransform.y; - this._ngZone.run(() => this.ended.emit({source: this})); + this._ngZone.run(() => this.ended.next({source: this})); this._dragDropRegistry.stopDragging(this); return; } @@ -580,6 +523,95 @@ export class CdkDrag implements AfterViewInit, OnDestroy { }); } + /** Starts the dragging sequence. */ + private _startDragSequence(event: MouseEvent | TouchEvent) { + // Emit the event on the item before the one on the container. + this.started.next({source: this}); + + if (isTouchEvent(event)) { + this._lastTouchEventTime = Date.now(); + } + + if (this.dropContainer) { + const element = this._rootElement; + + // Grab the `nextSibling` before the preview and placeholder + // have been created so we don't get the preview by accident. + this._nextSibling = element.nextSibling; + + const preview = this._preview = this._createPreviewElement(); + const placeholder = this._placeholder = this._createPlaceholderElement(); + + // We move the element out at the end of the body and we make it hidden, because keeping it in + // place will throw off the consumer's `:last-child` selectors. We can't remove the element + // from the DOM completely, because iOS will stop firing all subsequent events in the chain. + element.style.display = 'none'; + this._document.body.appendChild(element.parentNode!.replaceChild(placeholder, element)); + this._document.body.appendChild(preview); + this.dropContainer.start(); + } + } + + /** + * Sets up the different variables and subscriptions + * that will be necessary for the dragging sequence. + * @param referenceElement Element that started the drag sequence. + * @param event Browser event object that started the sequence. + */ + private _initializeDragSequence(referenceElement: HTMLElement, event: MouseEvent | TouchEvent) { + // Always stop propagation for the event that initializes + // the dragging sequence, in order to prevent it from potentially + // starting another sequence for a draggable parent somewhere up the DOM tree. + event.stopPropagation(); + + const isDragging = this.isDragging(); + const isTouchSequence = isTouchEvent(event); + const isAuxiliaryMouseButton = !isTouchSequence && (event as MouseEvent).button !== 0; + const isSyntheticEvent = !isTouchSequence && this._lastTouchEventTime && + this._lastTouchEventTime + MOUSE_EVENT_IGNORE_TIME > Date.now(); + + // If the event started from an element with the native HTML drag&drop, it'll interfere + // with our own dragging (e.g. `img` tags do it by default). Prevent the default action + // to stop it from happening. Note that preventing on `dragstart` also seems to work, but + // it's flaky and it fails if the user drags it away quickly. Also note that we only want + // to do this for `mousedown` since doing the same for `touchstart` will stop any `click` + // events from firing on touch devices. + if (event.target && (event.target as HTMLElement).draggable && event.type === 'mousedown') { + event.preventDefault(); + } + + // Abort if the user is already dragging or is using a mouse button other than the primary one. + if (isDragging || isAuxiliaryMouseButton || isSyntheticEvent) { + return; + } + + // Cache the previous transform amount only after the first drag sequence, because + // we don't want our own transforms to stack on top of each other. + if (this._initialTransform == null) { + this._initialTransform = this._rootElement.style.transform || ''; + } + + this._toggleNativeDragInteractions(); + this._hasStartedDragging = this._hasMoved = false; + this._initialContainer = this.dropContainer!; + this._pointerMoveSubscription = this._dragDropRegistry.pointerMove.subscribe(this._pointerMove); + this._pointerUpSubscription = this._dragDropRegistry.pointerUp.subscribe(this._pointerUp); + this._scrollPosition = this._viewportRuler.getViewportScrollPosition(); + + if (this._boundaryElement) { + this._boundaryRect = this._boundaryElement.getBoundingClientRect(); + } + + // If we have a custom preview template, the element won't be visible anyway so we avoid the + // extra `getBoundingClientRect` calls and just move the preview next to the cursor. + this._pickupPositionInElement = this._previewTemplate ? {x: 0, y: 0} : + this._getPointerPositionInElement(referenceElement, event); + const pointerPosition = this._pickupPositionOnPage = this._getPointerPositionOnPage(event); + this._pointerDirectionDelta = {x: 0, y: 0}; + this._pointerPositionAtLastDirectionChange = {x: pointerPosition.x, y: pointerPosition.y}; + this._dragDropRegistry.startDragging(this, event); + } + /** Cleans up the DOM artifacts that were added to facilitate the element being dragged. */ private _cleanupDragArtifacts(event: MouseEvent | TouchEvent) { // Restore the element's visibility and insert it at its old position in the DOM. @@ -600,20 +632,21 @@ export class CdkDrag implements AfterViewInit, OnDestroy { // Re-enter the NgZone since we bound `document` events on the outside. this._ngZone.run(() => { - const currentIndex = this.dropContainer.getItemIndex(this); + const container = this.dropContainer!; + const currentIndex = container.getItemIndex(this); const {x, y} = this._getPointerPositionOnPage(event); - const isPointerOverContainer = this.dropContainer._isOverContainer(x, y); + const isPointerOverContainer = container._isOverContainer(x, y); - this.ended.emit({source: this}); - this.dropped.emit({ + this.ended.next({source: this}); + this.dropped.next({ item: this, currentIndex, previousIndex: this._initialContainer.getItemIndex(this), - container: this.dropContainer, + container: container, previousContainer: this._initialContainer, isPointerOverContainer }); - this.dropContainer.drop(this, currentIndex, this._initialContainer, isPointerOverContainer); + container.drop(this, currentIndex, this._initialContainer, isPointerOverContainer); this.dropContainer = this._initialContainer; }); } @@ -624,7 +657,7 @@ export class CdkDrag implements AfterViewInit, OnDestroy { */ private _updateActiveDropContainer({x, y}: Point) { // Drop container that draggable has been moved into. - let newContainer = this.dropContainer._getSiblingContainerFromPosition(this, x, y); + let newContainer = this.dropContainer!._getSiblingContainerFromPosition(this, x, y); // If we couldn't find a new container to move the item into, and the item has left it's // initial container, check whether the it's over the initial container. This handles the @@ -638,16 +671,16 @@ export class CdkDrag implements AfterViewInit, OnDestroy { if (newContainer) { this._ngZone.run(() => { // Notify the old container that the item has left. - this.exited.emit({item: this, container: this.dropContainer}); - this.dropContainer.exit(this); + this.exited.next({item: this, container: this.dropContainer!}); + this.dropContainer!.exit(this); // Notify the new container that the item has entered. - this.entered.emit({item: this, container: newContainer!}); + this.entered.next({item: this, container: newContainer!}); this.dropContainer = newContainer!; this.dropContainer.enter(this, x, y); }); } - this.dropContainer._sortItem(this, x, y, this._pointerDirectionDelta); + this.dropContainer!._sortItem(this, x, y, this._pointerDirectionDelta); this._preview.style.transform = getTransform(x - this._pickupPositionInElement.x, y - this._pickupPositionInElement.y); } @@ -693,44 +726,6 @@ export class CdkDrag implements AfterViewInit, OnDestroy { return preview; } - /** Creates an element that will be shown instead of the current element while dragging. */ - private _createPlaceholderElement(): HTMLElement { - let placeholder: HTMLElement; - - if (this._placeholderTemplate) { - this._placeholderRef = this._viewContainerRef.createEmbeddedView( - this._placeholderTemplate.templateRef, - this._placeholderTemplate.data - ); - placeholder = this._placeholderRef.rootNodes[0]; - } else { - placeholder = deepCloneNode(this._rootElement); - } - - placeholder.classList.add('cdk-drag-placeholder'); - return placeholder; - } - - /** - * Figures out the coordinates at which an element was picked up. - * @param referenceElement Element that initiated the dragging. - * @param event Event that initiated the dragging. - */ - private _getPointerPositionInElement(referenceElement: HTMLElement, - event: MouseEvent | TouchEvent): Point { - const elementRect = this._rootElement.getBoundingClientRect(); - const handleElement = referenceElement === this._rootElement ? null : referenceElement; - const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect; - const point = this._isTouchEvent(event) ? event.targetTouches[0] : event; - const x = point.pageX - referenceRect.left - this._scrollPosition.left; - const y = point.pageY - referenceRect.top - this._scrollPosition.top; - - return { - x: referenceRect.left - elementRect.left + x, - y: referenceRect.top - elementRect.top + y - }; - } - /** * Animates the preview element from its current position to the location of the drop placeholder. * @returns Promise that resolves when the animation completes. @@ -778,20 +773,48 @@ export class CdkDrag implements AfterViewInit, OnDestroy { }); } + /** Creates an element that will be shown instead of the current element while dragging. */ + private _createPlaceholderElement(): HTMLElement { + let placeholder: HTMLElement; + + if (this._placeholderTemplate) { + this._placeholderRef = this._viewContainerRef.createEmbeddedView( + this._placeholderTemplate.templateRef, + this._placeholderTemplate.data + ); + placeholder = this._placeholderRef.rootNodes[0]; + } else { + placeholder = deepCloneNode(this._rootElement); + } + + placeholder.classList.add('cdk-drag-placeholder'); + return placeholder; + } + /** - * Helper to remove an element from the DOM and to do all the necessary null checks. - * @param element Element to be removed. + * Figures out the coordinates at which an element was picked up. + * @param referenceElement Element that initiated the dragging. + * @param event Event that initiated the dragging. */ - private _removeElement(element: HTMLElement | null) { - if (element && element.parentNode) { - element.parentNode.removeChild(element); - } + private _getPointerPositionInElement(referenceElement: HTMLElement, + event: MouseEvent | TouchEvent): Point { + const elementRect = this._rootElement.getBoundingClientRect(); + const handleElement = referenceElement === this._rootElement ? null : referenceElement; + const referenceRect = handleElement ? handleElement.getBoundingClientRect() : elementRect; + const point = isTouchEvent(event) ? event.targetTouches[0] : event; + const x = point.pageX - referenceRect.left - this._scrollPosition.left; + const y = point.pageY - referenceRect.top - this._scrollPosition.top; + + return { + x: referenceRect.left - elementRect.left + x, + y: referenceRect.top - elementRect.top + y + }; } /** Determines the point of the page that was touched by the user. */ private _getPointerPositionOnPage(event: MouseEvent | TouchEvent): Point { // `touches` will be empty for start/end events so we have to fall back to `changedTouches`. - const point = this._isTouchEvent(event) ? (event.touches[0] || event.changedTouches[0]) : event; + const point = isTouchEvent(event) ? (event.touches[0] || event.changedTouches[0]) : event; return { x: point.pageX - this._scrollPosition.left, @@ -799,6 +822,7 @@ export class CdkDrag implements AfterViewInit, OnDestroy { }; } + /** Gets the pointer position on the page, accounting for any position constraints. */ private _getConstrainedPointerPosition(event: MouseEvent | TouchEvent): Point { const point = this._getPointerPositionOnPage(event); @@ -826,36 +850,6 @@ export class CdkDrag implements AfterViewInit, OnDestroy { return point; } - /** Determines whether an event is a touch event. */ - private _isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent { - return event.type.startsWith('touch'); - } - - /** Destroys the preview element and its ViewRef. */ - private _destroyPreview() { - if (this._preview) { - this._removeElement(this._preview); - } - - if (this._previewRef) { - this._previewRef.destroy(); - } - - this._preview = this._previewRef = null!; - } - - /** Destroys the placeholder element and its ViewRef. */ - private _destroyPlaceholder() { - if (this._placeholder) { - this._removeElement(this._placeholder); - } - - if (this._placeholderRef) { - this._placeholderRef.destroy(); - } - - this._placeholder = this._placeholderRef = null!; - } /** Updates the current drag delta, based on the user's current pointer position on the page. */ private _updatePointerDirectionDelta(pointerPositionOnPage: Point) { @@ -884,26 +878,24 @@ export class CdkDrag implements AfterViewInit, OnDestroy { return delta; } - /** Gets the root draggable element, based on the `rootElementSelector`. */ - private _getRootElement(): HTMLElement { - const element = this.element.nativeElement; - const rootElement = this.rootElementSelector ? - getClosestMatchingAncestor(element, this.rootElementSelector) : null; + /** Toggles the native drag interactions, based on how many handles are registered. */ + private _toggleNativeDragInteractions() { + const shouldEnable = this._handles.length > 0; - return rootElement || element; + // We go through the trouble of keeping track of whether the interactions are enabled, + // because we want to avoid triggering style recalculations unless we really have to. + if (shouldEnable !== this._nativeInteractionsEnabled) { + this._nativeInteractionsEnabled = shouldEnable; + toggleNativeDragInteractions(this._rootElement, shouldEnable); + } } - /** Gets the boundary element, based on the `boundaryElementSelector`. */ - private _getBoundaryElement() { - const selector = this.boundaryElementSelector; - return selector ? getClosestMatchingAncestor(this.element.nativeElement, selector) : undefined; + /** Removes the manually-added event listeners from the root element. */ + private _removeRootElementListeners(element: HTMLElement) { + element.removeEventListener('mousedown', this._pointerDown, activeEventListenerOptions); + element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions); } - /** Unsubscribes from the global subscriptions. */ - private _removeSubscriptions() { - this._pointerMoveSubscription.unsubscribe(); - this._pointerUpSubscription.unsubscribe(); - } } /** Point on the page or within an element. */ @@ -919,7 +911,7 @@ interface Point { */ function getTransform(x: number, y: number): string { // Round the transforms since some browsers will - // blur the elements, for sub-pixel transforms. + // blur the elements for sub-pixel transforms. return `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`; } @@ -936,17 +928,17 @@ function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } -/** Gets the closest ancestor of an element that matches a selector. */ -function getClosestMatchingAncestor(element: HTMLElement, selector: string) { - let currentElement = element.parentElement as HTMLElement | null; - - while (currentElement) { - // IE doesn't support `matches` so we have to fall back to `msMatchesSelector`. - if (currentElement.matches ? currentElement.matches(selector) : - (currentElement as any).msMatchesSelector(selector)) { - return currentElement; - } - - currentElement = currentElement.parentElement; +/** + * Helper to remove an element from the DOM and to do all the necessary null checks. + * @param element Element to be removed. + */ +function removeElement(element: HTMLElement | null) { + if (element && element.parentNode) { + element.parentNode.removeChild(element); } } + +/** Determines whether an event is a touch event. */ +function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent { + return event.type.startsWith('touch'); +} diff --git a/src/cdk/drag-drop/drop-list-container.ts b/src/cdk/drag-drop/drop-list-container.ts index 7774c37cbcaa..f9b8f2a44412 100644 --- a/src/cdk/drag-drop/drop-list-container.ts +++ b/src/cdk/drag-drop/drop-list-container.ts @@ -7,9 +7,16 @@ */ import {InjectionToken, QueryList, ElementRef} from '@angular/core'; -import {CdkDrag} from './drag'; +import {CdkDrag} from './directives/drag'; +/** + * @deprecated To be removed. No longer being used. Previously the interface was used to avoid + * circular imports between `CdkDrag` and `CdkDropList`, however now we're using the + * `CdkDropListInternal` interface to achieve the same result, without having to maintain + * this large of an interface. + * @breaking-change 8.0.0 + */ export interface CdkDropListContainer { /** DOM node that corresponds to the drop container. */ element: ElementRef; @@ -40,7 +47,7 @@ export interface CdkDropListContainer { * @param isPointerOverContainer Whether the user's pointer was over the * container when the item was dropped. */ - drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropListContainer, + drop(item: CdkDrag, currentIndex: number, previousContainer: Partial, isPointerOverContainer: boolean): void; /** @@ -73,5 +80,13 @@ export interface CdkDropListContainer { * Injection token that is used to provide a CdkDropList instance to CdkDrag. * Used for avoiding circular imports. */ -export const CDK_DROP_LIST_CONTAINER = - new InjectionToken('CDK_DROP_LIST_CONTAINER'); +export const CDK_DROP_LIST = new InjectionToken('CDK_DROP_LIST'); + +/** + * Injection token that is used to provide a CdkDropList instance to CdkDrag. + * Used for avoiding circular imports. + * @deprecated Use `CDK_DROP_LIST` instead. + * @breaking-change 8.0.0 + */ +export const CDK_DROP_LIST_CONTAINER = CDK_DROP_LIST; + diff --git a/src/cdk/drag-drop/drop-list.ts b/src/cdk/drag-drop/drop-list-ref.ts similarity index 69% rename from src/cdk/drag-drop/drop-list.ts rename to src/cdk/drag-drop/drop-list-ref.ts index 5641ee6dbc69..db544ea7d1d2 100644 --- a/src/cdk/drag-drop/drop-list.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -6,34 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {coerceArray, coerceBooleanProperty} from '@angular/cdk/coercion'; -import { - ContentChildren, - ElementRef, - EventEmitter, - forwardRef, - Input, - OnDestroy, - OnInit, - Output, - QueryList, - Optional, - Directive, - ChangeDetectorRef, - SkipSelf, - Inject, -} from '@angular/core'; -import {Directionality} from '@angular/cdk/bidi'; -import {DOCUMENT} from '@angular/common'; -import {CdkDrag} from './drag'; +import {ElementRef} from '@angular/core'; import {DragDropRegistry} from './drag-drop-registry'; -import {CdkDragDrop, CdkDragEnter, CdkDragExit, CdkDragSortEvent} from './drag-events'; +import {Directionality} from '@angular/cdk/bidi'; +import {Subject} from 'rxjs'; import {moveItemInArray} from './drag-utils'; -import {CDK_DROP_LIST_CONTAINER} from './drop-list-container'; -import {CdkDropListGroup} from './drop-list-group'; +import {DragRefInternal as DragRef} from './drag-ref'; -/** Counter used to generate unique ids for drop zones. */ +/** Counter used to generate unique ids for drop refs. */ let _uniqueIdCounter = 0; /** @@ -61,7 +42,7 @@ interface PositionCache { */ interface ItemPositionCacheEntry { /** Instance of the drag item. */ - drag: CdkDrag; + drag: DragRef; /** Dimensions of the item. */ clientRect: ClientRect; /** Amount by which the item has been moved since dragging started. */ @@ -74,126 +55,81 @@ interface ItemPositionCacheEntry { */ interface ListPositionCacheEntry { /** Instance of the drop list. */ - drop: CdkDropList; + drop: DropListRef; /** Dimensions of the list. */ clientRect: ClientRect; } -/** Container that wraps a set of draggable items. */ -@Directive({ - selector: '[cdkDropList], cdk-drop-list', - exportAs: 'cdkDropList', - providers: [ - // Prevent child drop lists from picking up the same group as their parent. - {provide: CdkDropListGroup, useValue: undefined}, - {provide: CDK_DROP_LIST_CONTAINER, useExisting: CdkDropList}, - ], - host: { - 'class': 'cdk-drop-list', - '[id]': 'id', - '[class.cdk-drop-list-dragging]': '_dragging' - } -}) -export class CdkDropList implements OnInit, OnDestroy { - private _document: Document | undefined; +/** + * Internal compile-time-only representation of a `DropListRef`. + * Used to avoid circular import issues between the `DropListRef` and the `DragRef`. + * @docs-private + */ +export interface DropListRefInternal extends DropListRef {} - /** Draggable items in the container. */ - @ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList; +/** + * Reference to a drop list. Used to manipulate or dispose of the container. + * @docs-private + */ +export class DropListRef { + private _document: Document; /** - * 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. + * Unique ID for the drop list. + * @deprecated No longer being used. To be removed. + * @breaking-change 8.0.0 */ - @Input('cdkDropListConnectedTo') - connectedTo: (CdkDropList | string)[] | CdkDropList | string = []; + id = `cdk-drop-list-ref-${_uniqueIdCounter++}`; - /** Arbitrary data to attach to this container. */ - @Input('cdkDropListData') data: T; - - /** Direction in which the list is oriented. */ - @Input('cdkDropListOrientation') orientation: 'horizontal' | 'vertical' = 'vertical'; - - /** - * Unique ID for the drop zone. Can be used as a reference - * in the `connectedTo` of another `CdkDropList`. - */ - @Input() id: string = `cdk-drop-list-${_uniqueIdCounter++}`; + /** Whether starting a dragging sequence from this container is disabled. */ + disabled: boolean = false; /** Locks the position of the draggable elements inside the container along the specified axis. */ - @Input('cdkDropListLockAxis') lockAxis: 'x' | 'y'; - - /** Whether starting a dragging sequence from this container is disabled. */ - @Input('cdkDropListDisabled') - get disabled(): boolean { return this._disabled; } - set disabled(value: boolean) { - this._disabled = coerceBooleanProperty(value); - } - private _disabled = false; + lockAxis: 'x' | 'y'; /** * Function that is used to determine whether an item * is allowed to be moved into a drop container. */ - @Input('cdkDropListEnterPredicate') - enterPredicate: (drag: CdkDrag, drop: CdkDropList) => boolean = () => true + enterPredicate: (drag: DragRef, drop: DropListRef) => boolean = () => true; - /** Emits when the user drops an item inside the container. */ - @Output('cdkDropListDropped') - dropped: EventEmitter> = new EventEmitter>(); + /** Emits right before dragging has started. */ + beforeStarted = new Subject(); /** * Emits when the user has moved a new drag item into this container. */ - @Output('cdkDropListEntered') - entered: EventEmitter> = new EventEmitter>(); + entered = new Subject<{item: DragRef, container: DropListRef}>(); /** * Emits when the user removes an item from the container * by dragging it into another container. */ - @Output('cdkDropListExited') - exited: EventEmitter> = new EventEmitter>(); - - /** Emits as the user is swapping items while actively dragging. */ - @Output('cdkDropListSorted') - sorted: EventEmitter> = new EventEmitter>(); - - constructor( - public element: ElementRef, - private _dragDropRegistry: DragDropRegistry>, - private _changeDetectorRef: ChangeDetectorRef, - @Optional() private _dir?: Directionality, - @Optional() @SkipSelf() private _group?: CdkDropListGroup, - // @breaking-change 8.0.0 `_document` parameter to be made required. - @Optional() @Inject(DOCUMENT) _document?: any) { - - // @breaking-change 8.0.0 Remove null checks once `_document` parameter is required. - if (_document) { - this._document = _document; - } else if (typeof document !== 'undefined') { - this._document = document; - } - } - - ngOnInit() { - this._dragDropRegistry.registerDropContainer(this); + exited = new Subject<{item: DragRef, container: DropListRef}>(); - if (this._group) { - this._group._items.add(this); - } - } + /** Emits when the user drops an item inside the container. */ + dropped = new Subject<{ + item: DragRef, + currentIndex: number, + previousIndex: number, + container: DropListRef, + previousContainer: DropListRef, + isPointerOverContainer: boolean + }>(); - ngOnDestroy() { - this._dragDropRegistry.removeDropContainer(this); + /** Emits as the user is swapping items while actively dragging. */ + sorted = new Subject<{ + previousIndex: number, + currentIndex: number, + container: DropListRef, + item: DragRef + }>(); - if (this._group) { - this._group._items.delete(this); - } - } + /** Arbitrary data that can be attached to the drop list. */ + data: T; - /** Whether an item in the container is being dragged. */ - _dragging = false; + /** Whether an item in the list is being dragged. */ + private _isDragging = false; /** Cache of the dimensions of all the items and the sibling containers. */ private _positionCache: PositionCache = {items: [], siblings: [], self: {} as ClientRect}; @@ -203,41 +139,55 @@ export class CdkDropList implements OnInit, OnDestroy { * from `_draggables`, as well as any items that have been dragged in, but haven't * been dropped yet. */ - private _activeDraggables: CdkDrag[]; + private _activeDraggables: DragRef[]; /** * Keeps track of the item that was last swapped with the dragged item, as * well as what direction the pointer was moving in when the swap occured. */ - private _previousSwap = {drag: null as CdkDrag | null, delta: 0}; + private _previousSwap = {drag: null as DragRef | null, delta: 0}; + + /** + * Draggable items in the container. + * TODO(crisbeto): support arrays. + */ + private _draggables: DragRef[]; + + private _siblings: DropListRef[] = []; + + /** Direction in which the list is oriented. */ + private _orientation: 'horizontal' | 'vertical' = 'vertical'; + + constructor( + public element: ElementRef, + private _dragDropRegistry: DragDropRegistry, + _document: any, + private _dir?: Directionality) { + _dragDropRegistry.registerDropContainer(this); + this._document = _document; + } + + /** Removes the drop list functionality from the DOM element. */ + dispose() { + this.beforeStarted.complete(); + this.entered.complete(); + this.exited.complete(); + this.dropped.complete(); + this.sorted.complete(); + this._dragDropRegistry.removeDropContainer(this); + } + + /** Whether an item from this list is currently being dragged. */ + isDragging() { + return this._isDragging; + } /** Starts dragging an item. */ start(): void { - this._dragging = true; - this._activeDraggables = this._draggables.toArray(); + this.beforeStarted.next(); + this._isDragging = true; + this._activeDraggables = this._draggables.slice(); this._cachePositions(); - this._changeDetectorRef.markForCheck(); - } - - /** - * Drops an item into this container. - * @param item Item being dropped into the container. - * @param currentIndex Index at which the item should be inserted. - * @param previousContainer Container from which the item got dragged in. - * @param isPointerOverContainer Whether the user's pointer was over the - * container when the item was dropped. - */ - drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList, - isPointerOverContainer: boolean): void { - this._reset(); - this.dropped.emit({ - item, - currentIndex, - previousIndex: previousContainer.getItemIndex(item), - container: this, - previousContainer, - isPointerOverContainer - }); } /** @@ -246,8 +196,8 @@ export class CdkDropList implements OnInit, OnDestroy { * @param pointerX Position of the item along the X axis. * @param pointerY Position of the item along the Y axis. */ - enter(item: CdkDrag, pointerX: number, pointerY: number): void { - this.entered.emit({item, container: this}); + enter(item: DragRef, pointerX: number, pointerY: number): void { + this.entered.next({item, container: this}); this.start(); // We use the coordinates of where the item entered the drop @@ -286,24 +236,73 @@ export class CdkDropList implements OnInit, OnDestroy { * Removes an item from the container after it was dragged into another container by the user. * @param item Item that was dragged out. */ - exit(item: CdkDrag): void { + exit(item: DragRef): void { this._reset(); - this.exited.emit({item, container: this}); + this.exited.next({item, container: this}); + } + + /** + * Drops an item into this container. + * @param item Item being dropped into the container. + * @param currentIndex Index at which the item should be inserted. + * @param previousContainer Container from which the item got dragged in. + * @param isPointerOverContainer Whether the user's pointer was over the + * container when the item was dropped. + */ + drop(item: DragRef, currentIndex: number, previousContainer: DropListRef, + isPointerOverContainer: boolean): void { + this._reset(); + this.dropped.next({ + item, + currentIndex, + previousIndex: previousContainer.getItemIndex(item), + container: this, + previousContainer, + isPointerOverContainer + }); + } + + /** + * Sets the draggable items that are a part of this list. + * @param items Items that are a part of this list. + */ + withItems(items: DragRef[]): this { + this._draggables = items.slice(); + return this; + } + + /** + * Sets the containers that are connected to this one. When two or more containers are + * connected, the user will be allowed to transfer items between them. + * @param connectedTo Other containers that the current containers should be connected to. + */ + connectedTo(connectedTo: DropListRef[]): this { + this._siblings = connectedTo.slice(); + return this; + } + + /** + * Sets the orientation of the container. + * @param orientation New orientation for the container. + */ + withOrientation(orientation: 'vertical' | 'horizontal'): this { + this._orientation = orientation; + return this; } /** * Figures out the index of an item in the container. * @param item Item whose index should be determined. */ - getItemIndex(item: CdkDrag): number { - if (!this._dragging) { - return this._draggables.toArray().indexOf(item); + getItemIndex(item: DragRef): number { + if (!this._isDragging) { + return this._draggables.indexOf(item); } // Items are sorted always by top/left in the cache, however they flow differently in RTL. // The rest of the logic still stands no matter what orientation we're in, however // we need to invert the array when determining the index. - const items = this.orientation === 'horizontal' && this._dir && this._dir.value === 'rtl' ? + const items = this._orientation === 'horizontal' && this._dir && this._dir.value === 'rtl' ? this._positionCache.items.slice().reverse() : this._positionCache.items; return findIndex(items, currentItem => currentItem.drag === item); @@ -314,9 +313,9 @@ export class CdkDropList implements OnInit, OnDestroy { * @param item Item to be sorted. * @param pointerX Position of the item along the X axis. * @param pointerY Position of the item along the Y axis. - * @param pointerDeta Direction in which the pointer is moving along each axis. + * @param pointerDelta Direction in which the pointer is moving along each axis. */ - _sortItem(item: CdkDrag, pointerX: number, pointerY: number, + _sortItem(item: DragRef, pointerX: number, pointerY: number, pointerDelta: {x: number, y: number}): void { // Don't sort the item if it's out of range. if (!this._isPointerNearDropContainer(pointerX, pointerY)) { @@ -330,7 +329,7 @@ export class CdkDropList implements OnInit, OnDestroy { return; } - const isHorizontal = this.orientation === 'horizontal'; + const isHorizontal = this._orientation === 'horizontal'; const currentIndex = findIndex(siblings, currentItem => currentItem.drag === item); const siblingAtNewPosition = siblings[newIndex]; const currentPosition = siblings[currentIndex].clientRect; @@ -353,7 +352,7 @@ export class CdkDropList implements OnInit, OnDestroy { // Shuffle the array in place. moveItemInArray(siblings, currentIndex, newIndex); - this.sorted.emit({ + this.sorted.next({ previousIndex: currentIndex, currentIndex: newIndex, container: this, @@ -382,71 +381,17 @@ export class CdkDropList implements OnInit, OnDestroy { // Round the transforms since some browsers will // blur the elements, for sub-pixel transforms. elementToOffset.style.transform = `translate3d(${Math.round(sibling.offset)}px, 0, 0)`; - this._adjustClientRect(sibling.clientRect, 0, offset); + adjustClientRect(sibling.clientRect, 0, offset); } else { elementToOffset.style.transform = `translate3d(0, ${Math.round(sibling.offset)}px, 0)`; - this._adjustClientRect(sibling.clientRect, offset, 0); + adjustClientRect(sibling.clientRect, offset, 0); } }); } - /** - * Figures out whether an item should be moved into a sibling - * drop container, based on its current position. - * @param item Drag item that is being moved. - * @param x Position of the item along the X axis. - * @param y Position of the item along the Y axis. - */ - _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null { - const results = this._positionCache.siblings.filter(sibling => { - return isInsideClientRect(sibling.clientRect, x, y); - }); - - // No drop containers are intersecting with the pointer. - if (!results.length) { - return null; - } - - let result: ListPositionCacheEntry | undefined = results[0]; - - // @breaking-change 8.0.0 remove null check once the - // `_document` is made into a required parameter. - if (this._document) { - const elementFromPoint = this._document.elementFromPoint(x, y); - - // If there's no element at the pointer position, then - // the client rect is probably scrolled out of the view. - if (!elementFromPoint) { - return null; - } - - // The `ClientRect`, that we're using to find the container over which the user is - // hovering, doesn't give us any information on whether the element has been scrolled - // out of the view or whether it's overlapping with other containers. This means that - // we could end up transferring the item into a container that's invisible or is positioned - // below another one. We use the result from `elementFromPoint` to get the top-most element - // at the pointer position and to find whether it's one of the intersecting drop containers. - result = results.find(sibling => { - const element = sibling.drop.element.nativeElement; - return element === elementFromPoint || element.contains(elementFromPoint); - }); - } - - return result && result.drop.enterPredicate(item, result.drop) ? result.drop : null; - } - - /** - * Checks whether the user's pointer is positioned over the container. - * @param x Pointer position along the X axis. - * @param y Pointer position along the Y axis. - */ - _isOverContainer(x: number, y: number): boolean { - return isInsideClientRect(this._positionCache.self, x, y); - } - /** Refreshes the position cache of the items and sibling containers. */ private _cachePositions() { - const isHorizontal = this.orientation === 'horizontal'; + const isHorizontal = this._orientation === 'horizontal'; this._positionCache.self = this.element.nativeElement.getBoundingClientRect(); this._positionCache.items = this._activeDraggables @@ -480,7 +425,7 @@ export class CdkDropList implements OnInit, OnDestroy { a.clientRect.top - b.clientRect.top; }); - this._positionCache.siblings = this._getConnectedLists().map(drop => ({ + this._positionCache.siblings = this._siblings.map(drop => ({ drop, clientRect: drop.element.nativeElement.getBoundingClientRect() })); @@ -488,7 +433,7 @@ export class CdkDropList implements OnInit, OnDestroy { /** Resets the container to its initial state. */ private _reset() { - this._dragging = false; + this._isDragging = false; // TODO(crisbeto): may have to wait for the animations to finish. this._activeDraggables.forEach(item => item.getRootElement().style.transform = ''); @@ -500,54 +445,36 @@ export class CdkDropList implements OnInit, OnDestroy { } /** - * Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts. - * @param clientRect `ClientRect` that should be updated. - * @param top Amount to add to the `top` position. - * @param left Amount to add to the `left` position. - */ - private _adjustClientRect(clientRect: ClientRect, top: number, left: number) { - clientRect.top += top; - clientRect.bottom = clientRect.top + clientRect.height; - - clientRect.left += left; - clientRect.right = clientRect.left + clientRect.width; - } - - /** - * Gets the index of an item in the drop container, based on the position of the user's pointer. - * @param item Item that is being sorted. - * @param pointerX Position of the user's pointer along the X axis. - * @param pointerY Position of the user's pointer along the Y axis. - * @param delta Direction in which the user is moving their pointer. + * Gets the offset in pixels by which the items that aren't being dragged should be moved. + * @param currentIndex Index of the item currently being dragged. + * @param siblings All of the items in the list. + * @param delta Direction in which the user is moving. */ - private _getItemIndexFromPointerPosition(item: CdkDrag, pointerX: number, pointerY: number, - delta?: {x: number, y: number}) { - - const isHorizontal = this.orientation === 'horizontal'; + private _getSiblingOffsetPx(currentIndex: number, + siblings: ItemPositionCacheEntry[], + delta: 1 | -1) { - return findIndex(this._positionCache.items, ({drag, clientRect}, _, array) => { - if (drag === item) { - // If there's only one item left in the container, it must be - // the dragged item itself so we use it as a reference. - return array.length < 2; - } + const isHorizontal = this._orientation === 'horizontal'; + const currentPosition = siblings[currentIndex].clientRect; + const immediateSibling = siblings[currentIndex + delta * -1]; + let siblingOffset = currentPosition[isHorizontal ? 'width' : 'height'] * delta; - if (delta) { - const direction = isHorizontal ? delta.x : delta.y; + if (immediateSibling) { + const start = isHorizontal ? 'left' : 'top'; + const end = isHorizontal ? 'right' : 'bottom'; - // If the user is still hovering over the same item as last time, and they didn't change - // the direction in which they're dragging, we don't consider it a direction swap. - if (drag === this._previousSwap.drag && direction === this._previousSwap.delta) { - return false; - } + // Get the spacing between the start of the current item and the end of the one immediately + // after it in the direction in which the user is dragging, or vice versa. We add it to the + // offset in order to push the element to where it will be when it's inline and is influenced + // by the `margin` of its siblings. + if (delta === -1) { + siblingOffset -= immediateSibling.clientRect[start] - currentPosition[end]; + } else { + siblingOffset += currentPosition[start] - immediateSibling.clientRect[end]; } + } - return isHorizontal ? - // Round these down since most browsers report client rects with - // sub-pixel precision, whereas the pointer coordinates are rounded to pixels. - pointerX >= Math.floor(clientRect.left) && pointerX <= Math.floor(clientRect.right) : - pointerY >= Math.floor(clientRect.top) && pointerY <= Math.floor(clientRect.bottom); - }); + return siblingOffset; } /** @@ -571,7 +498,7 @@ export class CdkDropList implements OnInit, OnDestroy { * @param delta Direction in which the user is moving. */ private _getItemOffsetPx(currentPosition: ClientRect, newPosition: ClientRect, delta: 1 | -1) { - const isHorizontal = this.orientation === 'horizontal'; + const isHorizontal = this._orientation === 'horizontal'; let itemOffset = isHorizontal ? newPosition.left - currentPosition.left : newPosition.top - currentPosition.top; @@ -585,54 +512,105 @@ export class CdkDropList implements OnInit, OnDestroy { } /** - * Gets the offset in pixels by which the items that aren't being dragged should be moved. - * @param currentIndex Index of the item currently being dragged. - * @param siblings All of the items in the list. - * @param delta Direction in which the user is moving. + * Gets the index of an item in the drop container, based on the position of the user's pointer. + * @param item Item that is being sorted. + * @param pointerX Position of the user's pointer along the X axis. + * @param pointerY Position of the user's pointer along the Y axis. + * @param delta Direction in which the user is moving their pointer. */ - private _getSiblingOffsetPx(currentIndex: number, - siblings: ItemPositionCacheEntry[], - delta: 1 | -1) { + private _getItemIndexFromPointerPosition(item: DragRef, pointerX: number, pointerY: number, + delta?: {x: number, y: number}) { - const isHorizontal = this.orientation === 'horizontal'; - const currentPosition = siblings[currentIndex].clientRect; - const immediateSibling = siblings[currentIndex + delta * -1]; - let siblingOffset = currentPosition[isHorizontal ? 'width' : 'height'] * delta; + const isHorizontal = this._orientation === 'horizontal'; - if (immediateSibling) { - const start = isHorizontal ? 'left' : 'top'; - const end = isHorizontal ? 'right' : 'bottom'; + return findIndex(this._positionCache.items, ({drag, clientRect}, _, array) => { + if (drag === item) { + // If there's only one item left in the container, it must be + // the dragged item itself so we use it as a reference. + return array.length < 2; + } - // Get the spacing between the start of the current item and the end of the one immediately - // after it in the direction in which the user is dragging, or vice versa. We add it to the - // offset in order to push the element to where it will be when it's inline and is influenced - // by the `margin` of its siblings. - if (delta === -1) { - siblingOffset -= immediateSibling.clientRect[start] - currentPosition[end]; - } else { - siblingOffset += currentPosition[start] - immediateSibling.clientRect[end]; + if (delta) { + const direction = isHorizontal ? delta.x : delta.y; + + // If the user is still hovering over the same item as last time, and they didn't change + // the direction in which they're dragging, we don't consider it a direction swap. + if (drag === this._previousSwap.drag && direction === this._previousSwap.delta) { + return false; + } } - } - return siblingOffset; + return isHorizontal ? + // Round these down since most browsers report client rects with + // sub-pixel precision, whereas the pointer coordinates are rounded to pixels. + pointerX >= Math.floor(clientRect.left) && pointerX <= Math.floor(clientRect.right) : + pointerY >= Math.floor(clientRect.top) && pointerY <= Math.floor(clientRect.bottom); + }); + } + + /** + * Checks whether the user's pointer is positioned over the container. + * @param x Pointer position along the X axis. + * @param y Pointer position along the Y axis. + */ + _isOverContainer(x: number, y: number): boolean { + return isInsideClientRect(this._positionCache.self, x, y); } - /** Gets an array of unique drop lists that the current list is connected to. */ - private _getConnectedLists(): CdkDropList[] { - const siblings = coerceArray(this.connectedTo).map(drop => { - return typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop; + /** + * Figures out whether an item should be moved into a sibling + * drop container, based on its current position. + * @param item Drag item that is being moved. + * @param x Position of the item along the X axis. + * @param y Position of the item along the Y axis. + */ + _getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | null { + const results = this._positionCache.siblings.filter(sibling => { + return isInsideClientRect(sibling.clientRect, x, y); }); - if (this._group) { - this._group._items.forEach(drop => { - if (siblings.indexOf(drop) === -1) { - siblings.push(drop); - } - }); + // No drop containers are intersecting with the pointer. + if (!results.length) { + return null; } - return siblings.filter(drop => drop && drop !== this); + const elementFromPoint = this._document.elementFromPoint(x, y); + + // If there's no element at the pointer position, then + // the client rect is probably scrolled out of the view. + if (!elementFromPoint) { + return null; + } + + // The `ClientRect`, that we're using to find the container over which the user is + // hovering, doesn't give us any information on whether the element has been scrolled + // out of the view or whether it's overlapping with other containers. This means that + // we could end up transferring the item into a container that's invisible or is positioned + // below another one. We use the result from `elementFromPoint` to get the top-most element + // at the pointer position and to find whether it's one of the intersecting drop containers. + const result = results.find(sibling => { + const element = sibling.drop.element.nativeElement; + return element === elementFromPoint || element.contains(elementFromPoint); + }); + + return result && result.drop.enterPredicate(item, result.drop) ? result.drop : null; } + +} + + +/** + * Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts. + * @param clientRect `ClientRect` that should be updated. + * @param top Amount to add to the `top` position. + * @param left Amount to add to the `left` position. + */ +function adjustClientRect(clientRect: ClientRect, top: number, left: number) { + clientRect.top += top; + clientRect.bottom = clientRect.top + clientRect.height; + + clientRect.left += left; + clientRect.right = clientRect.left + clientRect.width; } diff --git a/src/cdk/drag-drop/public-api.ts b/src/cdk/drag-drop/public-api.ts index a4f5b1730024..2e9c253e2fca 100644 --- a/src/cdk/drag-drop/public-api.ts +++ b/src/cdk/drag-drop/public-api.ts @@ -6,14 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './drop-list'; -export * from './drop-list-group'; +// TODO(crisbeto): export once API is finalized +// export * from './drag-ref'; +// export * from './drop-list-ref'; + export * from './drop-list-container'; -export * from './drag'; -export * from './drag-handle'; export * from './drag-events'; export * from './drag-utils'; -export * from './drag-preview'; -export * from './drag-placeholder'; export * from './drag-drop-module'; export * from './drag-drop-registry'; + +export * from './directives/drop-list'; +export * from './directives/drop-list-group'; +export * from './directives/drag'; +export * from './directives/drag-handle'; +export * from './directives/drag-preview'; +export * from './directives/drag-placeholder'; + +import {DragRefConfig} from './drag-ref'; + +/** + * @deprecated Use `DragRefConfig` instead. + * @breaking-change 8.0.0 + */ +export interface CdkDragConfig extends DragRefConfig {} diff --git a/src/dev-app/drag-drop/drag-drop-demo.html b/src/dev-app/drag-drop/drag-drop-demo.html index 342562ed0318..af30924db583 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.html +++ b/src/dev-app/drag-drop/drag-drop-demo.html @@ -2,9 +2,6 @@

To do

- - Clone Mode -
; +export declare const CDK_DRAG_CONFIG: InjectionToken; -export declare function CDK_DRAG_CONFIG_FACTORY(): CdkDragConfig; +export declare function CDK_DRAG_CONFIG_FACTORY(): DragRefConfig; + +export declare const CDK_DROP_LIST: InjectionToken>; export declare const CDK_DROP_LIST_CONTAINER: InjectionToken>; export declare class CdkDrag implements AfterViewInit, OnDestroy { + _dragRef: DragRef>; _handles: QueryList; - _hasStartedDragging: boolean; _placeholderTemplate: CdkDragPlaceholder; - _pointerDown: (event: TouchEvent | MouseEvent) => void; _previewTemplate: CdkDragPreview; boundaryElementSelector: string; data: T; disabled: boolean; - dropContainer: CdkDropListContainer; + dropContainer: CdkDropList; dropped: EventEmitter>; element: ElementRef; ended: EventEmitter; @@ -25,8 +26,7 @@ export declare class CdkDrag implements AfterViewInit, OnDestroy { started: EventEmitter; constructor( element: ElementRef, - dropContainer: CdkDropListContainer, document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry, CdkDropListContainer>, _config: CdkDragConfig, _dir: Directionality); - _isDragging(): boolean; + dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry, _config: DragRefConfig, _dir: Directionality); getPlaceholderElement(): HTMLElement; getRootElement(): HTMLElement; ngAfterViewInit(): void; @@ -34,17 +34,15 @@ export declare class CdkDrag implements AfterViewInit, OnDestroy { reset(): void; } -export interface CdkDragConfig { - dragStartThreshold: number; - pointerDirectionChangeThreshold: number; +export interface CdkDragConfig extends DragRefConfig { } export interface CdkDragDrop { - container: CdkDropListContainer; + container: CdkDropList; currentIndex: number; isPointerOverContainer: boolean; item: CdkDrag; - previousContainer: CdkDropListContainer; + previousContainer: CdkDropList; previousIndex: number; } @@ -53,12 +51,12 @@ export interface CdkDragEnd { } export interface CdkDragEnter { - container: CdkDropListContainer; + container: CdkDropList; item: CdkDrag; } export interface CdkDragExit { - container: CdkDropListContainer; + container: CdkDropList; item: CdkDrag; } @@ -95,7 +93,7 @@ export declare class CdkDragPreview { } export interface CdkDragSortEvent { - container: CdkDropListContainer; + container: CdkDropList; currentIndex: number; item: CdkDrag; previousIndex: number; @@ -105,9 +103,9 @@ export interface CdkDragStart { source: CdkDrag; } -export declare class CdkDropList implements OnInit, OnDestroy { +export declare class CdkDropList implements CdkDropListContainer, OnDestroy { _draggables: QueryList; - _dragging: boolean; + _dropListRef: DropListRef>; connectedTo: (CdkDropList | string)[] | CdkDropList | string; data: T; disabled: boolean; @@ -120,19 +118,18 @@ export declare class CdkDropList implements OnInit, OnDestroy { lockAxis: 'x' | 'y'; orientation: 'horizontal' | 'vertical'; sorted: EventEmitter>; - constructor(element: ElementRef, _dragDropRegistry: DragDropRegistry>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup> | undefined, _document?: any); - _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropList | null; + constructor(element: ElementRef, dragDropRegistry: DragDropRegistry, _changeDetectorRef: ChangeDetectorRef, dir?: Directionality, _group?: CdkDropListGroup> | undefined, _document?: any); + _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropListContainer | null; _isOverContainer(x: number, y: number): boolean; _sortItem(item: CdkDrag, pointerX: number, pointerY: number, pointerDelta: { x: number; y: number; }): void; - drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropList, isPointerOverContainer: boolean): void; + drop(item: CdkDrag, currentIndex: number, previousContainer: Partial, isPointerOverContainer: boolean): void; enter(item: CdkDrag, pointerX: number, pointerY: number): void; exit(item: CdkDrag): void; getItemIndex(item: CdkDrag): number; ngOnDestroy(): void; - ngOnInit(): void; start(): void; } @@ -150,7 +147,7 @@ export interface CdkDropListContainer { x: number; y: number; }): void; - drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDropListContainer, isPointerOverContainer: boolean): void; + drop(item: CdkDrag, currentIndex: number, previousContainer: Partial, isPointerOverContainer: boolean): void; enter(item: CdkDrag, pointerX: number, pointerY: number): void; exit(item: CdkDrag): void; getItemIndex(item: CdkDrag): number; @@ -162,6 +159,9 @@ export declare class CdkDropListGroup implements OnDestroy { ngOnDestroy(): void; } +export interface CdkDropListInternal extends CdkDropList { +} + export declare function copyArrayItem(currentArray: T[], targetArray: T[], currentIndex: number, targetIndex: number): void; export declare class DragDropModule {