Skip to content

Commit 32610e9

Browse files
committed
feat(drag-drop): add API to get/set current position of a standalone draggable
Adds an API that allows the consumer to get the current position of a standalone draggable and to set it. This is useful for cases where the dragged position should be preserved when the user navigates away and then restored when they return. Fixes #14420. Fixes #14674.
1 parent d22f48c commit 32610e9

File tree

4 files changed

+116
-10
lines changed

4 files changed

+116
-10
lines changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,47 @@ describe('CdkDrag', () => {
645645
}).toThrowError(/^cdkDrag must be attached to an element node/);
646646
}));
647647

648+
it('should be able to get the current position', fakeAsync(() => {
649+
const fixture = createComponent(StandaloneDraggable);
650+
fixture.detectChanges();
651+
652+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
653+
const dragInstance = fixture.componentInstance.dragInstance;
654+
655+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0});
656+
657+
dragElementViaMouse(fixture, dragElement, 50, 100);
658+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
659+
660+
dragElementViaMouse(fixture, dragElement, 100, 200);
661+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300});
662+
}));
663+
664+
it('should be able to set the current position', fakeAsync(() => {
665+
const fixture = createComponent(StandaloneDraggable);
666+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
667+
fixture.detectChanges();
668+
669+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
670+
const dragInstance = fixture.componentInstance.dragInstance;
671+
672+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
673+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
674+
}));
675+
676+
it('should be able to continue dragging after the current position was set', fakeAsync(() => {
677+
const fixture = createComponent(StandaloneDraggable);
678+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
679+
fixture.detectChanges();
680+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
681+
682+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
683+
684+
dragElementViaMouse(fixture, dragElement, 100, 200);
685+
686+
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
687+
}));
688+
648689
});
649690

