angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.24k stars 6.7k forks source link

Drag and drop multiple cdkDrag elements #13807

Open thomaslein opened 5 years ago

thomaslein commented 5 years ago

Bug, feature request, or proposal

Feature request

What is the expected behavior?

Select multiple cdkDrag elements e.g with a checkbox and drag them to a cdk-drop container.

What is the current behavior?

It's only possible with one cdkDrag element at a time.

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Material2 7.0.1

Is there anything else we should know?

If it's possible somehow I would appreciate a working demo.

crisbeto commented 5 years ago

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.

cedvdb commented 5 years ago

Is there an ETA on this ?

mlg-pmilic commented 5 years ago

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.

RobinBomkampDv commented 5 years ago

+1 for this

Diemauerdk commented 5 years ago

+1 here too!

Diemauerdk commented 5 years ago

@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 :)

brandonreid commented 5 years ago

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. Mar-14-2019 09-58-47

Hope this helps!

Diemauerdk commented 5 years ago

@brandonreid Thanks for sharing your solution, it looks nice!

vyacheslavzhabitsky commented 5 years ago

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...

srikanthmadasu commented 5 years ago

See if this library can help you - https://www.npmjs.com/package/aui-select-box

william-holt commented 5 years ago

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. Mar-14-2019 09-58-47

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!

brandonreid commented 5 years ago

@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;
}
Anzil-Aufait commented 4 years ago

@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 ??

jnamdar commented 4 years ago

Hi, not going to quote your post again @brandonreid, but I would appreciate a StackBlitz demo as well if you can find the time!

6utt3rfly commented 4 years ago

Thank you for posting your code @brandonreid !! I've adapted your code somewhat but wanted to post back to help others too.

[Edit]: Added a stackblitz demo 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();
    }
  }
}
manabshy commented 4 years ago

@6utt3rfly looks good mate, would you like to share a stackblitz or a repo for this to demonstrate the working solution. ?.

6utt3rfly commented 4 years ago

@manabshy - https://stackblitz.com/edit/angular-multi-drag-drop

tk2232 commented 4 years ago

@6utt3rfly thanks for your result. I've found a bug, some idea how to fix it?

Bug List of numbers image

Choose first three numbers image

Try to sort them behind the 4 image

Wrong result image

The result should be: 4,1,2,3,5,6,7,8,9,10

6utt3rfly commented 4 years ago

@tk2232 : Can you check now (updated comment above and stackblitz)?

tk2232 commented 4 years ago

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?

6utt3rfly commented 4 years ago

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>
KevynTD commented 4 years ago

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.

Example:

dragdrop multidrag 1 Working: stackblitz.com/edit/angular-multi-dragdrop

Short introduction to operation:

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.

To deploy you need (6 steps) :

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>

Final Considerations

  1. If anyone finds a mistake warns me please that I'm wanting to put into production hehe
  2. A limitation is that the multidrag is only prepared to change between two lists, so it is not possible to select more than one list to change the position items, for example you cannot simultaneously take an item from list1, an item from list 2 and move both of them to list 3 at the same time, in which case you would have to move from list 1 to list 2, and then the two items to list 3.
JoelKap commented 4 years ago

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 :-)

6utt3rfly commented 4 years ago

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!

olek0012 commented 4 years ago

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) {
...
KevynTD commented 4 years ago

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 )

multidrag3

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! ^^

FcKoOl commented 4 years ago

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?

imagem

KevynTD commented 4 years ago

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 ;)


Update 2020-08-31:

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

nelsonfernandoe commented 3 years ago

@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:

  1. Please create 3 cdk drag list.
  2. Assign empty array to 2 drag list.
  3. Drag one item (from draglist1) into one empty drag list (draglist2) container
  4. click anywhere else in the screen, it will auto populate the 3rd empty drag list (draglist3) container

Note: This is happening only where the items array input is given as empty array "[ ]"

nelsonfernandoe commented 3 years ago

@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()?

6utt3rfly commented 3 years ago

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

cofad commented 3 years ago

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

0988jaylin commented 3 years ago

@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

6utt3rfly commented 3 years ago

@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 commented 3 years ago

@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).

@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.

calvinturbo commented 1 year ago

For anyone looking to use the solution provided by @KevynTD with newer versions of Angular, you will notice it being broken. I have looked into it and you need to apply the following changes to make it work properly again:

Make these changes in cdk-drag-drop-connected-sorting-group-example.ts:

allSelected.forEach((eli: HTMLElement) => { becomes allSelected.forEach((eli: HTMLElement) => { if (eli.parentNode !== document.body) { don't forget to add an ending curly bracket to end the new if statement

let DOMdragEl = e.source.element.nativeElement; // dragged element becomes const id = e.source.element.nativeElement.dataset.id; let DOMdragEl = document.querySelectorAll("[data-id='" + id + "'].cdk-drag-placeholder")[0];

Add [attr.data-id]="item" to the div element with class "example-box" in the 'cdk-drag-drop-connected-sorting-group-example.html' file.

Probably not the most elegant way to do this, but it will work perfectly fine again on Angular 14/15.