Skip to content

Commit 0bd93dd

Browse files
crisbetovivian-hu-zz
authored andcommittedJan 16, 2019
fix(drag-drop): unable to move item into connected container by passing through another container (#14651)
Fixes not being able to move an item from one container into another by passing it through an intermediate container that isn't connected to the final one. The issue comes from the fact that the way things are set up at the moment, the container from which the item started the sequence knows which containers it can go into, however all that knowledge is reset once the item enters into a different container. These changes rework the logic to have the individual containers know whether the item can enter into them and have the source container "ask" each of its siblings whether the item can enter. Fixes #14645.
1 parent 4913c36 commit 0bd93dd

File tree

3 files changed

+161
-103
lines changed

3 files changed

+161
-103
lines changed
 

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

+66
Original file line numberDiff line numberDiff line change
@@ -2736,6 +2736,63 @@ describe('CdkDrag', () => {
27362736
'Expected new container not to have the receiving class after entering.');
27372737
}));
27382738

2739+
it('should be able to move the item over an intermediate container before ' +
2740+
'dropping it into the final one', fakeAsync(() => {
2741+
const fixture = createComponent(ConnectedDropZones);
2742+
fixture.detectChanges();
2743+
2744+
const dropInstances = fixture.componentInstance.dropInstances.toArray();
2745+
dropInstances[0].connectedTo = [dropInstances[1], dropInstances[2]];
2746+
dropInstances[1].connectedTo = [];
2747+
dropInstances[2].connectedTo = [];
2748+
fixture.detectChanges();
2749+
2750+
const groups = fixture.componentInstance.groupedDragItems;
2751+
const dropZones = dropInstances.map(d => d.element.nativeElement);
2752+
const item = groups[0][1];
2753+
const intermediateRect = dropZones[1].getBoundingClientRect();
2754+
const finalRect = dropZones[2].getBoundingClientRect();
2755+
2756+
startDraggingViaMouse(fixture, item.element.nativeElement);
2757+
2758+
const placeholder = dropZones[0].querySelector('.cdk-drag-placeholder')!;
2759+
2760+
expect(placeholder).toBeTruthy();
2761+
expect(dropZones[0].contains(placeholder))
2762+
.toBe(true, 'Expected placeholder to be inside the first container.');
2763+
2764+
dispatchMouseEvent(document, 'mousemove',
2765+
intermediateRect.left + 1, intermediateRect.top + 1);
2766+
fixture.detectChanges();
2767+
2768+
expect(dropZones[1].contains(placeholder))
2769+
.toBe(true, 'Expected placeholder to be inside second container.');
2770+
2771+
dispatchMouseEvent(document, 'mousemove', finalRect.left + 1, finalRect.top + 1);
2772+
fixture.detectChanges();
2773+
2774+
expect(dropZones[2].contains(placeholder))
2775+
.toBe(true, 'Expected placeholder to be inside third container.');
2776+
2777+
dispatchMouseEvent(document, 'mouseup');
2778+
fixture.detectChanges();
2779+
flush();
2780+
fixture.detectChanges();
2781+
2782+
const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0];
2783+
2784+
expect(event).toBeTruthy();
2785+
expect(event).toEqual(jasmine.objectContaining({
2786+
previousIndex: 1,
2787+
currentIndex: 0,
2788+
item: groups[0][1],
2789+
container: dropInstances[2],
2790+
previousContainer: dropInstances[0],
2791+
isPointerOverContainer: false
2792+
}));
2793+
2794+
}));
2795+
27392796
});
27402797

