Documentation/Vue d'ensemble
Tableau de projet
Un tableau produit dans l'esprit de Linear et Trello — des colonnes que vous réordonnez, des limites WIP, une question avant Done, la sélection multiple avec une barre de sélection, des filtres à travers lesquels les glissements fonctionnent, une corbeille avec Undo, et un panneau de carte dont les boutons Status jouent de vrais glissements.
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 { provide } from 'vue';
import { DnDProvider, type Active, type AnnouncedPosition, type Announcements } from '@vue-dnd-kit/core';
import Board from './Board.vue';
import { BOARD, createBoard, type Card, type Column } from './store';
// Provided above the provider: the overlay it renders reads the board too.
const board = createBoard();
provide(BOARD, board);
// Screen readers hear cards and columns by name, not by id.
const subject = (active: Active) => {
const item = active.items[0]?.item;
if (active.type.includes('column')) return `the ${(item as Column).title} column`;
if (active.ids.length > 1) return `${active.ids.length} cards`;
return `${(item as Card).id}, ${(item as Card).title}`;
};
/** Where it is: "Todo, position 2 of 6." for a card, "Place 2 of 4." for a column. */
const where = (position: AnnouncedPosition | undefined) => {
if (!position) return '';
const column = board.column(position.list);
if (!column) return `Place ${position.index + 1} of ${position.length}.`;
return `${column.title}, position ${position.index + 1} of ${position.length}.`;
};
const announcements: Partial<Announcements> = {
onDragStart: ({ active }, { position }) => {
const keys = active.sensor === 'keyboard' ? ' Arrow keys move it, Space drops it, Escape cancels.' : '';
return `Picked up ${subject(active)}. ${where(position)}${keys}`;
},
onDragOver: ({ over }, { position }) => {
if (over?.id === 'trash') return 'Over the trash. Drop to delete.';
const column = over ? board.column(over.id) : undefined;
if (column && !over!.accepted) return `${column.title} is full: its limit is ${column.limit} cards.`;
return position ? where(position) : undefined;
},
onDragEnd: ({ active, over }, { position }) =>
over?.id === 'trash' ? `Deleted ${subject(active)}. Undo is in the notice.` : `Dropped ${subject(active)}. ${where(position)}`,
onDragCancel: ({ active }, { position }) => `${subject(active)} went back. ${where(position)}`,
onDragResume: ({ active }) => `Not dropped: still holding ${subject(active)}.`,
};
</script>
<template>
<!--
Space picks a card up and drops it: Enter is left to open the card. Touch drags vibrate
over a column that takes them, and when a drop is refused.
-->
<DnDProvider
:keyboard="{ keys: { start: ['Space'] } }"
screen-reader-instructions="Space picks up a card or a column, the arrow keys move it, Space drops it, Escape cancels. Enter opens a card, X selects it."
:announcements="announcements"
:haptics="{ enter: true, refuse: true }"
>
<Board />
</DnDProvider>
</template>Ce qu'il faut regarder
Board.vue: les colonnes forment leur propre liste triable horizontale (strategy: 'horizontal',axis: 'x'), et chaque colonne est un item triable qu'on fait glisser par son en-tête (handle). Agrandi, le tableau remplit l'écran ; sur un téléphone, il défile sur le côté.Column.vue: les cartes de toutes les colonnes forment un seulgroup, avec les règles de la colonne : sa limite WIP, la question avant Done, et les filtres. La jauge de son en-tête allume les cartes qu'un glissement y ferait entrer.Card.vue: un item triable detype: 'card', si bien que la corbeille accepte les cartes et pas les colonnes. Un clic l'ouvre, Ctrl/Cmd-clic la sélectionne (selected) ; au clavier, Espace la saisit, Entrée l'ouvre et X la sélectionne.store.ts: les données et les règles du tableau, sans aucun glisser-déposer, fournies au-dessus duDnDProviderpour que l'overlay les lise aussi.
Les règles d'une colonne
Chaque colonne passe quatre règles à sa liste. put voit toutes les cartes d'un multi-drag à la
fois : la colonne les accepte toutes si elles tiennent sous sa limite, ou aucune. Au-dessus d'une
colonne pleine, l'emplacement reste là où le glissement a commencé, la colonne devient rouge et
les cartes en main disent pourquoi. onBeforeDrop pose une question avant que des cartes
n'entrent dans Done, et animateChanges fait glisser les cartes qui arrivent et partent sans glissement :
un filtre, Add card, Undo.
const { items, isOver, isAccepting } = makeSortableList(list, {
id: props.column.id,
items: () => props.column.cards.filter(board.matches),
group: { name: 'cards', put: ({ keys }) => board.fits(props.column, keys) },
overlay: CardOverlay,
animateChanges: true,
onBeforeDrop: (event) => (props.column.confirm && !event.sameList ? board.ask(event.items) : 'accept'),
onDrop: (event) => {
event.preventDefault();
board.apply(event);
},
});
Des filtres, et des dépôts que le tableau applique
La recherche et les personnes de la barre d'outils filtrent les cartes, et chaque glissement
continue de fonctionner sur ce qui est affiché. Chaque liste lit sa colonne via un getter, filtrée,
si bien que la bibliothèque ne l'écrit jamais : onDrop appelle preventDefault() et passe au
tableau e.result, les nouvelles cartes filtrées de chaque colonne que le dépôt a modifiée. Le
tableau les remet aux places qu'avaient les cartes affichées, et les cartes que les filtres
masquent restent où elles étaient.
const merge = (all: Card[], shown: Card[]) => {
const queue = [...shown];
const merged: Card[] = [];
for (const entry of all) {
if (!matches(entry)) merged.push(entry);
else if (queue.length) merged.push(queue.shift()!);
}
// Cards that came in from another column and found no place left: at the end.
return [...merged, ...queue];
};
Les cartes en main
CardOverlay.vue est l'overlay des cartes. La carte saisie se soulève et s'incline, le reste
d'une sélection est posé dessous, construit à partir de clones DragPreview tournés par le
transform de leurs enveloppes, et un compteur est posé par-dessus. Sous les cartes, une
indication dit ce que ferait le relâchement, d'après l'over et l'accepted de l'overlay :
Move 2 cards to In review, Complete, Delete, ou In review is full · 2 max. Déposé dans une
colonne, chaque clone atterrit sur son propre emplacement ; au-dessus de la corbeille, la pile
rétrécit, et une fois déposées, les cartes y entrent l'une après l'autre.
const hint = computed(() => {
if (props.status !== 'dragging' || !props.over) return null;
const many = props.items.length > 1 ? ` ${props.items.length} cards` : '';
if (props.over.id === 'trash') return { tone: 'danger', icon: 'trash', text: `Delete${many}` };
const column = board.column(props.over.id);
if (!column) return null;
if (!props.accepted) return { tone: 'danger', icon: 'limit', text: `${column.title} is full · ${column.limit} max` };
if (board.find(props.items[0]!.id)?.column.id === column.id) return null;
if (column.confirm) return { tone: 'done', column, text: `Complete${many}` };
return { tone: '', column, text: `Move${many} to ${column.title}` };
});
Une colonne glissée par son en-tête est le clone par défaut, incliné avec la propriété rotate
tant que data-dnd-overlay vaut dragging.
Done demande d'abord
Un dépôt dans Done renvoie une promesse depuis onBeforeDrop : les cartes attendent là où elles
ont été relâchées pendant que ConfirmDialog.vue pose la question. Move to Done répond
'accept', Cancel et Échap 'reject' (les cartes reviennent à leur place), et Keep dragging'continue' : les cartes suivent de nouveau le pointeur, et le clic suivant les dépose. Une fois
les cartes arrivées (onDragSettle), leur coche apparaît dans Done et la progression du sprint
avance.
useDnDMonitor({
onDragSettle: (event) => {
const target = event.over ? board.column(event.over.id) : undefined;
if ('dropped' in event && target?.confirm && event.source?.list !== target.id) board.complete(event.accepted);
},
});
Plusieurs cartes, la corbeille et Undo
Les cartes sélectionnées voyagent ensemble : faites-en glisser une, et chaque carte sélectionnée
suit, depuis n'importe quelle colonne. La barre de sélection les déplace, les supprime, ou vide la
sélection (Échap aussi). Trash.vue est une simple cible de dépôt qui n'accepte que les
cartes ; elle les supprime du tableau, comme le font les boutons Delete, pour que la notification
puisse remettre chacune là où elle était.
makeDroppable(el, {
id: 'trash',
accept: 'card',
onDrop: (event) => {
board.remove(event.active.ids);
selection.delete(...event.active.ids);
},
});
Déplacer sans glisser
Le panneau d'une carte a des boutons Status, et la barre de sélection a un bouton par colonne. Ils
déplacent les cartes avec @vue-dnd-kit/actions : moveItem joue un vrai glissement au clavier
de la carte, et une carte sélectionnée emmène la sélection. Il est annoncé et animé comme les
vôtres, et la limite de la colonne et la question de Done s'y appliquent comme à n'importe quel
glissement.
export function useMove() {
const board = inject(BOARD)!;
const { moveItem } = useDnDActions();
return (ids: Key[], target: Column) => {
if (!board.fits(target, ids)) return board.notify(`${target.title} is full: ${target.limit} cards at most`);
void moveItem(ids[0]!, { list: target.id });
};
}
Clavier et lecteurs d'écran
Seul Espace saisit une carte : Entrée reste libre pour l'ouvrir ; les instructions le disent. Les flèches déplacent une carte d'emplacement en emplacement, et en travers vers la colonne suivante qui l'accepte : une colonne pleine est sautée, et au-delà de la dernière colonne, l'appui suivant emmène la carte jusqu'à la corbeille. Les annonces nomment les cartes et les colonnes au lieu des ids : "Picked up VDK-45, Haptics on Android tablets. Todo, position 6 of 6.", "In review is full: its limit is 2 cards."
<DnDProvider
:keyboard="{ keys: { start: ['Space'] } }"
screen-reader-instructions="Space picks up a card or a column, the arrow keys move it, Space drops it, Escape cancels. Enter opens a card, X selects it."
:announcements="announcements"
:haptics="{ enter: true, refuse: true }"
>
En savoir plus : Listes qui partagent des items · Décider d'un dépôt · Multi-drag · L'overlay · Glissements programmatiques · Accessibilité