Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cdk/drag-drop): custom drag clone target #20537

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <body> (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();
Expand Down Expand Up @@ -5934,6 +5972,59 @@ class DraggableInDropZoneWithCustomPreview {
constrainPosition: (point: Point) => Point;
}

@Component({
template: `
<div cdkDropList style="width: 100px; background: pink;">
<div *ngFor="let item of items" cdkDrag [cdkDragPreviewContainer]="previewContainer"
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">
{{item}}
<ng-template cdkDragPlaceholder>Hello {{item}}</ng-template>
</div>
</div>
<div id="preview-container" #previewContainer></div>
`,
})
class DraggableInDropZoneWithCustomDragPreviewContainerTemplateRef {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
items = ['Zero', 'One', 'Two', 'Three'];
}

@Component({
template: `
<div cdkDropList style="width: 100px; background: pink;">
<div *ngFor="let item of items" cdkDrag [cdkDragPreviewContainer]="previewContainer"
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">
{{item}}
<ng-template cdkDragPlaceholder>Hello {{item}}</ng-template>
</div>
</div>
<div id="preview-container" #previewContainer></div>
`,
})
class DraggableInDropZoneWithCustomDragPreviewContainerElementRef {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
@ViewChild('previewContainer') previewContainerElementRef: ElementRef<HTMLDivElement>;
items = ['Zero', 'One', 'Two', 'Three'];
}

@Component({
template: `
<div id="preview-container">
<div cdkDropList style="width: 100px; background: pink;">
<div *ngFor="let item of items" cdkDrag [cdkDragPreviewContainer]="previewContainer"
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">
{{item}}
<ng-template cdkDragPlaceholder>Hello {{item}}</ng-template>
</div>
</div>
</div>
`,
})
class DraggableInDropZoneWithCustomDragPreviewContainerString {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
previewContainer = 'global';
items = ['Zero', 'One', 'Two', 'Three'];
}

@Component({
template: `
Expand Down
33 changes: 32 additions & 1 deletion src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ export class CdkDrag<T = any> 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 `<body>`.
* An `ElementRef` or `HTMLElement` will place the clone inside the element.
*
* When `global` (default), the Drag Preview is appended to `<body>` 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> | HTMLElement;

geometricpanda marked this conversation as resolved.
Show resolved Hide resolved
/** Emits when the user starts dragging the item. */
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();

Expand Down Expand Up @@ -356,6 +372,20 @@ export class CdkDrag<T = any> 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<HTMLElement> | 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<CdkDrag<T>>) {
ref.beforeStarted.subscribe(() => {
Expand Down Expand Up @@ -383,7 +413,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
ref
.withBoundaryElement(this._getBoundaryElement())
.withPlaceholderTemplate(placeholder)
.withPreviewTemplate(preview);
.withPreviewTemplate(preview)
.withPreviewContainer(this._getPreviewContainer());

if (dir) {
ref.withDirection(dir.value);
Expand Down
10 changes: 10 additions & 0 deletions src/cdk/drag-drop/drag-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,16 @@ directive:

<!-- example(cdk-drag-drop-custom-placeholder) -->

### 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 `<body>`. There may be times when you need to have the preview created elsewhere.
geometricpanda marked this conversation as resolved.
Show resolved Hide resolved

This can be configured using the `cdkDragPreviewContainer` input.
Passing `global` will respect the default behavior and the preview will be appended to `<body>`.
You can also pass in `ElementRef` for finer control.

<!-- example(cdk-drag-drop-custom-preview-container) -->

### List orientation
The `cdkDropList` directive assumes that lists are vertical by default. This can be
changed by setting the `orientation` property to `"horizontal".
Expand Down
16 changes: 14 additions & 2 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ export class DragRef<T = any> {
/** 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';

Expand Down Expand Up @@ -378,6 +381,15 @@ export class DragRef<T = any> {
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<HTMLElement> | null): this {
this._previewContainer = previewContainer ? coerceElement<HTMLElement>(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
Expand Down Expand Up @@ -740,9 +752,9 @@ export class DragRef<T = any> {
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);

Expand All @@ -751,7 +763,7 @@ export class DragRef<T = any> {
// 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div class="example-container">
<div cdkDropList class="example-list example-list-one" (cdkDropListDropped)="drop(moviesOne, $event)">
<div class="example-box" *ngFor="let movie of moviesOne," cdkDrag cdkDragPreviewContainer="global">
{{movie.title}}
</div>
</div>
</div>

<div class="example-container">
<div cdkDropList class="example-list example-list-two" (cdkDropListDropped)="drop(moviesTwo, $event)">
<div class="example-box" *ngFor="let movie of moviesTwo," cdkDrag [cdkDragPreviewContainer]="previewContainer">
{{movie.title}}
</div>
</div>
</div>

<div class="example-preview-container" #previewContainer></div>
Original file line number Diff line number Diff line change
@@ -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);
}
}
5 changes: 5 additions & 0 deletions src/components-examples/cdk/drag-drop/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,6 +52,7 @@ export {
CdkDragDropConnectedSortingGroupExample,
CdkDragDropCustomPlaceholderExample,
CdkDragDropCustomPreviewExample,
CdkDragDropCustomPreviewContainerExample,
CdkDragDropDelayExample,
CdkDragDropDisabledExample,
CdkDragDropDisabledSortingExample,
Expand All @@ -69,6 +73,7 @@ const EXAMPLES = [
CdkDragDropConnectedSortingGroupExample,
CdkDragDropCustomPlaceholderExample,
CdkDragDropCustomPreviewExample,
CdkDragDropCustomPreviewContainerExample,
CdkDragDropDelayExample,
CdkDragDropDisabledExample,
CdkDragDropDisabledSortingExample,
Expand Down
Loading