Dokumentation/Leitfäden
Listen mit gemeinsamen Elementen
Listen in einer Gruppe verschieben, kopieren und tauschen Elemente – die ganze Spalte nimmt Drops an, pull und put entscheiden, was hinein- und hinausgeht, Paletten kopieren, und kontrollierte Listen wenden jeden Drop auf einen Store mit Rückgängig-Verlauf an.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Demo wird geladen…
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
const columns = reactive([
{
id: 'backlog',
title: 'Backlog',
stories: [
{ id: 's1', title: 'Saved searches', points: 5 },
{ id: 's2', title: 'Crash on empty board', points: 2, bug: true },
{ id: 's3', title: 'CSV export', points: 3 },
{ id: 's4', title: 'Dark mode email', points: 2 },
],
},
{
id: 'sprint',
title: 'Sprint 14',
stories: [{ id: 's5', title: 'Onboarding tour', points: 8 }],
},
]);
const wholeColumn = ref(true);
</script>
<template>
<DnDProvider>
<div class="planning">
<label class="switch">
<input v-model="wholeColumn" type="checkbox" />
<code>dropzone</code>: the whole column takes drops
</label>
<div class="columns">
<Column v-for="column in columns" :key="column.id" :column="column" :whole-column="wholeColumn" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.planning {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.switch {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
cursor: pointer;
}
.switch input {
accent-color: #42b883;
}
.switch code {
font: 12px/1 ui-monospace, Menlo, Consolas, monospace;
color: #42b883;
}
.columns {
display: flex;
justify-content: center;
gap: 14px;
width: 100%;
}
</style>Eine Gruppe
Listen mit demselben group-Namen tauschen Elemente aus. Standardmäßig ist jede Liste eine
eigene Gruppe.
<script setup lang="ts">
import { toRef, useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import StoryCard from './StoryCard.vue';
type Story = { id: string; title: string; points: number };
const props = defineProps<{ column: { id: string; title: string; stories: Story[] } }>();
const list = useTemplateRef<HTMLElement>('list');
const { items, isOver } = makeSortableList(list, {
id: props.column.id, // names the list in drop events
items: toRef(props.column, 'stories'), // a writable ref into the reactive column
group: 'planning', // same name: stories move between these lists
});
</script>
<template>
<section :class="{ over: isOver }">
<h3>{{ column.title }}</h3>
<ul ref="list">
<StoryCard v-for="story in items" :key="story.id" :story="story" />
</ul>
</section>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import Column from './Column.vue';
const columns = reactive([
{ id: 'backlog', title: 'Backlog', stories: [{ id: 's1', title: 'Saved searches', points: 5 }] },
{ id: 'sprint', title: 'Sprint 14', stories: [] },
]);
</script>
<template>
<Column v-for="column in columns" :key="column.id" :column="column" />
</template>
- Jede Spalte ist hier eine Komponente, wie Spalten es meistens sind. Eine Komponente kann auch mehrere Listen anlegen, etwa feste Listen Todo und Done nebeneinander: Jede Karte gehört zu der Liste, deren Element sie enthält.
toRef(props.column, 'stories')ist beschreibbar, weil das Elternelement ein reaktives Objekt übergibt. Der Drop schreibt beide Arrays, das, das die Story verlassen hat, und das, in das sie gekommen ist, und die Punkte beider Spalten ziehen nach.iderscheint in Drop-Events alsfrom.listundto.listund ist der Schlüssel ine.result, den neuen Arrays jeder Liste, die der Drop ändert.idundgroupwerden einmal gelesen.- Schlüssel müssen in der ganzen Gruppe eindeutig sein.
Das onDrop einer Liste läuft für die Drops in sie hinein. Ein Verschieben von backlog
nach sprint ruft das onDrop der Liste sprint auf, und deren e.result enthält die neuen
Arrays beider Listen.
Die ganze Spalte nimmt Drops an: dropzone
Eine kurze Liste in einer hohen Spalte lässt unter der letzten Karte leeren Platz. Mit
dropzone nimmt ein anderes Element die Drops für die Liste an: hier die ganze Spalte. Eine
Story, die unterhalb der letzten losgelassen wird, landet am Ende. Die Storys bleiben im <ul>;
nur der Bereich, der Drops annimmt, wächst. Die Demo umrandet diesen Bereich beim Ziehen:
Schalte ihn ab, und nur die Liste selbst nimmt die Story an.
const zone = useTemplateRef<HTMLElement>('zone');
const { items } = makeSortableList(list, {
items: toRef(props.column, 'stories'),
group: 'planning',
dropzone: zone, // or a getter: () => (wholeColumn.value ? zone.value : undefined)
});
dropzone braucht eine leere Liste trotzdem eine Größe, um Drops anzunehmen: Gib ihr eine
min-height.Was hinein- und hinausgeht: pull und put
group nimmt auch ein Objekt. pull sagt, was eine Liste verlassen darf, put, was
hineindarf:
Demo wird geladen…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type SortablePutContext } from '@vue-dnd-kit/core';
import RuleList from './RuleList.vue';
// Four lists of one group, `files`. What each gives and takes is its `group.pull` and `group.put`.
const shortlist = reactive({
id: 'shortlist',
title: 'Shortlist',
code: 'put: ({ keys }) => shortlist.files.length + keys.length <= 3',
refusal: 'full',
group: { name: 'files', put: ({ keys }: SortablePutContext) => shortlist.files.length + keys.length <= 3 },
files: [
{ id: 'pitch', name: 'pitch.pdf', kind: 'doc' as const },
{ id: 'mock', name: 'mockup.png', kind: 'image' as const },
],
});
const lists = reactive([
{
id: 'inbox',
title: 'Inbox',
code: '{ pull: true, put: false }',
refusal: 'put: false',
group: { name: 'files', pull: true, put: false },
files: [
{ id: 'logo', name: 'logo.svg', kind: 'image' as const },
{ id: 'brief', name: 'brief.pdf', kind: 'doc' as const },
{ id: 'hero', name: 'hero.png', kind: 'image' as const },
],
},
{
id: 'templates',
title: 'Templates',
code: "{ pull: 'clone', put: false }, sort: false",
refusal: 'put: false',
group: { name: 'files', pull: 'clone' as const, put: false },
sort: false,
files: [
{ id: 'invoice', name: 'invoice.doc', kind: 'doc' as const },
{ id: 'banner', name: 'banner.png', kind: 'image' as const },
],
},
{
id: 'images',
title: 'Images',
code: "put: 'image'",
refusal: 'images only',
group: { name: 'files', put: 'image' },
files: [],
},
shortlist,
]);
</script>
<template>
<DnDProvider>
<div class="rules">
<RuleList v-for="list in lists" :key="list.id" :list="list" />
</div>
</DnDProvider>
</template>
<style scoped>
.rules {
display: grid;
grid-template-columns: repeat(2, minmax(0, 260px));
gap: 12px;
justify-content: center;
}
@media (max-width: 480px) {
.rules {
grid-template-columns: minmax(0, 1fr);
}
}
</style>| Option | Werte |
|---|---|
pull | true (Standard): Elemente dürfen hinaus. false: Sie dürfen nicht. 'clone': Kopien gehen hinaus, die Elemente bleiben. |
put | true (Standard): Elemente dürfen hinein. false: Nichts kommt hinein. Ein Typ oder mehrere ('image', ['image', 'video']): Elemente dieser Typen kommen hinein. Eine Funktion: Du entscheidest pro Drag. |
// Inbox: gives, takes nothing.
group: { name: 'files', pull: true, put: false }
// Templates: gives copies, takes nothing, keeps its order.
group: { name: 'files', pull: 'clone', put: false }, sort: false
// Images: takes images only.
group: { name: 'files', put: 'image' }
Typen sind die type-Optionen der gezogenen Elemente (makeSortableItem(el, { item, type }));
Elemente ohne eine zählen als der Name der Gruppe. Mit Typen in put muss jedes gezogene
Element einen davon haben (mit acceptMode: 'some' kommen die hinein, die einen haben).
Eine put-Funktion bekommt { keys, items, types, from, to }: die gezogenen Schlüssel und
Elemente, ihre Typen, die Liste, aus der sie kommen (null bei einfachen ziehbaren Elementen),
und diese Liste. Verschieben innerhalb einer Liste ist immer erlaubt: Die Inbox sortiert ihre
eigenen Dateien trotzdem.
Über einer Liste, die den Drag ablehnt, geht der Slot dorthin zurück, wo der Drag gestartet ist,
damit die Listen zeigen, was ein Drop tun würde: nichts. Ein Drop dort wird abgebrochen, und das
Element fliegt zurück. isAccepting der Liste ist während eines solchen Drags false, und
isOver über ihr trotzdem true: So weiß jede Liste in der Demo, was sie sagen soll.
Auch über einem eigenen Drop-Ziel außerhalb der Listen, etwa einem Papierkorb, geht der Slot
zurück: Der Drop dort gehört dem Ziel, und die Listen behalten das Element, außer sein onDrop
nimmt es heraus (event.source.remove()). Ziele innerhalb der Listen (ein Drop in ein Element)
und um sie herum (ein Panel, das die Listen enthält) lassen den Slot, wo er ist.
const { items, isOver, isAccepting } = makeSortableList(el, { … });
const { active } = useDnD();
const verdict = computed(() => (active.value ? (isAccepting.value ? 'takes' : 'refuses') : null));
Limits
Eine Liste, die höchstens drei Dateien hält, die Shortlist:
const shortlist = reactive({
files: [/* … */],
group: { name: 'files', put: ({ keys }: SortablePutContext) => shortlist.files.length + keys.length <= 3 },
});
Sie liest ihre eigenen Elemente, wenn ein Drag fragt. Füll sie, und sie lehnt die nächste Datei ab, mit dem Grund in ihrer Kopfzeile.
Wenn mehrere ausgewählte Elemente verschiedener Typen gezogen werden, sagt acceptMode, ob
'all' (Standard) oder 'some' von ihnen put bestehen müssen. Mit 'some' landen die, die
bestehen, und die anderen fliegen zurück. Siehe
Multi-Drag und Auswahl.
Eine Gruppe pro Liste
Eine Liste gehört zu einer Gruppe: group nimmt einen Namen, nie mehrere. Die Listen einer
Gruppe teilen sich jeden Drag (ihre Entwürfe, was gezogen wird, welche von ihnen ihn annehmen),
deshalb wechseln Elemente nie zwischen Gruppen. Sollen Elemente nur in manche Richtungen
fließen, steck die Listen in eine Gruppe und lass put nach from entscheiden, der Liste, aus
der ein Drag kommt:
// Review takes tasks from both teams. A team takes tasks back from Review,
// never from the other team.
const fromReview = ({ from }: SortablePutContext) => from === 'review';
makeSortableList(review, { id: 'review', items: reviewTasks, group: 'tasks' });
makeSortableList(frontend, { id: 'frontend', items: frontendTasks, group: { name: 'tasks', put: fromReview } });
makeSortableList(backend, { id: 'backend', items: backendTasks, group: { name: 'tasks', put: fromReview } });
put: ['a', 'b'] die Gruppen, aus denen eine Liste
Elemente nimmt. Hier zählt ein Array in put die Typen der Elemente auf, die eine Liste
annimmt. Um Elemente aus anderen Listen zu nehmen, gib ihnen eine Gruppe und entscheide nach
from, wie oben.Kopieren: Paletten und die Alt-Taste
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Palette from './Palette.vue';
import Sheet from './Sheet.vue';
</script>
<template>
<!-- The palette and the page are lists of one group: copies go from one to the other. -->
<DnDProvider>
<div class="builder">
<Palette />
<Sheet />
</div>
</DnDProvider>
</template>
<style scoped>
.builder {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 24px;
}
</style>makeSortableList(list, {
items: templates,
group: { name: 'blocks', pull: 'clone', put: false }, // copies go out, nothing comes in
sort: false, // its own order stays
clone: (block) => ({ ...block, id: crypto.randomUUID() }),
});
makeSortableList(list, {
items: blocks,
group: 'blocks',
clone: (block) => ({ ...block, id: crypto.randomUUID() }), // for Alt-drags
dropOutside: 'remove', // drag off the Page panel to delete
});
pull: 'clone'kopiert jeden Drag aus der Liste heraus. Das Original bleibt, und die Kopie reist und landet.- Alt beim Ziehen gedrückt halten kopiert ebenfalls, aus jeder Liste mit einer
clone-Funktion: Sie erzeugt die Kopie. Ändere die Taste mitcopyModifierdes Providers oder schalte sie mitnullab. - So oder so braucht die Liste, aus der das Element kommt,
clone. Es muss die Kopie mit einem neuen Schlüssel zurückgeben. Ohneclonewird nichts kopiert. - Das Drop-Event hat
mode: 'copy', unde.itemssind die Kopien. Eine Overlay-Komponente bekommtmodeebenfalls, um beim Kopieren ein „+“ zu zeigen (siehe Das Overlay).
Zwischen Listen tauschen
Zwei Listen einer Gruppe, die beide behavior: 'swap' haben, tauschen Elemente untereinander:
Das gezogene Element nimmt den Platz des überfahrenen ein, und das überfahrene wandert in die
Liste des gezogenen. Es muss seine Liste verlassen (pull: true) und in die andere hinein
(put) dürfen. Der Bereitschaftsplan in
Sortierbare Listen tauscht über seine zwei
Spalten hinweg.
Einfache ziehbare Elemente
Eine Liste nimmt Elemente auf, die mit makeDraggable gezogen werden (ein Chip, eine Vorlage,
eine Datei aus einem Baum), wenn sie createItem hat. group.put entscheidet auch über sie,
mit from: null. Siehe
einfache ziehbare Elemente in eine Liste.
Außerhalb ablegen
Was passiert, wenn ein Drag über gar keinem Ziel endet, bestimmt dropOutside der Liste:
dropOutside | Die Elemente |
|---|---|
'placeholder' (Standard) | Landen dort, wo der Slot zuletzt gezeigt wurde. |
'cancel' | Fliegen zurück. Der Standard bei placeholder: 'indicator'. |
'remove' | Verlassen ihre Liste: zum Löschen hinausziehen. Das Blatt der Paletten-Demo macht das. |
'remove' fragt zuerst das onBeforeDrop der Liste. Dann läuft das onDrop der Liste mit
to.index === -1, und e.result enthält die Liste ohne die Elemente. Ein Papierkorb, der Drops
annimmt, funktioniert genauso: siehe Papierkorb-Zonen.
Kontrollierte Listen
Die Listen können einen Store lesen, der ihnen nicht gehört: Pinia, Serverzustand,
unveränderliche Daten. Übergib items als Getter (oder als computed oder schreibgeschütztes
Ref): Die Bibliothek schreibt nie hinein. Ruf in onDrop e.preventDefault() auf und wende
e.result selbst an:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Table from './Table.vue';
import { createSeating } from './store';
const seating = createSeating();
const { state, undo } = seating;
</script>
<template>
<DnDProvider>
<div class="seating">
<div class="tables">
<Table id="a" title="Table A" :seating="seating" />
<Table id="b" title="Table B" :seating="seating" />
</div>
<section class="history">
<header>
<h4>History</h4>
<button type="button" :disabled="!state.history.length" @click="undo">Undo</button>
</header>
<ol>
<li v-for="(entry, index) in state.history" :key="state.history.length - index">{{ entry.label }}</li>
<li v-if="!state.history.length" class="none">Move a guest</li>
</ol>
</section>
</div>
</DnDProvider>
</template>
<style scoped>
.seating {
display: flex;
flex-direction: column;
gap: 12px;
width: 460px;
max-width: 100%;
margin: 0 auto;
}
.tables {
display: flex;
gap: 12px;
}
.history {
padding: 10px 12px;
border-radius: 12px;
background: color-mix(in srgb, currentColor 5%, transparent);
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
h4 {
margin: 0;
font: 600 11px/1 ui-monospace, Menlo, Consolas, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
button {
padding: 5px 12px;
border: 0;
border-radius: 7px;
background: #42b883;
color: #fff;
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
button:disabled {
background: color-mix(in srgb, currentColor 12%, transparent);
color: inherit;
opacity: 0.5;
cursor: default;
}
ol {
display: flex;
flex-direction: column;
gap: 4px;
height: 66px;
margin: 8px 0 0;
padding: 0;
overflow-y: auto;
list-style: none;
font: 12px/1.4 ui-monospace, Menlo, Consolas, monospace;
}
li:first-child {
color: #42b883;
}
li.none {
color: inherit;
opacity: 0.5;
}
</style>const { items } = makeSortableList(list, {
id: props.id,
items: () => props.seating.state.tables[props.id]!, // read-only
group: 'seating',
onDrop: (e) => {
e.preventDefault(); // no write
props.seating.apply(e.result, `${e.items[0]?.name} → ${props.title}`);
},
});
const apply = (result: ReadonlyMap<Key, Guest[]>, label: string) => {
const before = [...result.keys()].map((id): [Key, Guest[]] => [id, state.tables[id]!]);
for (const [id, guests] of result) state.tables[id] = guests;
state.history.unshift({ label, before });
};
const undo = () => {
const last = state.history.shift();
for (const [id, guests] of last?.before ?? []) state.tables[id] = guests;
};
e.resultordnet jeder Liste, die der Drop ändert, ihr neues Array zu: beide Tische nach einem Verschieben, einen nach einem Umsortieren. Es sind neue Arrays, der Store kann die alten also fürs Rückgängigmachen behalten.- Der Entwurf folgt dem Store: Wende
resultinonDropan (oder bevor ein asynchronesonBeforeDropsich auflöst), und das Overlay landet in der neuen Reihenfolge. preventDefault()überspringt das Schreiben in jede Liste inresult. Wird es nirgends angewendet, bleiben die Listen, wie sie waren, und die Elemente fliegen zurück.- Entwicklungs-Builds warnen, wenn der Drop einer schreibgeschützten Liste der Bibliothek überlassen wird: Es würde sich nichts ändern.
Um vor dem Landen des Drops auf einem Server zu speichern oder zuerst den Nutzer zu fragen, siehe Über einen Drop entscheiden. Alle Optionen der Liste stehen in der makeSortableList-API.