Dokumentation/Leitfäden
Sortierbare Listen
Listen, Tab-Leisten und Raster, die sich beim Ziehen neu ordnen – makeSortableList rendert einen Entwurf, makeSortableItem macht die Zeilen, und der Drop schreibt die neue Reihenfolge in dein Ref. Handles, eine Drop-Linie, Tauschen.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Demo wird geladen…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import LayersPanel from './LayersPanel.vue';
// Front to back, as a design tool lists them.
const layers = ref([
{ id: 'title', name: 'Title', kind: 'text' as const, visible: true, locked: false },
{ id: 'badge', name: 'Badge', kind: 'shape' as const, visible: true, locked: false },
{ id: 'photo', name: 'Photo', kind: 'image' as const, visible: true, locked: false },
{ id: 'card', name: 'Card', kind: 'shape' as const, visible: true, locked: true },
]);
</script>
<template>
<DnDProvider>
<div class="editor">
<!-- The artboard reads `layers`: it restacks when a drop writes the new order. -->
<div class="artboard" aria-hidden="true">
<template v-for="(layer, index) in layers" :key="layer.id">
<div v-show="layer.visible" class="shape" :class="layer.id" :style="{ zIndex: layers.length - index }">
<template v-if="layer.id === 'title'">Summer sale</template>
<template v-if="layer.id === 'badge'">-30%</template>
</div>
</template>
</div>
<LayersPanel v-model="layers" />
</div>
</DnDProvider>
</template>
<style scoped>
.editor {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 24px;
}
.artboard {
position: relative;
overflow: hidden;
width: 240px;
height: 170px;
border-radius: 4px;
background: #fff;
box-shadow: 0 10px 30px -12px rgb(0 0 0 / 0.5);
}
.shape {
position: absolute;
}
.card {
inset: 18px;
border-radius: 12px;
background: #1e3a8a;
}
.photo {
top: 34px;
left: 66px;
width: 120px;
height: 84px;
border-radius: 8px;
background:
radial-gradient(circle at 75% 30%, #fde68a 0 12px, transparent 13px),
linear-gradient(170deg, #67e8f9, #0e7490);
}
.badge {
top: 20px;
left: 160px;
display: grid;
place-items: center;
width: 52px;
height: 52px;
border-radius: 50%;
background: #facc15;
color: #1e3a8a;
font: 800 13px/1 system-ui, sans-serif;
}
.title {
top: 104px;
left: 30px;
color: #fff;
font: 800 24px/1 system-ui, sans-serif;
letter-spacing: -0.02em;
text-shadow: 0 2px 6px rgb(0 0 0 / 0.35);
}
</style>Eine Liste und ihre Zeilen
Eine sortierbare Liste braucht zwei Komponenten: die Liste und ihre Zeile.
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import LayerRow from './LayerRow.vue';
const layers = defineModel<Layer[]>({ required: true });
const list = useTemplateRef<HTMLElement>('list');
const { items } = makeSortableList(list, { items: layers });
</script>
<template>
<ul ref="list">
<LayerRow v-for="layer in items" :key="layer.id" :layer="layer" />
</ul>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeSortableItem } from '@vue-dnd-kit/core';
const props = defineProps<{ layer: Layer }>();
const el = useTemplateRef<HTMLElement>('el');
makeSortableItem(el, { item: () => props.layer });
</script>
<template>
<li ref="el">{{ layer.name }}</li>
</template>
Die Zeile registriert sich bei der Liste, in der sie gerendert wird. Eine Komponente kann mehrere Listen anlegen (zwei Spalten, eine Liste in einer Liste): Jede Zeile gehört zu der, deren Element sie enthält, bei Listen in Listen zur innersten.
Der Entwurf
makeSortableList gibt items zurück, einen Entwurf deines Arrays. Während du ziehst,
ordnet sich der Entwurf live neu: Die gezogene Zeile nimmt ihren neuen Slot ein, und die
anderen gleiten aus dem Weg. Dein Array ändert sich bis zum Drop nicht. Dann schreibt die
Bibliothek die neue Reihenfolge hinein.
Das Ebenen-Panel oben zeigt beides: Das Panel rendert items und bewegt sich, während du
ziehst; die Zeichenfläche liest layers, das Array selbst, und stapelt sich erst um, wenn du
loslässt.
- Rendere
items, nicht dein Array. Renderst du dein Array, bewegt sich bis zum Drop nichts. - Das Schreiben passiert nach dem
onDropder Liste. InonDropenthält dein Array noch die alte Reihenfolge unde.resultdie neue. layersist ein normales Ref (hier eindefineModel, und das Schreiben emittiertupdate:modelValue), der Rest deiner App sieht die Änderung also wie jede andere.
makeSortableList(list, {
items: layers,
onDrop: (e) => {
console.log(e.from.index, '→', e.to.index, e.result.get(e.to.list));
},
});
computed
statt eines Refs. In eine schreibgeschützte Quelle schreibt die Bibliothek nicht: Ruf
e.preventDefault() auf und wende e.result selbst an. Siehe
kontrollierte Listen.Schlüssel
Zeilen werden über ihre Daten identifiziert, nie über den Index oder eine automatische ID.
Standardmäßig ist der Schlüssel item.id, bei Strings und Zahlen das Element selbst.
// LayersPanel.vue: your items have another key
makeSortableList(list, { items: layers, getKey: (layer) => layer.uuid });
// LayerRow.vue: `item` is keyed with the list's getKey…
makeSortableItem(el, { item: () => props.layer });
// …or pass the key yourself
makeSortableItem(el, { id: () => props.layer.uuid });
- Nimm denselben Schlüssel im
v-for::key="layer.uuid". Mit:key="index"animieren die falschen Zeilen, und ihr Zustand springt zwischen den Zeilen. - Schlüssel müssen über alle Listen hinweg eindeutig sein, die Elemente austauschen.
- Übergib
itemundidals Getter, damit eine Zeile synchron bleibt, wenn sich ihre Props ändern.
Handles
Standardmäßig startet die ganze Zeile einen Drag, außer den Buttons, Eingabefeldern und anderen
Bedienelementen darin: Das Auge und das Schloss einer Ebene bleiben einfache Buttons. Übergib
handle, um nur an einem Teil zu ziehen:
<script setup lang="ts">
makeSortableItem(el, { item: () => props.layer, handle: '.grip' });
</script>
<template>
<li ref="el">
<button type="button" class="grip" :aria-label="`Move ${layer.name}`">⠿</button>
{{ layer.name }}
</li>
</template>
handleist ein Selektor innerhalb des Elements oder ein Element-Ref.- Ein Maus-Drag startet am Handle sofort, ohne die üblichen 4 px. Ein Druck ohne Bewegung
bleibt ein Klick: Das Handle kann bei
clickein Menü öffnen. - Das Handle bekommt die Attribute des Drags (
role,tabindex,aria-*), und Drags per Tastatur starten an ihm.
Mach das Handle zu einem <button type="button"> mit einem aria-label: Es ist von sich aus
fokussierbar, und Screenreader sagen sein Label an. Ein nacktes <span> mit einem Icon
funktioniert auch (die Bibliothek gibt ihm tabindex="0" und role="button"), hat aber keinen
aussagekräftigen Namen.
touch-action: none und dem Element
activation: { touch: { distance: 4 } }.Strategien
strategy sagt, wie die Liste angeordnet ist, damit die Zeilen richtig Platz machen:
strategy | Für |
|---|---|
'vertical' (Standard) | Eine Spalte aus Zeilen. |
'horizontal' | Eine einzelne Zeile: Tabs, Chips, eine Zeitleiste. |
'grid' | CSS-Grids und umbrechende Flex-Zeilen, auch mit Elementen unterschiedlicher Größe. |
Eine Reihe von Tabs ist eine horizontale Liste:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Browser from './Browser.vue';
</script>
<template>
<DnDProvider>
<Browser />
</DnDProvider>
</template>const { items } = makeSortableList(strip, {
items: tabs,
strategy: 'horizontal',
axis: 'x', // the tabs stay on their row
animateChanges: true, // the buttons open and close tabs: slide the others then too
});
Der Schließen-Button des Tabs funktioniert im ziehbaren Tab weiter, und ein Klick, der einen
Drag beendet, wählt keinen Tab aus. Zeilen gleiten beim Ziehen von selbst; animateChanges ergänzt
dasselbe für Änderungen, die kein Drag gemacht hat, hier die der Buttons (siehe
Animationen).
Ein Raster sortiert in zwei Dimensionen, mit Kacheln jeder Größe:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import PhotoGrid from './PhotoGrid.vue';
</script>
<template>
<DnDProvider>
<PhotoGrid />
</DnDProvider>
</template>const { items } = makeSortableList(grid, { items: photos, strategy: 'grid' });
.grid { display: grid; grid-template-columns: repeat(4, 76px); grid-auto-rows: 76px; grid-auto-flow: dense; }
.wide { grid-column: span 2; }
.tall { grid-row: span 2; }
grid-auto-flow: dense hält das Raster dicht gepackt, wenn sich eine große Kachel bewegt. Ohne
das lässt das Raster Löcher, wo eine Kachel nicht mehr passt, und wächst um eine Zeile.
Die Achse sperren
makeSortableList(list, { items: layers, axis: 'y' });
axis: 'y' hält den Drag auf der vertikalen Achse, 'x' auf der horizontalen: Die Ebenen und
die Tabs oben nutzen das. Die eigene axis eines Elements gewinnt gegen die seiner Liste. Um
den Drag zusätzlich in der Liste zu halten, füge den Modifier restrictToParent hinzu. Mehr
unter Achsen, Grenzen und Raster.
Den Slot stylen
Die gezogene Zeile bleibt als der Slot, in dem sie landen wird, in der Liste und bekommt
data-dnd-placeholder. Der Standardstil blendet sie auf 40 % ab, mit Spezifität null, also
gewinnt jede deiner Regeln:
.layer[data-dnd-placeholder] {
opacity: 1;
box-shadow: inset 0 0 0 1.5px #42b883;
}
.layer[data-dnd-placeholder] > * {
visibility: hidden; /* an empty slot of the row's exact size */
}
makeSortableItem gibt denselben Zustand als Refs zurück, plus ein paar weitere:
| Rückgabe | Was es ist |
|---|---|
isPlaceholder | Diese Zeile ist der Slot, in dem ein gezogenes Element landen wird. |
isDragging | Diese Zeile wird gezogen (bei einem Multi-Drag jede gezogene Zeile). |
index | Ihr Index im Entwurf: Er ändert sich während des Ziehens. |
selected | Beschreibbar, an die Auswahl des Providers gebunden. |
indicator | Im Indikator-Modus, wo die Linie hinkommt. |
Die Liste gibt isOver und isAccepting für ihre eigene Hervorhebung zurück, außerdem
activeKey und activeIndex, das gezogene Element und seinen Platz im Entwurf dieser Liste.
Jeder Zustand, mit Live-Demos, steht unter Styling.
Dinge abschalten
| Option | Was sie stoppt |
|---|---|
disabled an einem Element | Dieses Element lässt sich nicht ziehen. Die anderen bewegen sich trotzdem an ihm vorbei: Sperre oben eine Ebene und ziehe dann eine andere darüber. |
disabled an der Liste | Keine Drags heraus, keine Drops hinein. |
sort: false an der Liste | Ihre Elemente lassen sich darin nicht neu ordnen, können sie aber verlassen: eine Palette, siehe Listen mit gemeinsamen Elementen. |
Alle nehmen einen Getter oder ein Ref und gelten ab dem nächsten Drag:
makeSortableItem(el, { item: () => props.layer, handle: '.grip', disabled: () => props.layer.locked });
Eine Zeile, die immer erste oder letzte bleiben muss, egal was die anderen tun, ist gar kein
Element: Rendere sie außerhalb der Liste. Die Ebene „Background“ des Panels sitzt unterhalb
des <ul>.
Nur einen Teil der Elemente zeigen
Ein Suchfeld, ein Tag: filter zeigt die Elemente, für die es true zurückgibt, und ein Drag
sortiert unter diesen. Die anderen bleiben in deinem Array, an ihren Plätzen:
Demo wird geladen…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Backlog from './Backlog.vue';
import type { Task } from './TaskRow.vue';
// In priority order: the array is the backlog.
const tasks = ref<Task[]>([
{ id: 1, title: 'Login fails on Safari', tag: 'bug' },
{ id: 2, title: 'Dark mode for settings', tag: 'feature' },
{ id: 3, title: 'Document the export API', tag: 'docs' },
{ id: 4, title: 'Crash when the list is empty', tag: 'bug' },
{ id: 5, title: 'Keyboard shortcuts', tag: 'feature' },
{ id: 6, title: 'Typo in the pricing page', tag: 'docs' },
{ id: 7, title: 'Slow search with 10k rows', tag: 'bug' },
{ id: 8, title: 'Share a board by link', tag: 'feature' },
]);
</script>
<template>
<DnDProvider>
<Backlog v-model="tasks" />
</DnDProvider>
</template>const query = ref('');
const tag = ref<Tag | null>(null);
const { items } = makeSortableList(list, {
items: tasks,
filter: (task) => (!tag.value || task.tag === tag.value) && task.title.includes(query.value),
});
itemsenthält die passenden Elemente: Rendere es wie gewohnt. Der Filter ist reaktiv: Tippen im Suchfeld aktualisiert die Liste sofort.- Der Drop schreibt das ganze Array. Jedes ausgeblendete Element bleibt direkt hinter dem gezeigten Element, dem es folgte; ist genau dieses das bewegte, bleibt es hinter dem nächsten davor, das sich nicht bewegt hat.
e.to.indexundactiveIndexder Liste zählen die gezeigten Elemente.e.resultenthält das ganze Array, die ausgeblendeten Elemente eingeschlossen.
computed, das dein Array filtert und als items übergeben wird, funktioniert auch, ist
aber nur lesbar: Du müsstest e.result selbst anwenden und selbst ausrechnen, wohin die
ausgeblendeten Elemente gehören. filter erledigt beides.Eine Einfügelinie
Mit placeholder: 'indicator' bleibt die Liste beim Ziehen still, und eine Linie zeigt, wohin
der Drop geht. Die Linie zeichnest du:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import IssueTable from './IssueTable.vue';
</script>
<template>
<DnDProvider>
<IssueTable />
</DnDProvider>
</template>const { items, indicator } = makeSortableList(body, { items: issues, placeholder: 'indicator' });
<script setup lang="ts">
const { indicator } = makeSortableItem(el, { item: () => props.issue });
</script>
<template>
<div ref="el" class="row" :class="indicator && `drop-${indicator.side}`">…</div>
</template>
<style scoped>
.row.drop-before::before { content: ''; position: absolute; top: -1.5px; left: 6px; right: 6px; height: 2px; background: #42b883; }
.row.drop-after::before { content: ''; position: absolute; bottom: -1.5px; left: 6px; right: 6px; height: 2px; background: #42b883; }
</style>
Feld von indicator | Was es enthält |
|---|---|
list | Die Liste, in die der Drop geht. |
index | Der Einfügeindex in der Liste, wie sie gerendert ist. |
key | Das Element, an dem die Linie hängt; null in einer leeren Liste (zeichne sie dann in der Liste). |
side | 'before' oder 'after' dieses Elements; 'swap' in einer Liste, die tauscht. Bäume ergänzen 'inside'. |
depth, parentKey | Nur Bäume: wo der Drop landet. Siehe Bäume. |
- Das
indicatorder Liste ist gesetzt, solange die Linie in dieser Liste ist (die Tabelle gibt es aus), und dasindicatoreines Elements, solange die Linie an diesem Element hängt. - Die gezogene Zeile bleibt, wo sie war, als Platzhalter markiert: Blende sie ab.
- Die Tastatur bewegt die Linie ebenfalls.
- Ein Drop außerhalb aller Ziele bricht in diesem Modus ab (
dropOutside: 'cancel'), da die Liste nie eine andere Reihenfolge gezeigt hat. - Das Overlay verdeckt einen Teil der Zeile darunter. Mach es ein wenig durchsichtig, dann
scheint die Linie durch:
[data-dnd-overlay] .row { opacity: 0.85 }.
Tauschen statt verschieben
Mit behavior: 'swap' tauscht das gezogene Element den Platz mit dem darunter, und die anderen
bleiben, wo sie sind:
Demo wird geladen…
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Rotation from './Rotation.vue';
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
const columns = reactive([
{
id: 'primary',
title: 'Primary',
people: [
{ id: 'ava', name: 'Ava', hue: 160 },
{ id: 'ben', name: 'Ben', hue: 20 },
{ id: 'cleo', name: 'Cleo', hue: 280 },
{ id: 'dev', name: 'Dev', hue: 210 },
{ id: 'eli', name: 'Eli', hue: 45 },
],
},
{
id: 'backup',
title: 'Backup',
people: [
{ id: 'fay', name: 'Fay', hue: 330 },
{ id: 'gus', name: 'Gus', hue: 100 },
{ id: 'hana', name: 'Hana', hue: 190 },
{ id: 'ivo', name: 'Ivo', hue: 0 },
{ id: 'jun', name: 'Jun', hue: 250 },
],
},
]);
/** Live: the swap shows while you drag. On drop: nothing moves, the partner is marked. */
const placeholder = ref<'item' | 'indicator'>('item');
const last = ref('');
</script>
<template>
<DnDProvider>
<div class="schedule">
<div class="modes" role="group" aria-label="Swap preview">
<button type="button" :aria-pressed="placeholder === 'item'" @click="placeholder = 'item'">Live</button>
<button type="button" :aria-pressed="placeholder === 'indicator'" @click="placeholder = 'indicator'">On drop</button>
</div>
<div class="week">
<ol class="days">
<li v-for="day in DAYS" :key="day">{{ day }}</li>
</ol>
<Rotation v-for="column in columns" :key="column.id" :column="column" :placeholder="placeholder" @swap="last = $event" />
</div>
<p class="last">{{ last ? `Swapped ${last}` : 'Drag a person onto another' }}</p>
</div>
</DnDProvider>
</template>
<style scoped>
.schedule {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.modes {
display: flex;
padding: 3px;
border-radius: 10px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
.modes button {
padding: 6px 12px;
border: 0;
border-radius: 7px;
background: none;
color: inherit;
font: inherit;
font-size: 12px;
font-weight: 500;
opacity: 0.6;
cursor: pointer;
}
.modes button[aria-pressed='true'] {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
opacity: 1;
}
.week {
display: flex;
gap: 10px;
width: 400px;
max-width: 100%;
}
.days {
display: flex;
flex-direction: column;
gap: 6px;
margin: 21px 0 0;
padding: 0;
list-style: none;
}
.days li {
height: 36px;
font: 11px/36px ui-monospace, Menlo, Consolas, monospace;
opacity: 0.55;
}
.last {
margin: 0;
font: 12px/1 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.6;
}
</style>const { items } = makeSortableList(list, {
id: props.column.id,
items: toRef(props.column, 'people'),
group: 'rotation',
behavior: 'swap',
placeholder: () => props.placeholder, // 'item' (live) or 'indicator' (on drop)
onDrop: (e) => {
if (e.partner) emit('swap', `${e.items[0]?.name} ⇄ ${e.partner.item.name}`);
},
});
- Live (
placeholder: 'item', Standard): Der Tausch ist beim Ziehen zu sehen. Zurückziehen oder die Liste verlassen macht ihn rückgängig. - Beim Drop (
placeholder: 'indicator'): Bis zum Drop bewegt sich nichts. Der Indikator markiert das Element, mit dem der Drop tauscht,side: 'swap': Umrande dieses Element, statt eine Linie zu zeichnen. Die Pfeiltasten bewegen die Markierung von Element zu Element; zurück auf dem gezogenen Element gibt es keine.
<li ref="el" :class="{ target: indicator?.side === 'swap' }">…</li>
Das Drop-Event hat mode: 'swap' und partner, das Element, mit dem getauscht wurde: seinen
key, item und sein eigenes from und to. Zwei Listen einer Gruppe tauschen untereinander,
wenn beide behavior: 'swap' haben, wie die zwei Spalten. Das überfahrene Element wandert dann
in die andere Liste, es muss also hinaus (group.pull) und hinein (group.put) dürfen: siehe
Listen mit gemeinsamen Elementen. Ein Multi-Drag sortiert, statt zu
tauschen.
Tastatur
Fokussiere eine Zeile oder ihr Handle und drücke Space oder Enter, um sie aufzunehmen. Die Pfeile entlang der Liste bewegen sie um einen Slot, in Rastern gehen hoch und runter in die nächste Zeile, und die Pfeile quer zur Liste bewegen sie in die nächste Liste ihrer Gruppe. Hinter dem Ende einer Liste geht der Pfeil weiter in die nächste Liste in dieser Richtung (Listen übereinander), sonst auf ein Drop-Ziel dort, etwa eine Papierkorb-Zone. Drücke erneut Space oder Enter zum Ablegen, Escape, um sie zurückzustellen. Jede Demo auf dieser Seite funktioniert so. Die Screenreader-Texte stehen unter Barrierefreiheit.
Animationen
Zeilen gleiten beim Ziehen an ihre neuen Plätze, und die abgelegte Zeile landet auf ihrem Slot.
Die Liste selbst wächst oder schrumpft auf ihre neue Größe, wenn Zeilen hinzukommen und gehen
(animateSize). Das ist standardmäßig an und braucht keinen Code. Umschließe Zeilen nicht mit
<TransitionGroup>: Bewegungen würden doppelt animiert. Setz auch keine CSS-transition auf das
transform der Zeilen. Für Änderungen, die kein Drag gemacht hat (Hinzufügen, Entfernen,
Mischen), schalte die Option animateChanges der Liste ein, wie es die Tabs tun. Siehe
Animationen.
Die vollständige Liste der Optionen steht in der API von makeSortableList und makeSortableItem.