diff --git a/src/cdk/drag-drop/directives/drag-handle.ts b/src/cdk/drag-drop/directives/drag-handle.ts index 30bdf97eb6e5..a871c42d6d63 100644 --- a/src/cdk/drag-drop/directives/drag-handle.ts +++ b/src/cdk/drag-drop/directives/drag-handle.ts @@ -18,6 +18,7 @@ import { booleanAttribute, } from '@angular/core'; import {Subject} from 'rxjs'; +import type {CdkDrag} from './drag'; import {CDK_DRAG_PARENT} from '../drag-parent'; import {assertElementNode} from './assertions'; @@ -38,9 +39,6 @@ export const CDK_DRAG_HANDLE = new InjectionToken('CdkDragHandle' providers: [{provide: CDK_DRAG_HANDLE, useExisting: CdkDragHandle}], }) export class CdkDragHandle implements OnDestroy { - /** Closest parent draggable instance. */ - _parentDrag: {} | undefined; - /** Emits when the state of the handle has changed. */ readonly _stateChanges = new Subject(); @@ -57,16 +55,17 @@ export class CdkDragHandle implements OnDestroy { constructor( public element: ElementRef, - @Inject(CDK_DRAG_PARENT) @Optional() @SkipSelf() parentDrag?: any, + @Inject(CDK_DRAG_PARENT) @Optional() @SkipSelf() private _parentDrag?: CdkDrag, ) { if (typeof ngDevMode === 'undefined' || ngDevMode) { assertElementNode(element.nativeElement, 'cdkDragHandle'); } - this._parentDrag = parentDrag; + _parentDrag?._addHandle(this); } ngOnDestroy() { + this._parentDrag?._removeHandle(this); this._stateChanges.complete(); } } diff --git a/src/cdk/drag-drop/directives/drag-placeholder.ts b/src/cdk/drag-drop/directives/drag-placeholder.ts index a98fa094b4b9..e6df55b485e8 100644 --- a/src/cdk/drag-drop/directives/drag-placeholder.ts +++ b/src/cdk/drag-drop/directives/drag-placeholder.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, TemplateRef, Input, InjectionToken} from '@angular/core'; +import {Directive, TemplateRef, Input, InjectionToken, inject, OnDestroy} from '@angular/core'; +import {CDK_DRAG_PARENT} from '../drag-parent'; /** * Injection token that can be used to reference instances of `CdkDragPlaceholder`. It serves as @@ -24,8 +25,17 @@ export const CDK_DRAG_PLACEHOLDER = new InjectionToken('CdkD standalone: true, providers: [{provide: CDK_DRAG_PLACEHOLDER, useExisting: CdkDragPlaceholder}], }) -export class CdkDragPlaceholder { +export class CdkDragPlaceholder implements OnDestroy { + private _drag = inject(CDK_DRAG_PARENT); + /** Context data to be added to the placeholder template instance. */ @Input() data: T; - constructor(public templateRef: TemplateRef) {} + + constructor(public templateRef: TemplateRef) { + this._drag._setPlaceholderTemplate(this); + } + + ngOnDestroy(): void { + this._drag._resetPlaceholderTemplate(this); + } } diff --git a/src/cdk/drag-drop/directives/drag-preview.ts b/src/cdk/drag-drop/directives/drag-preview.ts index 779cfba9f0a4..9c23d8a03bf2 100644 --- a/src/cdk/drag-drop/directives/drag-preview.ts +++ b/src/cdk/drag-drop/directives/drag-preview.ts @@ -6,7 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, InjectionToken, Input, TemplateRef, booleanAttribute} from '@angular/core'; +import { + Directive, + InjectionToken, + Input, + OnDestroy, + TemplateRef, + booleanAttribute, + inject, +} from '@angular/core'; +import {CDK_DRAG_PARENT} from '../drag-parent'; /** * Injection token that can be used to reference instances of `CdkDragPreview`. It serves as @@ -24,12 +33,20 @@ export const CDK_DRAG_PREVIEW = new InjectionToken('CdkDragPrevi standalone: true, providers: [{provide: CDK_DRAG_PREVIEW, useExisting: CdkDragPreview}], }) -export class CdkDragPreview { +export class CdkDragPreview implements OnDestroy { + private _drag = inject(CDK_DRAG_PARENT); + /** Context data to be added to the preview template instance. */ @Input() data: T; /** Whether the preview should preserve the same size as the item that is being dragged. */ @Input({transform: booleanAttribute}) matchSize: boolean = false; - constructor(public templateRef: TemplateRef) {} + constructor(public templateRef: TemplateRef) { + this._drag._setPreviewTemplate(this); + } + + ngOnDestroy(): void { + this._drag._resetPreviewTemplate(this); + } } diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 72f94741eba3..0b28c538b684 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -10,8 +10,6 @@ import {Directionality} from '@angular/cdk/bidi'; import {DOCUMENT} from '@angular/common'; import { AfterViewInit, - ContentChild, - ContentChildren, Directive, ElementRef, EventEmitter, @@ -21,7 +19,6 @@ import { OnDestroy, Optional, Output, - QueryList, SkipSelf, ViewContainerRef, OnChanges, @@ -32,7 +29,7 @@ import { booleanAttribute, } from '@angular/core'; import {coerceElement, coerceNumberProperty} from '@angular/cdk/coercion'; -import {Observable, Observer, Subject, merge} from 'rxjs'; +import {BehaviorSubject, Observable, Observer, Subject, merge} from 'rxjs'; import {startWith, take, map, takeUntil, switchMap, tap} from 'rxjs/operators'; import type { CdkDragDrop, @@ -44,8 +41,8 @@ import type { CdkDragRelease, } from '../drag-events'; import {CDK_DRAG_HANDLE, CdkDragHandle} from './drag-handle'; -import {CDK_DRAG_PLACEHOLDER, CdkDragPlaceholder} from './drag-placeholder'; -import {CDK_DRAG_PREVIEW, CdkDragPreview} from './drag-preview'; +import {CdkDragPlaceholder} from './drag-placeholder'; +import {CdkDragPreview} from './drag-preview'; import {CDK_DRAG_PARENT} from '../drag-parent'; import {DragRef, Point, PreviewContainer} from '../drag-ref'; import type {CdkDropList} from './drop-list'; @@ -77,19 +74,13 @@ export const CDK_DROP_LIST = new InjectionToken('CdkDropList'); export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { private readonly _destroyed = new Subject(); private static _dragInstances: CdkDrag[] = []; + private _handles = new BehaviorSubject([]); + private _previewTemplate: CdkDragPreview | null; + private _placeholderTemplate: CdkDragPlaceholder | null; /** Reference to the underlying drag instance. */ _dragRef: DragRef>; - /** Elements that can be used to drag the draggable item. */ - @ContentChildren(CDK_DRAG_HANDLE, {descendants: true}) _handles: QueryList; - - /** Element that will be used as a template to create the draggable item's preview. */ - @ContentChild(CDK_DRAG_PREVIEW) _previewTemplate: CdkDragPreview; - - /** Template for placeholder element rendered to show where a draggable would be dropped. */ - @ContentChild(CDK_DRAG_PLACEHOLDER) _placeholderTemplate: CdkDragPlaceholder; - /** Arbitrary data to attach to this drag instance. */ @Input('cdkDragData') data: T; @@ -351,12 +342,49 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { // Unnecessary in most cases, but used to avoid extra change detections with `zone-paths-rxjs`. this._ngZone.runOutsideAngular(() => { + this._handles.complete(); this._destroyed.next(); this._destroyed.complete(); this._dragRef.dispose(); }); } + _addHandle(handle: CdkDragHandle) { + const handles = this._handles.getValue(); + handles.push(handle); + this._handles.next(handles); + } + + _removeHandle(handle: CdkDragHandle) { + const handles = this._handles.getValue(); + const index = handles.indexOf(handle); + + if (index > -1) { + handles.splice(index, 1); + this._handles.next(handles); + } + } + + _setPreviewTemplate(preview: CdkDragPreview) { + this._previewTemplate = preview; + } + + _resetPreviewTemplate(preview: CdkDragPreview) { + if (preview === this._previewTemplate) { + this._previewTemplate = null; + } + } + + _setPlaceholderTemplate(placeholder: CdkDragPlaceholder) { + this._placeholderTemplate = placeholder; + } + + _resetPlaceholderTemplate(placeholder: CdkDragPlaceholder) { + if (placeholder === this._placeholderTemplate) { + this._placeholderTemplate = null; + } + } + /** Syncs the root element with the `DragRef`. */ private _updateRootElement() { const element = this.element.nativeElement as HTMLElement; @@ -559,30 +587,25 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { /** Sets up the listener that syncs the handles with the drag ref. */ private _setupHandlesListener() { // Listen for any newly-added handles. - this._handles.changes + this._handles .pipe( - startWith(this._handles), // Sync the new handles with the DragRef. - tap((handles: QueryList) => { - const childHandleElements = handles - .filter(handle => handle._parentDrag === this) - .map(handle => handle.element); + tap(handles => { + const handleElements = handles.map(handle => handle.element); // Usually handles are only allowed to be a descendant of the drag element, but if // the consumer defined a different drag root, we should allow the drag element // itself to be a handle too. if (this._selfHandle && this.rootElementSelector) { - childHandleElements.push(this.element); + handleElements.push(this.element); } - this._dragRef.withHandles(childHandleElements); + this._dragRef.withHandles(handleElements); }), // Listen if the state of any of the handles changes. - switchMap((handles: QueryList) => { + switchMap((handles: CdkDragHandle[]) => { return merge( - ...handles.map(item => { - return item._stateChanges.pipe(startWith(item)); - }), + ...handles.map(item => item._stateChanges.pipe(startWith(item))), ) as Observable; }), takeUntil(this._destroyed), diff --git a/src/cdk/drag-drop/drag-parent.ts b/src/cdk/drag-drop/drag-parent.ts index 5df90660c7f5..fa2a5baabf30 100644 --- a/src/cdk/drag-drop/drag-parent.ts +++ b/src/cdk/drag-drop/drag-parent.ts @@ -7,6 +7,7 @@ */ import {InjectionToken} from '@angular/core'; +import type {CdkDrag} from './directives/drag'; /** * Injection token that can be used for a `CdkDrag` to provide itself as a parent to the @@ -14,4 +15,4 @@ import {InjectionToken} from '@angular/core'; * to avoid circular imports. * @docs-private */ -export const CDK_DRAG_PARENT = new InjectionToken<{}>('CDK_DRAG_PARENT'); +export const CDK_DRAG_PARENT = new InjectionToken('CDK_DRAG_PARENT'); diff --git a/tools/public_api_guard/cdk/drag-drop.md b/tools/public_api_guard/cdk/drag-drop.md index f9d894ee8526..4882c460318c 100644 --- a/tools/public_api_guard/cdk/drag-drop.md +++ b/tools/public_api_guard/cdk/drag-drop.md @@ -18,7 +18,6 @@ import { NumberInput } from '@angular/cdk/coercion'; import { Observable } from 'rxjs'; import { OnChanges } from '@angular/core'; import { OnDestroy } from '@angular/core'; -import { QueryList } from '@angular/core'; import { ScrollDispatcher } from '@angular/cdk/scrolling'; import { SimpleChanges } from '@angular/core'; import { Subject } from 'rxjs'; @@ -33,7 +32,7 @@ export const CDK_DRAG_CONFIG: InjectionToken; export const CDK_DRAG_HANDLE: InjectionToken; // @public -export const CDK_DRAG_PARENT: InjectionToken<{}>; +export const CDK_DRAG_PARENT: InjectionToken>; // @public export const CDK_DRAG_PLACEHOLDER: InjectionToken>; @@ -53,6 +52,8 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { element: ElementRef, dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, config: DragDropConfig, _dir: Directionality, dragDrop: DragDrop, _changeDetectorRef: ChangeDetectorRef, _selfHandle?: CdkDragHandle | undefined, _parentDrag?: CdkDrag | undefined); + // (undocumented) + _addHandle(handle: CdkDragHandle): void; boundaryElement: string | ElementRef | HTMLElement; constrainPosition?: (userPointerPosition: Point, dragRef: DragRef, dimensions: DOMRect, pickupPositionInElement: Point) => Point; data: T; @@ -70,7 +71,6 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { getFreeDragPosition(): Readonly; getPlaceholderElement(): HTMLElement; getRootElement(): HTMLElement; - _handles: QueryList; lockAxis: DragAxis; readonly moved: Observable>; // (undocumented) @@ -81,17 +81,25 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { ngOnChanges(changes: SimpleChanges): void; // (undocumented) ngOnDestroy(): void; - _placeholderTemplate: CdkDragPlaceholder; previewClass: string | string[]; previewContainer: PreviewContainer; - _previewTemplate: CdkDragPreview; readonly released: EventEmitter; + // (undocumented) + _removeHandle(handle: CdkDragHandle): void; reset(): void; + // (undocumented) + _resetPlaceholderTemplate(placeholder: CdkDragPlaceholder): void; + // (undocumented) + _resetPreviewTemplate(preview: CdkDragPreview): void; rootElementSelector: string; setFreeDragPosition(value: Point): void; + // (undocumented) + _setPlaceholderTemplate(placeholder: CdkDragPlaceholder): void; + // (undocumented) + _setPreviewTemplate(preview: CdkDragPreview): void; readonly started: EventEmitter; // (undocumented) - static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, ["_previewTemplate", "_placeholderTemplate", "_handles"], never, true, never>; + static ɵdir: i0.ɵɵDirectiveDeclaration, "[cdkDrag]", ["cdkDrag"], { "data": { "alias": "cdkDragData"; "required": false; }; "lockAxis": { "alias": "cdkDragLockAxis"; "required": false; }; "rootElementSelector": { "alias": "cdkDragRootElement"; "required": false; }; "boundaryElement": { "alias": "cdkDragBoundary"; "required": false; }; "dragStartDelay": { "alias": "cdkDragStartDelay"; "required": false; }; "freeDragPosition": { "alias": "cdkDragFreeDragPosition"; "required": false; }; "disabled": { "alias": "cdkDragDisabled"; "required": false; }; "constrainPosition": { "alias": "cdkDragConstrainPosition"; "required": false; }; "previewClass": { "alias": "cdkDragPreviewClass"; "required": false; }; "previewContainer": { "alias": "cdkDragPreviewContainer"; "required": false; }; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, never, never, true, never>; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration, [null, { optional: true; skipSelf: true; }, null, null, null, { optional: true; }, { optional: true; }, null, null, { optional: true; self: true; }, { optional: true; skipSelf: true; }]>; } @@ -144,7 +152,7 @@ export interface CdkDragExit { // @public export class CdkDragHandle implements OnDestroy { - constructor(element: ElementRef, parentDrag?: any); + constructor(element: ElementRef, _parentDrag?: CdkDrag | undefined); get disabled(): boolean; set disabled(value: boolean); // (undocumented) @@ -153,7 +161,6 @@ export class CdkDragHandle implements OnDestroy { static ngAcceptInputType_disabled: unknown; // (undocumented) ngOnDestroy(): void; - _parentDrag: {} | undefined; readonly _stateChanges: Subject; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration; @@ -180,10 +187,12 @@ export interface CdkDragMove { } // @public -export class CdkDragPlaceholder { +export class CdkDragPlaceholder implements OnDestroy { constructor(templateRef: TemplateRef); data: T; // (undocumented) + ngOnDestroy(): void; + // (undocumented) templateRef: TemplateRef; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration, "ng-template[cdkDragPlaceholder]", never, { "data": { "alias": "data"; "required": false; }; }, {}, never, never, true, never>; @@ -192,13 +201,15 @@ export class CdkDragPlaceholder { } // @public -export class CdkDragPreview { +export class CdkDragPreview implements OnDestroy { constructor(templateRef: TemplateRef); data: T; matchSize: boolean; // (undocumented) static ngAcceptInputType_matchSize: unknown; // (undocumented) + ngOnDestroy(): void; + // (undocumented) templateRef: TemplateRef; // (undocumented) static ɵdir: i0.ɵɵDirectiveDeclaration, "ng-template[cdkDragPreview]", never, { "data": { "alias": "data"; "required": false; }; "matchSize": { "alias": "matchSize"; "required": false; }; }, {}, never, never, true, never>;