<template> <div ref="calendarWrapperEl" class="tw-h-full"> <FullCalendar :options="calendarConfig"> <template v-for="(_, slot) in $slots" #[slot]="attrs"> <slot :name="slot" v-bind="attrs ?? {}" /> </template> </FullCalendar> </div> </template> <script lang="ts" setup> import FullCalendar from '@fullcalendar/vue3' import fullCalendarTimeGridPlugin from '@fullcalendar/timegrid' import fullCalendarInteractionPlugin from '@fullcalendar/interaction' import { computed, ref } from 'vue' import { useI18n } from '@/i18n' import { getClosestSlot } from '@/util' import { CalendarOptions, DateSelectArg, EventClickArg } from '@fullcalendar/core' const props = defineProps<{ events: CalendarOptions['events'] slotDurationMinutes: number start: Date end: Date selectable?: boolean }>() const emit = defineEmits<{ select: [DateSelectArg] selectDay: [Date] selectEvent: [EventClickArg] syncDateRange: [{ start: Date; end: Date }] }>() const { locale, t } = useI18n() const calendarWrapperEl = ref<HTMLDivElement>() const calendarConfig = computed<CalendarOptions>(() => ({ plugins: [fullCalendarTimeGridPlugin, fullCalendarInteractionPlugin], initialView: 'timeGridWeek', locale: locale.value, initialDate: props.start, height: '100%', stickyHeaderDates: true, firstDay: 1, navLinks: true, events: props.events, buttonText: { today: t('calendar.today'), }, headerToolbar: { left: 'title', center: '', right: 'today prev,next', }, dayHeaderFormat: { day: 'numeric', month: 'numeric', weekday: 'short' }, eventTimeFormat: { hour: 'numeric', minute: '2-digit' }, slotLabelFormat: { hour: 'numeric', minute: '2-digit' }, allDaySlot: false, editable: false, nowIndicator: true, eventDidMount({ el, event, timeText }) { const { durationMinutes } = event.extendedProps let { title } = event.extendedProps if (durationMinutes < props.slotDurationMinutes) { title = `${timeText}: ${title}` } // here we add a simple tooltip to every event, so that the full title // of a show can be viewed el.setAttribute('title', title) }, datesSet: (view) => { const { start, end } = view if ( props.start?.toISOString() !== start.toISOString() || props.end?.toISOString() !== end.toISOString() ) { emit('syncDateRange', { start, end }) } }, eventClick(data) { emit('selectEvent', data) }, select(data) { emit('select', data) }, selectable: props.selectable, selectMirror: true, slotDuration: `00:${props.slotDurationMinutes.toString().padStart(2, '0')}:00`, eventMinHeight: 1, selectAllow({ start }) { return start >= getClosestSlot(props.slotDurationMinutes) }, selectOverlap: function (event) { return event.display === 'block' || event.display === 'background' }, navLinkDayClick(selectedDate) { emit('selectDay', selectedDate) }, })) </script>