diff --git a/src/cdk/drag-drop/directives/drag-handle.ts b/src/cdk/drag-drop/directives/drag-handle.ts index 9dd1bd514181..01d11aa430c8 100644 --- a/src/cdk/drag-drop/directives/drag-handle.ts +++ b/src/cdk/drag-drop/directives/drag-handle.ts @@ -7,6 +7,7 @@ */ import { + AfterViewInit, Directive, ElementRef, InjectionToken, @@ -19,6 +20,7 @@ import {Subject} from 'rxjs'; import type {CdkDrag} from './drag'; import {CDK_DRAG_PARENT} from '../drag-parent'; import {assertElementNode} from './assertions'; +import {DragDropRegistry} from '../drag-drop-registry'; /** * Injection token that can be used to reference instances of `CdkDragHandle`. It serves as @@ -35,10 +37,11 @@ export const CDK_DRAG_HANDLE = new InjectionToken('CdkDragHandle' }, providers: [{provide: CDK_DRAG_HANDLE, useExisting: CdkDragHandle}], }) -export class CdkDragHandle implements OnDestroy { +export class CdkDragHandle implements AfterViewInit, OnDestroy { element = inject>(ElementRef); private _parentDrag = inject(CDK_DRAG_PARENT, {optional: true, skipSelf: true}); + private _dragDropRegistry = inject(DragDropRegistry); /** Emits when the state of the handle has changed. */ readonly _stateChanges = new Subject(); @@ -64,6 +67,21 @@ export class CdkDragHandle implements OnDestroy { this._parentDrag?._addHandle(this); } + ngAfterViewInit() { + if (!this._parentDrag) { + let parent = this.element.nativeElement.parentElement; + while (parent) { + const ref = this._dragDropRegistry.getDragDirectiveForNode(parent); + if (ref) { + this._parentDrag = ref; + ref._addHandle(this); + break; + } + parent = parent.parentElement; + } + } + } + ngOnDestroy() { this._parentDrag?._removeHandle(this); this._stateChanges.complete(); diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 11535b2bdf2a..8ec7fab27f5e 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -48,8 +48,7 @@ import type {CdkDropList} from './drop-list'; import {DragDrop} from '../drag-drop'; import {CDK_DRAG_CONFIG, DragDropConfig, DragStartDelay, DragAxis} from './config'; import {assertElementNode} from './assertions'; - -const DRAG_HOST_CLASS = 'cdk-drag'; +import {DragDropRegistry} from '../drag-drop-registry'; /** * Injection token that can be used to reference instances of `CdkDropList`. It serves as @@ -63,7 +62,7 @@ export const CDK_DROP_LIST = new InjectionToken('CdkDropList'); selector: '[cdkDrag]', exportAs: 'cdkDrag', host: { - 'class': DRAG_HOST_CLASS, + 'class': 'cdk-drag', '[class.cdk-drag-disabled]': 'disabled', '[class.cdk-drag-dragging]': '_dragRef.isDragging()', }, @@ -78,9 +77,9 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { private _changeDetectorRef = inject(ChangeDetectorRef); private _selfHandle = inject(CDK_DRAG_HANDLE, {optional: true, self: true}); private _parentDrag = inject(CDK_DRAG_PARENT, {optional: true, skipSelf: true}); + private _dragDropRegistry = inject(DragDropRegistry); private readonly _destroyed = new Subject(); - private static _dragInstances: CdkDrag[] = []; private _handles = new BehaviorSubject([]); private _previewTemplate: CdkDragPreview | null; private _placeholderTemplate: CdkDragPlaceholder | null; @@ -240,11 +239,7 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { zIndex: config?.zIndex, }); 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); + this._dragDropRegistry.registerDirectiveNode(this.element.nativeElement, this); if (config) { this._assignDefaults(config); @@ -348,10 +343,7 @@ 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._dragDropRegistry.removeDirectiveNode(this.element.nativeElement); // Unnecessary in most cases, but used to avoid extra change detections with `zone-paths-rxjs`. this._ngZone.runOutsideAngular(() => { @@ -487,12 +479,9 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { // the item was projected into another item by something like `ngTemplateOutlet`. let parent = this.element.nativeElement.parentElement; while (parent) { - if (parent.classList.contains(DRAG_HOST_CLASS)) { - ref.withParent( - CdkDrag._dragInstances.find(drag => { - return drag.element.nativeElement === parent; - })?._dragRef || null, - ); + const parentDrag = this._dragDropRegistry.getDragDirectiveForNode(parent); + if (parentDrag) { + ref.withParent(parentDrag._dragRef); break; } parent = parent.parentElement; diff --git a/src/cdk/drag-drop/directives/standalone-drag.spec.ts b/src/cdk/drag-drop/directives/standalone-drag.spec.ts index 775f9e69ef91..c4449abe204e 100644 --- a/src/cdk/drag-drop/directives/standalone-drag.spec.ts +++ b/src/cdk/drag-drop/directives/standalone-drag.spec.ts @@ -9,6 +9,7 @@ import { ViewEncapsulation, signal, } from '@angular/core'; +import {NgTemplateOutlet} from '@angular/common'; import {fakeAsync, flush, tick} from '@angular/core/testing'; import { dispatchEvent, @@ -1631,6 +1632,25 @@ describe('Standalone CdkDrag', () => { .toBe('translate3d(50px, 100px, 0px)'); })); + it('should be able to drag with a handle that is defined in a separate embedded view', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggableWithExternalTemplateHandle); + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const handle = fixture.nativeElement.querySelector('.handle'); + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 50, 100); + + expect(dragElement.style.transform) + .withContext('Expected not to be able to drag the element by itself.') + .toBeFalsy(); + + dragElementViaMouse(fixture, handle, 50, 100); + expect(dragElement.style.transform) + .withContext('Expected to drag the element by its handle.') + .toBe('translate3d(50px, 100px, 0px)'); + })); + it('should disable the tap highlight while dragging via the handle', fakeAsync(() => { // This test is irrelevant if the browser doesn't support styling the tap highlight color. if (!('webkitTapHighlightColor' in document.body.style)) { @@ -2010,3 +2030,21 @@ class DraggableNgContainerWithAlternateRoot { class PlainStandaloneDraggable { @ViewChild(CdkDrag) dragInstance: CdkDrag; } + +@Component({ + template: ` +
+ +
+ + +
+
+ `, + standalone: true, + imports: [CdkDrag, CdkDragHandle, NgTemplateOutlet], +}) +class StandaloneDraggableWithExternalTemplateHandle { + @ViewChild('dragElement') dragElement: ElementRef; +} diff --git a/src/cdk/drag-drop/drag-drop-registry.ts b/src/cdk/drag-drop/drag-drop-registry.ts index 3c789a2f933b..a8e2bd079fc7 100644 --- a/src/cdk/drag-drop/drag-drop-registry.ts +++ b/src/cdk/drag-drop/drag-drop-registry.ts @@ -23,6 +23,7 @@ import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; import {Observable, Observer, Subject, merge} from 'rxjs'; import type {DropListRef} from './drop-list-ref'; import type {DragRef} from './drag-ref'; +import type {CdkDrag} from './directives/drag'; /** Event options that can be used to bind an active, capturing event. */ const activeCapturingEventOptions = normalizePassiveListenerOptions({ @@ -79,6 +80,13 @@ export class DragDropRegistry<_ = unknown, __ = unknown> implements OnDestroy { */ private _draggingPredicate = (item: DragRef) => item.isDragging(); + /** + * Map tracking DOM nodes and their corresponding drag directives. Note that this is different + * from looking through the `_dragInstances` and getting their root node, because the root node + * isn't necessarily the node that the directive is set on. + */ + private _domNodesToDirectives: WeakMap | null = null; + /** * Emits the `touchmove` or `mousemove` events that are dispatched * while the user is dragging a drag item instance. @@ -262,9 +270,36 @@ export class DragDropRegistry<_ = unknown, __ = unknown> implements OnDestroy { return merge(...streams); } + /** + * Tracks the DOM node which has a draggable directive. + * @param node Node to track. + * @param dragRef Drag directive set on the node. + */ + registerDirectiveNode(node: Node, dragRef: CdkDrag): void { + this._domNodesToDirectives ??= new WeakMap(); + this._domNodesToDirectives.set(node, dragRef); + } + + /** + * Stops tracking a draggable directive node. + * @param node Node to stop tracking. + */ + removeDirectiveNode(node: Node): void { + this._domNodesToDirectives?.delete(node); + } + + /** + * Gets the drag directive corresponding to a specific DOM node, if any. + * @param node Node for which to do the lookup. + */ + getDragDirectiveForNode(node: Node): CdkDrag | null { + return this._domNodesToDirectives?.get(node) || null; + } + ngOnDestroy() { this._dragInstances.forEach(instance => this.removeDragItem(instance)); this._dropInstances.forEach(instance => this.removeDropContainer(instance)); + this._domNodesToDirectives = null; this._clearGlobalListeners(); this.pointerMove.complete(); this.pointerUp.complete(); diff --git a/tools/public_api_guard/cdk/drag-drop.md b/tools/public_api_guard/cdk/drag-drop.md index bb39566f6fdc..0624dfeb708b 100644 --- a/tools/public_api_guard/cdk/drag-drop.md +++ b/tools/public_api_guard/cdk/drag-drop.md @@ -150,7 +150,7 @@ export interface CdkDragExit { } // @public -export class CdkDragHandle implements OnDestroy { +export class CdkDragHandle implements AfterViewInit, OnDestroy { constructor(...args: unknown[]); get disabled(): boolean; set disabled(value: boolean); @@ -159,6 +159,8 @@ export class CdkDragHandle implements OnDestroy { // (undocumented) static ngAcceptInputType_disabled: unknown; // (undocumented) + ngAfterViewInit(): void; + // (undocumented) ngOnDestroy(): void; readonly _stateChanges: Subject; // (undocumented) @@ -351,13 +353,16 @@ export class DragDropModule { // @public export class DragDropRegistry<_ = unknown, __ = unknown> implements OnDestroy { constructor(...args: unknown[]); + getDragDirectiveForNode(node: Node): CdkDrag | null; isDragging(drag: DragRef): boolean; // (undocumented) ngOnDestroy(): void; readonly pointerMove: Subject; readonly pointerUp: Subject; + registerDirectiveNode(node: Node, dragRef: CdkDrag): void; registerDragItem(drag: DragRef): void; registerDropContainer(drop: DropListRef): void; + removeDirectiveNode(node: Node): void; removeDragItem(drag: DragRef): void; removeDropContainer(drop: DropListRef): void; // @deprecated