Documentation/Guides
Glisser-déposer
Le glissement libre avec makeDraggable et les cibles de dépôt avec makeDroppable — des cibles typées qui acceptent certains glissements et en refusent d'autres, un plan d'appartement, des corbeilles, des dossiers dans une liste, et des éléments simples qu'une liste accueille.
Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais
Utilisez makeDraggable et makeDroppable pour tout ce qui n'est pas une liste triable : des
déchets dans des poubelles, des meubles sur un plan, des fichiers dans des dossiers, une
corbeille, un échiquier.
Chargement de la démo…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type Key } from '@vue-dnd-kit/core';
import Bin from './Bin.vue';
import { BINS, WASTE } from './waste';
// The tray takes everything back (no `accept`); the bins take what their rule allows.
const TRAY = { id: 'tray', name: 'To sort', color: '#42b883', accept: '*', rule: "'*'" };
/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>
<template>
<DnDProvider>
<div class="recycling">
<Bin :bin="TRAY" :items="itemsIn('tray')" tray @drop="drop('tray', $event)" />
<div class="bins">
<Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.recycling {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 600px;
margin: 0 auto;
}
.bins {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
@media (max-width: 560px) {
.bins {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>Un élément déplaçable
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDraggable } from '@vue-dnd-kit/core';
const props = defineProps<{ waste: { id: string; name: string; type: string } }>();
const el = useTemplateRef<HTMLElement>('el');
const { isDragging } = makeDraggable(el, {
id: () => props.waste.id, // read back as `active.id`
type: () => props.waste.type, // matched by the targets' `accept`
data: () => props.waste, // anything: `active.data`
});
</script>
<template>
<div ref="el" :class="{ dragging: isDragging }">{{ waste.name }}</div>
</template>
Passez l'id issu de vos données, sous forme de getter. L'id par défaut vient de useId() et
change pour chaque instance du composant : il ne signifie donc rien pour le reste de votre
application. Les options sont lues au moment où elles sont nécessaires : un getter ou une ref
les garde à jour.
| Option | Ce qu'elle fait |
|---|---|
id, data, type | Qui est glissé, ce qu'il transporte, et ce que compare l'accept des cibles. |
handle | Un sélecteur ou un élément à l'intérieur : lui seul démarre un glissement. Voir les poignées. |
disabled | Aucun glissement ne démarre. |
overlay | true (par défaut) : une copie suit le pointeur. Un composant : votre propre aperçu. false : l'élément se déplace lui-même (plus bas). |
axis, modifiers | Verrouiller le glissement sur 'x' ou 'y', l'aligner sur une grille, le garder dans des limites : voir les contraintes. |
keyboardMode | Comment les flèches le déplacent : au pixel (par défaut) ou de cible en cible (plus bas). |
activation, canDrag | Quand un appui devient un glissement : voir les capteurs. |
attributes | role, tabindex et aria-* pour le clavier et les lecteurs d'écran (true par défaut). |
| Renvoie | Ce que c'est |
|---|---|
isDragging | Cet élément est en cours de glissement. |
selected | Modifiable, lié à la sélection du provider. |
transform | Le décalage du glissement, pour overlay: false. |
id | L'id utilisé. |
Les boutons, les champs et les autres enfants interactifs ne démarrent pas de glissement au pointeur : ils continuent donc de fonctionner à l'intérieur d'un élément déplaçable. Toutes les options se trouvent dans l'API de makeDraggable.
Une cible de dépôt
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import { makeDroppable, type Key } from '@vue-dnd-kit/core';
import type { Bin, Waste } from './waste';
const props = defineProps<{ bin: Bin; items: Waste[] }>();
const emit = defineEmits<{ drop: [id: Key] }>();
const el = useTemplateRef<HTMLElement>('el');
const { isOver, isAccepting } = makeDroppable(el, {
id: props.bin.id,
accept: props.bin.accept, // 'glass', ['paper', 'cardboard'], a function…
onDrop: (event) => emit('drop', event.active.id),
});
</script>
<template>
<section ref="el" :class="{ accepting: isAccepting, over: isOver && isAccepting, refused: isOver && !isAccepting }">
<WasteItem v-for="waste in items" :key="waste.id" :waste="waste" />
</section>
</template>
| Renvoie | true quand |
|---|---|
isOver | Le glissement est au-dessus de cette cible, qu'elle l'accepte ou non. |
isAccepting | Quelque chose est en cours de glissement et cette cible l'accepte : mettez en valeur les cibles valides. |
placement | Pas un booléen : { top, right, bottom, left }, les moitiés de la cible où se trouve le pointeur, ou null quand elle n'est pas survolée. Utilisez-le pour « avant / après cette carte ». |
isOver sans isAccepting signifie que le glissement est au-dessus d'une cible qui le refuse.
Montrez-le, comme le font les poubelles ci-dessus : un dépôt à cet endroit est annulé et l'item
revient à sa place. Les poubelles qui ne peuvent pas accepter l'item s'estompent pendant son
trajet ; d'autres idées dans le style.
| Option | Ce qu'elle fait |
|---|---|
id, data | Relus sous la forme over.id et over.data. |
accept, acceptMode | Les glissements qu'elle accepte (section suivante). |
disabled | Elle n'accepte rien, et les glissements passent au-dessus d'elle. |
priority | La plus haute l'emporte quand des cibles se chevauchent sans être imbriquées (0 par défaut). |
onDragEnter, onDragLeave | Le glissement est entré ou sorti. |
onBeforeDrop, onDrop | Décider du dépôt, puis l'appliquer (plus bas). |
Les glissements qu'une cible accepte
Le type d'un élément déplaçable est comparé à l'accept d'une cible :
accept | Accepte |
|---|---|
omis ou '*' | Tout. |
'glass' | Les éléments déplaçables de type glass, et ceux sans type. |
['paper', 'cardboard'] | L'un ou l'autre type, et ceux sans type. |
({ active, items, types }) => boolean | Ce que dit votre fonction. |
Un élément déplaçable sans type passe toute règle sous forme de chaîne. type peut aussi être
un tableau (['paper', 'large']) : un seul type correspondant suffit. Une fonction décide selon
n'importe quel aspect du glissement. Le composteur ci-dessus lit les données des items, pas leur
type :
{
id: 'compost',
accept: ({ items }) => items.every((item) => (item.data as Waste).compostable),
}
accept est lu en direct, pendant que le glissement cherche sa cible : passez une ref pour le
changer.
Quand plusieurs items sont glissés à la fois, acceptMode indique combien doivent convenir :
'all' (par défaut) ou 'some'. Avec 'some', renvoyez depuis onBeforeDrop les ids à garder,
et les autres reviennent à leur place. Voir les décisions de dépôt.
Le dépôt
L'onDrop d'une simple cible de dépôt est l'endroit où vous modifiez vos données. La
bibliothèque ne déplace rien d'elle-même :
<script setup lang="ts">
/** Where each item is: the tray or a bin. */
const place = reactive<Record<string, string>>(Object.fromEntries(WASTE.map((waste) => [waste.id, 'tray'])));
const itemsIn = (bin: string) => WASTE.filter((waste) => place[waste.id] === bin);
const drop = (bin: string, id: Key) => (place[String(id)] = bin);
</script>
<template>
<Bin v-for="bin in BINS" :key="bin.id" :bin="bin" :items="itemsIn(bin.id)" @drop="drop(bin.id, $event)" />
</template>
| Champ de l'événement | Ce qu'il contient |
|---|---|
active | Le glissement : id, ids, items (chacun { id, data, type, item }), data, type. |
over | Cette cible : id, data, element, rect, accepted. |
pointer, delta | Où se trouve le pointeur, et de combien il s'est déplacé depuis le début. |
position | Où se trouve la boîte glissée dans cette cible (section suivante). |
source | Renseigné quand les items viennent d'une liste triable : { list, remove() }. |
active.ids contient tous les ids glissés, en commençant par celui qui a été saisi. L'ordre est
onBeforeDrop (décider), onDrop (appliquer), puis le drag-end du provider. Pour entendre
tous les glissements de la page au même endroit, utilisez useDnDMonitor (voir
le provider). Pour demander avant un dépôt, prolonger le glissement ou
n'accepter que certains items, voir les décisions de dépôt.
Le laisser là où il a été déposé
Un plan d'appartement, un canevas, un tableau : l'item reste là où vous le lâchez. Chaque
événement transporte position, le left / top qui place un item positionné en absolu là où
il a été déposé.
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Planner from './Planner.vue';
</script>
<template>
<DnDProvider>
<Planner />
</DnDProvider>
</template>const room = useTemplateRef<HTMLElement>('room');
makeDroppable(room, {
id: 'room',
onDrop: ({ active, position }) => {
const piece = find(active.id);
if (!piece || !position || !room.value) return;
// Clamped, so a piece let go half outside stays inside the walls.
piece.x = clamp(position.x, room.value.clientWidth - piece.width);
piece.y = clamp(position.y, room.value.clientHeight - piece.height);
piece.place = 'room';
},
});
<div ref="el" class="piece" :style="{ left: `${piece.x}px`, top: `${piece.y}px` }">…</div>
position est le coin supérieur gauche de la boîte glissée dans over, mesuré depuis le bord
de son padding, défilement compris : la bordure de la pièce, ses murs, ne compte pas. Il vaut
null au-dessus de rien.
active.rect et delta : cela ignore la bordure et le
défilement de la cible. Les grilles, les limites et le verrouillage d'axe pour ce genre de
tableaux se trouvent dans les contraintes.Sans overlay
Par défaut, une copie de l'élément suit le pointeur, et l'élément reste en place : c'est ainsi
que fonctionnent les meubles en réserve. Avec overlay: false, il n'y a pas de copie, et
l'élément se déplace lui-même grâce au transform que renvoie le glissement : ce sont les
meubles dans la pièce.
const inRoom = computed(() => props.piece.place === 'room');
const { isDragging, transform } = makeDraggable(el, {
id: () => props.piece.id,
overlay: computed(() => !inRoom.value), // a value or a ref, read when the drag starts
});
const style = computed(() => ({
left: `${props.piece.x}px`,
top: `${props.piece.y}px`,
...(transform.value && { transform: `translate3d(${transform.value.x}px, ${transform.value.y}px, 0)` }),
}));
positionest alors le coin de l'élément lui-même : le gestionnaire de dépôt reste le même.- L'élément est sous le pointeur pendant qu'il bouge ; pour trouver la cible, la bibliothèque
regarde à travers lui. Placez-le au-dessus de ses voisins pendant le glissement (
z-index). - Il défile avec la page, l'overlay non :
transformretranche ce dont son conteneur s'est déplacé depuis le début du glissement, si bien qu'il reste sous le pointeur quand la page défile. - Il se déplace à l'intérieur de son propre conteneur : un parent en
overflow: hiddenle rogne, et son contexte d'empilement décide de ce au-dessus de quoi il passe. L'overlay n'a aucun de ces deux problèmes.
Pour un aperçu personnalisé à la place de la copie, voir l'overlay.
Corbeilles
Un glissement qui commence dans une liste triable transporte e.source. Appelez remove() et la
liste retire les items :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Triage from './Triage.vue';
</script>
<template>
<DnDProvider>
<Triage />
</DnDProvider>
</template>const triage = (bin: 'done' | 'deleted') => (e: DnDEvent) => {
bins[bin].push(...e.active.items.map((entry) => entry.item as Notice));
e.source?.remove();
};
makeDroppable(doneZone, { id: 'done', onDrop: triage('done') });
makeDroppable(trashZone, { id: 'trash', onDrop: triage('deleted') });
e.active.items[i].itemest l'item de la liste lui-même. Gardez-le, et une annulation le remet là où il était.- L'
onDroppropre à la liste s'exécute alors avecto.index === -1, ete.resultcontient la liste sans les items. - L'overlay rétrécit dans la zone, puisque l'item n'est plus affiché nulle part.
- Une liste peut aussi retirer ce qui est déposé en dehors de tout : voir
dropOutside.
Delete
reste plus rapide pour beaucoup de gens : proposez-en un aussi.Déposer dans un item triable
Une ligne d'une liste triable peut aussi être une cible de dépôt : un dossier qui accepte des favoris, un groupe qui accepte des tâches. Placez un droppable sur l'élément de l'item :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Bookmarks from './Bookmarks.vue';
</script>
<template>
<DnDProvider>
<Bookmarks />
</DnDProvider>
</template>const el = useTemplateRef<HTMLElement>('el');
makeSortableItem(el, { item: () => props.folder, type: 'folder' });
const { isOver } = makeDroppable(el, {
accept: 'link',
onDrop: (e) => {
emit('add', e.active.items.map((entry) => entry.item as Link)); // Bookmarks adds them
e.source?.remove(); // take them out of the list they came from
},
});
Un dossier ouvert montre ses liens sous la forme d'une liste du même groupe, qui n'accepte que des liens : ils s'y trient, et on peut les faire glisser hors du dossier, vers le premier niveau ou dans un autre dossier.
Là où la liste trierait le glissement, une telle cible ne compte qu'au milieu de son item (de 25 à 75 % le long de l'axe de la liste). Là, le dépôt va dans l'item, et la liste ne bouge pas. Près des bords, la liste se trie comme d'habitude.
- Une cible dont l'
acceptrefuse le glissement est ignorée : la liste se trie au-dessus de l'item comme au-dessus de n'importe quel autre. Les dossiers n'acceptent que des liens : un dossier glissé se trie donc en passant les autres. - Une cible ne reçoit jamais son propre glissement.
- Au clavier, une flèche s'arrête d'abord sur le dossier ; l'appui suivant dans la même direction le dépasse.
- L'item n'est plus affiché après le dépôt : l'overlay rétrécit donc dans le dossier au lieu de revenir à sa place.
- Au-dessus du dossier, la liste garde l'emplacement de la ligne glissée là où il était : rien
ne bouge sous le pointeur. L'emplacement reçoit alors
data-dnd-placeholder="into": le dépôt n'y va pas, masquez-le donc si vous le souhaitez ([data-dnd-placeholder='into'] { opacity: 0 }).
Le gestionnaire de fichiers fait la même chose dans une grille, avec des dossiers imbriqués.
Des éléments déplaçables simples dans une liste
Une liste triable accueille les items glissés avec makeDraggable quand elle sait en faire un
item :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Agenda from './Agenda.vue';
import BlockChip from './BlockChip.vue';
const BLOCKS = [
{ title: 'Focus', minutes: 90, color: '#42b883' },
{ title: 'Meeting', minutes: 30, color: '#3b82f6' },
{ title: 'Lunch', minutes: 60, color: '#f59e0b' },
{ title: 'Gym', minutes: 45, color: '#ec4899' },
{ title: 'Break', minutes: 15, color: '#a1a1aa' },
];
</script>
<template>
<DnDProvider>
<div class="planner">
<aside class="blocks">
<h4>Blocks</h4>
<BlockChip v-for="block in BLOCKS" :key="block.title" :block="block" />
</aside>
<Agenda />
</div>
</DnDProvider>
</template>
<style scoped>
.planner {
display: flex;
justify-content: center;
gap: 24px;
}
.blocks {
display: flex;
flex: none;
flex-direction: column;
gap: 6px;
width: 140px;
}
h4 {
margin: 0 0 6px;
font: 600 11px/1 ui-monospace, Menlo, Consolas, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
@media (max-width: 480px) {
.blocks {
width: 112px;
}
}
</style>const { items } = makeSortableList(list, {
items: agenda,
createItem: (active) => ({ ...(active.data as Block), id: `entry-${++created}` }),
});
Pendant que l'élément déplaçable est au-dessus de la liste, l'item créé se trouve dans le
brouillon et votre template le rend comme n'importe quelle autre ligne : les horaires de l'agenda
le comptent déjà. Il devient réel au dépôt (mode: 'insert', from.list === null), et l'overlay
atterrit dessus.
- Renvoyez
undefinedpour refuser. Les listes sanscreateItemignorent les éléments déplaçables simples. - Donnez au nouvel item une clé à lui : le bloc reste dans la palette.
- Au clavier, une flèche vers la liste y fait entrer l'élément déplaçable, à sa hauteur, et les flèches suivantes le déplacent d'emplacement en emplacement.
Des fichiers depuis l'ordinateur
Les fichiers glissés depuis le bureau ou un gestionnaire de fichiers ne sont pas des glissements
de la bibliothèque : c'est le navigateur qui les transporte. Une cible de dépôt les accepte avec
external, et le dépôt les apporte dans e.external :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import AvatarDrop from './AvatarDrop.vue';
import Attachments from './Attachments.vue';
</script>
<template>
<DnDProvider>
<div class="form">
<AvatarDrop />
<Attachments />
</div>
</DnDProvider>
</template>
<style scoped>
.form {
display: flex;
flex-wrap: wrap;
align-items: stretch;
justify-content: center;
gap: 16px;
}
</style>const { isOver, isAccepting } = makeDroppable(el, {
external: { files: 'image/*', multiple: false },
onDrop: (e) => show(e.external!.files[0]!),
});
filesse lit comme<input accept>: des types MIME (image/*,application/pdf) et des extensions (.md).trueaccepte n'importe quel fichier, une fonction décide fichier par fichier.linksettextacceptent ce que glissent les autres applications et onglets,typesn'importe quel autre type MIME.- Le glissement se déroule comme un glissement du provider :
isOver,isAccepting, les événements et les annonces fonctionnent comme pour n'importe quel autre. Les cibles sansexternall'ignorent. Il n'y a pas d'overlay : le navigateur dessine l'image glissée. - Tant que les fichiers sont tenus au-dessus de la page, le navigateur ne donne que leurs types
(
e.external.fileTypes). Leurs noms, tailles et contenus arrivent avec le dépôt, et les extensions sont vérifiées à ce moment-là. Les dossiers sont ignorés ; avecmultiple: false, un glissement de plusieurs fichiers est refusé.
Les listes acceptent aussi des fichiers, des liens et du texte, et leur font de la place pendant qu'ils sont tenus. Les items peuvent aussi quitter la page, vers une autre application ou une autre de vos fenêtres : voir fichiers et autres applications.
<input type="file"> à côté de la zone, comme le fait la démo avec Browse.Clavier
Un élément déplaçable qui a le focus est saisi avec Space ou Enter, déposé avec les mêmes touches, et Escape annule. Entre les deux, les flèches le déplacent :
keyboardMode | Une flèche |
|---|---|
'pixels' (par défaut) | Le déplace de 25 px, le keyboard.step du provider. Avec Shift, quatre fois plus loin. Les meubles se déplacent ainsi. |
'droppables' | Saute vers la cible la plus proche dans cette direction qui l'accepte. Les déchets sautent de poubelle en poubelle, et ignorent celles qui les refusent. |
makeDraggable(el, { id: () => props.waste.id, type: () => props.waste.type, keyboardMode: 'droppables' });
Les touches peuvent être réassignées sur le provider. Plus de détails dans l'accessibilité.