diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 6beb14b7f46..c59c0871abe 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -30,7 +30,7 @@ import {CdkDropList} from './drop-list'; import {CdkDragHandle} from './drag-handle'; import {CdkDropListGroup} from './drop-list-group'; import {extendStyles} from '../drag-styling'; -import {DragRefConfig} from '../drag-ref'; +import {DragRefConfig, Point} from '../drag-ref'; const ITEM_HEIGHT = 25; const ITEM_WIDTH = 75; @@ -697,6 +697,24 @@ describe('CdkDrag', () => { expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)'); })); + it('should allow for the position constrain logic to be customized', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + const spy = jasmine.createSpy('constrain position spy').and.returnValue({ + x: 50, + y: 50 + } as Point); + + fixture.componentInstance.constrainPosition = spy; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + dragElementViaMouse(fixture, dragElement, 300, 300); + + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({x: 300, y: 300})); + expect(dragElement.style.transform).toBe('translate3d(50px, 50px, 0px)'); + })); + it('should throw if attached to an ng-container', fakeAsync(() => { expect(() => { createComponent(DraggableOnNgContainer).detectChanges(); @@ -2105,6 +2123,33 @@ describe('CdkDrag', () => { expect(Math.floor(previewRect.right)).toBe(Math.floor(listRect.right)); })); + it('should be able to constrain the preview position with a custom function', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + const spy = jasmine.createSpy('constrain position spy').and.returnValue({ + x: 50, + y: 50 + } as Point); + + fixture.componentInstance.constrainPosition = spy; + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + + startDraggingViaMouse(fixture, item); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + + startDraggingViaMouse(fixture, item, 200, 200); + flush(); + dispatchMouseEvent(document, 'mousemove', 200, 200); + fixture.detectChanges(); + + const previewRect = preview.getBoundingClientRect(); + + expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({x: 200, y: 200})); + expect(Math.floor(previewRect.top)).toBe(50); + expect(Math.floor(previewRect.left)).toBe(50); + })); + it('should revert the element back to its parent after dragging with a custom ' + 'preview has stopped', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPreview); @@ -3049,6 +3094,7 @@ describe('CdkDrag', () => { cdkDrag [cdkDragBoundary]="boundarySelector" [cdkDragStartDelay]="dragStartDelay" + [cdkDragConstrainPosition]="constrainPosition" (cdkDragStarted)="startedSpy($event)" (cdkDragReleased)="releasedSpy($event)" (cdkDragEnded)="endedSpy($event)" @@ -3065,6 +3111,7 @@ class StandaloneDraggable { releasedSpy = jasmine.createSpy('released spy'); boundarySelector: string; dragStartDelay: number; + constrainPosition: (point: Point) => Point; } @Component({ @@ -3263,6 +3310,7 @@ class DraggableInHorizontalDropZone {
{{item}} @@ -3283,6 +3331,7 @@ class DraggableInDropZoneWithCustomPreview { items = ['Zero', 'One', 'Two', 'Three']; boundarySelector: string; renderCustomPreview = true; + constrainPosition: (point: Point) => Point; } diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 7275b00d227..51b0402bd01 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -48,7 +48,7 @@ import {CdkDragPlaceholder} from './drag-placeholder'; import {CdkDragPreview} from './drag-preview'; import {CDK_DROP_LIST} from '../drop-list-container'; import {CDK_DRAG_PARENT} from '../drag-parent'; -import {DragRef, DragRefConfig} from '../drag-ref'; +import {DragRef, DragRefConfig, Point} from '../drag-ref'; import {DropListRef} from '../drop-list-ref'; import {CdkDropListInternal as CdkDropList} from './drop-list'; import {DragDrop} from '../drag-drop'; @@ -127,6 +127,14 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { } private _disabled = false; + /** + * Function that can be used to customize the logic of how the position of the drag item + * is limited while it's being dragged. Gets called with a point containing the current position + * of the user's pointer on the page and should return a point describing where the item should + * be rendered. + */ + @Input('cdkDragConstrainPosition') constrainPosition?: (point: Point) => Point; + /** Emits when the user starts dragging the item. */ @Output('cdkDragStarted') started: EventEmitter = new EventEmitter(); @@ -310,6 +318,7 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { ref.disabled = this.disabled; ref.lockAxis = this.lockAxis; ref.dragStartDelay = this.dragStartDelay; + ref.constrainPosition = this.constrainPosition; ref .withBoundaryElement(this._getBoundaryElement()) .withPlaceholderTemplate(placeholder) diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index c67b3bfa9f1..c29ac40a542 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -273,6 +273,14 @@ export class DragRef { /** Arbitrary data that can be attached to the drag item. */ data: T; + /** + * Function that can be used to customize the logic of how the position of the drag item + * is limited while it's being dragged. Gets called with a point containing the current position + * of the user's pointer on the page and should return a point describing where the item should + * be rendered. + */ + constrainPosition?: (point: Point) => Point; + constructor( element: ElementRef | HTMLElement, private _config: DragRefConfig, @@ -909,12 +917,13 @@ export class DragRef { /** Gets the pointer position on the page, accounting for any position constraints. */ private _getConstrainedPointerPosition(event: MouseEvent | TouchEvent): Point { const point = this._getPointerPositionOnPage(event); + const constrainedPoint = this.constrainPosition ? this.constrainPosition(point) : point; const dropContainerLock = this._dropContainer ? this._dropContainer.lockAxis : null; if (this.lockAxis === 'x' || dropContainerLock === 'x') { - point.y = this._pickupPositionOnPage.y; + constrainedPoint.y = this._pickupPositionOnPage.y; } else if (this.lockAxis === 'y' || dropContainerLock === 'y') { - point.x = this._pickupPositionOnPage.x; + constrainedPoint.x = this._pickupPositionOnPage.x; } if (this._boundaryRect) { @@ -926,11 +935,11 @@ export class DragRef { const minX = boundaryRect.left + pickupX; const maxX = boundaryRect.right - (previewRect.width - pickupX); - point.x = clamp(point.x, minX, maxX); - point.y = clamp(point.y, minY, maxY); + constrainedPoint.x = clamp(constrainedPoint.x, minX, maxX); + constrainedPoint.y = clamp(constrainedPoint.y, minY, maxY); } - return point; + return constrainedPoint; } @@ -984,7 +993,7 @@ export class DragRef { } /** Point on the page or within an element. */ -interface Point { +export interface Point { x: number; y: number; } diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index b52fa6bebc7..ab58a61661f 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -12,6 +12,7 @@ export declare class CdkDrag implements AfterViewInit, OnChanges, OnDes _placeholderTemplate: CdkDragPlaceholder; _previewTemplate: CdkDragPreview; boundaryElementSelector: string; + constrainPosition?: (point: Point) => Point; data: T; disabled: boolean; dragStartDelay: number; @@ -204,6 +205,7 @@ export declare class DragDropRegistry { beforeStarted: Subject; + constrainPosition?: (point: Point) => Point; data: T; disabled: boolean; dragStartDelay: number;