Skip to content

Commit 629460f

Browse files
crisbetojelbourn
authored andcommitted
feat(drag-drop): add the ability to disable sorting in a list (#15064)
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.
1 parent dd4257e commit 629460f

File tree

4 files changed

+140
-10
lines changed

4 files changed

+140
-10
lines changed

src/cdk/drag-drop/directives/drag.spec.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2241,6 +2241,59 @@ describe('CdkDrag', () => {
22412241
.toEqual(['Zero', 'One', 'Two', 'Three']);
22422242
}));
22432243

2244+
it('should not sort an item if sorting the list is disabled', fakeAsync(() => {
2245+
const fixture = createComponent(DraggableInDropZone);
2246+
fixture.detectChanges();
2247+
2248+
const dropInstance = fixture.componentInstance.dropInstance;
2249+
const dragItems = fixture.componentInstance.dragItems;
2250+
2251+
dropInstance.sortingDisabled = true;
2252+
2253+
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
2254+
.toEqual(['Zero', 'One', 'Two', 'Three']);
2255+
2256+
const firstItem = dragItems.first;
2257+
const thirdItemRect = dragItems.toArray()[2].element.nativeElement.getBoundingClientRect();
2258+
const targetX = thirdItemRect.left + 1;
2259+
const targetY = thirdItemRect.top + 1;
2260+
2261+
startDraggingViaMouse(fixture, firstItem.element.nativeElement);
2262+
2263+
const placeholder = document.querySelector('.cdk-drag-placeholder') as HTMLElement;
2264+
2265+
dispatchMouseEvent(document, 'mousemove', targetX, targetY);
2266+
fixture.detectChanges();
2267+
2268+
expect(getElementIndexByPosition(placeholder, 'top'))
2269+
.toBe(0, 'Expected placeholder to stay in place.');
2270+
2271+
dispatchMouseEvent(document, 'mouseup', targetX, targetY);
2272+
fixture.detectChanges();
2273+
2274+
flush();
2275+
fixture.detectChanges();
2276+
2277+
expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1);
2278+
2279+
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
2280+
2281+
// Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will
2282+
// go into an infinite loop trying to stringify the event, if the test fails.
2283+
expect(event).toEqual({
2284+
previousIndex: 0,
2285+
currentIndex: 0,
2286+
item: firstItem,
2287+
container: dropInstance,
2288+
previousContainer: dropInstance,
2289+
isPointerOverContainer: true
2290+
});
2291+
2292+
expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim()))
2293+
.toEqual(['Zero', 'One', 'Two', 'Three']);
2294+
}));
2295+
2296+
22442297
});
22452298

22462299
describe('in a connected drop container', () => {
@@ -2852,6 +2905,54 @@ describe('CdkDrag', () => {
28522905

28532906
}));
28542907

2908+
it('should return the item to its initial position, if sorting in the source container ' +
2909+
'was disabled', fakeAsync(() => {
2910+
const fixture = createComponent(ConnectedDropZones);
2911+
fixture.detectChanges();
2912+
2913+
const groups = fixture.componentInstance.groupedDragItems;
2914+
const dropZones = fixture.componentInstance.dropInstances.map(d => d.element.nativeElement);
2915+
const item = groups[0][1];
2916+
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
2917+
2918+
fixture.componentInstance.dropInstances.first.sortingDisabled = true;
2919+
startDraggingViaMouse(fixture, item.element.nativeElement);
2920+
2921+
const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!;
2922+
2923+
expect(placeholder).toBeTruthy();
2924+
expect(dropZones[0].contains(placeholder))
2925+
.toBe(true, 'Expected placeholder to be inside the first container.');
2926+
expect(getElementIndexByPosition(placeholder, 'top'))
2927+
.toBe(1, 'Expected placeholder to be at item index.');
2928+
2929+
dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1);
2930+
fixture.detectChanges();
2931+
2932+
expect(dropZones[1].contains(placeholder))
2933+
.toBe(true, 'Expected placeholder to be inside second container.');
2934+
expect(getElementIndexByPosition(placeholder, 'top'))
2935+
.toBe(3, 'Expected placeholder to be at the target index.');
2936+
2937+
const firstInitialSiblingRect = groups[0][0].element
2938+
.nativeElement.getBoundingClientRect();
2939+
2940+
// Return the item to an index that is different from the initial one.
2941+
dispatchMouseEvent(document, 'mousemove', firstInitialSiblingRect.left + 1,
2942+
firstInitialSiblingRect.top + 1);
2943+
fixture.detectChanges();
2944+
2945+
expect(dropZones[0].contains(placeholder))
2946+
.toBe(true, 'Expected placeholder to be back inside first container.');
2947+
expect(getElementIndexByPosition(placeholder, 'top'))
2948+
.toBe(1, 'Expected placeholder to be back at the initial index.');
2949+
2950+
dispatchMouseEvent(document, 'mouseup');
2951+
fixture.detectChanges();
2952+
2953+
expect(fixture.componentInstance.droppedSpy).not.toHaveBeenCalled();
2954+
}));
2955+
28552956
});
28562957

28572958
});

src/cdk/drag-drop/directives/drop-list.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ export class CdkDropList<T = any> implements CdkDropListContainer, AfterContentI
117117
}
118118
private _disabled = false;
119119

