Documentación/Guías

Sensores y arrastres programáticos

Cómo la entrada se convierte en un arrastre — los sensores de puntero y teclado, las restricciones de activación, canDrag, sensores propios y arrastres que programas con @vue-dnd-kit/actions.

Editar esta página

Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés

Un sensor convierte la entrada en arrastres. Solo le dice al provider «arrastra este ítem desde aquí», luego adónde se movió la entrada, y si soltó o canceló. El provider hace el resto, igual para todos los sensores: un arrastre a la vez, rechazo de los ítems desactivados, restricciones de activación, ordenación, desplazamiento automático, anuncios, Escape y todo el drop.

Los sensores integrados

SensorEntrada
pointerSensorRatón, táctil y lápiz, a través de Pointer Events. Captura el puntero, evita que la página seleccione texto o se desplace bajo un arrastre táctil, y se traga el clic que sigue a un arrastre.
keyboardSensorSpace y Enter agarran y sueltan, las flechas mueven, Escape cancela — cualquier arrastre, también los de puntero.

Los dos funcionan por defecto. Los sensors del provider sustituyen la lista:

<DnDProvider :sensors="[pointerSensor, keyboardSensor, joystick.sensor]">

Para desactivar los arrastres con teclado, prefiere :keyboard="false" a dejar fuera keyboardSensor: el sensor de teclado es también lo que cancela cualquier arrastre con Escape. Sus teclas se reasignan con keyboard.keys (consulta Accesibilidad).

Restricciones de activación

Una pulsación no arrastra de inmediato: espera a su restricción de activación, para que un clic siga siendo un clic y un deslizamiento pueda seguir desplazando la página. Cambia las reglas de esta lista y pulsa una fila:

Live

Cargando la demo…

<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Inbox from './Inbox.vue';
</script>

<template>
  <DnDProvider>
    <Inbox />
  </DnDProvider>
</template>

Una restricción tiene tres campos:

CampoEl arrastre empieza…
distanceCuando el puntero se ha movido esos px.
delayCuando la pulsación ha durado esos ms. Con distance también, lo que ocurra primero.
toleranceCon delay: cuánto puede moverse el puntero entretanto. Si lo supera, la pulsación se abandona, para que la página pueda desplazarse. 5 px por defecto cuando no hay distance.

Si no pones una propia, cada tipo de puntero tiene la suya por defecto:

PunteroPor defecto
RatónTras moverse 4 px. De inmediato al pulsar sobre un asa (handle), salvo que haya una restricción de ratón.
LápizTras moverse 4 px.
TáctilTras una pulsación de 200 ms. Moverse más de 5 px antes la abandona, y la página se desplaza en su lugar.

Dónde ponerlas

Por tipo de puntero — mouse, touch, pen — en el provider, en un área de arrastre o en el ítem. Para cada tipo gana la del ítem, luego la de su área y luego la del provider:

<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 } } });

Las opciones se leen cuando empieza una pulsación, así que un getter o una ref las cambia en vivo, como hace la demo. Los arrastres con teclado empiezan de inmediato. Un sensor propio pide su propia restricción, o la configurada para un tipo de puntero.

Un anillo de pulsación larga

Mientras una pulsación espera, useDnD().status es 'activating' y activation.progress va de 0 a 1 — por tiempo con delay, por la distancia recorrida con distance. La demo dibuja un anillo donde empezó la pulsación:

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>

Dale al anillo pointer-events: none: está bajo el puntero, y el arrastre hace la prueba de colisión con lo que hay ahí. Una pulsación que se abandona — movida más allá de su tolerance, o soltada demasiado pronto — devuelve status directamente a 'idle'.

Las asas empiezan de inmediato

Con un handle, solo el asa inicia un arrastre, y una pulsación de ratón sobre ella arrastra de inmediato: agarrar un asa no deja nada que distinguir. Una pulsación que se suelta donde empezó sigue siendo un clic, así que el asa puede abrir un menú en click (el puntero solo se captura cuando se mueve). Un toque sigue esperando su pulsación larga, salvo que el asa no pueda desplazar la página. Dale touch-action: none y al ítem una distancia corta:

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 } },
});

