Skip to content

Commit 82a0cf8

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 57aadc2 commit 82a0cf8

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
@@ -703,6 +703,47 @@ describe('CdkDrag', () => {
703703
}).toThrowError(/^cdkDrag must be attached to an element node/);
704704
}));
705705

706+
it('should be able to get the current position', fakeAsync(() => {
707+
const fixture = createComponent(StandaloneDraggable);
708+
fixture.detectChanges();
709+
710+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
711+
const dragInstance = fixture.componentInstance.dragInstance;
712+
713+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0});
714+
715+
dragElementViaMouse(fixture, dragElement, 50, 100);
716+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
717+
718+
dragElementViaMouse(fixture, dragElement, 100, 200);
719+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300});
720+
}));
721+
722+
it('should be able to set the current position', fakeAsync(() => {
723+
const fixture = createComponent(StandaloneDraggable);
724+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
725+
fixture.detectChanges();
726+
727+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
728+
const dragInstance = fixture.componentInstance.dragInstance;
729+
730+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
731+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
732+
}));
733+
734+
it('should be able to continue dragging after the current position was set', fakeAsync(() => {
735+
const fixture = createComponent(StandaloneDraggable);
736+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
737+
fixture.detectChanges();
738+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
739+
740+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
741+
742+
dragElementViaMouse(fixture, dragElement, 100, 200);
743+
744+
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
745+
}));
746+
706747
});
707748

