diff --git a/src/cdk/drag-drop/directives/config.ts b/src/cdk/drag-drop/directives/config.ts index 06a05c9c72cf..3734137ef226 100644 --- a/src/cdk/drag-drop/directives/config.ts +++ b/src/cdk/drag-drop/directives/config.ts @@ -19,7 +19,7 @@ export type DragAxis = 'x' | 'y'; export type DragConstrainPosition = (point: Point, dragRef: DragRef) => Point; /** Possible orientations for a drop list. */ -export type DropListOrientation = 'horizontal' | 'vertical'; +export type DropListOrientation = 'horizontal' | 'vertical' | 'mixed'; /** * Injection token that can be used to configure the diff --git a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts index e45102078fea..24c3630d1c73 100644 --- a/src/cdk/drag-drop/directives/drop-list-shared.spec.ts +++ b/src/cdk/drag-drop/directives/drop-list-shared.spec.ts @@ -803,6 +803,26 @@ export function defineCommonDropListTests(config: { scrollTo(0, 0); })); + it('should remove the anchor node once dragging stops', fakeAsync(() => { + const fixture = createComponent(DraggableInDropZone); + fixture.detectChanges(); + const item = fixture.componentInstance.dragItems.toArray()[1].element.nativeElement; + const list = fixture.componentInstance.dropInstance.element.nativeElement; + + startDraggingViaMouse(fixture, item); + + const anchor = Array.from(list.childNodes).find( + node => node.textContent === 'cdk-drag-anchor', + ); + expect(anchor).toBeTruthy(); + + dispatchMouseEvent(document, 'mouseup'); + fixture.detectChanges(); + flush(); + + expect(anchor!.parentNode).toBeFalsy(); + })); + it('should create a preview element while the item is dragged', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); @@ -1489,7 +1509,7 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted down', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); - assertDownwardSorting( + assertStartToEndSorting( 'vertical', fixture, config.getSortedSiblings, @@ -1503,7 +1523,7 @@ export function defineCommonDropListTests(config: { const cleanup = makeScrollable(); scrollTo(0, 5000); - assertDownwardSorting( + assertStartToEndSorting( 'vertical', fixture, config.getSortedSiblings, @@ -1515,7 +1535,7 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted up', fakeAsync(() => { const fixture = createComponent(DraggableInDropZone); fixture.detectChanges(); - assertUpwardSorting( + assertEndToStartSorting( 'vertical', fixture, config.getSortedSiblings, @@ -1529,7 +1549,7 @@ export function defineCommonDropListTests(config: { const cleanup = makeScrollable(); scrollTo(0, 5000); - assertUpwardSorting( + assertEndToStartSorting( 'vertical', fixture, config.getSortedSiblings, @@ -1541,7 +1561,7 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted to the right', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); fixture.detectChanges(); - assertDownwardSorting( + assertStartToEndSorting( 'horizontal', fixture, config.getSortedSiblings, @@ -1552,7 +1572,7 @@ export function defineCommonDropListTests(config: { it('should move the placeholder as an item is being sorted to the left', fakeAsync(() => { const fixture = createComponent(DraggableInHorizontalDropZone); fixture.detectChanges(); - assertUpwardSorting( + assertEndToStartSorting( 'horizontal', fixture, config.getSortedSiblings, @@ -3130,7 +3150,7 @@ export function defineCommonDropListTests(config: { documentElement.style.position = 'absolute'; documentElement.style.top = '100px'; - assertDownwardSorting( + assertStartToEndSorting( 'vertical', fixture, config.getSortedSiblings, @@ -3394,7 +3414,7 @@ export function defineCommonDropListTests(config: { fixture.detectChanges(); }); - assertDownwardSorting( + assertStartToEndSorting( 'vertical', fixture, config.getSortedSiblings, @@ -4674,7 +4694,7 @@ export function defineCommonDropListTests(config: { }); } -function assertDownwardSorting( +export function assertStartToEndSorting( listOrientation: 'vertical' | 'horizontal', fixture: ComponentFixture, getSortedSiblings: SortedSiblingsFunction, @@ -4714,7 +4734,7 @@ function assertDownwardSorting( flush(); } -function assertUpwardSorting( +export function assertEndToStartSorting( listOrientation: 'vertical' | 'horizontal', fixture: ComponentFixture, getSortedSiblings: SortedSiblingsFunction, diff --git a/src/cdk/drag-drop/directives/mixed-drop-list.spec.ts b/src/cdk/drag-drop/directives/mixed-drop-list.spec.ts new file mode 100644 index 000000000000..8deaae351dd5 --- /dev/null +++ b/src/cdk/drag-drop/directives/mixed-drop-list.spec.ts @@ -0,0 +1,144 @@ +import {Component, QueryList, ViewChild, ViewChildren} from '@angular/core'; +import {fakeAsync, flush} from '@angular/core/testing'; +import {CdkDropList} from './drop-list'; +import {CdkDrag} from './drag'; +import {moveItemInArray} from '../drag-utils'; +import {CdkDragDrop} from '../drag-events'; +import { + ITEM_HEIGHT, + ITEM_WIDTH, + assertStartToEndSorting, + assertEndToStartSorting, + defineCommonDropListTests, +} from './drop-list-shared.spec'; +import {createComponent, dragElementViaMouse} from './test-utils.spec'; + +describe('mixed drop list', () => { + defineCommonDropListTests({ + verticalListOrientation: 'mixed', + horizontalListOrientation: 'mixed', + getSortedSiblings, + }); + + it('should dispatch the `dropped` event in a wrapping drop zone', fakeAsync(() => { + const fixture = createComponent(DraggableInHorizontalWrappingDropZone); + fixture.detectChanges(); + const dragItems = fixture.componentInstance.dragItems; + + expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())).toEqual([ + 'Zero', + 'One', + 'Two', + 'Three', + 'Four', + 'Five', + 'Six', + 'Seven', + ]); + + const firstItem = dragItems.first; + const seventhItemRect = dragItems.toArray()[6].element.nativeElement.getBoundingClientRect(); + + dragElementViaMouse( + fixture, + firstItem.element.nativeElement, + seventhItemRect.left + 1, + seventhItemRect.top + 1, + ); + flush(); + fixture.detectChanges(); + + expect(fixture.componentInstance.droppedSpy).toHaveBeenCalledTimes(1); + const event = fixture.componentInstance.droppedSpy.calls.mostRecent().args[0]; + + // Assert the event like this, rather than `toHaveBeenCalledWith`, because Jasmine will + // go into an infinite loop trying to stringify the event, if the test fails. + expect(event).toEqual({ + previousIndex: 0, + currentIndex: 6, + item: firstItem, + container: fixture.componentInstance.dropInstance, + previousContainer: fixture.componentInstance.dropInstance, + isPointerOverContainer: true, + distance: {x: jasmine.any(Number), y: jasmine.any(Number)}, + dropPoint: {x: jasmine.any(Number), y: jasmine.any(Number)}, + event: jasmine.anything(), + }); + + expect(dragItems.map(drag => drag.element.nativeElement.textContent!.trim())).toEqual([ + 'One', + 'Two', + 'Three', + 'Four', + 'Five', + 'Six', + 'Zero', + 'Seven', + ]); + })); + + it('should move the placeholder as an item is being sorted to the right in a wrapping drop zone', fakeAsync(() => { + const fixture = createComponent(DraggableInHorizontalWrappingDropZone); + fixture.detectChanges(); + assertStartToEndSorting( + 'horizontal', + fixture, + getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), + ); + })); + + it('should move the placeholder as an item is being sorted to the left in a wrapping drop zone', fakeAsync(() => { + const fixture = createComponent(DraggableInHorizontalWrappingDropZone); + fixture.detectChanges(); + assertEndToStartSorting( + 'horizontal', + fixture, + getSortedSiblings, + fixture.componentInstance.dragItems.map(item => item.element.nativeElement), + ); + })); +}); + +function getSortedSiblings(item: Element) { + return Array.from(item.parentElement?.children || []); +} + +@Component({ + styles: ` + .cdk-drop-list { + display: block; + width: ${ITEM_WIDTH * 3}px; + background: pink; + font-size: 0; + } + + .cdk-drag { + height: ${ITEM_HEIGHT * 2}px; + width: ${ITEM_WIDTH}px; + background: red; + display: inline-block; + } + `, + template: ` +
+ @for (item of items; track item) { +
{{item}}
+ } +
+ `, + standalone: true, + imports: [CdkDropList, CdkDrag], +}) +class DraggableInHorizontalWrappingDropZone { + @ViewChildren(CdkDrag) dragItems: QueryList; + @ViewChild(CdkDropList) dropInstance: CdkDropList; + items = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven']; + droppedSpy = jasmine.createSpy('dropped spy').and.callFake((event: CdkDragDrop) => { + moveItemInArray(this.items, event.previousIndex, event.currentIndex); + }); +} diff --git a/src/cdk/drag-drop/drag-drop.md b/src/cdk/drag-drop/drag-drop.md index 0d90da0800fc..f9f7ad1b02e8 100644 --- a/src/cdk/drag-drop/drag-drop.md +++ b/src/cdk/drag-drop/drag-drop.md @@ -157,10 +157,22 @@ directive: ### List orientation The `cdkDropList` directive assumes that lists are vertical by default. This can be -changed by setting the `orientation` property to `"horizontal". +changed by setting the `cdkDropListOrientation` property to `horizontal`. +### List wrapping +By default the `cdkDropList` sorts the items by moving them around using a CSS `transform`. This +allows for the sorting to be animated which provides a better user experience, but comes with the +drawback that it works only one direction: vertically or horizontally. + +If you have a sortable list that needs to wrap, you can set `cdkDropListOrientation="mixed"` which +will use a different strategy of sorting the elements that works by moving them in the DOM. It has +the advantage of allowing the items to wrap to the next line, but it **cannot** animate the +sorting action. + + + ### Restricting movement within an element If you want to stop the user from being able to drag a `cdkDrag` element outside of another element, diff --git a/src/cdk/drag-drop/drag-ref.ts b/src/cdk/drag-drop/drag-ref.ts index b961326e84be..2b751e373171 100644 --- a/src/cdk/drag-drop/drag-ref.ts +++ b/src/cdk/drag-drop/drag-ref.ts @@ -822,7 +822,8 @@ export class DragRef { const element = this._rootElement; const parent = element.parentNode as HTMLElement; const placeholder = (this._placeholder = this._createPlaceholderElement()); - const anchor = (this._anchor = this._anchor || this._document.createComment('')); + const anchor = (this._anchor = + this._anchor || this._document.createComment(ngDevMode ? 'cdk-drag-anchor' : '')); // Insert an anchor node so that we can restore the element's position in the DOM. parent.insertBefore(anchor, element); diff --git a/src/cdk/drag-drop/drop-list-ref.ts b/src/cdk/drag-drop/drop-list-ref.ts index eaf77b8967ae..a1bf214aeb42 100644 --- a/src/cdk/drag-drop/drop-list-ref.ts +++ b/src/cdk/drag-drop/drop-list-ref.ts @@ -20,6 +20,8 @@ import {ParentPositionTracker} from './dom/parent-position-tracker'; import {DragCSSStyleDeclaration} from './dom/styling'; import {DropListSortStrategy} from './sorting/drop-list-sort-strategy'; import {SingleAxisSortStrategy} from './sorting/single-axis-sort-strategy'; +import {MixedSortStrategy} from './sorting/mixed-sort-strategy'; +import {DropListOrientation} from './directives/config'; /** * Proximity, as a ratio to width/height, at which a @@ -199,11 +201,9 @@ export class DropListRef { ) { this.element = coerceElement(element); this._document = _document; - this.withScrollableParents([this.element]); + this.withScrollableParents([this.element]).withOrientation('vertical'); _dragDropRegistry.registerDropContainer(this); this._parentPositions = new ParentPositionTracker(_document); - this._sortStrategy = new SingleAxisSortStrategy(this.element, _dragDropRegistry); - this._sortStrategy.withSortPredicate((index, item) => this.sortPredicate(index, item, this)); } /** Removes the drop list functionality from the DOM element. */ @@ -356,10 +356,23 @@ export class DropListRef { * Sets the orientation of the container. * @param orientation New orientation for the container. */ - withOrientation(orientation: 'vertical' | 'horizontal'): this { - // TODO(crisbeto): eventually we should be constructing the new sort strategy here based on - // the new orientation. For now we can assume that it'll always be `SingleAxisSortStrategy`. - (this._sortStrategy as SingleAxisSortStrategy).orientation = orientation; + withOrientation(orientation: DropListOrientation): this { + if (orientation === 'mixed') { + this._sortStrategy = new MixedSortStrategy( + coerceElement(this.element), + this._document, + this._dragDropRegistry, + ); + } else { + const strategy = new SingleAxisSortStrategy( + coerceElement(this.element), + this._dragDropRegistry, + ); + strategy.direction = this._direction; + strategy.orientation = orientation; + this._sortStrategy = strategy; + } + this._sortStrategy.withSortPredicate((index, item) => this.sortPredicate(index, item, this)); return this; } diff --git a/src/cdk/drag-drop/sorting/mixed-sort-strategy.ts b/src/cdk/drag-drop/sorting/mixed-sort-strategy.ts new file mode 100644 index 000000000000..ab728b64aed1 --- /dev/null +++ b/src/cdk/drag-drop/sorting/mixed-sort-strategy.ts @@ -0,0 +1,305 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {_getShadowRoot} from '@angular/cdk/platform'; +import {moveItemInArray} from '../drag-utils'; +import {DropListSortStrategy, SortPredicate} from './drop-list-sort-strategy'; +import {DragDropRegistry} from '../drag-drop-registry'; +import type {DragRef} from '../drag-ref'; + +/** + * Strategy that only supports sorting on a list that might wrap. + * Items are reordered by moving their DOM nodes around. + * @docs-private + */ +export class MixedSortStrategy implements DropListSortStrategy { + /** Function used to determine if an item can be sorted into a specific index. */ + private _sortPredicate: SortPredicate; + + /** Lazily-resolved root node containing the list. Use `_getRootNode` to read this. */ + private _rootNode: DocumentOrShadowRoot | undefined; + + /** + * Draggable items that are currently active inside the container. Includes the items + * that were there at the start of the sequence, as well as any items that have been dragged + * in, but haven't been dropped yet. + */ + private _activeItems: DragRef[]; + + /** + * Keeps track of the item that was last swapped with the dragged item, as well as what direction + * the pointer was moving in when the swap occurred and whether the user's pointer continued to + * overlap with the swapped item after the swapping occurred. + */ + private _previousSwap = { + drag: null as DragRef | null, + deltaX: 0, + deltaY: 0, + overlaps: false, + }; + + /** + * Keeps track of the relationship between a node and its next sibling. This information + * is used to restore the DOM to the order it was in before dragging started. + */ + private _relatedNodes: [node: Node, nextSibling: Node | null][] = []; + + constructor( + private _element: HTMLElement, + private _document: Document, + private _dragDropRegistry: DragDropRegistry, + ) {} + + /** + * To be called when the drag sequence starts. + * @param items Items that are currently in the list. + */ + start(items: readonly DragRef[]): void { + const childNodes = this._element.childNodes; + this._relatedNodes = []; + + for (let i = 0; i < childNodes.length; i++) { + const node = childNodes[i]; + this._relatedNodes.push([node, node.nextSibling]); + } + + this.withItems(items); + } + + /** + * To be called when an item is being sorted. + * @param item Item to be sorted. + * @param pointerX Position of the item along the X axis. + * @param pointerY Position of the item along the Y axis. + * @param pointerDelta Direction in which the pointer is moving along each axis. + */ + sort( + item: DragRef, + pointerX: number, + pointerY: number, + pointerDelta: {x: number; y: number}, + ): {previousIndex: number; currentIndex: number} | null { + const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY); + const previousSwap = this._previousSwap; + + if (newIndex === -1 || this._activeItems[newIndex] === item) { + return null; + } + + const toSwapWith = this._activeItems[newIndex]; + + // Prevent too many swaps over the same item. + if ( + previousSwap.drag === toSwapWith && + previousSwap.overlaps && + previousSwap.deltaX === pointerDelta.x && + previousSwap.deltaY === pointerDelta.y + ) { + return null; + } + + const previousIndex = this.getItemIndex(item); + const current = item.getPlaceholderElement(); + const overlapElement = toSwapWith.getRootElement(); + + if (newIndex > previousIndex) { + overlapElement.after(current); + } else { + overlapElement.before(current); + } + + moveItemInArray(this._activeItems, previousIndex, newIndex); + + const newOverlapElement = this._getRootNode().elementFromPoint(pointerX, pointerY); + // Note: it's tempting to save the entire `pointerDelta` object here, however that'll + // break this functionality, because the same object is passed for all `sort` calls. + previousSwap.deltaX = pointerDelta.x; + previousSwap.deltaY = pointerDelta.y; + previousSwap.drag = toSwapWith; + previousSwap.overlaps = + overlapElement === newOverlapElement || overlapElement.contains(newOverlapElement); + + return { + previousIndex, + currentIndex: newIndex, + }; + } + + /** + * Called when an item is being moved into the container. + * @param item Item that was moved into the container. + * @param pointerX Position of the item along the X axis. + * @param pointerY Position of the item along the Y axis. + * @param index Index at which the item entered. If omitted, the container will try to figure it + * out automatically. + */ + enter(item: DragRef, pointerX: number, pointerY: number, index?: number): void { + let enterIndex = + index == null || index < 0 + ? this._getItemIndexFromPointerPosition(item, pointerX, pointerY) + : index; + + // In some cases (e.g. when the container has padding) we might not be able to figure + // out which item to insert the dragged item next to, because the pointer didn't overlap + // with anything. In that case we find the item that's closest to the pointer. + if (enterIndex === -1) { + enterIndex = this._getClosestItemIndexToPointer(item, pointerX, pointerY); + } + + const targetItem = this._activeItems[enterIndex] as DragRef | undefined; + const currentIndex = this._activeItems.indexOf(item); + + if (currentIndex > -1) { + this._activeItems.splice(currentIndex, 1); + } + + if (targetItem && !this._dragDropRegistry.isDragging(targetItem)) { + this._activeItems.splice(enterIndex, 0, item); + targetItem.getRootElement().before(item.getPlaceholderElement()); + } else { + this._activeItems.push(item); + this._element.appendChild(item.getPlaceholderElement()); + } + } + + /** Sets the items that are currently part of the list. */ + withItems(items: readonly DragRef[]): void { + this._activeItems = items.slice(); + } + + /** Assigns a sort predicate to the strategy. */ + withSortPredicate(predicate: SortPredicate): void { + this._sortPredicate = predicate; + } + + /** Resets the strategy to its initial state before dragging was started. */ + reset(): void { + const root = this._element; + const previousSwap = this._previousSwap; + + // Moving elements around in the DOM can break things like the `@for` loop, because it + // uses comment nodes to know where to insert elements. To avoid such issues, we restore + // the DOM nodes in the list to their original order when the list is reset. + // Note that this could be simpler if we just saved all the nodes, cleared the root + // and then appended them in the original order. We don't do it, because it can break + // down depending on when the snapshot was taken. E.g. we may end up snapshotting the + // placeholder element which is removed after dragging. + for (let i = this._relatedNodes.length - 1; i > -1; i--) { + const [node, nextSibling] = this._relatedNodes[i]; + if (node.parentNode === root && node.nextSibling !== nextSibling) { + if (nextSibling === null) { + root.appendChild(node); + } else if (nextSibling.parentNode === root) { + root.insertBefore(node, nextSibling); + } + } + } + + this._relatedNodes = []; + this._activeItems = []; + previousSwap.drag = null; + previousSwap.deltaX = previousSwap.deltaY = 0; + previousSwap.overlaps = false; + } + + /** + * Gets a snapshot of items currently in the list. + * Can include items that we dragged in from another list. + */ + getActiveItemsSnapshot(): readonly DragRef[] { + return this._activeItems; + } + + /** Gets the index of a specific item. */ + getItemIndex(item: DragRef): number { + return this._activeItems.indexOf(item); + } + + /** Used to notify the strategy that the scroll position has changed. */ + updateOnScroll(): void { + this._activeItems.forEach(item => { + if (this._dragDropRegistry.isDragging(item)) { + // We need to re-sort the item manually, because the pointer move + // events won't be dispatched while the user is scrolling. + item._sortFromLastPointerPosition(); + } + }); + } + + /** + * 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 pointerX Position of the user's pointer along the X axis. + * @param pointerY Position of the user's pointer along the Y axis. + * @param delta Direction in which the user is moving their pointer. + */ + private _getItemIndexFromPointerPosition( + item: DragRef, + pointerX: number, + pointerY: number, + ): number { + const elementAtPoint = this._getRootNode().elementFromPoint( + Math.floor(pointerX), + Math.floor(pointerY), + ); + const index = elementAtPoint + ? this._activeItems.findIndex(item => { + const root = item.getRootElement(); + return elementAtPoint === root || root.contains(elementAtPoint); + }) + : -1; + return index === -1 || !this._sortPredicate(index, item) ? -1 : index; + } + + /** Lazily resolves the list's root node. */ + private _getRootNode(): DocumentOrShadowRoot { + // Resolve the root node lazily to ensure that the drop list is in its final place in the DOM. + if (!this._rootNode) { + this._rootNode = _getShadowRoot(this._element) || this._document; + } + return this._rootNode; + } + + /** + * Finds the index of the item that's closest to the item being dragged. + * @param item Item being dragged. + * @param pointerX Position of the user's pointer along the X axis. + * @param pointerY Position of the user's pointer along the Y axis. + */ + private _getClosestItemIndexToPointer(item: DragRef, pointerX: number, pointerY: number): number { + if (this._activeItems.length === 0) { + return -1; + } + + if (this._activeItems.length === 1) { + return 0; + } + + let minDistance = Infinity; + let minIndex = -1; + + // Find the Euclidean distance (https://en.wikipedia.org/wiki/Euclidean_distance) between each + // item and the pointer, and return the smallest one. Note that this is a bit flawed in that DOM + // nodes are rectangles, not points, so we use the top/left coordinates. It should be enough + // for our purposes. + for (let i = 0; i < this._activeItems.length; i++) { + const current = this._activeItems[i]; + if (current !== item) { + const {x, y} = current.getRootElement().getBoundingClientRect(); + const distance = Math.hypot(pointerX - x, pointerY - y); + + if (distance < minDistance) { + minDistance = distance; + minIndex = i; + } + } + } + + return minIndex; + } +} diff --git a/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts b/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts index 7ce70419deda..24cb859bf97c 100644 --- a/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts +++ b/src/cdk/drag-drop/sorting/single-axis-sort-strategy.ts @@ -7,8 +7,6 @@ */ import {Direction} from '@angular/cdk/bidi'; -import {ElementRef} from '@angular/core'; -import {coerceElement} from '@angular/cdk/coercion'; import {DragDropRegistry} from '../drag-drop-registry'; import {moveItemInArray} from '../drag-utils'; import {combineTransforms} from '../dom/styling'; @@ -57,7 +55,7 @@ export class SingleAxisSortStrategy implements DropListSortStrategy { direction: Direction; constructor( - private _element: HTMLElement | ElementRef, + private _element: HTMLElement, private _dragDropRegistry: DragDropRegistry, ) {} @@ -210,7 +208,7 @@ export class SingleAxisSortStrategy implements DropListSortStrategy { element.parentElement!.insertBefore(placeholder, element); activeDraggables.splice(newIndex, 0, item); } else { - coerceElement(this._element).appendChild(placeholder); + this._element.appendChild(placeholder); activeDraggables.push(item); } diff --git a/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.css b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.css new file mode 100644 index 000000000000..eec65dd76c5c --- /dev/null +++ b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.css @@ -0,0 +1,42 @@ +.example-list { + display: flex; + flex-wrap: wrap; + width: 505px; + max-width: 100%; + gap: 15px; + padding: 15px; + border: solid 1px #ccc; + min-height: 60px; + border-radius: 4px; + overflow: hidden; +} + +.example-box { + padding: 20px 10px; + border: solid 1px #ccc; + border-radius: 4px; + color: rgba(0, 0, 0, 0.87); + display: inline-block; + box-sizing: border-box; + cursor: move; + background: white; + text-align: center; + font-size: 14px; + min-width: 115px; +} + +.cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +.cdk-drag-placeholder { + opacity: 0; +} + +.cdk-drag-animating { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} diff --git a/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.html b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.html new file mode 100644 index 000000000000..e081bbe4b11b --- /dev/null +++ b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.html @@ -0,0 +1,5 @@ +
+ @for (item of items; track item) { +
{{item}}
+ } +
diff --git a/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.ts b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.ts new file mode 100644 index 000000000000..f2fd955665ad --- /dev/null +++ b/src/components-examples/cdk/drag-drop/cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example.ts @@ -0,0 +1,20 @@ +import {Component} from '@angular/core'; +import {CdkDragDrop, CdkDrag, CdkDropList, moveItemInArray} from '@angular/cdk/drag-drop'; + +/** + * @title Drag&Drop horizontal wrapping list + */ +@Component({ + selector: 'cdk-drag-drop-mixed-sorting-example', + templateUrl: 'cdk-drag-drop-mixed-sorting-example.html', + styleUrl: 'cdk-drag-drop-mixed-sorting-example.css', + standalone: true, + imports: [CdkDropList, CdkDrag], +}) +export class CdkDragDropMixedSortingExample { + items = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine']; + + drop(event: CdkDragDrop) { + moveItemInArray(this.items, event.previousIndex, event.currentIndex); + } +} diff --git a/src/components-examples/cdk/drag-drop/index.ts b/src/components-examples/cdk/drag-drop/index.ts index 7c41155fbf58..712fe39ac20f 100644 --- a/src/components-examples/cdk/drag-drop/index.ts +++ b/src/components-examples/cdk/drag-drop/index.ts @@ -16,3 +16,4 @@ export {CdkDragDropRootElementExample} from './cdk-drag-drop-root-element/cdk-dr export {CdkDragDropSortingExample} from './cdk-drag-drop-sorting/cdk-drag-drop-sorting-example'; export {CdkDragDropSortPredicateExample} from './cdk-drag-drop-sort-predicate/cdk-drag-drop-sort-predicate-example'; export {CdkDragDropTableExample} from './cdk-drag-drop-table/cdk-drag-drop-table-example'; +export {CdkDragDropMixedSortingExample} from './cdk-drag-drop-mixed-sorting/cdk-drag-drop-mixed-sorting-example'; diff --git a/src/dev-app/drag-drop/BUILD.bazel b/src/dev-app/drag-drop/BUILD.bazel index 416ba4b592d8..d41ec8215880 100644 --- a/src/dev-app/drag-drop/BUILD.bazel +++ b/src/dev-app/drag-drop/BUILD.bazel @@ -11,6 +11,7 @@ ng_module( ], deps = [ "//src/cdk/drag-drop", + "//src/material/checkbox", "//src/material/form-field", "//src/material/icon", "//src/material/input", diff --git a/src/dev-app/drag-drop/drag-drop-demo.html b/src/dev-app/drag-drop/drag-drop-demo.html index 5c011e0a1042..104101ae2496 100644 --- a/src/dev-app/drag-drop/drag-drop-demo.html +++ b/src/dev-app/drag-drop/drag-drop-demo.html @@ -68,6 +68,52 @@

Preferred Ages

+

Mixed orientation

+ +

+ Wrap list +

+ +
+
+
+ @for (item of mixedTodo; track item) { +
+ {{item}} + +
+ } +
+
+ +
+
+ @for (item of mixedDone; track item) { +
+ {{item}} + +
+ } +
+
+
+

Free dragging

{ @@ -544,7 +544,7 @@ export class DropListRef { _stopScrolling(): void; withDirection(direction: Direction): this; withItems(items: DragRef[]): this; - withOrientation(orientation: 'vertical' | 'horizontal'): this; + withOrientation(orientation: DropListOrientation): this; withScrollableParents(elements: HTMLElement[]): this; }