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): allow for preview container to be customized #21830

Merged
merged 1 commit into from
Feb 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cdk/drag-drop/directives/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
listAutoScrollDisabled?: boolean;
listOrientation?: DropListOrientation;
zIndex?: number;
previewContainer?: 'global' | 'parent';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this option type PreviewContainer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the interface for the global config object. It didn't seem like an HTMLElement would make sense there, given that it's configured before the app is initialized.

}
52 changes: 50 additions & 2 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {of as observableOf} from 'rxjs';

import {DragDropModule} from '../drag-drop-module';
import {CdkDragDrop, CdkDragEnter, CdkDragStart} from '../drag-events';
import {Point, DragRef} from '../drag-ref';
import {Point, DragRef, PreviewContainer} from '../drag-ref';
import {extendStyles} from '../drag-styling';
import {moveItemInArray} from '../drag-utils';

Expand Down Expand Up @@ -1235,7 +1235,8 @@ describe('CdkDrag', () => {
constrainPosition: () => ({x: 1337, y: 42}),
previewClass: 'custom-preview-class',
boundaryElement: '.boundary',
rootElementSelector: '.root'
rootElementSelector: '.root',
previewContainer: 'parent'
};

const fixture = createComponent(PlainStandaloneDraggable, [{
Expand All @@ -1251,6 +1252,7 @@ describe('CdkDrag', () => {
expect(drag.previewClass).toBe('custom-preview-class');
expect(drag.boundaryElement).toBe('.boundary');
expect(drag.rootElementSelector).toBe('.root');
expect(drag.previewContainer).toBe('parent');
}));

it('should not throw if touches and changedTouches are empty', fakeAsync(() => {
Expand Down Expand Up @@ -2580,6 +2582,47 @@ describe('CdkDrag', () => {
expect(placeholder.parentNode).toBeFalsy('Expected placeholder to be removed from the DOM');
}));

it('should insert the preview into the `body` if previewContainer is set to `global`',
fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.componentInstance.previewContainer = 'global';
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(document.body);
}));

it('should insert the preview into the parent node if previewContainer is set to `parent`',
fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.componentInstance.previewContainer = 'parent';
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const list = fixture.nativeElement.querySelector('.drop-list');

startDraggingViaMouse(fixture, item);
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(list).toBeTruthy();
expect(preview.parentNode).toBe(list);
}));

it('should insert the preview into a particular element, if specified', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const previewContainer = fixture.componentInstance.alternatePreviewContainer;

expect(previewContainer).toBeTruthy();
fixture.componentInstance.previewContainer = previewContainer;
fixture.detectChanges();

startDraggingViaMouse(fixture, item);
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
expect(preview.parentNode).toBe(previewContainer.nativeElement);
}));

it('should remove the id from the placeholder', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZone);
fixture.detectChanges();
Expand Down Expand Up @@ -5789,17 +5832,21 @@ const DROP_ZONE_FIXTURE_TEMPLATE = `
[cdkDragData]="item"
[cdkDragBoundary]="boundarySelector"
[cdkDragPreviewClass]="previewClass"
[cdkDragPreviewContainer]="previewContainer"
[style.height.px]="item.height"
[style.margin-bottom.px]="item.margin"
(cdkDragStarted)="startedSpy($event)"
style="width: 100%; background: red;">{{item.value}}</div>
</div>

<div #alternatePreviewContainer></div>
`;

