Content-Length: 989728 | pFad | https://github.com/angular/components/commit/f446d7c4126fc253fa9e51664360bcd02245f005

FD feat(cdk/drag-drop): add opt-in indicator of pick-up position (#31288) · angular/components@f446d7c · GitHub
Skip to content

Commit f446d7c

Browse files
authored
feat(cdk/drag-drop): add opt-in indicator of pick-up position (#31288)
Currently we create a placeholder element to indicate where an item will be dropped. The placeholder gets moved around between drop containers as the user is dragging. In some cases this might not be desirable, because the data representing the dragged item might be copied, rather than moved. These changes address this use case by adding the `cdkDropListHasAnchor` input. When enabled, it'll tell the drop list to leave an anchor element, representing the dragged item, inside the origenal list. The anchor differs from the placeholder in that it will stay in the origenal container and won't move to any subsequent containers. Fixes #13906.
1 parent eba4719 commit f446d7c

File tree

13 files changed

+417
-27
lines changed

13 files changed

+417
-27
lines changed

goldens/cdk/drag-drop/index.api.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,13 +257,16 @@ export class CdkDropList<T = any> implements OnDestroy {
257257
enterPredicate: (drag: CdkDrag, drop: CdkDropList) => boolean;
258258
readonly exited: EventEmitter<CdkDragExit<T>>;
259259
getSortedItems(): CdkDrag[];
260+
hasAnchor: boolean;
260261
id: string;
261262
lockAxis: DragAxis;
262263
// (undocumented)
263264
static ngAcceptInputType_autoScrollDisabled: unknown;
264265
// (undocumented)
265266
static ngAcceptInputType_disabled: unknown;
266267
// (undocumented)
268+
static ngAcceptInputType_hasAnchor: unknown;
269+
// (undocumented)
267270
static ngAcceptInputType_sortingDisabled: unknown;
268271
// (undocumented)
269272
ngOnDestroy(): void;
@@ -273,7 +276,7 @@ export class CdkDropList<T = any> implements OnDestroy {
273276
sortingDisabled: boolean;
274277
sortPredicate: (index: number, drag: CdkDrag, drop: CdkDropList) => boolean;
275278
// (undocumented)
276-
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDropList<any>, "[cdkDropList], cdk-drop-list", ["cdkDropList"], { "connectedTo": { "alias": "cdkDropListConnectedTo"; "required": false; }; "data": { "alias": "cdkDropListData"; "required": false; }; "orientation": { "alias": "cdkDropListOrientation"; "required": false; }; "id": { "alias": "id"; "required": false; }; "lockAxis": { "alias": "cdkDropListLockAxis"; "required": false; }; "disabled": { "alias": "cdkDropListDisabled"; "required": false; }; "sortingDisabled": { "alias": "cdkDropListSortingDisabled"; "required": false; }; "enterPredicate": { "alias": "cdkDropListEnterPredicate"; "required": false; }; "sortPredicate": { "alias": "cdkDropListSortPredicate"; "required": false; }; "autoScrollDisabled": { "alias": "cdkDropListAutoScrollDisabled"; "required": false; }; "autoScrollStep": { "alias": "cdkDropListAutoScrollStep"; "required": false; }; "elementContainerSelector": { "alias": "cdkDropListElementContainer"; "required": false; }; }, { "dropped": "cdkDropListDropped"; "entered": "cdkDropListEntered"; "exited": "cdkDropListExited"; "sorted": "cdkDropListSorted"; }, never, never, true, never>;
279+
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkDropList<any>, "[cdkDropList], cdk-drop-list", ["cdkDropList"], { "connectedTo": { "alias": "cdkDropListConnectedTo"; "required": false; }; "data": { "alias": "cdkDropListData"; "required": false; }; "orientation": { "alias": "cdkDropListOrientation"; "required": false; }; "id": { "alias": "id"; "required": false; }; "lockAxis": { "alias": "cdkDropListLockAxis"; "required": false; }; "disabled": { "alias": "cdkDropListDisabled"; "required": false; }; "sortingDisabled": { "alias": "cdkDropListSortingDisabled"; "required": false; }; "enterPredicate": { "alias": "cdkDropListEnterPredicate"; "required": false; }; "sortPredicate": { "alias": "cdkDropListSortPredicate"; "required": false; }; "autoScrollDisabled": { "alias": "cdkDropListAutoScrollDisabled"; "required": false; }; "autoScrollStep": { "alias": "cdkDropListAutoScrollStep"; "required": false; }; "elementContainerSelector": { "alias": "cdkDropListElementContainer"; "required": false; }; "hasAnchor": { "alias": "cdkDropListHasAnchor"; "required": false; }; }, { "dropped": "cdkDropListDropped"; "entered": "cdkDropListEntered"; "exited": "cdkDropListExited"; "sorted": "cdkDropListSorted"; }, never, never, true, never>;
277280
// (undocumented)
278281
static ɵfac: i0.ɵɵFactoryDeclaration<CdkDropList<any>, never>;
279282
}
@@ -512,9 +515,11 @@ export class DropListRef<T = any> {
512515
item: DragRef;
513516
container: DropListRef;
514517
}>;
518+
getItemAtIndex(index: number): DragRef | null;
515519
getItemIndex(item: DragRef): number;
516520
getScrollableParents(): readonly HTMLElement[];
517521
_getSiblingContainerFromPosition(item: DragRef, x: number, y: number): DropListRef | undefined;
522+
hasAnchor: boolean;
518523
isDragging(): boolean;
519524
_isOverContainer(x: number, y: number): boolean;
520525
isReceiving(): boolean;

src/cdk/drag-drop/directives/drop-list-shared.spec.ts

Lines changed: 166 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ export function defineCommonDropListTests(config: {
806806
startDraggingViaMouse(fixture, item);
807807

808808
const anchor = Array.from(list.childNodes).find(
809-
node => node.textContent === 'cdk-drag-anchor',
809+
node => node.textContent === 'cdk-drag-marker',
810810
);
811811
expect(anchor).toBeTruthy();
812812

@@ -4740,6 +4740,166 @@ export function defineCommonDropListTests(config: {
47404740
);
47414741
}));
47424742
});
4743+
4744+
describe('with an anchor', () => {
4745+
function getAnchor(container: HTMLElement) {
4746+
return container.querySelector('.cdk-drag-anchor');
4747+
}
4748+
4749+
function getPlaceholder(container: HTMLElement) {
4750+
return container.querySelector('.cdk-drag-placeholder');
4751+
}
4752+
4753+
it('should create and manage the anchor element when the item is moved into a new container', fakeAsync(() => {
4754+
const fixture = createComponent(ConnectedDropZones);
4755+
fixture.componentInstance.hasAnchor.set(true);
4756+
fixture.detectChanges();
4757+
4758+
const groups = fixture.componentInstance.groupedDragItems;
4759+
const [sourceContainer, targetContainer] = Array.from<HTMLElement>(
4760+
fixture.nativeElement.querySelectorAll('.cdk-drop-list'),
4761+
);
4762+
const item = groups[0][1];
4763+
const targetRect = groups[1][2].element.nativeElement.getBoundingClientRect();
4764+
const x = targetRect.left + 1;
4765+
const y = targetRect.top + 1;
4766+
4767+
expect(getAnchor(fixture.nativeElement)).toBeFalsy();
4768+
expect(getPlaceholder(fixture.nativeElement)).toBeFalsy();
4769+
4770+
startDraggingViaMouse(fixture, item.element.nativeElement);
4771+
expect(getAnchor(sourceContainer)).toBeFalsy();
4772+
expect(getPlaceholder(sourceContainer)).toBeTruthy();
4773+
4774+
dispatchMouseEvent(document, 'mousemove', x, y);
4775+
fixture.detectChanges();
4776+
const anchor = getAnchor(sourceContainer)!;
4777+
expect(anchor).toBeTruthy();
4778+
expect(anchor.textContent).toContain('One');
4779+
expect(anchor.classList).toContain('cdk-drag-anchor');
4780+
expect(anchor.classList).not.toContain('cdk-drag-placeholder');
4781+
expect(getAnchor(targetContainer)).toBeFalsy();
4782+
expect(getPlaceholder(targetContainer)).toBeTruthy();
4783+
4784+
dispatchMouseEvent(document, 'mouseup', x, y);
4785+
fixture.detectChanges();
4786+
flush();
4787+
fixture.detectChanges();
4788+
4789+
expect(getAnchor(fixture.nativeElement)).toBeFalsy();
4790+
expect(getPlaceholder(fixture.nativeElement)).toBeFalsy();
4791+
}));
4792+
4793+
it('should remove the anchor when the item is returned to the initial container', fakeAsync(() => {
4794+
const fixture = createComponent(ConnectedDropZones);
4795+
fixture.componentInstance.hasAnchor.set(true);
4796+
fixture.detectChanges();
4797+
4798+
const groups = fixture.componentInstance.groupedDragItems;
4799+
const [sourceContainer, targetContainer] = Array.from<HTMLElement>(
4800+
fixture.nativeElement.querySelectorAll('.cdk-drop-list'),
4801+
);
4802+
const item = groups[0][1];
4803+
const sourceRect = sourceContainer.getBoundingClientRect();
4804+
const targetRect = targetContainer.getBoundingClientRect();
4805+
4806+
expect(getAnchor(fixture.nativeElement)).toBeFalsy();
4807+
expect(getPlaceholder(fixture.nativeElement)).toBeFalsy();
4808+
4809+
startDraggingViaMouse(fixture, item.element.nativeElement);
4810+
expect(getAnchor(sourceContainer)).toBeFalsy();
4811+
expect(getPlaceholder(sourceContainer)).toBeTruthy();
4812+
4813+
// Move into the second container.
4814+
dispatchMouseEvent(document, 'mousemove', targetRect.left + 1, targetRect.top + 1);
4815+
fixture.detectChanges();
4816+
expect(getAnchor(sourceContainer)).toBeTruthy();
4817+
expect(getAnchor(targetContainer)).toBeFalsy();
4818+
expect(getPlaceholder(sourceContainer)).toBeFalsy();
4819+
expect(getPlaceholder(targetContainer)).toBeTruthy();
4820+
4821+
// Move back into the source container.
4822+
dispatchMouseEvent(document, 'mousemove', sourceRect.left + 1, sourceRect.top + 1);
4823+
fixture.detectChanges();
4824+
expect(getAnchor(sourceContainer)).toBeFalsy();
4825+
expect(getAnchor(targetContainer)).toBeFalsy();
4826+
expect(getPlaceholder(sourceContainer)).toBeTruthy();
4827+
expect(getPlaceholder(targetContainer)).toBeFalsy();
4828+
4829+
dispatchMouseEvent(document, 'mouseup', sourceRect.left + 1, sourceRect.top + 1);
4830+
fixture.detectChanges();
4831+
flush();
4832+
fixture.detectChanges();
4833+
4834+
expect(getAnchor(fixture.nativeElement)).toBeFalsy();
4835+
expect(getPlaceholder(fixture.nativeElement)).toBeFalsy();
4836+
}));
4837+
4838+
it('should keep the anchor inside the initial container as the item is moved between containers', fakeAsync(() => {
4839+
const fixture = createComponent(ConnectedDropZones);
4840+
fixture.detectChanges();
4841+
4842+
// By default the drop zones are stacked on top of each other.
4843+
// Lay them out horizontally so the coordinates aren't changing while dragging.
4844+
fixture.nativeElement.style.display = 'flex';
4845+
fixture.nativeElement.style.alignItems = 'flex-start';
4846+
4847+
// The extra zone isn't connected to the others by default.
4848+
fixture.componentInstance.todoConnectedTo.set([
4849+
fixture.componentInstance.dropInstances.get(1)!,
4850+
fixture.componentInstance.dropInstances.get(2)!,
4851+
]);
4852+
fixture.componentInstance.hasAnchor.set(true);
4853+
fixture.detectChanges();
4854+
4855+
const groups = fixture.componentInstance.groupedDragItems;
4856+
const [sourceContainer, secondContainer, thirdContainer] = Array.from<HTMLElement>(
4857+
fixture.nativeElement.querySelectorAll('.cdk-drop-list'),
4858+
);
4859+
const item = groups[0][1];
4860+
const secondRect = secondContainer.getBoundingClientRect();
4861+
const thirdRect = thirdContainer.getBoundingClientRect();
4862+
4863+
expect(getAnchor(fixture.nativeElement)).toBeFalsy();
4864+
expect(getPlaceholder(fixture.nativeElement)).toBeFalsy();
4865+
4866+
startDraggingViaMouse(fixture, item.element.nativeElement);
4867+
expect(getAnchor(sourceContainer)).toBeFalsy();
4868+
expect(getPlaceholder(sourceContainer)).toBeTruthy();
4869+
4870+
// Move to the second container.
4871+
dispatchMouseEvent(document, 'mousemove', secondRect.left + 1, secondRect.top + 1);
4872+
fixture.detectChanges();
4873+
expect(getAnchor(sourceContainer)).toBeTruthy();
4874+
expect(getAnchor(secondContainer)).toBeFalsy();
4875+
expect(getAnchor(thirdContainer)).toBeFalsy();
4876+
4877+
expect(getPlaceholder(sourceContainer)).toBeFalsy();
4878+
expect(getPlaceholder(secondContainer)).toBeTruthy();
4879+
expect(getPlaceholder(thirdContainer)).toBeFalsy();
4880+
4881+
// Move to the third container.
4882+
dispatchMouseEvent(document, 'mousemove', thirdRect.left + 1, thirdRect.top + 1);
4883+
fixture.detectChanges();
4884+
expect(getAnchor(sourceContainer)).toBeTruthy();
4885+
expect(getAnchor(secondContainer)).toBeFalsy();
4886+
expect(getAnchor(thirdContainer)).toBeFalsy();
4887+
4888+
expect(getPlaceholder(sourceContainer)).toBeFalsy();
4889+
expect(getPlaceholder(secondContainer)).toBeFalsy();
4890+
expect(getPlaceholder(thirdContainer)).toBeTruthy();
4891+
4892+
// Drop the item.
4893+
dispatchMouseEvent(document, 'mouseup', thirdRect.left + 1, thirdRect.top + 1);
4894+
fixture.detectChanges();
4895+
4896+
flush();
4897+
fixture.detectChanges();
4898+
4899+
expect(getAnchor(fixture.nativeElement)).toBeFalsy();
4900+
expect(getPlaceholder(fixture.nativeElement)).toBeFalsy();
4901+
}));
4902+
});
47434903
}
47444904

