From 1efd7a42f264ac4bb89a59f46bb044e37dc65393 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 24 Jan 2019 18:43:36 +0100 Subject: [PATCH] feat(drag-drop): add service for attaching drag&drop to arbitrary DOM nodes * Adds the `DragDrop` service that simplifies the construction logic for `DragRef` and `DropListRef` and allows consumers to attach drag&drop functionality to arbitrary DOM nodes, rather than having to go through the directives. * Reworks `DragRef` and `DragDropRef` to make them easier to construct. * Normalizes some of the instances where some parameters only accept an `ElementRef`, whereas others accept `ElementRef | HTMLElement`. --- src/cdk/drag-drop/BUILD.bazel | 1 + src/cdk/drag-drop/directives/drag.spec.ts | 5 +- src/cdk/drag-drop/directives/drag.ts | 62 +++++++--- src/cdk/drag-drop/directives/drop-list.ts | 70 ++++++++--- src/cdk/drag-drop/drag-drop-module.ts | 4 + src/cdk/drag-drop/drag-drop.spec.ts | 47 ++++++++ src/cdk/drag-drop/drag-drop.ts | 52 ++++++++ src/cdk/drag-drop/drag-ref.ts | 107 ++++++++++------- src/cdk/drag-drop/drop-list-ref.ts | 31 +++-- src/cdk/drag-drop/public-api.ts | 8 +- tools/public_api_guard/cdk/drag-drop.d.ts | 137 +++++++++++++++++++++- 11 files changed, 425 insertions(+), 99 deletions(-) create mode 100644 src/cdk/drag-drop/drag-drop.spec.ts create mode 100644 src/cdk/drag-drop/drag-drop.ts diff --git a/src/cdk/drag-drop/BUILD.bazel b/src/cdk/drag-drop/BUILD.bazel index 6c98b4a536ef..041fbef5ab79 100644 --- a/src/cdk/drag-drop/BUILD.bazel +++ b/src/cdk/drag-drop/BUILD.bazel @@ -23,6 +23,7 @@ ng_test_library( name = "drag-drop_test_sources", srcs = glob(["**/*.spec.ts"]), deps = [ + "@rxjs", "//src/cdk/testing", "//src/cdk/bidi", ":drag-drop", diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 8c81872fb8d7..0f7504b58303 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -21,6 +21,7 @@ import { createTouchEvent, } from '@angular/cdk/testing'; import {Directionality} from '@angular/cdk/bidi'; +import {of as observableOf} from 'rxjs'; import {CdkDrag, CDK_DRAG_CONFIG} from './drag'; import {CdkDragDrop} from '../drag-events'; import {moveItemInArray} from '../drag-utils'; @@ -1140,7 +1141,7 @@ describe('CdkDrag', () => { it('should dispatch the correct `dropped` event in RTL horizontal drop zone', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone, [{ provide: Directionality, - useValue: ({value: 'rtl'}) + useValue: ({value: 'rtl', change: observableOf()}) }]); fixture.nativeElement.setAttribute('dir', 'rtl'); @@ -1296,7 +1297,7 @@ describe('CdkDrag', () => { it('should pass the proper direction to the preview in rtl', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone, [{ provide: Directionality, - useValue: ({value: 'rtl'}) + useValue: ({value: 'rtl', change: observableOf()}) }]); fixture.detectChanges(); diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 48b429777be1..2fcaf11a82ad 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -50,6 +50,7 @@ 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'; +import {DragDrop} from '../drag-drop'; /** Injection token that can be used to configure the behavior of `CdkDrag`. */ export const CDK_DRAG_CONFIG = new InjectionToken('CDK_DRAG_CONFIG', { @@ -167,18 +168,30 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { @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; - this._syncInputs(ref); - this._proxyEvents(ref); + viewportRuler: ViewportRuler, + dragDropRegistry: DragDropRegistry, + @Inject(CDK_DRAG_CONFIG) config: DragRefConfig, + @Optional() private _dir: Directionality, + + /** + * @deprecated `viewportRuler` and `dragDropRegistry` parameters + * to be removed. Also `dragDrop` parameter to be made required. + * @breaking-change 8.0.0. + */ + dragDrop?: DragDrop) { + + + // @breaking-change 8.0.0 Remove null check once the paramter is made required. + if (dragDrop) { + this._dragRef = dragDrop.createDrag(element, config); + } else { + this._dragRef = new DragRef(element, config, _document, _ngZone, viewportRuler, + dragDropRegistry); + } + + this._dragRef.data = this; + this._syncInputs(this._dragRef); + this._proxyEvents(this._dragRef); } /** @@ -273,15 +286,28 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { private _syncInputs(ref: DragRef>) { ref.beforeStarted.subscribe(() => { if (!ref.isDragging()) { - const {_placeholderTemplate: placeholder, _previewTemplate: preview} = this; + const dir = this._dir; + const placeholder = this._placeholderTemplate ? { + template: this._placeholderTemplate.templateRef, + context: this._placeholderTemplate.data, + viewContainer: this._viewContainerRef + } : null; + const preview = this._previewTemplate ? { + template: this._previewTemplate.templateRef, + context: this._previewTemplate.data, + viewContainer: this._viewContainerRef + } : null; ref.disabled = this.disabled; ref.lockAxis = this.lockAxis; - ref.withBoundaryElement(this._getBoundaryElement()); - placeholder ? ref.withPlaceholderTemplate(placeholder.templateRef, placeholder.data) : - ref.withPlaceholderTemplate(null); - preview ? ref.withPreviewTemplate(preview.templateRef, preview.data) : - ref.withPreviewTemplate(null); + ref + .withBoundaryElement(this._getBoundaryElement()) + .withPlaceholderTemplate(placeholder) + .withPreviewTemplate(preview); + + if (dir) { + ref.withDirection(dir.value); + } } }); } diff --git a/src/cdk/drag-drop/directives/drop-list.ts b/src/cdk/drag-drop/directives/drop-list.ts index 0e0197508c81..36769f83242b 100644 --- a/src/cdk/drag-drop/directives/drop-list.ts +++ b/src/cdk/drag-drop/directives/drop-list.ts @@ -21,6 +21,7 @@ import { ChangeDetectorRef, SkipSelf, Inject, + AfterContentInit, } from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {Directionality} from '@angular/cdk/bidi'; @@ -31,6 +32,9 @@ import {CDK_DROP_LIST_CONTAINER, CdkDropListContainer} from '../drop-list-contai import {CdkDropListGroup} from './drop-list-group'; import {DropListRef} from '../drop-list-ref'; import {DragRef} from '../drag-ref'; +import {DragDrop} from '../drag-drop'; +import {Subject} from 'rxjs'; +import {startWith, takeUntil} from 'rxjs/operators'; /** Counter used to generate unique ids for drop zones. */ let _uniqueIdCounter = 0; @@ -62,7 +66,10 @@ export interface CdkDropListInternal extends CdkDropList {} '[class.cdk-drop-list-receiving]': '_dropListRef.isReceiving()', } }) -export class CdkDropList implements CdkDropListContainer, OnDestroy { +export class CdkDropList implements CdkDropListContainer, AfterContentInit, OnDestroy { + /** Emits when the list has been destroyed. */ + private _destroyed = new Subject(); + /** Keeps track of the drop lists that are currently on the page. */ private static _dropLists: CdkDropList[] = []; @@ -70,7 +77,11 @@ export class CdkDropList implements CdkDropListContainer, OnDestroy { _dropListRef: DropListRef>; /** Draggable items in the container. */ - @ContentChildren(forwardRef(() => CdkDrag)) _draggables: QueryList; + @ContentChildren(forwardRef(() => CdkDrag), { + // Explicitly set to false since some of the logic below makes assumptions about it. + // The `.withItems` call below should be updated if we ever need to switch this to `true`. + descendants: false + }) _draggables: QueryList; /** * Other draggable containers that this container is connected to and into which the @@ -134,24 +145,35 @@ export class CdkDropList implements CdkDropListContainer, OnDestroy { sorted: EventEmitter> = new EventEmitter>(); constructor( + /** Element that the drop list is attached to. */ public element: ElementRef, dragDropRegistry: DragDropRegistry, private _changeDetectorRef: ChangeDetectorRef, - @Optional() dir?: Directionality, + @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) { - + @Optional() @Inject(DOCUMENT) _document?: any, + + /** + * @deprecated `dragDropRegistry` and `_document` parameters to be removed. + * Also `dragDrop` parameter to be made required. + * @breaking-change 8.0.0. + */ + dragDrop?: DragDrop) { + + // @breaking-change 8.0.0 Remove null check once `dragDrop` parameter is made required. + if (dragDrop) { + this._dropListRef = dragDrop.createDropList(element); + } else { + this._dropListRef = new DropListRef(element, dragDropRegistry, _document || document); + } - // @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) => { + this._dropListRef.data = this; + this._dropListRef.enterPredicate = (drag: DragRef, drop: DropListRef) => { return this.enterPredicate(drag.data, drop.data); }; - this._syncInputs(ref); - this._handleEvents(ref); + + this._syncInputs(this._dropListRef); + this._handleEvents(this._dropListRef); CdkDropList._dropLists.push(this); if (_group) { @@ -159,9 +181,16 @@ export class CdkDropList implements CdkDropListContainer, OnDestroy { } } + ngAfterContentInit() { + this._draggables.changes + .pipe(startWith(this._draggables), takeUntil(this._destroyed)) + .subscribe((items: QueryList) => { + this._dropListRef.withItems(items.map(drag => drag._dragRef)); + }); + } + ngOnDestroy() { const index = CdkDropList._dropLists.indexOf(this); - this._dropListRef.dispose(); if (index > -1) { CdkDropList._dropLists.splice(index, 1); @@ -170,6 +199,10 @@ export class CdkDropList implements CdkDropListContainer, OnDestroy { if (this._group) { this._group._items.delete(this); } + + this._dropListRef.dispose(); + this._destroyed.next(); + this._destroyed.complete(); } /** Starts dragging an item. */ @@ -253,6 +286,12 @@ export class CdkDropList implements CdkDropListContainer, OnDestroy { /** Syncs the inputs of the CdkDropList with the options of the underlying DropListRef. */ private _syncInputs(ref: DropListRef) { + if (this._dir) { + this._dir.change + .pipe(startWith(this._dir.value), takeUntil(this._destroyed)) + .subscribe(value => ref.withDirection(value)); + } + ref.beforeStarted.subscribe(() => { const siblings = coerceArray(this.connectedTo).map(drop => { return typeof drop === 'string' ? @@ -270,8 +309,7 @@ export class CdkDropList implements CdkDropListContainer, OnDestroy { 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)); + .withOrientation(this.orientation); }); } diff --git a/src/cdk/drag-drop/drag-drop-module.ts b/src/cdk/drag-drop/drag-drop-module.ts index 6ccc01f1b532..e0cdd49463a9 100644 --- a/src/cdk/drag-drop/drag-drop-module.ts +++ b/src/cdk/drag-drop/drag-drop-module.ts @@ -13,6 +13,7 @@ import {CdkDrag} from './directives/drag'; import {CdkDragHandle} from './directives/drag-handle'; import {CdkDragPreview} from './directives/drag-preview'; import {CdkDragPlaceholder} from './directives/drag-placeholder'; +import {DragDrop} from './drag-drop'; @NgModule({ declarations: [ @@ -31,5 +32,8 @@ import {CdkDragPlaceholder} from './directives/drag-placeholder'; CdkDragPreview, CdkDragPlaceholder, ], + providers: [ + DragDrop, + ] }) export class DragDropModule {} diff --git a/src/cdk/drag-drop/drag-drop.spec.ts b/src/cdk/drag-drop/drag-drop.spec.ts new file mode 100644 index 000000000000..03211f369250 --- /dev/null +++ b/src/cdk/drag-drop/drag-drop.spec.ts @@ -0,0 +1,47 @@ +import {Component, ElementRef} from '@angular/core'; +import {fakeAsync, TestBed, inject} from '@angular/core/testing'; +import {DragDropModule} from './drag-drop-module'; +import {DragDrop} from './drag-drop'; +import {DragRef} from './drag-ref'; +import {DropListRef} from './drop-list-ref'; + +describe('DragDrop', () => { + let service: DragDrop; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [DragDropModule], + }); + + TestBed.compileComponents(); + })); + + beforeEach(inject([DragDrop], (d: DragDrop) => { + service = d; + })); + + it('should be able to attach a DragRef to a DOM node', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + const ref = service.createDrag(fixture.componentInstance.elementRef); + + expect(ref instanceof DragRef).toBe(true); + }); + + it('should be able to attach a DropListRef to a DOM node', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + const ref = service.createDropList(fixture.componentInstance.elementRef); + + expect(ref instanceof DropListRef).toBe(true); + }); +}); + + +@Component({ + template: '
' +}) +class TestComponent { + constructor(public elementRef: ElementRef) {} +} diff --git a/src/cdk/drag-drop/drag-drop.ts b/src/cdk/drag-drop/drag-drop.ts new file mode 100644 index 000000000000..b86466ee2c9f --- /dev/null +++ b/src/cdk/drag-drop/drag-drop.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Injectable, Inject, NgZone, ElementRef} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; +import {ViewportRuler} from '@angular/cdk/scrolling'; +import {DragRef, DragRefConfig} from './drag-ref'; +import {DropListRef} from './drop-list-ref'; +import {DragDropRegistry} from './drag-drop-registry'; + +/** Default configuration to be used when creating a `DragRef`. */ +const DEFAULT_CONFIG = { + dragStartThreshold: 5, + pointerDirectionChangeThreshold: 5 +}; + +/** + * Service that allows for drag-and-drop functionality to be attached to DOM elements. + */ +@Injectable({providedIn: 'root'}) +export class DragDrop { + constructor( + @Inject(DOCUMENT) private _document: any, + private _ngZone: NgZone, + private _viewportRuler: ViewportRuler, + private _dragDropRegistry: DragDropRegistry) {} + + /** + * Turns an element into a draggable item. + * @param element Element to which to attach the dragging functionality. + * @param config Object used to configure the dragging behavior. + */ + createDrag(element: ElementRef | HTMLElement, + config: DragRefConfig = DEFAULT_CONFIG): DragRef { + + return new DragRef(element, config, this._document, this._ngZone, this._viewportRuler, + this._dragDropRegistry); + } + + /** + * Turns an element into a drop list. + * @param element Element to which to attach the drop list functionality. + */ + createDropList(element: ElementRef | HTMLElement): DropListRef { + return new DropListRef(element, this._dragDropRegistry, this._document); + } +} diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index aeef2faad6a1..904a4d6d4988 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -8,7 +8,7 @@ import {EmbeddedViewRef, ElementRef, NgZone, ViewContainerRef, TemplateRef} from '@angular/core'; import {ViewportRuler} from '@angular/cdk/scrolling'; -import {Directionality} from '@angular/cdk/bidi'; +import {Direction} from '@angular/cdk/bidi'; import {normalizePassiveListenerOptions} from '@angular/cdk/platform'; import {coerceBooleanProperty, coerceElement} from '@angular/cdk/coercion'; import {Subscription, Subject, Observable, Observer} from 'rxjs'; @@ -57,6 +57,13 @@ const MOUSE_EVENT_IGNORE_TIME = 800; */ export interface DragRefInternal extends DragRef {} +/** Template that can be used to create a drag helper element (e.g. a preview or a placeholder). */ +interface DragHelperTemplate { + template: TemplateRef | null; + viewContainer: ViewContainerRef; + context: T; +} + /** * Reference to a draggable item. Used to manipulate or dispose of the item. * @docs-private @@ -173,10 +180,10 @@ export class DragRef { private _boundaryRect?: ClientRect; /** Element that will be used as a template to create the draggable item's preview. */ - private _previewTemplate?: {template: TemplateRef | null, context?: any}; + private _previewTemplate?: DragHelperTemplate | null; /** Template for placeholder element rendered to show where a draggable would be dropped. */ - private _placeholderTemplate?: {template: TemplateRef | null, context?: any}; + private _placeholderTemplate?: DragHelperTemplate | null; /** Elements that can be used to drag the draggable item. */ private _handles: HTMLElement[] = []; @@ -184,12 +191,18 @@ export class DragRef { /** Registered handles that are currently disabled. */ private _disabledHandles = new Set(); + /** Droppable container that the draggable is a part of. */ + private _dropContainer?: DropListRef; + + /** Layout direction of the item. */ + private _direction: Direction = 'ltr'; + /** Axis along which dragging is locked. */ lockAxis: 'x' | 'y'; /** Whether starting to drag this element is disabled. */ get disabled(): boolean { - return this._disabled || !!(this.dropContainer && this.dropContainer.disabled); + return this._disabled || !!(this._dropContainer && this._dropContainer.disabled); } set disabled(value: boolean) { const newValue = coerceBooleanProperty(value); @@ -253,15 +266,11 @@ export class DragRef { constructor( element: ElementRef | HTMLElement, + private _config: DragRefConfig, private _document: Document, private _ngZone: NgZone, - private _viewContainerRef: ViewContainerRef, private _viewportRuler: ViewportRuler, - private _dragDropRegistry: DragDropRegistry, - private _config: DragRefConfig, - /** Droppable container that the draggable is a part of. */ - public dropContainer?: DropListRef, - private _dir?: Directionality) { + private _dragDropRegistry: DragDropRegistry) { this.withRootElement(element); _dragDropRegistry.registerDragItem(this); @@ -291,20 +300,18 @@ export class DragRef { /** * Registers the template that should be used for the drag preview. * @param template Template that from which to stamp out the preview. - * @param context Variables to add to the template's context. */ - withPreviewTemplate(template: TemplateRef | null, context?: any): this { - this._previewTemplate = {template, context}; + withPreviewTemplate(template: DragHelperTemplate | null): this { + this._previewTemplate = template; return this; } /** * Registers the template that should be used for the drag placeholder. * @param template Template that from which to stamp out the placeholder. - * @param context Variables to add to the template's context. */ - withPlaceholderTemplate(template: TemplateRef | null, context?: any): this { - this._placeholderTemplate = {template, context}; + withPlaceholderTemplate(template: DragHelperTemplate | null): this { + this._placeholderTemplate = template; return this; } @@ -364,6 +371,7 @@ export class DragRef { this._moveEvents.complete(); this._handles = []; this._disabledHandles.clear(); + this._dropContainer = undefined; this._boundaryElement = this._rootElement = this._placeholderTemplate = this._previewTemplate = this._nextSibling = null!; } @@ -398,6 +406,17 @@ export class DragRef { this._disabledHandles.delete(handle); } + /** Sets the layout direction of the draggable item. */ + withDirection(direction: Direction): this { + this._direction = direction; + return this; + } + + /** Sets the container that the item is part of. */ + _withDropContainer(container: DropListRef) { + this._dropContainer = container; + } + /** Unsubscribes from the global subscriptions. */ private _removeSubscriptions() { this._pointerMoveSubscription.unsubscribe(); @@ -482,7 +501,7 @@ export class DragRef { event.preventDefault(); this._updatePointerDirectionDelta(constrainedPointerPosition); - if (this.dropContainer) { + if (this._dropContainer) { this._updateActiveDropContainer(constrainedPointerPosition); } else { const activeTransform = this._activeTransform; @@ -543,7 +562,7 @@ export class DragRef { this.released.next({source: this}); - if (!this.dropContainer) { + if (!this._dropContainer) { // Convert the active transform into a passive one. This means that next time // the user starts dragging the item, its position will be calculated relatively // to the new passive transform. @@ -569,7 +588,7 @@ export class DragRef { this._lastTouchEventTime = Date.now(); } - if (this.dropContainer) { + if (this._dropContainer) { const element = this._rootElement; // Grab the `nextSibling` before the preview and placeholder @@ -585,7 +604,7 @@ export class DragRef { element.style.display = 'none'; this._document.body.appendChild(element.parentNode!.replaceChild(placeholder, element)); this._document.body.appendChild(preview); - this.dropContainer.start(); + this._dropContainer.start(); } } @@ -639,7 +658,7 @@ export class DragRef { this._toggleNativeDragInteractions(); this._hasStartedDragging = this._hasMoved = false; - this._initialContainer = this.dropContainer!; + 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(); @@ -670,7 +689,7 @@ export class DragRef { if (this._nextSibling) { this._nextSibling.parentNode!.insertBefore(this._rootElement, this._nextSibling); } else { - this._initialContainer.element.nativeElement.appendChild(this._rootElement); + this._initialContainer.element.appendChild(this._rootElement); } this._destroyPreview(); @@ -679,7 +698,7 @@ export class DragRef { // Re-enter the NgZone since we bound `document` events on the outside. this._ngZone.run(() => { - const container = this.dropContainer!; + const container = this._dropContainer!; const currentIndex = container.getItemIndex(this); const {x, y} = this._getPointerPositionOnPage(event); const isPointerOverContainer = container._isOverContainer(x, y); @@ -694,7 +713,7 @@ export class DragRef { isPointerOverContainer }); container.drop(this, currentIndex, this._initialContainer, isPointerOverContainer); - this.dropContainer = this._initialContainer; + this._dropContainer = this._initialContainer; }); } @@ -704,31 +723,31 @@ export class DragRef { */ 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) || this._initialContainer._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 // case where two containers are connected one way and the user tries to undo dragging an // item into a new container. - if (!newContainer && this.dropContainer !== this._initialContainer && + if (!newContainer && this._dropContainer !== this._initialContainer && this._initialContainer._isOverContainer(x, y)) { newContainer = this._initialContainer; } - if (newContainer && newContainer !== this.dropContainer) { + if (newContainer && newContainer !== this._dropContainer) { this._ngZone.run(() => { // Notify the old container that the item has left. - this.exited.next({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.next({item: this, container: newContainer!}); - this.dropContainer = newContainer!; - this.dropContainer.enter(this, x, y); + 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); } @@ -738,12 +757,13 @@ export class DragRef { * and will be used as a preview of the element that is being dragged. */ private _createPreviewElement(): HTMLElement { - const previewTemplate = this._previewTemplate; + const previewConfig = this._previewTemplate; + const previewTemplate = previewConfig ? previewConfig.template : null; let preview: HTMLElement; - if (previewTemplate && previewTemplate.template) { - const viewRef = this._viewContainerRef.createEmbeddedView(previewTemplate.template, - previewTemplate.context); + if (previewTemplate) { + const viewRef = previewConfig!.viewContainer.createEmbeddedView(previewTemplate, + previewConfig!.context); preview = viewRef.rootNodes[0]; this._previewRef = viewRef; preview.style.transform = @@ -771,7 +791,7 @@ export class DragRef { toggleNativeDragInteractions(preview, false); preview.classList.add('cdk-drag-preview'); - preview.setAttribute('dir', this._dir ? this._dir.value : 'ltr'); + preview.setAttribute('dir', this._direction); return preview; } @@ -825,13 +845,14 @@ export class DragRef { /** Creates an element that will be shown instead of the current element while dragging. */ private _createPlaceholderElement(): HTMLElement { - const placeholderTemplate = this._placeholderTemplate; + const placeholderConfig = this._placeholderTemplate; + const placeholderTemplate = placeholderConfig ? placeholderConfig.template : null; let placeholder: HTMLElement; - if (placeholderTemplate && placeholderTemplate.template) { - this._placeholderRef = this._viewContainerRef.createEmbeddedView( - placeholderTemplate.template, - placeholderTemplate.context + if (placeholderTemplate) { + this._placeholderRef = placeholderConfig!.viewContainer.createEmbeddedView( + placeholderTemplate, + placeholderConfig!.context ); placeholder = this._placeholderRef.rootNodes[0]; } else { @@ -877,7 +898,7 @@ export class DragRef { /** Gets the pointer position on the page, accounting for any position constraints. */ private _getConstrainedPointerPosition(event: MouseEvent | TouchEvent): Point { const point = this._getPointerPositionOnPage(event); - const dropContainerLock = this.dropContainer ? this.dropContainer.lockAxis : null; + const dropContainerLock = this._dropContainer ? this._dropContainer.lockAxis : null; if (this.lockAxis === 'x' || dropContainerLock === 'x') { point.y = this._pickupPositionOnPage.y; diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index cdafa2c78bdc..26dd62ee73c5 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -8,7 +8,7 @@ import {ElementRef} from '@angular/core'; import {DragDropRegistry} from './drag-drop-registry'; -import {Directionality} from '@angular/cdk/bidi'; +import {Direction} from '@angular/cdk/bidi'; import {Subject} from 'rxjs'; import {moveItemInArray} from './drag-utils'; import {DragRefInternal as DragRef} from './drag-ref'; @@ -50,6 +50,9 @@ export interface DropListRefInternal extends DropListRef {} export class DropListRef { private _document: Document; + /** Element that the drop list is attached to. */ + readonly element: HTMLElement; + /** * Unique ID for the drop list. * @deprecated No longer being used. To be removed. @@ -138,13 +141,16 @@ export class DropListRef { /** Connected siblings that currently have a dragged item. */ private _activeSiblings = new Set(); + /** Layout direction of the drop list. */ + private _direction: Direction = 'ltr'; + constructor( - public element: ElementRef, + element: ElementRef | HTMLElement, private _dragDropRegistry: DragDropRegistry, - _document: any, - private _dir?: Directionality) { + _document: any) { _dragDropRegistry.registerDropContainer(this); this._document = _document; + this.element = element instanceof ElementRef ? element.nativeElement : element; } /** Removes the drop list functionality from the DOM element. */ @@ -203,7 +209,7 @@ export class DropListRef { element.parentElement!.insertBefore(placeholder, element); this._activeDraggables.splice(newIndex, 0, item); } else { - this.element.nativeElement.appendChild(placeholder); + this.element.appendChild(placeholder); this._activeDraggables.push(item); } @@ -251,6 +257,13 @@ export class DropListRef { */ withItems(items: DragRef[]): this { this._draggables = items.slice(); + items.forEach(item => item._withDropContainer(this)); + return this; + } + + /** Sets the layout direction of the drop list. */ + withDirection(direction: Direction): this { + this._direction = direction; return this; } @@ -285,7 +298,7 @@ export class DropListRef { // 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._direction === 'rtl' ? this._itemPositions.slice().reverse() : this._itemPositions; return findIndex(items, currentItem => currentItem.drag === item); @@ -382,7 +395,7 @@ export class DropListRef { /** Caches the position of the drop list. */ private _cacheOwnPosition() { - this._clientRect = this.element.nativeElement.getBoundingClientRect(); + this._clientRect = this.element.getBoundingClientRect(); } /** Refreshes the position cache of the items and sibling containers. */ @@ -574,15 +587,13 @@ export class DropListRef { return false; } - const element = this.element.nativeElement; - // 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. - return elementFromPoint === element || element.contains(elementFromPoint); + return elementFromPoint === this.element || this.element.contains(elementFromPoint); } /** diff --git a/src/cdk/drag-drop/public-api.ts b/src/cdk/drag-drop/public-api.ts index 2e9c253e2fca..473894dfde1e 100644 --- a/src/cdk/drag-drop/public-api.ts +++ b/src/cdk/drag-drop/public-api.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -// TODO(crisbeto): export once API is finalized -// export * from './drag-ref'; -// export * from './drop-list-ref'; +export {DragDrop} from './drag-drop'; +export {DragRef, DragRefConfig} from './drag-ref'; +export {DropListRef} from './drop-list-ref'; export * from './drop-list-container'; export * from './drag-events'; @@ -16,7 +16,7 @@ export * from './drag-utils'; export * from './drag-drop-module'; export * from './drag-drop-registry'; -export * from './directives/drop-list'; +export {CdkDropList} from './directives/drop-list'; export * from './directives/drop-list-group'; export * from './directives/drag'; export * from './directives/drag-handle'; diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index f6af137c403d..1ded5a5d2007 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -27,7 +27,8 @@ export declare class CdkDrag implements AfterViewInit, OnChanges, OnDes started: EventEmitter; constructor( element: ElementRef, - dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry, _config: DragRefConfig, _dir: Directionality); + dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, viewportRuler: ViewportRuler, dragDropRegistry: DragDropRegistry, config: DragRefConfig, _dir: Directionality, + dragDrop?: DragDrop); getPlaceholderElement(): HTMLElement; getRootElement(): HTMLElement; ngAfterViewInit(): void; @@ -111,7 +112,7 @@ export interface CdkDragStart { source: CdkDrag; } -export declare class CdkDropList implements CdkDropListContainer, OnDestroy { +export declare class CdkDropList implements CdkDropListContainer, AfterContentInit, OnDestroy { _draggables: QueryList; _dropListRef: DropListRef>; connectedTo: (CdkDropList | string)[] | CdkDropList | string; @@ -126,7 +127,9 @@ export declare class CdkDropList implements CdkDropListContainer, OnDes lockAxis: 'x' | 'y'; orientation: 'horizontal' | 'vertical'; sorted: EventEmitter>; - constructor(element: ElementRef, dragDropRegistry: DragDropRegistry, _changeDetectorRef: ChangeDetectorRef, dir?: Directionality, _group?: CdkDropListGroup> | undefined, _document?: any); + constructor( + element: ElementRef, dragDropRegistry: DragDropRegistry, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup> | undefined, _document?: any, + dragDrop?: DragDrop); _getSiblingContainerFromPosition(item: CdkDrag, x: number, y: number): CdkDropListContainer | null; _isOverContainer(x: number, y: number): boolean; _sortItem(item: CdkDrag, pointerX: number, pointerY: number, pointerDelta: { @@ -137,6 +140,7 @@ export declare class CdkDropList implements CdkDropListContainer, OnDes enter(item: CdkDrag, pointerX: number, pointerY: number): void; exit(item: CdkDrag): void; getItemIndex(item: CdkDrag): number; + ngAfterContentInit(): void; ngOnDestroy(): void; start(): void; } @@ -168,11 +172,14 @@ 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 DragDrop { + constructor(_document: any, _ngZone: NgZone, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry); + createDrag(element: ElementRef | HTMLElement, config?: DragRefConfig): DragRef; + createDropList(element: ElementRef | HTMLElement): DropListRef; +} + export declare class DragDropModule { } @@ -193,6 +200,124 @@ export declare class DragDropRegistry { + beforeStarted: Subject; + data: T; + disabled: boolean; + dropped: Subject<{ + previousIndex: number; + currentIndex: number; + item: DragRef; + container: DropListRef; + previousContainer: DropListRef; + isPointerOverContainer: boolean; + }>; + ended: Subject<{ + source: DragRef; + }>; + entered: Subject<{ + container: DropListRef; + item: DragRef; + }>; + exited: Subject<{ + container: DropListRef; + item: DragRef; + }>; + lockAxis: 'x' | 'y'; + moved: Observable<{ + source: DragRef; + pointerPosition: { + x: number; + y: number; + }; + event: MouseEvent | TouchEvent; + delta: { + x: -1 | 0 | 1; + y: -1 | 0 | 1; + }; + }>; + released: Subject<{ + source: DragRef; + }>; + started: Subject<{ + source: DragRef; + }>; + constructor(element: ElementRef | HTMLElement, _config: DragRefConfig, _document: Document, _ngZone: NgZone, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry); + _withDropContainer(container: DropListRef): void; + disableHandle(handle: HTMLElement): void; + dispose(): void; + enableHandle(handle: HTMLElement): void; + getPlaceholderElement(): HTMLElement; + getRootElement(): HTMLElement; + isDragging(): boolean; + reset(): void; + withBoundaryElement(boundaryElement: ElementRef | HTMLElement | null): this; + withDirection(direction: Direction): this; + withHandles(handles: (HTMLElement | ElementRef)[]): this; + withPlaceholderTemplate(template: DragHelperTemplate | null): this; + withPreviewTemplate(template: DragHelperTemplate | null): this; + withRootElement(rootElement: ElementRef | HTMLElement): this; +} + +export interface DragRefConfig { + dragStartThreshold: number; + pointerDirectionChangeThreshold: number; +} + +export declare class DropListRef { + beforeStarted: Subject; + data: T; + disabled: boolean; + dropped: Subject<{ + item: DragRef; + currentIndex: number; + previousIndex: number; + container: DropListRef; + previousContainer: DropListRef; + isPointerOverContainer: boolean; + }>; + readonly element: HTMLElement; + enterPredicate: (drag: DragRef, drop: DropListRef) => boolean; + entered: Subject<{ + item: DragRef; + container: DropListRef; + }>; + exited: Subject<{ + item: DragRef; + container: DropListRef; + }>; + id: string; + lockAxis: 'x' | 'y'; + sorted: Subject<{ + previousIndex: number; + currentIndex: number; + container: DropListRef; + item: DragRef; + }>; + constructor(element: ElementRef | HTMLElement, _dragDropRegistry: DragDropRegistry, _document: any); + _canReceive(item: DragRef, x: number, y: number): boolean; + _getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | undefined; + _isOverContainer(x: number, y: number): boolean; + _sortItem(item: DragRef, pointerX: number, pointerY: number, pointerDelta: { + x: number; + y: number; + }): void; + _startReceiving(sibling: DropListRef): void; + _stopReceiving(sibling: DropListRef): void; + connectedTo(connectedTo: DropListRef[]): this; + dispose(): void; + drop(item: DragRef, currentIndex: number, previousContainer: DropListRef, isPointerOverContainer: boolean): void; + enter(item: DragRef, pointerX: number, pointerY: number): void; + exit(item: DragRef): void; + getItemIndex(item: DragRef): number; + isDragging(): boolean; + isReceiving(): boolean; + start(): void; + withDirection(direction: Direction): this; + withItems(items: DragRef[]): this; + withOrientation(orientation: 'vertical' | 'horizontal'): this; +} + export declare function moveItemInArray(array: T[], fromIndex: number, toIndex: number): void; export declare function transferArrayItem(currentArray: T[], targetArray: T[], currentIndex: number, targetIndex: number): void;