From 9cb2afcacf2483459dda57beef327850e80e3b63 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 20 Feb 2019 07:56:59 +0100 Subject: [PATCH] feat(drag-drop): add the ability to disable sorting in a list Adds the ability to disable sorting inside of a `CdkDropList`. This allows for use cases where one list is considered the source and one or more connected lists are the destinations, and the user isn't supposed to sort the items within the source list. Fixes #14838. --- src/cdk/drag-drop/directives/drag.spec.ts | 101 ++++++++++++++++++++++ src/cdk/drag-drop/directives/drop-list.ts | 9 ++ src/cdk/drag-drop/drop-list-ref.ts | 38 +++++--- tools/public_api_guard/cdk/drag-drop.d.ts | 2 + 4 files changed, 140 insertions(+), 10 deletions(-) diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 0f7504b58303..32f63210102f 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -2183,6 +2183,59 @@ describe('CdkDrag', () => { .toEqual(['Zero', 'One', 'Two', 'Three']); })); + it('should not sort an item if sorting the list is disabled', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + + const dropInstance = fixture.componentInstance.dropInstance; + const dragItems = fixture.componentInstance.dragItems; + + dropInstance.sortingDisabled = true; + + expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) + .toEqual(['Zero', 'One', 'Two', 'Three']); + + const firstItem = dragItems.first; + const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect(); + const targetX = thirdItemRect.left + 1; + const targetY = thirdItemRect.top + 1; + + startDraggingViaMouse(fixture, firstItem.element.nativeElement); + + const placeholder = document.querySelector('.cdk-drag-placeholder') as HTMLElement; + + dispatchMouseEvent(document, 'mousemove', targetX, targetY); + fixture.detectChanges(); + + expect(getElementIndexByPosition(placeholder, 'top')) + .toBe(0, 'Expected placeholder to stay in place.'); + + dispatchMouseEvent(document, 'mouseup', targetX, targetY); + fixture.detectChanges(); + + flush(); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will + // go into an infinite loop trying to stringify the event, if the test fails. + expect(event).toEqual({ + previousIndex: 0, + currentIndex: 0, + item: firstItem, + container: dropInstance, + previousContainer: dropInstance, + isPointerOverContainer: true + }); + + expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())) + .toEqual(['Zero', 'One', 'Two', 'Three']); + })); + + }); describe('in a connected drop container', () => { @@ -2794,6 +2847,54 @@ describe('CdkDrag', () => { })); + it('should return the item to its initial position, if sorting in the source container ' + + 'was disabled', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); + + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement); + const item = groups[0][1]; + const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect(); + + fixture.componentInstance.dropInstances.first.sortingDisabled = true; + startDraggingViaMouse(fixture, item.element.nativeElement); + + const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!; + + expect(placeholder).toBeTruthy(); + expect(dropZones[0].contains(placeholder)) + .toBe(true, 'Expected placeholder to be inside the first container.'); + expect(getElementIndexByPosition(placeholder, 'top')) + .toBe(1, 'Expected placeholder to be at item index.'); + + dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1); + fixture.detectChanges(); + + expect(dropZones[1].contains(placeholder)) + .toBe(true, 'Expected placeholder to be inside second container.'); + expect(getElementIndexByPosition(placeholder, 'top')) + .toBe(3, 'Expected placeholder to be at the target index.'); + + const firstInitialSiblingRect = groups[0][0].element + .nativeElement.getBoundingClientRect(); + + // Return the item to an index that is different from the initial one. + dispatchMouseEvent(document, 'mousemove', firstInitialSiblingRect.left + 1, + firstInitialSiblingRect.top + 1); + fixture.detectChanges(); + + expect(dropZones[0].contains(placeholder)) + .toBe(true, 'Expected placeholder to be back inside first container.'); + expect(getElementIndexByPosition(placeholder, 'top')) + .toBe(1, 'Expected placeholder to be back at the initial index.'); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled(); + })); + }); }); diff --git a/src/cdk/drag-drop/directives/drop-list.ts b/src/cdk/drag-drop/directives/drop-list.ts index 36769f83242b..d8f04e82e8c2 100644 --- a/src/cdk/drag-drop/directives/drop-list.ts +++ b/src/cdk/drag-drop/directives/drop-list.ts @@ -116,6 +116,14 @@ export class CdkDropList implements CdkDropListContainer, AfterContentI } private _disabled = false; + /** Whether starting a dragging sequence from this container is disabled. */ + @Input('cdkDropListSortingDisabled') + get sortingDisabled(): boolean { return this._sortingDisabled; } + set sortingDisabled(value: boolean) { + this._sortingDisabled = coerceBooleanProperty(value); + } + private _sortingDisabled = false; + /** * Function that is used to determine whether an item * is allowed to be moved into a drop container. @@ -307,6 +315,7 @@ export class CdkDropList implements CdkDropListContainer, AfterContentI } ref.lockAxis = this.lockAxis; + ref.sortingDisabled = this.sortingDisabled; ref .connectedTo(siblings.filter(drop => drop && drop !== this).map(list => list._dropListRef)) .withOrientation(this.orientation); diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index 26dd62ee73c5..41b01b9db19f 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -63,6 +63,9 @@ export class DropListRef { /** Whether starting a dragging sequence from this container is disabled. */ disabled: boolean = false; + /** Whether sorting items within the list is disabled. */ + sortingDisabled: boolean = true; + /** Locks the position of the draggable elements inside the container along the specified axis. */ lockAxis: 'x' | 'y'; @@ -189,17 +192,32 @@ export class DropListRef { this.entered.next({item, container: this}); this.start(); - // We use the coordinates of where the item entered the drop - // zone to figure out at which index it should be inserted. - const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY); - const currentIndex = this._activeDraggables.indexOf(item); - const newPositionReference = this._activeDraggables[newIndex]; + // If sorting is disabled, we want the item to return to its starting + // position if the user is returning it to its initial container. + let newIndex = this.sortingDisabled ? this._draggables.indexOf(item) : -1; + + if (newIndex === -1) { + // We use the coordinates of where the item entered the drop + // zone to figure out at which index it should be inserted. + newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY); + } + + const activeDraggables = this._activeDraggables; + const currentIndex = activeDraggables.indexOf(item); const placeholder = item.getPlaceholderElement(); + let newPositionReference: DragRef | undefined = activeDraggables[newIndex]; + + // If the item at the new position is the same as the item that is being dragged, + // it means that we're trying to restore the item to its initial position. In this + // case we should use the next item from the list as the reference. + if (newPositionReference === item) { + newPositionReference = activeDraggables[newIndex + 1]; + } // Since the item may be in the `activeDraggables` already (e.g. if the user dragged it // into another container and back again), we have to ensure that it isn't duplicated. if (currentIndex > -1) { - this._activeDraggables.splice(currentIndex, 1); + activeDraggables.splice(currentIndex, 1); } // Don't use items that are being dragged as a reference, because @@ -207,10 +225,10 @@ export class DropListRef { if (newPositionReference && !this._dragDropRegistry.isDragging(newPositionReference)) { const element = newPositionReference.getRootElement(); element.parentElement!.insertBefore(placeholder, element); - this._activeDraggables.splice(newIndex, 0, item); + activeDraggables.splice(newIndex, 0, item); } else { this.element.appendChild(placeholder); - this._activeDraggables.push(item); + activeDraggables.push(item); } // The transform needs to be cleared so it doesn't throw off the measurements. @@ -321,8 +339,8 @@ export class DropListRef { */ _sortItem(item: DragRef, pointerX: number, pointerY: number, pointerDelta: {x: number, y: number}): void { - // Don't sort the item if it's out of range. - if (!this._isPointerNearDropContainer(pointerX, pointerY)) { + // Don't sort the item if sorting is disabled or it's out of range. + if (this.sortingDisabled || !this._isPointerNearDropContainer(pointerX, pointerY)) { return; } diff --git a/tools/public_api_guard/cdk/drag-drop.d.ts b/tools/public_api_guard/cdk/drag-drop.d.ts index 1ded5a5d2007..89afef965500 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -127,6 +127,7 @@ export declare class CdkDropList implements CdkDropListContainer, After lockAxis: 'x' | 'y'; orientation: 'horizontal' | 'vertical'; sorted: EventEmitter>; + sortingDisabled: boolean; constructor( element: ElementRef, dragDropRegistry: DragDropRegistry, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup> | undefined, _document?: any, dragDrop?: DragDrop); @@ -294,6 +295,7 @@ export declare class DropListRef { container: DropListRef; item: DragRef; }>; + sortingDisabled: boolean; constructor(element: ElementRef | HTMLElement, _dragDropRegistry: DragDropRegistry, _document: any); _canReceive(item: DragRef, x: number, y: number): boolean; _getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | undefined;