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.

Modifier cette page

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.

Live

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

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

OptionCe qu'elle fait
id, data, typeQui est glissé, ce qu'il transporte, et ce que compare l'accept des cibles.
handleUn sélecteur ou un élément à l'intérieur : lui seul démarre un glissement. Voir les poignées.
disabledAucun glissement ne démarre.
overlaytrue (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, modifiersVerrouiller le glissement sur 'x' ou 'y', l'aligner sur une grille, le garder dans des limites : voir les contraintes.
keyboardModeComment les flèches le déplacent : au pixel (par défaut) ou de cible en cible (plus bas).
activation, canDragQuand un appui devient un glissement : voir les capteurs.
attributesrole, tabindex et aria-* pour le clavier et les lecteurs d'écran (true par défaut).
RenvoieCe que c'est
isDraggingCet élément est en cours de glissement.
selectedModifiable, lié à la sélection du provider.
transformLe décalage du glissement, pour overlay: false.
idL'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

Bin.vue
<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>
Renvoietrue quand
isOverLe glissement est au-dessus de cette cible, qu'elle l'accepte ou non.
isAcceptingQuelque chose est en cours de glissement et cette cible l'accepte : mettez en valeur les cibles valides.
placementPas 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.

OptionCe qu'elle fait
id, dataRelus sous la forme over.id et over.data.
accept, acceptModeLes glissements qu'elle accepte (section suivante).
disabledElle n'accepte rien, et les glissements passent au-dessus d'elle.
priorityLa plus haute l'emporte quand des cibles se chevauchent sans être imbriquées (0 par défaut).
onDragEnter, onDragLeaveLe glissement est entré ou sorti.
onBeforeDrop, onDropDé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 :

acceptAccepte
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 }) => booleanCe 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 :

waste.ts
{
  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 :

index.vue
<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énementCe qu'il contient
activeLe glissement : id, ids, items (chacun { id, data, type, item }), data, type.
overCette cible : id, data, element, rect, accepted.
pointer, deltaOù se trouve le pointeur, et de combien il s'est déplacé depuis le début.
positionOù se trouve la boîte glissée dans cette cible (section suivante).
sourceRenseigné 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é.

Live

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>
Planner.vue
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';
  },
});
Piece.vue
<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.

Ne calculez pas l'emplacement à partir de 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.

Piece.vue
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)` }),
}));
  • position est 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 : transform retranche 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: hidden le 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 :

Live

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>
Triage.vue
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].item est l'item de la liste lui-même. Gardez-le, et une annulation le remet là où il était.
  • L'onDrop propre à la liste s'exécute alors avec to.index === -1, et e.result contient 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.
Au clavier, les flèches déplacent l'item d'une liste entre les emplacements de sa liste et les listes de son groupe ; au-delà de la fin des listes, l'appui suivant l'emmène jusqu'à la cible de dépôt dans cette direction, ici la corbeille, et la flèche opposée l'en ramène. Un bouton, un menu ou la touche 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 :

Live

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>
FolderRow.vue
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'accept refuse 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 :

Live

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>
Agenda.vue
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 undefined pour refuser. Les listes sans createItem ignorent 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 :

Live

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>
AvatarDrop.vue
const { isOver, isAccepting } = makeDroppable(el, {
  external: { files: 'image/*', multiple: false },
  onDrop: (e) => show(e.external!.files[0]!),
});
  • files se lit comme <input accept> : des types MIME (image/*, application/pdf) et des extensions (.md). true accepte n'importe quel fichier, une fonction décide fichier par fichier. links et text acceptent ce que glissent les autres applications et onglets, types n'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 sans external l'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 ; avec multiple: 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.

Les personnes qui utilisent le clavier ou un lecteur d'écran ne peuvent pas faire glisser de fichiers. Proposez un <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 :

keyboardModeUne 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é.