Drag and Drop com múltiplas seleções

Eu também achei que o artigo anterior estava muito fácil, por esse motivo iremos aplicar mais funcionalidades ao nosso Drag and Drop!

O que iremos fazer?

Dessa vez vamos melhorar um pouco o código feito no artigo anterior, a ideia agora é permitir que o usuário possa selecionar mais de um item e arrastá-los para a outra lista, mas vamos aproveitar para organizar nosso código e criar objetos que vão nos auxiliar nessa tarefa.

Talvez vocês estejam se perguntando, mas por que fazer isso? A resposta é simples, a tecnologia utilizada aqui não possui esta funcionalidade (pelo menos não até a versão que estamos estudando) e também uma hora ou outra você vai precisar arrastar múltiplos.

O que precisamos saber?

Leiam os comentários, todos eles sem exceção para a compreensão completa acerca deste artigo.

No arquivo drag-and-drop.component.css será necessário criar uma classe que muda a cor de fundo e a cor de texto do item selecionado, dessa forma iremos destacá-lo dos demais.

drag-and-drop.component.css
.selected{
  color: white;
  background-color: #3caf9d!important;
}

No arquivo drag-and-drop.component.html inserimos alguns comportamentos a div cdkDrag.

drag-and-drop.component.html
<div class="margin-custom">
  <div class="example-container" *ngFor="let zoologico of zoologicos; let i = index">
    <h2>{{'Lista de soltar ' + (i + 1)}}</h2>
    <div class="example-list" id="{{zoologico.nome}}"
         [cdkDropListConnectedTo]="nomesDropList"
         [cdkDropListData]="zoologico.animais"
         (cdkDropListDropped)="dropItems($event)" cdkDropList>

      <div class="example-box" *ngFor="let animal of zoologico.animais"
           [ngClass]="{'selected' : animal.selecionado}"
           (cdkDragStarted)="prepararArrasto(animal)"
           (click)="selecionarAnimal($event, animal)"
           cdkDrag>
        <span>{{animal.especie}}</span>
      </div>
    </div>
  </div>
</div>
  • ngClass: quando o atributo selecionado do item (animal) estiver true a classe selected será aplicada.

  • cdkDragStarted: assim que o componente for arrastado esse evento será disparado e como veremos mais à frente iremos prepará-lo para evitar um comportamento inadequado.

  • click: neste evento iremos verificar se o Ctrl está selecionado, caso esteja, mudaremos o valor do atributo selecionado do item (animal) que foi clicado.

Para controlar a lista dos itens a serem exibidos desta vez teremos um model para o Animal e outro para o Zoológico. Como já mencionado no artigo anterior é sempre importante representarmos nossos objetos por meio de classes.

animal.model.ts
export class AnimalModel implements DragDropAtributos {
  selecionado: boolean;

  constructor(
    public especie: string
  ) {
  }
}
zoologico.model.ts
export class ZoologicoModel {
  constructor(
    public nome: string,
    public animais: Array<AnimalModel>
  ) {
  }
}

Outra novidade é a interface drag-drop-atributos.ts para obrigarmos a implementação do atributo selecionado, como podemos ver no AnimalModel.

drag-drop-atributos.ts
export interface DragDropAtributos {
  selecionado: boolean;
}
export class DragAndDropComponent {

  /**
   * De preferência crie um serviço para buscar estes objetos a partir 
   * de um banco de dados 
   */
  zoologicos = [
    new ZoologicoModel('Zoo1',
      [
        new AnimalModel('Leão'),
        new AnimalModel('Macaco'),
        new AnimalModel('Girafa')
      ]
    ),
    new ZoologicoModel('Zoo2',
      [
        new AnimalModel('Tigre'),
        new AnimalModel('Elefante'),
        new AnimalModel('Zebra')
      ]
    )
  ];

  /**
   * Nome único para cada DropList, utilizamos o nome do zoológico
   */
  nomesDropList = [...this.zoologicos.map(zoo => zoo.nome)];

  /**
   * Ao clicar em um animal dispara este evento, se o control estiver 
   * pressionado então marca/desmarca o animal como selecionado
   * @param e
   * @param animal
   */
  selecionarAnimal(e: Event, animal: AnimalModel) {
    if ((<KeyboardEvent>e).ctrlKey) {
      animal.selecionado = !animal.selecionado;
    }
  }

  /**
   * Recebe o evento de cdkDropListDropped, captura a lista de animais do 
   * container atual e filtra pelos animais selecinados, caso tenham animais 
   * selecionados então os mesmos serão movidos
   * @param event
   */
  dropItems(event: CdkDragDrop<Array<AnimalModel>>) {
    let animais: Array<AnimalModel> = event.previousContainer.data;
    let animaisSelecionados = animais.filter(a => a.selecionado);

    if (animaisSelecionados.length > 1) {
      animaisSelecionados.forEach((animal, index) => {
        this.drop(event, animais.indexOf(animal), index);
      });
    } else {
      this.drop(event, -1, 0);
    }

    this.finalizarArrasto();
  }

  /**
   * Quando o indiceAnterior for maior que -1 então significa que uma lista de 
   * animais selecionados esta sendo movida, a variável agregarAoIndiceAtual 
   * é sempre o indice da lista de animais selecionados do item que esta sendo 
   * passado.
   * @param event do tipo cdkDropListDropped
   * @param indiceAnterior -1 como escape para comportamento default
   * @param agregarAoIndiceAtual 0 como escape para comportamento default
   */
  drop(event: CdkDragDrop<Array<AnimalModel>>, indiceAnterior: number, agregarAoIndiceAtual: number) {
    const previous = indiceAnterior > -1 ? indiceAnterior : event.previousIndex;
    const current = event.currentIndex + agregarAoIndiceAtual;

    if (event.previousContainer === event.container) {
      moveItemInArray(
        event.container.data,
        previous,
        current > previous ? current : event.currentIndex
      );
    } else {
      transferArrayItem(
        event.previousContainer.data,
        event.container.data,
        previous,
        current
      );
    }
  }

