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

SensorEingabe
pointerSensorMaus, 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.
keyboardSensorSpace 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:

Live

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:

FeldDer Drag startet …
distanceSobald sich der Zeiger so viele px bewegt hat.
delaySobald das Drücken so viele ms gedauert hat. Zusammen mit distance: was zuerst eintritt.
toleranceMit 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:

ZeigerStandard
MausNach 4 px Bewegung. Sofort beim Drücken auf ein handle, außer es ist eine Bedingung für die Maus gesetzt.
StiftNach 4 px Bewegung.
TouchNach 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:

Inbox.vue
<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:

Message.vue
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:

Live

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>
Task.vue
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;
  },
});
OptionGefragtWofür
disabledLive, und als aria-disabled gesetztElemente, die sich gar nicht bewegen können: gesperrt, schreibgeschützt.
canDragEinmal pro Drücken, mit seinem EventRegeln zur Eingabe: eine gehaltene Taste, eine Maustaste, eine Zeigerart.
handleWo das Drücken landetElemente, 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:

Live

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:

joystick.ts
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:

joystick.ts
/** 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:

joystick.ts
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:

MemberWas es ist
propsDie Props des Providers (activation, keyboard …). Lies sie, wenn du sie brauchst: Sie können sich ändern.
statusDer Drag-Status des Providers, egal welcher Sensor den Drag führt.
dragDer 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:

OptionBedeutung
targetWo 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.
pointWo 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.
activationEine Bedingung ({ delay: 300 }) oder eine Zeigerart ('touch'), um die dafür konfigurierte zu verwenden. Standard: keine, der Drag startet sofort.
eventDas 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:

MemberWas es tut
id, typeDie ID des gezogenen Elements und 'pointer' oder 'keyboard'.
state'pending' (wartet auf die Aktivierung), 'dragging', dann 'ended'.
startedDer 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.

Um einen eingebauten Sensor zu umhüllen, führe sein 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).

Live

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
  • list ist die id einer sortierbaren Liste. Ohne sie bleibt das Element in seiner Liste.
  • index ist 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 true auf, wenn der Drop durchging, mit false, 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“:

Live

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