Skip to content

Commit 2016c25

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 e739e61 commit 2016c25

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
@@ -752,6 +752,47 @@ describe('CdkDrag', () => {
752752
'Expected element to be dragged after all the time has passed.');
753753
}));
754754

755+
it('should be able to get the current position', fakeAsync(() => {
756+
const fixture = createComponent(StandaloneDraggable);
757+
fixture.detectChanges();
758+
759+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
760+
const dragInstance = fixture.componentInstance.dragInstance;
761+
762+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0});
763+
764+
dragElementViaMouse(fixture, dragElement, 50, 100);
765+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
766+
767+
dragElementViaMouse(fixture, dragElement, 100, 200);
768+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300});
769+
}));
770+
771+
it('should be able to set the current position', fakeAsync(() => {
772+
const fixture = createComponent(StandaloneDraggable);
773+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
774+
fixture.detectChanges();
775+
776+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
777+
const dragInstance = fixture.componentInstance.dragInstance;
778+
779+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
780+
expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100});
781+
}));
782+
783+
it('should be able to continue dragging after the current position was set', fakeAsync(() => {
784+
const fixture = createComponent(StandaloneDraggable);
785+
fixture.componentInstance.freeDragPosition = {x: 50, y: 100};
786+
fixture.detectChanges();
787+
const dragElement = fixture.componentInstance.dragElement.nativeElement;
788+
789+
expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)');
790+
791+
dragElementViaMouse(fixture, dragElement, 100, 200);
792+
793+
expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)');
794+
}));
795+
755796
});
756797

757798
describe('draggable with a handle', () => {
@@ -3095,6 +3136,7 @@ describe('CdkDrag', () => {
30953136
[cdkDragBoundary]="boundarySelector"
30963137
[cdkDragStartDelay]="dragStartDelay"
30973138
[cdkDragConstrainPosition]="constrainPosition"
3139+
[cdkDragFreeDragPosition]="freeDragPosition"
30983140
(cdkDragStarted)="startedSpy($event)"
30993141
(cdkDragReleased)="releasedSpy($event)"
31003142
(cdkDragEnded)="endedSpy($event)"
@@ -3112,6 +3154,7 @@ class StandaloneDraggable {
31123154
boundarySelector: string;
31133155
dragStartDelay: number;
31143156
constrainPosition: (point: Point) => Point;
3157+
freeDragPosition?: {x: number, y: number};
31153158
}
31163159

31173160
@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 {
@@ -229,6 +235,13 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
229235
this._dragRef.reset();
230236
}
231237

238+
/**
239+
* Gets the pixel coordinates of the draggable outside of a drop container.
240+
*/
241+
getFreeDragPosition(): {readonly x: number, readonly y: number} {
242+
return this._dragRef.getFreeDragPosition();
243+
}
244+
232245
ngAfterViewInit() {
233246
// We need to wait for the zone to stabilize, in order for the reference
234247
// element to be in the proper place in the DOM. This is mostly relevant
@@ -260,17 +273,27 @@ export class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDestroy {
260273
const handle = handleInstance.element.nativeElement;
261274
handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle);
262275
});
276+
277+
if (this.freeDragPosition) {
278+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
279+
}
263280
});
264281
}
265282

266283
ngOnChanges(changes: SimpleChanges) {
267284
const rootSelectorChange = changes['rootElementSelector'];
285+
const positionChange = changes.positionChange;
268286

269287
// We don't have to react to the first change since it's being
270288
// handled in `ngAfterViewInit` where it needs to be deferred.
271289
if (rootSelectorChange && !rootSelectorChange.firstChange) {
272290
this._updateRootElement();
273291
}
292+
293+
// Skip the first change since it's being handled in `ngAfterViewInit`.
294+
if (positionChange && !positionChange.firstChange && this.freeDragPosition) {
295+
this._dragRef.setFreeDragPosition(this.freeDragPosition);
296+
}
274297
}
275298

