Skip to content

feat(drag-drop): add the ability to customize how the position is constrained #15137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion src/cdk/drag-drop/directives/drag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -3049,6 +3094,7 @@ describe('CdkDrag', () => {
cdkDrag
[cdkDragBoundary]="boundarySelector"
[cdkDragStartDelay]="dragStartDelay"
[cdkDragConstrainPosition]="constrainPosition"
(cdkDragStarted)="startedSpy($event)"
(cdkDragReleased)="releasedSpy($event)"
(cdkDragEnded)="endedSpy($event)"
Expand All @@ -3065,6 +3111,7 @@ class StandaloneDraggable {
releasedSpy = jasmine.createSpy('released spy');
boundarySelector: string;
dragStartDelay: number;
constrainPosition: (point: Point) => Point;
}

@Component({
Expand Down Expand Up @@ -3263,6 +3310,7 @@ class DraggableInHorizontalDropZone {
<div
*ngFor="let item of items"
cdkDrag
[cdkDragConstrainPosition]="constrainPosition"
[cdkDragBoundary]="boundarySelector"
style="width: 100%; height: ${ITEM_HEIGHT}px; background: red;">
{{item}}
Expand All @@ -3283,6 +3331,7 @@ class DraggableInDropZoneWithCustomPreview {
items = ['Zero', 'One', 'Two', 'Three'];
boundarySelector: string;
renderCustomPreview = true;
constrainPosition: (point: Point) => Point;
}


Expand Down
11 changes: 10 additions & 1 deletion src/cdk/drag-drop/directives/drag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -127,6 +127,14 @@ export class CdkDrag<T = any> 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<CdkDragStart> = new EventEmitter<CdkDragStart>();

Expand Down Expand Up @@ -310,6 +318,7 @@ export class CdkDrag<T = any> 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)
Expand Down
21 changes: 15 additions & 6 deletions src/cdk/drag-drop/drag-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,14 @@ export class DragRef<T = any> {
/** 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> | HTMLElement,
private _config: DragRefConfig,
Expand Down Expand Up @@ -909,12 +917,13 @@ export class DragRef<T = any> {
/** 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) {
Expand All @@ -926,11 +935,11 @@ export class DragRef<T = any> {
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;
}


Expand Down Expand Up @@ -984,7 +993,7 @@ export class DragRef<T = any> {
}

/** Point on the page or within an element. */
interface Point {
export interface Point {
x: number;
y: number;
}
Expand Down
2 changes: 2 additions & 0 deletions tools/public_api_guard/cdk/drag-drop.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
_placeholderTemplate: CdkDragPlaceholder;
_previewTemplate: CdkDragPreview;
boundaryElementSelector: string;
constrainPosition?: (point: Point) => Point;
data: T;
disabled: boolean;
dragStartDelay: number;
Expand Down Expand Up @@ -204,6 +205,7 @@ export declare class DragDropRegistry<I, C extends {

export declare class DragRef<T = any> {
beforeStarted: Subject<void>;
constrainPosition?: (point: Point) => Point;
data: T;
disabled: boolean;
dragStartDelay: number;
Expand Down