diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index a40a1aaf52ab..b3ed516540a0 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -4255,6 +4255,44 @@ describe('CdkDrag', () => { expect(spy).not.toHaveBeenCalled(); })); + it('should clone the CDK Drag to preview container (via template ref)', () => { + const fixture = createComponent(DraggableInDropZoneWithCustomDragPreviewContainerTemplateRef); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + + startDraggingViaMouse(fixture, item); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + const previewContainer = document.querySelector('#preview-container')! as HTMLElement; + + expect(preview.parentNode).toBe(previewContainer); + }); + + it('should clone the CDK Drag to preview container (via element ref)', () => { + const fixture = createComponent(DraggableInDropZoneWithCustomDragPreviewContainerElementRef); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + + startDraggingViaMouse(fixture, item); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + expect(preview.parentNode) + .toBe(fixture.componentInstance.previewContainerElementRef.nativeElement); + }); + + it('should clone the CDK Drag to (global)', () => { + const fixture = createComponent(DraggableInDropZoneWithCustomDragPreviewContainerString); + fixture.componentInstance.previewContainer = 'global'; + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + + startDraggingViaMouse(fixture, item); + const previewContainer = document.querySelector('body')! as HTMLElement; + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + expect(preview.parentNode).toBe(previewContainer); + }); + it('should throw if drop list is attached to an ng-container', fakeAsync(() => { expect(() => { createComponent(DropListOnNgContainer).detectChanges(); @@ -5934,6 +5972,59 @@ class DraggableInDropZoneWithCustomPreview { constrainPosition: (point: Point) => Point; } +@Component({ + template: ` +
+
+ {{item}} + Hello {{item}} +
+
+
+ `, +}) +class DraggableInDropZoneWithCustomDragPreviewContainerTemplateRef { + @ViewChildren(CdkDrag) dragItems: QueryList; + items = ['Zero', 'One', 'Two', 'Three']; +} + +@Component({ + template: ` +
+
+ {{item}} + Hello {{item}} +
+
+
+ `, +}) +class DraggableInDropZoneWithCustomDragPreviewContainerElementRef { + @ViewChildren(CdkDrag) dragItems: QueryList; + @ViewChild('previewContainer') previewContainerElementRef: ElementRef; + items = ['Zero', 'One', 'Two', 'Three']; +} + +@Component({ + template: ` +
+
+
+ {{item}} + Hello {{item}} +
+
+
+ `, +}) +class DraggableInDropZoneWithCustomDragPreviewContainerString { + @ViewChildren(CdkDrag) dragItems: QueryList; + previewContainer = 'global'; + items = ['Zero', 'One', 'Two', 'Three']; +} @Component({ template: ` diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 8884997743b1..0daacaea8500 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -137,6 +137,22 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { /** Class to be added to the preview element. */ @Input('cdkDragPreviewClass') previewClass: string | string[]; + /** + * Target for Drag Preview clone to append into. + * `global` will place the clone at the end of ``. + * An `ElementRef` or `HTMLElement` will place the clone inside the element. + * + * When `global` (default), the Drag Preview is appended to `` to avoid + * potential issues with `z-index` and `overflow: hidden`. By changing the + * location you run risk of ancestors styled with `overflow: hidden` hiding part + * or all of the Drag Preview, as well as other items styled with a higher + * `z-index` overlapping the preview container. + * + * Specifying the cdkDropList can lead to rendering performance issues. + */ + @Input('cdkDragPreviewContainer') previewContainer: + 'global' | ElementRef | HTMLElement; + /** Emits when the user starts dragging the item. */ @Output('cdkDragStarted') started: EventEmitter = new EventEmitter(); @@ -356,6 +372,20 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { return element; } + /** + * Gets the element for the preview clone to be created in, + * based on the `previewContainer` value. + */ + private _getPreviewContainer(): HTMLElement | ElementRef | null { + const cloneTarget = this.previewContainer; + + if (!cloneTarget || cloneTarget === 'global') { + return null; + } + + return cloneTarget; + } + /** Syncs the inputs of the CdkDrag with the options of the underlying DragRef. */ private _syncInputs(ref: DragRef>) { ref.beforeStarted.subscribe(() => { @@ -383,7 +413,8 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { ref .withBoundaryElement(this._getBoundaryElement()) .withPlaceholderTemplate(placeholder) - .withPreviewTemplate(preview); + .withPreviewTemplate(preview) + .withPreviewContainer(this._getPreviewContainer()); if (dir) { ref.withDirection(dir.value); diff --git a/src/cdk/drag-drop/drag-drop.md b/src/cdk/drag-drop/drag-drop.md index 96b16bdc06a8..f61e2fdb352f 100644 --- a/src/cdk/drag-drop/drag-drop.md +++ b/src/cdk/drag-drop/drag-drop.md @@ -136,6 +136,16 @@ directive: +### Customizing the drag preview container +When a `cdkDrag` element is picked up, it will create a preview element which is appended to +the end of ``. There may be times when you need to have the preview created elsewhere. + +This can be configured using the `cdkDragPreviewContainer` input. +Passing `global` will respect the default behavior and the preview will be appended to ``. +You can also pass in `ElementRef` for finer control. + + + ### List orientation The `cdkDropList` directive assumes that lists are vertical by default. This can be changed by setting the `orientation` property to `"horizontal". diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 804f85f6d94b..3032ce3e2a22 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -227,6 +227,9 @@ export class DragRef { /** Layout direction of the item. */ private _direction: Direction = 'ltr'; + /** Location for preview clone to append into */ + private _previewContainer: HTMLElement | null; + /** Axis along which dragging is locked. */ lockAxis: 'x' | 'y'; @@ -378,6 +381,15 @@ export class DragRef { return this; } + /** + * Registers the element which the drag template is to be cloned into. + * @param previewContainer Element which the drag template is to be cloned into + */ + withPreviewContainer(previewContainer: HTMLElement | ElementRef | null): this { + this._previewContainer = previewContainer ? coerceElement(previewContainer) : null; + return this; + } + /** * 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 @@ -740,9 +752,9 @@ export class DragRef { const element = this._rootElement; const parent = element.parentNode!; const preview = this._preview = this._createPreviewElement(); + const previewContainer = this._previewContainer || getPreviewInsertionPoint(this._document); const placeholder = this._placeholder = this._createPlaceholderElement(); const anchor = this._anchor = this._anchor || this._document.createComment(''); - // Insert an anchor node so that we can restore the element's position in the DOM. parent.insertBefore(anchor, element); @@ -751,7 +763,7 @@ export class DragRef { // from the DOM completely, because iOS will stop firing all subsequent events in the chain. toggleVisibility(element, false); this._document.body.appendChild(parent.replaceChild(placeholder, element)); - getPreviewInsertionPoint(this._document).appendChild(preview); + previewContainer.appendChild(preview); this.started.next({source: this}); // Emit before notifying the container. dropContainer.start(); this._initialContainer = dropContainer; diff --git a/src/components-examples/cdk/drag-drop/cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example.css b/src/components-examples/cdk/drag-drop/cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example.css new file mode 100644 index 000000000000..edc5219836aa --- /dev/null +++ b/src/components-examples/cdk/drag-drop/cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example.css @@ -0,0 +1,64 @@ +.example-list { + width: 500px; + max-width: 100%; + border: solid 1px #ccc; + min-height: 60px; + display: block; + background: white; + border-radius: 4px; + overflow: hidden; +} + +.example-box { + padding: 20px 10px; + border-bottom: solid 1px #ccc; + color: rgba(0, 0, 0, 0.87); + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + cursor: move; + background: white; + font-size: 14px; +} + +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.example-box:last-child { + border: none; +} + +.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + +.example-container { + width: 300px; + max-width: 100%; + margin: 0 25px 25px 0; + display: inline-block; + vertical-align: top; +} + +.example-list-two .cdk-drag-dragging { + border: 1px solid #ccc; +} + +.example-preview-container .cdk-drag-dragging { + border: 2px solid #ccc; +} diff --git a/src/components-examples/cdk/drag-drop/cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example.html b/src/components-examples/cdk/drag-drop/cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example.html new file mode 100644 index 000000000000..87d2aa0fb60c --- /dev/null +++ b/src/components-examples/cdk/drag-drop/cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example.html @@ -0,0 +1,17 @@ +
+
+
+ {{movie.title}} +
+
+
+ +
+
+
+ {{movie.title}} +
+
+
+ +
diff --git a/src/components-examples/cdk/drag-drop/cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example.ts b/src/components-examples/cdk/drag-drop/cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example.ts new file mode 100644 index 000000000000..754b62006521 --- /dev/null +++ b/src/components-examples/cdk/drag-drop/cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example.ts @@ -0,0 +1,46 @@ +import {Component} from '@angular/core'; +import {CdkDragDrop, moveItemInArray} from '@angular/cdk/drag-drop'; + +/** + * @title Drag&Drop custom preview container example + */ +@Component({ + selector: 'cdk-drag-drop-custom-preview-container-example', + templateUrl: 'cdk-drag-drop-custom-preview-container-example.html', + styleUrls: ['cdk-drag-drop-custom-preview-container-example.css'], +}) +export class CdkDragDropCustomPreviewContainerExample { + moviesOne = [ + { + title: 'Episode I - The Phantom Menace' + }, + { + title: 'Episode II - Attack of the Clones' + }, + { + title: 'Episode III - Revenge of the Sith' + }, + { + title: 'Episode IV - A New Hope' + }, + ]; + + moviesTwo = [ + { + title: 'Episode V - The Empire Strikes Back' + }, + { + title: 'Episode VI - Return of the Jedi' + }, + { + title: 'Episode VII - The Force Awakens' + }, + { + title: 'Episode VIII - The Last Jedi' + }, + ]; + + drop(which: any[], event: CdkDragDrop<{title: string, poster: string}[]>) { + moveItemInArray(which, event.previousIndex, event.currentIndex); + } +} diff --git a/src/components-examples/cdk/drag-drop/index.ts b/src/components-examples/cdk/drag-drop/index.ts index b691711c9b6f..f70d14e7fba5 100644 --- a/src/components-examples/cdk/drag-drop/index.ts +++ b/src/components-examples/cdk/drag-drop/index.ts @@ -18,6 +18,9 @@ import { import { CdkDragDropCustomPreviewExample } from './cdk-drag-drop-custom-preview/cdk-drag-drop-custom-preview-example'; +import { + CdkDragDropCustomPreviewContainerExample, +} from './cdk-drag-drop-custom-preview-container/cdk-drag-drop-custom-preview-container-example'; import {CdkDragDropDelayExample} from './cdk-drag-drop-delay/cdk-drag-drop-delay-example'; import { CdkDragDropDisabledSortingExample @@ -49,6 +52,7 @@ export { CdkDragDropConnectedSortingGroupExample, CdkDragDropCustomPlaceholderExample, CdkDragDropCustomPreviewExample, + CdkDragDropCustomPreviewContainerExample, CdkDragDropDelayExample, CdkDragDropDisabledExample, CdkDragDropDisabledSortingExample, @@ -69,6 +73,7 @@ const EXAMPLES = [ CdkDragDropConnectedSortingGroupExample, CdkDragDropCustomPlaceholderExample, CdkDragDropCustomPreviewExample, + CdkDragDropCustomPreviewContainerExample, CdkDragDropDelayExample, CdkDragDropDisabledExample, CdkDragDropDisabledSortingExample, diff --git a/src/dev-app/drag-drop/drag-drop-demo.html b/src/dev-app/drag-drop/drag-drop-demo.html index 8a599a0d37ec..6989005e7bfe 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.html +++ b/src/dev-app/drag-drop/drag-drop-demo.html @@ -7,7 +7,7 @@

To do

(cdkDropListDropped)="drop($event)" [cdkDropListLockAxis]="axisLock" [cdkDropListData]="todo"> -
+
{{item}}
@@ -21,7 +21,7 @@

Done

(cdkDropListDropped)="drop($event)" [cdkDropListLockAxis]="axisLock" [cdkDropListData]="done"> -
+
{{item}}
@@ -38,7 +38,7 @@

Ages

(cdkDropListDropped)="drop($event)" [cdkDropListLockAxis]="axisLock" [cdkDropListData]="ages"> -
+
{{item}}
@@ -53,7 +53,7 @@

Preferred Ages

(cdkDropListDropped)="drop($event)" [cdkDropListLockAxis]="axisLock" [cdkDropListData]="preferredAges"> -
+
{{item}}
@@ -66,6 +66,8 @@

Free dragging

Drag me around
+
+

Data

{{todo.join(', ')}}
@@ -86,6 +88,18 @@

Axis locking

+
+

Drag Preview Container

+ + Drag preview should clone into + + Global + Preview Container (HTMLElement) + Preview Container (ElementRef) + + +
+

Drag start delay

diff --git a/src/dev-app/drag-drop/drag-drop-demo.scss b/src/dev-app/drag-drop/drag-drop-demo.scss index ce97633fa757..74c09f8489c2 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.scss +++ b/src/dev-app/drag-drop/drag-drop-demo.scss @@ -99,3 +99,11 @@ pre { justify-content: center; align-items: center; } + +.demo-preview-container .cdk-drag-preview { + border: 2px solid #3f51b5; +} + +.demo-list .cdk-drag-preview { + border: 2px solid #607d8b; +} diff --git a/src/dev-app/drag-drop/drag-drop-demo.ts b/src/dev-app/drag-drop/drag-drop-demo.ts index a3b7eed54a2a..be96d73c3464 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.ts +++ b/src/dev-app/drag-drop/drag-drop-demo.ts @@ -6,7 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, ViewEncapsulation, ChangeDetectionStrategy} from '@angular/core'; +import { + Component, + ViewEncapsulation, + ChangeDetectionStrategy, + ViewChild, + ElementRef +} from '@angular/core'; import {MatIconRegistry} from '@angular/material/icon'; import {DomSanitizer} from '@angular/platform-browser'; import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop'; @@ -19,8 +25,10 @@ import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk/drag changeDetection: ChangeDetectionStrategy.OnPush, }) export class DragAndDropDemo { + @ViewChild('previewContainer') previewContainerElementRef: ElementRef; axisLock: 'x' | 'y'; dragStartDelay = 0; + dragPreviewContainer: 'global' | ElementRef | HTMLElement = 'global'; todo = [ 'Go out for Lunch', 'Make a cool app', diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index 7422c8819f87..d5eaeb6cf631 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -36,6 +36,7 @@ export declare class CdkDrag implements AfterViewInit, OnChanges, OnDes lockAxis: DragAxis; moved: Observable>; previewClass: string | string[]; + previewContainer: 'global' | ElementRef | HTMLElement; released: EventEmitter; rootElementSelector: string; started: EventEmitter; @@ -54,7 +55,7 @@ export declare class CdkDrag implements AfterViewInit, OnChanges, OnDes ngOnDestroy(): void; reset(): void; static ngAcceptInputType_disabled: BooleanInput; - static ɵdir: i0.ɵɵDirectiveDefWithMeta, "[cdkDrag]", ["cdkDrag"], { "data": "cdkDragData"; "lockAxis": "cdkDragLockAxis"; "rootElementSelector": "cdkDragRootElement"; "boundaryElement": "cdkDragBoundary"; "dragStartDelay": "cdkDragStartDelay"; "freeDragPosition": "cdkDragFreeDragPosition"; "disabled": "cdkDragDisabled"; "constrainPosition": "cdkDragConstrainPosition"; "previewClass": "cdkDragPreviewClass"; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, ["_previewTemplate", "_placeholderTemplate", "_handles"]>; + static ɵdir: i0.ɵɵDirectiveDefWithMeta, "[cdkDrag]", ["cdkDrag"], { "data": "cdkDragData"; "lockAxis": "cdkDragLockAxis"; "rootElementSelector": "cdkDragRootElement"; "boundaryElement": "cdkDragBoundary"; "dragStartDelay": "cdkDragStartDelay"; "freeDragPosition": "cdkDragFreeDragPosition"; "disabled": "cdkDragDisabled"; "constrainPosition": "cdkDragConstrainPosition"; "previewClass": "cdkDragPreviewClass"; "previewContainer": "cdkDragPreviewContainer"; }, { "started": "cdkDragStarted"; "released": "cdkDragReleased"; "ended": "cdkDragEnded"; "entered": "cdkDragEntered"; "exited": "cdkDragExited"; "dropped": "cdkDragDropped"; "moved": "cdkDragMoved"; }, ["_previewTemplate", "_placeholderTemplate", "_handles"]>; static ɵfac: i0.ɵɵFactoryDef, [null, { optional: true; skipSelf: true; }, null, null, null, { optional: true; }, { optional: true; }, null, null, { optional: true; self: true; }]>; } @@ -315,6 +316,7 @@ export declare class DragRef { withDirection(direction: Direction): this; withHandles(handles: (HTMLElement | ElementRef)[]): this; withPlaceholderTemplate(template: DragHelperTemplate | null): this; + withPreviewContainer(previewContainer: HTMLElement | ElementRef | null): this; withPreviewTemplate(template: DragPreviewTemplate | null): this; withRootElement(rootElement: ElementRef | HTMLElement): this; }