650691
describe('draggable with a handle', () => {
@@ -2688,6 +2729,7 @@ describe('CdkDrag', () => {
26882729
<div
26892730
cdkDrag
26902731
[cdkDragBoundary]="boundarySelector"
2732+
[cdkDragFreeDragPosition]="freeDragPosition"
26912733
(cdkDragStarted)="startedSpy($event)"
26922734
(cdkDragReleased)="releasedSpy($event)"
26932735
(cdkDragEnded)="endedSpy($event)"
@@ -2703,6 +2745,7 @@ class StandaloneDraggable {
27032745
endedSpy = jasmine.createSpy('ended spy');
27042746
releasedSpy = jasmine.createSpy('released spy');
27052747
boundarySelector: string;
2748+
freeDragPosition?: {x: number, y: number};
27062749
}
27072750

27082751
@Component({

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
108108
*/
109109
@Input('cdkDragBoundary') boundaryElementSelector: string;
110110

111+
/**
112+
* Sets the position of a `CdkDrag` that is outside of a drop container.
113+
* Can be used to restore the element's position for a returning user.
114+
*/
115+
@Input('cdkDragFreeDragPosition') freeDragPosition: {x: number, y: number};
116+
111117
/** Whether starting to drag this element is disabled. */
112118
@Input('cdkDragDisabled')
113119
get disabled(): boolean {
@@ -209,6 +215,13 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
209215
this._dragRef.reset();
210216
}
211217

218+
/**
219+
* Gets the current position in pixels the draggable outside of a drop container.
220+
*/
221+
getFreeDragPosition(): {readonly x: number, readonly y: number} {
222+
return this._dragRef.getFreeDragPosition();
223+
}
224+
212225
ngAfterViewInit() {
213226
// We need to wait for the zone to stabilize, in order for the reference
214227
// element to be in the proper place in the DOM. This is mostly relevant
@@ -223,17 +236,27 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
223236
.subscribe((handleList: QueryList<CdkDragHandle>) => {
224237
this._dragRef.withHandles(handleList.filter(handle => handle._parentDrag === this));
225238
});
239+
240+
if (this.freeDragPosition) {
241+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
242+
}
226243
});
227244
}
228245

229246
ngOnChanges(changes: SimpleChanges) {
230247
const rootSelectorChange = changes.rootElementSelector;
248+
const positionChange = changes.positionChange;
231249

232250
// We don't have to react to the first change since it's being
233251
// handled in `ngAfterViewInit` where it needs to be deferred.
234252
if (rootSelectorChange && !rootSelectorChange.firstChange) {
235253
this._updateRootElement();
236254
}
255+
256+
// Skip the first change since it's being handled in `ngAfterViewInit`.
257+
if (positionChange && !positionChange.firstChange && this.freeDragPosition) {
258+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
259+
}
237260
}
238261

239262
ngOnDestroy() {

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

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,29 @@ export class DragRef<T = any> {
386386
this._passiveTransform = {x: 0, y: 0};
387387
}
388388

389+
/**
390+
* Gets the current position in pixels the draggable outside of a drop container.
391+
*/
392+
getFreeDragPosition(): Readonly<Point> {
393+
return {x: this._passiveTransform.x, y: this._passiveTransform.y};
394+
}
395+
396+
/**
397+
* Sets the current position in pixels the draggable outside of a drop container.
398+
* @param value New position to be set.
399+
*/
400+
setFreeDragPosition(value: Point): this {
401+
this._activeTransform = {x: 0, y: 0};
402+
this._passiveTransform.x = value.x;
403+
this._passiveTransform.y = value.y;
404+
405+
if (!this.dropContainer) {
406+
this._applyRootElementTransform(value.x, value.y);
407+
}
408+
409+
return this;
410+
}
411+
389412
/** Unsubscribes from the global subscriptions. */
390413
private _removeSubscriptions() {
391414
this._pointerMoveSubscription.unsubscribe();
@@ -480,11 +503,8 @@ export class DragRef<T = any> {
480503
constrainedPointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x;
481504
activeTransform.y =
482505
constrainedPointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y;
483-
const transform = getTransform(activeTransform.x, activeTransform.y);
484506

485-
// Preserve the previous `transform` value, if there was one.
486-
this._rootElement.style.transform = this._initialTransform ?
487-
this._initialTransform + ' ' + transform : transform;
507+
this._applyRootElementTransform(activeTransform.x, activeTransform.y);
488508

489509
// Apply transform as attribute if dragging and svg element to work for IE
490510
if (typeof SVGElement !== 'undefined' && this._rootElement instanceof SVGElement) {
@@ -611,12 +631,6 @@ export class DragRef<T = any> {
611631
return;
612632
}
613633

614-
// Cache the previous transform amount only after the first drag sequence, because
615-
// we don't want our own transforms to stack on top of each other.
616-
if (this._initialTransform == null) {
617-
this._initialTransform = this._rootElement.style.transform || '';
618-
}
619-
620634
// If we've got handles, we need to disable the tap highlight on the entire root element,
621635
// otherwise iOS will still add it, even though all the drag interactions on the handle
622636
// are disabled.
@@ -934,6 +948,24 @@ export class DragRef<T = any> {
934948
element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
935949
}
936950

951+
/**
952+
* Applies a `transform` to the root element, taking into account any existing transforms on it.
953+
* @param x New transform value along the X axis.
954+
* @param y New transform value along the Y axis.
955+
*/
956+
private _applyRootElementTransform(x: number, y: number) {
957+
const transform = getTransform(x, y);
958+
959+
// Cache the previous transform amount only after the first drag sequence, because
960+
// we don't want our own transforms to stack on top of each other.
961+
if (this._initialTransform == null) {
962+
this._initialTransform = this._rootElement.style.transform || '';
963+
}
964+
965+
// Preserve the previous `transform` value, if there was one.
966+
this._rootElement.style.transform = this._initialTransform ?
967+
this._initialTransform + ' ' + transform : transform;
968+
}
937969
}
938970

939971
/** Point on the page or within an element. */

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,18 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
2424
moved: Observable<CdkDragMove<T>>;
2525
released: EventEmitter<CdkDragRelease>;
2626
rootElementSelector: string;
27+
freeDragPosition: {
28+
x: number;
29+
y: number;
30+
};
2731
started: EventEmitter<CdkDragStart>;
2832
constructor(
2933
element: ElementRef<HTMLElement>,
3034
dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>, _config: DragRefConfig, _dir: Directionality);
35+
getFreeDragPosition(): {
36+
readonly x: number;
37+
readonly y: number;
38+
};
3139
getPlaceholderElement(): HTMLElement;
3240
getRootElement(): HTMLElement;
3341
ngAfterViewInit(): void;

0 commit comments

Comments
 (0)