Documentation/Guides
L'overlay
Ce qui suit le pointeur pendant le glissement — le clone par défaut et comment le styliser, des composants d'overlay qui changent pendant le glissement, plusieurs items à la fois, sa taille et l'endroit où il est rendu.
Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais
Pendant le glissement, une boîte suit le pointeur : l'overlay. L'élément glissé lui-même reste là où il était, comme l'emplacement où il va atterrir. Au dépôt, l'overlay vole jusqu'à cet emplacement et disparaît.
L'overlay est téléporté dans body, en dehors de vos composants. Par défaut, il affiche un
clone de l'élément glissé.
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Expenses from './Expenses.vue';
</script>
<template>
<DnDProvider>
<Expenses />
</DnDProvider>
</template>Le clone par défaut
Le clone est pris à la saisie, avant que quoi que ce soit ne bouge. C'est une copie profonde de l'élément :
- Les valeurs des formulaires et les pixels des
<canvas>sont copiés. - Les styles calculés que l'élément perdrait en dehors de sa liste sont mis en ligne : polices
et couleurs héritées, styles qui s'appliquent via les ancêtres (
.list li { … }), largeurs des cellules de tableau. Une ligne de tableau arrive enveloppée dans un tableau à elle. - Les
idet les attributsdata-dnd-*sont retirés, et le clone estaria-hidden. - Les ondulations et autres effets d'appui des kits d'UI (Vuetify, Quasar, PrimeVue, Naive UI) sont laissés de côté.
Dans le tableau ci-dessus, une ligne dans laquelle vous avez tapé voyage avec son texte, son montant, sa coche et son graphique, dans un tableau à elle dont les cellules gardent les largeurs des vraies.
Marquez tout ce qui ne doit pas voyager avec data-dnd-no-clone : un bouton de menu, une
infobulle, une marque de sélection. Le bouton de suppression de chaque ligne reste en arrière :
<tr ref="el" class="row">
…
<td><button type="button" aria-label="Remove" data-dnd-no-clone @click="emit('remove')">×</button></td>
</tr>
Les fonds ne sont pas devinés
Le clone montre l'élément tel qu'il est. Une ligne qui n'a pas de fond à elle, parce que sa
liste le peint (un <li> dans un <ul> blanc, un v-list-item, une ligne de tableau), donne
un clone transparent. Donnez-lui le fond de la liste en CSS, ici sur les cellules :
<style scoped>
[data-dnd-overlay] .row td {
background: color-mix(in srgb, currentColor 4%, Canvas);
}
</style>
Le clone garde les classes et les attributs de l'élément, y compris l'attribut de scope de Vue : la règle fonctionne donc dans le style scoped de la ligne comme dans une feuille de style globale. L'autre solution est un fond sur la ligne elle-même. Les icônes et les tuiles faites pour flotter restent transparentes exprès.
Styliser la boîte
La boîte est l'élément qui porte data-dnd-overlay. Elle est en position: fixed, au-dessus
de tout (z-index: 9999), en pointer-events: none et inert, et sa valeur indique ce qu'elle
est en train de faire :
data-dnd-overlay | Quand |
|---|---|
"dragging" | Elle suit le pointeur. |
"dropping" | Elle atterrit sur son emplacement, revient à sa place, ou attend un onBeforeDrop asynchrone. |
Un aspect soulevé pendant le glissement, qui disparaît à l'atterrissage :
[data-dnd-overlay='dragging'] .row td {
box-shadow: 0 14px 20px -16px rgb(0 0 0 / 0.7);
}
La boîte se déplace avec transform et reçoit sa taille en width et height : stylisez ce
qu'elle contient, pas la boîte. Le clone la remplit, avec width: 100%, height: 100% et
transform: none posés sur sa racine : faites-le tourner ou grandir avec les propriétés rotate et
scale, que les transitions comme les keyframes animent. Des recettes dans les
animations.
Composants d'overlay
Pour afficher autre chose que le clone, passez un composant. Il est rendu dans la boîte et reçoit le glissement en props, qui changent pendant le glissement : où se trouve le glissement, si cet endroit l'accepte, si le dépôt copie, et le dépôt lui-même.
Chargement de la démo…
<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
const columns = ref([
{
id: 'Backlog',
tickets: [
{ id: 't1', title: 'Dark mode toggle', tag: 'UI', approved: false },
{ id: 't2', title: 'Retry failed uploads', tag: 'API', approved: true },
{ id: 't3', title: 'Keyboard shortcuts', tag: 'UI', approved: false },
],
},
{ id: 'Doing', tickets: [{ id: 't4', title: 'Faster search', tag: 'API', approved: true }] },
{ id: 'Done', tickets: [{ id: 't5', title: 'Onboarding tour', tag: 'UI', approved: true }] },
]);
</script>
<template>
<DnDProvider>
<div class="board">
<Column v-for="column in columns" :key="column.id" :column="column" />
</div>
</DnDProvider>
</template>
<style scoped>
.board {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 12px;
width: 100%;
max-width: 600px;
margin: 0 auto;
}
</style><script setup lang="ts">
import { computed } from 'vue';
import type { DnDStatus, DragItem, DragMode, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null; accepted: boolean; status: DnDStatus; mode: DragMode }>();
const ticket = computed(() => props.items[0]?.item as Ticket);
const state = computed(() => {
if (props.status === 'dropping') return props.over ? 'landed' : 'back'; // a cancel flies back
if (!props.over) return 'free';
return props.accepted ? 'ok' : 'refused';
});
</script>
<template>
<!-- Fills the overlay box, which takes the size of the slot. -->
<div class="ticket-overlay" :data-state="state">
{{ ticket.title }}
<span v-if="mode === 'copy'" class="copy">+ copy</span>
</div>
</template>
<style scoped>
.ticket-overlay { width: 100%; height: 100%; }
.ticket-overlay[data-state='refused'] { background: color-mix(in srgb, #ef4444 8%, Canvas); }
</style>
| Prop | Ce qu'elle contient |
|---|---|
active | Le glissement (id, ids, data, type, …). |
items | Tous les items glissés, en commençant par celui qui a été saisi : { id, item, data, type }. item est l'item de la liste pour les glissements triables. |
over | La cible sous le glissement ; pendant qu'un dépôt atterrit, la cible sur laquelle il atterrit. null au-dessus de rien et pendant qu'un glissement refusé ou annulé revient à sa place. L'over.id d'une liste triable est l'id de la liste : les colonnes ci-dessus utilisent leurs noms. |
accepted | Si over accepte le glissement : teintez l'overlay en rouge quand ce n'est pas le cas. |
status | 'dragging', puis 'dropping' pendant l'atterrissage. |
mode | 'move', 'copy' (Alt maintenu pendant un glissement depuis une liste avec clone, ou un glissement depuis une palette), 'swap', ou 'insert' (un élément déplaçable simple qui entre dans une liste). |
to | L'endroit où un dépôt mettrait les items maintenant : { list, index }, et dans un arbre aussi parentKey et depth. null quand il ne les mettrait dans aucune liste, par exemple au-dessus d'une cible simple. Voir où irait le dépôt. |
Les props sont des OverlayProps ; ne déclarez que celles que vous lisez. Passez le composant à
ce qui est glissé :
import TicketOverlay from './TicketOverlay.vue';
makeSortableList(listRef, { items: tickets, overlay: TicketOverlay }); // every item of the list
makeSortableItem(el, { item: () => props.ticket, overlay: TicketOverlay }); // this item
makeDraggable(el, { id: 'chip', overlay: TicketOverlay }); // a plain draggable
Un composant peut être passé tel quel, dans un shallowRef pour pouvoir le changer (même en plein
glissement : l'overlay change aussitôt), ou sous forme de fonction, que Vue traite comme un
composant fonctionnel : overlay: (props) => <TicketOverlay {...props} compact /> en TSX. Ne le
mettez jamais dans une ref profonde ni dans un reactive : Vue rendrait le composant réactif
et afficherait un avertissement.
La racine d'un composant d'overlay doit remplir la boîte (width: 100%; height: 100%) : la
boîte prend la taille de l'emplacement et l'anime au dépôt. Une taille fixe sur la racine garde
au contraire cette taille à l'intérieur de la boîte.
Le slot #overlay du provider
Pour chaque glissement qui n'a pas son propre composant, le slot du provider rend l'overlay. Définissez-le une fois, à côté de la racine de votre application, et chaque liste et chaque élément déplaçable de l'application en profite. Ici, les notes ont leur propre fantôme, et les photos se rabattent sur le clone :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Moodboard from './Moodboard.vue';
import NoteGhost from './NoteGhost.vue';
</script>
<template>
<DnDProvider>
<Moodboard />
<!--
The overlay of every drag in the app that has none of its own. Notes get a ghost
of their own; for photos the slot renders nothing, so they fall back to the clone.
-->
<template #overlay="{ items, status }">
<NoteGhost v-if="items[0]?.type.includes('note')" :items="items" :status="status" />
</template>
</DnDProvider>
</template><DnDProvider>
<RouterView />
<template #overlay="{ items, status }">
<NoteGhost v-if="items[0]?.type.includes('note')" :items="items" :status="status" />
</template>
</DnDProvider>
Le slot reçoit les mêmes props qu'un composant d'overlay. Quand il ne rend rien (le v-if
ci-dessus), le glissement se rabat sur le clone. Distinguez les glissements par leur type : le
type d'un item triable est le nom de son groupe, sauf s'il définit le sien
(makeSortableItem(el, { type })).
Cibles qui changent l'overlay
Une cible de dépôt peut dire ce que montre l'overlay pendant qu'un glissement qu'elle accepte la
survole. makeDroppable, les listes et les arbres acceptent un overOverlay (l'overlay d'une
liste ou d'un arbre sert à ses propres items). Au-dessus d'un dossier, la pastille
des messages glissés se transforme en ce que fera le dépôt :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Mailbox from './Mailbox.vue';
</script>
<template>
<!-- The mailbox reads the selection with `useDnD()`: it renders inside the provider. -->
<DnDProvider>
<Mailbox />
</DnDProvider>
</template>makeDroppable(el, {
id: () => props.folder.id,
data: () => props.folder,
overOverlay: MovePill,
onDrop: (event) => emit('move', event.active.ids),
});
Le composant de la cible reçoit les mêmes props que n'importe quel composant d'overlay, dont
over, et l'overlay propre au glissement (son composant, le slot du provider ou le clone) comme
slot par défaut. MovePill laisse le slot de côté, et remplace donc la pastille :
<script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, Over } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; over: Over | null }>();
// The folder under the drag: its `data`.
const folder = computed(() => props.over?.data as Folder | undefined);
</script>
<template>
<!-- Fills the box like any overlay component. -->
<div class="pill-box">
<span v-if="folder" class="pill">Move {{ items.length }} to {{ folder.name }}</span>
</div>
</template>
Rendez <slot /> pour garder l'overlay propre au glissement et l'envelopper. C'est ce que fait
Done dans le tableau des composants d'overlay ci-dessus : au-dessus de
Done, un ticket approuvé garde son propre overlay, Done le tamponne, et un ticket venu d'une
autre colonne reçoit des confettis quand il est déposé.
makeSortableList(listRef, {
items: tickets,
group: { name: 'release', put },
overlay: TicketOverlay, // the tickets' own, in every column
overOverlay: strict ? DoneOverlay : undefined, // Done's, around it
});
<template>
<div class="done">
<slot /> <!-- TicketOverlay -->
<span class="stamp">Shipped</span>
<span v-if="status === 'dropping' && arrives" class="confetti">…</span>
</div>
</template>
- Il ne s'affiche que tant que la cible accepte le glissement, et reste pendant que le dépôt atterrit sur elle. Quitter la cible ramène l'overlay propre au glissement.
- Le changement se fait en fondu enchaîné. L'
overlayTransitiondu provider en décide :falsechange aussitôt, un objet donne les props d'une<Transition>autour du contenu (name,mode, hooks JS pour GSAP ou motion). Sansmode: 'out-in', les deux s'affichent un instant : donnez à celui qui partposition: absolutedans.<name>-leave-active. - Le dépôt attend les confettis : les animations et transitions CSS qui tournent dans l'overlay pendant qu'il atterrit vont jusqu'au bout, pendant deux secondes au plus.
- Un glissement avec
overlay: falsen'a d'overlay au-dessus d'aucune cible.
Les dossiers font aussi atterrir la pilule à leur façon, dans l'icône du dossier : une cible peut
aussi avoir une overDropAnimation. Voir les animations.
Où irait le dépôt : to
to indique à un overlay où un dépôt mettrait les items maintenant, comme le dira le to de
l'événement de dépôt : la liste et l'index dans celle-ci, et dans un arbre le parent et la
profondeur. Il change à mesure que l'emplacement se déplace. Une page de ce menu de site indique
dans quelle section elle irait, et à quelle ligne :
Chargement de la démo…
<script setup lang="ts">
import { provide, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import MenuTree from './MenuTree.vue';
import { MENU, site } from './menu';
// Provided above the provider: its overlay renders under it, so the overlay sees it too.
provide(MENU, ref(site()));
</script>
<template>
<DnDProvider>
<MenuTree />
</DnDProvider>
</template><script setup lang="ts">
import { computed } from 'vue';
import type { DragItem, DropDestination } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[]; to: DropDestination | null }>();
const page = computed(() => props.items[0]!.item as MenuNode);
// `parentKey` is the section the page would go into, `null` at the top level.
const where = computed(() => (props.to ? pathOf(menu.value, props.to.parentKey) : null));
</script>
<template>
<div class="move">
<div class="row">{{ page.title }}</div>
<p v-if="to && where" class="where">Move to <b>{{ where }}</b>, row {{ to.index + 1 }}</p>
</div>
</template>
En dehors de toute cible, to reste la place de l'emplacement : par défaut, un dépôt à cet endroit
laisse les items là où se trouve l'emplacement (dropOutside: 'placeholder'). Il vaut null
quand le dépôt ne les mettrait dans aucune liste : au-dessus d'une cible simple, au-dessus d'une
liste qui les refuse, à l'extérieur avec dropOutside: 'cancel' ou 'remove'.
Ce qui l'emporte
Pour chaque glissement, le premier qui s'applique :
- L'
overOverlayde la cible de dépôt (celui d'une liste ou d'un arbre aussi), tant que le glissement la survole et qu'elle l'accepte. Ce qui s'afficherait sinon est son slot par défaut. - L'
overlaypropre à l'item (makeSortableItem,makeDraggable). - L'
overlayde sa liste ou de son arbre. - Le slot
#overlaydu provider, sauf s'il ne rend rien. - Le clone par défaut. Dans un multi-drag, une colonne ou une pile de clones : voir le multi-drag.
Le fait qu'un glissement ait un overlay ou non (overlay: false) est décidé à son début. Le
composant affiché est lu en direct : une ref changée en plein glissement le remplace, tout comme
le fait d'entrer dans une cible qui a son propre overlay ou d'en sortir.
DragPreview : le clone dans votre mise en page
DragPreview rend le clone de la bibliothèque pour un item glissé. Un composant d'overlay peut
en disposer plusieurs, ou en encadrer un. Cliquez sur des livres pour les choisir, puis faites-en
glisser un : l'overlay déploie les vrais clones en éventail.
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Shelf from './Shelf.vue';
</script>
<template>
<DnDProvider>
<Shelf />
</DnDProvider>
</template><script setup lang="ts">
import { computed } from 'vue';
import { DragPreview, type DragItem } from '@vue-dnd-kit/core';
const props = defineProps<{ items: DragItem[] }>();
const fan = computed(() =>
props.items.slice(0, 4).map((entry, index) => ({
entry,
angle: index === 0 ? 0 : (index % 2 ? -1 : 1) * Math.ceil(index / 2) * 10,
})),
);
</script>
<template>
<div class="fan">
<div v-for="{ entry, angle } in fan" :key="entry.id" class="card" :style="{ transform: `rotate(${angle}deg)` }">
<DragPreview :id="entry.id" class="face" :style="{ width: '100%', height: '100%' }">
<div class="blank" /> <!-- an item without a clone -->
</DragPreview>
</div>
<span v-if="items.length > 1" class="count">{{ items.length }}</span>
</div>
</template>
- Des clones existent pour l'item saisi et les autres à côté de lui dans l'ordre du dépôt,
autant que l'indique l'option
overlayPreviewsdu provider (5 par défaut,'all'pour tous), s'ils sont rendus au moment de la saisie. Pour tout autre item,DragPreviewrend son slot par défaut. - Un aperçu a la taille de son item au moment de la saisie, posée en ligne. Dimensionnez-le avec
style, qui est transmis (une classe ne peut pas la remplacer), comme ci-dessus. - Dans un multi-drag triable (
multiple: 'group'ou'gaps'), chaque aperçu atterrit au dépôt sur l'emplacement de son propre item. Faites pivoter ses enveloppes avectransform(pas la propriétérotate) : l'atterrissage l'annule en chemin. Mettez l'apparence de l'item (coins, ombre) sur l'aperçu lui-même, comme le faitclass="face": les enveloppes restent dans la boîte.
Le guide du multi-drag a un overlay qui affiche un compteur et les titres au lieu des clones.
Taille : overlayFit
Au-dessus d'une liste aux tailles différentes, la boîte prend la taille de l'emplacement où elle
va atterrir. L'overlayFit du provider le change ; la page DnDProvider
a une démo avec une liste étroite et une liste large.
overlayFit (provider) | La boîte |
|---|---|
'slot' (par défaut) | Prend la taille de l'emplacement où elle va atterrir, en s'y ajustant en douceur. Au-dessus de la liste large, elle s'élargit. |
'source' | Garde la taille de l'élément glissé. |
Avec 'slot', la boîte se redimensionne autour du pointeur, qui reste à la même fraction de la
boîte. Le contenu n'est jamais mis à l'échelle : il se remet en page à la nouvelle taille, si
bien qu'une rangée flex comme les tâches de cette démo a l'air juste à n'importe quelle largeur. Un
emplacement stylé beaucoup plus petit que l'item, comme une fine ligne, ne rétrécit pas la boîte
sur cet axe.
Un dépôt fait la même chose avec l'un ou l'autre réglage : la boîte vole jusqu'à l'élément d'arrivée et s'anime vers sa taille, et le contenu se remet en page en chemin. Quand un item a un aspect différent dans deux listes (une puce compacte dans une palette, une carte complète sur un tableau), le clone de la source se remet en page à la taille de la cible, et peut y paraître écrasé ou étiré. Deux façons de garder un bon rendu :
- Donnez à l'item une mise en page qui fonctionne aux deux tailles, comme les lignes de tâches, ou la même taille dans chaque liste où il peut atterrir.
- Rendez l'aspect de la cible dans un composant d'overlay. Il reçoit
over: il peut donc prendre l'aspect de la liste qu'il survole, et il remplit la boîte à n'importe quelle taille.
Où il est rendu : overlayTo
Le provider téléporte l'overlay dans body. Un <dialog> modal (ouvert avec showModal()) se
trouve sur la top layer, au-dessus de tout ce qui est dans body : un overlay à cet endroit
serait donc caché en dessous. Pointez overlayTo vers un sélecteur ou un élément à l'intérieur
du dialog. La page DnDProvider a un dialog
pour l'essayer avec et sans :
<script setup lang="ts">
const dialog = useTemplateRef<HTMLDialogElement>('dialog');
const open = ref(false);
// While the dialog is open, the overlay renders inside it.
const overlayTo = computed(() => (open.value ? '#columns-dialog' : 'body'));
const show = () => {
dialog.value?.showModal();
open.value = true;
};
</script>
<template>
<DnDProvider :overlay-to="overlayTo">
<button type="button" @click="show">Edit columns</button>
<dialog id="columns-dialog" ref="dialog" @close="open = false">
<ColumnList v-model:columns="columns" />
</dialog>
</DnDProvider>
</template>
- La cible doit se trouver dans la page quand le provider rend l'overlay, après son montage. Un
<dialog>fermé s'y trouve : il est seulement masqué. Revenez àbodytant qu'il est fermé, pour que l'overlay des autres glissements s'affiche. - La boîte est en
position: fixed. Untransform, unfilterou uncontainsur la cible ou ses ancêtres en fait le cadre de la boîte au lieu du viewport : évitez-les donc sur le dialog. - Les styles du clone sont calculés pour l'endroit où il est affiché : dans un shadow root,
pointez
overlayToà l'intérieur et le clone garde les styles du shadow root.
Pas d'overlay
makeDraggable(el, { overlay: false }) n'a pas de boîte du tout : l'élément lui-même se déplace,
grâce au transform renvoyé.
const { transform } = makeDraggable(el, { overlay: false });
const style = computed(() =>
transform.value ? { translate: `${transform.value.x}px ${transform.value.y}px` } : undefined,
);
Voir le glisser-déposer. La façon dont atterrit
un glissement avec une boîte dépend de sa dropAnimation : celle de la cible de dépôt (overDropAnimation), celle de
l'item (ou de sa liste) ou celle du provider. Voir les animations.