Dokumentation/Leitfäden
Der Provider
DnDProvider ist für jeden Drag darunter zuständig. Er findet das Ziel, startet Drags, scrollt und teilt useDnD und useDnDMonitor alles mit, was passiert.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Ein Provider für die App
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
</script>
<template>
<DnDProvider>
<RouterView />
</DnDProvider>
</template>
Setze einen DnDProvider weit oben ein, in App.vue oder einem Layout, und vergiss ihn. Jedes
Composable (makeSortableList, makeDraggable, useDnD…) sucht den nächstgelegenen Provider
oberhalb der Komponente, in der es läuft.
<DnDProvider> enthält, wird
No <DnDProvider> found above this component geworfen: inject von Vue sieht nur Vorfahren.Wofür er zuständig ist
| Teil | Was der Provider tut |
|---|---|
| Eingabe | Macht aus Zeiger-, Touch- und Tastatureingaben Drags (sensors, activation), immer nur einen gleichzeitig. |
| Das Ziel finden | Prüft per Hit-Test, welche Drop-Ziele unter dem Drag liegen (collision), und teilt es ihnen mit. |
| Das Overlay | Rendert die Box, die dem Zeiger folgt, teleportiert sie nach body und lässt sie beim Drop landen. |
| Die Auswahl | Eine Menge ausgewählter IDs, die alle Listen und ziehbaren Elemente darunter teilen. |
| Auto-Scroll | Scrollt Container und das Fenster, wenn sich ein Drag ihren Rändern nähert. |
| Die Live-Region | Sagt Screenreadern an, wenn etwas aufgenommen, bewegt, abgelegt oder abgebrochen wird. |
Sonst rendert er nichts: Dein Markup bleibt, wie es ist.
Optionen im Überblick
Jede Option hat einen Standardwert, der funktioniert, deshalb braucht der Provider meist keine.
Der Provider liest seine Props dann, wenn er sie braucht: Die Demos unten schalten collision,
activation und autoScroll live um.
| Prop | Standard | Was sie festlegt |
|---|---|---|
collision | pointerFirst | Wie das Ziel unter dem Drag gewählt wird. Unten. |
activation | Maus 4 px, Touch 200 ms | Wann ein Drücken zum Drag wird, je nach Zeigerart. Unten. |
autoScroll | an | Scrollen an den Rändern: { threshold, maxSpeed, acceleration } oder false. Unten. |
haptics | eine kurze Vibration beim Aufnehmen per Touch | Vibrationen für Touch-Drags, oder false. Unten. |
copyModifier | 'Alt' | Taste, die, während des Ziehens gehalten, aus dem Verschieben ein Kopieren macht. |
modifiers | [] | Bewegungsregeln für jeden Drag: siehe Achsen, Grenzen und Raster. |
animation / dropAnimation | an | Elemente, die beim Sortieren gleiten / das Landen des Overlays: siehe Animationen. |
animateChanges | aus | Standard für Listen und Bäume: Datenänderungen animieren, die kein Drag ausgelöst hat. |
keyboard | Space / Enter, Escape | Tasten und Schrittweite für die Tastatur; false schaltet das Ziehen per Tastatur ab. Siehe Barrierefreiheit. |
sensors | Zeiger + Tastatur | Was Eingaben zu Drags macht: siehe Sensoren. |
overlayTo | 'body' | Wohin das Overlay teleportiert wird. |
overlayFit | 'slot' | Größe des Overlays: die des Slots, in dem es landen wird, oder die der Quelle. Siehe Das Overlay. |
overlayTransition | eine Überblendung | Wie das Overlay während des Ziehens von einer Komponente zur anderen wechselt: false oder <Transition>-Props. Siehe Ziele, die das Overlay ändern. |
overlayClass | — | Klassen für den Inhalt des Overlays ([data-dnd-overlay-content]), das Element, das als Ganzes animiert wird: ein Anheben beim Aufnehmen, ein Ablegen beim Landen. Siehe Das Overlay: anheben, neigen, ablegen. |
overlayPreviews | 5 | Multi-Drag: wie viele gezogene Elemente beim Aufnehmen einen Klon bekommen, oder 'all'. Siehe das Standard-Overlay. |
announcements | eingebaut | Texte für Screenreader, oder false. |
screenReaderInstructions | eingebaut | Der Text, auf den jedes ziehbare Element mit aria-describedby verweist. |
defaultStyles | true | Fügt CSS mit Spezifität null für Drag-Zustände ein: siehe Styling. |
Die vollständige Liste mit allen Formen steht in der DnDProvider-API.
Der Provider emittiert außerdem drag-start, drag-move, drag-over, drag-end,
drag-cancel, drag-resume und drag-settle, dieselben Events, die useDnDMonitor hört.
Den Drag lesen: useDnD()
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Tasks from './Tasks.vue';
import StatePanel from './StatePanel.vue';
</script>
<template>
<!-- One provider: the list drags, the panel reads the drag. Neither imports the other. -->
<DnDProvider>
<div class="demo">
<Tasks />
<StatePanel />
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 24px;
}
</style>useDnD() liefert den Drag als reaktiven Zustand, in jeder Komponente unter dem Provider:
import { useDnD } from '@vue-dnd-kit/core';
const { status, active, over, pointer, activation, isAutoScrolling, selection, cancel } = useDnD();
| Feld | Was es enthält |
|---|---|
status | 'idle', 'activating' (ein Drücken wartet auf seine Aktivierung), 'dragging', 'dropping' (kein Drag läuft, aber ein Overlay landet noch, oder ein asynchrones onBeforeDrop entscheidet), 'selecting' (ein Auswahlrahmen). |
active | Was gezogen wird: id, ids (jede gezogene ID, die gegriffene zuerst), items, data, type, element, rect, sensor. null im Ruhezustand. |
over | Das Ziel unter dem Drag: id, data, element, rect und accepted. null über nichts. |
pointer | Der Zeiger in Viewport-Koordinaten, nach den Modifiern. |
activation.progress | 0 bis 1, während ein Drücken auf seine Aktivierung wartet: Zeichne damit einen Ring. |
isAutoScrolling | Auto-Scroll bewegt gerade etwas. |
selection | Die Auswahl des Providers: ids, has, add, delete, toggle, set, clear. Siehe Multi-Drag und Auswahl. |
cancel() | Bricht den laufenden Drag ab (die Elemente fliegen zurück), ein Drücken, das sich gerade auflädt, oder einen Auswahlrahmen. |
<script setup lang="ts">
import { computed } from 'vue';
import { useDnD } from '@vue-dnd-kit/core';
const { status, over } = useDnD();
const refused = computed(() => status.value === 'dragging' && over.value?.accepted === false);
</script>
<template>
<p v-if="refused" role="status">This list doesn't take that.</p>
</template>
Sobald über einen Drop entschieden ist, lesen sich active, over und pointer als null,
auch während das Overlay noch landet (status ist dann 'dropping'). Eine UI, die daraus
abgeleitet ist, zeigt nie den alten Drag neben den neuen Daten. Während ein asynchrones
onBeforeDrop wartet, bleibt der Zustand so, wie er beim Loslassen war.
Der nächste Drag wartet nicht auf eine Landung: Er startet sofort, und status ist
'dragging', während das letzte Overlay um ihn herum landet. Ein Drücken auf ein landendes
Overlay fängt es im Flug: Der Drag geht von dort weiter, wo das Overlay gerade ist.
onDragSettle sagt dir, wann das Overlay eines Drags angekommen ist.
Jeden Drag mithören: useDnDMonitor()
import { useDnDMonitor } from '@vue-dnd-kit/core';
useDnDMonitor({
onDragStart: ({ active }) => {},
onDragMove: ({ pointer, delta }) => {},
onDragOver: ({ over }) => {}, // the target changed
onBeforeDrop: (e) => 'accept', // or 'reject', 'continue', some ids; may be async
onDragEnd: ({ active, over, dropped, accepted, position }) => {},
onDragCancel: ({ active }) => {},
onDragResume: ({ active }) => {}, // onBeforeDrop answered 'continue'
onDragSettle: ({ active, over }) => {}, // its overlay has landed
});
Ein Monitor hört jeden Drag unter seinem Provider, egal wo er startet: Das Panel oben protokolliert die Drags der Liste, ohne sie zu importieren. Er hört auf, wenn die Komponente, die ihn aufgerufen hat, unmountet wird.
| Event | Wann |
|---|---|
onDragStart | Ein Drag startet. |
onDragMove | Der Drag bewegt sich. |
onDragOver | Das Ziel unter dem Drag ändert sich. |
onBeforeDrop | Der Drag wird losgelassen. Seine Antwort entscheidet über den Drop: siehe Über einen Drop entscheiden. |
onDragEnd | Der Drop ist durchgegangen. Sortierbare Listen haben ihr onDrop schon ausgeführt und die neue Reihenfolge geschrieben. dropped ist false, wenn der Drag über keinem Ziel endete. |
onDragCancel | Escape, ein Drop auf ein Ziel, das ihn ablehnt, oder ein onBeforeDrop, das 'reject' geantwortet hat. |
onDragResume | onBeforeDrop hat 'continue' geantwortet: Nichts wurde abgelegt, und der Drag geht weiter. |
onDragSettle | Das Overlay des Drags ist gelandet (oder zurückgeflogen, oder vom nächsten Drag gefangen worden), mit dem Event, das onDragEnd oder onDragCancel bekommen hat. Inzwischen können andere Drags gestartet sein. |
Jedes Event trägt active, over, pointer, delta (der Versatz zum Start),
position (wo die gezogene Box in over sitzt, siehe
Drag and Drop) und, für Elemente
aus einer sortierbaren Liste, source. Die vollständigen Formen stehen in der
Events-API.
onDragEnd ist der Ort für Arbeit „nach dem Drop“: Speichern, Auswahl leeren, Analytics. Das
eigene onDrop einer Liste läuft vor ihrem Schreiben, sieht also noch die alte Reihenfolge.Das Ziel finden
Wähle eine Funktion und ziehe dann die Nadel oder die Karte über die Ziele. Die Badges zeigen, wie die Funktion sie in diesem Frame einordnet; das grüne ist das Ziel.
Demo wird geladen…
<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import {
DnDProvider,
closestCenter,
closestCorners,
pointerFirst,
pointerWithin,
rectIntersection,
type CollisionDetection,
type DnDEndEvent,
type Key,
} from '@vue-dnd-kit/core';
import Desk from './Desk.vue';
import { magnet } from './magnet';
const DETECTORS: Record<string, CollisionDetection> = {
pointerFirst,
pointerWithin,
rectIntersection,
closestCenter,
closestCorners,
magnet,
};
const names = Object.keys(DETECTORS);
const mode = ref('pointerFirst');
// The ranking of the current frame, best first: a wrapper around the chosen function
// records it, so the targets can show their rank.
const ranked = shallowRef<Key[]>([]);
const collision = computed<CollisionDetection>(() => {
const detect = DETECTORS[mode.value]!;
return (args) => (ranked.value = detect(args));
});
const result = ref('Drag the pin or the card over the targets');
const onEnd = ({ over }: DnDEndEvent) => {
ranked.value = [];
result.value = over ? `Dropped on ${over.id}` : 'Dropped on nothing';
};
const onCancel = () => {
ranked.value = [];
result.value = 'Cancelled';
};
</script>
<template>
<DnDProvider :collision="collision" @drag-end="onEnd" @drag-cancel="onCancel">
<div class="lab">
<div class="modes" role="radiogroup" aria-label="Collision detection">
<label v-for="name in names" :key="name" :class="{ on: mode === name }">
<input v-model="mode" type="radio" :value="name" />
{{ name }}
</label>
</div>
<code class="call"><DnDProvider :collision="{{ mode }}"></code>
<Desk :ranked="ranked" />
<p class="result" aria-live="polite">{{ result }}</p>
</div>
</DnDProvider>
</template>
<style scoped>
.lab {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
}
.modes {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2px;
max-width: 100%;
padding: 3px;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
.modes label {
padding: 5px 9px;
border-radius: 6px;
font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
cursor: pointer;
}
.modes label.on {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
font-weight: 600;
}
.modes label:has(input:focus-visible) {
outline: 2px solid #42b883;
}
.modes input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.call {
max-width: 100%;
padding: 6px 10px;
overflow-x: auto;
border-radius: 8px;
background: color-mix(in srgb, currentColor 6%, transparent);
font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
white-space: nowrap;
color: #42b883;
}
.result {
margin: 0;
font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.6;
}
</style>In jedem Frame eines Drags übergibt der Provider collision die Drop-Ziele im Spiel, und sie
gibt deren IDs zurück, das beste zuerst. Fünf Funktionen bringt die Bibliothek mit:
| Funktion | Ordnet ein | Gut für |
|---|---|---|
pointerFirst | Was unter dem Zeiger liegt; wenn dort nichts ist, was die gezogene Box überlappt. | Der Standard: präzise unter dem Zeiger, nachsichtig in den Lücken. |
pointerWithin | Nur was unter dem Zeiger liegt, das innerste zuerst. | Präzise Ziele: Eine Lücke legt nirgends ab. |
rectIntersection | Jedes Ziel, das die gezogene Box überlappt: höhere priority, dann tiefere Verschachtelung, dann die größere Überlappung. | Große Elemente über kleinen Zielen, wenn der Zeiger am Rand des Elements ist. |
closestCenter | Jedes Ziel, nach dem Abstand vom Mittelpunkt der Box zu ihrem. | Irgendetwas gewinnt immer, auch weit weg: Raster und Listen aus gleich großen Elementen. |
closestCorners | Jedes Ziel, nach den Abständen zwischen den vier Ecken. | Nebeneinanderstehende Spalten (Kanban), Ziele mit sehr unterschiedlicher Höhe. |
Unter dem Zeiger respektiert der Hit-Test, was der Nutzer sieht: Von überlappenden Zielen wird
nur das oberste getroffen (front, nie back, wo sie sich überlappen), und von verschachtelten
kommt das innerste zuerst (subfolder, dann folder).
Eine eigene Funktion
import type { CollisionDetection, Coordinates, Rect } from '@vue-dnd-kit/core';
/** How far the point is from the rect: 0 inside it. */
const gap = (rect: Rect, point: Coordinates) =>
Math.hypot(
Math.max(rect.left - point.x, 0, point.x - rect.right),
Math.max(rect.top - point.y, 0, point.y - rect.bottom),
);
/** What is under the pointer; in a gap, the nearest target within 40px. */
export const magnet: CollisionDetection = ({ hits, pointer, candidates }) => {
if (hits.length) return hits;
return candidates
.filter((candidate) => gap(candidate.rect, pointer) <= 40)
.sort((a, b) => gap(a.rect, pointer) - gap(b.rect, pointer))
.map((candidate) => candidate.id);
};
| Argument | Was es enthält |
|---|---|
pointer | Der Zeiger, nach den Modifiern. |
overlayRect | Die gezogene Box, wie sie angezeigt wird, null ohne Overlay. |
candidates | Jedes Ziel im Spiel: id, rect (sein sichtbarer Teil, von Scroll-Containern beschnitten), priority, depth (0 ganz oben, tiefer bei Verschachtelung). |
hits | Die Ziele unter dem Zeiger aus dem Hit-Test, das beste zuerst. |
Gib ein leeres Array für „kein Ziel“ zurück. Deaktivierte Ziele, Ziele in den gezogenen
Elementen und Ziele, die von ihren Scroll-Containern verdeckt sind, erreichen die Funktion nie.
Ein Ziel, das den Drag ablehnt, konkurriert trotzdem: Über ihm ist over.accepted false, und
ein Drop wird abgelehnt (ein Ziel in einem sortierbaren Element tritt stattdessen zur Seite,
damit die Liste darüber sortiert). Die Demo umhüllt die gewählte Funktion, um ihre Rangfolge
festzuhalten: (args) => (ranked.value = detect(args)).
Wann ein Drücken zum Drag wird
Ein Drücken zieht nicht sofort. activation legt je nach Zeigerart (mouse, touch, pen)
fest, worauf es wartet: eine distance in px, ein delay in ms (mit einer tolerance, nach
der das Drücken aufgibt, damit die Seite scrollen kann) oder was von beiden zuerst eintritt.
<DnDProvider :activation="{ mouse: { distance: 8 }, touch: { delay: 300, tolerance: 8 } }">
| Zeiger | Standard |
|---|---|
mouse, pen | { distance: 4 }: Ein Klick bleibt ein Klick. Ein Mausdruck auf ein handle startet sofort und bleibt ein Klick, wenn sich die Maus nicht bewegt. |
touch | { delay: 200, tolerance: 5 }: ein langes Drücken, damit Listen unter dem Finger weiter scrollen. |
Eine Art, die du weglässt, behält ihren Standard. Die eigene activation eines ziehbaren
Elements gewinnt gegen die eines Drag-Bereichs, und die gewinnt
gegen die des Providers. Während ein Drücken wartet, ist useDnD().status 'activating', und
activation.progress läuft von 0 bis 1; Escape oder useDnD().cancel() bricht es ab.
Probiere die Bedingungen live aus und zeichne aus dem Fortschritt einen Ring fürs lange Drücken,
in Sensoren und programmatisches Ziehen.
Auto-Scroll
Ziehe einen Kanal zum oberen oder unteren Ende der Liste. Die grünen Streifen sind die Randzonen: Je tiefer du in einer bist, desto schneller scrollt die Liste.
Demo wird geladen…
<script setup lang="ts">
import { computed, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Channels from './Channels.vue';
const OPTIONS = {
threshold: [0.1, 0.2, 0.4],
maxSpeed: [10, 25, 50],
acceleration: [4, 10, 25],
} as const;
const on = ref(true);
// The defaults: an edge zone of 20% of the container (24 to 96px), 10px a frame at the
// edge, up to 25px a frame past it.
const options = ref({ threshold: 0.2, maxSpeed: 25, acceleration: 10 });
const autoScroll = computed(() => (on.value ? options.value : false));
const call = computed(() =>
on.value
? `:auto-scroll="{ threshold: ${options.value.threshold}, maxSpeed: ${options.value.maxSpeed}, acceleration: ${options.value.acceleration} }"`
: ':auto-scroll="false"',
);
</script>
<template>
<DnDProvider :auto-scroll="autoScroll">
<div class="demo">
<div class="switches">
<label class="switch">
<input v-model="on" type="checkbox" />
autoScroll
</label>
<fieldset v-for="(values, name) in OPTIONS" :key="name" :disabled="!on">
<legend>{{ name }}</legend>
<label v-for="value in values" :key="value" :class="{ on: options[name] === value }">
<input v-model="options[name]" type="radio" :value="value" />
{{ value }}
</label>
</fieldset>
</div>
<code class="call">{{ call }}</code>
<Channels :threshold="on ? options.threshold : 0" />
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.switches {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px 14px;
}
fieldset {
display: inline-flex;
margin: 0;
padding: 3px;
border: 0;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
fieldset:disabled {
opacity: 0.4;
}
legend {
float: left;
align-self: center;
padding: 0 8px 0 6px;
font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
opacity: 0.55;
}
fieldset label {
padding: 5px 9px;
border-radius: 6px;
font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
cursor: pointer;
}
fieldset label.on {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
font-weight: 600;
}
fieldset label:has(input:focus-visible),
.switch:has(input:focus-visible) {
outline: 2px solid #42b883;
}
fieldset input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.switch {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
cursor: pointer;
}
.switch input {
accent-color: #42b883;
}
.call {
max-width: 100%;
padding: 6px 10px;
overflow-x: auto;
border-radius: 8px;
background: color-mix(in srgb, currentColor 6%, transparent);
font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
white-space: nowrap;
color: #42b883;
}
</style>Jedes Element mit overflow: auto oder scroll scrollt von selbst, ebenso das Fenster. Der
Provider scrollt zuerst den Container unter dem Zeiger, dann dessen Vorfahren, dann das
Fenster.
| Option | Standard | Was sie festlegt |
|---|---|---|
threshold | 0.2 | Die Randzone als Anteil der Containergröße, zwischen 24 und 96 px. |
acceleration | 10 | Die Geschwindigkeit ganz am Rand, in px pro Frame. Sie wächst mit dem Quadrat der Tiefe in der Zone, so ist der Anfang langsam und präzise. |
maxSpeed | 25 | Die Höchstgeschwindigkeit. Jenseits des Rands (der Zeiger außerhalb des Containers) wächst die Geschwindigkeit bis zu ihr weiter. |
:auto-scroll="false" schaltet es ab. Eine Richtung scrollt erst, wenn sich der Zeiger in
diese Richtung bewegt hat, damit ein Element, das nahe an einem Rand aufgenommen wird, nicht von
selbst scrollt. Listen, Bäume und Drag-Bereiche nehmen ein eigenes
autoScroll für ihren Container:
makeSortableList(list, { items: channels, autoScroll: { maxSpeed: 10 } });
Drags per Tastatur scrollen nie automatisch: Sie scrollen stattdessen das Element in den
sichtbaren Bereich. isAutoScrolling (aus useDnD() oder einem Drag-Bereich) ist true,
während etwas scrollt.
Haptik und die Kopiertaste
Ein Touch-Drag vibriert kurz, wenn er startet. :haptics="false" schaltet das ab; ein Objekt
legt jede Vibration fest:
<DnDProvider :haptics="{ start: true, enter: 10, drop: 10, refuse: [20, 40, 20] }">
| Schlüssel | Vibriert, wenn | Standard |
|---|---|---|
start | Das lange Drücken zum Drag wird. | true (10 ms) |
enter | Der Drag ein Ziel betritt, das ihn annimmt. | aus |
drop | Der Drop durchgeht. | aus |
refuse | Der Drop abgelehnt wird und die Elemente zurückfliegen. | aus |
Jeder nimmt true (das Standardmuster), eine Länge in ms, ein An/Aus-Muster oder false. Nur
Drags, die per Touch gestartet wurden, vibrieren, und iOS Safari kann überhaupt nicht vibrieren.
copyModifier ist die Taste, die, während eines Drags zwischen Listen gehalten, aus dem
Verschieben ein Kopieren macht: standardmäßig 'Alt', jeder KeyboardEvent.key oder null
für keine. Die Liste, aus der die Elemente kommen, braucht eine clone-Funktion; siehe
Listen mit gemeinsamen Elementen.
Mehrere Provider
Die meisten Apps brauchen genau einen. Setz die Wunschliste unter einen eigenen Provider und versuche dann, ein Buch hinüberzuschieben: Es kann seinen Provider nicht verlassen, und der äußere Monitor hört die Wunschliste nicht mehr.
Demo wird geladen…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Shelf from './Shelf.vue';
import EventLog from './EventLog.vue';
const reading = ref([
{ id: 'dune', title: 'Dune', hue: 32 },
{ id: 'emma', title: 'Emma', hue: 330 },
{ id: 'ulysses', title: 'Ulysses', hue: 210 },
]);
const wishlist = ref([
{ id: 'beloved', title: 'Beloved', hue: 140 },
{ id: 'solaris', title: 'Solaris', hue: 260 },
]);
const separate = ref(false);
</script>
<template>
<DnDProvider>
<div class="demo">
<div class="modes" role="radiogroup" aria-label="Wishlist">
<label :class="{ on: !separate }"><input v-model="separate" type="radio" :value="false" />one provider</label>
<label :class="{ on: separate }"><input v-model="separate" type="radio" :value="true" />wishlist in its own</label>
</div>
<div class="columns">
<div class="column">
<Shelf id="reading" v-model="reading" title="Reading" />
<EventLog title="outer monitor" />
</div>
<!-- A provider inside a provider: its items belong to it, and drags never cross. -->
<DnDProvider v-if="separate">
<div class="column island">
<span class="tag"><DnDProvider></span>
<Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
<EventLog title="inner monitor" />
</div>
</DnDProvider>
<div v-else class="column">
<Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
</div>
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
}
.modes {
display: inline-flex;
padding: 3px;
border-radius: 9px;
background: color-mix(in srgb, currentColor 7%, transparent);
}
.modes label {
padding: 5px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
}
.modes label.on {
background: Canvas;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
font-weight: 600;
}
.modes label:has(input:focus-visible) {
outline: 2px solid #42b883;
}
.modes input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.columns {
display: grid;
grid-template-columns: repeat(2, minmax(0, 230px));
gap: 16px;
max-width: 100%;
}
.column {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.island {
position: relative;
margin: -10px;
padding: 10px;
border: 1.5px dashed color-mix(in srgb, #42b883 60%, transparent);
border-radius: 16px;
}
.tag {
position: absolute;
top: -9px;
right: 14px;
padding: 0 6px;
background: Canvas;
font: 10px/18px ui-monospace, Menlo, Consolas, monospace;
color: #42b883;
}
</style><DnDProvider>
<Shelf v-model="reading" />
<DnDProvider>
<Shelf v-model="wishlist" />
<EventLog /> <!-- hears the inner provider's drags only -->
</DnDProvider>
</DnDProvider>
- Provider sind unabhängig: Ein Drag wechselt nie von einem zum anderen.
- Verschachtelte Provider sind erlaubt. Der nächste Besitzer gewinnt: Ein Element gehört zum nächstgelegenen Provider darüber, und der äußere Provider ignoriert es.
- Jeder Provider hat seine eigene Auswahl, sein Overlay, seine Live-Region und seine Monitore.
Ein zweiter Provider lohnt sich nur für einen Teil der Seite, der eigene providerweite Einstellungen braucht (Kollisionserkennung, Tastaturtasten, Ansagen) und dessen Elemente ihn nie verlassen. Wenn sich Elemente nicht zwischen zwei Teilen deiner Seite verschieben lassen, prüfe, ob sie nicht unter zwei Providern sitzen.
makeDragArea(board, { modifiers: [snapToGrid(20)], activation: { touch: { delay: 400 } } });
Eine App, die woanders gemountet ist, in einem Shadow Root oder als eigene Vue-App, kann sich
einem bestehenden Provider anschließen: Übergib useDnDContext() an
provideDnDContext(app, context), bevor du sie mountest. Hit-Test und Overlay funktionieren
über Shadow Roots hinweg.
SSR und Nuxt
Der Provider ist SSR-sicher. Listen und Elemente rendern auf dem Server wie jedes andere
Markup. Das Overlay und die Live-Region werden teleportiert und nur auf dem Client gemountet,
deshalb verursachen sie nie einen Hydration-Mismatch. Standard-IDs kommen von Vues useId() und
stimmen zwischen Server und Client überein.
In Nuxt registriert das Modul DnDProvider global und importiert die Composables automatisch:
siehe Installation.