diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 091a53d003b..b577e10dcb0 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -788,6 +788,47 @@ describe('CdkDrag', () => { 'Expected element to be dragged after all the time has passed.'); })); + it('should be able to get the current position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; + + expect(dragInstance.getFreeDragPosition()).toEqual({x: 0, y: 0}); + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 150, y: 300}); + })); + + it('should be able to set the current position', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; + fixture.detectChanges(); + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + const dragInstance = fixture.componentInstance.dragInstance; + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + expect(dragInstance.getFreeDragPosition()).toEqual({x: 50, y: 100}); + })); + + it('should be able to continue dragging after the current position was set', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.componentInstance.freeDragPosition = {x: 50, y: 100}; + fixture.detectChanges(); + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBe('translate3d(50px, 100px, 0px)'); + + dragElementViaMouse(fixture, dragElement, 100, 200); + + expect(dragElement.style.transform).toBe('translate3d(150px, 300px, 0px)'); + })); + }); describe('draggable with a handle', () => { @@ -3187,6 +3228,7 @@ describe('CdkDrag', () => { [cdkDragBoundary]="boundarySelector" [cdkDragStartDelay]="dragStartDelay" [cdkDragConstrainPosition]="constrainPosition" + [cdkDragFreeDragPosition]="freeDragPosition" (cdkDragStarted)="startedSpy($event)" (cdkDragReleased)="releasedSpy($event)" (cdkDragEnded)="endedSpy($event)" @@ -3204,6 +3246,7 @@ class StandaloneDraggable { boundarySelector: string; dragStartDelay: number | string; constrainPosition: (point: Point) => Point; + freeDragPosition?: {x: number, y: number}; } @Component({ diff --git a/src/cdk/drag-drop/directives/drag.ts b/src/cdk/drag-drop/directives/drag.ts index 239f5e8043d..2cededecb4a 100644 --- a/src/cdk/drag-drop/directives/drag.ts +++ b/src/cdk/drag-drop/directives/drag.ts @@ -116,6 +116,12 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { */ @Input('cdkDragStartDelay') dragStartDelay: number = 0; + /** + * Sets the position of a `CdkDrag` that is outside of a drop container. + * Can be used to restore the element's position for a returning user. + */ + @Input('cdkDragFreeDragPosition') freeDragPosition: {x: number, y: number}; + /** Whether starting to drag this element is disabled. */ @Input('cdkDragDisabled') get disabled(): boolean { @@ -229,6 +235,13 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { this._dragRef.reset(); } + /** + * Gets the pixel coordinates of the draggable outside of a drop container. + */ + getFreeDragPosition(): {readonly x: number, readonly y: number} { + return this._dragRef.getFreeDragPosition(); + } + ngAfterViewInit() { // We need to wait for the zone to stabilize, in order for the reference // element to be in the proper place in the DOM. This is mostly relevant @@ -260,17 +273,27 @@ export class CdkDrag implements AfterViewInit, OnChanges, OnDestroy { const handle = handleInstance.element.nativeElement; handleInstance.disabled ? dragRef.disableHandle(handle) : dragRef.enableHandle(handle); }); + + if (this.freeDragPosition) { + this._dragRef.setFreeDragPosition(this.freeDragPosition); + } }); } ngOnChanges(changes: SimpleChanges) { const rootSelectorChange = changes['rootElementSelector']; + const positionChange = changes['positionChange']; // We don't have to react to the first change since it's being // handled in `ngAfterViewInit` where it needs to be deferred. if (rootSelectorChange && !rootSelectorChange.firstChange) { this._updateRootElement(); } + + // Skip the first change since it's being handled in `ngAfterViewInit`. + if (positionChange && !positionChange.firstChange && this.freeDragPosition) { + this._dragRef.setFreeDragPosition(this.freeDragPosition); + } } ngOnDestroy() { diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index c29ac40a542..341a1c27b78 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -434,6 +434,29 @@ export class DragRef { this._dropContainer = container; } + /** + * Gets the current position in pixels the draggable outside of a drop container. + */ + getFreeDragPosition(): Readonly { + return {x: this._passiveTransform.x, y: this._passiveTransform.y}; + } + + /** + * Sets the current position in pixels the draggable outside of a drop container. + * @param value New position to be set. + */ + setFreeDragPosition(value: Point): this { + this._activeTransform = {x: 0, y: 0}; + this._passiveTransform.x = value.x; + this._passiveTransform.y = value.y; + + if (!this._dropContainer) { + this._applyRootElementTransform(value.x, value.y); + } + + return this; + } + /** Unsubscribes from the global subscriptions. */ private _removeSubscriptions() { this._pointerMoveSubscription.unsubscribe(); @@ -527,13 +550,8 @@ export class DragRef { constrainedPointerPosition.x - this._pickupPositionOnPage.x + this._passiveTransform.x; activeTransform.y = constrainedPointerPosition.y - this._pickupPositionOnPage.y + this._passiveTransform.y; - const transform = getTransform(activeTransform.x, activeTransform.y); - // Preserve the previous `transform` value, if there was one. Note that we apply our own - // transform before the user's, because things like rotation can affect which direction - // the element will be translated towards. - this._rootElement.style.transform = this._initialTransform ? - transform + ' ' + this._initialTransform : transform; + this._applyRootElementTransform(activeTransform.x, activeTransform.y); // Apply transform as attribute if dragging and svg element to work for IE if (typeof SVGElement !== 'undefined' && this._rootElement instanceof SVGElement) { @@ -660,12 +678,6 @@ export class DragRef { return; } - // Cache the previous transform amount only after the first drag sequence, because - // we don't want our own transforms to stack on top of each other. - if (this._initialTransform == null) { - this._initialTransform = this._rootElement.style.transform || ''; - } - // If we've got handles, we need to disable the tap highlight on the entire root element, // otherwise iOS will still add it, even though all the drag interactions on the handle // are disabled. @@ -990,6 +1002,26 @@ export class DragRef { element.removeEventListener('touchstart', this._pointerDown, passiveEventListenerOptions); } + /** + * Applies a `transform` to the root element, taking into account any existing transforms on it. + * @param x New transform value along the X axis. + * @param y New transform value along the Y axis. + */ + private _applyRootElementTransform(x: number, y: number) { + const transform = getTransform(x, y); + + // Cache the previous transform amount only after the first drag sequence, because + // we don't want our own transforms to stack on top of each other. + if (this._initialTransform == null) { + this._initialTransform = this._rootElement.style.transform || ''; + } + + // Preserve the previous `transform` value, if there was one. Note that we apply our own + // transform before the user's, because things like rotation can affect which direction + // the element will be translated towards. + this._rootElement.style.transform = this._initialTransform ? + transform + ' ' + this._initialTransform : transform; + } } /** Point on the page or within an element. */ diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index ab58a61661f..bf726d7e2ae 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -22,6 +22,10 @@ export declare class CdkDrag implements AfterViewInit, OnChanges, OnDes ended: EventEmitter; entered: EventEmitter>; exited: EventEmitter>; + freeDragPosition: { + x: number; + y: number; + }; lockAxis: 'x' | 'y'; moved: Observable>; released: EventEmitter; @@ -31,6 +35,10 @@ export declare class CdkDrag implements AfterViewInit, OnChanges, OnDes element: ElementRef, dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, viewportRuler: ViewportRuler, dragDropRegistry: DragDropRegistry, config: DragRefConfig, _dir: Directionality, dragDrop?: DragDrop, _changeDetectorRef?: ChangeDetectorRef | undefined); + getFreeDragPosition(): { + readonly x: number; + readonly y: number; + }; getPlaceholderElement(): HTMLElement; getRootElement(): HTMLElement; ngAfterViewInit(): void; @@ -252,10 +260,12 @@ export declare class DragRef { disableHandle(handle: HTMLElement): void; dispose(): void; enableHandle(handle: HTMLElement): void; + getFreeDragPosition(): Readonly; getPlaceholderElement(): HTMLElement; getRootElement(): HTMLElement; isDragging(): boolean; reset(): void; + setFreeDragPosition(value: Point): this; withBoundaryElement(boundaryElement: ElementRef | HTMLElement | null): this; withDirection(direction: Direction): this; withHandles(handles: (HTMLElement | ElementRef)[]): this;