Una restricción de ratón puesta en el ítem, su área o el provider también se aplica a las asas. Haz que el asa sea un <button>, como hace la demo: recibe el foco, así que un arrastre con teclado también empieza desde ella.

Solo algunas pulsaciones

canDrag es un filtro más en los elementos arrastrables y los ítems ordenables. Recibe el evento de entrada y responde true o false para esta pulsación. Aquí los arrastres de puntero necesitan Shift; el teclado arrastra como siempre:

Live

Cargando la demo…

<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;
  },
});
OpciónSe preguntaÚsala para
disabledEn vivo, y se pone como aria-disabledÍtems que no pueden moverse en absoluto: bloqueados, de solo lectura.
canDragUna vez por pulsación, con su eventoReglas sobre la entrada: una tecla mantenida, un botón, un tipo de puntero.
handleDonde cae la pulsaciónÍtems que solo inician un arrastre desde una parte.

A canDrag se le pregunta antes de la restricción de activación, con el pointerdown o el keydown. Un tipo de puntero es una regla más que puede poner:

// Mouse and pen drag; a touch on the row always scrolls the page.
canDrag: (event) => !(event instanceof PointerEvent && event.pointerType === 'touch'),

Los hijos interactivos — botones, inputs, casillas, campos editables — nunca inician un arrastre de puntero, así que siguen funcionando dentro de un elemento arrastrable, como la casilla y el menú del responsable de la demo. Con un handle, solo el asa inicia uno.

Un sensor propio

Un mando, un joystick en pantalla, comandos de voz: cualquier cosa que pueda señalar un ítem y moverse puede conducir un arrastre. Este mando es un sensor. Mantén A sobre un héroe para agarrarlo, dirige con la cruceta, A otra vez lo suelta y B cancela. El ratón y el teclado siguen funcionando a su lado:

Live

Cargando la demo…

<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 da al sensor su nombre y un setup que recibe la parte del provider, una vez por 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;
    };
  },
});

Una pulsación pide un arrastre al provider con activate(). El destino se comprueba como una pulsación real: debe ser un elemento arrastrable de este provider, no desactivado, y no un botón dentro de uno:

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
};

Luego el arrastre se mueve con la cruceta, y A o B lo terminan:

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();

Añade el sensor a los sensors del provider, como hace la demo, o ejecútalo mientras viva un componente con useSensor(joystick.sensor). Los puntos están en coordenadas del viewport: la demo guarda su cursor en coordenadas del escenario, para que se desplace con la página, y lo convierte en point().

Lo que recibe el sensor

setup(context) recibe la parte del provider:

MiembroQué es
propsLas props del provider (activation, keyboard…). Léelas cuando las necesites: pueden cambiar.
statusEl estado de arrastre del provider, sea cual sea el sensor que lleve el arrastre.
dragEl arrastre actual del provider, pendiente o en curso, sea cual sea el sensor que lo empezó. null cuando no hay ninguno.
activate(options)Pide un arrastre. Devuelve un SensorDrag, o null cuando no puede empezar ningún arrastre.
cancel()Cancela el arrastre o la pulsación pendiente del provider.

activate() acepta:

OpciónSignificado
targetDónde cayó la entrada: un elemento dentro de un elemento arrastrable, o el id de un elemento arrastrable. Un elemento se comprueba como una pulsación real (asa, ningún hijo interactivo); un id se salta las comprobaciones.
pointDónde empieza el arrastre, en coordenadas del viewport. Por defecto: el centro del elemento arrastrable.
type'pointer' (por defecto): sigue puntos, hace la prueba de colisión con los destinos, desplaza automáticamente. 'keyboard': se mueve de ítem en ítem, hace visibles los destinos, devuelve el foco al terminar.
activationUna restricción ({ delay: 300 }), o un tipo de puntero ('touch') para usar la configurada. Por defecto: ninguna, el arrastre empieza de inmediato.
eventEl evento de entrada, que se pasa al canDrag del ítem (sin él, no se pregunta a canDrag). Un pointerType táctil lo convierte en un arrastre táctil (vibración).
onStart(drag)El arrastre empezó, tras su activación.
onEnd(drag)El arrastre terminó: abortado, soltado o cancelado — por quien sea.
onResume(drag)onBeforeDrop respondió 'continue': el arrastre continúa, y move, drop y cancel vuelven a aplicarse.

