Skip to content

Commit d0bc928

Browse files
committed
feat(cdk/drag-drop): support immediate drag with preview snapped to cursor on mousedown
1 parent 0998216 commit d0bc928

File tree

5 files changed

+108
-3
lines changed

5 files changed

+108
-3
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export class CdkDragPreview<T = any> implements OnDestroy {
4343
/** Whether the preview should preserve the same size as the item that is being dragged. */
4444
@Input({transform: booleanAttribute}) matchSize: boolean = false;
4545

46+
/** Whether the preview should snap the starting position centered under the cursor. */
47+
@Input({transform: booleanAttribute}) snapToCursor: boolean = false;
48+
4649
constructor(...args: unknown[]);
4750

4851
constructor() {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
437437
template: this._previewTemplate.templateRef,
438438
context: this._previewTemplate.data,
439439
matchSize: this._previewTemplate.matchSize,
440+
snapToCursor: this._previewTemplate.snapToCursor,
440441
viewContainer: this._viewContainerRef,
441442
}
442443
: null;

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,92 @@ export function defineCommonDropListTests(config: {
823823
expect(anchor!.parentNode).toBeFalsy();
824824
}));
825825

826+
it('should display preview on mousedown for config.dragStartThreshold = 0', fakeAsync(() => {
827+
const fixture = createComponent(DraggableInDropZoneWithCustomPreview, {
828+
providers: [
829+
{
830+
provide: CDK_DRAG_CONFIG,
831+
useValue: {
832+
dragStartThreshold: 0,
833+
},
834+
},
835+
],
836+
});
837+
fixture.detectChanges();
838+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
839+
const itemRect = item.getBoundingClientRect();
840+
flush();
841+
dispatchMouseEvent(item, 'mousedown', undefined, undefined);
842+
fixture.detectChanges();
843+
844+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
845+
expect(preview).not.toBeNull();
846+
const previewRect = preview.getBoundingClientRect();
847+
expect(previewRect).not.toBeNull();
848+
}));
849+
850+
it('should snap preview to cursor when snapToCursor is set on preview template', fakeAsync(() => {
851+
@Component({
852+
styles: `
853+
.list {
854+
display: flex;
855+
width: 100px;
856+
flex-direction: row;
857+
}
858+
859+
.item {
860+
display: flex;
861+
flex-grow: 1;
862+
flex-basis: 0;
863+
min-height: 50px;
864+
}
865+
`,
866+
template: `
867+
<div class="list" cdkDropList>
868+
@for (item of items; track item) {
869+
<div class="item" cdkDrag>
870+
{{item}}
871+
<ng-template cdkDragPreview snapToCursor>
872+
<div class="item">{{item}}</div>
873+
</ng-template>
874+
</div>
875+
}
876+
</div>
877+
`,
878+
imports: [CdkDropList, CdkDrag, CdkDragPreview],
879+
})
880+
class DraggableInHorizontalFlexDropZoneWithSnapToCursorPreview {
881+
@ViewChild(CdkDropList) dropInstance: CdkDropList;
882+
@ViewChildren(CdkDrag) dragItems: QueryList<CdkDrag>;
883+
items = ['Zero', 'One', 'Two'];
884+
}
885+
886+
const fixture = createComponent(DraggableInHorizontalFlexDropZoneWithSnapToCursorPreview);
887+
fixture.detectChanges();
888+
const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement;
889+
const listRect =
890+
fixture.componentInstance.dropInstance.element.nativeElement.getBoundingClientRect();
891+
892+
startDraggingViaMouse(fixture, item);
893+
894+
const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement;
895+
896+
startDraggingViaMouse(fixture, item, listRect.right + 50, listRect.bottom + 50);
897+
flush();
898+
dispatchMouseEvent(document, 'mousemove', listRect.right + 50, listRect.bottom + 50);
899+
fixture.detectChanges();
900+
901+
const previewRect = preview.getBoundingClientRect();
902+
903+
// centered on the cursor
904+
expect(Math.floor(previewRect.bottom - previewRect.height / 2)).toBe(
905+
Math.floor(listRect.bottom + 50),
906+
);
907+
expect(Math.floor(previewRect.right - previewRect.width / 2)).toBe(
908+
Math.floor(listRect.right + 50),
909+
);
910+
}));
911+
826912
it('should create a preview element while the item is dragged', fakeAsync(() => {
827913
const fixture = createComponent(DraggableInDropZone);
828914
fixture.detectChanges();

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ export class DragRef<T = any> {
677677
};
678678

679679
/** Handler that is invoked when the user moves their pointer after they've initiated a drag. */
680-
private _pointerMove = (event: MouseEvent | TouchEvent) => {
680+
private _pointerMove = (event: MouseEvent | TouchEvent, mouseDown: boolean = false) => {
681681
const pointerPosition = this._getPointerPositionOnPage(event);
682682

683683
if (!this._hasStartedDragging()) {
@@ -711,8 +711,9 @@ export class DragRef<T = any> {
711711
this._ngZone.run(() => this._startDragSequence(event));
712712
}
713713
}
714-
715-
return;
714+
if (!mouseDown) {
715+
return;
716+
}
716717
}
717718

718719
// We prevent the default action down here so that we know that dragging has started. This is
@@ -977,6 +978,16 @@ export class DragRef<T = any> {
977978
this._pointerPositionAtLastDirectionChange = {x: pointerPosition.x, y: pointerPosition.y};
978979
this._dragStartTime = Date.now();
979980
this._dragDropRegistry.startDragging(this, event);
981+
982+
// when pixel threshold = 0 and dragStartDelay = 0 and a preview container/position exists we immediately drag
983+
if (
984+
event.type == 'mousedown' &&
985+
previewTemplate &&
986+
this._config.dragStartThreshold === 0 &&
987+
this._getDragStartDelay(event) === 0
988+
) {
989+
this._pointerMove(event, true);
990+
}
980991
}
981992

982993
/** Cleans up the DOM artifacts that were added to facilitate the element being dragged. */
@@ -1086,6 +1097,9 @@ export class DragRef<T = any> {
10861097

10871098
if (this.constrainPosition) {
10881099
this._applyPreviewTransform(x, y);
1100+
} else if (this._previewTemplate?.snapToCursor) {
1101+
const previewRect = this._getPreviewRect();
1102+
this._applyPreviewTransform(rawX - previewRect.width / 2, rawY - previewRect.height / 2);
10891103
} else {
10901104
this._applyPreviewTransform(
10911105
x - this._pickupPositionInElement.x,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {getTransformTransitionDurationInMs} from './dom/transition-duration';
2121
/** Template that can be used to create a drag preview element. */
2222
export interface DragPreviewTemplate<T = any> {
2323
matchSize?: boolean;
24+
snapToCursor?: boolean;
2425
template: TemplateRef<T> | null;
2526
viewContainer: ViewContainerRef;
2627
context: T;

0 commit comments

Comments
 (0)