@Component({template: DROP_ZONE_FIXTURE_TEMPLATE})
class DraggableInDropZone implements AfterViewInit {
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
@ViewChild(CdkDropList) dropInstance: CdkDropList;
@ViewChild('alternatePreviewContainer') alternatePreviewContainer: ElementRef<HTMLElement>;
items = [
{value: 'Zero', height: ITEM_HEIGHT, margin: 0},
{value: 'One', height: ITEM_HEIGHT, margin: 0},
Expand All @@ -5814,6 +5861,7 @@ class DraggableInDropZone implements AfterViewInit {
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
});
startedSpy = jasmine.createSpy('started spy');
previewContainer: PreviewContainer = 'global';

constructor(protected _elementRef: ElementRef) {}

Expand Down
28 changes: 24 additions & 4 deletions src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ 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 {CDK_DRAG_PARENT} from '../drag-parent';
import {DragRef, Point} from '../drag-ref';
import {DragRef, Point, PreviewContainer} from '../drag-ref';
import {CDK_DROP_LIST, CdkDropListInternal as CdkDropList} from './drop-list';
import {DragDrop} from '../drag-drop';
import {CDK_DRAG_CONFIG, DragDropConfig, DragStartDelay, DragAxis} from './config';
Expand Down Expand Up @@ -140,6 +140,21 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
/** Class to be added to the preview element. */
@Input('cdkDragPreviewClass') previewClass: string | string[];

/**
* Configures the place into which the preview of the item will be inserted. Can be configured
* globally through `CDK_DROP_LIST`. Possible values:
* - `global` - Preview will be inserted at the bottom of the `<body>`. The advantage is that
* you don't have to worry about `overflow: hidden` or `z-index`, but the item won't retain
* its inherited styles.
* - `parent` - Preview will be inserted into the parent of the drag item. The advantage is that
* inherited styles will be preserved, but it may be clipped by `overflow: hidden` or not be
* visible due to `z-index`. Furthermore, the preview is going to have an effect over selectors
* like `:nth-child` and some flexbox configurations.
* - `ElementRef<HTMLElement> | HTMLElement` - Preview will be inserted into a specific element.
* Same advantages and disadvantages as `parent`.
*/
@Input('cdkDragPreviewContainer') previewContainer: PreviewContainer;

/** Emits when the user starts dragging the item. */
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();

Expand Down Expand Up @@ -396,7 +411,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
ref
.withBoundaryElement(this._getBoundaryElement())
.withPlaceholderTemplate(placeholder)
.withPreviewTemplate(preview);
.withPreviewTemplate(preview)
.withPreviewContainer(this.previewContainer || 'global');

if (dir) {
ref.withDirection(dir.value);
Expand Down Expand Up @@ -481,8 +497,8 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
/** Assigns the default input values based on a provided config object. */
private _assignDefaults(config: DragDropConfig) {
const {
lockAxis, dragStartDelay, constrainPosition, previewClass,
boundaryElement, draggingDisabled, rootElementSelector
lockAxis, dragStartDelay, constrainPosition, previewClass, boundaryElement, draggingDisabled,
rootElementSelector, previewContainer
} = config;

this.disabled = draggingDisabled == null ? false : draggingDisabled;
Expand All @@ -507,6 +523,10 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
if (rootElementSelector) {
this.rootElementSelector = rootElementSelector;
}

if (previewContainer) {
this.previewContainer = previewContainer;
}
}

static ngAcceptInputType_disabled: BooleanInput;
Expand Down
13 changes: 13 additions & 0 deletions src/cdk/drag-drop/drag-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,19 @@ to be applied.

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

### Drag preview insertion point
By default, the preview of a `cdkDrag` will be inserted into the `<body>` of the page in order to
avoid issues with `z-index` and `overflow: hidden`. This may not be desireable in some cases,
because the preview won't retain its inherited styles. You can control where the preview is inserted
using the `cdkDrawPreviewContainer` input. The possible values are:

| Value | Description | Advantages | Disadvantages |
|-------------------|-------------------------|------------------------|---------------------------|
| `global` | Default value. Preview is inserted into the `<body>` or the closest shadow root. | Preview won't be affected by `z-index` or `overflow: hidden`. It also won't affect `:nth-child` selectors and flex layouts. | Doesn't retain inherited styles.
| `parent` | Preview is inserted inside the parent of the item that is being dragged. | Preview inherits the same styles as the dragged item. | Preview may be clipped by `overflow: hidden` or be placed under other elements due to `z-index`. Furthermore, it can affect `:nth-child` selectors and some flex layouts.
| `ElementRef` or `HTMLElement` | Preview will be inserted into the specified element. | Preview inherits styles from the specified container element. | Preview may be clipped by `overflow: hidden` or be placed under other elements due to `z-index`. Furthermore, it can affect `:nth-child` selectors and some flex layouts.


### Customizing the drag placeholder
While a `cdkDrag` element is being dragged, the CDK will create a placeholder element that will
show where it will be placed when it's dropped. By default the placeholder is a clone of the element
Expand Down
69 changes: 54 additions & 15 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ export interface Point {
y: number;
}

/**
* Possible places into which the preview of a drag item can be inserted.
* - `global` - Preview will be inserted at the bottom of the `<body>`. The advantage is that
* you don't have to worry about `overflow: hidden` or `z-index`, but the item won't retain
* its inherited styles.
* - `parent` - Preview will be inserted into the parent of the drag item. The advantage is that
* inherited styles will be preserved, but it may be clipped by `overflow: hidden` or not be
* visible due to `z-index`. Furthermore, the preview is going to have an effect over selectors
* like `:nth-child` and some flexbox configurations.
* - `ElementRef<HTMLElement> | HTMLElement` - Preview will be inserted into a specific element.
* Same advantages and disadvantages as `parent`.
*/
export type PreviewContainer = 'global' | 'parent' | ElementRef<HTMLElement> | HTMLElement;

/**
* Reference to a draggable item. Used to manipulate or dispose of the item.
*/
Expand All @@ -93,6 +107,9 @@ export class DragRef<T = any> {
/** Reference to the view of the preview element. */
private _previewRef: EmbeddedViewRef<any> | null;

/** Container into which to insert the preview. */
private _previewContainer: PreviewContainer | undefined;

/** Reference to the view of the placeholder element. */
private _placeholderRef: EmbeddedViewRef<any> | null;

Expand Down Expand Up @@ -542,6 +559,15 @@ export class DragRef<T = any> {
return this;
}

/**
* Sets the container into which to insert the preview element.
* @param value Container into which to insert the preview.
*/
withPreviewContainer(value: PreviewContainer): this {
this._previewContainer = value;
return this;
}

/** Updates the item's sort order based on the last-known pointer position. */
_sortFromLastPointerPosition() {
const position = this._lastKnownPointerPosition;
Expand Down Expand Up @@ -762,7 +788,7 @@ export class DragRef<T = any> {

if (dropContainer) {
const element = this._rootElement;
const parent = element.parentNode!;
const parent = element.parentNode as HTMLElement;
const preview = this._preview = this._createPreviewElement();
const placeholder = this._placeholder = this._createPlaceholderElement();
const anchor = this._anchor = this._anchor || this._document.createComment('');
Expand All @@ -778,7 +804,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, shadowRoot).appendChild(preview);
this._getPreviewInsertionPoint(parent, shadowRoot).appendChild(preview);
this.started.next({source: this}); // Emit before notifying the container.
dropContainer.start();
this._initialContainer = dropContainer;
Expand Down Expand Up @@ -1361,6 +1387,32 @@ export class DragRef<T = any> {

return this._cachedShadowRoot;
}

/** Gets the element into which the drag preview should be inserted. */
private _getPreviewInsertionPoint(initialParent: HTMLElement,
shadowRoot: ShadowRoot | null): HTMLElement {
const previewContainer = this._previewContainer || 'global';

if (previewContainer === 'parent') {
return initialParent;
}

if (previewContainer === 'global') {
const documentRef = this._document;

// We can't use the body if the user is in fullscreen mode,
// because the preview will render under the fullscreen element.
// TODO(crisbeto): dedupe this with the `FullscreenOverlayContainer` eventually.
return shadowRoot ||
documentRef.fullscreenElement ||
(documentRef as any).webkitFullscreenElement ||
(documentRef as any).mozFullScreenElement ||
(documentRef as any).msFullscreenElement ||
documentRef.body;
}

return coerceElement(previewContainer);
}
}

/**
Expand Down Expand Up @@ -1397,19 +1449,6 @@ function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
return event.type[0] === 't';
}

/** Gets the element into which the drag preview should be inserted. */
function getPreviewInsertionPoint(documentRef: any, shadowRoot: ShadowRoot | null): HTMLElement {
// We can't use the body if the user is in fullscreen mode,
// because the preview will render under the fullscreen element.
// TODO(crisbeto): dedupe this with the `FullscreenOverlayContainer` eventually.
return shadowRoot ||
documentRef.fullscreenElement ||
documentRef.webkitFullscreenElement ||
documentRef.mozFullScreenElement ||
documentRef.msFullscreenElement ||
documentRef.body;
}

/**
* Gets the root HTML element of an embedded view.
* If the root is not an HTML element it gets wrapped in one.
Expand Down
2 changes: 1 addition & 1 deletion src/cdk/drag-drop/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

export {DragDrop} from './drag-drop';
export {DragRef, DragRefConfig, Point} from './drag-ref';
export {DragRef, DragRefConfig, Point, PreviewContainer} from './drag-ref';
export {DropListRef} from './drop-list-ref';
export {CDK_DRAG_PARENT} from './drag-parent';

Expand Down
2 changes: 1 addition & 1 deletion src/dev-app/drag-drop/drag-drop-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
justify-content: space-between;
box-sizing: border-box;

.cdk-drop-list-dragging &:not(.cdk-drag-placeholder) {
.cdk-drop-list-dragging &:not(.cdk-drag-placeholder):not(.cdk-drag-preview) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

Expand Down
7 changes: 6 additions & 1 deletion tools/public_api_guard/cdk/drag-drop.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
lockAxis: DragAxis;
moved: Observable<CdkDragMove<T>>;
previewClass: string | string[];
previewContainer: PreviewContainer;
released: EventEmitter<CdkDragRelease>;
rootElementSelector: string;
started: EventEmitter<CdkDragStart>;
Expand All @@ -54,7 +55,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
ngOnDestroy(): void;
reset(): void;
static ngAcceptInputType_disabled: BooleanInput;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkDrag<any>, "[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<any>, "[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<CdkDrag<any>, [null, { optional: true; skipSelf: true; }, null, null, null, { optional: true; }, { optional: true; }, null, null, { optional: true; self: true; }, { optional: true; skipSelf: true; }]>;
}

Expand Down Expand Up @@ -220,6 +221,7 @@ export interface DragDropConfig extends Partial<DragRefConfig> {
listOrientation?: DropListOrientation;
lockAxis?: DragAxis;
previewClass?: string | string[];
previewContainer?: 'global' | 'parent';
rootElementSelector?: string;
sortingDisabled?: boolean;
zIndex?: number;
Expand Down Expand Up @@ -320,6 +322,7 @@ export declare class DragRef<T = any> {
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;
withParent(parent: DragRef<unknown> | null): this;
withPlaceholderTemplate(template: DragHelperTemplate | null): this;
withPreviewContainer(value: PreviewContainer): this;
withPreviewTemplate(template: DragPreviewTemplate | null): this;
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this;
}
Expand Down Expand Up @@ -408,4 +411,6 @@ export interface Point {
y: number;
}

export declare type PreviewContainer = 'global' | 'parent' | ElementRef<HTMLElement> | HTMLElement;

export declare function transferArrayItem<T = any>(currentArray: T[], targetArray: T[], currentIndex: number, targetIndex: number): void;