  /**
   * Adiciona como selecionado o animal que for arrastado, dessa forma 
   * garantimos que não aconteça o erro de selecionar um, arrastar outro 
   * e o selecionado ficar para traz
   * @param animal
   */
  prepararArrasto(animal: AnimalModel) {
    animal.selecionado = true;
  }

  /**
   * Altera para false o valor do atributo selecionado de todos os animais
   */
  finalizarArrasto() {
    this.zoologicos.forEach(zoo =>
      zoo.animais.forEach(animal => animal.selecionado = false)
    );
  }
}

Para entendermos melhor o que foi feito, iremos ver método a método.

  /**
   * Ao clicar em um animal dispara este evento, se o control estiver 
   * pressionado então marca/desmarca o animal como selecionado
   * @param e
   * @param animal
   */
  selecionarAnimal(e: Event, animal: AnimalModel) {
    if ((<KeyboardEvent>e).ctrlKey) {
      animal.selecionado = !animal.selecionado;
    }
  }

No arquivo drag-and-drop.component.html temos a uma div cdkDrag da qual implementamos atribuímos a função selecionarAnimal(e: Event, animal: AnimalModel) no seu evento de click. Se ao clicar no item (Animal) da lista o botão Ctrl estiver pressionado, então o atributo selecionado terá seu valor alterado.

  /**
   * Adiciona como selecionado o animal que for arrastado, 
   * dessa forma garantimos que não aconteça o erro de selecionar
   * um, arrastar outro e o selecionado ficar para traz
   * @param animal
   */
  prepararArrasto(animal: AnimalModel) {
    animal.selecionado = true;
  }

Existe um comportamento que não é o desejado, quando selecionamos um animal e em seguida arrastamos outro que não foi marcado como selecionado então somente esse último é arrastado para a nova lista, e não é isso que queremos não é mesmo?! Por esse motivo utilizamos o evento cdkDragStarted para selecionar também o animal que esta sendo arrastado, mesmo que ele já esteja como true no seu atributo selecionado.

 /**
   * Recebe o evento de cdkDropListDropped, captura a lista de animais 
   * do container atual e filtra pelos animais selecinados,
   * Caso tenham animais selecionados então os mesmos serão movidos
   * @param event
   */
  dropItems(event: CdkDragDrop<Array<AnimalModel>>) {
    let animais: Array<AnimalModel> = event.previousContainer.data;
    let animaisSelecionados = animais.filter(a => a.selecionado);

    if (animaisSelecionados.length > 1) {
      animaisSelecionados.forEach((animal, index) => {
        this.drop(event, animais.indexOf(animal), index);
      });
    } else {
      this.drop(event, -1, 0);
    }

    this.finalizarArrasto();
  }

Neste método buscamos uma lista dos animais que foram selecionados, se a lista for maior que 1 registro então aplicamos o método drop item a item da lista, passando sempre o índice atual e o número que deve ser agregado ao índice para que assim os animais sejam transferidos para a outra lista na sequência correta.

 /**
   * Quando o indiceAnterior for maior que -1 então significa que uma lista 
   * de animais selecionados esta sendo movida, a variável agregarAoIndiceAtual 
   * é sempre o indice da lista de animais selecionados do item que esta sendo 
   * passado.
   * @param event do tipo cdkDropListDropped
   * @param indiceAnterior -1 como escape para comportamento default
   * @param valorAgregacaoIndiceAtual 0 como escape para comportamento default
   */
  drop(event: CdkDragDrop<Array<AnimalModel>>, indiceAnterior: number, valorAgregacaoIndiceAtual: number) {
    const previous = indiceAnterior > -1 ? indiceAnterior : event.previousIndex;
    const current = event.currentIndex + valorAgregacaoIndiceAtual;

    if (event.previousContainer === event.container) {
      moveItemInArray(
        event.container.data,
        previous,
        current > previous ? current : event.currentIndex
      );
    } else {
      transferArrayItem(
        event.previousContainer.data,
        event.container.data,
        previous,
        current
      );
    }
  } 

Aqui é simples também -1 para o indiceAnterior e 0 para o valorAgregacaoIndiceAtual são valores de escape, assim a função de moveItemInArray não irá se perder ao arrastar um único item e ira utilizar o event.currentIndex seguindo o comportamento padrão deste método do qual podemos encontrar em inúmeros exemplos na internet.

  /**
   * Altera para false o valor do atributo selecionado de todos os animais
   */
  finalizarArrasto() {
    this.zoologicos.forEach(zoo =>
      zoo.animais.forEach(animal => animal.selecionado = false)
    );
  }

Para finalizar não queremos que os animais fiquem selecionados após termos arrastados eles para outro lista ou até mesmo na mesma lista, por esse motivo vamos passar false para o atributo selecionado de todos eles.

Percebeu que essa seleção só está funcionando com o Ctrl? Não foi por acaso. Agora que você já sabe como funciona, que tal você mesmo implementar a opção de selecionar com Shift? Tenha em mente, que com o Shift você seleciona um range de animais, ou seja, se selecionarmos o animal na posição 1 e na posição 3 então a posição 2 deve ser marcada também como selecionado, fazendo isso todo resto já irá funcionar da forma que está implementada.

Boa sorte e bons estudos.

Last updated