Skip to content
Snippets Groups Projects
AScheduleCalendar.vue 2.88 KiB
<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>