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..911b39f1b252 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..c70a0e849bfc 100644 --- a/tools/public_api_guard/cdk/drag-drop.d.ts +++ b/tools/public_api_guard/cdk/drag-drop.d.ts @@ -242,6 +242,7 @@ export declare class DragRef { started: Subject<{ source: DragRef; }>; + sortingDisabled: boolean; constructor(element: ElementRef | HTMLElement, _config: DragRefConfig, _document: Document, _ngZone: NgZone, _viewportRuler: ViewportRuler, _dragDropRegistry: DragDropRegistry); _withDropContainer(container: DropListRef): void; disableHandle(handle: HTMLElement): void; @@ -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;