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

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 angular#18177.
  • Loading branch information
crisbeto committed Feb 1, 2020
1 parent 12edc0b commit 41d5bb7
Show file tree
Hide file tree
Showing 5 changed files with 87 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 @@ -2890,6 +2890,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 @@ -5009,10 +5040,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 @@ -5024,6 +5056,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 @@ -346,6 +346,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
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 @@ -189,7 +194,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 @@ -321,7 +326,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 @@ -758,10 +763,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 @@ -861,16 +868,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 @@ -1279,3 +1287,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 @@ -305,7 +308,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 41d5bb7

Please sign in to comment.