Feature request
Select multiple cdkDrag elements e.g with a checkbox and drag them to a cdk-drop container.
It's only possible with one cdkDrag element at a time.
Material2 7.0.1
If it's possible somehow I would appreciate a working demo.
It's not possible at the moment, but we have a lot of the foundation work in place already. It still needs an API that allows consumers to start dragging programmatically.
Is there an ETA on this ?
It is possible to achieve this already, by tracking checked elements and in dropped event looking for all the checked elements to transfer (in case the dragged element was checked as well), but still the UI looks wierd as only the element you are dragging is moving across the lists.
+1 for this
+1 here too!
@mlg-pmilic How do you then perform the drag and drop using code? I have experimented a bit with the moveItemInArray function but can't get it to work.
Thanks :)
So I was able to get a pretty decent interaction going for this. I'll detail how multiple selected items were moved at once, I've also got some stuff around multi select and being able to do so with CTRL/shift clicking, etc. but I won't cover all that here.
Markup:
<table class="layout-table">
<tbody cdkDropList
(cdkDropListDropped)="drop($event)"
[class.dragging]="dragging">
<tr class="item"
*ngFor="let item of items; let i = index"
(click)="select($event, i)"
[class.selected]="item.selected"
cdkDrag
(cdkDragStarted)="onDragStart($event)">
<div class="layout-item-drag-preview" *cdkDragPreview>
{{ selections.length > 0 ? selections.length : 1 }}
</div>
<td>{{ item.text }}</td>
</tr>
<tbody>
</table>
In my component:
// imports...
import { CdkDragDrop, CdkDragStart, moveItemInArray } from '@angular/cdk/drag-drop';
// functions in my component...
public onDragStart(event: CdkDragStart<string[]>) {
this.dragging = true;
}
public drop(event: CdkDragDrop<string[]>) {
const selections = [];
// Get the indexes for all selected items
_.each(this.items, (item, i) => {
if (item.selected) {
selections.push(i);
}
});
if (this.selections.length > 1) {
// If multiple selections exist
let newIndex = event.currentIndex;
let indexCounted = false;
// create an array of the selected items
// set newCurrentIndex to currentIndex - (any items before that index)
this.selections = _.sortBy(this.selections, s => s);
const selectedItems = _.map(this.selections, s => {
if (s < event.currentIndex) {
newIndex --;
indexCounted = true;
}
return this.items[s];
});
// correct the index
if (indexCounted) {
newIndex++;
}
// remove selected items
this.items = _.without(this.items, ...selectedItems);
// add selected items at new index
this.items.splice(newIndex, 0, ...selectedItems);
} else {
// If a single selection
moveItemInArray(this.items, event.previousIndex, event.currentIndex);
}
this.dragging = false;
}
Sass styles:
$active: blue;
$frame: gray;
$red: red;
.layout-table {
.layout-item {
background-color: white;
&.selected {
background-color: lighten($active, 15%);
}
}
tbody.dragging {
.layout-item.selected:not(.cdk-drag-placeholder) {
opacity: 0.2;
td {
border-color: rgba($frame, 0.2);
}
}
}
}
.layout-item-drag-preview {
background: $red;
color: white;
font-weight: bold;
padding: 0.25em 0.5em 0.2em;
border-radius: 30px;
display: inline-block;
}
.cdk-drop-list-dragging .cdk-drag {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
So the other selected items get faded out while dragging multiple, removing or display: none
caused the placeholder to be offset from the mouse and this affect seemed less disorientating anyway.
Hope this helps!
@brandonreid Thanks for sharing your solution, it looks nice!
there is one way for me to drag a few elements in one selection, it to create wrapper and push there items which should be dragged.
small pseudo code:
<wrapper cdkDrag *ngIf="selectedItems > 1">
<draggableItem *ngFor="selectedItems">
</draggableItem>
</wrapper>
issue of this, that will be reinitialize of <draggableItem>
in dynamic usage, but mb it can be solved via 'portal' from cdk
not sure that is possible to implement native programmatically dragging under the hood, bcs it require native mouse event and target...
See if this library can help you - https://www.npmjs.com/package/aui-select-box
So I was able to get a pretty decent interaction going for this. I'll detail how multiple selected items were moved at once, I've also got some stuff around multi select and being able to do so with CTRL/shift clicking, etc. but I won't cover all that here.
Markup:
<table class="layout-table"> <tbody cdkDropList (cdkDropListDropped)="drop($event)" [class.dragging]="dragging"> <tr class="item" *ngFor="let item of items; let i = index" (click)="select($event, i)" [class.selected]="item.selected" cdkDrag (cdkDragStarted)="onDragStart($event)"> <div class="layout-item-drag-preview" *cdkDragPreview> {{ selections.length > 0 ? selections.length : 1 }} </div> <td>{{ item.text }}</td> </tr> <tbody> </table>
In my component:
// imports... import { CdkDragDrop, CdkDragStart, moveItemInArray } from '@angular/cdk/drag-drop'; // functions in my component... public onDragStart(event: CdkDragStart<string[]>) { this.dragging = true; } public drop(event: CdkDragDrop<string[]>) { const selections = []; // Get the indexes for all selected items _.each(this.items, (item, i) => { if (item.selected) { selections.push(i); } }); if (this.selections.length > 1) { // If multiple selections exist let newIndex = event.currentIndex; let indexCounted = false; // create an array of the selected items // set newCurrentIndex to currentIndex - (any items before that index) this.selections = _.sortBy(this.selections, s => s); const selectedItems = _.map(this.selections, s => { if (s < event.currentIndex) { newIndex --; indexCounted = true; } return this.items[s]; }); // correct the index if (indexCounted) { newIndex++; } // remove selected items this.items = _.without(this.items, ...selectedItems); // add selected items at new index this.items.splice(newIndex, 0, ...selectedItems); } else { // If a single selection moveItemInArray(this.items, event.previousIndex, event.currentIndex); } this.dragging = false; }
Sass styles:
$active: blue; $frame: gray; $red: red; .layout-table { .layout-item { background-color: white; &.selected { background-color: lighten($active, 15%); } } tbody.dragging { .layout-item.selected:not(.cdk-drag-placeholder) { opacity: 0.2; td { border-color: rgba($frame, 0.2); } } } } .layout-item-drag-preview { background: $red; color: white; font-weight: bold; padding: 0.25em 0.5em 0.2em; border-radius: 30px; display: inline-block; } .cdk-drop-list-dragging .cdk-drag { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); }
So the other selected items get faded out while dragging multiple, removing or
display: none
caused the placeholder to be offset from the mouse and this affect seemed less disorientating anyway.
Hope this helps!
Hey @brandonreid, What was the logic behind the select() function in the html? I'm attempting to implement a similar solution and would love to see what your process was here. Thanks!
@william-holt yeah multi select with shift/ctrl can be pretty mind bending. You've got to track the last item clicked for shift select, track what's been shift selected in case the user shift clicks further, etc. I'll post the code, it's got some comments in there so hopefully it helps.
public dataLayoutItems: Array<DataLayoutItem> = []; // the items in my sortable list
private currentSelectionSpan: number[] = [];
private lastSingleSelection: number;
public selections: number[] = [];
// handles "ctrl/command + a" to select all
@HostListener('document:keydown', ['$event'])
private handleKeyboardEvent(event: KeyboardEvent) {
if (
this.selections.length > 0 &&
(event.key === 'a' && event.ctrlKey ||
event.key === 'a' && event.metaKey) &&
document.activeElement.nodeName !== 'INPUT') {
event.preventDefault();
this.selectAll();
}
}
public select(event, index) {
if (event.srcElement.localName !== 'td' &&
event.srcElement.localName !== 'strong') {
return;
}
let itemSelected = true;
const shiftSelect = event.shiftKey &&
(this.lastSingleSelection || this.lastSingleSelection === 0) &&
this.lastSingleSelection !== index;
if (!this.selections || this.selections.length < 1) {
// if nothing selected yet, init selection mode and select.
this.selections = [index];
this.lastSingleSelection = index;
} else if (event.metaKey || event.ctrlKey) {
// if holding ctrl / cmd
const alreadySelected = _.find(this.selections, s => s === index);
if (alreadySelected) {
_.remove(this.selections, s => s === index);
itemSelected = false;
this.lastSingleSelection = null;
} else {
this.selections.push(index);
this.lastSingleSelection = index;
}
} else if (shiftSelect) {
// if holding shift, add group to selection and currentSelectionSpan
const newSelectionBefore = index < this.lastSingleSelection;
const count = (
newSelectionBefore ? this.lastSingleSelection - (index - 1) :
(index + 1) - this.lastSingleSelection
);
// clear previous shift selection
if (this.currentSelectionSpan && this.currentSelectionSpan.length > 0) {
_.each(this.currentSelectionSpan, i => {
this.dataLayoutItems[i].selected = false;
_.remove(this.selections, s => s === i);
});
this.currentSelectionSpan = [];
}
// build new currentSelectionSpan
_.times(count, c => {
if (newSelectionBefore) {
this.currentSelectionSpan.push(this.lastSingleSelection - c);
} else {
this.currentSelectionSpan.push(this.lastSingleSelection + c);
}
});
// select currentSelectionSpan
_.each(this.currentSelectionSpan, (i) => {
this.dataLayoutItems[i].selected = true;
if (!_.includes(this.selections, i)) {
this.selections.push(i);
}
});
} else {
// Select only this item or clear selections.
const alreadySelected = _.find(this.selections, s => s === index);
if ((!alreadySelected && !event.shiftKey) ||
(alreadySelected && this.selections.length > 1)) {
this.clearSelection();
this.selections = [index];
this.lastSingleSelection = index;
} else if (alreadySelected) {
this.clearSelection();
itemSelected = false;
}
}
if (!event.shiftKey) {
// clear currentSelectionSpan if not holding shift
this.currentSelectionSpan = [];
}
// select clicked item
this.dataLayoutItems[index].selected = itemSelected;
}
public clearSelection() {
_.each(this.dataLayoutItems, (l) => {
if (l.selected) { l.selected = false; }
});
this.selections = [];
this.currentSelectionSpan = [];
this.lastSingleSelection = null;
}
public selectAll() {
this.selections = [];
_.each(this.dataLayoutItems, (l, i) => {
l.selected = true;
this.selections.push(i);
});
this.currentSelectionSpan = [];
this.lastSingleSelection = null;
}
@william-holt yeah multi select with shift/ctrl can be pretty mind bending. You've got to track the last item clicked for shift select, track what's been shift selected in case the user shift clicks further, etc. I'll post the code, it's got some comments in there so hopefully it helps.
public dataLayoutItems: Array<DataLayoutItem> = []; // the items in my sortable list private currentSelectionSpan: number[] = []; private lastSingleSelection: number; public selections: number[] = []; // handles "ctrl/command + a" to select all @HostListener('document:keydown', ['$event']) private handleKeyboardEvent(event: KeyboardEvent) { if ( this.selections.length > 0 && (event.key === 'a' && event.ctrlKey || event.key === 'a' && event.metaKey) && document.activeElement.nodeName !== 'INPUT') { event.preventDefault(); this.selectAll(); } } public select(event, index) { if (event.srcElement.localName !== 'td' && event.srcElement.localName !== 'strong') { return; } let itemSelected = true; const shiftSelect = event.shiftKey && (this.lastSingleSelection || this.lastSingleSelection === 0) && this.lastSingleSelection !== index; if (!this.selections || this.selections.length < 1) { // if nothing selected yet, init selection mode and select. this.selections = [index]; this.lastSingleSelection = index; } else if (event.metaKey || event.ctrlKey) { // if holding ctrl / cmd const alreadySelected = _.find(this.selections, s => s === index); if (alreadySelected) { _.remove(this.selections, s => s === index); itemSelected = false; this.lastSingleSelection = null; } else { this.selections.push(index); this.lastSingleSelection = index; } } else if (shiftSelect) { // if holding shift, add group to selection and currentSelectionSpan const newSelectionBefore = index < this.lastSingleSelection; const count = ( newSelectionBefore ? this.lastSingleSelection - (index - 1) : (index + 1) - this.lastSingleSelection ); // clear previous shift selection if (this.currentSelectionSpan && this.currentSelectionSpan.length > 0) { _.each(this.currentSelectionSpan, i => { this.dataLayoutItems[i].selected = false; _.remove(this.selections, s => s === i); }); this.currentSelectionSpan = []; } // build new currentSelectionSpan _.times(count, c => { if (newSelectionBefore) { this.currentSelectionSpan.push(this.lastSingleSelection - c); } else { this.currentSelectionSpan.push(this.lastSingleSelection + c); } }); // select currentSelectionSpan _.each(this.currentSelectionSpan, (i) => { this.dataLayoutItems[i].selected = true; if (!_.includes(this.selections, i)) { this.selections.push(i); } }); } else { // Select only this item or clear selections. const alreadySelected = _.find(this.selections, s => s === index); if ((!alreadySelected && !event.shiftKey) || (alreadySelected && this.selections.length > 1)) { this.clearSelection(); this.selections = [index]; this.lastSingleSelection = index; } else if (alreadySelected) { this.clearSelection(); itemSelected = false; } } if (!event.shiftKey) { // clear currentSelectionSpan if not holding shift this.currentSelectionSpan = []; } // select clicked item this.dataLayoutItems[index].selected = itemSelected; } public clearSelection() { _.each(this.dataLayoutItems, (l) => { if (l.selected) { l.selected = false; } }); this.selections = []; this.currentSelectionSpan = []; this.lastSingleSelection = null; } public selectAll() { this.selections = []; _.each(this.dataLayoutItems, (l, i) => { l.selected = true; this.selections.push(i); }); this.currentSelectionSpan = []; this.lastSingleSelection = null; }
Can you please produce a working demo ??
StackBlitz ??
Hi, not going to quote your post again @brandonreid, but I would appreciate a StackBlitz demo as well if you can find the time!
Thank you for posting your code @brandonreid !! I've adapted your code somewhat but wanted to post back to help others too.
ng-container
/ ng-template
so that the items can be styled in any way by the user (grid, list, etc.). cdkDropListGroup
itemsAdded
, itemsRemoved
and itemsUpdated
selectionChanged
https://stackblitz.com/edit/angular-multi-drag-drop
Example usage:
<multi-drag-drop [items]="['a', 'b', 'c', 'd']">
<ng-template let-item>
--{{ item }}--
</ng-template>
</multi-drag-drop>
multi-drag-drop.component.html
:
<div
class="drop-list"
cdkDropList
[class.item-dragging]="dragging"
(cdkDropListDropped)="droppedIntoList($event)"
>
<div
*ngFor="let item of items; let index = index"
class="item"
[class.selected]="isSelected(index)"
cdkDrag
(cdkDragStarted)="dragStarted($event, index)"
(cdkDragEnded)="dragEnded()"
(cdkDragDropped)="dropped($event)"
(click)="select($event, index)"
>
<div *ngIf="!dragging || !isSelected(index)">
<ng-container
*ngTemplateOutlet="templateRef; context: { $implicit: item, item: item, index: index }"
></ng-container>
<div *cdkDragPreview>
<div class="select-item-drag-preview float-left">
{{ selections.length || 1 }}
</div>
<ng-container
*ngTemplateOutlet="templateRef; context: { $implicit: item, item: item, index: index }"
></ng-container>
</div>
</div>
</div>
</div>
multi-drag-drop.component.scss
:
.drop-list {
min-height: 10px;
min-width: 10px;
height: 100%;
width: 100%;
border: 1px #393E40 solid;
overflow-y: scroll;
}
.item {
border: 0 dotted #393E40;
border-width: 0 0 1px 0;
cursor: grab;
padding: 3px;
&.selected {
background-color: rgba(144, 171, 200, 0.5);
}
}
.item-dragging {
.item.selected:not(.cdk-drag-placeholder) {
opacity: 0.3;
}
}
.select-item-drag-preview {
background-color: rgba(204, 0, 102, 0);
font-weight: bold;
border: 2px solid #666;
border-radius: 50%;
display: inline-block;
width: 30px;
line-height: 30px;
text-align: center;
}
.cdk-drop-list-dragging .cdk-drag {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
multi-drag-drop.component.ts
:
import { CdkDragDrop, CdkDragStart } from '@angular/cdk/drag-drop/typings/drag-events';
import { DragRef } from '@angular/cdk/drag-drop/typings/drag-ref';
import {
ChangeDetectionStrategy, ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
EventEmitter,
HostListener,
Input,
Output,
TemplateRef
} from '@angular/core';
import * as _ from 'lodash';
@Component({
selector: 'multi-drag-drop',
templateUrl: './multi-drag-drop.component.html',
styleUrls: ['./multi-drag-drop.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MultiDragDropComponent {
@Input() items: any[];
@Output() itemsRemoved = new EventEmitter<any[]>();
@Output() itemsAdded = new EventEmitter<any[]>();
@Output() itemsUpdated = new EventEmitter<any[]>();
@Output() selectionChanged = new EventEmitter<any[]>();
@ContentChild(TemplateRef, { static: false }) templateRef;
public dragging: DragRef = null;
public selections: number[] = [];
private currentSelectionSpan: number[] = [];
private lastSingleSelection: number;
constructor(
private eRef: ElementRef,
private cdRef: ChangeDetectorRef,
) {
}
dragStarted(ev: CdkDragStart, index: number): void {
this.dragging = ev.source._dragRef;
const indices = this.selections.length ? this.selections : [index];
ev.source.data = {
indices,
values: indices.map(i => this.items[i]),
source: this,
};
this.cdRef.detectChanges();
}
dragEnded(): void {
this.dragging = null;
this.cdRef.detectChanges();
}
dropped(ev: CdkDragDrop<any>): void {
if (!ev.isPointerOverContainer || !_.get(ev, 'item.data.source')) {
return;
}
const data = ev.item.data;
if (data.source === this) {
const removed = _.pullAt(this.items, data.indices);
if (ev.previousContainer !== ev.container) {
this.itemsRemoved.emit(removed);
this.itemsUpdated.emit(this.items);
}
}
this.dragging = null;
setTimeout(() => this.clearSelection());
}
droppedIntoList(ev: CdkDragDrop<any>): void {
if (!ev.isPointerOverContainer || !_.get(ev, 'item.data.source')) {
return;
}
const data = ev.item.data;
let spliceIntoIndex = ev.currentIndex;
if (ev.previousContainer === ev.container && this.selections.length > 1) {
this.selections.splice(-1, 1);
const sum = _.sumBy(this.selections, selectedIndex => selectedIndex <= spliceIntoIndex ? 1 : 0);
spliceIntoIndex -= sum;
}
this.items.splice(spliceIntoIndex, 0, ...data.values);
if (ev.previousContainer !== ev.container) {
this.itemsAdded.emit(data.values);
}
this.itemsUpdated.emit(this.items);
setTimeout(() => this.cdRef.detectChanges());
}
isSelected(i: number): boolean {
return this.selections.indexOf(i) >= 0;
}
select(event, index) {
const shiftSelect = event.shiftKey &&
(this.lastSingleSelection || this.lastSingleSelection === 0) &&
this.lastSingleSelection !== index;
if (!this.selections || this.selections.length < 1) {
// if nothing selected yet, init selection mode and select.
this.selections = [index];
this.lastSingleSelection = index;
} else if (event.metaKey || event.ctrlKey) {
// if holding ctrl / cmd
const alreadySelected = _.find(this.selections, s => s === index);
if (alreadySelected) {
_.remove(this.selections, s => s === index);
this.lastSingleSelection = null;
} else {
this.selections.push(index);
this.lastSingleSelection = index;
}
} else if (shiftSelect) {
// if holding shift, add group to selection and currentSelectionSpan
const newSelectionBefore = index < this.lastSingleSelection;
const count = (
newSelectionBefore ? this.lastSingleSelection - (index - 1) :
(index + 1) - this.lastSingleSelection
);
// clear previous shift selection
if (this.currentSelectionSpan && this.currentSelectionSpan.length > 0) {
_.each(this.currentSelectionSpan, i => {
_.remove(this.selections, s => s === i);
});
this.currentSelectionSpan = [];
}
// build new currentSelectionSpan
_.times(count, c => {
if (newSelectionBefore) {
this.currentSelectionSpan.push(this.lastSingleSelection - c);
} else {
this.currentSelectionSpan.push(this.lastSingleSelection + c);
}
});
// select currentSelectionSpan
_.each(this.currentSelectionSpan, (i) => {
if (!_.includes(this.selections, i)) {
this.selections.push(i);
}
});
} else {
// Select only this item or clear selections.
const alreadySelected = _.find(this.selections, s => s === index);
if ((!alreadySelected && !event.shiftKey) ||
(alreadySelected && this.selections.length > 1)) {
this.clearSelection();
this.selections = [index];
this.lastSingleSelection = index;
} else if (alreadySelected) {
this.clearSelection();
}
}
if (!event.shiftKey) {
this.currentSelectionSpan = [];
}
this.selectionChanged.emit(this.selections.map(i => this.items[i]));
this.cdRef.detectChanges();
}
clearSelection() {
if (this.selections.length) {
this.selections = [];
this.currentSelectionSpan = [];
this.lastSingleSelection = null;
this.selectionChanged.emit(this.selections.map(i => this.items[i]));
this.cdRef.detectChanges();
}
}
selectAll() {
if (this.selections.length !== this.items.length) {
this.selections = _.map(this.items, (item, i) => i);
this.currentSelectionSpan = [];
this.lastSingleSelection = null;
this.selectionChanged.emit(this.items);
this.cdRef.detectChanges();
}
}
// handles "ctrl/command + a" to select all
@HostListener('document:keydown', ['$event'])
private handleKeyboardEvent(event: KeyboardEvent) {
if (event.key === 'a' &&
(event.ctrlKey || event.metaKey) &&
this.selections.length &&
document.activeElement.nodeName !== 'INPUT'
) {
event.preventDefault();
this.selectAll();
} else if (event.key === 'Escape' && this.dragging) {
this.dragging.reset();
document.dispatchEvent(new Event('mouseup'));
}
}
@HostListener('document:click', ['$event'])
private clickout(event) {
if (this.selections.length && !this.eRef.nativeElement.contains(event.target)) {
this.clearSelection();
}
}
}
@6utt3rfly looks good mate, would you like to share a stackblitz or a repo for this to demonstrate the working solution. ?.
@6utt3rfly thanks for your result.
I've found a bug, some idea how to fix it?
Bug
List of numbers
Choose first three numbers
Try to sort them behind the 4
Wrong result
The result should be: 4,1,2,3,5,6,7,8,9,10
@tk2232 : Can you check now (updated comment above and stackblitz)?
That looks good. Do you have any ideas on how to change the preview so that the elements appear in a row like moving a single element?
You could modify the cdkDragPreview. Right now it's the selection length, plus the last item selected. But you could show all items by using ng-container multiple times. You would have to play with styling, but it would be something like:
<div *cdkDragPreview>
<div class="select-item-drag-preview float-left">
{{ selections.length || 1 }}
</div>
<div *ngFor="let sel of selections">
<ng-container
*ngTemplateOutlet="templateRef; context: { $implicit: sel, item: sel, index: index }"
></ng-container>
</div>
</div>
The last time I was here I did not have the stackblitz working, So I started making my own solution which uses CSS classes to work. Well, I'll at least comment on this solution.
Working: stackblitz.com/edit/angular-multi-dragdrop
I use two main objects, that govern everything from multidrag, both are independent of each other:
I used the example of "_Drag & Drop connected sorting group_" from the site material.angular.io/cdk/drag-drop/examples as a base.
.selected{
border: 1px solid green!important;
}
.selected.hide{
opacity: 0.3;
}
import {CdkDragDrop, CdkDragStart, CdkDragEnd, moveItemInArray, transferArrayItem} from '@angular/cdk/drag-drop';
drop(event: CdkDragDrop<string[]>) {
// If current element has ".selected"
if(event.item.element.nativeElement.classList.contains("selected")){
this.multiDrag.dropListDropped(event);
}
// If dont have ".selected" (normal case)
else{
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
transferArrayItem(event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex);
}
}
}
// Multi Select
multiSelect = { // Put ".selected" on elements when clicking after longPress or Ctrl+Click
// Initial Variables
longPressTime: 500, // in ms unit
verifyLongPress: 0,
multiSelect: false,
verifyDragStarted: false,
ctrlMode: false,
firstContainer: null as unknown as HTMLElement,
selectDrag(el: HTMLElement) {
while (!el.classList.contains("cdk-drag")) {
el = el.parentElement as HTMLElement;
}
return el;
},
mouseDown(e: Event) {
let target = this.selectDrag(e.target as HTMLElement);
let ctrlKey = (e as KeyboardEvent).ctrlKey;
if (this.multiSelect) { // If multiSelect is enabled
/* The responsibility for removing only the first ".selected" has to be with mouseDown and not with mouseUp.
if not you can't add the first one */
// Remove
let allSelected = document.querySelectorAll(".selected").length;
if (allSelected == 1 && target.classList.contains("selected") && (this.ctrlMode ? ctrlKey : true)) { // If only have one ".selected" and it was clicked
target.classList.remove("selected", "last"); // remove ".selected" and ".last"
this.multiSelect = false; // turns off multiSelect
}
}
else { // If multiSelect is disabled
// Do this
let addSelected = () => {
this.multiSelect = true; // enable multiSelect
this.firstContainer = target.parentElement as HTMLElement; //saves the container of the first selected element
target.classList.add("selected", "last"); // and add ".selected" and ".last" to the current element clicked
}
// If using CTRL
if (ctrlKey) {
this.ctrlMode = true;
addSelected();
};
// If using longPress
this.verifyLongPress = <any>setTimeout(() => { // If there is a LongPress
this.ctrlMode = false;
addSelected();
}, this.longPressTime); // after "longPressTime"(ms)
}
},
mouseUp(e: Event) {
clearTimeout(this.verifyLongPress); // cancel LongPress
if (this.multiSelect && !this.verifyDragStarted) { // If multiSelect is enabled AND not start DragStarted
let target = this.selectDrag(e.target as HTMLElement);
let allSelected = document.querySelectorAll(".selected");
let ctrlKey = (e as KeyboardEvent).ctrlKey;
let last = document.querySelector(".last");
// If use Shift
if (last && (e as KeyboardEvent).shiftKey) {
// take range informations
let containerLast = Array.from(last.parentElement!.children);
let lastIndex = containerLast.indexOf(last);
let currIndex = containerLast.indexOf(target);
let max = Math.max(lastIndex, currIndex);
let min = Math.min(lastIndex, currIndex);
// toggle .selected in the range
for (let i = min; i <= max; i++) {
if (i != lastIndex) { // Does not toggle the penult element clicked
containerLast[i].classList.toggle("selected");
}
}
// put .last if last clicked was selected at end
if (target.classList.contains("selected")) {
last && last.classList.remove("last"); // remove .last from penult element clicked
target.classList.add("last"); // and add ".last" to the current element
}
}
//If don't use shift
else {
// To remove from selection
/* responsibility to remove from selection assigned to mouseUp */
if (target.classList.contains("selected") && allSelected.length > 1 && (this.ctrlMode ? ctrlKey : true)) { // If the clicked element already has ".selected" AND If you have more than 1 (not to remove the first one added)
target.classList.remove("selected"); // remove ".selected"
target.classList.remove("last"); // remove ".last"
}
// To add to selection
else { // if the clicked element does not have the ".selected"
if (this.firstContainer == target.parentElement && (this.ctrlMode ? ctrlKey : true)) { //if the item click is made within the same container
last && last.classList.remove("last"); // remove .last from penult element clicked
target.classList.add("selected", "last"); // add ".selected" and ".last"
}
else if (this.ctrlMode ? ctrlKey : true) { // if in different container, and with ctrl (if ctrl)
allSelected.forEach((el) => { // remove all selected from last container
el.classList.remove("selected", "hide", "last");
});
this.firstContainer = target.parentElement as HTMLElement; //saves the container of the new selected element
target.classList.add("selected", "last"); // and add ".selected" to the element clicked in the new container
}
}
}
}
},
dragStarted() {
this.verifyDragStarted = true; // shows to mouseDown and mouseUp that Drag started
clearTimeout(this.verifyLongPress); // cancel longPress
},
dragEnded() {
this.verifyDragStarted = false; // show mouseDown and mouseUp that Drag is over
},
dropListDropped(e: CdkDragDrop<string[]>) {
let el = e.item.element.nativeElement;
if (el.classList.contains("selected")) { // the dragged element was of the "selected" class
this.multiSelect = false; // disable multiSelect
}
},
}
// Multi Drag
multiDrag = { // Adjusts clicked items that have ".selected" to organize together
// Initial Variables
dragList: [""], // has the value of the selected items in sequence from listData
dragListCopy: [""], // a copy of the listData, but with the selected elements marked with "DragErase" to delete later
dragErase: Symbol("DragErase") as any, // a symbol to have unique value when deleting
dragStarted(e: CdkDragStart) {
if (e.source.element.nativeElement.classList.contains("selected")) { // If the dragged element has ".selected"
//prepare
let listData = e.source.dropContainer.data; // get list data value
this.dragList = []; // reset the dragList
this.dragListCopy = [...listData]; // copy listData into variable
let DOMdragEl = e.source.element.nativeElement; // dragged element
let DOMcontainer = Array.from(DOMdragEl.parentElement!.children); // container where all draggable elements are
let DOMdragElIndex = DOMcontainer.indexOf(DOMdragEl); // index of the dragged element
let allSelected = document.querySelectorAll(".selected"); // get all the ".selected"
// Goes through all ".selected"
allSelected.forEach((eli) => {
// get index of current element
let CurrDOMelIndexi = DOMcontainer.indexOf(eli);
// Add listData of current ".selected" to dragList
this.dragList.push(listData[CurrDOMelIndexi]);
// Replaces current position in dragListCopy with "DragErase" (to erase exact position later)
this.dragListCopy[CurrDOMelIndexi] = this.dragErase;
// Put opacity effect (by CSS class ".hide") on elements (after starting Drag)
if (DOMdragElIndex !== CurrDOMelIndexi) {
eli.classList.add("hide");
}
});
}
},
dropListDropped(e: CdkDragDrop<string[]>) {
if (e.previousContainer === e.container) { // If in the same container
let posAdjust = e.previousIndex < e.currentIndex ? 1 : 0; // Adjusts the placement position
this.dragListCopy.splice(e.currentIndex + posAdjust, 0, ...this.dragList); // put elements in dragListCopy
this.dragListCopy = this.dragListCopy.filter((el) => (el !== this.dragErase)); // remove the "DragErase" from the list
// Pass item by item to final list
for (let i = 0; i < e.container.data.length; i++) {
e.container.data[i] = this.dragListCopy[i];
}
}
else { // If in different containers
// remove the "DragErase" from the list
this.dragListCopy = this.dragListCopy.filter((el) => (el !== this.dragErase));
// Pass item by item to initial list
for (let i = 0; i < e.previousContainer.data.length; i++) {
e.previousContainer.data[i] = this.dragListCopy[i];
}
for (let i = 0; i < this.dragList.length; i++) {
e.previousContainer.data.pop();
}
let otherListCopy = [...e.container.data]; // list of new container
otherListCopy.splice(e.currentIndex, 0, ...this.dragList); // put elements in otherListCopy
// Pass item by item to final list
for (let i = 0; i < otherListCopy.length; i++) {
e.container.data[i] = otherListCopy[i];
}
}
// Remove ".hide"
let allHidden = document.querySelectorAll(".hide");
allHidden.forEach((el) => {
el.classList.remove("hide");
});
// Remove ".selected" after 300ms
setTimeout(() => {
let allSelected = document.querySelectorAll(".selected");
allSelected.forEach((el) => {
el.classList.remove("selected", "last");
});
}, 300);
this.dragListCopy = []; // reset the dragListCopy
this.dragList = []; // reset the dragList
},
}
multiDrag.dragStarted($event)
object in "cdkDragStarted"Regarding the last 2 items, my code looked like this (if you want to place objects elsewhere, note the "multiDrag" and "multiSelect" object call here):
<div class="example-list"
cdkDropList
[cdkDropListData]="todo"
(cdkDropListDropped)="drop($event);multiSelect.dropListDropped($event)"
>
<div class="example-box" *ngFor="let item of todo"
cdkDrag
(pointerdown)="multiSelect.mouseDown($event)"
(pointerup)="multiSelect.mouseUp($event)"
(cdkDragStarted)="multiSelect.dragStarted();multiDrag.dragStarted($event)"
(cdkDragEnded)="multiSelect.dragEnded()"
>
{{item}}
</div>
</div>
Hi @6utt3rfly, thanks for your solutions, am currently using it and modified few places here and there, i was wondering if you have tried adding a virtual scroll in this particular solution if so, would you mind sharing. Thanks again :-)
Hi @6utt3rfly, thanks for your solutions, am currently using it and modified few places here and there, i was wondering if you have tried adding a virtual scroll in this particular solution if so, would you mind sharing. Thanks again :-)
@JoelKap I haven't tried virtual scroll (my use case has rather small data sets). I'm not sure if *cdkVirtualFor works...?? I quickly tried and it didn't seem to let me drag out of the viewport. I also found a S/O question that's similar. Feel free to fork the stackblitz and share a working example if you figure it out!
Hi,
Thanks for solution. I add/change some features and it's work how i want. In example:
...
removeAllSelected() {
const allSelected = document.querySelectorAll('.selected');
allSelected.forEach((el) => {
el.classList.remove('selected');
});
},
click(e: any, targetList: string, obj: any, list: any) {
const target = e.target;
if (!(this.selectedList === targetList)) {
this.removeAllSelected();
this.selectedList = targetList;
}
if (e.ctrlKey) {
...
i think it's needed to check if seleceted item is from the same list or unselect all
Thank you very much @olek0012!!
On my website I don't use multiple lists, I did it thinking about the future, but thank you very much for reporting this error, I already solved it, and I also added the click mode with Ctrl in parallel, so it is now compatible with both mobile and desktop! (I hadn't done it before because I was focusing on a mobile application, but it's there now too ;D )
Changes:
- Added a variable inside multiSelect with the name "firstContainer" and it is used when adding items to see if they are all in the same container
- If it is from another container I unselect all and select the new item on other container.
- I noticed that there was another mistake that didn't let me select when dragging some other item, so I put the event entry in the dropListDropped and I always ask if it has the "selected" class to see if I remove the multiple selection mode or not
- Adjusted some types
- Added the mouse with Ctrl in parallel with longPress, the method you start using to select, defines how the rest of the selection will be, and changed from "click" to "pointer", so it is compatible with any device (touch screens and computers)
If you encounter any problems or have any questions please let me know! Thank you! ^^
Hi @KevynTD I'm using your solution, nice work. I am trying to figure out how can I customize the content of the list with CSS without have multi objects to "select". Ex: I need 2 different font sizes for 2 different items.
if I add one div on {{item}} it will make it as an object to select. How can I avoid that?
Well noticed @FcKoOl !
Thank you very much for the report!
I was taking the element directly from the event, and the event will not always come from the draggable element, as you commented, it may be from some of his children. So to fix it I put a function inside the multiSelect to always get the draggable element, and called this function in two parts inside multiSelect.
selectDrag(el:HTMLElement){
while(!el.classList.contains("cdk-drag")){
el = el.parentElement as HTMLElement;
}
return el;
},
The Code is already updated on stackBlitz and here ;)
Updated code with Shift function, here and on the stackblitz.
Now you can use both Shift and Ctrl on the PC, as well as on mobile use the longTap at the same time
@manabshy - https://stackblitz.com/edit/angular-multi-drag-drop
Hey thanks for your awesome work.. Could you please help in resolving a bug i found when i was using this?
Steps to reproduce:
Note: This is happening only where the items array input is given as empty array "[ ]"
@manabshy And also am getting this error very often from the below line of code.
setTimeout(() => this.clearSelection());
ERROR Error: ViewDestroyedError: Attempt to use a destroyed view: detectChanges.
Do we need this setTimout()?
Hey thanks for your awesome work.. Could you please help in resolving a bug i found when i was using this?
Do we need this setTimout()?
@nelsonfernandoe - I took a look today and just updated packages and then the 3-list CDK drag/drop works with no other code changes. The setTimeout would have be added to fix an "Expression has changed after it was checked" error, but if they aren't happening then it looks like it's okay without. Maybe it depends on how the component is used?
Anyway here's an updated stackblitz forked from the first one:
https://stackblitz.com/edit/angular-multi-drag-drop-3part
That looks good. Do you have any ideas on how to change the preview so that the elements appear in a row like moving a single element?
You could modify the cdkDragPreview. Right now it's the selection length, plus the last item selected. But you could show all items by using ng-container multiple times. You would have to play with styling, but it would be something like:
<div *cdkDragPreview> <div class="select-item-drag-preview float-left"> {{ selections.length || 1 }} </div> <div *ngFor="let sel of selections"> <ng-container *ngTemplateOutlet="templateRef; context: { $implicit: sel, item: sel, index: index }" ></ng-container> </div> </div>
@tk2232 - I got @6utt3rfly's solution to work with a little tweaking. I had to update the $implicit: sel
to $implicit items[sel]
to get the item instead of the index.
https://stackblitz.com/edit/angular-multi-drag-drop-preview-all-selected
@manabshy - https://stackblitz.com/edit/angular-multi-drag-drop
dear author of angular-multi-drag-drop @6utt3rfly , your code is awesome, but I was trying to figure out how to limit the drag drop to be one way only from ListA to ListB with cdkDropListEnterPredicate, but I dunno where I did wrong, the enterPredicate just doesn't get triggered...
not sure if anyone can help me to take a look at my stackblitz... why my cdkDropListEnterPredicate implementation does not work, huge thanks to you guys first.
https://stackblitz.com/edit/angular-multi-drag-drop-3part-vxkzje?file=src/app/multi-drag-drop.component.ts
@0988jaylin - according to the Drag and Drop API, cdkDropListEnterPredicate
is an Input
to the CdkDropList
directive. So you would have to move it to the outer div (where the CdkDropList
directive is defined), and you would have to write it as an input ([cdkDropListEnterPredicate]="hasEnterPredicate"
) rather than an ouput (()
).
If you want to prevent dragging from a list (instead of only preventing dropping by the cdkDropListEnterPredicate), you might want to use one of the available ...Disabled
inputs (and maybe add it as an input to the MultiDragDrop component).
@0988jaylin - according to the Drag and Drop API,
cdkDropListEnterPredicate
is anInput
to theCdkDropList
directive. So you would have to move it to the outer div (where theCdkDropList
directive is defined), and you would have to write it as an input ([cdkDropListEnterPredicate]="hasEnterPredicate"
) rather than an ouput (()
).If you want to prevent dragging from a list (instead of only preventing dropping by the cdkDropListEnterPredicate), you might want to use one of the available
...Disabled
inputs (and maybe add it as an input to the MultiDragDrop component).
@6utt3rfly Shelly, wow thank you so much for the help! but I'm facing another problem...
does this mean I need to put all the enter predicate logic within that multi-drag-drop
child component's hasEnterPredicate()
function? It sounds a bit weird. The correct way should be to assign different predicate for each <multi-drag-drop>
inside AppComponent
and write different predicate implementations within app.component.ts
.
But I dont know how to pass cdkDropListEnterPredicate
function from app.component.ts
to multi-drag-drop.component.ts
... I tried the @output, EventEmitter and CallBack, didnt work out...
Can you give me some hint on this? thanks so much.
Most helpful comment
So I was able to get a pretty decent interaction going for this. I'll detail how multiple selected items were moved at once, I've also got some stuff around multi select and being able to do so with CTRL/shift clicking, etc. but I won't cover all that here.
Markup:
In my component:
Sass styles:
So the other selected items get faded out while dragging multiple, removing or
display: none
caused the placeholder to be offset from the mouse and this affect seemed less disorientating anyway.Hope this helps!