Documentação/Guias
Multi-drag e seleção
Selecione vários itens com cliques ou um retângulo de seleção e arraste-os juntos — como um bloco, deixando vãos, ou empilhados no overlay —, aceite só alguns deles no drop e mostre-os em um overlay seu.
Esta página foi traduzida por IA e pode conter imprecisões. Ler o original em inglês
Selecione alguns arquivos, depois arraste um deles: os outros vêm junto. Troque multiple para
ver o que eles fazem no caminho, e o layout para ver o overlay de uma grade.
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Files from './Files.vue';
</script>
<template>
<DnDProvider>
<Files />
</DnDProvider>
</template>A seleção
Cada provider guarda uma seleção: um conjunto reativo de ids. Arrastar um item selecionado arrasta todos os itens selecionados, o pego primeiro. Arrastar um item que não está selecionado arrasta só esse item.
A biblioteca não decide como as pessoas selecionam: as regras de clique são suas. Ela também nunca limpa a seleção por conta própria, e a seleção sobrevive a desmontagens, como as das linhas que uma lista virtual recicla.
Cliques: Ctrl/Cmd e Shift
Todo item ordenável e todo draggable retorna selected, um ref gravável ligado à seleção.
Ligue-o a uma classe, a um checkbox (v-model="selected"), ou defina-o em um handler de clique:
<script setup lang="ts">
const { selected } = makeSortableItem(el, { item: () => props.file });
</script>
<template>
<li ref="el" class="file" :class="{ selected }" @click="emit('pick', $event)">…</li>
</template>
A lista da demo define as regras: um clique seleciona um arquivo, Ctrl/Cmd-clique adiciona ou remove um, Shift-clique seleciona o trecho a partir do último arquivo clicado:
const { selection } = useDnD();
let anchor: string | null = null;
const pick = (id: string, event: MouseEvent) => {
const ids = items.value.map((file) => file.id);
if (event.shiftKey && anchor) {
const [a, b] = [ids.indexOf(anchor), ids.indexOf(id)];
selection.set(ids.slice(Math.min(a, b), Math.max(a, b) + 1));
return;
}
if (event.ctrlKey || event.metaKey) selection.toggle(id);
else selection.set([id]);
anchor = id;
};
O clique que termina um arraste não chega ao @click, então um drop nunca muda a seleção por
acidente. Use @click, não pointerup, para as ações dos itens.
De qualquer lugar: useDnD().selection
import { useDnD, useDnDMonitor } from '@vue-dnd-kit/core';
const { selection } = useDnD();
selection.has('a'); // is it selected?
selection.add('a', 'b'); // select
selection.delete('a'); // deselect
selection.toggle('a'); // flip it, or force it: toggle('a', true)
selection.set(['a', 'c']); // exactly these
selection.clear();
selection.ids; // a reactive ReadonlySet<Key>
// Clear it after every drop, if your UI wants that:
useDnDMonitor({ onDragEnd: () => selection.clear() });
A seleção é indexada por id, então pode conter itens de várias listas, e também draggables simples: um arraste leva todos os itens selecionados sob o provider.
Seleção por retângulo
makeSelectionArea transforma um elemento em uma área onde pressionar o espaço vazio desenha um
retângulo de seleção, e os itens que ele cruza são selecionados. Nesta área de trabalho, desenhe
um retângulo em volta de alguns ícones, depois arraste um deles:
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Desktop from './Desktop.vue';
</script>
<template>
<!-- Every selected icon gets a clone at pickup: the overlay shows them all. -->
<DnDProvider overlay-previews="all">
<Desktop />
</DnDProvider>
</template><script setup lang="ts">
const desktop = useTemplateRef<HTMLElement>('desktop');
const { isSelecting, style } = makeSelectionArea(desktop);
</script>
<template>
<!-- The area needs a position: the band is placed inside it. -->
<div ref="desktop" class="desktop">
<DesktopIcon v-for="icon in icons" :key="icon.id" :icon="icon" />
<div v-if="isSelecting" class="band" :style="style" />
</div>
</template>
<style scoped>
.desktop { position: relative; height: 236px; }
.band { border: 1px solid #42b883; background: color-mix(in srgb, #42b883 15%, transparent); }
</style>
- Você renderiza o retângulo:
styleo posiciona, relativo ao conteúdo da área. - A área precisa de uma posição não estática (
relativeserve), senão o retângulo vai parar em outro lugar. - Um retângulo só começa a partir da própria área, então faça a área grande: o painel inteiro, com espaço em volta dos itens, não um wrapper justo.
- Pressionar um item o arrasta. Botões, campos e a barra de rolagem da área continuam recebendo as suas pressões.
- O retângulo fica ancorado ao conteúdo enquanto a área ou a página rola. Escape o cancela e restaura a seleção.
useDnD().statusé'selecting'enquanto um retângulo é desenhado: a linha embaixo da área de trabalho lê isso.
| Opção | O que faz |
|---|---|
strategy | 'toggle' (padrão): os itens que o retângulo cruza invertem o estado. 'select': só os itens dentro do retângulo ficam selecionados, então um clique no espaço vazio desmarca os itens da área. |
modifier | Teclas que precisam estar seguradas para desenhar: { keys: ['Shift'], method: 'every' } (ou 'some'). |
accept | Quais draggables o retângulo seleciona, uma regra de aceitação comparada com o type deles. |
disabled | Nenhum retângulo; ligar isso cancela um em andamento. |
onSelected(ids) | Chamado quando um retângulo termina, com a seleção. |
strategy, modifier e disabled aceitam um valor, um ref ou um getter, lido quando uma pressão
inicia um retângulo. Com áreas dentro de áreas, a mais interna recebe a pressão. A página da
API do makeSelectionArea as alterna ao vivo.
Movê-los como estão dispostos
Os ícones são draggables simples (makeDraggable), cada um com o seu ícone como data. O overlay
deles é um componente da própria demo: ele dispõe um DragPreview de cada ícone arrastado como os
ícones estão na área de trabalho, em volta do pego. Os ícones esperam no lugar, esmaecidos por
isDragging, que continua true até as cópias deles pousarem.
<script setup lang="ts">
const props = defineProps<{ items: DragItem[] }>();
const placed = computed(() => {
const grabbed = props.items[0]!.data as Icon;
return props.items.map(({ id, data }) => {
const icon = data as Icon;
return { id, left: `${icon.x - grabbed.x}px`, top: `${icon.y - grabbed.y}px` };
});
});
</script>
<template>
<div class="formation">
<DragPreview v-for="{ id, left, top } in placed" :id="id" :key="id" class="copy" :style="{ left, top }" />
</div>
</template>
const { isDragging, selected } = makeDraggable(el, { id, data: () => props.icon, overlay: Formation });
A área de trabalho recebe o drop. O position dele é onde o ícone pego foi solto, nas coordenadas
da área de trabalho: o ícone vai para lá, e os outros se movem a mesma distância. Cada cópia então
pousa no seu próprio ícone, no novo lugar. Soltas fora da área de trabalho, as cópias voltam para
os ícones.
makeDroppable(desktop, {
id: 'desktop',
onDrop: ({ active, position }) => {
const grabbed = icons.value.find((icon) => icon.id === active.id)!;
const shift = { x: position!.x - grabbed.x, y: position!.y - grabbed.y };
for (const icon of icons.value) {
if (!active.ids.includes(icon.id)) continue;
icon.x += shift.x; // the demo also keeps each icon on the desktop
icon.y += shift.y;
}
},
});
Por padrão, um clone é feito no início do arraste para no máximo 5 itens: o
overlay-previews="all" do provider faz um para cada ícone selecionado.
Para dentro de uma pasta
Nesta janela, o retângulo seleciona arquivos, e as pastas os recebem. O overlay é o padrão: uma pilha dos arquivos escolhidos. Soltos em uma pasta, eles entram um depois do outro, o pego por último, e a pasta os engole. Se o arraste for cancelado, cada um volta voando para o seu arquivo. Clique em uma pasta para abri-la, e arraste arquivos de volta para Home no caminho.
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Finder from './Finder.vue';
</script>
<template>
<!-- Every picked file gets a clone at pickup: they all go into the folder. -->
<DnDProvider overlay-previews="all">
<Finder />
</DnDProvider>
</template><!-- Every picked file gets a clone at pickup: they all go in. -->
<DnDProvider overlay-previews="all">
<Finder />
</DnDProvider>
makeDroppable(el, {
id: () => props.folder.id,
accept: 'file',
onDrop: (event) => emit('move', event.active.ids),
});
// The files are in: the folder gulps.
useDnDMonitor({
onDragSettle: (event) => {
if ('dropped' in event && event.over?.id === props.folder.id) gulps.value++;
},
});
Só isso: os arquivos saem da janela com o drop, então não têm onde pousar a não ser na pasta, e a
animação de drop padrão leva um multi-drag para dentro de uma zona um item depois do outro.
overlayPreviews diz quantos itens ganham um clone no início do arraste (5 por padrão). Todo
clone voa: para dentro da zona, para a sua posição ou de volta para o seu item.
A pasta é um botão: um retângulo nunca começa em botões e campos, então um clique a abre.
Como os outros itens se movem: multiple
A lista onde um multi-drag começa decide o que os outros itens selecionados fazem enquanto ele acontece. Troque isso na primeira demo desta página:
makeSortableList(listRef, { items: files, multiple: 'gaps' });
multiple | Durante o arraste | O overlay |
|---|---|---|
'group' (padrão) | Os outros itens deixam os seus lugares e viajam com o pego como um único bloco de posições, então a lista já mostra o resultado do drop. | Uma coluna dos itens arrastados ao longo da lista (até overlayPreviews). |
'gaps' | Os outros ficam onde estão, marcados com data-dnd-dragging, até o drop. | A mesma coluna. |
'stack' | Os outros voam para dentro do overlay. | O item pego com uma contagem. |
No drop, os itens pousam juntos no índice de destino, na ordem que tinham. Em 'group' e
'gaps', cada item do overlay voa para a sua própria posição. A opção é lida quando um arraste
começa, então um ref ou um getter a troca.
Sobre um alvo de drop que não é uma lista, como uma lixeira ou uma playlist, ou sobre uma lista
que recusa o arraste, um bloco 'group' se desfaz: cada item volta a aparecer no seu próprio
lugar, como um drop ali o deixa, e o bloco se forma de novo quando o ponteiro passa sobre uma lista.
Estilize os itens deixados para trás em 'gaps', esmaecidos por padrão:
.file[data-dnd-dragging] {
opacity: 0.35;
}
O overlay padrão
Em 'group' e 'gaps', o overlay é uma coluna dos itens arrastados como eles vão pousar: o pego
sob o ponteiro, os outros antes e depois dele na ordem do drop (uma fileira em listas
horizontais). Ele mostra tantos quanto o overlayPreviews do provider (5 por padrão, 'all'
para todos os renderizados). Os que passam disso são contados onde estão: +N no começo da
coluna para os itens que pousam antes dos mostrados, +N no fim para os que vêm depois.
Em grades, uma coluna de células inteiras cobriria a página, então o overlay é uma pilha: até três itens em leque atrás do pego, com a contagem total por cima. Mude a demo para Grid para ver.
Draggables simples (makeDraggable) também formam uma pilha, como fazem os arquivos acima. Cada
clone do overlay pousa por conta própria: na sua posição depois de um drop em uma lista, no seu
item onde quer que o drop o tenha deixado (de volta no lugar depois de um cancelamento), e para
dentro da zona em que um drop entra quando os itens saem da página, um depois do outro, a caixa
por último.
Aceitar só alguns
O onBeforeDrop de uma lista pode aceitar parte de um multi-drag: retorne as keys que ela aceita,
e os outros voltam para onde vieram. Ele pode ser assíncrono e perguntar: o overlay espera onde
foi solto até a resposta chegar. Aqui, Scheduled quer posts com capa. Solto com um que não tem
capa, ele pergunta se deve adicionar as capas que faltam e aceitar todos, aceitar só os posts
prontos, ou nenhum:
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Blog from './Blog.vue';
</script>
<template>
<DnDProvider>
<Blog />
</DnDProvider>
</template>const { items } = makeSortableList(list, {
id: props.id,
items: posts,
group: 'blog',
onBeforeDrop: async (event) => {
const missing = event.items.filter((post) => post.cover === null);
if (!props.ask || event.sameList || !missing.length) return true;
const choice = await props.ask(missing, event.items.length - missing.length); // the question under the lists
if (choice === 'cover') missing.forEach((post) => (post.cover = pickCover()));
if (choice === 'ready') return event.keys.filter((_, index) => event.items[index]!.cover !== null);
return choice === 'cover'; // all of them, or none: they fly back
},
});
event.keyssão as keys arrastadas, eevent.items[i]é o item deevent.keys[i].- Depois da resposta, o
onDroprecebeacceptederejected, eevent.resultcontém as listas como o drop parcial as deixa. - Enquanto o drop espera, as listas continuam mostrando cada post arrastado onde ele pousaria: a
resposta decide. Para uma regra que não precisa de pergunta,
group.putdecide enquanto o arraste está sobre a lista (veja tipos misturados abaixo). Mais em decidindo um drop.
Um limite
Neste quadro, In progress e Review têm um limite de cards. Uma coluna cheia não aceita nada: o
group.put dela recusa, e a pilha fica vermelha sobre ela. Uma coluna com espaço para alguns dos
cards escolhidos aceita quantos couberem, e os outros voltam ao lugar. O overlay da coluna avisa
isso antes do drop.
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Board from './Board.vue';
</script>
<template>
<DnDProvider>
<Board />
</DnDProvider>
</template>const room = (keys: Key[]) => limit - column.cards.filter((card) => !keys.includes(card.id)).length;
makeSortableList(list, {
items: cards,
group: { name: 'board', put: ({ keys, from, to }) => from === to || room(keys) > 0 },
multiple: 'stack',
overlay: CardPile, // red where `accepted` is false
overOverlay: fit,
onBeforeDrop: (event) => {
if (event.sameList) return true;
const free = room(event.keys);
return free >= event.keys.length || event.keys.slice(0, free);
},
});
fit é uma função, que o Vue trata como um componente funcional. Ela passa ao FitBadge o que
só a coluna sabe, o espaço que lhe resta, e repassa a pilha como slot do badge.
const fit: FunctionalComponent<OverlayProps> = (overlay, { slots }) =>
h(FitBadge, { count: overlay.items.length, room: room(overlay.items.map((item) => item.id)), limit }, slots);
Tipos misturados
Uma lista também pode recusar alguns itens pelo tipo, antes do drop. Com acceptMode: 'some',
group.put é consultado para cada item separadamente: a lista aceita o arraste quando pelo menos
um item se encaixa, e o resto volta ao lugar no drop. Com 'all' (o padrão), todo item precisa
se encaixar.
makeSortableList(gallery, {
items: photos,
group: { name: 'files', put: ({ types }) => types.includes('image') },
acceptMode: 'some',
});
Alvos de drop simples também aceitam acceptMode. Veja
listas que compartilham itens e
arrastar e soltar.
Um overlay personalizado para vários itens
Componentes de overlay e o slot #overlay do provider recebem items: todos os itens
arrastados, o pego primeiro. Mostre um resumo em vez dos próprios itens: aqui, uma contagem, o
primeiro assunto e o que o drop vai fazer.
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Mailbox from './Mailbox.vue';
</script>
<template>
<DnDProvider>
<Mailbox />
</DnDProvider>
</template><script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null }>();
const first = computed(() => props.items[0]?.item as Mail);
const count = computed(() => props.items.length);
// What the drop does, from the `data` of the zone under it.
const zone = computed(() => props.over?.data as { verb: string; danger?: boolean } | undefined);
const action = computed(() => {
const noun = count.value === 1 ? 'conversation' : 'conversations';
return `${zone.value?.verb ?? 'Move'} ${count.value} ${noun}`;
});
</script>
<template>
<!-- The box keeps the row's size; the summary hangs off its middle, by the pointer. -->
<div class="box">
<div class="summary" :class="{ stack: count > 1, danger: zone?.danger }">
<span class="count">{{ count }}</span>
<strong>{{ action }}</strong>
<small>{{ first.subject }}{{ count > 1 ? ` and ${count - 1} more` : '' }}</small>
</div>
</div>
</template>
<style scoped>
.box { position: relative; width: 100%; height: 100%; }
.summary { position: absolute; top: calc(50% + 10px); left: calc(50% + 10px); width: 230px; }
</style>
// The box is centered on the pointer, so the summary hangs off the pointer and leaves
// the zone under it in sight.
const { items } = makeSortableList(list, { items: mails, overlay: MailOverlay, modifiers: [snapCenterToCursor] });
// A zone takes the mails out of the inbox; its `data` tells the overlay what it does.
makeDroppable(archive, {
id: 'Archive',
data: { verb: 'Archive' },
onDrop: (event) => {
event.source?.remove();
selection.clear();
},
});
Para dispor os itens reais, renderize os clones deles com DragPreview, como faz a
área de trabalho acima; o
guia do overlay abre uma pilha de
livros em leque. Os clones são feitos ao pegar, do item pego e dos outros ao lado dele na ordem
do drop, tantos quanto o overlayPreviews; para qualquer outro item, DragPreview renderiza o
seu slot padrão. Cada prévia pousa no seu próprio item no drop.
Árvores
Em uma árvore, os nós selecionados se movem como um bloco, cada um com a sua subárvore, na ordem do documento. Um nó selecionado dentro de outro selecionado vai junto na subárvore do pai. Veja árvores.