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.
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.
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.
E finalmente iremos falar sobre o arquivo drag-and-drop.component.ts