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.
Devemos clonar o projeto de exemplo mat-drag-and-drop-example, só que dessa vez vamos utilizar o branch multi-select-drag-drop. Temos que ficar atentos a esse detalhe!
Leiam os comentários, todos eles sem exceção para a compreensão completa acerca deste artigo.
Chega de enrolação, queremos aprender logo!
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.
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.
exportclassDragAndDropComponent {/** * De preferência crie um serviço para buscar estes objetos a partir * de um banco de dados */ zoologicos = [newZoologicoModel('Zoo1', [newAnimalModel('Leão'),newAnimalModel('Macaco'),newAnimalModel('Girafa') ] ),newZoologicoModel('Zoo2', [newAnimalModel('Tigre'),newAnimalModel('Elefante'),newAnimalModel('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) {constprevious= indiceAnterior >-1? indiceAnterior :event.previousIndex;constcurrent=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) {constprevious= indiceAnterior >-1? indiceAnterior :event.previousIndex;constcurrent=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.
E finalmente iremos falar sobre o arquivo drag-and-drop.component.ts