Documentation/Guides
Capteurs et glissements programmatiques
Comment une entrée devient un glissement — les capteurs pointeur et clavier, les contraintes d'activation, canDrag, vos propres capteurs, et les glissements que vous scriptez avec @vue-dnd-kit/actions.
Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais
Un capteur transforme des entrées en glissements. Il dit seulement au provider « fais glisser cet item depuis ici », puis où l'entrée s'est déplacée, et si elle a déposé ou annulé. Le provider fait le reste, de la même façon pour chaque capteur : un glissement à la fois, refus des items désactivés, contraintes d'activation, tri, défilement automatique, annonces, Escape et tout le dépôt.
Les capteurs intégrés
| Capteur | Entrée |
|---|---|
pointerSensor | Souris, toucher et stylet, via les Pointer Events. Il capture le pointeur, empêche la page de sélectionner du texte ou de défiler sous un glissement tactile, et avale le clic qui suit un glissement. |
keyboardSensor | Space et Enter saisissent et déposent, les flèches déplacent, Escape annule — n'importe quel glissement, y compris au pointeur. |
Les deux tournent par défaut. Les sensors du provider remplacent la liste :
<DnDProvider :sensors="[pointerSensor, keyboardSensor, joystick.sensor]">
Pour désactiver les glissements au clavier, préférez :keyboard="false" au retrait de
keyboardSensor : le capteur clavier est aussi ce qui annule n'importe quel glissement avec
Escape. Ses touches se réassignent avec keyboard.keys (voir
l'accessibilité).
Contraintes d'activation
Un appui ne glisse pas tout de suite : il attend sa contrainte d'activation, pour qu'un clic reste un clic et qu'un balayage puisse toujours faire défiler la page. Changez les règles de cette liste et appuyez sur une ligne :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Inbox from './Inbox.vue';
</script>
<template>
<DnDProvider>
<Inbox />
</DnDProvider>
</template>Une contrainte a trois champs :
| Champ | Le glissement démarre… |
|---|---|
distance | Une fois que le pointeur s'est déplacé de ce nombre de px. |
delay | Une fois que l'appui a duré ce nombre de ms. Avec distance aussi, au premier des deux. |
tolerance | Avec delay : jusqu'où le pointeur peut bouger entre-temps. Au-delà, l'appui abandonne, pour que la page puisse défiler. 5 px par défaut quand il n'y a pas de distance. |
Sans contrainte à vous, chaque type de pointeur a la sienne par défaut :
| Pointeur | Par défaut |
|---|---|
| Souris | Après un déplacement de 4 px. Tout de suite quand l'appui est sur une poignée (handle), sauf si une contrainte souris est définie. |
| Stylet | Après un déplacement de 4 px. |
| Toucher | Après un appui de 200 ms. Bouger de plus de 5 px avant cela abandonne, et la page défile à la place. |
Où les définir
Par type de pointeur — mouse, touch, pen — sur le provider, une zone de glissement ou
l'item. Pour chaque type, celle de l'item l'emporte, puis celle de sa zone, puis celle du
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 } } });
Les options sont lues quand un appui commence : un getter ou une ref les change donc en direct, comme le fait la démo. Les glissements au clavier démarrent tout de suite. Votre propre capteur demande sa propre contrainte, ou celle configurée pour un type de pointeur.
Un anneau d'appui long
Pendant qu'un appui attend, useDnD().status vaut 'activating' et activation.progress va de
0 à 1 — selon le temps avec delay, selon la distance parcourue avec distance. La démo dessine
un anneau là où l'appui a commencé :
<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>
Donnez à l'anneau pointer-events: none : il se trouve sous le pointeur, et le glissement teste
les collisions avec ce qui s'y trouve. Un appui qui abandonne — déplacé au-delà de sa
tolerance, ou relâché trop tôt — ramène status directement à 'idle'.
Les poignées démarrent tout de suite
Avec une handle, seule la poignée démarre un glissement, et un appui de souris dessus glisse
tout de suite : saisir une poignée ne laisse rien à distinguer. Un appui relâché là où il a
commencé reste un clic : la poignée peut donc ouvrir un menu sur click (le pointeur n'est
capturé qu'une fois qu'il bouge). Un toucher attend toujours son appui long, sauf si la poignée
ne peut pas faire défiler la page. Donnez-lui
touch-action: none et une courte distance à l'item :
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 } },
});
Une contrainte souris définie sur l'item, sa zone ou le provider s'applique aussi aux poignées.
Faites de la poignée un <button>, comme le fait la démo : elle prend le focus, et un glissement
au clavier démarre donc aussi depuis elle.
Seulement certains appuis
canDrag est un filtre de plus sur les éléments déplaçables et les items triables. Il reçoit
l'événement d'entrée et répond true ou false pour cet appui. Ici, les glissements au pointeur
demandent Shift ; le clavier glisse comme d'habitude :
Chargement de la démo…
<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 | Interrogée | Utilisez-la pour |
|---|---|---|
disabled | En direct, et posée en aria-disabled | Les items qui ne peuvent pas bouger du tout : verrouillés, en lecture seule. |
canDrag | Une fois par appui, avec son événement | Les règles qui portent sur l'entrée : une touche maintenue, un bouton, un type de pointeur. |
handle | Là où l'appui se pose | Les items qui ne démarrent un glissement que depuis une partie. |
canDrag est interrogé avant la contrainte d'activation, avec le pointerdown ou le keydown.
Un type de pointeur est une règle de plus qu'il peut poser :
// Mouse and pen drag; a touch on the row always scrolls the page.
canDrag: (event) => !(event instanceof PointerEvent && event.pointerType === 'touch'),
Les enfants interactifs — boutons, champs, cases à cocher, champs éditables — ne démarrent jamais
de glissement au pointeur : ils continuent donc de fonctionner à l'intérieur d'un élément
déplaçable, comme la case à cocher et le menu du responsable dans la démo. Avec une handle,
seule la poignée en démarre un.
Votre propre capteur
Une manette, un joystick à l'écran, des commandes vocales : tout ce qui peut désigner un item et bouger peut piloter un glissement. Cette manette est un capteur. Maintenez A au-dessus d'un héros pour le saisir, dirigez avec la croix, A de nouveau le dépose, B annule. La souris et le clavier fonctionnent toujours à côté :
Chargement de la démo…
<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 donne au capteur son nom et un setup qui reçoit le côté du provider, une fois par
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;
};
},
});
Un appui demande un glissement au provider avec activate(). La cible est vérifiée comme pour un
vrai appui : ce doit être un élément déplaçable de ce provider, non désactivé, et pas un bouton à
l'intérieur de l'un d'eux :
/** 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
};
Ensuite, le glissement bouge avec la croix, et A ou B le terminent :
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();
Ajoutez le capteur aux sensors du provider, comme le fait la démo, ou faites-le tourner tant
qu'un composant existe avec useSensor(joystick.sensor). Les points sont en coordonnées du
viewport : la démo garde son curseur dans les coordonnées de la scène, pour qu'il défile avec la
page, et le convertit dans point().
Ce que reçoit le capteur
setup(context) reçoit le côté du provider :
| Membre | Ce que c'est |
|---|---|
props | Les props du provider (activation, keyboard…). Lisez-les au moment où vous en avez besoin : elles peuvent changer. |
status | Le statut de glissement du provider, quel que soit le capteur qui mène le glissement. |
drag | Le glissement en cours du provider, en attente ou actif, quel que soit le capteur qui l'a démarré. null quand il n'y en a pas. |
activate(options) | Demande un glissement. Renvoie un SensorDrag, ou null quand aucun glissement ne peut démarrer. |
cancel() | Annule le glissement ou l'appui en attente du provider. |
activate() accepte :
| Option | Signification |
|---|---|
target | Là où l'entrée s'est posée : un élément à l'intérieur d'un élément déplaçable, ou l'id d'un élément déplaçable. Un élément est vérifié comme un vrai appui (poignée, pas d'enfant interactif) ; un id saute les vérifications. |
point | Là où le glissement commence, en coordonnées du viewport. Par défaut : le centre de l'élément déplaçable. |
type | 'pointer' (par défaut) : suit des points, teste les collisions avec les cibles, défile automatiquement. 'keyboard' : se déplace d'item en item, rend les cibles visibles, rend le focus à la fin. |
activation | Une contrainte ({ delay: 300 }), ou un type de pointeur ('touch') pour utiliser celle qui est configurée. Par défaut : aucune, le glissement démarre tout de suite. |
event | L'événement d'entrée, transmis au canDrag de l'item (sans lui, canDrag n'est pas interrogé). Un pointerType tactile en fait un glissement tactile (vibrations). |
onStart(drag) | Le glissement a démarré, après son activation. |
onEnd(drag) | Le glissement est terminé : interrompu, déposé ou annulé — par qui que ce soit. |
onResume(drag) | onBeforeDrop a répondu 'continue' : le glissement continue, et move, drop et cancel s'appliquent de nouveau. |
Le SensorDrag qu'elle renvoie :
| Membre | Ce qu'il fait |
|---|---|
id, type | L'id de l'item glissé, et 'pointer' ou 'keyboard'. |
state | 'pending' (en attente de l'activation), 'dragging', puis 'ended'. |
started | Le glissement a démarré, même s'il s'est terminé depuis. |
move(point) | L'entrée s'est déplacée. En attente, vérifie la tolérance ; pendant le glissement, déplace le glissement. |
step(direction) | Glissements au clavier : un pas de flèche, 'up' | 'down' | 'left' | 'right'. |
place({ list, index }) | Place l'item à une position d'une liste triable, comme le font les déplacements au clavier. Se résout à false quand aucune liste ne l'accepte à cet endroit. |
drop() | Dépose. Se résout à true quand le dépôt a abouti, une fois qu'il a atterri. |
cancel() | Annule : l'item revient à sa place, ou un appui en attente est abandonné. |
Les appels qui ne s'appliquent plus — un déplacement après la fin, un second drop() — sont
ignorés. Les types complets : Capteurs.
setup avec un contexte à vous. Créez-le avec
Object.create(context, { activate: { value: myActivate } }) plutôt qu'avec une décomposition,
pour que les getters en direct (status, drag, props) continuent de fonctionner.Glissements programmatiques
@vue-dnd-kit/actions déplace des items depuis le code : boutons « Monter », menus, raccourcis
clavier, annulation, visites guidées, tests. Il est construit sur l'API publique des capteurs, et
ses glissements sont de vrais glissements : annoncés et animés, passant par onBeforeDrop,
onDrop, e.result et preventDefault(), sous les règles du provider (un glissement à la fois,
items désactivés, accept et group.put).
Chargement de la démo…
<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 : un appel, un déplacement
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
listest l'idd'une liste triable. Sans lui, l'item reste dans sa liste.indexest l'endroit où l'item finit, compté sans l'item lui-même, et ramené dans les limites de la liste. Sans lui, l'item va à la fin.- Il se résout à
truequand le dépôt a abouti, àfalsequand l'item n'est pas rendu ou est désactivé, qu'aucune liste ne l'accepte à cet endroit, ou que le dépôt a été refusé. - Les appels sont mis en file : chacun attend que le provider soit au repos, pour qu'un second clic rapide ne soit pas perdu. Le mélange de la démo, ce sont cinq appels d'affilée.
- Il s'exécute comme un glissement au clavier : il est donc annoncé et le focus reste sur l'item.
- Listes uniquement : les positions dans un arbre ont aussi une profondeur.
Voir l'accessibilité pour une ligne avec des boutons de déplacement.
startDrag : un glissement scripté
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 résout une fois le déplacement rendu, drop une fois le dépôt atterri. Le bouton
« Play a scripted drag » de la démo fait remonter la dernière piste par petits pas de moveTo,
puis la dépose en haut. startDrag(id, { from: { x, y } }) saisit l'item à un point donné au lieu
de son centre.
Confier un glissement au clavier
startDrag(id, { type: 'keyboard' }) soulève l'item comme un glissement au clavier. Votre code
peut le déplacer avec await drag.step('down'), ou le laisser à l'utilisateur : le capteur
clavier pilote chaque glissement au clavier, peu importe qui l'a démarré. Cela donne un élément
de menu « Déplacer » :
Chargement de la démo…
<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' });
};
Les flèches déplacent la ligne d'emplacement en emplacement, ArrowLeft et
ArrowRight vers l'autre semaine (les listes partagent un group). Quand le
glissement se termine, le focus revient sur la ligne. Les indications de touches sous le tableau
s'éclairent tant que useDnD().active.value?.sensor vaut 'keyboard'.
En dehors des composants
useDnDActions() fait tourner un capteur, nommé actions, dans le provider au-dessus. Pour
piloter un provider depuis un store ou un harnais de test, ou sous un nom à vous, créez les
actions dans un capteur que vous faites tourner vous-même :
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]">
Ensuite, await tour?.moveItem('card-1', { list: 'done' }) depuis n'importe où. Pendant le rendu
serveur, rien ne glisse : moveItem se résout à false et startDrag renvoie null.