Skip to content

Commit d254ea5

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 88601fa commit d254ea5

File tree

4 files changed

+118
-12
lines changed

4 files changed

+118
-12
lines changed

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

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

682+
it('should be able to get the current position', fakeAsync(() => {
683+
const fixture = createComponent(StandaloneDraggable);
684+
fixture.detectChanges();
685+
686+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
687+
const dragInstance = fixture.componentInstance.dragInstance;
688+
689+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0});
690+
691+
dragElementViaMouse(fixture, dragElement, 50, 100);
692+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
693+
694+
dragElementViaMouse(fixture, dragElement, 100, 200);
695+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300});
696+
}));
697+
698+
it('should be able to set the current position', fakeAsync(() => {
699+
const fixture = createComponent(StandaloneDraggable);
700+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
701+
fixture.detectChanges();
702+
703+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
704+
const dragInstance = fixture.componentInstance.dragInstance;
705+
706+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
707+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
708+
}));
709+
710+
it('should be able to continue dragging after the current position was set', fakeAsync(() => {
711+
const fixture = createComponent(StandaloneDraggable);
712+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
713+
fixture.detectChanges();
714+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
715+
716+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
717+
718+
dragElementViaMouse(fixture, dragElement, 100, 200);
719+
720+
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
721+
}));
722+
682723
});
683724

684725
describe('draggable with a handle', () => {
@@ -2803,6 +2844,7 @@ describe('CdkDrag', () => {
28032844
<div
28042845
cdkDrag
28052846
[cdkDragBoundary]="boundarySelector"
2847+
[cdkDragFreeDragPosition]="freeDragPosition"
28062848
(cdkDragStarted)="startedSpy($event)"
28072849
(cdkDragReleased)="releasedSpy($event)"
28082850
(cdkDragEnded)="endedSpy($event)"
@@ -2818,6 +2860,7 @@ class StandaloneDraggable {
28182860
endedSpy = jasmine.createSpy('ended spy');
28192861
releasedSpy = jasmine.createSpy('released spy');
28202862
boundarySelector: string;
2863+
freeDragPosition?: {x: number, y: number};
28212864
}
28222865

28232866
@Component({

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

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

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

208+
/**
209+
* Gets the pixel coordinates of the draggable outside of a drop container.
210+
*/
211+
getFreeDragPosition(): {readonly x: number, readonly y: number} {
212+
return this._dragRef.getFreeDragPosition();
213+
}
214+
202215
ngAfterViewInit() {
203216
// We need to wait for the zone to stabilize, in order for the reference
204217
// element to be in the proper place in the DOM. This is mostly relevant
@@ -230,17 +243,27 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
230243
const handle = handleInstance.element.nativeElement;
231244
handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
232245
});
246+
247+
if (this.freeDragPosition) {
248+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
249+
}
233250
});
234251
}
235252

236253
ngOnChanges(changes: SimpleChanges) {
237254
const rootSelectorChange = changes.rootElementSelector;
255+
const positionChange = changes.positionChange;
238256

239257
// We don't have to react to the first change since it's being
240258
// handled in `ngAfterViewInit` where it needs to be deferred.
241259
if (rootSelectorChange && !rootSelectorChange.firstChange) {
242260
this._updateRootElement();
243261
}
262+
263+
// Skip the first change since it's being handled in `ngAfterViewInit`.
264+
if (positionChange && !positionChange.firstChange && this.freeDragPosition) {
265+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
266+
}
244267
}
245268

246269
ngOnDestroy() {

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

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,29 @@ export class DragRef<T = any> {
398398
this._disabledHandles.delete(handle);
399399
}
400400

401+
/**
402+
* Gets the current position in pixels the draggable outside of a drop container.
403+
*/
404+
getFreeDragPosition(): Readonly<Point> {
405+
return {x: this._passiveTransform.x, y: this._passiveTransform.y};
406+
}
407+
408+
/**
409+
* Sets the current position in pixels the draggable outside of a drop container.
410+
* @param value New position to be set.
411+
*/
412+
setFreeDragPosition(value: Point): this {
413+
this._activeTransform = {x: 0, y: 0};
414+
this._passiveTransform.x = value.x;
415+
this._passiveTransform.y = value.y;
416+
417+
if (!this.dropContainer) {
418+
this._applyRootElementTransform(value.x, value.y);
419+
}
420+
421+
return this;
422+
}
423+
401424
/** Unsubscribes from the global subscriptions. */
402425
private _removeSubscriptions() {
403426
this._pointerMoveSubscription.unsubscribe();
@@ -490,13 +513,8 @@ export class DragRef<T = any> {
490513
constrainedPointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x;
491514
activeTransform.y =
492515
constrainedPointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y;
493-
const transform = getTransform(activeTransform.x, activeTransform.y);
494516

495-
// Preserve the previous `transform` value, if there was one. Note that we apply our own
496-
// transform before the user's, because things like rotation can affect which direction
497-
// the element will be translated towards.
498-
this._rootElement.style.transform = this._initialTransform ?
499-
transform + ' ' + this._initialTransform : transform;
517+
this._applyRootElementTransform(activeTransform.x, activeTransform.y);
500518

501519
// Apply transform as attribute if dragging and svg element to work for IE
502520
if (typeof SVGElement !== 'undefined' && this._rootElement instanceof SVGElement) {
@@ -623,12 +641,6 @@ export class DragRef<T = any> {
623641
return;
624642
}
625643

626-
// Cache the previous transform amount only after the first drag sequence, because
627-
// we don't want our own transforms to stack on top of each other.
628-
if (this._initialTransform == null) {
629-
this._initialTransform = this._rootElement.style.transform || '';
630-
}
631-
632644
// If we've got handles, we need to disable the tap highlight on the entire root element,
633645
// otherwise iOS will still add it, even though all the drag interactions on the handle
634646
// are disabled.
@@ -949,6 +961,26 @@ export class DragRef<T = any> {
949961
element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
950962
}
951963

964+
/**
965+
* Applies a `transform` to the root element, taking into account any existing transforms on it.
966+
* @param x New transform value along the X axis.
967+
* @param y New transform value along the Y axis.
968+
*/
969+
private _applyRootElementTransform(x: number, y: number) {
970+
const transform = getTransform(x, y);
971+
972+
// Cache the previous transform amount only after the first drag sequence, because
973+
// we don't want our own transforms to stack on top of each other.
974+
if (this._initialTransform == null) {
975+
this._initialTransform = this._rootElement.style.transform || '';
976+
}
977+
978+
// Preserve the previous `transform` value, if there was one. Note that we apply our own
979+
// transform before the user's, because things like rotation can affect which direction
980+
// the element will be translated towards.
981+
this._rootElement.style.transform = this._initialTransform ?
982+
transform + ' ' + this._initialTransform : transform;
983+
}
952984
}
953985

954986
/** 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)