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;