Dokumentation/Leitfäden
Über einen Drop entscheiden
Nimm einen Drop an, lehne ihn ab oder schiebe ihn auf – Regeln beim Ziehen oder beim Loslassen, ein Teil eines Multi-Drags, ein Dialog, ein Server, der zuerst zustimmen muss, Monitore mit Vetorecht und Drops außerhalb aller Ziele.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Ziehe eine Karte nach Done. Der Drop wartet auf deine Antwort: die Karte verschieben, sie woandershin weiterziehen oder abbrechen.
Demo wird geladen…
<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'),
});
Listen, Bäume, Drop-Ziele (makeDroppable) und Monitore (useDnDMonitor) nehmen ein
onBeforeDrop. Es läuft, wenn der Drag losgelassen wird, bevor sich irgendetwas ändert, und seine
Antwort entscheidet:
| Antwort | Was passiert |
|---|---|
'accept', true oder nichts | Der Drop geht durch. |
'reject' oder false | Kein Drop: Die Elemente fliegen zurück. Ein geworfener Fehler lehnt ebenfalls ab. |
| Ein Array von Schlüsseln | Nur diese Elemente werden abgelegt; die anderen fliegen zurück. |
'continue' | Kein Drop, und der Drag geht weiter. |
| Ein Promise auf eines davon | Das Overlay wartet dort, wo es losgelassen wurde, bis das Promise erfüllt ist. |
Listen und Bäume übergeben ein SortableDropEvent: keys, items, from, to, mode,
sameList und result, die neuen Arrays jeder Liste, die der Drop ändert. Drop-Ziele und
Monitore bekommen das allgemeine DnDEvent: active, over, position und source. Siehe
Events.
Eine Regel beim Ziehen oder beim Loslassen
Email und Chat nehmen beide Anhänge bis höchstens 10 MB an. Ziehe demo.mov über beide: Email lehnt die Datei schon beim Überfahren ab, Chat nimmt sie an und schickt sie zurück, wenn du loslässt.
Demo wird geladen…
<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 (oder das accept eines Ziels) | onBeforeDrop | |
|---|---|---|
| Gefragt | Beim Ziehen, sobald der Drag die Liste erreicht. | Einmal, wenn der Drag losgelassen wird. |
| Der Nutzer sieht | Die Liste lehnt sofort ab: Kein Slot öffnet sich, isAccepting ist false, over.accepted ist false. | Der Slot öffnet sich wie gewohnt; die Elemente fliegen beim Loslassen zurück. |
| Weiß | Was gezogen wird: keys, items, types, from, to. | Den ganzen Drop: to.index, sameList, mode, result. |
| Kann | Ja oder Nein sagen. | Auf ein Promise warten, einige Schlüssel annehmen, den Drag weiterlaufen lassen. |
Nimm group.put für das, was allein aus den Elementen bekannt ist (ein Typ, ein Flag, eine
Anzahl): Der Nutzer erfährt es, bevor er loslässt. Nimm onBeforeDrop für das, was vom Ort, von
der Antwort des Nutzers oder der des Servers abhängt. Die eigenen Regeln des Ziels kommen zuerst:
Über einer Liste oder einem Ziel, das den Drag ablehnt, wird der Drop abgebrochen, und sein
onBeforeDrop wird nicht gefragt.
Einige der Elemente annehmen
Manche Regeln lassen sich erst beim Loslassen prüfen, bei einem Server. Plätze in diesem Workshop buchen auch andere: Beim Loslassen fragt die Liste den Server, wie viele noch frei sind, und nimmt so viele der hereingezogenen Personen an. Hake ein paar an und ziehe sie gemeinsam hinein.
Demo wird geladen…
<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`;
},
});
Bei einem Multi-Drag landen die Schlüssel, die du zurückgibst,
gemeinsam, und der Rest fliegt an seine Plätze zurück. onDrop sieht sie dann in e.accepted und
e.rejected, und sein e.result ist das endgültige. Ein leeres Array nimmt nichts an: Der Drop
wird abgelehnt.
Weiterziehen: 'continue'
'continue' bricht den Drop ab, nicht den Drag. Antworte im Dialog oben mit Keep dragging:
Das Element bleibt von dort aus, wo es war, in der Hand, die Seite geht zurück in den
Drag-Modus, und der Nutzer macht weiter.
- Mit einer Maus oder einem Stift folgt das Element dem Zeiger, ohne dass eine Taste gehalten wird. Das nächste Loslassen – ein Klick – legt es ab, und dieser Klick erreicht die Seite darunter nie.
- Ein Touch nimmt das Element dort wieder auf, wo der Finger aufsetzt; Anheben des Fingers legt es ab.
- Ein Drag per Tastatur geht weiter: Die Pfeiltasten bewegen das Element, Space oder Enter legen es ab.
- Escape bricht ihn ab, wie jeden Drag.
- Der nächste Drop fragt
onBeforeDroperneut.
Monitore hören onDragResume, und Screenreader hören, dass das Element nicht abgelegt wurde und
sich noch bewegt (überschreibe den Text mit announcements.onDragResume des Providers).
useDnDMonitor({
onDragResume() {
hint.value = 'Still dragging: click to drop, Esc to cancel';
},
});
Den Nutzer fragen
onBeforeDrop darf ein Promise zurückgeben. Öffne einen Dialog und erfülle das Promise mit dem
Button, den der Nutzer wählt:
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 };
});
Solange das Promise aussteht:
- wartet das Overlay dort, wo es losgelassen wurde, markiert mit
data-dnd-overlay="dropping"; - behält
useDnD()den Zustand vom Loslassen, undstatusist'dropping'; - ist die Seite aus dem Drag-Modus: Cursor und Textauswahl sind zurück, ein Dialog funktioniert also wie gewohnt;
- kann kein anderer Drag starten.
<dialog>, das mit showModal() geöffnet wird, liegt im Top Layer, über dem wartenden
Overlay. Mach aus seinem cancel-Event (Escape) ein 'reject', wie es das Beispiel
tut.Die Reihenfolge der Hooks
Ordne die Notizen um, wirf eine in den Papierkorb, stelle dann eine Antwort um und mach es noch einmal. Das Panel listet die Hooks des letzten Drops in der Reihenfolge auf, in der sie liefen.
Demo wird geladen…
<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>onBeforeDropjedesuseDnDMonitor, der Reihe nach. Ein'reject'oder ein'continue'beendet das Fragen; Schlüssel grenzen den Drop ein.onBeforeDropdes Ziels: die Liste oder der Baum, in die die Elemente gehen, oder das Drop-Ziel.onDropnur des Ziels: Eine Liste, die Elemente in eine andere verlassen, wird nicht gefragt, und ihr neues Array steht ime.resultdes Ziels. Wenn Elemente entfernt werden (eine Papierkorb-Zone,dropOutside: 'remove'), läuft dasonDropihrer Liste, mitto.indexvon-1.- Das Standard-Schreiben: Jede Liste, die der Drop ändert, übernimmt ihr neues Array.
onDragEnddes Providers (@drag-end) und der Monitore. Dann landet das Overlay, undonDragSettle(@drag-settle) meldet, dass es gelandet ist.
Ein abgelehnter Drop springt stattdessen zu onDragCancel (@drag-cancel), und die Elemente
fliegen zurück. Ein 'continue' beendet das Fragen ebenfalls, mit onDragResume: Der Drag geht
weiter, und sein nächster Drop fragt erneut.
Monitore entscheiden vor dem Ziel, ein Veto kommt also nie, nachdem eine Liste schon einen eigenen Drop angewendet hat. Das macht einen Monitor zum Ort für Regeln über jeden Drop der App:
const { syncing } = useSync();
// Read-only while syncing: every drop under the provider flies back.
useDnDMonitor({
onBeforeDrop: () => (syncing.value ? 'reject' : 'accept'),
});
onDrop: vor dem Schreiben informiert
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));
},
});
Das onDrop einer Liste läuft, sobald der Drop angenommen ist, synchron, direkt bevor die Liste
ihn schreibt. Um nach dem Schreiben zu handeln, nimm @drag-end des Providers oder
useDnDMonitor({ onDragEnd }). An einem einfachen Drop-Ziel ist onDrop der Ort, an dem du deine
Daten änderst: Die Bibliothek verschiebt dafür nichts.
Stores und Server: kontrollierte Listen
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);
},
});
Die Bibliothek schreibt einen Drop nur in ein beschreibbares Ref. Ein Getter, ein computed oder
ein readonly-Ref wird nie beschrieben: Ruf e.preventDefault() auf und wende e.result selbst
an – die neuen Arrays jeder Liste, die der Drop ändert, nach Listen-ID. preventDefault()
überspringt das Schreiben in alle davon, denn nur einige zu schreiben würde Elemente verlieren
oder verdoppeln. Entwicklungs-Builds warnen, wenn der Drop einer schreibgeschützten Liste der
Bibliothek überlassen wird.
Zuerst auf einem Server speichern
Verschiebe eine Karte, während der Server flaky (jede zweite Anfrage schlägt fehl) oder down ist. Die Karte wartet mit einem Badge, während die Anfrage läuft, und landet dann oder fliegt zurück, wenn sie fehlgeschlagen ist.
Demo wird geladen…
<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';
},
});
Wende e.result an, bevor das Promise sich auflöst, und das Overlay landet in der neuen
Reihenfolge. Wenn die Anfrage fehlschlägt, wende nichts an und lehne ab: Die Elemente fliegen
zurück, und Monitore hören onDragCancel.
Das Badge kommt aus dem #overlay-Slot des Providers, der das Overlay jedes Drags rendert. Ein
DragPreview zeigt die übliche Kopie der Karte, und das Badge sitzt obendrauf, während die
Anfrage läuft:
<DnDProvider>
<Board />
<template #overlay="{ active }">
<div class="carried">
<DragPreview :id="active.id" />
<span v-if="saving" class="saving">Saving…</span>
</div>
</template>
</DnDProvider>
Außerhalb aller Ziele ablegen
Ziehe eine App aus dem Dock und lass sie los, in jedem Modus.
Demo wird geladen…
<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 | Ein Drop außerhalb aller Ziele … |
|---|---|
'placeholder' | Landet dort, wo der Platzhalter ist: am letzten Platz, den die Liste gezeigt hat. Der Standard. |
'cancel' | Wird abgebrochen: Die Elemente fliegen zurück. Der Standard bei placeholder: 'indicator'. |
'remove' | Entfernt die Elemente aus ihrer Liste. Das onBeforeDrop der Liste wird zuerst gefragt, und ihr onDrop bekommt to.index von -1. |
Es wird beim Drop gelesen, ein Ref oder ein Getter kann es also umschalten. Eine Papierkorb-Zone
macht absichtlich dasselbe: makeDroppable(trash, { onDrop: (e) => e.source?.remove() }). Siehe
Drag and Drop.