import { Calendar, EventClickArg, EventSourceInput } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; import multiMonthPlugin from '@fullcalendar/multimonth'; import timeGrid from '@fullcalendar/timegrid'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { dateRangeStrToDates } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; import { BoolCast, NumCast, StrCast } from '../../../../fields/Types'; import { CollectionSubView, SubCollectionViewProps } from '../../collections/CollectionSubView'; import './CalendarBox.scss'; import { Id } from '../../../../fields/FieldSymbols'; import { DocServer } from '../../../DocServer'; import { DocumentView } from '../DocumentView'; import { OpenWhere } from '../OpenWhere'; import { DragManager } from '../../../util/DragManager'; import { DocData } from '../../../../fields/DocSymbols'; type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay'; @observer export class CalendarBox extends CollectionSubView() { _calendarRef: HTMLDivElement | null = null; _calendar: Calendar | undefined; _oldWheel: HTMLElement | null = null; _observer: ResizeObserver | undefined; _eventsDisposer: IReactionDisposer | undefined; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @observable _multiMonth = 0; isMultiMonth: boolean | undefined; componentDidMount(): void { this._props.setContentViewBox?.(this); this._eventsDisposer = reaction( () => this.calendarEvents, events => this._calendar?.setOption('events', events), { fireImmediately: true } ); } componentWillUnmount(): void { this._eventsDisposer?.(); } @computed get calendarEvents(): EventSourceInput | undefined { return this.childDocs.map(doc => { const { startDate: start, endDate: end } = dateRangeStrToDates(StrCast(doc.date_range)); return { title: StrCast(doc.title), start, end, groupId: doc[Id], startEditable: true, endEditable: true, allDay: BoolCast(doc.allDay), classNames: ['mother'], // will determine the style editable: true, // subject to change in the future backgroundColor: this.eventToColor(doc), color: 'white', extendedProps: { description: StrCast(doc.description), }, }; }); } @computed get dateRangeStrDates() { return dateRangeStrToDates(StrCast(this.Document.date_range)); } // Choose a calendar view based on the date range @computed get calendarViewType(): CalendarView { if (this.dataDoc[this.fieldKey + '_calendarType']) return StrCast(this.dataDoc[this.fieldKey + '_calendarType']) as CalendarView; if (this.isMultiMonth) return 'multiMonth'; const { startDate, endDate } = this.dateRangeStrDates; if (startDate.getFullYear() !== endDate.getFullYear() || startDate.getMonth() !== endDate.getMonth()) return 'multiMonth'; if (Math.abs(startDate.getDay() - endDate.getDay()) > 7) return 'dayGridMonth'; return 'timeGridWeek'; } // TODO: Return a different color based on the event type eventToColor(event: Doc): string { return 'blue'; } internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) { if (!super.onInternalDrop(e, de)) return false; de.complete.docDragData?.droppedDocuments.forEach(doc => { const today = new Date().toISOString(); if (!doc.date_range) doc[DocData].date_range = `${today}|${today}`; }); return true; } onInternalDrop = (e: Event, de: DragManager.DropEvent): boolean => { if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); return false; }; handleEventClick = (arg: EventClickArg) => { const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); DocumentView.DeselectAll(); if (doc) { DocumentView.showDocument(doc, { openLocation: OpenWhere.lightboxAlways }); arg.jsEvent.stopPropagation(); } }; // https://fullcalendar.io renderCalendar = () => { const cal = !this._calendarRef ? null : (this._calendar = new Calendar(this._calendarRef, { plugins: [multiMonthPlugin, dayGridPlugin, timeGrid], headerToolbar: { left: 'prev,next today', center: 'title', right: 'multiMonth dayGridMonth timeGridWeek timeGridDay', }, initialView: this.calendarViewType === 'multiMonth' ? undefined : this.calendarViewType, initialDate: this.dateRangeStrDates.startDate, navLinks: true, editable: false, displayEventTime: false, displayEventEnd: false, aspectRatio: NumCast(this.Document.width) / NumCast(this.Document.height), events: this.calendarEvents, eventClick: this.handleEventClick, })); cal?.render(); }; onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); render() { return (
setTimeout( action(() => { const cname = (e.nativeEvent.target as HTMLButtonElement)?.className ?? ''; if (cname.includes('multiMonth')) this.dataDoc[this.fieldKey + '_calendarType'] = 'multiMonth'; if (cname.includes('dayGridMonth')) this.dataDoc[this.fieldKey + '_calendarType'] = 'dayGridMonth'; if (cname.includes('timeGridWeek')) this.dataDoc[this.fieldKey + '_calendarType'] = 'timeGridWeek'; if (cname.includes('timeGridDay')) this.dataDoc[this.fieldKey + '_calendarType'] = 'timeGridDay'; }) ) } style={{ width: this._props.PanelWidth() / this._props.ScreenToLocalTransform().Scale, height: this._props.PanelHeight() / this._props.ScreenToLocalTransform().Scale, transform: `scale(${this._props.ScreenToLocalTransform().Scale})`, transformOrigin: 'top left', }} ref={r => { this.createDashEventsTarget(r); this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); this._oldWheel = r; // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); if (r) { this._observer?.disconnect(); (this._observer = new ResizeObserver(() => { this._calendar?.setOption('aspectRatio', NumCast(this.Document.width) / NumCast(this.Document.height)); this._calendar?.updateSize(); })).observe(r); this.renderCalendar(); } }}>
(this._calendarRef = r)} />
); } }