diff --git a/src/public/app/widgets/buttons/calendar.js b/src/public/app/widgets/buttons/calendar.ts similarity index 73% rename from src/public/app/widgets/buttons/calendar.js rename to src/public/app/widgets/buttons/calendar.ts index d7f2f7514..b279e7689 100644 --- a/src/public/app/widgets/buttons/calendar.js +++ b/src/public/app/widgets/buttons/calendar.ts @@ -8,6 +8,7 @@ import RightDropdownButtonWidget from "./right_dropdown_button.js"; import toastService from "../../services/toast.js"; import options from "../../services/options.js"; import { Dropdown } from "bootstrap"; +import type { EventData } from "../../components/app_context.js"; const MONTHS = [ t("calendar.january"), @@ -66,8 +67,26 @@ const DROPDOWN_TPL = ` const DAYS_OF_WEEK = [t("calendar.sun"), t("calendar.mon"), t("calendar.tue"), t("calendar.wed"), t("calendar.thu"), t("calendar.fri"), t("calendar.sat")]; +interface DateNotesForMonth { + [date: string]: string; +} + export default class CalendarWidget extends RightDropdownButtonWidget { - constructor(title, icon) { + private $month!: JQuery; + private $weekHeader!: JQuery; + private $monthSelect!: JQuery; + private $yearSelect!: JQuery; + private $next!: JQuery; + private $previous!: JQuery; + private $nextYear!: JQuery; + private $previousYear!: JQuery; + private monthDropdown!: Dropdown; + private firstDayOfWeek!: number; + private activeDate: Date | null = null; + private todaysDate!: Date; + private date!: Date; + + constructor(title: string = "", icon: string = "") { super(title, icon, DROPDOWN_TPL); } @@ -85,11 +104,14 @@ export default class CalendarWidget extends RightDropdownButtonWidget { // Don't trigger dropdownShown() at widget level when the month selection dropdown is shown, since it would cause a redundant refresh. e.stopPropagation(); }); - this.monthDropdown = Dropdown.getOrCreateInstance(this.$monthSelect); + this.monthDropdown = Dropdown.getOrCreateInstance(this.$monthSelect[0]); this.$dropdownContent.find('[data-calendar-input="month-list"] button').on("click", (e) => { - this.date.setMonth(e.target.dataset.value); - this.createMonth(); - this.monthDropdown.hide(); + const target = e.target as HTMLElement; + const value = target.dataset.value; + if (value) { + this.date.setMonth(parseInt(value)); + this.createMonth(); + } }); this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]'); this.$next.on("click", () => { @@ -97,7 +119,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.createMonth(); }); this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]'); - this.$previous.on("click", (e) => { + this.$previous.on("click", () => { this.date.setMonth(this.date.getMonth() - 1); this.createMonth(); }); @@ -105,7 +127,8 @@ export default class CalendarWidget extends RightDropdownButtonWidget { // Year navigation this.$yearSelect = this.$dropdownContent.find('[data-calendar-input="year"]'); this.$yearSelect.on("input", (e) => { - this.date.setFullYear(e.target.value); + const target = e.target as HTMLInputElement; + this.date.setFullYear(parseInt(target.value)); this.createMonth(); }); this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]'); @@ -114,40 +137,54 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.createMonth(); }); this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]'); - this.$previousYear.on("click", (e) => { + this.$previousYear.on("click", () => { this.date.setFullYear(this.date.getFullYear() - 1); this.createMonth(); }); - this.$dropdownContent.find(".calendar-header").on("click", (e) => e.stopPropagation()); - this.$dropdownContent.on("click", ".calendar-date", async (ev) => { const date = $(ev.target).closest(".calendar-date").attr("data-calendar-date"); - const note = await dateNoteService.getDayNote(date); + if (date) { + const note = await dateNoteService.getDayNote(date); - if (note) { - appContext.tabManager.getActiveContext().setNote(note.noteId); - this.dropdown.hide(); - } else { - toastService.showError(t("calendar.cannot_find_day_note")); + if (note) { + appContext.tabManager.getActiveContext().setNote(note.noteId); + this.dropdown?.hide(); + } else { + toastService.showError(t("calendar.cannot_find_day_note")); + } } ev.stopPropagation(); }); - // Prevent dismissing the calendar popup by clicking on an empty space inside it. - this.$dropdownContent.on("click", (e) => e.stopPropagation()); + // Handle click events for the entire calendar widget + this.$dropdownContent.on("click", (e) => { + const $target = $(e.target); + + // Keep dropdown open when clicking on month select button or year selector area + if ($target.closest('.btn.dropdown-toggle.select-button').length || + $target.closest('.calendar-year-selector').length) { + e.stopPropagation(); + return; + } + + // Hide dropdown for all other cases + this.monthDropdown.hide(); + // Prevent dismissing the calendar popup by clicking on an empty space inside it. + e.stopPropagation(); + }); } manageFirstDayOfWeek() { - this.firstDayOfWeek = options.getInt("firstDayOfWeek"); + this.firstDayOfWeek = options.getInt("firstDayOfWeek") || 0; // Generate the list of days of the week taking into consideration the user's selected first day of week. let localeDaysOfWeek = [...DAYS_OF_WEEK]; const daysToBeAddedAtEnd = localeDaysOfWeek.splice(0, this.firstDayOfWeek); localeDaysOfWeek = [...localeDaysOfWeek, ...daysToBeAddedAtEnd]; - this.$weekHeader.html(localeDaysOfWeek.map((el) => `${el}`)); + this.$weekHeader.html(localeDaysOfWeek.map((el) => `${el}`).join('')); } async dropdownShown() { @@ -158,7 +195,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.init(activeNote?.getOwnedLabelValue("dateNote")); } - init(activeDate) { + init(activeDate: string | null) { // attaching time fixes local timezone handling this.activeDate = activeDate ? new Date(`${activeDate}T12:00:00`) : null; this.todaysDate = new Date(); @@ -168,9 +205,9 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.createMonth(); } - createDay(dateNotesForMonth, num, day) { + createDay(dateNotesForMonth: DateNotesForMonth, num: number, day: number) { const $newDay = $("").addClass("calendar-date").attr("data-calendar-date", utils.formatDateISO(this.date)); - const $date = $("").html(num); + const $date = $("").html(String(num)); // if it's the first day of the month if (num === 1) { @@ -202,23 +239,25 @@ export default class CalendarWidget extends RightDropdownButtonWidget { return $newDay; } - isEqual(a, b) { + isEqual(a: Date, b: Date | null) { if ((!a && b) || (a && !b)) { return false; } + if (!b) return false; + return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); } async createMonth() { const month = utils.formatDateISO(this.date).substr(0, 7); - const dateNotesForMonth = await server.get(`special-notes/notes-for-month/${month}`); + const dateNotesForMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${month}`); this.$month.empty(); const currentMonth = this.date.getMonth(); while (this.date.getMonth() === currentMonth) { - const $day = this.createDay(dateNotesForMonth, this.date.getDate(), this.date.getDay(), this.date.getFullYear()); + const $day = this.createDay(dateNotesForMonth, this.date.getDate(), this.date.getDay()); this.$month.append($day); @@ -232,7 +271,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget { this.$yearSelect.val(this.date.getFullYear()); } - async entitiesReloadedEvent({ loadResults }) { + async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { if (!loadResults.getOptionNames().includes("firstDayOfWeek")) { return; } diff --git a/src/public/app/widgets/buttons/history_navigation.js b/src/public/app/widgets/buttons/history_navigation.ts similarity index 52% rename from src/public/app/widgets/buttons/history_navigation.js rename to src/public/app/widgets/buttons/history_navigation.ts index 4fdaec363..9fa54c60a 100644 --- a/src/public/app/widgets/buttons/history_navigation.js +++ b/src/public/app/widgets/buttons/history_navigation.ts @@ -2,17 +2,36 @@ import utils from "../../services/utils.js"; import contextMenu from "../../menus/context_menu.js"; import treeService from "../../services/tree.js"; import ButtonFromNoteWidget from "./button_from_note.js"; +import type FNote from "../../entities/fnote.js"; +import type { CommandNames } from "../../components/app_context.js"; + +interface WebContents { + history: string[]; + getActiveIndex(): number; + clearHistory(): void; + canGoBack(): boolean; + canGoForward(): boolean; + goToIndex(index: string): void; +} + +interface ContextMenuItem { + title: string; + idx: string; + uiIcon: string; +} export default class HistoryNavigationButton extends ButtonFromNoteWidget { - constructor(launcherNote, command) { + private webContents?: WebContents; + + constructor(launcherNote: FNote, command: string) { super(); this.title(() => launcherNote.title) .icon(() => launcherNote.getIcon()) - .command(() => command) + .command(() => command as CommandNames) .titlePlacement("right") .buttonNoteIdProvider(() => launcherNote.noteId) - .onContextMenu((e) => this.showContextMenu(e)) + .onContextMenu((e) => { if (e) this.showContextMenu(e); }) .class("launcher-button"); } @@ -23,35 +42,31 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget { this.webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); // without this, the history is preserved across frontend reloads - this.webContents.clearHistory(); + this.webContents?.clearHistory(); this.refresh(); } } - async showContextMenu(e) { + async showContextMenu(e: JQuery.ContextMenuEvent) { e.preventDefault(); - // API is broken and will be replaced: https://github.com/electron/electron/issues/33899 - // until then no context menu - if (true) { - // avoid warning in dev console + if (!this.webContents || this.webContents.history.length < 2) { return; } - if (this.webContents.history.length < 2) { - return; - } - - let items = []; + let items: ContextMenuItem[] = []; const activeIndex = this.webContents.getActiveIndex(); + const history = this.webContents.history; - for (const idx in this.webContents.history) { - const url = this.webContents.history[idx]; - const [_, notePathWithTab] = url.split("#"); - // broken: use linkService.parseNavigationStateFromUrl(); - const [notePath, ntxId] = notePathWithTab.split("-"); + for (const idx in history) { + const url = history[idx]; + const parts = url.split("#"); + if (parts.length < 2) continue; + + const notePathWithTab = parts[1]; + const notePath = notePathWithTab.split("-")[0]; const title = await treeService.getNotePathTitle(notePath); @@ -59,9 +74,9 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget { title, idx, uiIcon: - idx == activeIndex + parseInt(idx) === activeIndex ? "bx bx-radio-circle-marked" // compare with type coercion! - : idx < activeIndex + : parseInt(idx) < activeIndex ? "bx bx-left-arrow-alt" : "bx bx-right-arrow-alt" }); @@ -77,22 +92,14 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget { x: e.pageX, y: e.pageY, items, - selectMenuItemHandler: ({ idx }) => this.webContents.goToIndex(idx) + selectMenuItemHandler: (item: any) => { + if (item && item.idx && this.webContents) { + this.webContents.goToIndex(item.idx); + } + } }); } - refresh() { - if (!utils.isElectron()) { - return; - } - - // disabling this because in electron 9 there's a weird performance problem which makes these webContents calls - // block UI thread for > 1 second on specific notes (book notes displaying underlying render notes with scripts) - - // this.$backInHistory.toggleClass('disabled', !this.webContents.canGoBack()); - // this.$forwardInHistory.toggleClass('disabled', !this.webContents.canGoForward()); - } - activeNoteChangedEvent() { this.refresh(); }