Documentação/Guias
Listas que compartilham itens
Listas de um mesmo grupo movem, copiam e trocam itens — a coluna inteira recebe drops, pull e put decidem o que entra e o que sai, paletas copiam, e listas controladas aplicam cada drop a uma store com histórico de desfazer.
Esta página foi traduzida por IA e pode conter imprecisões. Ler o original em inglês
Carregando a demo…
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
const columns = reactive([
{
id: 'backlog',
title: 'Backlog',
stories: [
{ id: 's1', title: 'Saved searches', points: 5 },
{ id: 's2', title: 'Crash on empty board', points: 2, bug: true },
{ id: 's3', title: 'CSV export', points: 3 },
{ id: 's4', title: 'Dark mode email', points: 2 },
],
},
{
id: 'sprint',
title: 'Sprint 14',
stories: [{ id: 's5', title: 'Onboarding tour', points: 8 }],
},
]);
const wholeColumn = ref(true);
</script>
<template>
<DnDProvider>
<div class="planning">
<label class="switch">
<input v-model="wholeColumn" type="checkbox" />
<code>dropzone</code>: the whole column takes drops
</label>
<div class="columns">
<Column v-for="column in columns" :key="column.id" :column="column" :whole-column="wholeColumn" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.planning {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.switch {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
cursor: pointer;
}
.switch input {
accent-color: #42b883;
}
.switch code {
font: 12px/1 ui-monospace, Menlo, Consolas, monospace;
color: #42b883;
}
.columns {
display: flex;
justify-content: center;
gap: 14px;
width: 100%;
}
</style>Um grupo
Listas com o mesmo nome de group trocam itens. Por padrão, cada lista é um grupo só seu.
<script setup lang="ts">
import { toRef, useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import StoryCard from './StoryCard.vue';
type Story = { id: string; title: string; points: number };
const props = defineProps<{ column: { id: string; title: string; stories: Story[] } }>();
const list = useTemplateRef<HTMLElement>('list');
const { items, isOver } = makeSortableList(list, {
id: props.column.id, // names the list in drop events
items: toRef(props.column, 'stories'), // a writable ref into the reactive column
group: 'planning', // same name: stories move between these lists
});
</script>
<template>
<section :class="{ over: isOver }">
<h3>{{ column.title }}</h3>
<ul ref="list">
<StoryCard v-for="story in items" :key="story.id" :story="story" />
</ul>
</section>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import Column from './Column.vue';
const columns = reactive([
{ id: 'backlog', title: 'Backlog', stories: [{ id: 's1', title: 'Saved searches', points: 5 }] },
{ id: 'sprint', title: 'Sprint 14', stories: [] },
]);
</script>
<template>
<Column v-for="column in columns" :key="column.id" :column="column" />
</template>
- Aqui cada coluna é um componente, como as colunas costumam ser. Um componente também pode criar várias listas, um Todo e um Done fixos lado a lado: cada cartão entra na lista cujo elemento o contém.
toRef(props.column, 'stories')é gravável porque o pai passa um objeto reativo. O drop grava os dois arrays, o que a história deixou e o em que ela entrou, e os pontos das duas colunas acompanham.idaparece nos eventos de drop comofrom.listeto.list, e é a chave dee.result, os novos arrays de cada lista que o drop altera.idegroupsão lidos uma vez só.- As keys precisam ser únicas no grupo inteiro.
O onDrop de uma lista roda para os drops nela. Um movimento de backlog para sprint
chama o onDrop da lista sprint, e o e.result dele contém os novos arrays das duas listas.
A coluna inteira recebe drops: dropzone
Uma lista curta em uma coluna alta deixa espaço vazio abaixo do último card. Com dropzone,
outro elemento recebe os drops pela lista: aqui, a coluna inteira. Uma história solta abaixo da
última pousa no fim. As histórias continuam no <ul>; só a área que recebe drops cresce. A demo
contorna essa área enquanto você arrasta: desligue-a, e só a própria lista recebe a história.
const zone = useTemplateRef<HTMLElement>('zone');
const { items } = makeSortableList(list, {
items: toRef(props.column, 'stories'),
group: 'planning',
dropzone: zone, // or a getter: () => (wholeColumn.value ? zone.value : undefined)
});
dropzone, uma lista vazia ainda precisa de um tamanho para receber drops: dê a ela um
min-height.O que entra e o que sai: pull e put
group também aceita um objeto. pull diz o que pode sair de uma lista, put o que pode entrar:
Carregando a demo…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type SortablePutContext } from '@vue-dnd-kit/core';
import RuleList from './RuleList.vue';
// Four lists of one group, `files`. What each gives and takes is its `group.pull` and `group.put`.
const shortlist = reactive({
id: 'shortlist',
title: 'Shortlist',
code: 'put: ({ keys }) => shortlist.files.length + keys.length <= 3',
refusal: 'full',
group: { name: 'files', put: ({ keys }: SortablePutContext) => shortlist.files.length + keys.length <= 3 },
files: [
{ id: 'pitch', name: 'pitch.pdf', kind: 'doc' as const },
{ id: 'mock', name: 'mockup.png', kind: 'image' as const },
],
});
const lists = reactive([
{
id: 'inbox',
title: 'Inbox',
code: '{ pull: true, put: false }',
refusal: 'put: false',
group: { name: 'files', pull: true, put: false },
files: [
{ id: 'logo', name: 'logo.svg', kind: 'image' as const },
{ id: 'brief', name: 'brief.pdf', kind: 'doc' as const },
{ id: 'hero', name: 'hero.png', kind: 'image' as const },
],
},
{
id: 'templates',
title: 'Templates',
code: "{ pull: 'clone', put: false }, sort: false",
refusal: 'put: false',
group: { name: 'files', pull: 'clone' as const, put: false },
sort: false,
files: [
{ id: 'invoice', name: 'invoice.doc', kind: 'doc' as const },
{ id: 'banner', name: 'banner.png', kind: 'image' as const },
],
},
{
id: 'images',
title: 'Images',
code: "put: 'image'",
refusal: 'images only',
group: { name: 'files', put: 'image' },
files: [],
},
shortlist,
]);
</script>
<template>
<DnDProvider>
<div class="rules">
<RuleList v-for="list in lists" :key="list.id" :list="list" />
</div>
</DnDProvider>
</template>
<style scoped>
.rules {
display: grid;
grid-template-columns: repeat(2, minmax(0, 260px));
gap: 12px;
justify-content: center;
}
@media (max-width: 480px) {
.rules {
grid-template-columns: minmax(0, 1fr);
}
}
</style>| Opção | Valores |
|---|---|
pull | true (padrão): os itens podem sair. false: não podem. 'clone': saem cópias, os itens ficam. |
put | true (padrão): os itens podem entrar. false: nada entra. Um tipo ou tipos ('image', ['image', 'video']): entram os itens desses tipos. Uma função: você decide a cada arraste. |
// Inbox: gives, takes nothing.
group: { name: 'files', pull: true, put: false }
// Templates: gives copies, takes nothing, keeps its order.
group: { name: 'files', pull: 'clone', put: false }, sort: false
// Images: takes images only.
group: { name: 'files', put: 'image' }
Os tipos são as opções type dos itens arrastados (makeSortableItem(el, { item, type }));
itens sem tipo contam como o nome do grupo. Com tipos no put, cada item arrastado precisa ter
um deles (com acceptMode: 'some', entram os que têm).
Uma função put recebe { keys, items, types, from, to }: as keys e os itens arrastados, os
tipos deles, a lista de onde vêm (null para draggables simples) e esta lista. Movimentos
dentro de uma lista são sempre permitidos: a Inbox ainda ordena os próprios arquivos.
Sobre uma lista que recusa o arraste, a posição volta para onde o arraste começou, então as
listas mostram o que um drop faria: nada. Um drop ali é cancelado e o item volta ao lugar. O
isAccepting da lista é false durante um arraste assim, e o isOver continua true sobre
ela: é assim que cada lista da demo sabe o que dizer.
A posição também volta para casa sobre um alvo de drop próprio fora das listas, como uma lixeira:
o drop ali é do alvo, e as listas mantêm o item, a não ser que o onDrop dele o retire
(event.source.remove()). Alvos dentro das listas (um drop dentro de um item) e em volta delas
(um painel que contém as listas) deixam a posição onde está.
const { items, isOver, isAccepting } = makeSortableList(el, { … });
const { active } = useDnD();
const verdict = computed(() => (active.value ? (isAccepting.value ? 'takes' : 'refuses') : null));
Limites
Uma lista que comporta no máximo três arquivos, a Shortlist:
const shortlist = reactive({
files: [/* … */],
group: { name: 'files', put: ({ keys }: SortablePutContext) => shortlist.files.length + keys.length <= 3 },
});
Ela lê os próprios itens quando um arraste pergunta. Encha-a, e ela recusa o próximo arquivo, com o motivo no cabeçalho.
Quando vários itens selecionados de tipos diferentes são arrastados, acceptMode diz se
'all' (padrão) ou 'some' deles precisam passar no put. Com 'some', os que passam pousam e
os outros voltam ao lugar. Veja multi-drag.
Um grupo por lista
Uma lista pertence a um único grupo: group recebe um nome só, nunca vários. As listas de um
grupo compartilham cada arraste (seus rascunhos, o que está sendo arrastado, quais delas o
aceitam), então os itens nunca passam de um grupo para outro. Quando os itens devem ir só em
alguns sentidos, coloque as listas em um grupo e deixe o put decidir pelo from, a lista de
onde o arraste vem:
// Review takes tasks from both teams. A team takes tasks back from Review,
// never from the other team.
const fromReview = ({ from }: SortablePutContext) => from === 'review';
makeSortableList(review, { id: 'review', items: reviewTasks, group: 'tasks' });
makeSortableList(frontend, { id: 'frontend', items: frontendTasks, group: { name: 'tasks', put: fromReview } });
makeSortableList(backend, { id: 'backend', items: backendTasks, group: { name: 'tasks', put: fromReview } });
put: ['a', 'b'] nomeia os grupos dos quais uma lista recebe itens.
Aqui, um array no put lista os tipos dos itens que uma lista aceita. Para receber itens
de outras listas, coloque-as em um grupo e decida pelo from, como acima.Copiar: paletas e a tecla Alt
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Palette from './Palette.vue';
import Sheet from './Sheet.vue';
</script>
<template>
<!-- The palette and the page are lists of one group: copies go from one to the other. -->
<DnDProvider>
<div class="builder">
<Palette />
<Sheet />
</div>
</DnDProvider>
</template>
<style scoped>
.builder {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 24px;
}
</style>makeSortableList(list, {
items: templates,
group: { name: 'blocks', pull: 'clone', put: false }, // copies go out, nothing comes in
sort: false, // its own order stays
clone: (block) => ({ ...block, id: crypto.randomUUID() }),
});
makeSortableList(list, {
items: blocks,
group: 'blocks',
clone: (block) => ({ ...block, id: crypto.randomUUID() }), // for Alt-drags
dropOutside: 'remove', // drag off the Page panel to delete
});
pull: 'clone'copia todo arraste que sai da lista. O original fica, e a cópia viaja e pousa.- Segurar Alt durante o arraste também copia, de qualquer lista com uma função
clone: é ela que faz a cópia. Mude a tecla com ocopyModifierdo provider, ou desligue comnull. - De qualquer forma, a lista de onde o item vem precisa de
clone. Ele precisa retornar a cópia com uma key nova. Semclone, nada é copiado. - O evento de drop tem
mode: 'copy', ee.itemssão as cópias. Um componente de overlay também recebemode, para mostrar um "+" ao copiar (veja o overlay).
Trocar de lugar entre listas
Duas listas de um mesmo grupo que têm behavior: 'swap' trocam itens entre si: o item arrastado
ocupa o lugar daquele sob o ponteiro, e este vai para a lista do item arrastado. Ele precisa ter
permissão para sair da sua lista (pull: true) e para entrar na outra (put). O rodízio de
plantão em listas ordenáveis troca itens
entre as suas duas colunas.
Draggables simples
Uma lista recebe itens arrastados com makeDraggable (um chip, um modelo, um arquivo de uma
árvore) quando tem createItem. group.put também decide sobre eles, com from: null. Veja
draggables simples em uma lista.
Soltar fora
O que acontece quando um arraste termina fora de qualquer alvo é definido pelo dropOutside da
lista:
dropOutside | Os itens |
|---|---|
'placeholder' (padrão) | Pousam onde a posição foi mostrada pela última vez. |
'cancel' | Voltam ao lugar. O padrão com placeholder: 'indicator'. |
'remove' | Saem da lista: arraste para fora para excluir. A folha da demo da paleta faz isso. |
'remove' consulta primeiro o onBeforeDrop da lista. Depois, o onDrop da lista roda com
to.index === -1, e e.result contém a lista sem os itens. Uma lixeira que recebe drops funciona
do mesmo jeito: veja lixeiras.
Listas controladas
As listas podem ler uma store que não é delas: Pinia, estado do servidor, dados imutáveis. Passe
items como getter (ou um computed, ou um ref somente leitura): a biblioteca nunca grava nele.
Chame e.preventDefault() no onDrop e aplique e.result você mesmo:
Carregando a demo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Table from './Table.vue';
import { createSeating } from './store';
const seating = createSeating();
const { state, undo } = seating;
</script>
<template>
<DnDProvider>
<div class="seating">
<div class="tables">
<Table id="a" title="Table A" :seating="seating" />
<Table id="b" title="Table B" :seating="seating" />
</div>
<section class="history">
<header>
<h4>History</h4>
<button type="button" :disabled="!state.history.length" @click="undo">Undo</button>
</header>
<ol>
<li v-for="(entry, index) in state.history" :key="state.history.length - index">{{ entry.label }}</li>
<li v-if="!state.history.length" class="none">Move a guest</li>
</ol>
</section>
</div>
</DnDProvider>
</template>
<style scoped>
.seating {
display: flex;
flex-direction: column;
gap: 12px;
width: 460px;
max-width: 100%;
margin: 0 auto;
}
.tables {
display: flex;
gap: 12px;
}
.history {
padding: 10px 12px;
border-radius: 12px;
background: color-mix(in srgb, currentColor 5%, transparent);
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
h4 {
margin: 0;
font: 600 11px/1 ui-monospace, Menlo, Consolas, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
button {
padding: 5px 12px;
border: 0;
border-radius: 7px;
background: #42b883;
color: #fff;
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
button:disabled {
background: color-mix(in srgb, currentColor 12%, transparent);
color: inherit;
opacity: 0.5;
cursor: default;
}
ol {
display: flex;
flex-direction: column;
gap: 4px;
height: 66px;
margin: 8px 0 0;
padding: 0;
overflow-y: auto;
list-style: none;
font: 12px/1.4 ui-monospace, Menlo, Consolas, monospace;
}
li:first-child {
color: #42b883;
}
li.none {
color: inherit;
opacity: 0.5;
}
</style>const { items } = makeSortableList(list, {
id: props.id,
items: () => props.seating.state.tables[props.id]!, // read-only
group: 'seating',
onDrop: (e) => {
e.preventDefault(); // no write
props.seating.apply(e.result, `${e.items[0]?.name} → ${props.title}`);
},
});
const apply = (result: ReadonlyMap<Key, Guest[]>, label: string) => {
const before = [...result.keys()].map((id): [Key, Guest[]] => [id, state.tables[id]!]);
for (const [id, guests] of result) state.tables[id] = guests;
state.history.unshift({ label, before });
};
const undo = () => {
const last = state.history.shift();
for (const [id, guests] of last?.before ?? []) state.tables[id] = guests;
};
e.resultassocia cada lista que o drop altera ao seu novo array: as duas mesas depois de um movimento, uma depois de uma reordenação. São arrays novos, então a store pode guardar os antigos para desfazer.- O rascunho segue a store: aplique
resultnoonDrop(ou antes de umonBeforeDropassíncrono resolver), e o overlay pousa na nova ordem. preventDefault()pula a gravação em todas as listas deresult. Sem aplicar em lugar nenhum, as listas ficam como estavam e os itens voltam ao lugar.- Os builds de desenvolvimento avisam quando o drop de uma lista somente leitura fica a cargo da biblioteca: nada mudaria.
Para salvar em um servidor antes de o drop pousar, ou para perguntar ao usuário primeiro, veja decidindo um drop. Todas as opções de lista estão na API do makeSortableList.