Dokumentation/Leitfäden
Sensoren und programmatisches Ziehen
Wie aus Eingaben ein Drag wird – die Sensoren für Zeiger und Tastatur, Aktivierungsbedingungen, canDrag, eigene Sensoren und Drags, die du mit @vue-dnd-kit/actions per Skript steuerst.
Diese Seite wurde von KI übersetzt und kann Ungenauigkeiten enthalten. Englisches Original lesen
Ein Sensor macht aus Eingaben Drags. Er sagt dem Provider nur „zieh dieses Element von hier“, dann, wohin sich die Eingabe bewegt hat und ob abgelegt oder abgebrochen wurde. Den Rest erledigt der Provider, für jeden Sensor gleich: ein Drag gleichzeitig, deaktivierte Elemente abgelehnt, Aktivierungsbedingungen, Sortieren, Auto-Scroll, Ansagen, Escape und der ganze Drop.
Die eingebauten Sensoren
| Sensor | Eingabe |
|---|---|
pointerSensor | Maus, Touch und Stift, über Pointer Events. Er fängt den Zeiger ein, verhindert, dass die Seite unter einem Touch-Drag Text markiert oder scrollt, und schluckt den Klick, der auf einen Drag folgt. |
keyboardSensor | Space und Enter nehmen auf und legen ab, die Pfeiltasten bewegen, Escape bricht ab – jeden Drag, auch Drags per Zeiger. |
Beide laufen standardmäßig. sensors des Providers ersetzt die Liste:
<DnDProvider :sensors="[pointerSensor, keyboardSensor, joystick.sensor]">
Um das Ziehen per Tastatur abzuschalten, nimm lieber :keyboard="false", statt keyboardSensor
wegzulassen: Der Tastatur-Sensor ist auch das, was jeden Drag mit Escape abbricht.
Seine Tasten belegst du mit keyboard.keys neu (siehe
Barrierefreiheit).
Aktivierungsbedingungen
Ein Drücken zieht nicht sofort: Es wartet auf seine Aktivierungsbedingung, sodass ein Klick ein Klick bleibt und ein Wischen die Seite weiterhin scrollen kann. Schalte die Regeln dieser Liste um und drück auf eine Zeile:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Inbox from './Inbox.vue';
</script>
<template>
<DnDProvider>
<Inbox />
</DnDProvider>
</template>Eine Bedingung hat drei Felder:
| Feld | Der Drag startet … |
|---|---|
distance | Sobald sich der Zeiger so viele px bewegt hat. |
delay | Sobald das Drücken so viele ms gedauert hat. Zusammen mit distance: was zuerst eintritt. |
tolerance | Mit delay: wie weit sich der Zeiger in der Zwischenzeit bewegen darf. Darüber hinaus gibt das Drücken auf, damit die Seite scrollen kann. Standard 5 px, wenn es keine distance gibt. |
Ohne eigene Bedingung hat jede Zeigerart ihren Standard:
| Zeiger | Standard |
|---|---|
| Maus | Nach 4 px Bewegung. Sofort beim Drücken auf ein handle, außer es ist eine Bedingung für die Maus gesetzt. |
| Stift | Nach 4 px Bewegung. |
| Touch | Nach 200 ms Drücken. Eine Bewegung um mehr als 5 px vorher bricht es ab, und die Seite scrollt stattdessen. |
Wo du sie setzt
Pro Zeigerart – mouse, touch, pen – auf dem Provider, einem Drag-Bereich oder dem Element.
Für jede Art gewinnt die des Elements, dann die seines Bereichs, dann die des Providers:
<DnDProvider :activation="{ mouse: { distance: 8 }, touch: { delay: 250, tolerance: 5 } }">
// Inbox.vue: the rules of every drag that starts inside the panel.
makeDragArea(panel, { activation: () => MODES[mode.value].activation });
// One item: a longer press by touch, the area's rule for the mouse.
makeSortableItem(el, { item: () => props.task, activation: { touch: { delay: 400, tolerance: 8 } } });
Die Optionen werden gelesen, wenn ein Drücken beginnt, deshalb schaltet ein Getter oder ein Ref sie live um, wie in der Demo. Drags per Tastatur starten sofort. Ein eigener Sensor fragt nach seiner eigenen Bedingung oder nach der, die für eine Zeigerart konfiguriert ist.
Ein Ring für langes Drücken
Während ein Drücken wartet, ist useDnD().status gleich 'activating', und activation.progress
läuft von 0 bis 1 – nach Zeit mit delay, nach zurückgelegter Strecke mit distance. Die Demo
zeichnet einen Ring dort, wo das Drücken begann:
<script setup lang="ts">
const { status, activation } = useDnD();
</script>
<template>
<svg v-if="status === 'activating'" class="ring" viewBox="0 0 40 40">
<circle cx="20" cy="20" r="17" pathLength="1" :stroke-dasharray="`${activation.progress.value} 1`" />
</svg>
</template>
Gib dem Ring pointer-events: none: Er liegt unter dem Zeiger, und der Drag prüft per Hit-Test,
was dort ist. Ein Drücken, das aufgibt – über seine tolerance hinaus bewegt oder zu früh
losgelassen –, setzt status direkt wieder auf 'idle'.
Handles starten sofort
Mit einem handle startet nur das Handle einen Drag, und ein Mausdruck darauf zieht sofort: Beim Packen
eines Griffs gibt es nichts zu unterscheiden. Ein Druck, der dort losgelassen wird, wo er begann,
bleibt ein Klick, der Griff kann also bei click ein Menü öffnen (der Zeiger wird erst
eingefangen, wenn er sich bewegt). Ein Touch wartet trotzdem auf sein langes Drücken, außer das Handle kann die Seite nicht scrollen. Gib ihm touch-action: none und dem
Element eine kurze Strecke:
makeSortableItem(el, {
item: () => props.chat,
handle: '.grip',
// The grip has `touch-action: none`: a touch on it can't scroll, so it drags after 4 px.
activation: { touch: { distance: 4 } },
});
Eine Bedingung für die Maus, die auf dem Element, seinem Bereich oder dem Provider gesetzt ist,
gilt auch für Handles. Mach den Griff zu einem <button>, wie die Demo: Er bekommt den Fokus,
also startet auch ein Drag per Tastatur von ihm aus.
Nur manche Drücke
canDrag ist eine weitere Schranke auf ziehbaren und sortierbaren Elementen. Es bekommt das
Eingabe-Event und antwortet für dieses Drücken mit true oder false. Hier brauchen Drags per
Zeiger Shift; die Tastatur zieht wie gewohnt:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Checklist from './Checklist.vue';
</script>
<template>
<DnDProvider>
<Checklist />
</DnDProvider>
</template>makeSortableItem(el, {
item: () => props.task,
// Never: the row is locked (and says so to screen readers with aria-disabled).
disabled: () => !!props.task.locked,
// Per press: pointer drags only with Shift held; the keyboard as usual.
canDrag: (event) => {
if (!props.shiftOnly || event instanceof KeyboardEvent || event.shiftKey) return true;
emit('refused');
return false;
},
});
| Option | Gefragt | Wofür |
|---|---|---|
disabled | Live, und als aria-disabled gesetzt | Elemente, die sich gar nicht bewegen können: gesperrt, schreibgeschützt. |
canDrag | Einmal pro Drücken, mit seinem Event | Regeln zur Eingabe: eine gehaltene Taste, eine Maustaste, eine Zeigerart. |
handle | Wo das Drücken landet | Elemente, die einen Drag nur von einem Teil aus starten. |
canDrag wird vor der Aktivierungsbedingung gefragt, mit dem pointerdown oder keydown. Eine
Zeigerart ist eine weitere Regel, die es aufstellen kann:
// Mouse and pen drag; a touch on the row always scrolls the page.
canDrag: (event) => !(event instanceof PointerEvent && event.pointerType === 'touch'),
Interaktive Kinder – Buttons, Eingabefelder, Checkboxen, editierbare Felder – starten nie einen
Drag per Zeiger, deshalb funktionieren sie in einem ziehbaren Element weiter, wie die Checkbox und
das Owner-Menü in der Demo. Mit einem handle startet nur das Handle einen.
Ein eigener Sensor
Ein Gamepad, ein Joystick auf dem Bildschirm, Sprachbefehle: Alles, was auf ein Element zeigen und sich bewegen kann, kann einen Drag steuern. Dieses Gamepad ist ein Sensor. Halte A über einem Helden, um ihn aufzunehmen, steuere mit dem Steuerkreuz, ein erneutes A legt ab, B bricht ab. Maus und Tastatur funktionieren daneben weiter:
Demo wird geladen…
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue';
import { DnDProvider, keyboardSensor, pointerSensor } from '@vue-dnd-kit/core';
import Party from './Party.vue';
import Gamepad from './Gamepad.vue';
import { createJoystick } from './joystick';
const stage = useTemplateRef<HTMLElement>('stage');
const joystick = createJoystick(() => stage.value);
// The gamepad drives drags next to the mouse, touch and the keyboard.
const sensors = [pointerSensor, keyboardSensor, joystick.sensor];
onMounted(() => {
const first = stage.value?.querySelector('li');
if (first) joystick.aim(first);
});
</script>
<template>
<DnDProvider :sensors="sensors">
<div ref="stage" class="arcade">
<Party />
<Gamepad :joystick="joystick" />
<span class="cursor" :style="{ left: `${joystick.cursor.value.x}px`, top: `${joystick.cursor.value.y}px` }" />
</div>
</DnDProvider>
</template>
<style scoped>
.arcade {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 32px 48px;
padding: 8px;
}
/* The gamepad's cursor: above everything, never what a hit test finds. */
.cursor {
position: absolute;
z-index: 1;
width: 18px;
height: 18px;
margin: -9px 0 0 -9px;
border: 2px solid #42b883;
border-radius: 50%;
background: color-mix(in srgb, #42b883 25%, transparent);
box-shadow: 0 0 0 4px color-mix(in srgb, #42b883 15%, transparent);
pointer-events: none;
}
</style>defineSensor gibt dem Sensor seinen Namen und ein setup, das die Seite des Providers
bekommt, einmal pro Provider:
const sensor = defineSensor({
name: 'joystick',
// Once per provider, in the browser. The cleanup runs when the provider unmounts.
setup(ctx) {
context = ctx;
return () => {
context = null;
drag = null;
};
},
});
Ein Drücken fordert mit activate() einen Drag beim Provider an. Das Ziel wird wie ein echtes
Drücken geprüft: Es muss ein ziehbares Element dieses Providers sein, nicht deaktiviert und kein
Button darin:
/** A goes down: drops a running drag, or starts a hold over what is under the cursor. */
const press = () => {
if (drag?.state === 'dragging') return void drag.drop();
const at = point();
const target = document.elementFromPoint(at.x, at.y);
if (!target || !context) return;
drag = context.activate({
target, // checked like a real press: a draggable of this provider, not disabled
point: at,
type: 'pointer', // follows points: lists sort under it, containers auto-scroll
activation: { delay: 300 }, // `useDnD().activation.progress` runs meanwhile
onEnd: (ended) => {
if (drag === ended) drag = null;
},
}); // null: nothing to drag there
};
Dann bewegt sich der Drag mit dem Steuerkreuz, und A oder B beenden ihn:
const move = (dx: number, dy: number) => {
cursor.value = { x: cursor.value.x + dx, y: cursor.value.y + dy }; // clamped in the demo
drag?.move(point()); // pending: checks the tolerance; dragging: moves
};
const release = () => {
if (drag?.state === 'pending') drag.cancel(); // A let go before the hold is over
};
const cancel = () => drag?.cancel();
Füge den Sensor zu sensors des Providers hinzu, wie die Demo, oder lass ihn mit
useSensor(joystick.sensor) laufen, solange eine Komponente lebt. Punkte sind in
Viewport-Koordinaten: Die Demo hält ihren Cursor in den Koordinaten der Bühne, damit er mit der
Seite scrollt, und rechnet ihn in point() um.
Was der Sensor bekommt
setup(context) bekommt die Seite des Providers:
| Member | Was es ist |
|---|---|
props | Die Props des Providers (activation, keyboard …). Lies sie, wenn du sie brauchst: Sie können sich ändern. |
status | Der Drag-Status des Providers, egal welcher Sensor den Drag führt. |
drag | Der aktuelle Drag des Providers, wartend oder laufend, egal welcher Sensor ihn gestartet hat. null, wenn es keinen gibt. |
activate(options) | Fordert einen Drag an. Gibt ein SensorDrag zurück oder null, wenn kein Drag starten darf. |
cancel() | Bricht den Drag oder das wartende Drücken des Providers ab. |
activate() nimmt:
| Option | Bedeutung |
|---|---|
target | Wo die Eingabe gelandet ist: ein Element in einem ziehbaren Element oder die ID eines ziehbaren Elements. Ein Element wird wie ein echtes Drücken geprüft (Handle, kein interaktives Kind); eine ID überspringt die Prüfungen. |
point | Wo der Drag startet, in Viewport-Koordinaten. Standard: die Mitte des ziehbaren Elements. |
type | 'pointer' (Standard): folgt Punkten, prüft Ziele per Hit-Test, nutzt Auto-Scroll. 'keyboard': bewegt sich von Element zu Element, scrollt Ziele ins Bild, gibt am Ende den Fokus zurück. |
activation | Eine Bedingung ({ delay: 300 }) oder eine Zeigerart ('touch'), um die dafür konfigurierte zu verwenden. Standard: keine, der Drag startet sofort. |
event | Das Eingabe-Event, das an canDrag des Elements geht (ohne Event wird canDrag nicht gefragt). Ein Touch-pointerType macht daraus einen Touch-Drag (Haptik). |
onStart(drag) | Der Drag hat begonnen, nach seiner Aktivierung. |
onEnd(drag) | Der Drag ist vorbei: abgebrochen, abgelegt oder verworfen – von wem auch immer. |
onResume(drag) | onBeforeDrop hat 'continue' geantwortet: Der Drag geht weiter, und move, drop und cancel gelten wieder. |
Das SensorDrag, das sie zurückgibt:
| Member | Was es tut |
|---|---|
id, type | Die ID des gezogenen Elements und 'pointer' oder 'keyboard'. |
state | 'pending' (wartet auf die Aktivierung), 'dragging', dann 'ended'. |
started | Der Drag hat begonnen, auch wenn er inzwischen vorbei ist. |
move(point) | Die Eingabe hat sich bewegt. Im Wartezustand prüft es die Toleranz; während des Ziehens bewegt es den Drag. |
step(direction) | Drags per Tastatur: ein Pfeilschritt, 'up' | 'down' | 'left' | 'right'. |
place({ list, index }) | Setzt das Element an eine Position einer sortierbaren Liste, wie es Bewegungen per Tastatur tun. Löst mit false auf, wenn keine Liste es dort annimmt. |
drop() | Legt ab. Löst mit true auf, wenn der Drop durchging, sobald er gelandet ist. |
cancel() | Bricht ab: Das Element fliegt zurück, oder ein wartendes Drücken wird verworfen. |
Aufrufe, die nicht mehr passen – eine Bewegung nach dem Ende, ein zweites drop() –, werden
ignoriert. Vollständige Typen: Sensoren.
setup mit einem eigenen Kontext aus. Erzeuge
ihn mit Object.create(context, { activate: { value: myActivate } }) statt mit einem Spread,
damit die Live-Getter (status, drag, props) weiter funktionieren.Programmatisches Ziehen
@vue-dnd-kit/actions verschiebt Elemente per Code: „Move up“-Buttons, Menüs, Tastenkürzel,
Rückgängig, Onboarding-Touren, Tests. Es baut auf der öffentlichen Sensor-API auf, und seine Drags
sind echte: angesagt und animiert, durch onBeforeDrop, onDrop, e.result und
preventDefault(), nach den Regeln des Providers (ein Drag gleichzeitig, deaktivierte Elemente,
accept und group.put).
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Queue from './Queue.vue';
</script>
<template>
<!-- @vue-dnd-kit/actions moves the rows from code: buttons, a shuffle, a scripted drag. -->
<DnDProvider>
<Queue />
</DnDProvider>
</template>npm i @vue-dnd-kit/actions
moveItem: ein Aufruf, eine Bewegung
import { useDnDActions } from '@vue-dnd-kit/actions';
const { moveItem } = useDnDActions(); // in setup, under a provider
await moveItem('card-1', { index: 0 }); // to the top of its list
await moveItem('card-1', { list: 'done' }); // to the end of another list
await moveItem('card-1', { list: 'done', index: 2 }); // to a place in another list
listist dieideiner sortierbaren Liste. Ohne sie bleibt das Element in seiner Liste.indexist die Stelle, an der das Element landet, ohne das Element selbst gezählt und auf die Liste begrenzt. Ohne ihn kommt das Element ans Ende.- Es löst mit
trueauf, wenn der Drop durchging, mitfalse, wenn das Element nicht gerendert oder deaktiviert ist, keine Liste es dort annimmt oder der Drop abgelehnt wurde. - Aufrufe reihen sich ein: Jeder wartet, bis der Provider frei ist, sodass ein schneller zweiter Klick nicht verloren geht. Das Mischen in der Demo sind fünf Aufrufe hintereinander.
- Es läuft als Drag per Tastatur, wird also angesagt, und der Fokus bleibt auf dem Element.
- Nur Listen: Positionen in Bäumen haben auch eine Tiefe.
Eine Zeile mit Buttons zum Verschieben findest du unter Barrierefreiheit.
startDrag: ein Drag per Skript
const { startDrag } = useDnDActions();
const drag = startDrag('card-1'); // null when it can't start now
if (drag) {
await drag.moveTo(document.querySelector('#trash')!); // onto an element
await drag.moveTo({ x: 400, y: 120 }); // to a point, in viewport coordinates
await drag.moveTo({ list: 'done', index: 2 }); // to a list position
await drag.drop(); // true when the drop went through; or drag.cancel()
}
moveTo löst auf, sobald die Bewegung gerendert ist, drop, sobald der Drop gelandet ist. „Play
a scripted drag“ in der Demo schiebt den letzten Track in kleinen moveTo-Schritten nach oben und
legt ihn dann ganz oben ab. startDrag(id, { from: { x, y } }) nimmt das Element an einem Punkt
statt in seiner Mitte auf.
Einen Drag an die Tastatur übergeben
startDrag(id, { type: 'keyboard' }) hebt das Element als Drag per Tastatur an. Dein Code kann es
mit await drag.step('down') bewegen oder das dem Nutzer überlassen: Der Tastatur-Sensor steuert
jeden Drag per Tastatur, egal wer ihn gestartet hat. So entsteht ein Menüpunkt „Move“:
Demo wird geladen…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Planner from './Planner.vue';
</script>
<template>
<!-- The ⋯ menus move rows with @vue-dnd-kit/actions. -->
<DnDProvider>
<Planner />
</DnDProvider>
</template>const { moveItem, startDrag } = useDnDActions();
// One move in one call: picked up, put there and dropped, announced and animated.
const toOther = () => moveItem(props.task.id, { list: props.other.id, index: 0 });
// A keyboard drag, handed to the arrow keys: Space or Enter drops, Escape cancels.
const withKeys = () => {
el.value?.focus();
startDrag(props.task.id, { type: 'keyboard' });
};
Die Pfeiltasten bewegen die Zeile von Slot zu Slot, ArrowLeft und
ArrowRight in die andere Woche (die Listen teilen eine group). Wenn der Drag endet,
kehrt der Fokus zur Zeile zurück. Die Tastenhinweise unter dem Board leuchten, solange
useDnD().active.value?.sensor gleich 'keyboard' ist.
Außerhalb von Komponenten
useDnDActions() lässt einen Sensor namens actions im Provider darüber laufen. Um einen Provider
aus einem Store oder einer Testumgebung zu steuern, oder unter einem eigenen Namen, erzeuge die
Actions in einem Sensor, den du selbst laufen lässt:
import { defineSensor } from '@vue-dnd-kit/core';
import { createDnDActions, type DnDActions } from '@vue-dnd-kit/actions';
export let tour: DnDActions | undefined;
export const tourSensor = defineSensor({
name: 'tour',
setup: (context) => void (tour = createDnDActions(context)),
});
<DnDProvider :sensors="[pointerSensor, keyboardSensor, tourSensor]">
Dann await tour?.moveItem('card-1', { list: 'done' }) von überall. Beim Server-Rendering wird
nichts gezogen: moveItem löst mit false auf, und startDrag gibt null zurück.