chore(react/launch_bar): get calendar to render days

This commit is contained in:
Elian Doran 2025-12-04 19:03:28 +02:00
parent 604488b166
commit 5c8445f3fe
No known key found for this signature in database
5 changed files with 165 additions and 139 deletions

View File

@ -8,7 +8,6 @@ import options from "../../services/options.js";
import { Dropdown } from "bootstrap";
import type { EventData } from "../../components/app_context.js";
import { dayjs, type Dayjs } from "@triliumnext/commons";
import "../../stylesheets/calendar.css";
import type { AttributeRow, OptionDefinitions } from "@triliumnext/commons";
const MONTHS = [
@ -28,12 +27,6 @@ const MONTHS = [
const DROPDOWN_TPL = `
<div class="calendar-dropdown-widget">
<style>
.calendar-dropdown-widget {
width: 400px;
}
</style>
<div class="calendar-header">
<div class="calendar-month-selector">
<button class="calendar-btn tn-tool-button bx bx-chevron-left" data-calendar-toggle="previous"></button>
@ -61,7 +54,6 @@ const DROPDOWN_TPL = `
</div>
<div class="calendar-week"></div>
<div class="calendar-body" data-calendar-area="month"></div>
</div>`;
const DAYS_OF_WEEK = [
@ -74,9 +66,7 @@ const DAYS_OF_WEEK = [
t("calendar.sat")
];
interface DateNotesForMonth {
[date: string]: string;
}
interface WeekCalculationOptions {
firstWeekType: number;
@ -228,9 +218,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
// Store firstDayOfWeek as ISO (17)
manageFirstDayOfWeek() {
const rawFirstDayOfWeek = options.getInt("firstDayOfWeek") || 0;
this.firstDayOfWeekISO = rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek;
let localeDaysOfWeek = [...DAYS_OF_WEEK];
const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek);
localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted];
@ -244,34 +231,13 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
};
}
getWeekStartDate(date: Dayjs): Dayjs {
const currentISO = date.isoWeekday();
const diff = (currentISO - this.firstDayOfWeekISO + 7) % 7;
return date.clone().subtract(diff, "day").startOf("day");
}
getWeekNumber(date: Dayjs): number {
const weekStart = this.getWeekStartDate(date);
return weekStart.isoWeek();
}
async dropdownShown() {
await this.getWeekNoteEnable();
this.weekNotes = await server.get<string[]>(`attribute-values/weekNote`);
this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null);
this.init( ?? null);
}
init(activeDate: string | null) {
this.activeDate = activeDate ? dayjs(`${activeDate}T12:00:00`) : null;
this.todaysDate = dayjs();
this.date = dayjs(this.activeDate || this.todaysDate).startOf('month');
this.createMonth();
}
createDay(dateNotesForMonth: DateNotesForMonth, num: number) {
const $newDay = $("<a>")
.addClass("calendar-date")
.attr("data-calendar-date", this.date.local().format('YYYY-MM-DD'));
createDay() {
const $date = $("<span>").html(String(num));
const dateNoteId = dateNotesForMonth[this.date.local().format('YYYY-MM-DD')];
@ -304,105 +270,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
return $newWeekNumber;
}
// Use isoWeekday() consistently
private getPrevMonthDays(firstDayISO: number): { weekNumber: number, dates: Dayjs[] } {
const prevMonthLastDay = this.date.subtract(1, 'month').endOf('month');
const daysToAdd = (firstDayISO - this.firstDayOfWeekISO + 7) % 7;
const dates: Dayjs[] = [];
const firstDay = this.date.startOf('month');
const weekNumber = this.getWeekNumber(firstDay);
// Get dates from previous month
for (let i = daysToAdd - 1; i >= 0; i--) {
dates.push(prevMonthLastDay.subtract(i, 'day'));
}
return { weekNumber, dates };
}
private getNextMonthDays(lastDayISO: number): Dayjs[] {
const nextMonthFirstDay = this.date.add(1, 'month').startOf('month');
const dates: Dayjs[] = [];
const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1; // ISO wrap
const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7;
for (let i = 0; i < daysToAdd; i++) {
dates.push(nextMonthFirstDay.add(i, 'day'));
}
return dates;
}
async createMonth() {
const month = this.date.format('YYYY-MM');
const dateNotesForMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${month}`);
this.$month.empty();
const firstDay = this.date.startOf('month');
const firstDayISO = firstDay.isoWeekday();
// Previous month filler
if (firstDayISO !== this.firstDayOfWeekISO) {
const { weekNumber, dates } = this.getPrevMonthDays(firstDayISO);
const prevMonth = this.date.subtract(1, 'month').format('YYYY-MM');
const dateNotesForPrevMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${prevMonth}`);
const $weekNumber = this.createWeekNumber(weekNumber);
this.$month.append($weekNumber);
dates.forEach(date => {
const tempDate = this.date;
this.date = date;
const $day = this.createDay(dateNotesForPrevMonth, date.date());
$day.addClass('calendar-date-prev-month');
this.$month.append($day);
this.date = tempDate;
});
}
const currentMonth = this.date.month();
// Main month
while (this.date.month() === currentMonth) {
const weekNumber = this.getWeekNumber(this.date);
if (this.date.isoWeekday() === this.firstDayOfWeekISO) {
const $weekNumber = this.createWeekNumber(weekNumber);
this.$month.append($weekNumber);
}
const $day = this.createDay(dateNotesForMonth, this.date.date());
this.$month.append($day);
this.date = this.date.add(1, 'day');
}
// while loop trips over and day is at 30/31, bring it back
this.date = this.date.startOf('month').subtract(1, 'month');
// Add dates from next month
const lastDayOfMonth = this.date.endOf('month');
const lastDayISO = lastDayOfMonth.isoWeekday();
const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1;
if (lastDayISO !== lastDayOfUserWeek) {
const dates = this.getNextMonthDays(lastDayISO);
const nextMonth = this.date.add(1, 'month').format('YYYY-MM');
const dateNotesForNextMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${nextMonth}`);
dates.forEach(date => {
const tempDate = this.date;
this.date = date;
const $day = this.createDay(dateNotesForNextMonth, date.date());
$day.addClass('calendar-date-next-month');
this.$month.append($day);
this.date = tempDate;
});
}
this.$monthSelect.text(MONTHS[this.date.month()]);
this.$yearSelect.val(this.date.year());
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
const WEEK_OPTIONS: (keyof OptionDefinitions)[] = [
"firstDayOfWeek",

View File

@ -1,4 +1,3 @@
import CalendarWidget from "../buttons/calendar.js";
import SyncStatusWidget from "../sync_status.js";
import BasicWidget, { wrapReactWidgets } from "../basic_widget.js";
import utils, { isMobile } from "../../services/utils.js";
@ -16,6 +15,7 @@ import QuickSearchWidget from "../quick_search.js";
import { ParentComponent } from "../react/react_utils.jsx";
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "../launch_bar/launch_bar_widgets.jsx";
import CalendarWidget from "../launch_bar/CalendarWidget.jsx";
interface InnerWidget extends BasicWidget {
settings?: {
@ -98,7 +98,7 @@ export default class LauncherWidget extends BasicWidget {
const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
case "calendar":
return new CalendarWidget(note.title, note.getIcon());
return <CalendarWidget launcherNote={note} />
case "spacer":
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");

View File

@ -0,0 +1,159 @@
import { useEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import { LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
import { Dayjs, dayjs } from "@triliumnext/commons";
import appContext from "../../components/app_context";
import { useTriliumOptionInt } from "../react/hooks";
import { VNode } from "preact";
import clsx from "clsx";
import "./CalendarWidget.css";
import server from "../../services/server";
interface DateNotesForMonth {
[date: string]: string;
}
export default function CalendarWidget({ launcherNote }: { launcherNote: FNote }) {
const { title, icon } = useLauncherIconAndTitle(launcherNote);
const [ date, setDate ] = useState<Dayjs>();
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek") ?? 0;
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
useEffect(() => {
})
return (
<LaunchBarDropdownButton
icon={icon} title={title}
onShown={() => {
const dateNote = appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote");
const activeDate = dateNote ? dayjs(`${dateNote}T12:00:00`) : null;
const todaysDate = dayjs();
const date = dayjs(activeDate || todaysDate).startOf('month');
setDate(date);
}}
>
{date && <div className="calendar-dropdown-widget" style={{ width: 400 }}>
<Calendar date={date} firstDayOfWeekISO={firstDayOfWeekISO} />
</div>}
</LaunchBarDropdownButton>
)
}
function Calendar({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) {
const month = date.format('YYYY-MM');
const firstDay = date.startOf('month');
const firstDayISO = firstDay.isoWeekday();
return (
<div className="calendar-body" data-calendar-area="month">
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays date={date} firstDayISO={firstDayISO} firstDayOfWeekISO={firstDayOfWeekISO} />}
<CurrentMonthDays date={date} firstDayOfWeekISO={firstDayOfWeekISO} />
<NextMonthDays date={date} firstDayOfWeekISO={firstDayOfWeekISO} />
</div>
)
}
function PreviousMonthDays({ date, firstDayISO, firstDayOfWeekISO }: { date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number }) {
const prevMonth = date.subtract(1, 'month').format('YYYY-MM');
const { weekNumber, dates } = getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO);
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState<DateNotesForMonth>();
useEffect(() => {
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${prevMonth}`).then(setDateNotesForPrevMonth);
}, [ date ]);
return dates.map(date => (
<CalendarDay date={date} dateNotesForMonth={dateNotesForPrevMonth} className="calendar-date-prev-month" />
));
}
function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) {
const dates = getCurMonthDays(date, firstDayOfWeekISO);
return dates.map(date => (
<CalendarDay date={date} dateNotesForMonth={{}} />
));
}
function NextMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) {
const lastDayOfMonth = date.endOf('month');
const lastDayISO = lastDayOfMonth.isoWeekday();
const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1;
const nextMonth = date.add(1, 'month').format('YYYY-MM');
const [ dateNotesForNextMonth, setDateNotesForNextMonth ] = useState<DateNotesForMonth>();
useEffect(() => {
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${nextMonth}`).then(setDateNotesForNextMonth);
}, [ date ]);
const dates = lastDayISO !== lastDayOfUserWeek ? getNextMonthDays(date, lastDayISO, firstDayOfWeekISO) : [];
return dates.map(date => (
<CalendarDay date={date} dateNotesForMonth={dateNotesForNextMonth} className="calendar-date-next-month" />
));
}
function CalendarDay({ date, dateNotesForMonth, className }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string }) {
return (
<a
className={clsx("calendar-date", className)}
data-calendar-date={date.local().format("YYYY-MM-DD")}
>
<span>
{date.date()}
</span>
</a>
);
}
function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): { weekNumber: number, dates: Dayjs[] } {
const prevMonthLastDay = date.subtract(1, 'month').endOf('month');
const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7;
const dates: Dayjs[] = [];
const firstDay = date.startOf('month');
const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO);
// Get dates from previous month
for (let i = daysToAdd - 1; i >= 0; i--) {
dates.push(prevMonthLastDay.subtract(i, 'day'));
}
return { weekNumber, dates };
}
function getCurMonthDays(date: Dayjs, firstDayOfWeekISO: number) {
let dateCursor = date;
const currentMonth = date.month();
const dates: Dayjs[] = [];
while (dateCursor.month() === currentMonth) {
dates.push(dateCursor);
dateCursor = dateCursor.add(1, "day");
}
return dates;
}
function getNextMonthDays(date: Dayjs, lastDayISO: number, firstDayOfWeekISO): Dayjs[] {
const nextMonthFirstDay = date.add(1, 'month').startOf('month');
const dates: Dayjs[] = [];
const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1; // ISO wrap
const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7;
for (let i = 0; i < daysToAdd; i++) {
dates.push(nextMonthFirstDay.add(i, 'day'));
}
return dates;
}
function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number {
const weekStart = getWeekStartDate(date, firstDayOfWeekISO);
return weekStart.isoWeek();
}
function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs {
const currentISO = date.isoWeekday();
const diff = (currentISO - firstDayOfWeekISO + 7) % 7;
return date.clone().subtract(diff, "day").startOf("day");
}

View File

@ -20,7 +20,7 @@ export function LaunchBarActionButton(props: Omit<ActionButtonProps, "className"
)
}
export function LaunchBarDropdownButton({ children, icon, ...props }: Pick<DropdownProps, "title" | "children"> & { icon: string }) {
export function LaunchBarDropdownButton({ children, icon, ...props }: Pick<DropdownProps, "title" | "children" | "onShown"> & { icon: string }) {
return (
<Dropdown
className="right-dropdown-widget"