Documentación/Resumen
Calendario semanal
Una agenda semanal — eventos que se mueven entre días y cuartos de hora, se estiran por sus bordes, se dibujan en el tiempo libre, pasan a todo el día, se planifican desde una lista de tareas, con deshacer.
Esta página la ha traducido una IA y puede contener imprecisiones. Leer el original en inglés
Cargando la demo…
<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>Qué observar
EventBlock.vue: un evento en las horas es un elemento arrastrable simple cuyo overlay,EventOverlay.vue, dice dónde aterrizaría: "Wed 10:30 – 11:15". Los eventos que se solapan se colocan uno al lado del otro (layoutDayenplacement.ts).Edge.vue: los bordes superior e inferior de un evento son elementos arrastrables propios. Arrastra uno y el evento se estira, de cuarto de hora en cuarto de hora.DayColumn.vue: las horas de cada día son un destino de drop, y bajo sus eventos también hay un elemento arrastrable: pulsa en el tiempo libre y arrastra para dibujar un evento nuevo, luego escribe su título.AllDayCell.vueyTasks.vue: suelta un evento en la celda de todo el día de un día y pierde su hora; suéltalo en la lista de tareas y pierde su día. Las tareas (EventChip.vue) se arrastran a la semana y conservan su duración.Week.vue: el área de arrastre de las horas y su propio modificador, los movimientos con teclado, la línea de la hora actual.calendar.ts: los eventos y un historial para deshacer. Cada cambio pasa por una sola función, y el toast ofrece deshacerlo.
Amplía el ejemplo: si hay sitio, un panel lateral muestra el mes, los calendarios que mostrar u ocultar, y las tareas como lista. En un teléfono la semana se convierte en tres días, y una pulsación larga arrastra.
Una respuesta para cada drop
Lo que haría un drop se pregunta en cuatro sitios: la etiqueta del overlay, el fantasma que una
tarea muestra en el día de debajo, lo que oyen los lectores de pantalla y el propio drop. Todos
llaman a placeOf, así que nunca discrepan. Cada destino de drop lleva lo que es en su data, y
position es dónde está la caja arrastrada dentro de él: su borde superior es la hora.
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) };
}
El overlay la lee en vivo, desde useDnD().position, y conserva su última respuesta mientras
aterriza:
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 evento conserva su id, su data y su overlay esté donde esté: un bloque en las horas, un chip
de todo el día o entre las tareas. Si se suelta en otro sitio, lo renderiza otro componente, y el
overlay aterriza igualmente en el nuevo elemento.
El modificador propio de las horas
makeDragArea da a los arrastres que empiezan en las horas (un evento, un borde, el tiempo libre)
un modificador propio. Sobre los días, la caja se mueve por días enteros, tantos como cruzó el
puntero, así que un evento junto a otro conserva su sitio en el día; su borde superior se ajusta a
los cuartos de hora del día en que empezó. containerRect es ese día donde está ahora: sigue al
scroll, y el ajuste se mantiene mientras las horas se desplazan solas bajo un arrastre.
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 } });
El autoScroll del área estrecha el borde que desplaza las horas, para todos los arrastres sobre
ellas: la fila de todo el día, justo encima, sigue siendo fácil de alcanzar. Las tareas y los
eventos de todo el día empiezan fuera del área y se mueven libremente; el día de debajo muestra un
fantasma donde aterrizarían.
Estirar: un borde es un elemento arrastrable
Un borde está dentro del elemento de su evento, y una pulsación arrastra el elemento arrastrable más cercano: el borde. No tiene overlay, solo se mueve arriba y abajo, y se queda en las horas 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
});
El borde en sí no se mueve. Lo que crece es el evento, desde donde está el borde en el día, leído
de position igual que lo leerá el drop:
const time = computed(() =>
resizing.value && position.value
? resizeTo(props.event, resizing.value, position.value)
: { start: props.event.start!, duration: props.event.duration },
);
Dibujar en el tiempo libre
Bajo los eventos, cada día tiene una franja, y la franja es un elemento arrastrable sin overlay.
Nunca se mueve: lo lejos que fue su caja (position) es lo lejos que fue el puntero desde la
pulsación, ajustado a cuartos de hora por el área. El día muestra el evento nuevo mientras se
dibuja, y su drop lo crea.
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
});
El accept del día es una función: acepta cualquier evento, y solo sus propios bordes y franjas.
Las celdas de todo el día y la lista de tareas aceptan 'event', una cadena, así que los tipos
edge y new se rechazan ahí sin una línea de código. El drop es un solo manejador:
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);
}
},
El teclado
Llega a un evento con Tab y agárralo con Espacio: el monitor de la semana responde a las flechas,
getKeyboardCoordinates, con el siguiente cuarto de hora (con Mayús, una hora) o el siguiente
día. Los arrastres con teclado no se desplazan solos, así que cerca de un extremo de las horas
visibles la flecha las desplaza en su lugar. Las tareas y los eventos de todo el día saltan de
destino en destino (keyboardMode: 'droppables') hasta que están sobre las horas.
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;
},
});
Los announcements del provider en index.vue dicen qué se agarró y cómo moverlo; la semana dice
la hora a medida que las flechas la cambian ("Tuesday, 10:30 to 11:15"), y el toast lo que cambió
un drop. Sobre un evento con el foco, Supr lo borra, y Ctrl+Z deshace cualquier cambio.
Más información: Un modificador propio · Reglas para una parte de la página · Dejarlo donde se soltó · Sin overlay · Componentes de overlay · Accesibilidad