27412798
});
@@ -3025,6 +3082,14 @@ class DraggableInDropZoneWithCustomPlaceholder {
30253082
(cdkDropListDropped)="droppedSpy($event)">
30263083
<div [cdkDragData]="item" *ngFor="let item of done" cdkDrag>{{item}}</div>
30273084
</div>
3085+
3086+
<div
3087+
cdkDropList
3088+
#extraZone="cdkDropList"
3089+
[cdkDropListData]="extra"
3090+
(cdkDropListDropped)="droppedSpy($event)">
3091+
<div [cdkDragData]="item" *ngFor="let item of extra" cdkDrag>{{item}}</div>
3092+
</div>
30283093
`
30293094
})
30303095
class ConnectedDropZones implements AfterViewInit {
@@ -3034,6 +3099,7 @@ class ConnectedDropZones implements AfterViewInit {
30343099
groupedDragItems: CdkDrag[][] = [];
30353100
todo = ['Zero', 'One', 'Two', 'Three'];
30363101
done = ['Four', 'Five', 'Six'];
3102+
extra = [];
30373103
droppedSpy = jasmine.createSpy('dropped spy');
30383104

30393105
ngAfterViewInit() {

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,8 @@ export class DragRef<T = any> {
704704
*/
705705
private _updateActiveDropContainer({x, y}: Point) {
706706
// Drop container that draggable has been moved into.
707-
let newContainer = this.dropContainer!._getSiblingContainerFromPosition(this, x, y);
707+
let newContainer = this.dropContainer!._getSiblingContainerFromPosition(this, x, y) ||
708+
this._initialContainer._getSiblingContainerFromPosition(this, x, y);
708709

709710
// If we couldn't find a new container to move the item into, and the item has left it's
710711
// initial container, check whether the it's over the initial container. This handles the
@@ -715,7 +716,7 @@ export class DragRef<T = any> {
715716
newContainer = this._initialContainer;
716717
}
717718

718-
if (newContainer) {
719+
if (newContainer && newContainer !== this.dropContainer) {
719720
this._ngZone.run(() => {
720721
// Notify the old container that the item has left.
721722
this.exited.next({item: this, container: this.dropContainer!});

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

+92-101
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,11 @@ let _uniqueIdCounter = 0;
2323
*/
2424
const DROP_PROXIMITY_THRESHOLD = 0.05;
2525

26-
/**
27-
* Object used to cache the position of a drag list, its items. and siblings.
28-
* @docs-private
29-
*/
30-
interface PositionCache {
31-
/** Cached positions of the items in the list. */
32-
items: ItemPositionCacheEntry[];
33-
/** Cached positions of the connected lists. */
34-
siblings: ListPositionCacheEntry[];
35-
/** Dimensions of the list itself. */
36-
self: ClientRect;
37-
}
38-
3926
/**
4027
* Entry in the position cache for draggable items.
4128
* @docs-private
4229
*/
43-
interface ItemPositionCacheEntry {
30+
interface CachedItemPosition {
4431
/** Instance of the drag item. */
4532
drag: DragRef;
4633
/** Dimensions of the item. */
@@ -49,17 +36,6 @@ interface ItemPositionCacheEntry {
4936
offset: number;
5037
}
5138

52-
/**
53-
* Entry in the position cache for drop lists.
54-
* @docs-private
55-
*/
56-
interface ListPositionCacheEntry {
57-
/** Instance of the drop list. */
58-
drop: DropListRef;
59-
/** Dimensions of the list. */
60-
clientRect: ClientRect;
61-
}
62-
6339
/**
6440
* Internal compile-time-only representation of a `DropListRef`.
6541
* Used to avoid circular import issues between the `DropListRef` and the `DragRef`.
@@ -131,8 +107,11 @@ export class DropListRef<T = any> {
131107
/** Whether an item in the list is being dragged. */
132108
private _isDragging = false;
133109

134-
/** Cache of the dimensions of all the items and the sibling containers. */
135-
private _positionCache: PositionCache = {items: [], siblings: [], self: {} as ClientRect};
110+
/** Cache of the dimensions of all the items inside the container. */
111+
private _itemPositions: CachedItemPosition[] = [];
112+
113+
/** Cached `ClientRect` of the drop list. */
114+
private _clientRect: ClientRect;
136115

137116
/**
138117
* Draggable items that are currently active inside the container. Includes the items
@@ -150,13 +129,14 @@ export class DropListRef<T = any> {
150129
/** Draggable items in the container. */
151130
private _draggables: DragRef[];
152131

132+
/** Drop lists that are connected to the current one. */
153133
private _siblings: DropListRef[] = [];
154134

155135
/** Direction in which the list is oriented. */
156136
private _orientation: 'horizontal' | 'vertical' = 'vertical';
157137

158-
/** Amount of connected siblings that currently have a dragged item. */
159-
private _activeSiblings = 0;
138+
/** Connected siblings that currently have a dragged item. */
139+
private _activeSiblings = new Set<DropListRef>();
160140

161141
constructor(
162142
public element: ElementRef<HTMLElement>,
@@ -174,6 +154,7 @@ export class DropListRef<T = any> {
174154
this.exited.complete();
175155
this.dropped.complete();
176156
this.sorted.complete();
157+
this._activeSiblings.clear();
177158
this._dragDropRegistry.removeDropContainer(this);
178159
}
179160

@@ -187,8 +168,9 @@ export class DropListRef<T = any> {
187168
this.beforeStarted.next();
188169
this._isDragging = true;
189170
this._activeDraggables = this._draggables.slice();
190-
this._cachePositions();
191-
this._positionCache.siblings.forEach(sibling => sibling.drop._toggleIsReceiving(true));
171+
this._cacheOwnPosition();
172+
this._cacheItemPositions();
173+
this._siblings.forEach(sibling => sibling._startReceiving(this));
192174
}
193175

194176
/**
@@ -230,7 +212,7 @@ export class DropListRef<T = any> {
230212

231213
// Note that the positions were already cached when we called `start` above,
232214
// but we need to refresh them since the amount of items has changed.
233-
this._cachePositions();
215+
this._cacheItemPositions();
234216
}
235217

236218
/**
@@ -304,7 +286,7 @@ export class DropListRef<T = any> {
304286
// The rest of the logic still stands no matter what orientation we're in, however
305287
// we need to invert the array when determining the index.
306288
const items = this._orientation === 'horizontal' && this._dir && this._dir.value === 'rtl' ?
307-
this._positionCache.items.slice().reverse() : this._positionCache.items;
289+
this._itemPositions.slice().reverse() : this._itemPositions;
308290

309291
return findIndex(items, currentItem => currentItem.drag === item);
310292
}
@@ -314,7 +296,7 @@ export class DropListRef<T = any> {
314296
* is currently being dragged inside a connected drop list.
315297
*/
316298
isReceiving(): boolean {
317-
return this._activeSiblings > 0;
299+
return this._activeSiblings.size > 0;
318300
}
319301

320302
/**
@@ -331,7 +313,7 @@ export class DropListRef<T = any> {
331313
return;
332314
}
333315

334-
const siblings = this._positionCache.items;
316+
const siblings = this._itemPositions;
335317
const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY, pointerDelta);
336318

337319
if (newIndex === -1 && siblings.length > 0) {
@@ -398,54 +380,43 @@ export class DropListRef<T = any> {
398380
});
399381
}
400382

383+
/** Caches the position of the drop list. */
384+
private _cacheOwnPosition() {
385+
this._clientRect = this.element.nativeElement.getBoundingClientRect();
386+
}
387+
401388
/** Refreshes the position cache of the items and sibling containers. */
402-
private _cachePositions() {
389+
private _cacheItemPositions() {
403390
const isHorizontal = this._orientation === 'horizontal';
404391

405-
this._positionCache.self = this.element.nativeElement.getBoundingClientRect();
406-
this._positionCache.items = this._activeDraggables
407-
.map(drag => {
408-
const elementToMeasure = this._dragDropRegistry.isDragging(drag) ?
409-
// If the element is being dragged, we have to measure the
410-
// placeholder, because the element is hidden.
411-
drag.getPlaceholderElement() :
412-
drag.getRootElement();
413-
const clientRect = elementToMeasure.getBoundingClientRect();
414-
415-
return {
416-
drag,
417-
offset: 0,
418-
// We need to clone the `clientRect` here, because all the values on it are readonly
419-
// and we need to be able to update them. Also we can't use a spread here, because
420-
// the values on a `ClientRect` aren't own properties. See:
421-
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes
422-
clientRect: {
423-
top: clientRect.top,
424-
right: clientRect.right,
425-
bottom: clientRect.bottom,
426-
left: clientRect.left,
427-
width: clientRect.width,
428-
height: clientRect.height
429-
}
430-
};
431-
})
432-
.sort((a, b) => {
433-
return isHorizontal ? a.clientRect.left - b.clientRect.left :
434-
a.clientRect.top - b.clientRect.top;
435-
});
436-
437-
this._positionCache.siblings = this._siblings.map(drop => ({
438-
drop,
439-
clientRect: drop.element.nativeElement.getBoundingClientRect()
440-
}));
441-
}
442-
443-
/**
444-
* Toggles whether the list can receive the item that is currently being dragged.
445-
* Usually called by a sibling that initiated the dragging.
446-
*/
447-
_toggleIsReceiving(isDragging: boolean) {
448-
this._activeSiblings = Math.max(0, this._activeSiblings + (isDragging ? 1 : -1));
392+
this._itemPositions = this._activeDraggables.map(drag => {
393+
const elementToMeasure = this._dragDropRegistry.isDragging(drag) ?
394+
// If the element is being dragged, we have to measure the
395+
// placeholder, because the element is hidden.
396+
drag.getPlaceholderElement() :
397+
drag.getRootElement();
398+
const clientRect = elementToMeasure.getBoundingClientRect();
399+
400+
return {
401+
drag,
402+
offset: 0,
403+
// We need to clone the `clientRect` here, because all the values on it are readonly
404+
// and we need to be able to update them. Also we can't use a spread here, because
405+
// the values on a `ClientRect` aren't own properties. See:
406+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes
407+
clientRect: {
408+
top: clientRect.top,
409+
right: clientRect.right,
410+
bottom: clientRect.bottom,
411+
left: clientRect.left,
412+
width: clientRect.width,
413+
height: clientRect.height
414+
}
415+
};
416+
}).sort((a, b) => {
417+
return isHorizontal ? a.clientRect.left - b.clientRect.left :
418+
a.clientRect.top - b.clientRect.top;
419+
});
449420
}
450421

451422
/** Resets the container to its initial state. */
@@ -454,10 +425,9 @@ export class DropListRef<T = any> {
454425

455426
// TODO(crisbeto): may have to wait for the animations to finish.
456427
this._activeDraggables.forEach(item => item.getRootElement().style.transform = '');
457-
this._positionCache.siblings.forEach(sibling => sibling.drop._toggleIsReceiving(false));
428+
this._siblings.forEach(sibling => sibling._stopReceiving(this));
458429
this._activeDraggables = [];
459-
this._positionCache.items = [];
460-
this._positionCache.siblings = [];
430+
this._itemPositions = [];
461431
this._previousSwap.drag = null;
462432
this._previousSwap.delta = 0;
463433
}
@@ -469,7 +439,7 @@ export class DropListRef<T = any> {
469439
* @param delta Direction in which the user is moving.
470440
*/
471441
private _getSiblingOffsetPx(currentIndex: number,
472-
siblings: ItemPositionCacheEntry[],
442+
siblings: CachedItemPosition[],
473443
delta: 1 | -1) {
474444

475445
const isHorizontal = this._orientation === 'horizontal';
@@ -501,7 +471,7 @@ export class DropListRef<T = any> {
501471
* @param pointerY Coordinates along the Y axis.
502472
*/
503473
private _isPointerNearDropContainer(pointerX: number, pointerY: number): boolean {
504-
const {top, right, bottom, left, width, height} = this._positionCache.self;
474+
const {top, right, bottom, left, width, height} = this._clientRect;
505475
const xThreshold = width * DROP_PROXIMITY_THRESHOLD;
506476
const yThreshold = height * DROP_PROXIMITY_THRESHOLD;
507477

@@ -538,10 +508,9 @@ export class DropListRef<T = any> {
538508
*/
539509
private _getItemIndexFromPointerPosition(item: DragRef, pointerX: number, pointerY: number,
540510
delta?: {x: number, y: number}) {
541-
542511
const isHorizontal = this._orientation === 'horizontal';
543512

544-
return findIndex(this._positionCache.items, ({drag, clientRect}, _, array) => {
513+
return findIndex(this._itemPositions, ({drag, clientRect}, _, array) => {
545514
if (drag === item) {
546515
// If there's only one item left in the container, it must be
547516
// the dragged item itself so we use it as a reference.
@@ -572,7 +541,7 @@ export class DropListRef<T = any> {
572541
* @param y Pointer position along the Y axis.
573542
*/
574543
_isOverContainer(x: number, y: number): boolean {
575-
return isInsideClientRect(this._positionCache.self, x, y);
544+
return isInsideClientRect(this._clientRect, x, y);
576545
}
577546

578547
/**
@@ -582,38 +551,60 @@ export class DropListRef<T = any> {
582551
* @param x Position of the item along the X axis.
583552
* @param y Position of the item along the Y axis.
584553
*/
585-
_getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | null {
586-
const results = this._positionCache.siblings.filter(sibling => {
587-
return isInsideClientRect(sibling.clientRect, x, y);
588-
});
554+
_getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | undefined {
555+
return this._siblings.find(sibling => sibling._canReceive(item, x, y));
556+
}
589557

590-
// No drop containers are intersecting with the pointer.
591-
if (!results.length) {
592-
return null;
558+
/**
559+
* Checks whether the drop list can receive the passed-in item.
560+
* @param item Item that is being dragged into the list.
561+
* @param x Position of the item along the X axis.
562+
* @param y Position of the item along the Y axis.
563+
*/
564+
_canReceive(item: DragRef, x: number, y: number): boolean {
565+
if (!this.enterPredicate(item, this) || !isInsideClientRect(this._clientRect, x, y)) {
566+
return false;
593567
}
594568

595569
const elementFromPoint = this._document.elementFromPoint(x, y);
596570

597571
// If there's no element at the pointer position, then
598572
// the client rect is probably scrolled out of the view.
599573
if (!elementFromPoint) {
600-
return null;
574+
return false;
601575
}
602576

577+
const element = this.element.nativeElement;
578+
603579
// The `ClientRect`, that we're using to find the container over which the user is
604580
// hovering, doesn't give us any information on whether the element has been scrolled
605581
// out of the view or whether it's overlapping with other containers. This means that
606582
// we could end up transferring the item into a container that's invisible or is positioned
607583
// below another one. We use the result from `elementFromPoint` to get the top-most element
608584
// at the pointer position and to find whether it's one of the intersecting drop containers.
609-
const result = results.find(sibling => {
610-
const element = sibling.drop.element.nativeElement;
611-
return element === elementFromPoint || element.contains(elementFromPoint);
612-
});
585+
return elementFromPoint === element || element.contains(elementFromPoint);
586+
}
587+
588+
/**
589+
* Called by one of the connected drop lists when a dragging sequence has started.
590+
* @param sibling Sibling in which dragging has started.
591+
*/
592+
_startReceiving(sibling: DropListRef) {
593+
const activeSiblings = this._activeSiblings;
613594

614-
return result && result.drop.enterPredicate(item, result.drop) ? result.drop : null;
595+
if (!activeSiblings.has(sibling)) {
596+
activeSiblings.add(sibling);
597+
this._cacheOwnPosition();
598+
}
615599
}
616600

601+
/**
602+
* Called by a connected drop list when dragging has stopped.
603+
* @param sibling Sibling whose dragging has stopped.
604+
*/
605+
_stopReceiving(sibling: DropListRef) {
606+
this._activeSiblings.delete(sibling);
607+
}
617608
}
618609

619610

0 commit comments

Comments
 (0)
Please sign in to comment.