mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(drag-drop): add support for sorting animations (#12530)
This commit is contained in:
committed by
Joey Perrott
parent
8c08bb1977
commit
adcac367bf
@@ -562,7 +562,7 @@ describe('CdkDrag', () => {
|
||||
// Add a few pixels to the left offset so we get some overlap.
|
||||
dispatchMouseEvent(document, 'mousemove', elementRect.left + 5, elementRect.top);
|
||||
fixture.detectChanges();
|
||||
expect(getElementIndex(placeholder)).toBe(i);
|
||||
expect(getElementIndexByPosition(placeholder, 'left')).toBe(i);
|
||||
}
|
||||
|
||||
dispatchMouseEvent(document, 'mouseup');
|
||||
@@ -590,7 +590,7 @@ describe('CdkDrag', () => {
|
||||
// Remove a few pixels from the right offset so we get some overlap.
|
||||
dispatchMouseEvent(document, 'mousemove', elementRect.right - 5, elementRect.top);
|
||||
fixture.detectChanges();
|
||||
expect(getElementIndex(placeholder)).toBe(Math.min(i + 1, items.length - 1));
|
||||
expect(getElementIndexByPosition(placeholder, 'left')).toBe(i);
|
||||
}
|
||||
|
||||
dispatchMouseEvent(document, 'mouseup');
|
||||
@@ -660,6 +660,31 @@ describe('CdkDrag', () => {
|
||||
expect(placeholder.textContent!.trim()).toContain('Custom placeholder');
|
||||
}));
|
||||
|
||||
it('should clear the `transform` value from siblings when item is dropped`', fakeAsync(() => {
|
||||
const fixture = createComponent(DraggableInDropZone);
|
||||
fixture.detectChanges();
|
||||
|
||||
const dragItems = fixture.componentInstance.dragItems;
|
||||
const firstItem = dragItems.first;
|
||||
const thirdItem = dragItems.toArray()[2].element.nativeElement;
|
||||
const thirdItemRect = thirdItem.getBoundingClientRect();
|
||||
|
||||
dispatchMouseEvent(firstItem.element.nativeElement, 'mousedown');
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchMouseEvent(document, 'mousemove', thirdItemRect.left + 1, thirdItemRect.top + 1);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(thirdItem.style.transform).toBeTruthy();
|
||||
|
||||
dispatchMouseEvent(document, 'mouseup');
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(thirdItem.style.transform).toBeFalsy();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('in a connected drop container', () => {
|
||||
@@ -1151,9 +1176,15 @@ function dragElementViaTouch(fixture: ComponentFixture<any>,
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
/** Gets the index of a DOM element inside its parent. */
|
||||
function getElementIndex(element: HTMLElement) {
|
||||
return element.parentElement ? Array.from(element.parentElement.children).indexOf(element) : -1;
|
||||
/** Gets the index of an element among its siblings, based on their position on the page. */
|
||||
function getElementIndexByPosition(element: HTMLElement, direction: 'top' | 'left') {
|
||||
if (!element.parentElement) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return Array.from(element.parentElement.children)
|
||||
.sort((a, b) => a.getBoundingClientRect()[direction] - b.getBoundingClientRect()[direction])
|
||||
.indexOf(element);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1193,7 +1224,7 @@ function assertDownwardSorting(fixture: ComponentFixture<any>, items: Element[])
|
||||
// Add a few pixels to the top offset so we get some overlap.
|
||||
dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.top + 5);
|
||||
fixture.detectChanges();
|
||||
expect(getElementIndex(placeholder)).toBe(i);
|
||||
expect(getElementIndexByPosition(placeholder, 'top')).toBe(i);
|
||||
}
|
||||
|
||||
dispatchMouseEvent(document, 'mouseup');
|
||||
@@ -1222,7 +1253,7 @@ function assertUpwardSorting(fixture: ComponentFixture<any>, items: Element[]) {
|
||||
// Remove a few pixels from the bottom offset so we get some overlap.
|
||||
dispatchMouseEvent(document, 'mousemove', elementRect.left, elementRect.bottom - 5);
|
||||
fixture.detectChanges();
|
||||
expect(getElementIndex(placeholder)).toBe(Math.min(i + 1, items.length - 1));
|
||||
expect(getElementIndexByPosition(placeholder, 'top')).toBe(i);
|
||||
}
|
||||
|
||||
dispatchMouseEvent(document, 'mouseup');
|
||||
|
||||
@@ -35,8 +35,8 @@ import {CdkDragDropRegistry} from './drag-drop-registry';
|
||||
import {Subject, merge} from 'rxjs';
|
||||
import {takeUntil} from 'rxjs/operators';
|
||||
|
||||
// TODO: add auto-scrolling functionality.
|
||||
// TODO: add an API for moving a draggable up/down the
|
||||
// TODO(crisbeto): add auto-scrolling functionality.
|
||||
// TODO(crisbeto): add an API for moving a draggable up/down the
|
||||
// list programmatically. Useful for keyboard controls.
|
||||
|
||||
/** Element that can be moved inside a CdkDrop container. */
|
||||
@@ -236,7 +236,7 @@ export class CdkDrag<T = any> implements OnDestroy {
|
||||
|
||||
/** Handler that is invoked when the user moves their pointer after they've initiated a drag. */
|
||||
private _pointerMove = (event: MouseEvent | TouchEvent) => {
|
||||
// TODO: this should start dragging after a certain threshold,
|
||||
// TODO(crisbeto): this should start dragging after a certain threshold,
|
||||
// otherwise we risk interfering with clicks on the element.
|
||||
if (!this._dragDropRegistry.isDragging(this)) {
|
||||
return;
|
||||
@@ -279,8 +279,6 @@ export class CdkDrag<T = any> implements OnDestroy {
|
||||
|
||||
/** Cleans up the DOM artifacts that were added to facilitate the element being dragged. */
|
||||
private _cleanupDragArtifacts() {
|
||||
const currentIndex = this._getElementIndexInDom(this._placeholder);
|
||||
|
||||
// Restore the element's visibility and insert it at its old position in the DOM.
|
||||
// It's important that we maintain the position, because moving the element around in the DOM
|
||||
// can throw off `NgFor` which does smart diffing and re-creates elements only when necessary,
|
||||
@@ -298,6 +296,8 @@ export class CdkDrag<T = any> implements OnDestroy {
|
||||
|
||||
// Re-enter the NgZone since we bound `document` events on the outside.
|
||||
this._ngZone.run(() => {
|
||||
const currentIndex = this.dropContainer.getItemIndex(this);
|
||||
|
||||
this.ended.emit({source: this});
|
||||
this.dropped.emit({
|
||||
item: this,
|
||||
@@ -328,7 +328,7 @@ export class CdkDrag<T = any> implements OnDestroy {
|
||||
// Notify the new container that the item has entered.
|
||||
this.entered.emit({ item: this, container: newContainer });
|
||||
this.dropContainer = newContainer;
|
||||
this.dropContainer.enter(this);
|
||||
this.dropContainer.enter(this, x, y);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -386,37 +386,6 @@ export class CdkDrag<T = any> implements OnDestroy {
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
/** Gets the index of an element, based on its index in the DOM. */
|
||||
private _getElementIndexInDom(element: HTMLElement): number {
|
||||
// Note: we may be able to figure this in memory while sorting, but doing so won't be very
|
||||
// reliable when transferring between containers, because the new container doesn't have
|
||||
// the proper indices yet. Also this will work better for the case where the consumer
|
||||
// isn't using an `ngFor` to render the list.
|
||||
if (!element.parentElement) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Avoid accessing `children` and `children.length` too much since they're a "live collection".
|
||||
let index = 0;
|
||||
const siblings = element.parentElement.children;
|
||||
const siblingsLength = siblings.length;
|
||||
const draggableElements = this.dropContainer._draggables
|
||||
.filter(item => item !== this)
|
||||
.map(item => item.element.nativeElement);
|
||||
|
||||
// Loop through the sibling elements to find out the index of the
|
||||
// current one, while skipping any elements that aren't draggable.
|
||||
for (let i = 0; i < siblingsLength; i++) {
|
||||
if (siblings[i] === element) {
|
||||
return index;
|
||||
} else if (draggableElements.indexOf(siblings[i] as HTMLElement) > -1) {
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out the coordinates at which an element was picked up.
|
||||
* @param referenceElement Element that initiated the dragging.
|
||||
|
||||
@@ -30,8 +30,10 @@ export interface CdkDropContainer<T = any> {
|
||||
/**
|
||||
* Emits an event to indicate that the user moved an item into the container.
|
||||
* @param item Item that was moved into the container.
|
||||
* @param xOffset Position of the item along the X axis.
|
||||
* @param yOffset Position of the item along the Y axis.
|
||||
*/
|
||||
enter(item: CdkDrag): void;
|
||||
enter(item: CdkDrag, xOffset: number, yOffset: number): void;
|
||||
|
||||
/**
|
||||
* Removes an item from the container after it was dragged into another container by the user.
|
||||
|
||||
@@ -100,14 +100,22 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
|
||||
|
||||
/** Cache of the dimensions of all the items and the sibling containers. */
|
||||
private _positionCache = {
|
||||
items: [] as {drag: CdkDrag, clientRect: ClientRect}[],
|
||||
items: [] as {drag: CdkDrag, clientRect: ClientRect, offset: number}[],
|
||||
siblings: [] as {drop: CdkDrop, clientRect: ClientRect}[]
|
||||
};
|
||||
|
||||
/**
|
||||
* Draggable items that are currently active inside the container. Includes the items
|
||||
* from `_draggables`, as well as any items that have been dragged in, but haven't
|
||||
* been dropped yet.
|
||||
*/
|
||||
private _activeDraggables: CdkDrag[];
|
||||
|
||||
/** Starts dragging an item. */
|
||||
start(): void {
|
||||
this._dragging = true;
|
||||
this._refreshPositions();
|
||||
this._activeDraggables = this._draggables.toArray();
|
||||
this._cachePositions();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,25 +125,57 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
|
||||
* @param previousContainer Container from which the item got dragged in.
|
||||
*/
|
||||
drop(item: CdkDrag, currentIndex: number, previousContainer: CdkDrop): void {
|
||||
this._reset();
|
||||
this.dropped.emit({
|
||||
item,
|
||||
currentIndex,
|
||||
previousIndex: previousContainer.getItemIndex(item),
|
||||
container: this,
|
||||
// TODO: reconsider whether to make this null if the containers are the same.
|
||||
// TODO(crisbeto): reconsider whether to make this null if the containers are the same.
|
||||
previousContainer
|
||||
});
|
||||
|
||||
this._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event to indicate that the user moved an item into the container.
|
||||
* @param item Item that was moved into the container.
|
||||
* @param xOffset Position of the item along the X axis.
|
||||
* @param yOffset Position of the item along the Y axis.
|
||||
*/
|
||||
enter(item: CdkDrag): void {
|
||||
enter(item: CdkDrag, xOffset: number, yOffset: number): void {
|
||||
this.entered.emit({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, xOffset, yOffset);
|
||||
const currentIndex = this._activeDraggables.indexOf(item);
|
||||
const newPositionReference = this._activeDraggables[newIndex];
|
||||
const placeholder = item.getPlaceholderElement();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Don't use items that are being dragged as a reference, because
|
||||
// their element has been moved down to the bottom of the body.
|
||||
if (newPositionReference && !this._dragDropRegistry.isDragging(newPositionReference)) {
|
||||
const element = newPositionReference.element.nativeElement;
|
||||
element.parentElement!.insertBefore(placeholder, element);
|
||||
this._activeDraggables.splice(newIndex, 0, item);
|
||||
} else {
|
||||
this.element.nativeElement.appendChild(placeholder);
|
||||
this._activeDraggables.push(item);
|
||||
}
|
||||
|
||||
// The transform needs to be cleared so it doesn't throw off the measurements.
|
||||
placeholder.style.transform = '';
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,7 +192,9 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
|
||||
* @param item Item whose index should be determined.
|
||||
*/
|
||||
getItemIndex(item: CdkDrag): number {
|
||||
return this._draggables.toArray().indexOf(item);
|
||||
return this._dragging ?
|
||||
this._positionCache.items.findIndex(currentItem => currentItem.drag === item) :
|
||||
this._draggables.toArray().indexOf(item);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,37 +205,45 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
|
||||
*/
|
||||
_sortItem(item: CdkDrag, xOffset: number, yOffset: number): void {
|
||||
const siblings = this._positionCache.items;
|
||||
const newPosition = siblings.find(({drag, clientRect}) => {
|
||||
if (drag === item) {
|
||||
// If there's only one left item in the container, it must be
|
||||
// the dragged item itself so we use it as a reference.
|
||||
return siblings.length < 2;
|
||||
}
|
||||
const isHorizontal = this.orientation === 'horizontal';
|
||||
const newIndex = this._getItemIndexFromPointerPosition(item, xOffset, yOffset);
|
||||
const placeholder = item.getPlaceholderElement();
|
||||
|
||||
return this.orientation === 'horizontal' ?
|
||||
xOffset > clientRect.left && xOffset < clientRect.right :
|
||||
yOffset > clientRect.top && yOffset < clientRect.bottom;
|
||||
});
|
||||
|
||||
if (!newPosition && siblings.length > 0) {
|
||||
if (newIndex === -1 && siblings.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't take the element of a dragged item as a reference,
|
||||
// because it has been moved down to the end of the body.
|
||||
const element = (newPosition && !this._dragDropRegistry.isDragging(newPosition.drag)) ?
|
||||
newPosition.drag.element.nativeElement : null;
|
||||
const next = element ? element!.nextSibling : null;
|
||||
const parent = element ? element.parentElement! : this.element.nativeElement;
|
||||
const placeholder = item.getPlaceholderElement();
|
||||
const currentIndex = siblings.findIndex(currentItem => currentItem.drag === item);
|
||||
const currentPosition = siblings[currentIndex];
|
||||
const newPosition = siblings[newIndex];
|
||||
|
||||
if (next) {
|
||||
parent.insertBefore(placeholder, next === placeholder ? element : next);
|
||||
} else {
|
||||
parent.appendChild(placeholder);
|
||||
}
|
||||
// Figure out the offset necessary for the items to be swapped.
|
||||
const offset = isHorizontal ?
|
||||
currentPosition.clientRect.left - newPosition.clientRect.left :
|
||||
currentPosition.clientRect.top - newPosition.clientRect.top;
|
||||
const topAdjustment = isHorizontal ? 0 : offset;
|
||||
const leftAdjustment = isHorizontal ? offset : 0;
|
||||
|
||||
this._refreshPositions();
|
||||
// Since we've moved the items with a `transform`, we need to adjust their cached
|
||||
// client rects to reflect their new position, as well as swap their positions in the cache.
|
||||
// Note that we shouldn't use `getBoundingClientRect` here to update the cache, because the
|
||||
// elements may be mid-animation which will give us a wrong result.
|
||||
this._adjustClientRect(currentPosition.clientRect, -topAdjustment, -leftAdjustment);
|
||||
currentPosition.offset -= offset;
|
||||
siblings[currentIndex] = newPosition;
|
||||
|
||||
this._adjustClientRect(newPosition.clientRect, topAdjustment, leftAdjustment);
|
||||
newPosition.offset += offset;
|
||||
siblings[newIndex] = currentPosition;
|
||||
|
||||
// Swap the placeholder's position with the one of the target draggable.
|
||||
placeholder.style.transform = isHorizontal ?
|
||||
`translate3d(${currentPosition.offset}px, 0, 0)` :
|
||||
`translate3d(0, ${currentPosition.offset}px, 0)`;
|
||||
|
||||
newPosition.drag.element.nativeElement.style.transform = isHorizontal ?
|
||||
`translate3d(${newPosition.offset}px, 0, 0)` :
|
||||
`translate3d(0, ${newPosition.offset}px, 0)`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,12 +262,37 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/** Refreshes the position cache of the items and sibling containers. */
|
||||
private _refreshPositions() {
|
||||
this._positionCache.items = this._draggables
|
||||
.map(drag => ({drag, clientRect: drag.element.nativeElement.getBoundingClientRect()}))
|
||||
private _cachePositions() {
|
||||
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.element.nativeElement;
|
||||
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) => a.clientRect.top - b.clientRect.top);
|
||||
|
||||
// TODO: add filter here that ensures that the current container isn't being passed to itself.
|
||||
// TODO(crisbeto): add filter here that ensures that the
|
||||
// current container isn't being passed to itself.
|
||||
this._positionCache.siblings = this.connectedTo
|
||||
.map(drop => typeof drop === 'string' ? this._dragDropRegistry.getDropContainer(drop)! : drop)
|
||||
.filter(Boolean)
|
||||
@@ -227,7 +302,47 @@ export class CdkDrop<T = any> implements OnInit, OnDestroy {
|
||||
/** Resets the container to its initial state. */
|
||||
private _reset() {
|
||||
this._dragging = false;
|
||||
|
||||
// TODO(crisbeto): may have to wait for the animations to finish.
|
||||
this._activeDraggables.forEach(item => item.element.nativeElement.style.transform = '');
|
||||
this._activeDraggables = [];
|
||||
this._positionCache.items = [];
|
||||
this._positionCache.siblings = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the top/left positions of a `ClientRect`, as well as their bottom/right counterparts.
|
||||
* @param clientRect `ClientRect` that should be updated.
|
||||
* @param top New value for the `top` position.
|
||||
* @param left New value for the `left` position.
|
||||
*/
|
||||
private _adjustClientRect(clientRect: ClientRect, top: number, left: number) {
|
||||
clientRect.top += top;
|
||||
clientRect.bottom = clientRect.top + clientRect.height;
|
||||
|
||||
clientRect.left += left;
|
||||
clientRect.right = clientRect.left + clientRect.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index of an item in the drop container, based on the position of the user's pointer.
|
||||
* @param item Item that is being sorted.
|
||||
* @param xOffset Position of the user's pointer along the X axis.
|
||||
* @param yOffset Position of the user's pointer along the Y axis.
|
||||
*/
|
||||
private _getItemIndexFromPointerPosition(item: CdkDrag, xOffset: number, yOffset: number) {
|
||||
return this._positionCache.items.findIndex(({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.
|
||||
return array.length < 2;
|
||||
}
|
||||
|
||||
return this.orientation === 'horizontal' ?
|
||||
// Round these down since most browsers report client rects with
|
||||
// sub-pixel precision, whereas the mouse coordinates are rounded to pixels.
|
||||
xOffset >= Math.floor(clientRect.left) && xOffset <= Math.floor(clientRect.right) :
|
||||
yOffset >= Math.floor(clientRect.top) && yOffset <= Math.floor(clientRect.bottom);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
|
||||
.cdk-drop-dragging & {
|
||||
transition: transform 500ms ease;
|
||||
.cdk-drop-dragging &:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.horizontal & {
|
||||
@@ -65,11 +65,11 @@
|
||||
}
|
||||
|
||||
.cdk-drag-animating {
|
||||
transition: transform 500ms ease;
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0.5;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
|
||||
Reference in New Issue
Block a user