From 92cb67e2a2e0fda00d742d4a022e22675aea8358 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 25 Jan 2021 17:20:19 +0100 Subject: [PATCH] fix(cdk/drag-drop): not detecting parent when projected using ngTemplateOutlet (#21668) Some logic was added in #21227 which would only `stopPropagation` when dragging if a parent drag item is detected. The problem is that we resolve the parent using DI which won't work if the item is projected through something like `ngTemplateOutlet`. These changes resolve the issue by falling back to resolving the parent through the DOM. --- src/cdk/drag-drop/directives/drag.spec.ts | 61 +++++++++++++++++++++++ src/cdk/drag-drop/directives/drag.ts | 40 +++++++++++++-- src/cdk/drag-drop/drag-ref.ts | 15 ++++-- tools/public_api_guard/cdk/drag-drop.d.ts | 3 +- 4 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 0b8c79b0e907..a80f1f05c063 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -5592,6 +5592,21 @@ describe('CdkDrag', () => { expect(event.stopPropagation).toHaveBeenCalled(); })); + + it('should stop event propagation when dragging item nested via ng-template', fakeAsync(() => { + const fixture = createComponent(NestedDragsThroughTemplate); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.item.nativeElement; + + const event = createMouseEvent('mousedown'); + spyOn(event, 'stopPropagation').and.callThrough(); + + dispatchEvent(dragElement, event); + fixture.detectChanges(); + + expect(event.stopPropagation).toHaveBeenCalled(); + })); + }); }); @@ -6557,6 +6572,52 @@ class NestedDragsComponent { itemDragReleasedSpy = jasmine.createSpy('item drag released spy'); } +@Component({ + styles: [` + :host { + height: 400px; + width: 400px; + position: absolute; + } + .container { + height: 200px; + width: 200px; + position: absolute; + } + .item { + height: 50px; + width: 50px; + position: absolute; + } + `], + template: ` +
+ +
+ + +
+
+
+ ` +}) +class NestedDragsThroughTemplate { + @ViewChild('container') container: ElementRef; + @ViewChild('item') item: ElementRef; +} + @Component({ styles: [` .drop-list { diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 312a09edfb40..b14ae7c1db5f 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -56,12 +56,14 @@ import {DragDrop} from '../drag-drop'; import {CDK_DRAG_CONFIG, DragDropConfig, DragStartDelay, DragAxis} from './config'; import {assertElementNode} from './assertions'; +const DRAG_HOST_CLASS = 'cdk-drag'; + /** Element that can be moved inside a CdkDropList container. */ @Directive({ selector: '[cdkDrag]', exportAs: 'cdkDrag', host: { - 'class': 'cdk-drag', + 'class': DRAG_HOST_CLASS, '[class.cdk-drag-disabled]': 'disabled', '[class.cdk-drag-dragging]': '_dragRef.isDragging()', }, @@ -69,6 +71,7 @@ import {assertElementNode} from './assertions'; }) export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { private _destroyed = new Subject(); + private static _dragInstances: CdkDrag[] = []; /** Reference to the underlying drag instance. */ _dragRef: DragRef>; @@ -193,17 +196,21 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { @Optional() private _dir: Directionality, dragDrop: DragDrop, private _changeDetectorRef: ChangeDetectorRef, @Optional() @Self() @Inject(CDK_DRAG_HANDLE) private _selfHandle?: CdkDragHandle, - @Optional() @SkipSelf() @Inject(CDK_DRAG_PARENT) parentDrag?: CdkDrag) { + @Optional() @SkipSelf() @Inject(CDK_DRAG_PARENT) private _parentDrag?: CdkDrag) { this._dragRef = dragDrop.createDrag(element, { dragStartThreshold: config && config.dragStartThreshold != null ? config.dragStartThreshold : 5, pointerDirectionChangeThreshold: config && config.pointerDirectionChangeThreshold != null ? config.pointerDirectionChangeThreshold : 5, zIndex: config?.zIndex, - parentDragRef: parentDrag?._dragRef }); this._dragRef.data = this; + // We have to keep track of the drag instances in order to be able to match an element to + // a drag instance. We can't go through the global registry of `DragRef`, because the root + // element could be different. + CdkDrag._dragInstances.push(this); + if (config) { this._assignDefaults(config); } @@ -318,6 +325,10 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { this.dropContainer.removeItem(this); } + const index = CdkDrag._dragInstances.indexOf(this); + if (index > -1) { + CdkDrag._dragInstances.splice(index, -1); + } this._destroyed.next(); this._destroyed.complete(); this._dragRef.dispose(); @@ -392,6 +403,29 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { } } }); + + // This only needs to be resolved once. + ref.beforeStarted.pipe(take(1)).subscribe(() => { + // If we managed to resolve a parent through DI, use it. + if (this._parentDrag) { + ref.withParent(this._parentDrag._dragRef); + return; + } + + // Otherwise fall back to resolving the parent by looking up the DOM. This can happen if + // the item was projected into another item by something like `ngTemplateOutlet`. + let parent = this.element.nativeElement.parentElement; + while (parent) { + // `classList` needs to be null checked, because IE doesn't have it on some elements. + if (parent.classList?.contains(DRAG_HOST_CLASS)) { + ref.withParent(CdkDrag._dragInstances.find(drag => { + return drag.element.nativeElement === parent; + })?._dragRef || null); + break; + } + parent = parent.parentElement; + } + }); } /** Handles the events from the underlying `DragRef`. */ diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index d0340a3567de..1784b1e546f5 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -230,6 +230,9 @@ export class DragRef { /** Layout direction of the item. */ private _direction: Direction = 'ltr'; + /** Ref that the current drag item is nested in. */ + private _parentDragRef: DragRef | null; + /** * Cached shadow root that the element is placed in. `null` means that the element isn't in * the shadow DOM and `undefined` means that it hasn't been resolved yet. Should be read via @@ -324,7 +327,7 @@ export class DragRef { private _viewportRuler: ViewportRuler, private _dragDropRegistry: DragDropRegistry) { - this.withRootElement(element); + this.withRootElement(element).withParent(_config.parentDragRef || null); this._parentPositions = new ParentPositionTracker(_document, _viewportRuler); _dragDropRegistry.registerDragItem(this); } @@ -430,6 +433,12 @@ export class DragRef { return this; } + /** Sets the parent ref that the ref is nested in. */ + withParent(parent: DragRef | null): this { + this._parentDragRef = parent; + return this; + } + /** Removes the dragging functionality from the DOM element. */ dispose() { this._removeRootElementListeners(this._rootElement); @@ -461,7 +470,7 @@ export class DragRef { this._resizeSubscription.unsubscribe(); this._parentPositions.clear(); this._boundaryElement = this._rootElement = this._ownerSVGElement = this._placeholderTemplate = - this._previewTemplate = this._anchor = null!; + this._previewTemplate = this._anchor = this._parentDragRef = null!; } /** Checks whether the element is currently being dragged. */ @@ -790,7 +799,7 @@ export class DragRef { private _initializeDragSequence(referenceElement: HTMLElement, event: MouseEvent | TouchEvent) { // Stop propagation if the item is inside another // draggable so we don't start multiple drag sequences. - if (this._config.parentDragRef) { + if (this._parentDragRef) { event.stopPropagation(); } diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index fc5588fd0f21..cfc4447935d8 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -42,7 +42,7 @@ export declare class CdkDrag implements AfterViewInit, OnChanges, OnDes constructor( element: ElementRef, dropContainer: CdkDropList, - _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, config: DragDropConfig, _dir: Directionality, dragDrop: DragDrop, _changeDetectorRef: ChangeDetectorRef, _selfHandle?: CdkDragHandle | undefined, parentDrag?: CdkDrag); + _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, config: DragDropConfig, _dir: Directionality, dragDrop: DragDrop, _changeDetectorRef: ChangeDetectorRef, _selfHandle?: CdkDragHandle | undefined, _parentDrag?: CdkDrag | undefined); getFreeDragPosition(): { readonly x: number; readonly y: number; @@ -318,6 +318,7 @@ export declare class DragRef { withBoundaryElement(boundaryElement: ElementRef | HTMLElement | null): this; withDirection(direction: Direction): this; withHandles(handles: (HTMLElement | ElementRef)[]): this; + withParent(parent: DragRef | null): this; withPlaceholderTemplate(template: DragHelperTemplate | null): this; withPreviewTemplate(template: DragPreviewTemplate | null): this; withRootElement(rootElement: ElementRef | HTMLElement): this;