Skip to content

Commit 4faa61b

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 2a086ce commit 4faa61b

File tree

4 files changed

+120
-12
lines changed

4 files changed

+120
-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
@@ -734,6 +734,47 @@ describe('CdkDrag', () => {
734734
'Expected element to be dragged after all the time has passed.');
735735
}));
736736

737+
it('should be able to get the current position', fakeAsync(() => {
738+
const fixture = createComponent(StandaloneDraggable);
739+
fixture.detectChanges();
740+
741+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
742+
const dragInstance = fixture.componentInstance.dragInstance;
743+
744+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0});
745+
746+
dragElementViaMouse(fixture, dragElement, 50, 100);
747+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
748+
749+
dragElementViaMouse(fixture, dragElement, 100, 200);
750+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300});
751+
}));
752+
753+
it('should be able to set the current position', fakeAsync(() => {
754+
const fixture = createComponent(StandaloneDraggable);
755+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
756+
fixture.detectChanges();
757+
758+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
759+
const dragInstance = fixture.componentInstance.dragInstance;
760+
761+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
762+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
763+
}));
764+
765+
it('should be able to continue dragging after the current position was set', fakeAsync(() => {
766+
const fixture = createComponent(StandaloneDraggable);
767+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
768+
fixture.detectChanges();
769+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
770+
771+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
772+
773+
dragElementViaMouse(fixture, dragElement, 100, 200);
774+
775+
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
776+
}));
777+
737778
});
738779

