feat(drag-drop): add API to get/set current position of a standalone draggable (#14696)

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.
This commit is contained in:
Kristiyan Kostadinov
2019-04-03 21:53:59 +02:00
committed by mmalerba
parent ae32092c22
commit 2f009d00da
4 changed files with 120 additions and 12 deletions
+43
View File
@@ -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({
+23
View File
@@ -116,6 +116,12 @@ export class CdkDrag<T = any> 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<T = any> 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<T = any> 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() {
+44 -12
View File
@@ -434,6 +434,29 @@ export class DragRef<T = any> {
this._dropContainer = container;
}
/**
* Gets the current position in pixels the draggable outside of a drop container.
*/
getFreeDragPosition(): Readonly<Point> {
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<T = any> {
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<T = any> {
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<T = any> {
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. */
+10
View File
@@ -22,6 +22,10 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
ended: EventEmitter<CdkDragEnd>;
entered: EventEmitter<CdkDragEnter<any>>;
exited: EventEmitter<CdkDragExit<any>>;
freeDragPosition: {
x: number;
y: number;
};
lockAxis: 'x' | 'y';
moved: Observable<CdkDragMove<T>>;
released: EventEmitter<CdkDragRelease>;
@@ -31,6 +35,10 @@ export declare class CdkDrag<T = any> implements AfterViewInit, OnChanges, OnDes
element: ElementRef<HTMLElement>,
dropContainer: CdkDropList, _document: any, _ngZone: NgZone, _viewContainerRef: ViewContainerRef, viewportRuler: ViewportRuler, dragDropRegistry: DragDropRegistry<DragRef, DropListRef>, 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<T = any> {
disableHandle(handle: HTMLElement): void;
dispose(): void;
enableHandle(handle: HTMLElement): void;
getFreeDragPosition(): Readonly<Point>;
getPlaceholderElement(): HTMLElement;
getRootElement(): HTMLElement;
isDragging(): boolean;
reset(): void;
setFreeDragPosition(value: Point): this;
withBoundaryElement(boundaryElement: ElementRef<HTMLElement> | HTMLElement | null): this;
withDirection(direction: Direction): this;
withHandles(handles: (HTMLElement | ElementRef<HTMLElement>)[]): this;