Skip to content

Commit

Permalink
feat(drag-drop): add option to match size of dragged element in custo…
Browse files Browse the repository at this point in the history
…m preview (#18362)

By default we don't resize custom previews, because we'd have to make assumptions about what the consumer wants to show. These changes add the `matchSize` input which allows the consumer to opt into matching the custom preview size to the dragged element size.

Fixes #18177.
  • Loading branch information
crisbeto authored Feb 20, 2020
1 parent 591ac9c commit 74b3441
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 19 deletions.
10 changes: 10 additions & 0 deletions src/cdk/drag-drop/directives/drag-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import {Directive, TemplateRef, Input} from '@angular/core';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';

/**
* Element that will be used as a template for the preview
Expand All @@ -18,5 +19,14 @@ import {Directive, TemplateRef, Input} from '@angular/core';
export class CdkDragPreview<T = any> {
/** 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()
get matchSize(): boolean { return this._matchSize; }
set matchSize(value: boolean) { this._matchSize = coerceBooleanProperty(value); }
private _matchSize = false;

constructor(public templateRef: TemplateRef<T>) {}

static ngAcceptInputType_matchSize: BooleanInput;
}
41 changes: 37 additions & 4 deletions src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2931,6 +2931,37 @@ describe('CdkDrag', () => {
expect(preview.classList).toContain('custom-class');
}));

it('should be able to apply the size of the dragged element to a custom preview',
fakeAsync(() => {
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
fixture.componentInstance.matchPreviewSize = true;
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
const itemRect = item.getBoundingClientRect();

startDraggingViaMouse(fixture, item);

const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;

expect(preview).toBeTruthy();
expect(preview.style.width).toBe(`${itemRect.width}px`);
expect(preview.style.height).toBe(`${itemRect.height}px`);
}));

it('should preserve the pickup position if the custom preview inherits the size of the ' +
'dragged element', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
fixture.componentInstance.matchPreviewSize = true;
fixture.detectChanges();
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;

startDraggingViaMouse(fixture, item, 50, 50);

const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;

expect(preview.style.transform).toBe('translate3d(8px, 33px, 0px)');
}));

it('should not throw when custom preview only has text', fakeAsync(() => {
const fixture = createComponent(DraggableInDropZoneWithCustomTextOnlyPreview);
fixture.detectChanges();
Expand Down Expand Up @@ -5050,10 +5081,11 @@ class DraggableInScrollableHorizontalDropZone extends DraggableInHorizontalDropZ
{{item}}
<ng-container *ngIf="renderCustomPreview">
<div
class="custom-preview"
style="width: 50px; height: 50px; background: purple;"
*cdkDragPreview>Custom preview</div>
<ng-template cdkDragPreview [matchSize]="matchPreviewSize">
<div
class="custom-preview"
style="width: 50px; height: 50px; background: purple;">Custom preview</div>
</ng-template>
</ng-container>
</div>
</div>
Expand All @@ -5065,6 +5097,7 @@ class DraggableInDropZoneWithCustomPreview {
items = ['Zero', 'One', 'Two', 'Three'];
boundarySelector: string;
renderCustomPreview = true;
matchPreviewSize = false;
previewClass: string | string[];
constrainPosition: (point: Point) => Point;
}
Expand Down
1 change: 1 addition & 0 deletions src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
const preview = this._previewTemplate ? {
template: this._previewTemplate.templateRef,
context: this._previewTemplate.data,
matchSize: this._previewTemplate.matchSize,
viewContainer: this._viewContainerRef
} : null;

Expand Down
4 changes: 4 additions & 0 deletions src/cdk/drag-drop/drag-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ restrict the user to only be able to do so using a handle element, you can do it
When a `cdkDrag` element is picked up, it will create a preview element visible while dragging.
By default, this will be a clone of the original element positioned next to the user's cursor.
This preview can be customized, though, by providing a custom template via `*cdkDragPreview`.
Using the default configuration the custom preview won't match the size of the original dragged
element, because the CDK doesn't make assumptions about the element's content. If you want the
size to be matched, you can pass `true` to the `matchSize` input.

Note that the cloned element will remove its `id` attribute in order to avoid having multiple
elements with the same `id` on the page. This will cause any CSS that targets that `id` not
to be applied.
Expand Down
47 changes: 34 additions & 13 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ interface DragHelperTemplate<T = any> {
context: T;
}

/** Template that can be used to create a drag preview element. */
interface DragPreviewTemplate<T = any> extends DragHelperTemplate<T> {
matchSize?: boolean;
}

/** Point on the page or within an element. */
export interface Point {
x: number;
Expand Down Expand Up @@ -192,7 +197,7 @@ export class DragRef<T = any> {
private _boundaryRect?: ClientRect;

/** Element that will be used as a template to create the draggable item's preview. */
private _previewTemplate?: DragHelperTemplate | null;
private _previewTemplate?: DragPreviewTemplate | null;

/** Template for placeholder element rendered to show where a draggable would be dropped. */
private _placeholderTemplate?: DragHelperTemplate | null;
Expand Down Expand Up @@ -332,7 +337,7 @@ export class DragRef<T = any> {
* Registers the template that should be used for the drag preview.
* @param template Template that from which to stamp out the preview.
*/
withPreviewTemplate(template: DragHelperTemplate | null): this {
withPreviewTemplate(template: DragPreviewTemplate | null): this {
this._previewTemplate = template;
return this;
}
Expand Down Expand Up @@ -772,10 +777,12 @@ export class DragRef<T = any> {
this._boundaryRect = this._boundaryElement.getBoundingClientRect();
}

// If we have a custom preview template, the element won't be visible anyway so we avoid the
// extra `getBoundingClientRect` calls and just move the preview next to the cursor.
this._pickupPositionInElement = this._previewTemplate && this._previewTemplate.template ?
{x: 0, y: 0} :
// If we have a custom preview we can't know ahead of time how large it'll be so we position
// it next to the cursor. The exception is when the consumer has opted into making the preview
// the same size as the root element, in which case we do know the size.
const previewTemplate = this._previewTemplate;
this._pickupPositionInElement = previewTemplate && previewTemplate.template &&
!previewTemplate.matchSize ? {x: 0, y: 0} :
this._getPointerPositionInElement(referenceElement, event);
const pointerPosition = this._pickupPositionOnPage = this._getPointerPositionOnPage(event);
this._pointerDirectionDelta = {x: 0, y: 0};
Expand Down Expand Up @@ -879,16 +886,17 @@ export class DragRef<T = any> {
previewConfig!.context);
preview = getRootNode(viewRef, this._document);
this._previewRef = viewRef;
preview.style.transform =
getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);

if (previewConfig!.matchSize) {
matchElementSize(preview, this._rootElement);
} else {
preview.style.transform =
getTransform(this._pickupPositionOnPage.x, this._pickupPositionOnPage.y);
}
} else {
const element = this._rootElement;
const elementRect = element.getBoundingClientRect();

preview = deepCloneNode(element);
preview.style.width = `${elementRect.width}px`;
preview.style.height = `${elementRect.height}px`;
preview.style.transform = getTransform(elementRect.left, elementRect.top);
matchElementSize(preview, element);
}

extendStyles(preview.style, {
Expand Down Expand Up @@ -1297,3 +1305,16 @@ function getRootNode(viewRef: EmbeddedViewRef<any>, _document: Document): HTMLEl

return rootNode as HTMLElement;
}

/**
* Matches the target element's size to the source's size.
* @param target Element that needs to be resized.
* @param source Element whose size needs to be matched.
*/
function matchElementSize(target: HTMLElement, source: HTMLElement): void {
const sourceRect = source.getBoundingClientRect();

target.style.width = `${sourceRect.width}px`;
target.style.height = `${sourceRect.height}px`;
target.style.transform = getTransform(sourceRect.left, sourceRect.top);
}
7 changes: 5 additions & 2 deletions tools/public_api_guard/cdk/drag-drop.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,12 @@ export declare class CdkDragPlaceholder<T = any> {

export declare class CdkDragPreview<T = any> {
data: T;
get matchSize(): boolean;
set matchSize(value: boolean);
templateRef: TemplateRef<T>;
constructor(templateRef: TemplateRef<T>);
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkDragPreview<any>, "ng-template[cdkDragPreview]", never, { "data": "data"; }, {}, never>;
static ngAcceptInputType_matchSize: BooleanInput;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<CdkDragPreview<any>, "ng-template[cdkDragPreview]", never, { "data": "data"; "matchSize": "matchSize"; }, {}, never>;
static ɵfac: i0.ɵɵFactoryDef<CdkDragPreview<any>>;
}

Expand Down Expand Up @@ -308,7 +311,7 @@ export declare class DragRef<T = any> {
withDirection(direction: Direction): this;
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;
withPlaceholderTemplate(template: DragHelperTemplate | null): this;
withPreviewTemplate(template: DragHelperTemplate | null): this;
withPreviewTemplate(template: DragPreviewTemplate | null): this;
withRootElement(rootElement: ElementRef<HTMLElement> | HTMLElement): this;
}

Expand Down

0 comments on commit 74b3441

Please sign in to comment.