739780
describe('draggable with a handle', () => {
@@ -3049,6 +3090,7 @@ describe('CdkDrag', () => {
30493090
cdkDrag
30503091
[cdkDragBoundary]="boundarySelector"
30513092
[cdkDragStartDelay]="dragStartDelay"
3093+
[cdkDragFreeDragPosition]="freeDragPosition"
30523094
(cdkDragStarted)="startedSpy($event)"
30533095
(cdkDragReleased)="releasedSpy($event)"
30543096
(cdkDragEnded)="endedSpy($event)"
@@ -3065,6 +3107,7 @@ class StandaloneDraggable {
30653107
releasedSpy = jasmine.createSpy('released spy');
30663108
boundarySelector: string;
30673109
dragStartDelay: number;
3110+
freeDragPosition?: {x: number, y: number};
30683111
}
30693112

30703113
@Component({

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
116116
*/
117117
@Input('cdkDragStartDelay') dragStartDelay: number = 0;
118118

119+
/**
120+
* Sets the position of a `CdkDrag` that is outside of a drop container.
121+
* Can be used to restore the element's position for a returning user.
122+
*/
123+
@Input('cdkDragFreeDragPosition') freeDragPosition: {x: number, y: number};
124+
119125
/** Whether starting to drag this element is disabled. */
120126
@Input('cdkDragDisabled')
121127
get disabled(): boolean {
@@ -221,6 +227,13 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
221227
this._dragRef.reset();
222228
}
223229

230+
/**
231+
* Gets the pixel coordinates of the draggable outside of a drop container.
232+
*/
233+
getFreeDragPosition(): {readonly x: number, readonly y: number} {
234+
return this._dragRef.getFreeDragPosition();
235+
}
236+
224237
ngAfterViewInit() {
225238
// We need to wait for the zone to stabilize, in order for the reference
226239
// element to be in the proper place in the DOM. This is mostly relevant
@@ -252,17 +265,27 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
252265
const handle = handleInstance.element.nativeElement;
253266
handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
254267
});
268+
269+
if (this.freeDragPosition) {
270+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
271+
}
255272
});
256273
}
257274

258275
ngOnChanges(changes: SimpleChanges) {
259276
const rootSelectorChange = changes['rootElementSelector'];
277+
const positionChange = changes.positionChange;
260278

261279
// We don't have to react to the first change since it's being
262280
// handled in `ngAfterViewInit` where it needs to be deferred.
263281
if (rootSelectorChange && !rootSelectorChange.firstChange) {
264282
this._updateRootElement();
265283
}
284+
285+
// Skip the first change since it's being handled in `ngAfterViewInit`.
286+
if (positionChange && !positionChange.firstChange && this.freeDragPosition) {
287+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
288+
}
266289
}
267290

268291
ngOnDestroy() {

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

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

429+
/**
430+
* Gets the current position in pixels the draggable outside of a drop container.
431+
*/
432+
getFreeDragPosition(): Readonly<Point> {
433+
return {x: this._passiveTransform.x, y: this._passiveTransform.y};
434+
}
435+
436+
/**
437+
* Sets the current position in pixels the draggable outside of a drop container.
438+
* @param value New position to be set.
439+
*/
440+
setFreeDragPosition(value: Point): this {
441+
this._activeTransform = {x: 0, y: 0};
442+
this._passiveTransform.x = value.x;
443+
this._passiveTransform.y = value.y;
444+
445+
if (!this._dropContainer) {
446+
this._applyRootElementTransform(value.x, value.y);
447+
}
448+
449+
return this;
450+
}
451+
429452
/** Unsubscribes from the global subscriptions. */
430453
private _removeSubscriptions() {
431454
this._pointerMoveSubscription.unsubscribe();
@@ -519,13 +542,8 @@ export class DragRef<T = any> {
519542
constrainedPointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x;
520543
activeTransform.y =
521544
constrainedPointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y;
522-
const transform = getTransform(activeTransform.x, activeTransform.y);
523545

524-
// Preserve the previous `transform` value, if there was one. Note that we apply our own
525-
// transform before the user's, because things like rotation can affect which direction
526-
// the element will be translated towards.
527-
this._rootElement.style.transform = this._initialTransform ?
528-
transform + ' ' + this._initialTransform : transform;
546+
this._applyRootElementTransform(activeTransform.x, activeTransform.y);
529547

530548
// Apply transform as attribute if dragging and svg element to work for IE
531549
if (typeof SVGElement !== 'undefined' && this._rootElement instanceof SVGElement) {
@@ -652,12 +670,6 @@ export class DragRef<T = any> {
652670
return;
653671
}
654672

655-
// Cache the previous transform amount only after the first drag sequence, because
656-
// we don't want our own transforms to stack on top of each other.
657-
if (this._initialTransform == null) {
658-
this._initialTransform = this._rootElement.style.transform || '';
659-
}
660-
661673
// If we've got handles, we need to disable the tap highlight on the entire root element,
662674
// otherwise iOS will still add it, even though all the drag interactions on the handle
663675
// are disabled.
@@ -981,6 +993,26 @@ export class DragRef<T = any> {
981993
element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
982994
}
983995

996+
/**
997+
* Applies a `transform` to the root element, taking into account any existing transforms on it.
998+
* @param x New transform value along the X axis.
999+
* @param y New transform value along the Y axis.
1000+
*/
1001+
private _applyRootElementTransform(x: number, y: number) {
1002+
const transform = getTransform(x, y);
1003+
1004+
// Cache the previous transform amount only after the first drag sequence, because
1005+
// we don't want our own transforms to stack on top of each other.
1006+
if (this._initialTransform == null) {
1007+
this._initialTransform = this._rootElement.style.transform || '';
1008+
}
1009+
1010+
// Preserve the previous `transform` value, if there was one. Note that we apply our own
1011+
// transform before the user's, because things like rotation can affect which direction
1012+
// the element will be translated towards.
1013+
this._rootElement.style.transform = this._initialTransform ?
1014+
transform + ' ' + this._initialTransform : transform;
1015+
}
9841016
}
9851017

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

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
2121
ended: EventEmitter<CdkDragEnd>;
2222
entered: EventEmitter<CdkDragEnter<any>>;
2323
exited: EventEmitter<CdkDragExit<any>>;
24+
freeDragPosition: {
25+
x: number;
26+
y: number;
27+
};
2428
lockAxis: 'x' | 'y';
2529
moved: Observable<CdkDragMove<T>>;
2630
released: EventEmitter<CdkDragRelease>;
@@ -30,6 +34,10 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
3034
element: ElementRef<HTMLElement>,
3135
dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, viewportRuler: ViewportRuler, dragDropRegistry: DragDropRegistry<DragRef, DropListRef>, config: DragRefConfig, _dir: Directionality,
3236
dragDrop?: DragDrop, _changeDetectorRef?: ChangeDetectorRef | undefined);
37+
getFreeDragPosition(): {
38+
readonly x: number;
39+
readonly y: number;
40+
};
3341
getPlaceholderElement(): HTMLElement;
3442
getRootElement(): HTMLElement;
3543
ngAfterViewInit(): void;
@@ -250,10 +258,12 @@ export declare class DragRef<T = any> {
250258
disableHandle(handle: HTMLElement): void;
251259
dispose(): void;
252260
enableHandle(handle: HTMLElement): void;
261+
getFreeDragPosition(): Readonly<Point>;
253262
getPlaceholderElement(): HTMLElement;
254263
getRootElement(): HTMLElement;
255264
isDragging(): boolean;
256265
reset(): void;
266+
setFreeDragPosition(value: Point): this;
257267
withBoundaryElement(boundaryElement: ElementRef<HTMLElement> | HTMLElement | null): this;
258268
withDirection(direction: Direction): this;
259269
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;

0 commit comments

Comments
 (0)