Documentação/Guias
Decidindo um drop
Aceite, recuse ou adie um drop — regras durante o arraste ou ao soltar, parte de um multi-drag, um diálogo, um servidor que precisa concordar antes, monitores que vetam e drops fora de qualquer alvo.
Esta página foi traduzida por IA e pode conter imprecisões. Ler o original em inglês
Arraste um card para Done. O drop espera a sua resposta: mover o card, continuar arrastando para outro lugar, ou cancelar.
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>onBeforeDrop
makeSortableList(listRef, {
items: tasks,
onBeforeDrop: (e) => (e.items.some((task) => task.locked) ? 'reject' : 'accept'),
});
Listas, árvores, alvos de drop (makeDroppable) e monitores (useDnDMonitor) aceitam um
onBeforeDrop. Ele roda quando o arraste é solto, antes de qualquer coisa mudar, e a resposta
dele decide:
| Resposta | O que acontece |
|---|---|
'accept', true ou nada | O drop é concluído. |
'reject' ou false | Sem drop: os itens voltam ao lugar. Um erro lançado também rejeita. |
| Um array de keys | Só esses itens são soltos; os outros voltam ao lugar. |
'continue' | Sem drop, e o arraste continua. |
| Uma promise de qualquer um desses | O overlay espera onde foi solto até a promise se resolver. |
Listas e árvores passam um SortableDropEvent: keys, items, from, to, mode, sameList e
result, os novos arrays de cada lista que o drop altera. Alvos de drop e monitores recebem o
DnDEvent genérico: active, over, position e source. Veja eventos.
Uma regra durante o arraste, ou ao soltar
Email e Chat aceitam anexos de no máximo 10 MB. Arraste demo.mov sobre cada um: Email o recusa enquanto você passa por cima, Chat o aceita e o devolve quando você solta.
Carregando a demo…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
const columns = reactive([
{
id: 'desktop',
title: 'Desktop',
files: [
{ id: 'notes', name: 'notes.pdf', size: 2 },
{ id: 'demo', name: 'demo.mov', size: 48 },
{ id: 'logo', name: 'logo.png', size: 1 },
{ id: 'backup', name: 'backup.zip', size: 120 },
],
},
// Both take files of 10 MB at most, and say so at different times.
{ id: 'email', title: 'Email', rule: 'put' as const, files: [] },
{ id: 'chat', title: 'Chat', rule: 'onBeforeDrop' as const, files: [] },
]);
</script>
<template>
<DnDProvider>
<div class="board">
<Column v-for="column in columns" :key="column.id" :column="column" />
</div>
</DnDProvider>
</template>
<style scoped>
.board {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.board > * {
flex: 0 1 170px;
min-width: 150px;
}
</style>// Asked while dragging: a refused drag never enters the list.
group: { name: 'attachments', put: ({ items }) => fits(items as Attachment[]) },
// Asked on release: the drag enters the list, then flies back.
onBeforeDrop: (e) => (fits(e.items) ? 'accept' : 'reject'),
group.put (ou o accept de um alvo) | onBeforeDrop | |
|---|---|---|
| Consultado | Durante o arraste, assim que ele chega à lista. | Uma vez, quando o arraste é solto. |
| O usuário vê | A lista recusa na hora: nenhuma posição se abre, isAccepting é false, over.accepted é false. | A posição se abre como sempre; os itens voltam ao lugar ao soltar. |
| Sabe | O que é arrastado: keys, items, types, from, to. | O drop inteiro: to.index, sameList, mode, result. |
| Pode | Dizer sim ou não. | Esperar uma promise, aceitar algumas keys, manter o arraste em andamento. |
Use group.put para o que se sabe só pelos itens (um tipo, uma flag, uma contagem): o usuário
fica sabendo antes de soltar. Use onBeforeDrop para o que depende do lugar, da resposta do
usuário ou da do servidor. As regras do próprio alvo vêm primeiro: sobre uma lista ou um alvo que
recusa o arraste, o drop é cancelado e o onBeforeDrop dele não é consultado.
Aceitar alguns dos itens
Algumas regras só podem ser verificadas ao soltar, junto a um servidor. As vagas deste workshop também são reservadas por outras pessoas: ao soltar, a lista pergunta ao servidor quantas restam e aceita essa quantidade das pessoas arrastadas para dentro. Marque algumas e arraste-as juntas.
Carregando a demo…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
const columns = reactive([
{
id: 'waiting',
title: 'Waiting list',
people: [
{ id: 'ada', name: 'Ada', hue: 200 },
{ id: 'bo', name: 'Bo', hue: 30 },
{ id: 'cy', name: 'Cyrus', hue: 280 },
{ id: 'dee', name: 'Dee', hue: 140 },
],
},
{
id: 'workshop',
title: 'Workshop',
seats: true,
people: [
{ id: 'eli', name: 'Eli', hue: 340 },
{ id: 'fay', name: 'Fay', hue: 90 },
],
},
]);
</script>
<template>
<DnDProvider>
<div class="demo">
<p class="tip">Tick a few people, then drag one of them into the workshop</p>
<div class="board">
<Column v-for="column in columns" :key="column.id" :column="column" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
}
.tip {
margin: 0;
font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.55;
}
.board {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 230px));
justify-content: center;
gap: 14px;
width: 100%;
}
</style>makeSortableList(list, {
items: toRef(props.column, 'people'),
group: 'workshop',
// Some keys: those land, the others fly back to where they came from.
onBeforeDrop: async (e) => {
const { free } = await seatsLeft(props.column.people.length); // the overlay waits
return e.keys.slice(0, free);
},
onDrop: (e) => {
selection.delete(...e.accepted); // the ones sent back stay selected
notice.value = `${e.accepted.length} booked, ${e.rejected.length} back to the waiting list`;
},
});
Em um multi-drag, as keys que você retorna pousam juntas e o resto volta
aos seus lugares. O onDrop então as vê em e.accepted e e.rejected, e o e.result dele é o
final. Um array vazio não aceita nada: o drop é recusado.
Continuar arrastando: 'continue'
'continue' cancela o drop, não o arraste. Responda Keep dragging no diálogo acima: o item
continua na mão a partir de onde estava, a página volta para o modo de arraste, e o usuário
segue em frente.
- Com um mouse ou uma caneta, o item segue o ponteiro sem nenhum botão pressionado. O próximo soltar — um clique — solta o item, e esse clique nunca chega à página embaixo.
- Um toque pega o item de novo onde o dedo encosta; levantar o dedo o solta.
- Um arraste por teclado continua: as setas movem o item, Space ou Enter o soltam.
- Escape o cancela, como qualquer arraste.
- O próximo drop consulta o
onBeforeDropde novo.
Os monitores ouvem onDragResume, e os leitores de tela anunciam que o item não foi solto e ainda
está se movendo (substitua o texto com o announcements.onDragResume do provider).
useDnDMonitor({
onDragResume() {
hint.value = 'Still dragging: click to drop, Esc to cancel';
},
});
Perguntar ao usuário
onBeforeDrop pode retornar uma promise. Abra um diálogo, e resolva a promise com o botão que o
usuário escolher:
const { items } = makeSortableList(list, {
id: props.column.id,
items: toRef(props.column, 'tasks'),
group: 'release',
onBeforeDrop: (e) => (props.column.confirm && !e.sameList ? props.ask(e) : 'accept'),
});
type Answer = 'accept' | 'continue' | 'reject';
const question = shallowRef<{ title: string; resolve: (answer: Answer) => void } | null>(null);
/** Opens the dialog: `onBeforeDrop` waits for the button the user picks. */
const ask = (e: SortableDropEvent<Task>) =>
new Promise<Answer>((resolve) => {
question.value = { title: e.items[0]!.title, resolve };
});
Enquanto a promise está pendente:
- o overlay espera onde foi solto, marcado com
data-dnd-overlay="dropping"; useDnD()mantém o estado que tinha no momento de soltar, estatusé'dropping';- a página sai do modo de arraste: o cursor e a seleção de texto voltam, então um diálogo funciona como sempre;
- nenhum outro arraste pode começar.
<dialog> modal aberto com showModal() fica na top layer, acima do overlay que espera.
Transforme o evento cancel dele (Escape) em 'reject', como o exemplo faz.A ordem dos hooks
Reordene as notas, solte uma na lixeira, depois troque uma resposta e faça de novo. O painel lista os hooks do último drop na ordem em que rodaram.
Carregando a demo…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Monitor from './Monitor.vue';
import Notes from './Notes.vue';
import { ANSWERS, type Answer } from './types';
const monitorAnswer = ref<Answer>('accept');
const targetAnswer = ref<Answer>('accept');
// Every hook of the last drop, in the order they ran.
const steps = ref<{ who: string; what: string }[]>([]);
const trace = (who: string, what: string) => steps.value.push({ who, what });
</script>
<template>
<DnDProvider
@drag-start="steps = []"
@drag-end="trace('provider', '@drag-end')"
@drag-cancel="trace('provider', '@drag-cancel')"
@drag-settle="trace('provider', '@drag-settle: landed')"
>
<div class="demo">
<div class="switches">
<fieldset>
<legend>monitor answers</legend>
<label v-for="value in ANSWERS" :key="value" :class="{ on: monitorAnswer === value }">
<input v-model="monitorAnswer" type="radio" :value="value" />
{{ value }}
</label>
</fieldset>
<fieldset>
<legend>target answers</legend>
<label v-for="value in ANSWERS" :key="value" :class="{ on: targetAnswer === value }">
<input v-model="targetAnswer" type="radio" :value="value" />
{{ value }}
</label>
</fieldset>
</div>
<div class="layout">
<Notes :answer="targetAnswer" :trace="trace" />
<ol class="steps" aria-label="Hooks of the last drop">
<li v-for="(step, index) in steps" :key="index">
<span class="who">{{ step.who }}</span>{{ step.what }}
</li>
<li v-if="!steps.length" class="empty">Move a note, or drop one in the trash</li>
</ol>
</div>
<Monitor :answer="monitorAnswer" :trace="trace" />
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.switches {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px 16px;
}
fieldset {
display: inline-flex;
margin: 0;
padding: 3px;
border: 0;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
legend {
float: left;
align-self: center;
padding: 0 8px 0 6px;
font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.55;
}
label {
padding: 5px 10px;
border-radius: 6px;
font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
cursor: pointer;
}
label.on {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
font-weight: 600;
}
label:has(input:focus-visible) {
outline: 2px solid #42b883;
}
input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.layout {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 16px;
}
.steps {
box-sizing: border-box;
min-width: 220px;
min-height: 150px;
margin: 0;
padding: 10px 14px 10px 32px;
border-radius: 10px;
background: color-mix(in srgb, currentColor 5%, transparent);
font: 11px/1.8 ui-monospace, Menlo, Consolas, monospace;
white-space: nowrap;
list-style: decimal;
}
.steps li::marker {
color: color-mix(in srgb, currentColor 45%, transparent);
}
.who {
display: inline-block;
min-width: 60px;
color: #42b883;
}
.steps li.empty {
margin-left: -18px;
list-style: none;
opacity: 0.45;
}
</style>- O
onBeforeDropde cadauseDnDMonitor, um de cada vez. Um'reject'ou um'continue'encerra a consulta; keys restringem o drop. - O
onBeforeDropdo alvo: a lista ou a árvore em que os itens entram, ou o alvo de drop. - O
onDropsó do alvo: uma lista que os itens deixam rumo a outra não é consultada, e o novo array dela está noe.resultdo alvo. Quando itens são removidos (uma lixeira,dropOutside: 'remove'), oonDropda lista deles roda, comto.indexigual a-1. - A gravação padrão: cada lista que o drop altera recebe o seu novo array.
- O
onDragEnddo provider (@drag-end) e dos monitores. Depois o overlay pousa, eonDragSettle(@drag-settle) avisa que pousou.
Um drop recusado pula direto para onDragCancel (@drag-cancel), e os itens voltam ao lugar. Um
'continue' também encerra a consulta, com onDragResume: o arraste continua, e o próximo drop
consulta de novo.
Os monitores decidem antes do alvo, então um veto nunca vem depois que uma lista já aplicou um drop por conta própria. Isso faz de um monitor o lugar para regras sobre todo drop do app:
const { syncing } = useSync();
// Read-only while syncing: every drop under the provider flies back.
useDnDMonitor({
onBeforeDrop: () => (syncing.value ? 'reject' : 'accept'),
});
onDrop: avisado antes da gravação
makeSortableList(listRef, {
items: tasks,
onDrop: (e) => {
// `tasks` still holds the old order, `e.result` the new one.
console.log(e.keys, e.from, e.to, e.result.get(e.to.list));
},
});
O onDrop de uma lista roda assim que o drop é aceito, de forma síncrona, logo antes de a lista
gravá-lo. Para agir depois da gravação, use o @drag-end do provider ou
useDnDMonitor({ onDragEnd }). Em um alvo de drop simples, o onDrop é onde você muda os seus
dados: a biblioteca não move nada por ele.
Stores e servidores: listas controladas
const store = useBoardStore();
const { items } = makeSortableList(listRef, {
id: 'todo',
group: 'board',
items: () => store.lists.todo, // a getter: read-only
onDrop: (e) => {
e.preventDefault();
for (const [listId, next] of e.result) store.setList(listId, next);
},
});
A biblioteca só grava um drop em um ref gravável. Um getter, um computed ou um ref readonly
nunca é gravado: chame e.preventDefault() e aplique e.result você mesmo — os novos arrays de
cada lista que o drop altera, por id de lista. preventDefault() pula a gravação em todas elas, já
que gravar só em algumas perderia ou duplicaria itens. Os builds de desenvolvimento avisam quando o
drop de uma lista somente leitura fica a cargo da biblioteca.
Salvar em um servidor primeiro
Mova um card com o servidor flaky (uma requisição sim, outra não, falha) ou down. O card espera com um selo enquanto a requisição roda, depois pousa, ou volta ao lugar se ela falhou.
Carregando a demo…
<script setup lang="ts">
import { DnDProvider, DragPreview } from '@vue-dnd-kit/core';
import Column from './Column.vue';
import { mode, requests, saving, type ServerMode } from './server';
const MODES: ServerMode[] = ['reliable', 'flaky', 'down'];
</script>
<template>
<DnDProvider>
<div class="demo">
<fieldset class="modes">
<legend>server</legend>
<label v-for="value in MODES" :key="value" :class="{ on: mode === value }">
<input v-model="mode" type="radio" :value="value" />
{{ value }}
</label>
</fieldset>
<div class="board">
<Column id="backlog" title="Backlog" />
<Column id="sprint" title="Sprint" />
</div>
<ol class="requests" aria-label="Requests">
<li v-for="request in requests" :key="request.id" :class="`status-${request.status}`">
{{ request.text }} <span>{{ request.status === 'pending' ? '…' : request.status }}</span>
</li>
<li v-if="!requests.length" class="empty">No requests yet</li>
</ol>
</div>
<!-- The overlay of every drag: the card's copy, and a badge while the server answers. -->
<template #overlay="{ active }">
<div class="carried">
<DragPreview :id="active.id" />
<span v-if="saving" class="saving">Saving…</span>
</div>
</template>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.modes {
display: inline-flex;
margin: 0;
padding: 3px;
border: 0;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
legend {
float: left;
align-self: center;
padding: 0 8px 0 6px;
font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.55;
}
.modes label {
padding: 5px 10px;
border-radius: 6px;
font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
cursor: pointer;
}
.modes label.on {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
font-weight: 600;
}
.modes label:has(input:focus-visible) {
outline: 2px solid #42b883;
}
.modes input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.board {
display: grid;
grid-template-columns: repeat(2, minmax(0, 220px));
gap: 14px;
justify-content: center;
width: 100%;
}
.requests {
width: min(100%, 454px);
min-height: 60px;
margin: 0;
padding: 8px 12px;
border-radius: 10px;
background: color-mix(in srgb, currentColor 5%, transparent);
font: 11px/1.6 ui-monospace, Menlo, Consolas, monospace;
list-style: none;
white-space: pre;
overflow-x: auto;
}
.requests li + li {
opacity: 0.55;
}
.requests .status-200 span {
color: #42b883;
}
.requests .status-503 span {
color: #e5484d;
}
.requests li.empty {
color: inherit;
opacity: 0.45;
}
.carried {
position: relative;
width: 100%;
height: 100%;
}
.saving {
position: absolute;
top: 50%;
right: 10px;
display: inline-flex;
align-items: center;
gap: 6px;
translate: 0 -50%;
padding: 4px 8px;
border-radius: 99px;
background: #42b883;
color: #fff;
font: 600 11px/1 system-ui, sans-serif;
}
.saving::before {
content: '';
width: 9px;
height: 9px;
border: 2px solid rgb(255 255 255 / 0.4);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to {
rotate: 1turn;
}
}
</style>const { items } = makeSortableList(list, {
id: props.id,
// A getter: the library never writes it. The drop is applied below, once saved.
items: () => board[props.id]!,
group: 'sprint',
onBeforeDrop: async (e) => {
e.preventDefault();
try {
await saveMove(e.keys, e.to);
} catch {
return 'reject'; // nothing applied: the card flies back
}
for (const [id, tasks] of e.result) board[id] = tasks; // lands in its new place
return 'accept';
},
});
Aplique e.result antes de a promise resolver, e o overlay pousa na nova ordem. Quando a
requisição falha, não aplique nada e rejeite: os itens voltam ao lugar, e os monitores ouvem
onDragCancel.
O selo vem do slot #overlay do provider, que renderiza o overlay de todo arraste. Um
DragPreview mostra a cópia de sempre do card, e o selo fica por cima enquanto a requisição roda:
<DnDProvider>
<Board />
<template #overlay="{ active }">
<div class="carried">
<DragPreview :id="active.id" />
<span v-if="saving" class="saving">Saving…</span>
</div>
</template>
</DnDProvider>
Soltar fora de qualquer alvo
Arraste um app para fora do dock e solte, em cada modo.
Carregando a demo…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider, type DropOutside } from '@vue-dnd-kit/core';
import Dock from './Dock.vue';
const MODES: DropOutside[] = ['placeholder', 'cancel', 'remove'];
const dropOutside = ref<DropOutside>('remove');
</script>
<template>
<DnDProvider>
<div class="demo">
<fieldset class="modes">
<legend>dropOutside</legend>
<label v-for="value in MODES" :key="value" :class="{ on: dropOutside === value }">
<input v-model="dropOutside" type="radio" :value="value" />
{{ value }}
</label>
</fieldset>
<code class="call">makeSortableList(dock, { strategy: 'horizontal', dropOutside: '{{ dropOutside }}' })</code>
<Dock :drop-outside="dropOutside" />
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.modes {
display: inline-flex;
margin: 0;
padding: 3px;
border: 0;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
legend {
float: left;
align-self: center;
padding: 0 8px 0 6px;
font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.55;
}
.modes label {
padding: 5px 10px;
border-radius: 6px;
font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
cursor: pointer;
}
.modes label.on {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
font-weight: 600;
}
.modes label:has(input:focus-visible) {
outline: 2px solid #42b883;
}
.modes input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.call {
max-width: 100%;
padding: 6px 10px;
overflow-x: auto;
border-radius: 8px;
background: color-mix(in srgb, currentColor 6%, transparent);
font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
white-space: nowrap;
color: #42b883;
}
</style>const { items } = makeSortableList(dock, {
items: apps,
strategy: 'horizontal',
dropOutside: () => props.dropOutside,
// Called on the list the items leave, with `to.index` -1, when they are removed.
onDrop: (e) => {
if (e.to.index === -1) notice.value = `Removed ${e.items.map((app) => app.name).join(', ')}`;
},
});
dropOutside | Um drop fora de qualquer alvo… |
|---|---|
'placeholder' | Pousa onde o placeholder está: o último lugar que a lista mostrou. O padrão. |
'cancel' | É cancelado: os itens voltam ao lugar. O padrão com placeholder: 'indicator'. |
'remove' | Remove os itens da lista deles. O onBeforeDrop da lista é consultado primeiro, e o onDrop dela recebe to.index igual a -1. |
Ele é lido no drop, então um ref ou um getter pode trocá-lo. Uma lixeira faz o mesmo de propósito:
makeDroppable(trash, { onDrop: (e) => e.source?.remove() }). Veja
arrastar e soltar.