diff --git a/src/public/app/services/utils.ts b/src/public/app/services/utils.ts index 6fd5678de..7accea1fb 100644 --- a/src/public/app/services/utils.ts +++ b/src/public/app/services/utils.ts @@ -30,6 +30,27 @@ function parseDate(str: string) { } } +// Source: https://stackoverflow.com/a/30465299/4898894 +function getMonthsInDateRange(startDate: string, endDate: string) { + const start = startDate.split('-'); + const end = endDate.split('-'); + const startYear = parseInt(start[0]); + const endYear = parseInt(end[0]); + const dates = []; + + for (let i = startYear; i <= endYear; i++) { + const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1; + const startMon = i === startYear ? parseInt(start[1])-1 : 0; + + for(let j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j+1) { + const month = j+1; + const displayMonth = month < 10 ? '0'+month : month; + dates.push([i, displayMonth].join('-')); + } + } + return dates; +} + function padNum(num: number) { return `${num <= 9 ? "0" : ""}${num}`; } @@ -621,6 +642,7 @@ export default { reloadFrontendApp, reloadTray, parseDate, + getMonthsInDateRange, formatDateISO, formatDateTime, formatTimeInterval, diff --git a/src/public/app/widgets/type_widgets/editable_text.js b/src/public/app/widgets/type_widgets/editable_text.js index acd006e9b..c5f2df34b 100644 --- a/src/public/app/widgets/type_widgets/editable_text.js +++ b/src/public/app/widgets/type_widgets/editable_text.js @@ -223,7 +223,9 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { } $classicToolbarWidget.empty(); - $classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element); + if ($classicToolbarWidget.length) { + $classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element); + } if (utils.isMobile()) { $classicToolbarWidget.addClass("visible"); diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index 8a7e797bb..11e6553ff 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -1,4 +1,4 @@ -import type { Calendar, DateSelectArg, EventChangeArg, EventDropArg, EventSourceInput, PluginDef } from "@fullcalendar/core"; +import type { Calendar, DateSelectArg, EventChangeArg, EventDropArg, EventInput, EventSourceFunc, EventSourceFuncArg, EventSourceInput, PluginDef } from "@fullcalendar/core"; import froca from "../../services/froca.js"; import ViewMode, { type ViewModeArgs } from "./view_mode.js"; import type FNote from "../../entities/fnote.js"; @@ -10,6 +10,8 @@ import dialogService from "../../services/dialog.js"; import attributes from "../../services/attributes.js"; import type { EventData } from "../../components/app_context.js"; import utils from "../../services/utils.js"; +import date_notes from "../../services/date_notes.js"; +import appContext from "../../components/app_context.js"; const TPL = `
@@ -67,6 +69,7 @@ export default class CalendarView extends ViewMode { private noteIds: string[]; private parentNote: FNote; private calendar?: Calendar; + private isCalendarRoot: boolean; constructor(args: ViewModeArgs) { super(args); @@ -75,7 +78,7 @@ export default class CalendarView extends ViewMode { this.$calendarContainer = this.$root.find(".calendar-container"); this.noteIds = args.noteIds; this.parentNote = args.parentNote; - console.log(args); + this.isCalendarRoot = false; args.$parent.append(this.$root); } @@ -84,20 +87,27 @@ export default class CalendarView extends ViewMode { } async renderList(): Promise | undefined> { - const isEditable = true; + this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot"); + const isEditable = !this.isCalendarRoot; const { Calendar } = await import("@fullcalendar/core"); const plugins: PluginDef[] = []; plugins.push((await import("@fullcalendar/daygrid")).default); - - if (isEditable) { + if (isEditable || this.isCalendarRoot) { plugins.push((await import("@fullcalendar/interaction")).default); } + let eventBuilder: EventSourceFunc; + if (!this.isCalendarRoot) { + eventBuilder = async () => await this.#buildEvents(this.noteIds) + } else { + eventBuilder = async (e: EventSourceFuncArg) => await this.#buildEventsForCalendar(e); + } + const calendar = new Calendar(this.$calendarContainer[0], { plugins, initialView: "dayGridMonth", - events: async () => await CalendarView.#buildEvents(this.noteIds), + events: eventBuilder, editable: isEditable, selectable: isEditable, select: (e) => this.#onCalendarSelection(e), @@ -117,7 +127,17 @@ export default class CalendarView extends ViewMode { html += utils.escapeHtml(e.event.title); return { html }; - }) + }), + dateClick: async (e) => { + if (!this.isCalendarRoot) { + return; + } + + const note = await date_notes.getDayNote(e.dateStr); + if (note) { + appContext.tabManager.getActiveContext().setNote(note.noteId); + } + } }); calendar.render(); this.calendar = calendar; @@ -210,39 +230,96 @@ export default class CalendarView extends ViewMode { } } - static async #buildEvents(noteIds: string[]) { + async #buildEventsForCalendar(e: EventSourceFuncArg) { + const events = []; + + // Gather all the required date note IDs. + const dateRange = utils.getMonthsInDateRange(e.startStr, e.endStr); + let allDateNoteIds: string[] = []; + for (const month of dateRange) { + // TODO: Deduplicate get type. + const dateNotesForMonth = await server.get>(`special-notes/notes-for-month/${month}`); + const dateNoteIds = Object.values(dateNotesForMonth); + allDateNoteIds = [ ...allDateNoteIds, ...dateNoteIds ]; + } + + // Request all the date notes. + const dateNotes = await froca.getNotes(allDateNoteIds); + const childNoteToDateMapping: Record = {}; + for (const dateNote of dateNotes) { + const startDate = dateNote.getLabelValue("dateNote"); + if (!startDate) { + continue; + } + + events.push(await CalendarView.#buildEvent(dateNote, startDate)); + + if (dateNote.hasChildren()) { + const childNoteIds = dateNote.getChildNoteIds(); + for (const childNoteId of childNoteIds) { + childNoteToDateMapping[childNoteId] = startDate; + } + } + } + + // Request all child notes of date notes in a single run. + const childNoteIds = Object.keys(childNoteToDateMapping); + const childNotes = await froca.getNotes(childNoteIds); + for (const childNote of childNotes) { + const startDate = childNoteToDateMapping[childNote.noteId]; + const event = await CalendarView.#buildEvent(childNote, startDate); + events.push(event); + } + + return events.flat(); + } + + async #buildEvents(noteIds: string[]) { const notes = await froca.getNotes(noteIds); const events: EventSourceInput = []; for (const note of notes) { - const startDate = note.getAttributeValue("label", "startDate"); - const customTitle = note.getAttributeValue("label", "calendar:title"); - const color = note.getAttributeValue("label", "calendar:color") ?? note.getAttributeValue("label", "color") ?? undefined; + let startDate = note.getLabelValue("startDate"); + + if (note.hasChildren()) { + const childrenEventData = await this.#buildEvents(note.getChildNoteIds()); + if (childrenEventData.length > 0) { + events.push(childrenEventData); + } + } if (!startDate) { continue; } - const titles = await CalendarView.#parseCustomTitle(customTitle, note); - for (const title of titles) { - const eventData: typeof events[0] = { - title: title, - start: startDate, - url: `#${note.noteId}`, - noteId: note.noteId, - color: color, - iconClass: note.getLabelValue("iconClass") - }; - - const endDate = CalendarView.#offsetDate(note.getAttributeValue("label", "endDate") ?? startDate, 1); - if (endDate) { - eventData.end = CalendarView.#formatDateToLocalISO(endDate); - } - - events.push(eventData); - } + const endDate = note.getAttributeValue("label", "endDate"); + events.push(await CalendarView.#buildEvent(note, startDate, endDate)); } + return events.flat(); + } + + static async #buildEvent(note: FNote, startDate: string, endDate?: string | null) { + const customTitle = note.getLabelValue("calendar:title"); + const titles = await CalendarView.#parseCustomTitle(customTitle, note); + const color = note.getLabelValue("calendar:color") ?? note.getLabelValue("color"); + const events: EventInput[] = []; + for (const title of titles) { + const eventData: EventInput = { + title: title, + start: startDate, + url: `#${note.noteId}`, + noteId: note.noteId, + color: color ?? undefined, + iconClass: note.getLabelValue("iconClass") + }; + + const endDateOffset = CalendarView.#offsetDate(endDate ?? startDate, 1); + if (endDateOffset) { + eventData.end = CalendarView.#formatDateToLocalISO(endDateOffset); + } + events.push(eventData); + } return events; }