Skip to content

Commit 35fb68b

Browse files
crisbetojelbourn
authored andcommitted
fix(drag-drop): incorrectly displaying items if user skips multiple positions while sorting (#12739)
The current logic for moving items around while sorting works by swapping the sorted item with the one above/below it. This works for most cases since the user is usually moving down one-by-one, however it breaks down if they move down quickly, causing the element to skip multiple positions. In this case the element would swap its position with the end item, rather than shifting an entire slice of the list. These changes rework the logic so it can handle these location skips correctly. It's also a prerequisite to being able to limit how far from the element sorting should work.
1 parent 7e37280 commit 35fb68b

File tree

2 files changed

+110
-40
lines changed

2 files changed

+110
-40
lines changed

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

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,60 @@ describe('CdkDrag', () => {
675675
flush();
676676
}));
677677

678+
it('should lay out the elements correctly, if an element skips multiple positions when ' +
679+
'sorting vertically', fakeAsync(() => {
680+
const fixture = createComponent(DraggableInDropZone);
681+
fixture.detectChanges();
682+
683+
const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement);
684+
const draggedItem = items[0];
685+
const {top, left} = draggedItem.getBoundingClientRect();
686+
687+
dispatchMouseEvent(draggedItem, 'mousedown', left, top);
688+
fixture.detectChanges();
689+
690+
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
691+
const targetRect = items[items.length - 1].getBoundingClientRect();
692+
693+
// Add a few pixels to the top offset so we get some overlap.
694+
dispatchMouseEvent(document, 'mousemove', targetRect.left, targetRect.top + 5);
695+
fixture.detectChanges();
696+
697+
expect(getElementSibligsByPosition(placeholder, 'top').map(e => e.textContent!.trim()))
698+
.toEqual(['One', 'Two', 'Three', 'Zero']);
699+
700+
dispatchMouseEvent(document, 'mouseup');
701+
fixture.detectChanges();
702+
flush();
703+
}));
704+
705+
it('should lay out the elements correctly, if an element skips multiple positions when ' +
706+
'sorting horizontally', fakeAsync(() => {
707+
const fixture = createComponent(DraggableInHorizontalDropZone);
708+
fixture.detectChanges();
709+
710+
const items = fixture.componentInstance.dragItems.map(i => i.element.nativeElement);
711+
const draggedItem = items[0];
712+
const {top, left} = draggedItem.getBoundingClientRect();
713+
714+
dispatchMouseEvent(draggedItem, 'mousedown', left, top);
715+
fixture.detectChanges();
716+
717+
const placeholder = document.querySelector('.cdk-drag-placeholder')! as HTMLElement;
718+
const targetRect = items[items.length - 1].getBoundingClientRect();
719+
720+
// Add a few pixels to the left offset so we get some overlap.
721+
dispatchMouseEvent(document, 'mousemove', targetRect.right - 5, targetRect.top);
722+
fixture.detectChanges();
723+
724+
expect(getElementSibligsByPosition(placeholder, 'left').map(e => e.textContent!.trim()))
725+
.toEqual(['One', 'Two', 'Three', 'Zero']);
726+
727+
dispatchMouseEvent(document, 'mouseup');
728+
fixture.detectChanges();
729+
flush();
730+
}));
731+
678732
it('should clean up the preview element if the item is destroyed mid-drag', fakeAsync(() => {
679733
const fixture = createComponent(DraggableInDropZone);
680734
fixture.detectChanges();
@@ -1343,13 +1397,14 @@ function dragElementViaTouch(fixture: ComponentFixture<any>,
13431397

13441398
/** Gets the index of an element among its siblings, based on their position on the page. */
13451399
function getElementIndexByPosition(element: HTMLElement, direction: 'top' | 'left') {
1346-
if (!element.parentElement) {
1347-
return -1;
1348-
}
1400+
return getElementSibligsByPosition(element, direction).indexOf(element);
1401+
}
13491402

1350-
return Array.from(element.parentElement.children)
1351-
.sort((a, b) => a.getBoundingClientRect()[direction] - b.getBoundingClientRect()[direction])
1352-
.indexOf(element);
1403+
/** Gets the siblings of an element, sorted by their position on the page. */
1404+
function getElementSibligsByPosition(element: HTMLElement, direction: 'top' | 'left') {
1405+
return element.parentElement ? Array.from(element.parentElement.children).sort((a, b) => {
1406+
return a.getBoundingClientRect()[direction] - b.getBoundingClientRect()[direction];
1407+
}) : [];
13531408
}
13541409

13551410
/**

src/cdk-experimental/drag-drop/drop.ts

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {CdkDrag} from './drag';
2424
import {CdkDragExit, CdkDragEnter, CdkDragDrop} from './drag-events';
2525
import {CDK_DROP_CONTAINER} from './drop-container';
2626
import {DragDropRegistry} from './drag-drop-registry';
27+
import {moveItemInArray} from './drag-utils';
2728

2829
/** Counter used to generate unique ids for drop zones. */
2930
let _uniqueIdCounter = 0;
@@ -214,45 +215,59 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
214215
*/
215216
_sortItem(item: CdkDrag, xOffset: number, yOffset: number): void {
216217
const siblings = this._positionCache.items;
217-
const isHorizontal = this.orientation === 'horizontal';
218218
const newIndex = this._getItemIndexFromPointerPosition(item, xOffset, yOffset);
219-
const placeholder = item.getPlaceholderElement();
220219

221220
if (newIndex === -1 && siblings.length > 0) {
222221
return;
223222
}
224223

224+
const isHorizontal = this.orientation === 'horizontal';
225225
const currentIndex = siblings.findIndex(currentItem => currentItem.drag === item);
226-
const currentPosition = siblings[currentIndex];
227-
const newPosition = siblings[newIndex];
228-
229-
// Figure out the offset necessary for the items to be swapped.
230-
const offset = isHorizontal ?
231-
currentPosition.clientRect.left - newPosition.clientRect.left :
232-
currentPosition.clientRect.top - newPosition.clientRect.top;
233-
const topAdjustment = isHorizontal ? 0 : offset;
234-
const leftAdjustment = isHorizontal ? offset : 0;
235-
236-
// Since we've moved the items with a `transform`, we need to adjust their cached
237-
// client rects to reflect their new position, as well as swap their positions in the cache.
238-
// Note that we shouldn't use `getBoundingClientRect` here to update the cache, because the
239-
// elements may be mid-animation which will give us a wrong result.
240-
this._adjustClientRect(currentPosition.clientRect, -topAdjustment, -leftAdjustment);
241-
currentPosition.offset -= offset;
242-
siblings[currentIndex] = newPosition;
243-
244-
this._adjustClientRect(newPosition.clientRect, topAdjustment, leftAdjustment);
245-
newPosition.offset += offset;
246-
siblings[newIndex] = currentPosition;
247-
248-
// Swap the placeholder's position with the one of the target draggable.
249-
placeholder.style.transform = isHorizontal ?
250-
`translate3d(${currentPosition.offset}px, 0, 0)` :
251-
`translate3d(0, ${currentPosition.offset}px, 0)`;
252-
253-
newPosition.drag.element.nativeElement.style.transform = isHorizontal ?
254-
`translate3d(${newPosition.offset}px, 0, 0)` :
255-
`translate3d(0, ${newPosition.offset}px, 0)`;
226+
const currentPosition = siblings[currentIndex].clientRect;
227+
const newPosition = siblings[newIndex].clientRect;
228+
const delta = currentIndex > newIndex ? 1 : -1;
229+
230+
// How many pixels the item's placeholder should be offset.
231+
const itemOffset = isHorizontal ? newPosition.left - currentPosition.left :
232+
newPosition.top - currentPosition.top;
233+
234+
// How many pixels all the other items should be offset.
235+
const siblingOffset = isHorizontal ? currentPosition.width * delta :
236+
currentPosition.height * delta;
237+
238+
// Save the previous order of the items before moving the item to its new index.
239+
// We use this to check whether an item has been moved as a result of the sorting.
240+
const oldOrder = siblings.slice();
241+
242+
// Shuffle the array in place.
243+
moveItemInArray(siblings, currentIndex, newIndex);
244+
245+
siblings.forEach((sibling, index) => {
246+
// Don't do anything if the position hasn't changed.
247+
if (oldOrder[index] === sibling) {
248+
return;
249+
}
250+
251+
const isDraggedItem = sibling.drag === item;
252+
const offset = isDraggedItem ? itemOffset : siblingOffset;
253+
const elementToOffset = isDraggedItem ? item.getPlaceholderElement() :
254+
sibling.drag.element.nativeElement;
255+
256+
// Update the offset to reflect the new position.
257+
sibling.offset += offset;
258+
259+
// Since we're moving the items with a `transform`, we need to adjust their cached
260+
// client rects to reflect their new position, as well as swap their positions in the cache.
261+
// Note that we shouldn't use `getBoundingClientRect` here to update the cache, because the
262+
// elements may be mid-animation which will give us a wrong result.
263+
if (isHorizontal) {
264+
elementToOffset.style.transform = `translate3d(${sibling.offset}px, 0, 0)`;
265+
this._adjustClientRect(sibling.clientRect, 0, offset);
266+
} else {
267+
elementToOffset.style.transform = `translate3d(0, ${sibling.offset}px, 0)`;
268+
this._adjustClientRect(sibling.clientRect, offset, 0);
269+
}
270+
});
256271
}
257272

258273
/**
@@ -321,8 +336,8 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
321336
/**
322337
* Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts.
323338
* @param clientRect `ClientRect` that should be updated.
324-
* @param top New value for the `top` position.
325-
* @param left New value for the `left` position.
339+
* @param top Amount to add to the `top` position.
340+
* @param left Amount to add to the `left` position.
326341
*/
327342
private _adjustClientRect(clientRect: ClientRect, top: number, left: number) {
328343
clientRect.top += top;

0 commit comments

Comments
 (0)