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

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

Ein Composable muss in einer Komponente laufen, die im Provider gerendert wird. Rufst du es in der Komponente auf, deren eigenes Template <DnDProvider> enthält, wird No <DnDProvider> found above this component geworfen: inject von Vue sieht nur Vorfahren.

Wofür er zuständig ist

TeilWas der Provider tut
EingabeMacht aus Zeiger-, Touch- und Tastatureingaben Drags (sensors, activation), immer nur einen gleichzeitig.
Das Ziel findenPrüft per Hit-Test, welche Drop-Ziele unter dem Drag liegen (collision), und teilt es ihnen mit.
Das OverlayRendert die Box, die dem Zeiger folgt, teleportiert sie nach body und lässt sie beim Drop landen.
Die AuswahlEine Menge ausgewählter IDs, die alle Listen und ziehbaren Elemente darunter teilen.
Auto-ScrollScrollt Container und das Fenster, wenn sich ein Drag ihren Rändern nähert.
Die Live-RegionSagt 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.

PropStandardWas sie festlegt
collisionpointerFirstWie das Ziel unter dem Drag gewählt wird. Unten.
activationMaus 4 px, Touch 200 msWann ein Drücken zum Drag wird, je nach Zeigerart. Unten.
autoScrollanScrollen an den Rändern: { threshold, maxSpeed, acceleration } oder false. Unten.
hapticseine kurze Vibration beim Aufnehmen per TouchVibrationen 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 / dropAnimationanElemente, die beim Sortieren gleiten / das Landen des Overlays: siehe Animationen.
animateChangesausStandard für Listen und Bäume: Datenänderungen animieren, die kein Drag ausgelöst hat.
keyboardSpace / Enter, EscapeTasten und Schrittweite für die Tastatur; false schaltet das Ziehen per Tastatur ab. Siehe Barrierefreiheit.
sensorsZeiger + TastaturWas 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.
overlayTransitioneine ÜberblendungWie 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.
overlayPreviews5Multi-Drag: wie viele gezogene Elemente beim Aufnehmen einen Klon bekommen, oder 'all'. Siehe das Standard-Overlay.
announcementseingebautTexte für Screenreader, oder false.
screenReaderInstructionseingebautDer Text, auf den jedes ziehbare Element mit aria-describedby verweist.
defaultStylestrueFü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()

Live

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();
FeldWas 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).
activeWas gezogen wird: id, ids (jede gezogene ID, die gegriffene zuerst), items, data, type, element, rect, sensor. null im Ruhezustand.
overDas Ziel unter dem Drag: id, data, element, rect und accepted. null über nichts.
pointerDer Zeiger in Viewport-Koordinaten, nach den Modifiern.
activation.progress0 bis 1, während ein Drücken auf seine Aktivierung wartet: Zeichne damit einen Ring.
isAutoScrollingAuto-Scroll bewegt gerade etwas.
selectionDie 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.
DropHint.vue
<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.

EventWann
onDragStartEin Drag startet.
onDragMoveDer Drag bewegt sich.
onDragOverDas Ziel unter dem Drag ändert sich.
onBeforeDropDer Drag wird losgelassen. Seine Antwort entscheidet über den Drop: siehe Über einen Drop entscheiden.
onDragEndDer 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.
onDragCancelEscape, ein Drop auf ein Ziel, das ihn ablehnt, oder ein onBeforeDrop, das 'reject' geantwortet hat.
onDragResumeonBeforeDrop hat 'continue' geantwortet: Nichts wurde abgelegt, und der Drag geht weiter.
onDragSettleDas 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.

Live

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">&lt;DnDProvider :collision="{{ mode }}"&gt;</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:

FunktionOrdnet einGut für
pointerFirstWas 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.
pointerWithinNur was unter dem Zeiger liegt, das innerste zuerst.Präzise Ziele: Eine Lücke legt nirgends ab.
rectIntersectionJedes 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.
closestCenterJedes Ziel, nach dem Abstand vom Mittelpunkt der Box zu ihrem.Irgendetwas gewinnt immer, auch weit weg: Raster und Listen aus gleich großen Elementen.
closestCornersJedes 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

magnet.ts
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);
};
ArgumentWas es enthält
pointerDer Zeiger, nach den Modifiern.
overlayRectDie gezogene Box, wie sie angezeigt wird, null ohne Overlay.
candidatesJedes Ziel im Spiel: id, rect (sein sichtbarer Teil, von Scroll-Containern beschnitten), priority, depth (0 ganz oben, tiefer bei Verschachtelung).
hitsDie 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)).

Der Provider dämpft die Wechsel, die die Funktion verlangt. Ein Ziel, das der Drag gerade verlassen hat, kann erst wieder gewinnen, wenn sich der Zeiger 10 px bewegt hat, damit Grenzen nicht flackern. Zwischen den Zielen, die in einem Elternelement verschachtelt sind (die Lücken einer Spalte aus Karten), bleibt das letzte Ziel.

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 } }">
ZeigerStandard
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.

Live

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.

OptionStandardWas sie festlegt
threshold0.2Die Randzone als Anteil der Containergröße, zwischen 24 und 96 px.
acceleration10Die 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.
maxSpeed25Die 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üsselVibriert, wennStandard
startDas lange Drücken zum Drag wird.true (10 ms)
enterDer Drag ein Ziel betritt, das ihn annimmt.aus
dropDer Drop durchgeht.aus
refuseDer 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.

Live

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">&lt;DnDProvider&gt;</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.

Regeln für einen Teil der Seite (ein Board, das einrastet, ein Feed mit längerem Touch-Drücken, ein Panel, in dem nichts gezogen wird) brauchen keinen eigenen Provider:
makeDragArea(board, { modifiers: [snapToGrid(20)], activation: { touch: { delay: 400 } } });
Die Elemente des Bereichs bleiben im Provider der App und können sich weiterhin überallhin bewegen. Siehe Achsen, Grenzen und Raster.

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.