diff --git a/src/cdk/drag-drop/directives/drag.spec.ts b/src/cdk/drag-drop/directives/drag.spec.ts index 0264940f31a..1eed650c6ec 100644 --- a/src/cdk/drag-drop/directives/drag.spec.ts +++ b/src/cdk/drag-drop/directives/drag.spec.ts @@ -2736,6 +2736,63 @@ describe('CdkDrag', () => { 'Expected new container not to have the receiving class after entering.'); })); + it('should be able to move the item over an intermediate container before ' + + 'dropping it into the final one', fakeAsync(() => { + const fixture = createComponent(ConnectedDropZones); + fixture.detectChanges(); + + const dropInstances = fixture.componentInstance.dropInstances.toArray(); + dropInstances[0].connectedTo = [dropInstances[1], dropInstances[2]]; + dropInstances[1].connectedTo = []; + dropInstances[2].connectedTo = []; + fixture.detectChanges(); + + const groups = fixture.componentInstance.groupedDragItems; + const dropZones = dropInstances.map(d => d.element.nativeElement); + const item = groups[0][1]; + const intermediateRect = dropZones[1].getBoundingClientRect(); + const finalRect = dropZones[2].getBoundingClientRect(); + + 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.'); + + dispatchMouseEvent(document, 'mousemove', + intermediateRect.left + 1, intermediateRect.top + 1); + fixture.detectChanges(); + + expect(dropZones[1].contains(placeholder)) + .toBe(true, 'Expected placeholder to be inside second container.'); + + dispatchMouseEvent(document, 'mousemove', finalRect.left + 1, finalRect.top + 1); + fixture.detectChanges(); + + expect(dropZones[2].contains(placeholder)) + .toBe(true, 'Expected placeholder to be inside third container.'); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + expect(event).toBeTruthy(); + expect(event).toEqual(jasmine.objectContaining({ + previousIndex: 1, + currentIndex: 0, + item: groups[0][1], + container: dropInstances[2], + previousContainer: dropInstances[0], + isPointerOverContainer: false + })); + + })); + }); }); @@ -3025,6 +3082,14 @@ class DraggableInDropZoneWithCustomPlaceholder { (cdkDropListDropped)="droppedSpy($event)">
{{item}}
+ +
+
{{item}}
+
` }) class ConnectedDropZones implements AfterViewInit { @@ -3034,6 +3099,7 @@ class ConnectedDropZones implements AfterViewInit { groupedDragItems: CdkDrag[][] = []; todo = ['Zero', 'One', 'Two', 'Three']; done = ['Four', 'Five', 'Six']; + extra = []; droppedSpy = jasmine.createSpy('dropped spy'); ngAfterViewInit() { diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index 91de75788a1..aeef2faad6a 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -704,7 +704,8 @@ export class DragRef { */ private _updateActiveDropContainer({x, y}: Point) { // Drop container that draggable has been moved into. - let newContainer = this.dropContainer!._getSiblingContainerFromPosition(this, x, y); + let newContainer = this.dropContainer!._getSiblingContainerFromPosition(this, x, y) || + this._initialContainer._getSiblingContainerFromPosition(this, x, y); // If we couldn't find a new container to move the item into, and the item has left it's // initial container, check whether the it's over the initial container. This handles the @@ -715,7 +716,7 @@ export class DragRef { newContainer = this._initialContainer; } - if (newContainer) { + if (newContainer && newContainer !== this.dropContainer) { this._ngZone.run(() => { // Notify the old container that the item has left. this.exited.next({item: this, container: this.dropContainer!}); diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index 66cdd9c4168..cdafa2c78bd 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -23,24 +23,11 @@ let _uniqueIdCounter = 0; */ const DROP_PROXIMITY_THRESHOLD = 0.05; -/** - * Object used to cache the position of a drag list, its items. and siblings. - * @docs-private - */ -interface PositionCache { - /** Cached positions of the items in the list. */ - items: ItemPositionCacheEntry[]; - /** Cached positions of the connected lists. */ - siblings: ListPositionCacheEntry[]; - /** Dimensions of the list itself. */ - self: ClientRect; -} - /** * Entry in the position cache for draggable items. * @docs-private */ -interface ItemPositionCacheEntry { +interface CachedItemPosition { /** Instance of the drag item. */ drag: DragRef; /** Dimensions of the item. */ @@ -49,17 +36,6 @@ interface ItemPositionCacheEntry { offset: number; } -/** - * Entry in the position cache for drop lists. - * @docs-private - */ -interface ListPositionCacheEntry { - /** Instance of the drop list. */ - drop: DropListRef; - /** Dimensions of the list. */ - clientRect: ClientRect; -} - /** * Internal compile-time-only representation of a `DropListRef`. * Used to avoid circular import issues between the `DropListRef` and the `DragRef`. @@ -131,8 +107,11 @@ export class DropListRef { /** Whether an item in the list is being dragged. */ private _isDragging = false; - /** Cache of the dimensions of all the items and the sibling containers. */ - private _positionCache: PositionCache = {items: [], siblings: [], self: {} as ClientRect}; + /** Cache of the dimensions of all the items inside the container. */ + private _itemPositions: CachedItemPosition[] = []; + + /** Cached `ClientRect` of the drop list. */ + private _clientRect: ClientRect; /** * Draggable items that are currently active inside the container. Includes the items @@ -150,13 +129,14 @@ export class DropListRef { /** Draggable items in the container. */ private _draggables: DragRef[]; + /** Drop lists that are connected to the current one. */ private _siblings: DropListRef[] = []; /** Direction in which the list is oriented. */ private _orientation: 'horizontal' | 'vertical' = 'vertical'; - /** Amount of connected siblings that currently have a dragged item. */ - private _activeSiblings = 0; + /** Connected siblings that currently have a dragged item. */ + private _activeSiblings = new Set(); constructor( public element: ElementRef, @@ -174,6 +154,7 @@ export class DropListRef { this.exited.complete(); this.dropped.complete(); this.sorted.complete(); + this._activeSiblings.clear(); this._dragDropRegistry.removeDropContainer(this); } @@ -187,8 +168,9 @@ export class DropListRef { this.beforeStarted.next(); this._isDragging = true; this._activeDraggables = this._draggables.slice(); - this._cachePositions(); - this._positionCache.siblings.forEach(sibling => sibling.drop._toggleIsReceiving(true)); + this._cacheOwnPosition(); + this._cacheItemPositions(); + this._siblings.forEach(sibling => sibling._startReceiving(this)); } /** @@ -230,7 +212,7 @@ export class DropListRef { // Note that the positions were already cached when we called `start` above, // but we need to refresh them since the amount of items has changed. - this._cachePositions(); + this._cacheItemPositions(); } /** @@ -304,7 +286,7 @@ export class DropListRef { // The rest of the logic still stands no matter what orientation we're in, however // we need to invert the array when determining the index. const items = this._orientation === 'horizontal' && this._dir && this._dir.value === 'rtl' ? - this._positionCache.items.slice().reverse() : this._positionCache.items; + this._itemPositions.slice().reverse() : this._itemPositions; return findIndex(items, currentItem => currentItem.drag === item); } @@ -314,7 +296,7 @@ export class DropListRef { * is currently being dragged inside a connected drop list. */ isReceiving(): boolean { - return this._activeSiblings > 0; + return this._activeSiblings.size > 0; } /** @@ -331,7 +313,7 @@ export class DropListRef { return; } - const siblings = this._positionCache.items; + const siblings = this._itemPositions; const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY, pointerDelta); if (newIndex === -1 && siblings.length > 0) { @@ -398,54 +380,43 @@ export class DropListRef { }); } - /** Refreshes the position cache of the items and sibling containers. */ - private _cachePositions() { - const isHorizontal = this._orientation === 'horizontal'; - - this._positionCache.self = this.element.nativeElement.getBoundingClientRect(); - this._positionCache.items = this._activeDraggables - .map(drag => { - const elementToMeasure = this._dragDropRegistry.isDragging(drag) ? - // If the element is being dragged, we have to measure the - // placeholder, because the element is hidden. - drag.getPlaceholderElement() : - drag.getRootElement(); - const clientRect = elementToMeasure.getBoundingClientRect(); - - return { - drag, - offset: 0, - // We need to clone the `clientRect` here, because all the values on it are readonly - // and we need to be able to update them. Also we can't use a spread here, because - // the values on a `ClientRect` aren't own properties. See: - // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes - clientRect: { - top: clientRect.top, - right: clientRect.right, - bottom: clientRect.bottom, - left: clientRect.left, - width: clientRect.width, - height: clientRect.height - } - }; - }) - .sort((a, b) => { - return isHorizontal ? a.clientRect.left - b.clientRect.left : - a.clientRect.top - b.clientRect.top; - }); - - this._positionCache.siblings = this._siblings.map(drop => ({ - drop, - clientRect: drop.element.nativeElement.getBoundingClientRect() - })); + /** Caches the position of the drop list. */ + private _cacheOwnPosition() { + this._clientRect = this.element.nativeElement.getBoundingClientRect(); } - /** - * Toggles whether the list can receive the item that is currently being dragged. - * Usually called by a sibling that initiated the dragging. - */ - _toggleIsReceiving(isDragging: boolean) { - this._activeSiblings = Math.max(0, this._activeSiblings + (isDragging ? 1 : -1)); + /** Refreshes the position cache of the items and sibling containers. */ + private _cacheItemPositions() { + const isHorizontal = this._orientation === 'horizontal'; + + this._itemPositions = this._activeDraggables.map(drag => { + const elementToMeasure = this._dragDropRegistry.isDragging(drag) ? + // If the element is being dragged, we have to measure the + // placeholder, because the element is hidden. + drag.getPlaceholderElement() : + drag.getRootElement(); + const clientRect = elementToMeasure.getBoundingClientRect(); + + return { + drag, + offset: 0, + // We need to clone the `clientRect` here, because all the values on it are readonly + // and we need to be able to update them. Also we can't use a spread here, because + // the values on a `ClientRect` aren't own properties. See: + // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect#Notes + clientRect: { + top: clientRect.top, + right: clientRect.right, + bottom: clientRect.bottom, + left: clientRect.left, + width: clientRect.width, + height: clientRect.height + } + }; + }).sort((a, b) => { + return isHorizontal ? a.clientRect.left - b.clientRect.left : + a.clientRect.top - b.clientRect.top; + }); } /** Resets the container to its initial state. */ @@ -454,10 +425,9 @@ export class DropListRef { // TODO(crisbeto): may have to wait for the animations to finish. this._activeDraggables.forEach(item => item.getRootElement().style.transform = ''); - this._positionCache.siblings.forEach(sibling => sibling.drop._toggleIsReceiving(false)); + this._siblings.forEach(sibling => sibling._stopReceiving(this)); this._activeDraggables = []; - this._positionCache.items = []; - this._positionCache.siblings = []; + this._itemPositions = []; this._previousSwap.drag = null; this._previousSwap.delta = 0; } @@ -469,7 +439,7 @@ export class DropListRef { * @param delta Direction in which the user is moving. */ private _getSiblingOffsetPx(currentIndex: number, - siblings: ItemPositionCacheEntry[], + siblings: CachedItemPosition[], delta: 1 | -1) { const isHorizontal = this._orientation === 'horizontal'; @@ -501,7 +471,7 @@ export class DropListRef { * @param pointerY Coordinates along the Y axis. */ private _isPointerNearDropContainer(pointerX: number, pointerY: number): boolean { - const {top, right, bottom, left, width, height} = this._positionCache.self; + const {top, right, bottom, left, width, height} = this._clientRect; const xThreshold = width * DROP_PROXIMITY_THRESHOLD; const yThreshold = height * DROP_PROXIMITY_THRESHOLD; @@ -538,10 +508,9 @@ export class DropListRef { */ private _getItemIndexFromPointerPosition(item: DragRef, pointerX: number, pointerY: number, delta?: {x: number, y: number}) { - const isHorizontal = this._orientation === 'horizontal'; - return findIndex(this._positionCache.items, ({drag, clientRect}, _, array) => { + return findIndex(this._itemPositions, ({drag, clientRect}, _, array) => { if (drag === item) { // If there's only one item left in the container, it must be // the dragged item itself so we use it as a reference. @@ -572,7 +541,7 @@ export class DropListRef { * @param y Pointer position along the Y axis. */ _isOverContainer(x: number, y: number): boolean { - return isInsideClientRect(this._positionCache.self, x, y); + return isInsideClientRect(this._clientRect, x, y); } /** @@ -582,14 +551,19 @@ export class DropListRef { * @param x Position of the item along the X axis. * @param y Position of the item along the Y axis. */ - _getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | null { - const results = this._positionCache.siblings.filter(sibling => { - return isInsideClientRect(sibling.clientRect, x, y); - }); + _getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | undefined { + return this._siblings.find(sibling => sibling._canReceive(item, x, y)); + } - // No drop containers are intersecting with the pointer. - if (!results.length) { - return null; + /** + * Checks whether the drop list can receive the passed-in item. + * @param item Item that is being dragged into the list. + * @param x Position of the item along the X axis. + * @param y Position of the item along the Y axis. + */ + _canReceive(item: DragRef, x: number, y: number): boolean { + if (!this.enterPredicate(item, this) || !isInsideClientRect(this._clientRect, x, y)) { + return false; } const elementFromPoint = this._document.elementFromPoint(x, y); @@ -597,23 +571,40 @@ export class DropListRef { // If there's no element at the pointer position, then // the client rect is probably scrolled out of the view. if (!elementFromPoint) { - return null; + return false; } + const element = this.element.nativeElement; + // The `ClientRect`, that we're using to find the container over which the user is // hovering, doesn't give us any information on whether the element has been scrolled // out of the view or whether it's overlapping with other containers. This means that // we could end up transferring the item into a container that's invisible or is positioned // below another one. We use the result from `elementFromPoint` to get the top-most element // at the pointer position and to find whether it's one of the intersecting drop containers. - const result = results.find(sibling => { - const element = sibling.drop.element.nativeElement; - return element === elementFromPoint || element.contains(elementFromPoint); - }); - - return result && result.drop.enterPredicate(item, result.drop) ? result.drop : null; + return elementFromPoint === element || element.contains(elementFromPoint); } + /** + * Called by one of the connected drop lists when a dragging sequence has started. + * @param sibling Sibling in which dragging has started. + */ + _startReceiving(sibling: DropListRef) { + const activeSiblings = this._activeSiblings; + + if (!activeSiblings.has(sibling)) { + activeSiblings.add(sibling); + this._cacheOwnPosition(); + } + } + + /** + * Called by a connected drop list when dragging has stopped. + * @param sibling Sibling whose dragging has stopped. + */ + _stopReceiving(sibling: DropListRef) { + this._activeSiblings.delete(sibling); + } }