Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/cdk/drag-drop/directives/drop-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ export class CdkDropList<T = any> implements OnDestroy {

/** Handles events from the underlying DropListRef. */
private _handleEvents(ref: DropListRef<CdkDropList>) {
merge(ref.receivingStarted, ref.receivingStopped)
.pipe(takeUntil(this._destroyed))
.subscribe(() => this._changeDetectorRef.markForCheck());

ref.beforeStarted.subscribe(() => {
this._syncItemsWithRef(this.getSortedItems().map(item => item._dragRef));
this._changeDetectorRef.markForCheck();
Expand Down
109 changes: 107 additions & 2 deletions src/cdk/drag-drop/drop-list-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ export class DropListRef<T = any> {
/** Direction of the list's layout. */
private _direction: Direction = 'ltr';

/**
* Cache of all connected lists (including self) ordered by DOM hierarchy — outer containers
* first, inner containers last. Invalidated whenever siblings change.
*/
private _cachedSortedSiblings: DropListRef[] | null = null;

constructor(
element: ElementRef<HTMLElement> | HTMLElement,
private _dragDropRegistry: DragDropRegistry,
Expand Down Expand Up @@ -240,6 +246,7 @@ export class DropListRef<T = any> {
this._activeSiblings.clear();
this._scrollNode = null!;
this._parentPositions.clear();
this._cachedSortedSiblings = null;
this._dragDropRegistry.removeDropContainer(this);
}

Expand Down Expand Up @@ -368,6 +375,8 @@ export class DropListRef<T = any> {
*/
connectedTo(connectedTo: DropListRef[]): this {
this._siblings = connectedTo.slice();
// Invalidate the hierarchy cache since siblings have changed.
this._cachedSortedSiblings = null;
return this;
}

Expand Down Expand Up @@ -443,6 +452,8 @@ export class DropListRef<T = any> {
}

this._cachedShadowRoot = null;
// Invalidate the hierarchy cache since the DOM structure may have changed.
this._cachedSortedSiblings = null;
this._scrollableElements.unshift(container);
this._container = container;
return this;
Expand Down Expand Up @@ -687,7 +698,101 @@ export class DropListRef<T = any> {
* @param y Position of the item along the Y axis.
*/
_getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | undefined {
return this._siblings.find(sibling => sibling._canReceive(item, x, y));
// Possible targets include siblings and 'this'.
const targets = [this, ...this._siblings];

// Only consider targets where the drag position is within the client rect
// (this avoids calling enterPredicate on each possible target).
const matchingTargets = targets.filter(
ref => ref._domRect && isInsideClientRect(ref._domRect, x, y),
);

// Stop if no targets match the coordinates.
if (matchingTargets.length === 0) {
return undefined;
}

// Use the cached hierarchy order, computing it only when siblings have changed.
// This avoids rebuilding the DOM tree on every pointermove event.
if (!this._cachedSortedSiblings) {
this._cachedSortedSiblings = this._orderByHierarchy([this, ...this._siblings]);
}

// Filter the pre-ordered list to retain only the matching targets,
// preserving the hierarchy order without re-sorting.
const orderedMatchingTargets = this._cachedSortedSiblings.filter(ref =>
matchingTargets.includes(ref),
);

// The drop target is the last matching target in the ordered list,
// i.e. the innermost container in the DOM hierarchy.
const matchingTarget = orderedMatchingTargets[orderedMatchingTargets.length - 1];

// Only return the matching target if it is a sibling, not 'this'.
if (matchingTarget === this) {
return undefined;
}

// Can the matching target receive the item?
if (!matchingTarget._canReceive(item, x, y)) {
return undefined;
}

return matchingTarget;
}

/**
* Sorts a list of DropListRefs such that for every nested pair of drop containers,
* the outer drop container appears before the inner drop container.
* @param refs List of DropListRefs to sort.
*/
private _orderByHierarchy(refs: DropListRef[]): DropListRef[] {
// Build a map from HTMLElement to DropListRef for fast ancestor lookup.
const refsByElement = new Map<HTMLElement, DropListRef>();
refs.forEach(ref => refsByElement.set(ref._container, ref));

// Finds the closest ancestor DropListRef of a given ref, if any.
const findAncestor = (ref: DropListRef): DropListRef | undefined => {
let ancestor = ref._container.parentElement;
while (ancestor) {
if (refsByElement.has(ancestor)) {
return refsByElement.get(ancestor);
}
ancestor = ancestor.parentElement;
}
return undefined;
};

// Node type for the tree structure.
type NodeType = {ref: DropListRef; parent?: NodeType; children: NodeType[]};

// Create a tree node for each ref.
const tree = new Map<DropListRef, NodeType>();
refs.forEach(ref => tree.set(ref, {ref, children: []}));

// Build parent-child relationships.
refs.forEach(ref => {
const parent = findAncestor(ref);
if (parent) {
const node = tree.get(ref)!;
const parentNode = tree.get(parent)!;
node.parent = parentNode;
parentNode.children.push(node);
}
});

// Find root nodes (those without a parent among the refs).
const roots = Array.from(tree.values()).filter(node => !node.parent);

// Recursively build the ordered list: parent before children.
const buildOrderedList = (nodes: NodeType[], list: DropListRef[]) => {
list.push(...nodes.map(node => node.ref));
nodes.forEach(node => buildOrderedList(node.children, list));
};

const ordered: DropListRef[] = [];
buildOrderedList(roots, ordered);
return ordered;
}

/**
Expand Down Expand Up @@ -863,7 +968,7 @@ function getElementScrollDirections(

// Note that we here we do some extra checks for whether the element is actually scrollable in
// a certain direction and we only assign the scroll direction if it is. We do this so that we
// can allow other elements to be scrolled, if the current element can't be scrolled anymore.
// can allow other elements to be scrolled, if the current element can't be scrollated anymore.
// This allows us to handle cases where the scroll regions of two scrollable elements overlap.
if (computedVertical) {
const scrollTop = element.scrollTop;
Expand Down
83 changes: 66 additions & 17 deletions src/dev-app/drag-drop/drag-drop-demo.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,53 @@
<div cdkDropListGroup>
<div class="demo-list">
<h2>To do</h2>

<div
cdkDropList
[cdkDropListData]="todo2"
(cdkDropListDropped)="drop($event)"
[cdkDropListConnectedTo]="dls"
>
@for (item of todo2; track item) {
<div class="example-box" cdkDrag>
@if (isArray(item)) {
<div class="demo-list">
<div
cdkDropList
[cdkDropListData]="item"
class="example-list"
(cdkDropListDropped)="drop($event)"
[cdkDropListConnectedTo]="dls"
>
@for (innerItem of item; track innerItem) {
<div class="example-box" cdkDrag>
{{innerItem}}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
</div>
}
</div>
</div>
} @else {
<div>
{{item}}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
</div>
}
</div>
}
</div>
</div>
</div>

<div cdkDropListGroup>
<div class="demo-list">
<h2>To do</h2>
<div
cdkDropList
(cdkDropListDropped)="drop($event)"
[cdkDropListLockAxis]="axisLock"
[cdkDropListData]="todo">
[cdkDropListData]="todo"
>
@for (item of todo; track item) {
<div cdkDrag>
{{item}}
Expand All @@ -21,10 +63,11 @@ <h2>Done</h2>
cdkDropList
(cdkDropListDropped)="drop($event)"
[cdkDropListLockAxis]="axisLock"
[cdkDropListData]="done">
[cdkDropListData]="done"
>
@for (item of done; track item) {
<div cdkDrag>
{{item}}
{{ item }}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
</div>
}
Expand All @@ -40,10 +83,11 @@ <h2>Ages</h2>
cdkDropListOrientation="horizontal"
(cdkDropListDropped)="drop($event)"
[cdkDropListLockAxis]="axisLock"
[cdkDropListData]="ages">
[cdkDropListData]="ages"
>
@for (item of ages; track item) {
<div cdkDrag>
{{item}}
{{ item }}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
</div>
}
Expand All @@ -57,10 +101,11 @@ <h2>Preferred Ages</h2>
cdkDropListOrientation="horizontal"
(cdkDropListDropped)="drop($event)"
[cdkDropListLockAxis]="axisLock"
[cdkDropListData]="preferredAges">
[cdkDropListData]="preferredAges"
>
@for (item of preferredAges; track item) {
<div cdkDrag>
{{item}}
{{ item }}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
</div>
}
Expand All @@ -78,16 +123,18 @@ <h2>Mixed orientation</h2>
<div
class="demo-list"
[class.demo-list-wrapping]="mixedWrap"
[class.demo-list-horizontal]="mixedWrap">
[class.demo-list-horizontal]="mixedWrap"
>
<div
cdkDropList
cdkDropListOrientation="mixed"
(cdkDropListDropped)="drop($event)"
[cdkDropListLockAxis]="axisLock"
[cdkDropListData]="mixedTodo">
[cdkDropListData]="mixedTodo"
>
@for (item of mixedTodo; track item) {
<div cdkDrag>
{{item}}
{{ item }}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
</div>
}
Expand All @@ -97,16 +144,18 @@ <h2>Mixed orientation</h2>
<div
class="demo-list"
[class.demo-list-wrapping]="mixedWrap"
[class.demo-list-horizontal]="mixedWrap">
[class.demo-list-horizontal]="mixedWrap"
>
<div
cdkDropList
cdkDropListOrientation="mixed"
(cdkDropListDropped)="drop($event)"
[cdkDropListLockAxis]="axisLock"
[cdkDropListData]="mixedDone">
[cdkDropListData]="mixedDone"
>
@for (item of mixedDone; track item) {
<div cdkDrag>
{{item}}
{{ item }}
<mat-icon cdkDragHandle svgIcon="dnd-move"></mat-icon>
</div>
}
Expand Down Expand Up @@ -159,10 +208,10 @@ <h2>Drag with box boundary and custom constrain</h2>

<div>
<h2>Data</h2>
<pre>{{todo.join(', ')}}</pre>
<pre>{{done.join(', ')}}</pre>
<pre>{{ages.join(', ')}}</pre>
<pre>{{preferredAges.join(', ')}}</pre>
<pre>{{ todo.join(', ') }}</pre>
<pre>{{ done.join(', ') }}</pre>
<pre>{{ ages.join(', ') }}</pre>
<pre>{{ preferredAges.join(', ') }}</pre>
</div>

<div>
Expand Down
Loading
Loading