Dokumentation/Leitfäden
Multi-Drag und Auswahl
Wähle mehrere Elemente per Klick oder Auswahlrahmen aus und ziehe sie gemeinsam – als Block, mit Lücken oder im Overlay gestapelt –, nimm beim Drop nur einige davon an und zeige sie in einem eigenen Overlay.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Wähle ein paar Dateien aus und ziehe dann eine davon: Die anderen kommen mit. Schalte multiple
um, um zu sehen, was sie unterwegs tun, und das Layout, um das Overlay eines Rasters zu sehen.
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Files from './Files.vue';
</script>
<template>
<DnDProvider>
<Files />
</DnDProvider>
</template>Die Auswahl
Jeder Provider hält eine Auswahl: eine reaktive Menge von IDs. Ein ausgewähltes Element zu ziehen zieht jedes ausgewählte Element, das gegriffene zuerst. Ein nicht ausgewähltes Element zu ziehen zieht nur dieses Element.
Die Bibliothek entscheidet nicht, wie Leute auswählen: Die Klickregeln gehören dir. Sie leert die Auswahl auch nie von selbst, und die Auswahl überlebt Unmounts, etwa die Zeilen, die eine virtuelle Liste recycelt.
Klicks: Strg/Cmd und Shift
Jedes sortierbare Element und jedes ziehbare Element gibt selected zurück, ein beschreibbares
Ref, das an die Auswahl gebunden ist. Binde es an eine Klasse, eine Checkbox
(v-model="selected") oder setze es in einem Klick-Handler:
<script setup lang="ts">
const { selected } = makeSortableItem(el, { item: () => props.file });
</script>
<template>
<li ref="el" class="file" :class="{ selected }" @click="emit('pick', $event)">…</li>
</template>
Die Liste der Demo bestimmt die Regeln: Ein Klick wählt eine Datei aus, Strg/Cmd-Klick fügt eine hinzu oder entfernt sie, Shift-Klick wählt den Bereich ab der zuletzt geklickten Datei aus:
const { selection } = useDnD();
let anchor: string | null = null;
const pick = (id: string, event: MouseEvent) => {
const ids = items.value.map((file) => file.id);
if (event.shiftKey && anchor) {
const [a, b] = [ids.indexOf(anchor), ids.indexOf(id)];
selection.set(ids.slice(Math.min(a, b), Math.max(a, b) + 1));
return;
}
if (event.ctrlKey || event.metaKey) selection.toggle(id);
else selection.set([id]);
anchor = id;
};
Der Klick, der einen Drag beendet, erreicht @click nicht, also ändert ein Drop die Auswahl nie
versehentlich. Nimm für Aktionen an Elementen @click, nicht pointerup.
Von überall: useDnD().selection
import { useDnD, useDnDMonitor } from '@vue-dnd-kit/core';
const { selection } = useDnD();
selection.has('a'); // is it selected?
selection.add('a', 'b'); // select
selection.delete('a'); // deselect
selection.toggle('a'); // flip it, or force it: toggle('a', true)
selection.set(['a', 'c']); // exactly these
selection.clear();
selection.ids; // a reactive ReadonlySet<Key>
// Clear it after every drop, if your UI wants that:
useDnDMonitor({ onDragEnd: () => selection.clear() });
Die Auswahl ist über IDs geschlüsselt, sie kann also Elemente mehrerer Listen enthalten, und auch einfache ziehbare Elemente: Ein Drag nimmt jedes ausgewählte Element unter dem Provider mit.
Auswahl per Rahmen
makeSelectionArea macht aus einem Element einen Bereich, in dem ein Drücken auf die leere
Fläche einen Rahmen zieht, und die Elemente, die er kreuzt, werden ausgewählt. Zieh auf diesem
Desktop einen Rahmen um ein paar Icons und ziehe dann eines davon:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Desktop from './Desktop.vue';
</script>
<template>
<!-- Every selected icon gets a clone at pickup: the overlay shows them all. -->
<DnDProvider overlay-previews="all">
<Desktop />
</DnDProvider>
</template><script setup lang="ts">
const desktop = useTemplateRef<HTMLElement>('desktop');
const { isSelecting, style } = makeSelectionArea(desktop);
</script>
<template>
<!-- The area needs a position: the band is placed inside it. -->
<div ref="desktop" class="desktop">
<DesktopIcon v-for="icon in icons" :key="icon.id" :icon="icon" />
<div v-if="isSelecting" class="band" :style="style" />
</div>
</template>
<style scoped>
.desktop { position: relative; height: 236px; }
.band { border: 1px solid #42b883; background: color-mix(in srgb, #42b883 15%, transparent); }
</style>
- Den Rahmen renderst du:
styleplatziert ihn, relativ zum Inhalt des Bereichs. - Der Bereich braucht eine nicht statische Position (
relativegenügt), sonst landet der Rahmen woanders. - Ein Rahmen startet nur im Bereich selbst, mach den Bereich also groß: das ganze Panel, mit Platz um die Elemente, keinen eng anliegenden Wrapper.
- Ein Drücken auf ein Element zieht es. Buttons, Felder und die Scrollbar des Bereichs behalten ihr Drücken.
- Der Rahmen bleibt am Inhalt verankert, während der Bereich oder die Seite scrollt. Escape bricht ihn ab und stellt die Auswahl wieder her.
useDnD().statusist'selecting', während ein Rahmen gezogen wird: Die Zeile unter dem Desktop liest es.
| Option | Was sie tut |
|---|---|
strategy | 'toggle' (Standard): Die Elemente, die der Rahmen kreuzt, kehren ihren Zustand um. 'select': Nur die Elemente im Rahmen sind am Ende ausgewählt, ein Klick auf die leere Fläche hebt also die Auswahl der Elemente des Bereichs auf. |
modifier | Tasten, die zum Zeichnen gehalten werden müssen: { keys: ['Shift'], method: 'every' } (oder 'some'). |
accept | Welche ziehbaren Elemente der Rahmen auswählt, eine accept-Regel, die mit ihrem type abgeglichen wird. |
disabled | Kein Rahmen; Einschalten bricht einen laufenden ab. |
onSelected(ids) | Wird aufgerufen, wenn ein Rahmen endet, mit der Auswahl. |
strategy, modifier und disabled nehmen einen Wert, ein Ref oder einen Getter, gelesen, wenn | |
| ein Drücken einen Rahmen startet. Bei Bereichen in Bereichen nimmt der innerste das Drücken. Auf | |
| der Seite der makeSelectionArea-API kannst du sie live umschalten. |
Sie so bewegen, wie sie liegen
Die Icons sind einfache ziehbare Elemente (makeDraggable), jedes mit seinem Icon als data.
Ihr Overlay ist eine eigene Komponente der Demo: Sie legt eine DragPreview jedes gezogenen
Icons so aus, wie die Icons auf dem Desktop liegen, rund um das gegriffene. Die Icons warten an
ihrem Platz, abgeblendet über isDragging, das true bleibt, bis ihre Kopien gelandet sind.
<script setup lang="ts">
const props = defineProps<{ items: DragItem[] }>();
const placed = computed(() => {
const grabbed = props.items[0]!.data as Icon;
return props.items.map(({ id, data }) => {
const icon = data as Icon;
return { id, left: `${icon.x - grabbed.x}px`, top: `${icon.y - grabbed.y}px` };
});
});
</script>
<template>
<div class="formation">
<DragPreview v-for="{ id, left, top } in placed" :id="id" :key="id" class="copy" :style="{ left, top }" />
</div>
</template>
const { isDragging, selected } = makeDraggable(el, { id, data: () => props.icon, overlay: Formation });
Der Desktop nimmt den Drop an. Seine position ist die Stelle, an der das gegriffene Icon
losgelassen wurde, in den Koordinaten des Desktops: Das Icon kommt dorthin, und die anderen
verschieben sich um denselben Betrag. Jede Kopie landet dann auf ihrem eigenen Icon an dessen
neuem Platz. Außerhalb des Desktops abgelegt, fliegen die Kopien zurück zu den Icons.
makeDroppable(desktop, {
id: 'desktop',
onDrop: ({ active, position }) => {
const grabbed = icons.value.find((icon) => icon.id === active.id)!;
const shift = { x: position!.x - grabbed.x, y: position!.y - grabbed.y };
for (const icon of icons.value) {
if (!active.ids.includes(icon.id)) continue;
icon.x += shift.x; // the demo also keeps each icon on the desktop
icon.y += shift.y;
}
},
});
Standardmäßig wird beim Aufnehmen für höchstens 5 Elemente ein Klon angelegt:
overlay-previews="all" des Providers legt einen für jedes ausgewählte Icon an.
In einen Ordner
In diesem Fenster wählt der Rahmen Dateien aus, und Ordner nehmen sie an. Das Overlay ist das Standard-Overlay: ein Stapel der gewählten Dateien. In einen Ordner abgelegt, gehen sie eine nach der anderen hinein, die gegriffene zuletzt, und der Ordner schluckt sie. Bei einem Abbruch fliegt jede zu ihrer Datei zurück. Klicke auf einen Ordner, um ihn zu öffnen, und ziehe Dateien zurück auf Home im Pfad.
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Finder from './Finder.vue';
</script>
<template>
<!-- Every picked file gets a clone at pickup: they all go into the folder. -->
<DnDProvider overlay-previews="all">
<Finder />
</DnDProvider>
</template><!-- Every picked file gets a clone at pickup: they all go in. -->
<DnDProvider overlay-previews="all">
<Finder />
</DnDProvider>
makeDroppable(el, {
id: () => props.folder.id,
accept: 'file',
onDrop: (event) => emit('move', event.active.ids),
});
// The files are in: the folder gulps.
useDnDMonitor({
onDragSettle: (event) => {
if ('dropped' in event && event.over?.id === props.folder.id) gulps.value++;
},
});
Mehr braucht es nicht: Die Dateien verlassen mit dem Drop das Fenster, haben also keinen anderen
Ort zum Landen als den Ordner, und die Standard-Drop-Animation schickt einen Multi-Drag Element
für Element in eine Zone. overlayPreviews legt fest, wie viele Elemente beim Aufnehmen einen
Klon bekommen (standardmäßig 5). Jeder Klon fliegt: in die Zone, zu seinem Slot oder zurück zu
seinem Element.
Der Ordner ist ein Button: Ein Rahmen startet nie auf Buttons und Feldern, ein Klick öffnet ihn also.
Wie sich die anderen Elemente bewegen: multiple
Die Liste, in der ein Multi-Drag startet, entscheidet, was die anderen ausgewählten Elemente tun, solange er läuft. Schalte es in der ersten Demo dieser Seite um:
makeSortableList(listRef, { items: files, multiple: 'gaps' });
multiple | Beim Ziehen | Das Overlay |
|---|---|---|
'group' (Standard) | Die anderen Elemente verlassen ihre Plätze und reisen mit dem gegriffenen als ein Block von Slots, die Liste zeigt also schon das Ergebnis des Drops. | Eine Spalte der gezogenen Elemente entlang der Liste (bis zu overlayPreviews). |
'gaps' | Die anderen bleiben bis zum Drop, wo sie sind, markiert mit data-dnd-dragging. | Dieselbe Spalte. |
'stack' | Die anderen fliegen ins Overlay. | Das gegriffene Element mit einer Anzahl. |
Beim Drop landen die Elemente gemeinsam am Zielindex, in der Reihenfolge, die sie hatten. In
'group' und 'gaps' fliegt jedes Element des Overlays zu seinem eigenen Slot. Die Option wird
beim Start eines Drags gelesen, ein Ref oder ein Getter schaltet sie also um.
Über einem Drop-Ziel, das keine Liste ist, etwa einem Papierkorb oder einer Playlist, oder über
einer Liste, die den Drag ablehnt, löst sich ein 'group'-Block auf: Jedes Element erscheint
wieder an seinem eigenen Platz, so wie ein Drop dort es hinterlässt, und der Block bildet sich
neu, sobald der Zeiger über einer Liste ist.
Gestalte die Elemente, die in 'gaps' zurückbleiben, standardmäßig abgeblendet:
.file[data-dnd-dragging] {
opacity: 0.35;
}
Das Standard-Overlay
In 'group' und 'gaps' ist das Overlay eine Spalte der gezogenen Elemente, so wie sie landen
werden: das gegriffene unter dem Zeiger, die anderen davor und danach in Drop-Reihenfolge (eine
Zeile in horizontalen Listen). Es zeigt so viele, wie overlayPreviews des Providers vorgibt
(standardmäßig 5, 'all' für jedes gerenderte). Die darüber hinaus werden dort gezählt, wo sie
sind: +N am Anfang der Spalte für die Elemente, die vor den gezeigten landen,
+N am Ende für die danach.
In Rastern würde eine Spalte aus ganzen Zellen die Seite verdecken, also ist das Overlay ein Stapel: bis zu drei Elemente hinter dem gegriffenen aufgefächert, mit der Gesamtzahl obendrauf. Schalte die Demo auf Grid, um ihn zu sehen.
Einfache ziehbare Elemente (makeDraggable) bilden ebenfalls einen Stapel, wie die Dateien
oben. Jeder Klon im Overlay landet für sich: auf seinem Slot nach einem Drop in eine Liste, auf
seinem Element, wo auch immer der Drop es hinterlassen hat (zurück an seinem Platz nach einem
Abbruch), und in der Zone, in die ein Drop geht, wenn die Elemente von der Seite verschwunden
sind, einer nach dem anderen, die Box zuletzt.
Nur einige annehmen
Das onBeforeDrop einer Liste kann einen Teil eines Multi-Drags annehmen: Gib die Schlüssel
zurück, die sie annimmt, und die anderen fliegen dorthin zurück, woher sie kamen. Es darf
asynchron sein und nachfragen: Das Overlay wartet dort, wo es losgelassen wurde, bis die Antwort
kommt. Hier will Scheduled Posts mit Titelbild. Ist beim Drop einer ohne dabei, fragt die Liste,
ob die fehlenden Titelbilder ergänzt und alle angenommen werden sollen, nur die fertigen Posts
oder keiner:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Blog from './Blog.vue';
</script>
<template>
<DnDProvider>
<Blog />
</DnDProvider>
</template>const { items } = makeSortableList(list, {
id: props.id,
items: posts,
group: 'blog',
onBeforeDrop: async (event) => {
const missing = event.items.filter((post) => post.cover === null);
if (!props.ask || event.sameList || !missing.length) return true;
const choice = await props.ask(missing, event.items.length - missing.length); // the question under the lists
if (choice === 'cover') missing.forEach((post) => (post.cover = pickCover()));
if (choice === 'ready') return event.keys.filter((_, index) => event.items[index]!.cover !== null);
return choice === 'cover'; // all of them, or none: they fly back
},
});
event.keyssind die gezogenen Schlüssel, undevent.items[i]ist das Element vonevent.keys[i].- Nach der Antwort bekommt
onDropacceptedundrejected, undevent.resultenthält die Listen so, wie der teilweise Drop sie hinterlässt. - Solange der Drop wartet, zeigen die Listen jeden gezogenen Post weiter dort, wo er landen
würde: Die Antwort entscheidet. Für eine Regel, die keine Frage braucht, entscheidet
group.putschon beim Darüberziehen (siehe Gemischte Typen unten). Mehr unter Über einen Drop entscheiden.
Ein Limit
Auf diesem Board haben In progress und Review ein Limit für Karten. Eine volle Spalte nimmt nichts
an: Ihr group.put lehnt ab, und der Stapel wird über ihr rot. Eine Spalte mit Platz für einige
der gewählten Karten nimmt so viele an, wie passen, und die anderen fliegen zurück. Das Overlay
der Spalte sagt das schon vor dem Drop.
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>const room = (keys: Key[]) => limit - column.cards.filter((card) => !keys.includes(card.id)).length;
makeSortableList(list, {
items: cards,
group: { name: 'board', put: ({ keys, from, to }) => from === to || room(keys) > 0 },
multiple: 'stack',
overlay: CardPile, // red where `accepted` is false
overOverlay: fit,
onBeforeDrop: (event) => {
if (event.sameList) return true;
const free = room(event.keys);
return free >= event.keys.length || event.keys.slice(0, free);
},
});
fit ist eine Funktion, die Vue als funktionale Komponente nimmt. Sie reicht FitBadge, was
nur die Spalte weiß, ihren freien Platz, und gibt den Stapel als Slot des Badges weiter.
const fit: FunctionalComponent<OverlayProps> = (overlay, { slots }) =>
h(FitBadge, { count: overlay.items.length, room: room(overlay.items.map((item) => item.id)), limit }, slots);
Gemischte Typen
Eine Liste kann einige Elemente auch schon vor dem Drop anhand ihres Typs ablehnen. Mit
acceptMode: 'some' wird group.put für jedes Element einzeln gefragt: Die Liste nimmt den Drag
an, wenn mindestens ein Element passt, und der Rest fliegt beim Drop zurück. Mit 'all' (dem
Standard) muss jedes Element passen.
makeSortableList(gallery, {
items: photos,
group: { name: 'files', put: ({ types }) => types.includes('image') },
acceptMode: 'some',
});
Einfache Drop-Ziele nehmen ebenfalls acceptMode. Siehe
Listen mit gemeinsamen Elementen und
Drag and Drop.
Ein eigenes Overlay für mehrere Elemente
Overlay-Komponenten und der #overlay-Slot des Providers bekommen items: jedes gezogene
Element, das gegriffene zuerst. Zeig eine Zusammenfassung statt der Elemente selbst: hier eine
Anzahl, den ersten Betreff und was der Drop tun wird.
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Mailbox from './Mailbox.vue';
</script>
<template>
<DnDProvider>
<Mailbox />
</DnDProvider>
</template><script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null }>();
const first = computed(() => props.items[0]?.item as Mail);
const count = computed(() => props.items.length);
// What the drop does, from the `data` of the zone under it.
const zone = computed(() => props.over?.data as { verb: string; danger?: boolean } | undefined);
const action = computed(() => {
const noun = count.value === 1 ? 'conversation' : 'conversations';
return `${zone.value?.verb ?? 'Move'} ${count.value} ${noun}`;
});
</script>
<template>
<!-- The box keeps the row's size; the summary hangs off its middle, by the pointer. -->
<div class="box">
<div class="summary" :class="{ stack: count > 1, danger: zone?.danger }">
<span class="count">{{ count }}</span>
<strong>{{ action }}</strong>
<small>{{ first.subject }}{{ count > 1 ? ` and ${count - 1} more` : '' }}</small>
</div>
</div>
</template>
<style scoped>
.box { position: relative; width: 100%; height: 100%; }
.summary { position: absolute; top: calc(50% + 10px); left: calc(50% + 10px); width: 230px; }
</style>
// The box is centered on the pointer, so the summary hangs off the pointer and leaves
// the zone under it in sight.
const { items } = makeSortableList(list, { items: mails, overlay: MailOverlay, modifiers: [snapCenterToCursor] });
// A zone takes the mails out of the inbox; its `data` tells the overlay what it does.
makeDroppable(archive, {
id: 'Archive',
data: { verb: 'Archive' },
onDrop: (event) => {
event.source?.remove();
selection.clear();
},
});
Um stattdessen die echten Elemente anzuordnen, rendere ihre Klone mit DragPreview, wie es der
Desktop oben tut; der
Overlay-Leitfaden fächert einen
Bücherstapel auf. Klone werden beim Aufnehmen für das gegriffene Element und die anderen daneben
in Drop-Reihenfolge angelegt, so viele wie overlayPreviews; für jedes andere Element rendert
DragPreview seinen Standard-Slot. Beim Drop landet jede Vorschau auf ihrem eigenen Element.
Bäume
In einem Baum bewegen sich die ausgewählten Knoten als ein Block, jeder mit seinem Teilbaum, in Dokumentreihenfolge. Ein ausgewählter Knoten innerhalb eines anderen ausgewählten fährt im Teilbaum seines Elternknotens mit. Siehe Bäume.