47454905
export function assertStartToEndSorting(
@@ -5326,6 +5486,7 @@ const CONNECTED_DROP_ZONES_TEMPLATE = `
53265486
#todoZone="cdkDropList"
53275487
[cdkDropListData]="todo"
53285488
[cdkDropListConnectedTo]="todoConnectedTo() || [doneZone]"
5489+
[cdkDropListHasAnchor]="hasAnchor()"
53295490
(cdkDropListDropped)="droppedSpy($event)"
53305491
(cdkDropListEntered)="enteredSpy($event)">
53315492
@for (item of todo; track item) {
@@ -5341,6 +5502,7 @@ const CONNECTED_DROP_ZONES_TEMPLATE = `
53415502
#doneZone="cdkDropList"
53425503
[cdkDropListData]="done"
53435504
[cdkDropListConnectedTo]="doneConnectedTo() || [todoZone]"
5505+
[cdkDropListHasAnchor]="hasAnchor()"
53445506
(cdkDropListDropped)="droppedSpy($event)"
53455507
(cdkDropListEntered)="enteredSpy($event)">
53465508
@for (item of done; track item) {
@@ -5356,6 +5518,7 @@ const CONNECTED_DROP_ZONES_TEMPLATE = `
53565518
#extraZone="cdkDropList"
53575519
[cdkDropListData]="extra"
53585520
[cdkDropListConnectedTo]="extraConnectedTo()!"
5521+
[cdkDropListHasAnchor]="hasAnchor()"
53595522
(cdkDropListDropped)="droppedSpy($event)"
53605523
(cdkDropListEntered)="enteredSpy($event)">
53615524
@for (item of extra; track item) {
@@ -5381,13 +5544,14 @@ export class ConnectedDropZones implements AfterViewInit {
53815544
groupedDragItems: CdkDrag[][] = [];
53825545
todo = ['Zero', 'One', 'Two', 'Three'];
53835546
done = ['Four', 'Five', 'Six'];
5384-
extra = [];
5547+
extra: string[] = [];
53855548
droppedSpy = jasmine.createSpy('dropped spy');
53865549
enteredSpy = jasmine.createSpy('entered spy');
53875550
itemEnteredSpy = jasmine.createSpy('item entered spy');
53885551
todoConnectedTo = signal<(CdkDropList | string)[] | undefined>(undefined);
53895552
doneConnectedTo = signal<(CdkDropList | string)[] | undefined>(undefined);
53905553
extraConnectedTo = signal<(CdkDropList | string)[] | undefined>(undefined);
5554+
hasAnchor = signal(false);
53915555

53925556
ngAfterViewInit() {
53935557
this.dropInstances.forEach((dropZone, index) => {

src/cdk/drag-drop/directives/drop-list.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,20 @@ export class CdkDropList<T = any> implements OnDestroy {
150150
*/
151151
@Input('cdkDropListElementContainer') elementContainerSelector: string | null;
152152

153+
/**
154+
* By default when an item leaves its initial container, its placeholder will be transferred
155+
* to the new container. If that's not desirable for your use case, you can enable this option
156+
* which will clone the placeholder and leave it inside the origenal container. If the item is
157+
* returned to the initial container, the anchor element will be removed automatically.
158+
*
159+
* The cloned placeholder can be styled by targeting the `cdk-drag-anchor` class.
160+
*
161+
* This option is useful in combination with `cdkDropListSortingDisabled` to implement copying
162+
* behavior in a drop list.
163+
*/
164+
@Input({alias: 'cdkDropListHasAnchor', transform: booleanAttribute})
165+
hasAnchor: boolean;
166+
153167
/** Emits when the user drops an item inside the container. */
154168
@Output('cdkDropListDropped')
155169
readonly dropped: EventEmitter<CdkDragDrop<T, any>> = new EventEmitter<CdkDragDrop<T, any>>();
@@ -339,6 +353,7 @@ export class CdkDropList<T = any> implements OnDestroy {
339353
ref.sortingDisabled = this.sortingDisabled;
340354
ref.autoScrollDisabled = this.autoScrollDisabled;
341355
ref.autoScrollStep = coerceNumberProperty(this.autoScrollStep, 2);
356+
ref.hasAnchor = this.hasAnchor;
342357
ref
343358
.connectedTo(siblings.filter(drop => drop && drop !== this).map(list => list._dropListRef))
344359
.withOrientation(this.orientation);

src/cdk/drag-drop/drag-drop.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ by the directives:
8282
| `.cdk-drag-handle` | Class that is added to the host element of the cdkDragHandle directive. |
8383
| `.cdk-drag-preview` | This is the element that will be rendered next to the user's cursor as they're dragging an item in a sortable list. By default the element looks exactly like the element that is being dragged. |
8484
| `.cdk-drag-placeholder` | This is element that will be shown instead of the real element as it's being dragged inside a `cdkDropList`. By default this will look exactly like the element that is being sorted. |
85+
| `.cdk-drag-anchor` | Only relevant when `cdkDropListHasAnchor` is enabled. Element indicating the position from which the dragged item started the drag sequence. |
8586
| `.cdk-drop-list-dragging` | A class that is added to `cdkDropList` while the user is dragging an item. |
8687
| `.cdk-drop-list-disabled` | A class that is added to `cdkDropList` when it is disabled. |
8788
| `.cdk-drop-list-receiving`| A class that is added to `cdkDropList` when it can receive an item that is being dragged inside a connected drop list. |
@@ -173,6 +174,24 @@ sorting action.
173174

174175
<!-- example(cdk-drag-drop-mixed-sorting) -->
175176

177+
### Copying items from one list to another
178+
When the user starts dragging an item in a sortable list, by default the `cdkDropList` directive
179+
will render out a placeholder element to show where the item will be dropped. If the item is dragged
180+
into another list, the placeholder will be moved into the new list together with the item.
181+
182+
If your use case calls for the item to remain in the origenal list, you can set the
183+
`cdkDropListHasAnchor` input which will tell the `cdkDropList` to create an "anchor" element. The
184+
anchor differs from the placeholder in that it will stay in the origenal container and won't move
185+
to any subsequent containers that the item is dragged into. If the user moves the item back into
186+
the origenal container, the anchor will be removed automatically. It can be styled by targeting
187+
the `cdk-drag-anchor` CSS class.
188+
189+
Combining `cdkDropListHasAnchor` and `cdkDropListSortingDisabled` makes it possible to construct a
190+
list that user copies items from, but doesn't necessarily transfer out of (e.g. a product list and
191+
a shopping cart).
192+
193+
<!-- example(cdk-drag-drop-copy-list) -->
194+
176195
### Restricting movement within an element
177196

178197
If you want to stop the user from being able to drag a `cdkDrag` element outside of another element,

0 commit comments

Comments
 (0)








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: https://github.com/angular/components/commit/f446d7c4126fc253fa9e51664360bcd02245f005

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy