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.
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
| Sensor | Entrada |
|---|---|
pointerSensor | Rató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. |
keyboardSensor | Space 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:
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:
| Campo | El arrastre empieza… |
|---|---|
distance | Cuando el puntero se ha movido esos px. |
delay | Cuando la pulsación ha durado esos ms. Con distance también, lo que ocurra primero. |
tolerance | Con 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:
| Puntero | Por defecto |
|---|---|
| Ratón | Tras moverse 4 px. De inmediato al pulsar sobre un asa (handle), salvo que haya una restricción de ratón. |
| Lápiz | Tras moverse 4 px. |
| Táctil | Tras 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:
<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:
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:
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>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ón | Se pregunta | Úsala para |
|---|---|---|
disabled | En vivo, y se pone como aria-disabled | Ítems que no pueden moverse en absoluto: bloqueados, de solo lectura. |
canDrag | Una vez por pulsación, con su evento | Reglas sobre la entrada: una tecla mantenida, un botón, un tipo de puntero. |
handle | Donde 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:
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:
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:
/** 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:
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:
| Miembro | Qué es |
|---|---|
props | Las props del provider (activation, keyboard…). Léelas cuando las necesites: pueden cambiar. |
status | El estado de arrastre del provider, sea cual sea el sensor que lleve el arrastre. |
drag | El 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ón | Significado |
|---|---|
target | Dó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. |
point | Dó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. |
activation | Una restricción ({ delay: 300 }), o un tipo de puntero ('touch') para usar la configurada. Por defecto: ninguna, el arrastre empieza de inmediato. |
event | El 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:
| Miembro | Qué hace |
|---|---|
id, type | El id del ítem arrastrado, y 'pointer' o 'keyboard'. |
state | 'pending' (esperando la activación), 'dragging' y luego 'ended'. |
started | El 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.
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).
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
listes elidde una lista ordenable. Sin él, el ítem se queda en su lista.indexes 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
truecuando el drop se completó, y afalsecuando 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»:
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>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.