276299
ngOnDestroy() {

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

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

437+
/**
438+
* Gets the current position in pixels the draggable outside of a drop container.
439+
*/
440+
getFreeDragPosition(): Readonly<Point> {
441+
return {x: this._passiveTransform.x, y: this._passiveTransform.y};
442+
}
443+
444+
/**
445+
* Sets the current position in pixels the draggable outside of a drop container.
446+
* @param value New position to be set.
447+
*/
448+
setFreeDragPosition(value: Point): this {
449+
this._activeTransform = {x: 0, y: 0};
450+
this._passiveTransform.x = value.x;
451+
this._passiveTransform.y = value.y;
452+
453+
if (!this._dropContainer) {
454+
this._applyRootElementTransform(value.x, value.y);
455+
}
456+
457+
return this;
458+
}
459+
437460
/** Unsubscribes from the global subscriptions. */
438461
private _removeSubscriptions() {
439462
this._pointerMoveSubscription.unsubscribe();
@@ -527,13 +550,8 @@ export class DragRef<T = any> {
527550
constrainedPointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x;
528551
activeTransform.y =
529552
constrainedPointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y;
530-
const transform = getTransform(activeTransform.x, activeTransform.y);
531553

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

538556
// Apply transform as attribute if dragging and svg element to work for IE
539557
if (typeof SVGElement !== 'undefined' && this._rootElement instanceof SVGElement) {
@@ -660,12 +678,6 @@ export class DragRef<T = any> {
660678
return;
661679
}
662680

663-
// Cache the previous transform amount only after the first drag sequence, because
664-
// we don't want our own transforms to stack on top of each other.
665-
if (this._initialTransform == null) {
666-
this._initialTransform = this._rootElement.style.transform || '';
667-
}
668-
669681
// If we've got handles, we need to disable the tap highlight on the entire root element,
670682
// otherwise iOS will still add it, even though all the drag interactions on the handle
671683
// are disabled.
@@ -990,6 +1002,26 @@ export class DragRef<T = any> {
9901002
element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions);
9911003
}
9921004

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

9951027
/** 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
@@ -22,6 +22,10 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
2222
ended: EventEmitter<CdkDragEnd>;
2323
entered: EventEmitter<CdkDragEnter<any>>;
2424
exited: EventEmitter<CdkDragExit<any>>;
25+
freeDragPosition: {
26+
x: number;
27+
y: number;
28+
};
2529
lockAxis: 'x' | 'y';
2630
moved: Observable<CdkDragMove<T>>;
2731
released: EventEmitter<CdkDragRelease>;
@@ -31,6 +35,10 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
3135
element: ElementRef<HTMLElement>,
3236
dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, viewportRuler: ViewportRuler, dragDropRegistry: DragDropRegistry<DragRef, DropListRef>, config: DragRefConfig, _dir: Directionality,
3337
dragDrop?: DragDrop, _changeDetectorRef?: ChangeDetectorRef | undefined);
38+
getFreeDragPosition(): {
39+
readonly x: number;
40+
readonly y: number;
41+
};
3442
getPlaceholderElement(): HTMLElement;
3543
getRootElement(): HTMLElement;
3644
ngAfterViewInit(): void;
@@ -252,10 +260,12 @@ export declare class DragRef<T = any> {
252260
disableHandle(handle: HTMLElement): void;
253261
dispose(): void;
254262
enableHandle(handle: HTMLElement): void;
263+
getFreeDragPosition(): Readonly<Point>;
255264
getPlaceholderElement(): HTMLElement;
256265
getRootElement(): HTMLElement;
257266
isDragging(): boolean;
258267
reset(): void;
268+
setFreeDragPosition(value: Point): this;
259269
withBoundaryElement(boundaryElement: ElementRef<HTMLElement> | HTMLElement | null): this;
260270
withDirection(direction: Direction): this;
261271
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;

0 commit comments

Comments
 (0)