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.

Live

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

WasteItem.vue
<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.

OptionWas sie tut
id, data, typeWer gezogen wird, was er mitträgt und womit das accept der Ziele abgeglichen wird.
handleEin Selektor oder ein Element darin: Nur dieses startet einen Drag. Siehe Handles.
disabledEs startet kein Drag.
overlaytrue (Standard): Eine Kopie folgt dem Zeiger. Eine Komponente: deine eigene Vorschau. false: Das Element bewegt sich selbst (unten).
axis, modifiersDen Drag auf 'x' oder 'y' sperren, einrasten lassen, in Grenzen halten: siehe Achsen, Grenzen und Raster.
keyboardModeWie die Pfeiltasten es bewegen: pixelweise (Standard) oder von Ziel zu Ziel (unten).
activation, canDragWann ein Drücken zum Drag wird: siehe Sensoren.
attributesrole, tabindex und aria-* für Tastaturen und Screenreader (Standard true).
RückgabeWas es ist
isDraggingDieses Element wird gezogen.
selectedBeschreibbar, an die Auswahl des Providers gebunden.
transformDer Versatz des Drags, für overlay: false.
idDie 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

Bin.vue
<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ückgabetrue, wenn
isOverDer Drag über diesem Ziel ist, egal ob es ihn annimmt oder nicht.
isAcceptingGerade etwas gezogen wird und dieses Ziel es annimmt: Heb die gültigen Ziele hervor.
placementKein 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.

OptionWas sie tut
id, dataZurückgelesen als over.id und over.data.
accept, acceptModeWelche Drags es annimmt (nächster Abschnitt).
disabledEs nimmt nichts an, und Drags gleiten darüber hinweg.
priorityHöher gewinnt, wenn sich Ziele ohne Verschachtelung überlappen (Standard 0).
onDragEnter, onDragLeaveDer 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:

acceptNimmt 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 }) => booleanWas 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:

waste.ts
{
  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:

index.vue
<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 EventsWas es enthält
activeDer Drag: id, ids, items (je { id, data, type, item }), data, type.
overDieses Ziel: id, data, element, rect, accepted.
pointer, deltaWo der Zeiger ist und wie weit er sich seit dem Start bewegt hat.
positionWo die gezogene Box in diesem Ziel sitzt (nächster Abschnitt).
sourceGesetzt, 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.

Live

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>
Planner.vue
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';
  },
});
Piece.vue
<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.

Berechne die Stelle nicht aus 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.

Piece.vue
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)` }),
}));
  • position ist 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: transform zieht 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: hidden beschneidet 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:

Live

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>
Triage.vue
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].item ist das Element der Liste selbst. Behalte es, und ein Rückgängig stellt es dorthin zurück, wo es war.
  • Das eigene onDrop der Liste läuft dann mit to.index === -1, und e.result enthä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.
Mit der Tastatur bewegen die Pfeiltasten ein Listenelement zwischen den Slots seiner Liste und den Listen seiner Gruppe; hinter dem Ende der Listen bringt der nächste Druck es weiter zum Drop-Ziel in dieser Richtung, hier dem Papierkorb, und der Pfeil in die Gegenrichtung holt es zurück. Ein Button, ein Menü oder die Taste 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:

Live

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>
FolderRow.vue
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 accept den 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:

Live

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>
Agenda.vue
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 undefined zurück, um abzulehnen. Listen ohne createItem ignorieren 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:

Live

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>
AvatarDrop.vue
const { isOver, isAccepting } = makeDroppable(el, {
  external: { files: 'image/*', multiple: false },
  onDrop: (e) => show(e.external!.files[0]!),
});
  • files liest sich wie <input accept>: MIME-Typen (image/*, application/pdf) und Dateiendungen (.md). true nimmt jede Datei an, eine Funktion entscheidet Datei für Datei. links und text nehmen an, was andere Apps und Tabs ziehen, types jeden anderen MIME-Typ.
  • Der Drag läuft als Drag des Providers: isOver, isAccepting, die Events und die Ansagen funktionieren wie bei jedem anderen. Ziele ohne external ignorieren 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; mit multiple: false wird 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.

Wer mit Tastatur oder Screenreader arbeitet, kann keine Dateien ziehen. Biete neben der Zone ein <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:

keyboardModeEin 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.