Dokumentation/Leitfäden
Drag and Drop
Freies Ziehen mit makeDraggable und Drop-Ziele mit makeDroppable – typisierte Ziele, die manche Drags annehmen und andere ablehnen, ein Grundriss, Papierkorb-Zonen, Ordner in einer Liste und einfache Elemente, die eine Liste aufnimmt.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Nimm makeDraggable und makeDroppable für alles, was keine sortierbare Liste ist: Müll in
Tonnen, Möbel auf einem Grundriss, Dateien in Ordner, ein Papierkorb, ein Schachbrett.
Demo wird geladen…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type Key } from '@vue-dnd-kit/core';
import Bin from './Bin.vue';
import { BINS, WASTE } from './waste';
// The tray takes everything back (no `accept`); the bins take what their rule allows.
const TRAY = { id: 'tray', name: 'To sort', color: '#42b883', accept: '*', rule: "'*'" };
/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>
<template>
<DnDProvider>
<div class="recycling">
<Bin :bin="TRAY" :items="itemsIn('tray')" tray @drop="drop('tray', $event)" />
<div class="bins">
<Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.recycling {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 600px;
margin: 0 auto;
}
.bins {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
@media (max-width: 560px) {
.bins {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>Ein ziehbares Element
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ waste: { id: string; name: string; type: string } }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, {
id: () => props.waste.id, // read back as `active.id`
type: () => props.waste.type, // matched by the targets' `accept`
data: () => props.waste, // anything: `active.data`
});
</script>
<template>
<div ref="el" :class="{ dragging: isDragging }">{{ waste.name }}</div>
</template>
Übergib die ID aus deinen Daten, als Getter. Die Standard-ID kommt von useId() und ist für
jede Komponenteninstanz neu, sie bedeutet dem Rest deiner App also nichts. Optionen werden
gelesen, wenn sie gebraucht werden, ein Getter oder ein Ref hält sie also aktuell.
| Option | Was sie tut |
|---|---|
id, data, type | Wer gezogen wird, was er mitträgt und womit das accept der Ziele abgeglichen wird. |
handle | Ein Selektor oder ein Element darin: Nur dieses startet einen Drag. Siehe Handles. |
disabled | Es startet kein Drag. |
overlay | true (Standard): Eine Kopie folgt dem Zeiger. Eine Komponente: deine eigene Vorschau. false: Das Element bewegt sich selbst (unten). |
axis, modifiers | Den Drag auf 'x' oder 'y' sperren, einrasten lassen, in Grenzen halten: siehe Achsen, Grenzen und Raster. |
keyboardMode | Wie die Pfeiltasten es bewegen: pixelweise (Standard) oder von Ziel zu Ziel (unten). |
activation, canDrag | Wann ein Drücken zum Drag wird: siehe Sensoren. |
attributes | role, tabindex und aria-* für Tastaturen und Screenreader (Standard true). |
| Rückgabe | Was es ist |
|---|---|
isDragging | Dieses Element wird gezogen. |
selected | Beschreibbar, an die Auswahl des Providers gebunden. |
transform | Der Versatz des Drags, für overlay: false. |
id | Die verwendete ID. |
Buttons, Eingabefelder und andere interaktive Kinder starten keinen Zeiger-Drag, sie funktionieren in einem ziehbaren Element also weiter. Alle Optionen stehen in der makeDraggable-API.
Ein Drop-Ziel
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable, type Key } from '@vue-dnd-kit/core';
import type { Bin, Waste } from './waste';
const props = defineProps<{ bin: Bin; items: Waste[] }>();
const emit = defineEmits<{ drop: [id: Key] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isOver, isAccepting } = makeDroppable(el, {
id: props.bin.id,
accept: props.bin.accept, // 'glass', ['paper', 'cardboard'], a function…
onDrop: (event) => emit('drop', event.active.id),
});
</script>
<template>
<section ref="el" :class="{ accepting: isAccepting, over: isOver && isAccepting, refused: isOver && !isAccepting }">
<WasteItem v-for="waste in items" :key="waste.id" :waste="waste" />
</section>
</template>
| Rückgabe | true, wenn |
|---|---|
isOver | Der Drag über diesem Ziel ist, egal ob es ihn annimmt oder nicht. |
isAccepting | Gerade etwas gezogen wird und dieses Ziel es annimmt: Heb die gültigen Ziele hervor. |
placement | Kein Boolean: { top, right, bottom, left }, die Hälften des Ziels, in denen der Zeiger ist, oder null, wenn es nicht überfahren wird. Nimm es für „vor / nach dieser Karte“. |
isOver ohne isAccepting heißt, dass der Drag über einem Ziel ist, das ihn ablehnt. Zeig
das, wie es die Tonnen oben tun: Ein Drop dort wird abgebrochen, und das Element fliegt zurück.
Die Tonnen, die das Element nicht nehmen können, verblassen, während es unterwegs ist; weitere
Looks unter Styling.
| Option | Was sie tut |
|---|---|
id, data | Zurückgelesen als over.id und over.data. |
accept, acceptMode | Welche Drags es annimmt (nächster Abschnitt). |
disabled | Es nimmt nichts an, und Drags gleiten darüber hinweg. |
priority | Höher gewinnt, wenn sich Ziele ohne Verschachtelung überlappen (Standard 0). |
onDragEnter, onDragLeave | Der Drag kam herein oder ging hinaus. |
onBeforeDrop, onDrop | Über den Drop entscheiden, dann ihn anwenden (unten). |
Welche Drags ein Ziel annimmt
Der type eines ziehbaren Elements wird mit dem accept eines Ziels abgeglichen:
accept | Nimmt an |
|---|---|
weggelassen oder '*' | Alles. |
'glass' | Ziehbare Elemente vom Typ glass und solche ohne Typ. |
['paper', 'cardboard'] | Beide Typen und solche ohne Typ. |
({ active, items, types }) => boolean | Was deine Funktion sagt. |
Ein ziehbares Element ohne type passt zu jeder String-Regel. type kann auch ein Array sein
(['paper', 'large']): Ein passender Typ genügt. Eine Funktion entscheidet anhand von allem, was
den Drag betrifft. Die Kompost-Tonne oben liest die Daten der Elemente, nicht ihren Typ:
{
id: 'compost',
accept: ({ items }) => items.every((item) => (item.data as Waste).compostable),
}
accept wird live gelesen, während der Drag sein Ziel sucht: Übergib ein Ref, um es zu ändern.
Wenn mehrere Elemente auf einmal gezogen werden, sagt acceptMode, wie viele passen müssen:
'all' (Standard) oder 'some'. Gib bei 'some' aus onBeforeDrop die IDs zurück, die
bleiben sollen, und die anderen fliegen zurück. Siehe
Über einen Drop entscheiden.
Der Drop
onDrop eines einfachen Drop-Ziels ist der Ort, an dem du deine Daten änderst. Die
Bibliothek verschiebt von selbst nichts:
<script setup lang="ts">
/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>
<template>
<Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
</template>
| Feld des Events | Was es enthält |
|---|---|
active | Der Drag: id, ids, items (je { id, data, type, item }), data, type. |
over | Dieses Ziel: id, data, element, rect, accepted. |
pointer, delta | Wo der Zeiger ist und wie weit er sich seit dem Start bewegt hat. |
position | Wo die gezogene Box in diesem Ziel sitzt (nächster Abschnitt). |
source | Gesetzt, wenn die Elemente aus einer sortierbaren Liste kommen: { list, remove() }. |
active.ids enthält jede gezogene ID, die gegriffene zuerst. Die Reihenfolge ist
onBeforeDrop (entscheiden), onDrop (anwenden), dann drag-end des Providers. Um jeden Drag
der Seite an einer Stelle zu hören, nimm useDnDMonitor (siehe
Der Provider). Um vor einem Drop nachzufragen, den Drag weiterlaufen zu
lassen oder nur einige Elemente anzunehmen, siehe
Über einen Drop entscheiden.
Dort lassen, wo es abgelegt wurde
Ein Grundriss, eine Zeichenfläche, ein Board: Das Element bleibt dort, wo du loslässt. Jedes
Event trägt position, das left / top, das ein absolut positioniertes Element dorthin setzt,
wo es abgelegt wurde.
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Planner from './Planner.vue';
</script>
<template>
<DnDProvider>
<Planner />
</DnDProvider>
</template>const room = useTemplateRef<HTMLElement>('room');
makeDroppable(room, {
id: 'room',
onDrop: ({ active, position }) => {
const piece = find(active.id);
if (!piece || !position || !room.value) return;
// Clamped, so a piece let go half outside stays inside the walls.
piece.x = clamp(position.x, room.value.clientWidth - piece.width);
piece.y = clamp(position.y, room.value.clientHeight - piece.height);
piece.place = 'room';
},
});
<div ref="el" class="piece" :style="{ left: `${piece.x}px`, top: `${piece.y}px` }">…</div>
position ist die linke obere Ecke der gezogenen Box in over, gemessen ab der
Padding-Kante des Ziels, plus dessen Scroll: Der Rahmen des Raums, seine Wände, zählt nicht. Über nichts
ist es null.
active.rect und delta: Damit fehlen Rahmen und Scroll des
Ziels. Raster, Grenzen und Achsensperren für solche Boards stehen in
Achsen, Grenzen und Raster.Ohne Overlay
Standardmäßig folgt eine Kopie des Elements dem Zeiger, und das Element bleibt an seinem Platz:
So funktionieren die Möbel im Lager. Mit overlay: false gibt es keine Kopie, und das Element
bewegt sich selbst um das transform, das der Drag zurückgibt: die Möbel im Raum.
const inRoom = computed(() => props.piece.place === 'room');
const { isDragging, transform } = makeDraggable(el, {
id: () => props.piece.id,
overlay: computed(() => !inRoom.value), // a value or a ref, read when the drag starts
});
const style = computed(() => ({
left: `${props.piece.x}px`,
top: `${props.piece.y}px`,
...(transform.value && { transform: `translate3d(${transform.value.x}px, ${transform.value.y}px, 0)` }),
}));
positionist dann die eigene Ecke des Elements, der Drop-Handler bleibt also derselbe.- Das Element liegt beim Bewegen unter dem Zeiger; beim Suchen des Ziels schaut die Bibliothek
hindurch. Heb es während des Ziehens über seine Nachbarn (
z-index). - Es scrollt mit der Seite, das Overlay nicht:
transformzieht ab, was sich sein Container seit dem Start des Drags bewegt hat, so bleibt es unter dem Zeiger, wenn die Seite scrollt. - Es bewegt sich in seinem eigenen Container: Ein Elternelement mit
overflow: hiddenbeschneidet es, und sein Stapelkontext entscheidet, worüber es hinweggleitet. Das Overlay hat keines der beiden Probleme.
Für eine eigene Vorschau statt der Kopie siehe Das Overlay.
Papierkorb-Zonen
Ein Drag, der in einer sortierbaren Liste startet, trägt e.source. Ruf remove() auf, und die
Liste entfernt die Elemente:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Triage from './Triage.vue';
</script>
<template>
<DnDProvider>
<Triage />
</DnDProvider>
</template>const triage = (bin: 'done' | 'deleted') => (e: DnDEvent) => {
bins[bin].push(...e.active.items.map((entry) => entry.item as Notice));
e.source?.remove();
};
makeDroppable(doneZone, { id: 'done', onDrop: triage('done') });
makeDroppable(trashZone, { id: 'trash', onDrop: triage('deleted') });
e.active.items[i].itemist das Element der Liste selbst. Behalte es, und ein Rückgängig stellt es dorthin zurück, wo es war.- Das eigene
onDropder Liste läuft dann mitto.index === -1, unde.resultenthält die Liste ohne die Elemente. - Das Overlay schrumpft in die Zone hinein, da das Element nirgends mehr angezeigt wird.
- Eine Liste kann auch entfernen, was außerhalb von allem abgelegt wird: siehe
dropOutside.
Delete ist
für viele trotzdem schneller: Biete auch so etwas an.In ein sortierbares Element ablegen
Eine Zeile einer sortierbaren Liste kann auch ein Drop-Ziel sein: ein Ordner, der Lesezeichen annimmt, eine Gruppe, die Aufgaben annimmt. Setz ein Droppable auf das Element:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Bookmarks from './Bookmarks.vue';
</script>
<template>
<DnDProvider>
<Bookmarks />
</DnDProvider>
</template>const el = useTemplateRef<HTMLElement>('el');
makeSortableItem(el, { item: () => props.folder, type: 'folder' });
const { isOver } = makeDroppable(el, {
accept: 'link',
onDrop: (e) => {
emit('add', e.active.items.map((entry) => entry.item as Link)); // Bookmarks adds them
e.source?.remove(); // take them out of the list they came from
},
});
Ein geöffneter Ordner zeigt seine Links als Liste derselben Gruppe, die nur Links annimmt: Dort werden sie sortiert, und man kann sie zurück auf die oberste Ebene oder in einen anderen Ordner ziehen.
Solange die Liste den Drag sortieren würde, zählt ein solches Ziel nur in der Mitte seines Elements (25 bis 75 % entlang der Achse der Liste). Dort geht der Drop in das Element, und die Liste bewegt sich nicht. Nahe an den Rändern sortiert die Liste wie gewohnt.
- Ein Ziel, dessen
acceptden Drag ablehnt, wird übersprungen: Die Liste sortiert über das Element hinweg wie über jedes andere. Die Ordner nehmen nur Links an, also sortiert sich ein gezogener Ordner an den anderen vorbei. - Ein Ziel erhält nie seinen eigenen Drag.
- Mit der Tastatur hält ein Pfeil zuerst auf dem Ordner an; der nächste Druck in dieselbe Richtung geht an ihm vorbei.
- Das Element wird nach dem Drop nicht mehr angezeigt, also schrumpft das Overlay in den Ordner hinein, statt zurückzufliegen.
- Über dem Ordner lässt die Liste den Slot der gezogenen Zeile, wo er war, damit sich unter dem
Zeiger nichts verschiebt. Der Slot bekommt dann
data-dnd-placeholder="into": Der Drop geht nicht dorthin, also blende ihn ruhig aus ([data-dnd-placeholder='into'] { opacity: 0 }).
Der Dateimanager macht dasselbe in einem Raster, mit verschachtelten Ordnern.
Einfache ziehbare Elemente in eine Liste
Eine sortierbare Liste nimmt Elemente auf, die mit makeDraggable gezogen werden, wenn sie weiß,
wie sie daraus ein Element macht:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Agenda from './Agenda.vue';
import BlockChip from './BlockChip.vue';
const BLOCKS = [
{ title: 'Focus', minutes: 90, color: '#42b883' },
{ title: 'Meeting', minutes: 30, color: '#3b82f6' },
{ title: 'Lunch', minutes: 60, color: '#f59e0b' },
{ title: 'Gym', minutes: 45, color: '#ec4899' },
{ title: 'Break', minutes: 15, color: '#a1a1aa' },
];
</script>
<template>
<DnDProvider>
<div class="planner">
<aside class="blocks">
<h4>Blocks</h4>
<BlockChip v-for="block in BLOCKS" :key="block.title" :block="block" />
</aside>
<Agenda />
</div>
</DnDProvider>
</template>
<style scoped>
.planner {
display: flex;
justify-content: center;
gap: 24px;
}
.blocks {
display: flex;
flex: none;
flex-direction: column;
gap: 6px;
width: 140px;
}
h4 {
margin: 0 0 6px;
font: 600 11px/1 ui-monospace, Menlo, Consolas, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
@media (max-width: 480px) {
.blocks {
width: 112px;
}
}
</style>const { items } = makeSortableList(list, {
items: agenda,
createItem: (active) => ({ ...(active.data as Block), id: `entry-${++created}` }),
});
Solange das ziehbare Element über der Liste ist, sitzt das erzeugte Element im Entwurf und wird
von deinem Template gerendert wie jede andere Zeile: Die Uhrzeiten der Agenda zählen es schon
mit. Beim Drop wird es echt (mode: 'insert', from.list === null), und das Overlay landet
darauf.
- Gib
undefinedzurück, um abzulehnen. Listen ohnecreateItemignorieren einfache ziehbare Elemente. - Gib dem neuen Element einen eigenen Schlüssel: Der Block bleibt in der Palette.
- Mit der Tastatur nimmt ein Pfeil in Richtung der Liste das ziehbare Element auf, auf seiner Höhe, und die nächsten Pfeile bewegen es slotweise.
Dateien vom Computer
Dateien, die vom Desktop oder aus einem Dateimanager hereingezogen werden, sind keine Drags der
Bibliothek: Der Browser trägt sie. Ein Drop-Ziel nimmt sie mit external an, und der Drop
bringt sie in e.external:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import AvatarDrop from './AvatarDrop.vue';
import Attachments from './Attachments.vue';
</script>
<template>
<DnDProvider>
<div class="form">
<AvatarDrop />
<Attachments />
</div>
</DnDProvider>
</template>
<style scoped>
.form {
display: flex;
flex-wrap: wrap;
align-items: stretch;
justify-content: center;
gap: 16px;
}
</style>const { isOver, isAccepting } = makeDroppable(el, {
external: { files: 'image/*', multiple: false },
onDrop: (e) => show(e.external!.files[0]!),
});
filesliest sich wie<input accept>: MIME-Typen (image/*,application/pdf) und Dateiendungen (.md).truenimmt jede Datei an, eine Funktion entscheidet Datei für Datei.linksundtextnehmen an, was andere Apps und Tabs ziehen,typesjeden anderen MIME-Typ.- Der Drag läuft als Drag des Providers:
isOver,isAccepting, die Events und die Ansagen funktionieren wie bei jedem anderen. Ziele ohneexternalignorieren ihn. Es gibt kein Overlay: Der Browser zeichnet das gezogene Bild. - Solange die Dateien über der Seite gehalten werden, nennt der Browser nur ihre Typen
(
e.external.fileTypes). Ihre Namen, Größen und Inhalte kommen mit dem Drop, und erst dann werden Dateiendungen geprüft. Ordner werden ausgelassen; mitmultiple: falsewird ein Drag mehrerer Dateien abgelehnt.
Listen nehmen ebenfalls Dateien, Links und Text an und machen ihnen Platz, solange sie gehalten werden. Elemente können die Seite auch verlassen, in eine andere App oder ein anderes Fenster von dir: siehe Dateien und andere Apps.
<input type="file"> an, wie die Demo mit Browse.Tastatur
Ein fokussiertes ziehbares Element wird mit Space oder Enter aufgenommen, mit denselben Tasten abgelegt, und Escape bricht ab. Dazwischen bewegen es die Pfeiltasten:
keyboardMode | Ein Pfeil |
|---|---|
'pixels' (Standard) | Bewegt es um 25 px, den keyboard.step des Providers. Mit Shift viermal so weit. So bewegen sich die Möbel. |
'droppables' | Springt zum nächstgelegenen Ziel in dieser Richtung, das es annimmt. Der Müll springt von Tonne zu Tonne und überspringt die Tonnen, die ihn ablehnen. |
makeDraggable(el, { id: () => props.waste.id, type: () => props.waste.type, keyboardMode: 'droppables' });
Die Tasten lassen sich am Provider neu belegen. Mehr unter Barrierefreiheit.