708749
describe('draggable with a handle', () => {
@@ -2832,6 +2873,7 @@ describe('CdkDrag', () => {
28322873
<div
28332874
cdkDrag
28342875
[cdkDragBoundary]="boundarySelector"
2876+
[cdkDragFreeDragPosition]="freeDragPosition"
28352877
(cdkDragStarted)="startedSpy($event)"
28362878
(cdkDragReleased)="releasedSpy($event)"
28372879
(cdkDragEnded)="endedSpy($event)"
@@ -2847,6 +2889,7 @@ class StandaloneDraggable {
28472889
endedSpy = jasmine.createSpy('ended spy');
28482890
releasedSpy = jasmine.createSpy('released spy');
28492891
boundarySelector: string;
2892+
freeDragPosition?: {x: number, y: number};
28502893
}
28512894

28522895
@Component({

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

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

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

224+
/**
225+
* Gets the pixel coordinates of the draggable outside of a drop container.
226+
*/
227+
getFreeDragPosition(): {readonly x: number, readonly y: number} {
228+
return this._dragRef.getFreeDragPosition();
229+
}
230+
218231
ngAfterViewInit() {
219232
// We need to wait for the zone to stabilize, in order for the reference
220233
// element to be in the proper place in the DOM. This is mostly relevant
@@ -246,17 +259,27 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
246259
const handle = handleInstance.element.nativeElement;
247260
handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
248261
});
262+
263+
if (this.freeDragPosition) {
264+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
265+
}
249266
});
250267
}
251268

252269
ngOnChanges(changes: SimpleChanges) {
253270
const rootSelectorChange = changes.rootElementSelector;
271+
const positionChange = changes.positionChange;
254272

255273
// We don't have to react to the first change since it's being
256274
// handled in `ngAfterViewInit` where it needs to be deferred.
257275
if (rootSelectorChange && !rootSelectorChange.firstChange) {
258276
this._updateRootElement();
259277
}
278+
279+
// Skip the first change since it's being handled in `ngAfterViewInit`.
280+
if (positionChange && !positionChange.firstChange && this.freeDragPosition) {
281+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
282+
}
260283
}
261284

262285
ngOnDestroy() {

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

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,29 @@ export class DragRef<T = any> {
417417
this._dropContainer = container;
418418
}
419419

420+
/**
421+
* Gets the current position in pixels the draggable outside of a drop container.
422+
*/
423+
getFreeDragPosition(): Readonly<Point> {
424+
return {x: this._passiveTransform.x, y: this._passiveTransform.y};
425+
}
426+
427+
/**
428+
* Sets the current position in pixels the draggable outside of a drop container.
429+
* @param value New position to be set.
430+
*/
431+
setFreeDragPosition(value: Point): this {
432+
this._activeTransform = {x: 0, y: 0};
433+
this._passiveTransform.x = value.x;
434+
this._passiveTransform.y = value.y;
435+
436+
if (!this._dropContainer) {
437+
this._applyRootElementTransform(value.x, value.y);
438+
}
439+
440+
return this;
441+
}
442+
420443
/** Unsubscribes from the global subscriptions. */
421444
private _removeSubscriptions() {
422445
this._pointerMoveSubscription.unsubscribe();
@@ -509,13 +532,8 @@ export class DragRef<T = any> {
509532
constrainedPointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x;
510533
activeTransform.y =
511534
constrainedPointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y;
512-
const transform = getTransform(activeTransform.x, activeTransform.y);
513535

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

520538
// Apply transform as attribute if dragging and svg element to work for IE
521539
if (typeof SVGElement !== 'undefined' && this._rootElement instanceof SVGElement) {
@@ -642,12 +660,6 @@ export class DragRef<T = any> {
642660
return;
643661
}
644662

645-
// Cache the previous transform amount only after the first drag sequence, because
646-
// we don't want our own transforms to stack on top of each other.
647-
if (this._initialTransform == null) {
648-
this._initialTransform = this._rootElement.style.transform || '';
649-
}
650-
651663
// If we've got handles, we need to disable the tap highlight on the entire root element,
652664
// otherwise iOS will still add it, even though all the drag interactions on the handle
653665
// are disabled.
@@ -970,6 +982,26 @@ export class DragRef<T = any> {
970982
element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
971983
}
972984

985+
/**
986+
* Applies a `transform` to the root element, taking into account any existing transforms on it.
987+
* @param x New transform value along the X axis.
988+
* @param y New transform value along the Y axis.
989+
*/
990+
private _applyRootElementTransform(x: number, y: number) {
991+
const transform = getTransform(x, y);
992+
993+
// Cache the previous transform amount only after the first drag sequence, because
994+
// we don't want our own transforms to stack on top of each other.
995+
if (this._initialTransform == null) {
996+
this._initialTransform = this._rootElement.style.transform || '';
997+
}
998+
999+
// Preserve the previous `transform` value, if there was one. Note that we apply our own
1000+
// transform before the user's, because things like rotation can affect which direction
1001+
// the element will be translated towards.
1002+
this._rootElement.style.transform = this._initialTransform ?
1003+
transform + ' ' + this._initialTransform : transform;
1004+
}
9731005
}
9741006

9751007
/** 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
@@ -20,6 +20,10 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
2020
ended: EventEmitter<CdkDragEnd>;
2121
entered: EventEmitter<CdkDragEnter<any>>;
2222
exited: EventEmitter<CdkDragExit<any>>;
23+
freeDragPosition: {
24+
x: number;
25+
y: number;
26+
};
2327
lockAxis: 'x' | 'y';
2428
moved: Observable<CdkDragMove<T>>;
2529
released: EventEmitter<CdkDragRelease>;
@@ -29,6 +33,10 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
2933
element: ElementRef<HTMLElement>,
3034
dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, viewportRuler: ViewportRuler, dragDropRegistry: DragDropRegistry<DragRef, DropListRef>, config: DragRefConfig, _dir: Directionality,
3135
dragDrop?: DragDrop, _changeDetectorRef?: ChangeDetectorRef | undefined);
36+
getFreeDragPosition(): {
37+
readonly x: number;
38+
readonly y: number;
39+
};
3240
getPlaceholderElement(): HTMLElement;
3341
getRootElement(): HTMLElement;
3442
ngAfterViewInit(): void;

0 commit comments

Comments
 (0)