diff options
Diffstat (limited to 'src/client/views/nodes/calendarBox/CalendarBox.tsx')
| -rw-r--r-- | src/client/views/nodes/calendarBox/CalendarBox.tsx | 248 |
1 files changed, 167 insertions, 81 deletions
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx index 2b20a666d..26aed72c3 100644 --- a/src/client/views/nodes/calendarBox/CalendarBox.tsx +++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx @@ -1,15 +1,17 @@ -import { Calendar, EventClickArg, EventDropArg, EventSourceInput } from '@fullcalendar/core'; +import { Calendar, DateSelectArg, EventClickArg, EventDropArg, EventMountArg, EventSourceInput } from '@fullcalendar/core'; +import { EventResizeDoneArg } from '@fullcalendar/interaction'; import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import multiMonthPlugin from '@fullcalendar/multimonth'; import timeGrid from '@fullcalendar/timegrid'; -import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; +import FullCalendar from '@fullcalendar/react'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction, untracked } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { dateRangeStrToDates } from '../../../../ClientUtils'; import { Doc } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; -import { BoolCast, NumCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, StrCast } from '../../../../fields/Types'; import { DocServer } from '../../../DocServer'; import { DragManager } from '../../../util/DragManager'; import { CollectionSubView, SubCollectionViewProps } from '../../collections/CollectionSubView'; @@ -17,26 +19,34 @@ import { ContextMenu } from '../../ContextMenu'; import { DocumentView } from '../DocumentView'; import { OpenWhere } from '../OpenWhere'; import './CalendarBox.scss'; +import { DateField } from '../../../../fields/DateField'; +import { undoable } from '../../../util/UndoManager'; +import { DocumentType } from '../../../documents/DocumentTypes'; type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay'; @observer export class CalendarBox extends CollectionSubView() { - _calendarRef: HTMLDivElement | null = null; + _calendarRef: FullCalendar | null = null; _calendar: Calendar | undefined; _observer: ResizeObserver | undefined; _eventsDisposer: IReactionDisposer | undefined; _selectDisposer: IReactionDisposer | undefined; + _isMultiMonth: boolean | undefined; + + @observable _multiMonth = 0; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } - @observable _multiMonth = 0; - isMultiMonth: boolean | undefined; + @computed get calTypeFieldKey() { + return this.fieldKey + '_calendarType'; + } componentDidMount(): void { + this.Document.$calendar = ''; // needed only to make the keyvalue view look nice. this._props.setContentViewBox?.(this); this._eventsDisposer = reaction( () => ({ events: this.calendarEvents }), @@ -52,7 +62,7 @@ export class CalendarBox extends CollectionSubView() { type: 'CHANGE_DATE', dateMarker: state.dateEnv.createMarker(initialDate.start), }); - setTimeout(() => (initialDate.start.toISOString() !== initialDate.end.toISOString() ? this._calendar?.select(initialDate.start, initialDate.end) : this._calendar?.select(initialDate.start))); + setTimeout(() => initialDate.start.toISOString() !== initialDate.end.toISOString() && this._calendar?.select(initialDate.start, initialDate.end)); }, { fireImmediately: true } ); @@ -64,7 +74,8 @@ export class CalendarBox extends CollectionSubView() { @computed get calendarEvents(): EventSourceInput | undefined { return this.childDocs.map(doc => { - const { start, end } = dateRangeStrToDates(StrCast(doc.date_range)); + const { start, end } = dateRangeStrToDates(StrCast(doc.$task_dateRange)); + const isCompleted = BoolCast(doc.$task_completed); // AARAV ADD return { title: StrCast(doc.title), start, @@ -72,8 +83,8 @@ export class CalendarBox extends CollectionSubView() { groupId: doc[Id], startEditable: true, endEditable: true, - allDay: BoolCast(doc.allDay), - classNames: ['mother'], // will determine the style + allDay: BoolCast(doc.$task_allDay), + classNames: ['mother', isCompleted ? 'completed-task' : ''], // will determine the style editable: true, // subject to change in the future backgroundColor: this.eventToColor(doc), borderColor: this.eventToColor(doc), @@ -86,16 +97,16 @@ export class CalendarBox extends CollectionSubView() { } @computed get dateRangeStrDates() { - return dateRangeStrToDates(StrCast(this.Document.date_range)); + return dateRangeStrToDates(StrCast(this.Document._calendar_dateRange)); } get dateSelect() { - return dateRangeStrToDates(StrCast(this.Document.date)); + return dateRangeStrToDates(StrCast(this.Document._calendar_date)); } // 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'; + if (this.dataDoc[this.calTypeFieldKey]) return StrCast(this.dataDoc[this.calTypeFieldKey]) as CalendarView; + if (this._isMultiMonth) return 'multiMonth'; const { start, end } = this.dateRangeStrDates; if (start.getFullYear() !== end.getFullYear() || start.getMonth() !== end.getMonth()) return 'multiMonth'; if (Math.abs(start.getDay() - end.getDay()) > 7) return 'dayGridMonth'; @@ -104,7 +115,9 @@ export class CalendarBox extends CollectionSubView() { // TODO: Return a different color based on the event type eventToColor = (event: Doc): string => { - return 'red' + event; + return StrCast(event.type) === DocumentType.TASK + ? '#20B2AA' // teal for tasks + : 'red'; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -112,7 +125,7 @@ export class CalendarBox extends CollectionSubView() { if (!super.onInternalDrop(e, de)) return false; de.complete.docDragData?.droppedDocuments.forEach(doc => { const today = new Date().toISOString(); - if (!doc.date_range) doc.$date_range = `${today}|${today}`; + if (!doc.$task_dateRange) doc.$task_dateRange = `${today}|${today}`; }); return true; }; @@ -122,10 +135,31 @@ export class CalendarBox extends CollectionSubView() { return false; }; - handleEventDrop = (arg: EventDropArg) => { + handleEventDrop = undoable((arg: EventDropArg | EventResizeDoneArg) => { const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); - doc && arg.event.start && (doc.date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString()); - }; + // doc && arg.event.start && (doc.$task_dateRange = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString()); + if (!doc || !arg.event.start) return; + + // get the new start and end dates + const startDate = new Date(arg.event.start); + const endDate = new Date(arg.event.end ?? arg.event.start); + + // update date range, time range, and all day status + doc.$task_dateRange = `${startDate.toISOString()}|${endDate.toISOString()}`; + + const allDayStatus = arg.event.allDay ?? false; + if (doc.$task_allDay !== allDayStatus) { + doc.$task_allDay = allDayStatus; + } + + if (doc.$task_allDay) { + delete doc.$task_startTime; + delete doc.$task_endTime; + } else { + doc.$task_startTime = new DateField(startDate); + doc.task_endTime = new DateField(endDate); + } + }, 'change event date'); handleEventClick = (arg: EventClickArg) => { const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); @@ -144,68 +178,129 @@ export class CalendarBox extends CollectionSubView() { }; // https://fullcalendar.io - renderCalendar = () => { - const cal = !this._calendarRef - ? null - : (this._calendar = new Calendar(this._calendarRef, { - plugins: [multiMonthPlugin, dayGridPlugin, timeGrid, interactionPlugin], - headerToolbar: { - left: 'prev,next today', - center: 'title', - right: 'multiMonth dayGridMonth timeGridWeek timeGridDay', - }, - selectable: true, - initialView: this.calendarViewType === 'multiMonth' ? undefined : this.calendarViewType, - initialDate: this.dateSelect.start, - navLinks: true, - editable: false, - displayEventTime: false, - displayEventEnd: false, - select: info => { - const start = dateRangeStrToDates(info.startStr).start.toISOString(); - const end = dateRangeStrToDates(info.endStr).start.toISOString(); - this.dataDoc.date = start + '|' + end; - }, - aspectRatio: NumCast(this.Document.width) / NumCast(this.Document.height), - events: this.calendarEvents, - eventClick: this.handleEventClick, - eventDrop: this.handleEventDrop, - eventDidMount: arg => { - arg.el.addEventListener('pointerdown', ev => { - ev.button && ev.stopPropagation(); - }); - if (navigator.userAgent.includes('Macintosh')) { - arg.el.addEventListener('pointerup', ev => { - ev.button && ev.stopPropagation(); - ev.button && this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId); - }); - } - arg.el.addEventListener('contextmenu', ev => { - if (!navigator.userAgent.includes('Macintosh')) { - this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId); + @computed get renderCalendar() { + const availableWidth = this._props.PanelWidth() / (this._props.DocumentView?.().UIBtnScaling ?? 1); + const btn = (text: string, view: string | (() => void), hint: string) => ({ text, hint, click: typeof view === 'string' ? () => this._calendarRef?.getApi().changeView(view) : view }); + return ( + <FullCalendar + ref={(r: unknown) => (this._calendarRef = r as FullCalendar)} + customButtons={{ + nowBtn: btn('Now', () => this._calendarRef?.getApi().gotoDate(new Date()), 'Go to Today'), + multiBtn: btn('M+', 'multiMonth', 'Multiple Month View'), + monthBtn: btn('M', 'dayGridMonth', 'Month View'), + weekBtn: btn('W', 'timeGridWeek', 'Week View'), + dayBtn: btn('D', 'timeGridDay', 'Day View'), + }} + headerToolbar={ + availableWidth > 450 + ? { + left: 'prev,next nowBtn', + center: 'title', + right: 'multiBtn monthBtn weekBtn dayBtn', } - ev.stopPropagation(); - ev.preventDefault(); - }); - }, - })); - cal?.render(); - setTimeout(() => cal?.view.calendar.select(this.dateSelect.start, this.dateSelect.end)); - }; + : availableWidth > 300 + ? { + left: 'prev,next', + center: 'title', + right: '', + } + : { + left: '', + center: 'title', + right: '', + } + } + selectable={true} + initialView={this.calendarViewType === 'multiMonth' ? undefined : this.calendarViewType} + views={{ + multiMonth: { + type: 'multiMonth', + duration: { months: 12 }, + }, + }} + initialDate={untracked(() => this.dateSelect.start)} + navLinks={true} + editable={false} + // expandRows={true} + // handleWindowResize={true} + displayEventTime={false} + displayEventEnd={false} + plugins={[multiMonthPlugin, dayGridPlugin, timeGrid, interactionPlugin]} + aspectRatio={this._props.PanelWidth() / this._props.PanelHeight()} + weekends={true} + events={this.calendarEvents} + eventClick={this.handleEventClick} + eventDrop={this.handleEventDrop} + unselectAuto={false} + // unselect={() => {}} + select={(info: DateSelectArg) => { + const start = dateRangeStrToDates(info.startStr).start.toISOString(); + const end = info.allDay ? start : dateRangeStrToDates(info.endStr).start.toISOString(); + this.Document._calendar_date = start + '|' + end; + }} + // eventContent={() => { + // return null; + // }} + eventDidMount={(arg: EventMountArg) => { + const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? ''); + if (!doc) return; + + if (doc.type === DocumentType.TASK) { + const checkButton = document.createElement('button'); + checkButton.innerText = doc.$task_completed ? '✅' : '⬜'; + checkButton.style.position = 'absolute'; + checkButton.style.right = '5px'; + checkButton.style.top = '50%'; + checkButton.style.transform = 'translateY(-50%)'; + checkButton.style.background = 'transparent'; + checkButton.style.border = 'none'; + checkButton.style.cursor = 'pointer'; + checkButton.style.fontSize = '18px'; + checkButton.style.zIndex = '1000'; + checkButton.style.padding = '0'; + checkButton.style.margin = '0'; + + checkButton.onclick = ev => { + ev.stopPropagation(); + doc.$task_completed = !doc.$task_completed; + this._calendar?.refetchEvents(); + }; + + arg.el.style.position = 'relative'; + arg.el.appendChild(checkButton); + } + arg.el.addEventListener('pointerdown', ev => ev.button && ev.stopPropagation()); + if (navigator.userAgent.includes('Macintosh')) { + arg.el.addEventListener('pointerup', ev => { + ev.button && ev.stopPropagation(); + ev.button && this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId); + }); + } + arg.el.addEventListener('contextmenu', ev => { + if (!navigator.userAgent.includes('Macintosh')) { + this.handleEventContextMenu(ev.pageX, ev.pageY, arg.event._def.groupId); + } + ev.stopPropagation(); + ev.preventDefault(); + }); + }} + /> + ); + } render() { return ( <div key={this.calendarViewType} - className="calendarBox" + className={`calendarBox${this._props.isContentActive() ? '-interactive' : ''}`} onPointerDown={e => { 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'; + if (cname.includes('multiMonth')) this.dataDoc[this.calTypeFieldKey] = 'multiMonth'; + if (cname.includes('dayGridMonth')) this.dataDoc[this.calTypeFieldKey] = 'dayGridMonth'; + if (cname.includes('timeGridWeek')) this.dataDoc[this.calTypeFieldKey] = 'timeGridWeek'; + if (cname.includes('timeGridDay')) this.dataDoc[this.calTypeFieldKey] = 'timeGridDay'; }) ); }} @@ -217,17 +312,8 @@ export class CalendarBox extends CollectionSubView() { ref={r => { this.createDashEventsTarget(r); this.fixWheelEvents(r, this._props.isContentActive); - - 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(); - } }}> - <div className="calendarBox-wrapper" ref={r => (this._calendarRef = r)} /> + {this.renderCalendar} </div> ); } |