El SensorDrag que devuelve:

MiembroQué hace
id, typeEl id del ítem arrastrado, y 'pointer' o 'keyboard'.
state'pending' (esperando la activación), 'dragging' y luego 'ended'.
startedEl arrastre empezó, aunque haya terminado desde entonces.
move(point)La entrada se movió. Mientras está pendiente, comprueba la tolerancia; durante el arrastre, mueve el arrastre.
step(direction)Arrastres con teclado: un paso de flecha, 'up' | 'down' | 'left' | 'right'.
place({ list, index })Pone el ítem en una posición de una lista ordenable, como hacen los movimientos con teclado. Se resuelve a false cuando ninguna lista lo acepta ahí.
drop()Suelta. Se resuelve a true cuando el drop se completó, una vez que ha aterrizado.
cancel()Cancela: el ítem vuelve a su sitio, o se abandona una pulsación pendiente.

Las llamadas que ya no se aplican — un movimiento tras el final, un segundo drop() — se ignoran. Tipos completos: Sensores.

Para envolver un sensor integrado, ejecuta su setup con un contexto propio. Créalo con Object.create(context, { activate: { value: myActivate } }) en lugar de con un spread, para que los getters en vivo (status, drag, props) sigan funcionando.

Arrastres programáticos

@vue-dnd-kit/actions mueve ítems desde el código: botones de «Subir», menús, atajos de teclado, deshacer, recorridos de bienvenida, tests. Está construido sobre la API pública de sensores, y sus arrastres son reales: anunciados y animados, pasando por onBeforeDrop, onDrop, e.result y preventDefault(), bajo las reglas del provider (un arrastre a la vez, ítems desactivados, accept y group.put).

Live

Cargando la demo…

<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: una llamada, un movimiento

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 es el id de una lista ordenable. Sin él, el ítem se queda en su lista.
  • index es donde acaba el ítem, contado sin el propio ítem y ajustado a los límites de la lista. Sin él, el ítem va al final.
  • Se resuelve a true cuando el drop se completó, y a false cuando el ítem no está renderizado o está desactivado, ninguna lista lo acepta ahí o se rechazó el drop.
  • Las llamadas se ponen en cola: cada una espera a que el provider esté en reposo, para que no se pierda un segundo clic rápido. El barajado de la demo son cinco llamadas seguidas.
  • Se ejecuta como un arrastre con teclado, así que se anuncia y el foco se queda en el ítem.
  • Solo listas: las posiciones en un árbol también tienen profundidad.

Consulta Accesibilidad para una fila con botones de mover.

startDrag: un arrastre programado

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 se resuelve una vez renderizado el movimiento, drop una vez aterrizado el drop. El botón «Play a scripted drag» de la demo sube la última pista en pequeños pasos de moveTo y luego la suelta arriba. startDrag(id, { from: { x, y } }) agarra el ítem en un punto en lugar de en su centro.

Pasar un arrastre al teclado

startDrag(id, { type: 'keyboard' }) levanta el ítem como un arrastre con teclado. Tu código puede moverlo con await drag.step('down'), o dejárselo al usuario: el sensor de teclado conduce cualquier arrastre con teclado, lo haya empezado quien lo haya empezado. Así se hace un elemento de menú «Mover»:

Live

Cargando la demo…

<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' });
};

Las flechas mueven la fila de hueco en hueco, ArrowLeft y ArrowRight a la otra semana (las listas comparten un group). Cuando termina el arrastre, el foco vuelve a la fila. Las pistas de teclas bajo el tablero se iluminan mientras useDnD().active.value?.sensor es 'keyboard'.

Fuera de los componentes

useDnDActions() ejecuta un sensor, llamado actions, en el provider de arriba. Para conducir un provider desde un store o un entorno de tests, o con un nombre propio, crea las acciones en un sensor que ejecutes tú:

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]">

Luego await tour?.moveItem('card-1', { list: 'done' }) desde cualquier sitio. Durante el renderizado en el servidor no se arrastra nada: moveItem se resuelve a false y startDrag devuelve null.