Skip to content

Commit 5571f7f

Browse files
committed
feat(drag-drop): add the ability to customize how the position is constrained
Adds the `constrainPosition` function that allows people to hook into the logic that constrains the position of the element as it's being dragged and to customize it. Fixes #15055.
1 parent 57aadc2 commit 5571f7f

File tree

4 files changed

+77
-8
lines changed

4 files changed

+77
-8
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {CdkDropList} from './drop-list';
2929
import {CdkDragHandle} from './drag-handle';
3030
import {CdkDropListGroup} from './drop-list-group';
3131
import {extendStyles} from '../drag-styling';
32-
import {DragRefConfig} from '../drag-ref';
32+
import {DragRefConfig, Point} from '../drag-ref';
3333

3434
const ITEM_HEIGHT = 25;
3535
const ITEM_WIDTH = 75;
@@ -696,6 +696,24 @@ describe('CdkDrag', () => {
696696
expect(dragElement.style.transform).toBe('translate3d(100px, 100px, 0px)');
697697
}));
698698

699+
it('should allow for the position constrain logic to be customized', fakeAsync(() => {
700+
const fixture = createComponent(StandaloneDraggable);
701+
const spy = jasmine.createSpy('constrain position spy').and.returnValue({
702+
x: 50,
703+
y: 50
704+
} as Point);
705+
706+
fixture.componentInstance.constrainPosition = spy;
707+
fixture.detectChanges();
708+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
709+
710+
expect(dragElement.style.transform).toBeFalsy();
711+
dragElementViaMouse(fixture, dragElement, 300, 300);
712+
713+
expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({x: 300, y: 300}));
714+
expect(dragElement.style.transform).toBe('translate3d(50px, 50px, 0px)');
715+
}));
716+
699717
it('should throw if attached to an ng-container', fakeAsync(() => {
700718
expect(() => {
701719
createComponent(DraggableOnNgContainer).detectChanges();
@@ -1990,6 +2008,33 @@ describe('CdkDrag', () => {
19902008
expect(Math.floor(previewRect.right)).toBe(Math.floor(listRect.right));
19912009
}));
19922010

2011+
it('should be able to constrain the preview position with a custom function', fakeAsync(() => {
2012+
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
2013+
const spy = jasmine.createSpy('constrain position spy').and.returnValue({
2014+
x: 50,
2015+
y: 50
2016+
} as Point);
2017+
2018+
fixture.componentInstance.constrainPosition = spy;
2019+
fixture.detectChanges();
2020+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
2021+
2022+
startDraggingViaMouse(fixture, item);
2023+
2024+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
2025+
2026+
startDraggingViaMouse(fixture, item, 200, 200);
2027+
flush();
2028+
dispatchMouseEvent(document, 'mousemove', 200, 200);
2029+
fixture.detectChanges();
2030+
2031+
const previewRect = preview.getBoundingClientRect();
2032+
2033+
expect(spy).toHaveBeenCalledWith(jasmine.objectContaining({x: 200, y: 200}));
2034+
expect(Math.floor(previewRect.top)).toBe(50);
2035+
expect(Math.floor(previewRect.left)).toBe(50);
2036+
}));
2037+
19932038
it('should revert the element back to its parent after dragging with a custom ' +
19942039
'preview has stopped', fakeAsync(() => {
19952040
const fixture = createComponent(DraggableInDropZoneWithCustomPreview);
@@ -2832,6 +2877,7 @@ describe('CdkDrag', () => {
28322877
<div
28332878
cdkDrag
28342879
[cdkDragBoundary]="boundarySelector"
2880+
[cdkDragConstrainPosition]="constrainPosition"
28352881
(cdkDragStarted)="startedSpy($event)"
28362882
(cdkDragReleased)="releasedSpy($event)"
28372883
(cdkDragEnded)="endedSpy($event)"
@@ -2847,6 +2893,7 @@ class StandaloneDraggable {
28472893
endedSpy = jasmine.createSpy('ended spy');
28482894
releasedSpy = jasmine.createSpy('released spy');
28492895
boundarySelector: string;
2896+
constrainPosition: (point: Point) => Point;
28502897
}
28512898

28522899
@Component({
@@ -3045,6 +3092,7 @@ class DraggableInHorizontalDropZone {
30453092
<div
30463093
*ngFor="let item of items"
30473094
cdkDrag
3095+
[cdkDragConstrainPosition]="constrainPosition"
30483096
[cdkDragBoundary]="boundarySelector"
30493097
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">
30503098
{{item}}
@@ -3065,6 +3113,7 @@ class DraggableInDropZoneWithCustomPreview {
30653113
items = ['Zero', 'One', 'Two', 'Three'];
30663114
boundarySelector: string;
30673115
renderCustomPreview = true;
3116+
constrainPosition: (point: Point) => Point;
30683117
}
30693118

30703119

src/cdk/drag-drop/directives/drag.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import {CdkDragPlaceholder} from './drag-placeholder';
4848
import {CdkDragPreview} from './drag-preview';
4949
import {CDK_DROP_LIST} from '../drop-list-container';
5050
import {CDK_DRAG_PARENT} from '../drag-parent';
51-
import {DragRef, DragRefConfig} from '../drag-ref';
51+
import {DragRef, DragRefConfig, Point} from '../drag-ref';
5252
import {DropListRef} from '../drop-list-ref';
5353
import {CdkDropListInternal as CdkDropList} from './drop-list';
5454
import {DragDrop} from '../drag-drop';
@@ -121,6 +121,14 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
121121
}
122122
private _disabled = false;
123123

124+
/**
125+
* Function that can be used to customize the logic of how the position of the drag item
126+
* is limited while it's being dragged. Gets called with a point containing the current position
127+
* of the user's pointer on the page and should return a point describing where the item should
128+
* be rendered.
129+
*/
130+
@Input('cdkDragConstrainPosition') constrainPosition?: (point: Point) => Point;
131+
124132
/** Emits when the user starts dragging the item. */
125133
@Output('cdkDragStarted') started: EventEmitter<CdkDragStart> = new EventEmitter<CdkDragStart>();
126134

@@ -303,6 +311,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
303311

304312
ref.disabled = this.disabled;
305313
ref.lockAxis = this.lockAxis;
314+
ref.constrainPosition = this.constrainPosition;
306315
ref
307316
.withBoundaryElement(this._getBoundaryElement())
308317
.withPlaceholderTemplate(placeholder)

src/cdk/drag-drop/drag-ref.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,14 @@ export class DragRef<T = any> {
264264
/** Arbitrary data that can be attached to the drag item. */
265265
data: T;
266266

267+
/**
268+
* Function that can be used to customize the logic of how the position of the drag item
269+
* is limited while it's being dragged. Gets called with a point containing the current position
270+
* of the user's pointer on the page and should return a point describing where the item should
271+
* be rendered.
272+
*/
273+
constrainPosition?: (point: Point) => Point;
274+
267275
constructor(
268276
element: ElementRef<HTMLElement> | HTMLElement,
269277
private _config: DragRefConfig,
@@ -898,12 +906,13 @@ export class DragRef<T = any> {
898906
/** Gets the pointer position on the page, accounting for any position constraints. */
899907
private _getConstrainedPointerPosition(event: MouseEvent | TouchEvent): Point {
900908
const point = this._getPointerPositionOnPage(event);
909+
const constrainedPoint = this.constrainPosition ? this.constrainPosition(point) : point;
901910
const dropContainerLock = this._dropContainer ? this._dropContainer.lockAxis : null;
902911

903912
if (this.lockAxis === 'x' || dropContainerLock === 'x') {
904-
point.y = this._pickupPositionOnPage.y;
913+
constrainedPoint.y = this._pickupPositionOnPage.y;
905914
} else if (this.lockAxis === 'y' || dropContainerLock === 'y') {
906-
point.x = this._pickupPositionOnPage.x;
915+
constrainedPoint.x = this._pickupPositionOnPage.x;
907916
}
908917

909918
if (this._boundaryRect) {
@@ -915,11 +924,11 @@ export class DragRef<T = any> {
915924
const minX = boundaryRect.left + pickupX;
916925
const maxX = boundaryRect.right - (previewRect.width - pickupX);
917926

918-
point.x = clamp(point.x, minX, maxX);
919-
point.y = clamp(point.y, minY, maxY);
927+
constrainedPoint.x = clamp(constrainedPoint.x, minX, maxX);
928+
constrainedPoint.y = clamp(constrainedPoint.y, minY, maxY);
920929
}
921930

922-
return point;
931+
return constrainedPoint;
923932
}
924933

925934

@@ -973,7 +982,7 @@ export class DragRef<T = any> {
973982
}
974983

975984
/** Point on the page or within an element. */
976-
interface Point {
985+
export interface Point {
977986
x: number;
978987
y: number;
979988
}

tools/public_api_guard/cdk/drag-drop.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
1212
_placeholderTemplate: CdkDragPlaceholder;
1313
_previewTemplate: CdkDragPreview;
1414
boundaryElementSelector: string;
15+
constrainPosition?: (point: Point) => Point;
1516
data: T;
1617
disabled: boolean;
1718
dropContainer: CdkDropList;
@@ -202,6 +203,7 @@ export declare class DragDropRegistry<I, C extends {
202203

203204
export declare class DragRef<T = any> {
204205
beforeStarted: Subject<void>;
206+
constrainPosition?: (point: Point) => Point;
205207
data: T;
206208
disabled: boolean;
207209
dropped: Subject<{

0 commit comments

Comments
 (0)