120+
/** Whether starting a dragging sequence from this container is disabled. */
121+
@Input('cdkDropListSortingDisabled')
122+
get sortingDisabled(): boolean { return this._sortingDisabled; }
123+
set sortingDisabled(value: boolean) {
124+
this._sortingDisabled = coerceBooleanProperty(value);
125+
}
126+
private _sortingDisabled = false;
127+
120128
/**
121129
* Function that is used to determine whether an item
122130
* is allowed to be moved into a drop container.
@@ -308,6 +316,7 @@ export class CdkDropList<T = any> implements CdkDropListContainer, AfterContentI
308316
}
309317

310318
ref.lockAxis = this.lockAxis;
319+
ref.sortingDisabled = this.sortingDisabled;
311320
ref
312321
.connectedTo(siblings.filter(drop => drop && drop !== this).map(list => list._dropListRef))
313322
.withOrientation(this.orientation);

src/cdk/drag-drop/drop-list-ref.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ export class DropListRef<T = any> {
6363
/** Whether starting a dragging sequence from this container is disabled. */
6464
disabled: boolean = false;
6565

66+
/** Whether sorting items within the list is disabled. */
67+
sortingDisabled: boolean = true;
68+
6669
/** Locks the position of the draggable elements inside the container along the specified axis. */
6770
lockAxis: 'x' | 'y';
6871

@@ -189,28 +192,43 @@ export class DropListRef<T = any> {
189192
this.entered.next({item, container: this});
190193
this.start();
191194

192-
// We use the coordinates of where the item entered the drop
193-
// zone to figure out at which index it should be inserted.
194-
const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY);
195-
const currentIndex = this._activeDraggables.indexOf(item);
196-
const newPositionReference = this._activeDraggables[newIndex];
195+
// If sorting is disabled, we want the item to return to its starting
196+
// position if the user is returning it to its initial container.
197+
let newIndex = this.sortingDisabled ? this._draggables.indexOf(item) : -1;
198+
199+
if (newIndex === -1) {
200+
// We use the coordinates of where the item entered the drop
201+
// zone to figure out at which index it should be inserted.
202+
newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY);
203+
}
204+
205+
const activeDraggables = this._activeDraggables;
206+
const currentIndex = activeDraggables.indexOf(item);
197207
const placeholder = item.getPlaceholderElement();
208+
let newPositionReference: DragRef | undefined = activeDraggables[newIndex];
209+
210+
// If the item at the new position is the same as the item that is being dragged,
211+
// it means that we're trying to restore the item to its initial position. In this
212+
// case we should use the next item from the list as the reference.
213+
if (newPositionReference === item) {
214+
newPositionReference = activeDraggables[newIndex + 1];
215+
}
198216

199217
// Since the item may be in the `activeDraggables` already (e.g. if the user dragged it
200218
// into another container and back again), we have to ensure that it isn't duplicated.
201219
if (currentIndex > -1) {
202-
this._activeDraggables.splice(currentIndex, 1);
220+
activeDraggables.splice(currentIndex, 1);
203221
}
204222

205223
// Don't use items that are being dragged as a reference, because
206224
// their element has been moved down to the bottom of the body.
207225
if (newPositionReference && !this._dragDropRegistry.isDragging(newPositionReference)) {
208226
const element = newPositionReference.getRootElement();
209227
element.parentElement!.insertBefore(placeholder, element);
210-
this._activeDraggables.splice(newIndex, 0, item);
228+
activeDraggables.splice(newIndex, 0, item);
211229
} else {
212230
this.element.appendChild(placeholder);
213-
this._activeDraggables.push(item);
231+
activeDraggables.push(item);
214232
}
215233

216234
// The transform needs to be cleared so it doesn't throw off the measurements.
@@ -321,8 +339,8 @@ export class DropListRef<T = any> {
321339
*/
322340
_sortItem(item: DragRef, pointerX: number, pointerY: number,
323341
pointerDelta: {x: number, y: number}): void {
324-
// Don't sort the item if it's out of range.
325-
if (!this._isPointerNearDropContainer(pointerX, pointerY)) {
342+
// Don't sort the item if sorting is disabled or it's out of range.
343+
if (this.sortingDisabled || !this._isPointerNearDropContainer(pointerX, pointerY)) {
326344
return;
327345
}
328346

tools/public_api_guard/cdk/drag-drop.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export declare class CdkDropList<T = any> implements CdkDropListContainer, After
128128
lockAxis: 'x' | 'y';
129129
orientation: 'horizontal' | 'vertical';
130130
sorted: EventEmitter<CdkDragSortEvent<T>>;
131+
sortingDisabled: boolean;
131132
constructor(
132133
element: ElementRef<HTMLElement>, dragDropRegistry: DragDropRegistry<DragRef, DropListRef>, _changeDetectorRef: ChangeDetectorRef, _dir?: Directionality | undefined, _group?: CdkDropListGroup<CdkDropList<any>> | undefined, _document?: any,
133134
dragDrop?: DragDrop);
@@ -296,6 +297,7 @@ export declare class DropListRef<T = any> {
296297
container: DropListRef<any>;
297298
item: DragRef;
298299
}>;
300+
sortingDisabled: boolean;
299301
constructor(element: ElementRef<HTMLElement> | HTMLElement, _dragDropRegistry: DragDropRegistry<DragRef, DropListRef>, _document: any);
300302
_canReceive(item: DragRef, x: number, y: number): boolean;
301303
_getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | undefined;

0 commit comments

Comments
 (0)