aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/calendarBox/CalendarBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/calendarBox/CalendarBox.tsx')
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.tsx248
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>
);
}