Documentation/Vue d'ensemble
Calendrier de la semaine
Un planning de la semaine — des événements déplacés d'un jour et d'un quart d'heure à l'autre, étirés par leurs bords, tracés sur un créneau libre, passés en journée entière, planifiés depuis une liste de tâches, avec 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 { provide } from 'vue';
import { DnDProvider, type Announcements } from '@vue-dnd-kit/core';
import Calendar from './Calendar.vue';
import { CALENDAR, createCalendar } from './calendar';
import { spokenOf, type DragData } from './placement';
import { toDay } from './time';
// The calendar's state, shared by every component under it.
provide(CALENDAR, createCalendar(toDay(new Date())));
/** The event a drag carries; edges and empty time carry none. */
const eventOf = (data: unknown) => ((data as DragData).kind === 'event' ? (data as Extract<DragData, { kind: 'event' }>).event : null);
// Screen readers hear titles and times, not ids. Where a keyboard drag would land is spoken by
// the week as the arrows move it, and what a drop changed by the toast: the live region
// stays quiet for both.
const announcements: Partial<Announcements> = {
onDragStart: ({ active }) => {
const event = eventOf(active.data);
if (!event) return '';
const keys =
active.sensor === 'keyboard'
? ' Arrow keys move it by a quarter of an hour and by a day, with Shift by an hour. Space drops it, Escape cancels.'
: '';
return `Picked up ${event.title}, ${spokenOf(event, event.duration)}.${keys}`;
},
onDragOver: () => '',
onDragEnd: () => '',
onDragCancel: ({ active }) => {
const event = eventOf(active.data);
return event ? `${event.title} stays where it was.` : '';
},
};
</script>
<template>
<DnDProvider :announcements="announcements">
<Calendar />
</DnDProvider>
</template>Ce qu'il faut regarder
EventBlock.vue: un événement dans les heures est un élément déplaçable simple dont l'overlay,EventOverlay.vue, dit où il atterrirait : "Wed 10:30 – 11:15". Les événements qui se chevauchent se placent côte à côte (layoutDaydansplacement.ts).Edge.vue: les bords haut et bas d'un événement sont des éléments déplaçables à part entière. Faites-en glisser un et l'événement s'étire, d'un quart d'heure à la fois.DayColumn.vue: les heures de chaque jour sont une cible de dépôt, et sous ses événements se trouve aussi un élément déplaçable : appuyez sur un créneau libre et faites glisser pour tracer un nouvel événement, puis tapez son titre.AllDayCell.vueetTasks.vue: déposez un événement sur la cellule « journée entière » d'un jour et il perd son horaire ; déposez-le sur la liste de tâches et il perd son jour. Les tâches (EventChip.vue) se glissent dans la semaine, et gardent leur durée.Week.vue: la zone de glissement des heures et leur propre modificateur, les déplacements au clavier, la ligne de l'heure actuelle.calendar.ts: les événements et un historique d'annulation. Chaque changement passe par une seule fonction, et le toast propose de l'annuler.
Agrandissez l'exemple : quand il y a la place, un panneau latéral affiche le mois, les calendriers à afficher ou masquer, et les tâches sous forme de liste. Sur un téléphone, la semaine devient trois jours, et un appui long fait glisser.
Une seule réponse pour chaque dépôt
Ce que ferait un dépôt est demandé à quatre endroits : le libellé de l'overlay, le fantôme qu'une
tâche montre dans le jour en dessous, ce qu'entendent les lecteurs d'écran, et le dépôt lui-même.
Tous appellent placeOf : ils ne se contredisent donc jamais. Chaque cible de dépôt porte ce
qu'elle est dans son data, et position est l'endroit où se trouve la boîte glissée à
l'intérieur : son haut est l'heure.
export function placeOf(event: CalendarEvent, over: Over | null, position: Coordinates | null): Place | null {
const target = over?.accepted ? (over.data as Target) : null;
if (!target) return null;
if (target.kind === 'tasks') return { day: null, start: null };
if (target.kind === 'all-day') return { day: target.day, start: null };
if (!position) return null;
return { day: target.day, start: clamp(minutesAt(position.y), 0, DAY_MINUTES - event.duration) };
}
L'overlay la lit en direct, depuis useDnD().position, et garde sa dernière réponse pendant qu'il
atterrit :
const { position } = useDnD();
const place = shallowRef(placeOf(event.value, props.over, position.value));
watchEffect(() => {
if (props.status === 'dragging') place.value = placeOf(event.value, props.over, position.value);
});
Un événement garde son id, son data et son overlay où qu'il soit : un bloc dans les heures, une
pastille en journée entière ou parmi les tâches. Déposé ailleurs, il est rendu par un autre
composant, et l'overlay atterrit quand même sur le nouvel élément.
Le modificateur propre aux heures
makeDragArea donne aux glissements qui commencent dans les heures (un événement, un bord, un
créneau libre) un modificateur à eux. Au-dessus des jours, la boîte se déplace par jours entiers,
autant que le pointeur en a traversé : un événement à côté d'un autre garde donc sa place dans le
jour ; son haut s'aligne sur les quarts d'heure du jour où il a commencé. containerRect est ce
jour, là où il se trouve maintenant : il suit le défilement, et l'alignement tient pendant que les
heures défilent automatiquement sous un glissement.
const snapToWeek: Modifier = ({ transform, overlayRect, containerRect, origin }) => {
const days = grid.value?.getBoundingClientRect();
if (!days || !containerRect) return transform;
const width = days.width / props.days.length;
const column = (x: number) => Math.floor((x - days.left) / width);
const pointer = origin.x + transform.x;
const x = pointer < days.left || pointer >= days.right ? transform.x : (column(pointer) - column(origin.x)) * width;
const top = overlayRect.top - containerRect.top;
return { x, y: transform.y + Math.round(top / STEP_PX) * STEP_PX - top };
};
makeDragArea(grid, { modifiers: [snapToWeek], autoScroll: { threshold: 0.12 } });
L'autoScroll de la zone rétrécit le bord qui fait défiler les heures, pour chaque glissement
au-dessus d'elles : la rangée « journée entière » juste au-dessus reste facile à atteindre. Les
tâches et les événements sur la journée entière commencent hors de la zone et se déplacent
librement ; le jour en dessous montre un fantôme là où ils atterriraient.
Étirer : un bord est un élément déplaçable
Un bord se trouve à l'intérieur de l'élément de son événement, et un appui fait glisser l'élément déplaçable le plus proche : le bord. Il n'a pas d'overlay, ne bouge que vers le haut et le bas, et reste dans les heures visibles.
makeDraggable(el, {
id: () => `${props.event.id}-${props.edge}`,
type: 'edge',
data: (): DragData => ({ kind: 'edge', event: props.event, edge: props.edge }),
overlay: false,
axis: 'y',
modifiers: [restrictToFirstScrollableAncestor],
attributes: false, // no tab stop: Shift + arrows change the end of a focused event
});
Le bord lui-même ne bouge pas. C'est l'événement qui grandit, depuis l'endroit où se trouve le
bord dans le jour, lu dans position comme le dépôt le lira :
const time = computed(() =>
resizing.value && position.value
? resizeTo(props.event, resizing.value, position.value)
: { start: props.event.start!, duration: props.event.duration },
);
Tracer sur un créneau libre
Sous les événements, chaque jour a une piste, et la piste est un élément déplaçable sans overlay.
Elle ne bouge jamais : la distance parcourue par sa boîte (position) est la distance parcourue
par le pointeur depuis l'appui, alignée sur les quarts d'heure par la zone. Le jour montre le
nouvel événement à mesure qu'il est tracé, et son dépôt le crée.
makeDraggable(lane, {
id: () => `new-${props.day}`,
type: 'new',
data: (): DragData => ({ kind: 'new', day: props.day, from: from.value }),
overlay: false,
axis: 'y',
attributes: false, // the New event button is the keyboard's way
});
L'accept du jour est une fonction : il accepte n'importe quel événement, et seulement ses propres
bords et pistes. Les cellules « journée entière » et la liste de tâches acceptent 'event', une
chaîne : les types edge et new y sont donc refusés sans une ligne de code. Le dépôt tient en
un seul gestionnaire :
onDrop: ({ active, over, position }) => {
const data = active.data as DragData;
if (!position) return;
if (data.kind === 'event') {
const place = placeOf(data.event, over, position);
if (place) calendar.place(data.event.id, place);
} else if (data.kind === 'edge') {
calendar.resize(data.event.id, resizeTo(data.event, data.edge, position));
} else {
const { start, duration } = rangeOf(data.from, data.from + position.y);
calendar.create(props.day, start, duration);
}
},
Le clavier
Allez sur un événement avec Tab, saisissez-le avec Espace : le moniteur de la semaine répond aux
flèches, getKeyboardCoordinates, avec le quart d'heure suivant (avec Maj, une heure) ou le jour
suivant. Les glissements au clavier ne défilent pas automatiquement : près d'un bout des heures
visibles, la flèche les fait donc défiler à la place. Les tâches et les événements sur la journée
entière sautent de cible en cible (keyboardMode: 'droppables') jusqu'à ce qu'ils soient au-dessus
des heures.
useDnDMonitor({
getKeyboardCoordinates: (key, { pointer, direction }) => {
// …only over the hours
const dy = direction.y * STEP_PX * (key.shiftKey ? 4 : 1);
const next = { x: pointer.x + direction.x * (grid.value.offsetWidth / props.days.length), y: pointer.y + dy };
if (canScroll && (next.y < view.top + HOUR_PX || next.y > view.bottom - HOUR_PX)) {
hours.scrollTop += dy;
return { x: next.x, y: pointer.y };
}
return next;
},
});
Les announcements du provider dans index.vue disent ce qui a été saisi et comment le déplacer ;
la semaine énonce l'horaire à mesure que les flèches le changent ("Tuesday, 10:30 to 11:15"), et
le toast ce qu'un dépôt a changé. Sur un événement qui a le focus, Suppr le supprime, et Ctrl+Z
annule n'importe quel changement.
En savoir plus : Votre propre modificateur · Des règles pour une partie de la page · Le laisser là où il a été déposé · Sans overlay · Composants d'overlay · Accessibilité