From 4d430bd146867062bbeb493a44c65f01c1d382a8 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 15 Aug 2018 00:52:57 +0200 Subject: [PATCH] feat(drag-drop): add the ability to lock dragging along an axis (#12604) Adds inputs that allow consumers to lock dragging of a particular drag item, or all items in a drag container, along an axis. --- src/cdk-experimental/drag-drop/drag.spec.ts | 92 +++++++++++++++++++ src/cdk-experimental/drag-drop/drag.ts | 25 ++++- .../drag-drop/drop-container.ts | 3 + src/cdk-experimental/drag-drop/drop.ts | 3 + src/demo-app/drag-drop/drag-drop-demo.html | 21 ++++- src/demo-app/drag-drop/drag-drop-demo.ts | 1 + 6 files changed, 137 insertions(+), 8 deletions(-) diff --git a/src/cdk-experimental/drag-drop/drag.spec.ts b/src/cdk-experimental/drag-drop/drag.spec.ts index fadccc09d16..a8cf6b3bc2e 100644 --- a/src/cdk-experimental/drag-drop/drag.spec.ts +++ b/src/cdk-experimental/drag-drop/drag.spec.ts @@ -246,6 +246,38 @@ describe('CdkDrag', () => { subscription.unsubscribe(); }); + it('should be able to lock dragging along the x axis', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + fixture.componentInstance.dragInstance.lockAxis = 'x'; + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(50px, 0px, 0px)'); + + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(150px, 0px, 0px)'); + })); + + it('should be able to lock dragging along the y axis', fakeAsync(() => { + const fixture = createComponent(StandaloneDraggable); + fixture.detectChanges(); + fixture.componentInstance.dragInstance.lockAxis = 'y'; + + const dragElement = fixture.componentInstance.dragElement.nativeElement; + + expect(dragElement.style.transform).toBeFalsy(); + + dragElementViaMouse(fixture, dragElement, 50, 100); + expect(dragElement.style.transform).toBe('translate3d(0px, 100px, 0px)'); + + dragElementViaMouse(fixture, dragElement, 100, 200); + expect(dragElement.style.transform).toBe('translate3d(0px, 300px, 0px)'); + })); + }); describe('draggable with a handle', () => { @@ -690,6 +722,65 @@ describe('CdkDrag', () => { expect(preview.style.transform).toBe('translate3d(50px, 50px, 0px)'); })); + it('should lock position inside a drop container along the x axis', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + fixture.detectChanges(); + + const item = fixture.componentInstance.dragItems.toArray()[1]; + const element = item.element.nativeElement; + + item.lockAxis = 'x'; + + dispatchMouseEvent(element, 'mousedown', 50, 50); + fixture.detectChanges(); + + dispatchMouseEvent(element, 'mousemove', 100, 100); + fixture.detectChanges(); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + + expect(preview.style.transform).toBe('translate3d(100px, 50px, 0px)'); + })); + + it('should lock position inside a drop container along the y axis', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + fixture.detectChanges(); + + const item = fixture.componentInstance.dragItems.toArray()[1]; + const element = item.element.nativeElement; + + item.lockAxis = 'y'; + + dispatchMouseEvent(element, 'mousedown', 50, 50); + fixture.detectChanges(); + + dispatchMouseEvent(element, 'mousemove', 100, 100); + fixture.detectChanges(); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + + expect(preview.style.transform).toBe('translate3d(50px, 100px, 0px)'); + })); + + it('should inherit the position locking from the drop container', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZoneWithCustomPreview); + fixture.detectChanges(); + + const element = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + + fixture.componentInstance.dropInstance.lockAxis = 'x'; + + dispatchMouseEvent(element, 'mousedown', 50, 50); + fixture.detectChanges(); + + dispatchMouseEvent(element, 'mousemove', 100, 100); + fixture.detectChanges(); + + const preview = document.querySelector('.cdk-drag-preview')! as HTMLElement; + + expect(preview.style.transform).toBe('translate3d(100px, 50px, 0px)'); + })); + it('should be able to customize the placeholder', fakeAsync(() => { const fixture = createComponent(DraggableInDropZoneWithCustomPlaceholder); fixture.detectChanges(); @@ -1103,6 +1194,7 @@ export class DraggableInHorizontalDropZone { ` }) export class DraggableInDropZoneWithCustomPreview { + @ViewChild(CdkDrop) dropInstance: CdkDrop; @ViewChildren(CdkDrag) dragItems: QueryList; items = ['Zero', 'One', 'Two', 'Three']; } diff --git a/src/cdk-experimental/drag-drop/drag.ts b/src/cdk-experimental/drag-drop/drag.ts index 3ad3a0d648e..2c665b866fe 100644 --- a/src/cdk-experimental/drag-drop/drag.ts +++ b/src/cdk-experimental/drag-drop/drag.ts @@ -119,14 +119,15 @@ export class CdkDrag implements OnDestroy { /** Element that will be used as a template to create the draggable item's preview. */ @ContentChild(CdkDragPreview) _previewTemplate: CdkDragPreview; - /** - * Template for placeholder element rendered to show where a draggable would be dropped. - */ + /** Template for placeholder element rendered to show where a draggable would be dropped. */ @ContentChild(CdkDragPlaceholder) _placeholderTemplate: CdkDragPlaceholder; /** Arbitrary data to attach to this drag instance. */ @Input() data: T; + /** Locks the position of the dragged element along the specified axis. */ + @Input('cdkDragLockAxis') lockAxis: 'x' | 'y'; + /** Emits when the user starts dragging the item. */ @Output('cdkDragStarted') started: EventEmitter = new EventEmitter(); @@ -276,7 +277,7 @@ export class CdkDrag implements OnDestroy { this._hasMoved = true; event.preventDefault(); - const pointerPosition = this._getPointerPositionOnPage(event); + const pointerPosition = this._getConstrainedPointerPosition(event); if (this.dropContainer) { this._updateActiveDropContainer(pointerPosition); @@ -361,7 +362,7 @@ export class CdkDrag implements OnDestroy { * Updates the item's position in its drop container, or moves it * into a new one, depending on its current drag position. */ - private _updateActiveDropContainer({x, y}: Point) { + private _updateActiveDropContainer({x, y}) { // Drop container that draggable has been moved into. const newContainer = this.dropContainer._getSiblingContainerFromPosition(x, y); @@ -531,6 +532,20 @@ export class CdkDrag implements OnDestroy { }; } + /** Gets the pointer position on the page, accounting for any position constraints. */ + private _getConstrainedPointerPosition(event: MouseEvent | TouchEvent): Point { + const point = this._getPointerPositionOnPage(event); + const dropContainerLock = this.dropContainer ? this.dropContainer.lockAxis : null; + + if (this.lockAxis === 'x' || dropContainerLock === 'x') { + point.y = this._pickupPositionOnPage.y; + } else if (this.lockAxis === 'y' || dropContainerLock === 'y') { + point.x = this._pickupPositionOnPage.x; + } + + return point; + } + /** Determines whether an event is a touch event. */ private _isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent { return event.type.startsWith('touch'); diff --git a/src/cdk-experimental/drag-drop/drop-container.ts b/src/cdk-experimental/drag-drop/drop-container.ts index 36bd6f49df5..7030d0684c2 100644 --- a/src/cdk-experimental/drag-drop/drop-container.ts +++ b/src/cdk-experimental/drag-drop/drop-container.ts @@ -19,6 +19,9 @@ export interface CdkDropContainer { /** Direction in which the list is oriented. */ orientation: 'horizontal' | 'vertical'; + /** Locks the position of the draggable elements inside the container along the specified axis. */ + lockAxis: 'x' | 'y'; + /** Starts dragging an item. */ start(): void; diff --git a/src/cdk-experimental/drag-drop/drop.ts b/src/cdk-experimental/drag-drop/drop.ts index e26003584af..d8fcc2c8025 100644 --- a/src/cdk-experimental/drag-drop/drop.ts +++ b/src/cdk-experimental/drag-drop/drop.ts @@ -69,6 +69,9 @@ export class CdkDrop implements OnInit, OnDestroy { */ @Input() id: string = `cdk-drop-${_uniqueIdCounter++}`; + /** Locks the position of the draggable elements inside the container along the specified axis. */ + @Input() lockAxis: 'x' | 'y'; + /** Emits when the user drops an item inside the container. */ @Output() dropped: EventEmitter> = new EventEmitter>(); diff --git a/src/demo-app/drag-drop/drag-drop-demo.html b/src/demo-app/drag-drop/drag-drop-demo.html index 1ebf05dbd83..a82df8bd50b 100644 --- a/src/demo-app/drag-drop/drag-drop-demo.html +++ b/src/demo-app/drag-drop/drag-drop-demo.html @@ -4,6 +4,7 @@
@@ -18,6 +19,7 @@
@@ -34,6 +36,7 @@
{{item}} @@ -43,6 +46,11 @@
+
+

Free dragging

+
Drag me around
+
+

Data

{{todo.join(', ')}}
@@ -50,7 +58,14 @@
{{horizontalData.join(', ')}}
-
-

Free dragging

-
Drag me around
+
+

Axis locking

+ + Lock position along axis + + None + X axis + Y axis + +
diff --git a/src/demo-app/drag-drop/drag-drop-demo.ts b/src/demo-app/drag-drop/drag-drop-demo.ts index b72cedcf930..0d08cf4aef8 100644 --- a/src/demo-app/drag-drop/drag-drop-demo.ts +++ b/src/demo-app/drag-drop/drag-drop-demo.ts @@ -19,6 +19,7 @@ import {CdkDragDrop, moveItemInArray, transferArrayItem} from '@angular/cdk-expe encapsulation: ViewEncapsulation.None, }) export class DragAndDropDemo { + axisLock: 'x' | 'y'; todo = [ 'Come up with catchy start-up name', 'Add "blockchain" to name',