Documentation/Guides
Listes qui partagent des items
Les listes d'un même groupe déplacent, copient et échangent des items — toute la colonne accepte les dépôts, pull et put décident de ce qui entre et sort, les palettes copient, et les listes contrôlées appliquent chaque dépôt à un store avec un historique d'annulation.
Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais
Chargement de la démo…
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Column from './Column.vue';
const columns = reactive([
{
id: 'backlog',
title: 'Backlog',
stories: [
{ id: 's1', title: 'Saved searches', points: 5 },
{ id: 's2', title: 'Crash on empty board', points: 2, bug: true },
{ id: 's3', title: 'CSV export', points: 3 },
{ id: 's4', title: 'Dark mode email', points: 2 },
],
},
{
id: 'sprint',
title: 'Sprint 14',
stories: [{ id: 's5', title: 'Onboarding tour', points: 8 }],
},
]);
const wholeColumn = ref(true);
</script>
<template>
<DnDProvider>
<div class="planning">
<label class="switch">
<input v-model="wholeColumn" type="checkbox" />
<code>dropzone</code>: the whole column takes drops
</label>
<div class="columns">
<Column v-for="column in columns" :key="column.id" :column="column" :whole-column="wholeColumn" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
.planning {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.switch {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
cursor: pointer;
}
.switch input {
accent-color: #42b883;
}
.switch code {
font: 12px/1 ui-monospace, Menlo, Consolas, monospace;
color: #42b883;
}
.columns {
display: flex;
justify-content: center;
gap: 14px;
width: 100%;
}
</style>Un groupe
Les listes qui portent le même nom de group s'échangent des items. Par défaut, chaque liste est
un groupe à elle seule.
<script setup lang="ts">
import { toRef, useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import StoryCard from './StoryCard.vue';
type Story = { id: string; title: string; points: number };
const props = defineProps<{ column: { id: string; title: string; stories: Story[] } }>();
const list = useTemplateRef<HTMLElement>('list');
const { items, isOver } = makeSortableList(list, {
id: props.column.id, // names the list in drop events
items: toRef(props.column, 'stories'), // a writable ref into the reactive column
group: 'planning', // same name: stories move between these lists
});
</script>
<template>
<section :class="{ over: isOver }">
<h3>{{ column.title }}</h3>
<ul ref="list">
<StoryCard v-for="story in items" :key="story.id" :story="story" />
</ul>
</section>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import Column from './Column.vue';
const columns = reactive([
{ id: 'backlog', title: 'Backlog', stories: [{ id: 's1', title: 'Saved searches', points: 5 }] },
{ id: 'sprint', title: 'Sprint 14', stories: [] },
]);
</script>
<template>
<Column v-for="column in columns" :key="column.id" :column="column" />
</template>
- Chaque colonne est ici un composant, comme le sont souvent les colonnes. Un composant peut aussi créer plusieurs listes, un Todo et un Done fixes côte à côte : chaque carte rejoint la liste dont l'élément la contient.
toRef(props.column, 'stories')est modifiable parce que le parent passe un objet réactif. Le dépôt écrit les deux tableaux, celui que la story a quitté et celui dans lequel elle est entrée, et les points des deux colonnes suivent.idapparaît dans les événements de dépôt sous la formefrom.listetto.list, et sert de clé àe.result, les nouveaux tableaux de chaque liste que le dépôt modifie.idetgroupsont lus une seule fois.- Les clés doivent être uniques dans tout le groupe.
L'onDrop d'une liste s'exécute pour les dépôts dans cette liste. Un déplacement de
backlog vers sprint appelle l'onDrop de la liste sprint, et son e.result contient les
nouveaux tableaux des deux listes.
Toute la colonne accepte les dépôts : dropzone
Une liste courte dans une colonne haute laisse un espace vide sous la dernière carte. Avec
dropzone, un autre élément accepte les dépôts pour la liste : ici, toute la colonne. Une story
relâchée sous la dernière atterrit à la fin. Les stories restent dans le <ul> ; seule la zone
qui accepte les dépôts s'agrandit. La démo entoure cette zone pendant le glissement :
désactivez-la, et seule la liste elle-même accepte la story.
const zone = useTemplateRef<HTMLElement>('zone');
const { items } = makeSortableList(list, {
items: toRef(props.column, 'stories'),
group: 'planning',
dropzone: zone, // or a getter: () => (wholeColumn.value ? zone.value : undefined)
});
dropzone, une liste vide a toujours besoin d'une taille pour accepter les dépôts :
donnez-lui une min-height.Ce qui entre et ce qui sort : pull et put
group accepte aussi un objet. pull indique ce qui peut quitter une liste, put ce qui peut y
entrer :
Chargement de la démo…
<script setup lang="ts">
import { reactive } from 'vue';
import { DnDProvider, type SortablePutContext } from '@vue-dnd-kit/core';
import RuleList from './RuleList.vue';
// Four lists of one group, `files`. What each gives and takes is its `group.pull` and `group.put`.
const shortlist = reactive({
id: 'shortlist',
title: 'Shortlist',
code: 'put: ({ keys }) => shortlist.files.length + keys.length <= 3',
refusal: 'full',
group: { name: 'files', put: ({ keys }: SortablePutContext) => shortlist.files.length + keys.length <= 3 },
files: [
{ id: 'pitch', name: 'pitch.pdf', kind: 'doc' as const },
{ id: 'mock', name: 'mockup.png', kind: 'image' as const },
],
});
const lists = reactive([
{
id: 'inbox',
title: 'Inbox',
code: '{ pull: true, put: false }',
refusal: 'put: false',
group: { name: 'files', pull: true, put: false },
files: [
{ id: 'logo', name: 'logo.svg', kind: 'image' as const },
{ id: 'brief', name: 'brief.pdf', kind: 'doc' as const },
{ id: 'hero', name: 'hero.png', kind: 'image' as const },
],
},
{
id: 'templates',
title: 'Templates',
code: "{ pull: 'clone', put: false }, sort: false",
refusal: 'put: false',
group: { name: 'files', pull: 'clone' as const, put: false },
sort: false,
files: [
{ id: 'invoice', name: 'invoice.doc', kind: 'doc' as const },
{ id: 'banner', name: 'banner.png', kind: 'image' as const },
],
},
{
id: 'images',
title: 'Images',
code: "put: 'image'",
refusal: 'images only',
group: { name: 'files', put: 'image' },
files: [],
},
shortlist,
]);
</script>
<template>
<DnDProvider>
<div class="rules">
<RuleList v-for="list in lists" :key="list.id" :list="list" />
</div>
</DnDProvider>
</template>
<style scoped>
.rules {
display: grid;
grid-template-columns: repeat(2, minmax(0, 260px));
gap: 12px;
justify-content: center;
}
@media (max-width: 480px) {
.rules {
grid-template-columns: minmax(0, 1fr);
}
}
</style>| Option | Valeurs |
|---|---|
pull | true (par défaut) : les items peuvent sortir. false : ils ne le peuvent pas. 'clone' : des copies sortent, les items restent. |
put | true (par défaut) : les items peuvent entrer. false : rien n'entre. Un type ou plusieurs ('image', ['image', 'video']) : les items de ces types entrent. Une fonction : vous décidez pour chaque glissement. |
// Inbox: gives, takes nothing.
group: { name: 'files', pull: true, put: false }
// Templates: gives copies, takes nothing, keeps its order.
group: { name: 'files', pull: 'clone', put: false }, sort: false
// Images: takes images only.
group: { name: 'files', put: 'image' }
Les types sont les options type des items glissés (makeSortableItem(el, { item, type })) ;
les items qui n'en ont pas comptent comme le nom du groupe. Avec des types dans put, chaque
item glissé doit avoir l'un d'eux (avec acceptMode: 'some', ceux qui en ont un entrent).
Une fonction put reçoit { keys, items, types, from, to } : les clés et les items glissés,
leurs types, la liste d'où ils viennent (null pour les éléments déplaçables simples) et cette
liste. Les déplacements à l'intérieur d'une liste sont toujours autorisés : l'Inbox trie
toujours ses propres fichiers.
Au-dessus d'une liste qui refuse le glissement, l'emplacement revient là où le glissement a
commencé : les listes montrent ainsi ce que ferait un dépôt, c'est-à-dire rien. Un dépôt à cet
endroit est annulé et l'item revient à sa place. L'isAccepting de la liste vaut false
pendant un tel glissement, et isOver reste true au-dessus d'elle : c'est ainsi que chaque
liste de la démo sait quoi dire.
L'emplacement rentre aussi chez lui au-dessus d'une cible de dépôt à part, hors des listes, comme
une corbeille : le dépôt à cet endroit revient à la cible, et les listes gardent l'item, sauf si
son onDrop le retire (event.source.remove()). Les cibles à l'intérieur des listes (un dépôt
dans un item) et autour d'elles (un panneau qui contient les listes) laissent l'emplacement où il
est.
const { items, isOver, isAccepting } = makeSortableList(el, { … });
const { active } = useDnD();
const verdict = computed(() => (active.value ? (isAccepting.value ? 'takes' : 'refuses') : null));
Limites
Une liste qui contient au plus trois fichiers, la Shortlist :
const shortlist = reactive({
files: [/* … */],
group: { name: 'files', put: ({ keys }: SortablePutContext) => shortlist.files.length + keys.length <= 3 },
});
Elle lit ses propres items quand un glissement le lui demande. Remplissez-la, et elle refuse le fichier suivant, avec la raison dans son en-tête.
Quand plusieurs items sélectionnés de types différents sont glissés, acceptMode indique si
'all' (par défaut) ou 'some' d'entre eux doivent passer put. Avec 'some', ceux qui
passent atterrissent et les autres reviennent à leur place. Voir le
multi-drag.
Un groupe par liste
Une liste appartient à un seul groupe : group prend un nom, jamais plusieurs. Les listes d'un
groupe partagent chaque glissement (leurs brouillons, ce qui est glissé, lesquelles l'acceptent),
donc les items ne passent jamais d'un groupe à l'autre. Quand les items ne doivent aller que dans
certains sens, mettez les listes dans un même groupe et laissez put décider selon from, la
liste d'où vient le glissement :
// Review takes tasks from both teams. A team takes tasks back from Review,
// never from the other team.
const fromReview = ({ from }: SortablePutContext) => from === 'review';
makeSortableList(review, { id: 'review', items: reviewTasks, group: 'tasks' });
makeSortableList(frontend, { id: 'frontend', items: frontendTasks, group: { name: 'tasks', put: fromReview } });
makeSortableList(backend, { id: 'backend', items: backendTasks, group: { name: 'tasks', put: fromReview } });
put: ['a', 'b'] nomme les groupes dont une liste prend
des items. Ici, un tableau dans put énumère les types des items qu'une liste accepte. Pour
prendre des items d'autres listes, mettez-les dans un même groupe et décidez selon from, comme
ci-dessus.Copier : palettes et touche Alt
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Palette from './Palette.vue';
import Sheet from './Sheet.vue';
</script>
<template>
<!-- The palette and the page are lists of one group: copies go from one to the other. -->
<DnDProvider>
<div class="builder">
<Palette />
<Sheet />
</div>
</DnDProvider>
</template>
<style scoped>
.builder {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 24px;
}
</style>makeSortableList(list, {
items: templates,
group: { name: 'blocks', pull: 'clone', put: false }, // copies go out, nothing comes in
sort: false, // its own order stays
clone: (block) => ({ ...block, id: crypto.randomUUID() }),
});
makeSortableList(list, {
items: blocks,
group: 'blocks',
clone: (block) => ({ ...block, id: crypto.randomUUID() }), // for Alt-drags
dropOutside: 'remove', // drag off the Page panel to delete
});
pull: 'clone'copie chaque glissement qui sort de la liste. L'original reste, et la copie voyage et atterrit.- Maintenir Alt pendant le glissement copie aussi, depuis n'importe quelle liste qui a
une fonction
clone: c'est elle qui fait la copie. Changez la touche avec lecopyModifierdu provider, ou désactivez-la avecnull. - Dans les deux cas, la liste d'où vient l'item a besoin de
clone. Elle doit renvoyer la copie avec une nouvelle clé. Sansclone, rien n'est copié. - L'événement de dépôt a
mode: 'copy', ete.itemssont les copies. Un composant d'overlay reçoit aussimode, pour afficher un « + » pendant la copie (voir l'overlay).
Échanger entre des listes
Deux listes d'un même groupe qui ont toutes deux behavior: 'swap' s'échangent des items :
l'item glissé prend la place de l'item survolé, et l'item survolé passe dans la liste de l'item
glissé. Il doit avoir le droit de sortir de sa liste (pull: true) et d'entrer dans l'autre
(put). Le planning d'astreinte des listes triables
fait des échanges entre ses deux colonnes.
Éléments déplaçables simples
Une liste accueille les items glissés avec makeDraggable (une puce, un modèle, un fichier issu
d'un arbre) quand elle a createItem. group.put décide aussi pour eux, avec from: null. Voir
des éléments déplaçables simples dans une liste.
Déposer à l'extérieur
Ce qui se passe quand un glissement se termine au-dessus d'aucune cible dépend du dropOutside
de la liste :
dropOutside | Les items |
|---|---|
'placeholder' (par défaut) | Atterrissent là où l'emplacement a été affiché en dernier. |
'cancel' | Reviennent à leur place. La valeur par défaut avec placeholder: 'indicator'. |
'remove' | Quittent leur liste : glisser au-dehors supprime. La page de la démo de palette le fait. |
'remove' demande d'abord à l'onBeforeDrop de la liste. Ensuite, l'onDrop de la liste
s'exécute avec to.index === -1, et e.result contient la liste sans les items. Une corbeille
qui accepte les dépôts fonctionne de la même façon : voir les
corbeilles.
Listes contrôlées
Les listes peuvent lire un store qui ne leur appartient pas : Pinia, un état serveur, des données
immuables. Passez items sous forme de getter (ou de computed, ou de ref en lecture seule) :
la bibliothèque n'y écrit jamais. Appelez e.preventDefault() dans onDrop et appliquez
e.result vous-même :
Chargement de la démo…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Table from './Table.vue';
import { createSeating } from './store';
const seating = createSeating();
const { state, undo } = seating;
</script>
<template>
<DnDProvider>
<div class="seating">
<div class="tables">
<Table id="a" title="Table A" :seating="seating" />
<Table id="b" title="Table B" :seating="seating" />
</div>
<section class="history">
<header>
<h4>History</h4>
<button type="button" :disabled="!state.history.length" @click="undo">Undo</button>
</header>
<ol>
<li v-for="(entry, index) in state.history" :key="state.history.length - index">{{ entry.label }}</li>
<li v-if="!state.history.length" class="none">Move a guest</li>
</ol>
</section>
</div>
</DnDProvider>
</template>
<style scoped>
.seating {
display: flex;
flex-direction: column;
gap: 12px;
width: 460px;
max-width: 100%;
margin: 0 auto;
}
.tables {
display: flex;
gap: 12px;
}
.history {
padding: 10px 12px;
border-radius: 12px;
background: color-mix(in srgb, currentColor 5%, transparent);
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
h4 {
margin: 0;
font: 600 11px/1 ui-monospace, Menlo, Consolas, monospace;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.55;
}
button {
padding: 5px 12px;
border: 0;
border-radius: 7px;
background: #42b883;
color: #fff;
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
button:disabled {
background: color-mix(in srgb, currentColor 12%, transparent);
color: inherit;
opacity: 0.5;
cursor: default;
}
ol {
display: flex;
flex-direction: column;
gap: 4px;
height: 66px;
margin: 8px 0 0;
padding: 0;
overflow-y: auto;
list-style: none;
font: 12px/1.4 ui-monospace, Menlo, Consolas, monospace;
}
li:first-child {
color: #42b883;
}
li.none {
color: inherit;
opacity: 0.5;
}
</style>const { items } = makeSortableList(list, {
id: props.id,
items: () => props.seating.state.tables[props.id]!, // read-only
group: 'seating',
onDrop: (e) => {
e.preventDefault(); // no write
props.seating.apply(e.result, `${e.items[0]?.name} → ${props.title}`);
},
});
const apply = (result: ReadonlyMap<Key, Guest[]>, label: string) => {
const before = [...result.keys()].map((id): [Key, Guest[]] => [id, state.tables[id]!]);
for (const [id, guests] of result) state.tables[id] = guests;
state.history.unshift({ label, before });
};
const undo = () => {
const last = state.history.shift();
for (const [id, guests] of last?.before ?? []) state.tables[id] = guests;
};
e.resultassocie chaque liste que le dépôt modifie à son nouveau tableau : les deux tables après un déplacement, une seule après un réordonnancement. Ce sont de nouveaux tableaux : le store peut donc garder les anciens pour l'annulation.- Le brouillon suit le store : appliquez
resultdansonDrop(ou avant qu'unonBeforeDropasynchrone ne se résolve), et l'overlay atterrit dans le nouvel ordre. preventDefault()saute l'écriture dans chaque liste deresult. S'il n'est appliqué nulle part, les listes restent telles qu'elles étaient et les items reviennent à leur place.- Les builds de développement avertissent quand le dépôt d'une liste en lecture seule est laissé à la bibliothèque : rien ne changerait.
Pour enregistrer sur un serveur avant que le dépôt n'atterrisse, ou pour demander d'abord à l'utilisateur, voir les décisions de dépôt. Toutes les options des listes se trouvent dans l'API de makeSortableList.