refactor(react): use beta approach for handling events everywhere

This commit is contained in:
Elian Doran 2025-08-24 21:18:48 +03:00
parent a507991808
commit f2db7baeba
No known key found for this signature in database
37 changed files with 129 additions and 202 deletions

View File

@ -1,6 +1,8 @@
import utils from "../services/utils.js";
import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js";
type EventHandler = ((data: any) => void);
/**
* Abstract class for all components in the Trilium's frontend.
*
@ -19,6 +21,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
initialized: Promise<void> | null;
parent?: TypedComponent<any>;
_position!: number;
private listeners: Record<string, EventHandler[]> | null = {};
constructor() {
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
@ -76,6 +79,14 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
const promises: Promise<unknown>[] = [];
// Handle React children.
if (this.listeners?.[name]) {
for (const listener of this.listeners[name]) {
listener(data);
}
}
// Handle legacy children.
for (const child of this.children) {
const ret = child.handleEvent(name, data) as Promise<void>;
@ -120,6 +131,35 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return promise;
}
registerHandler<T extends EventNames>(name: T, handler: EventHandler) {
if (!this.listeners) {
this.listeners = {};
}
if (!this.listeners[name]) {
this.listeners[name] = [];
}
if (this.listeners[name].includes(handler)) {
return;
}
this.listeners[name].push(handler);
}
removeHandler<T extends EventNames>(name: T, handler: EventHandler) {
if (!this.listeners?.[name]?.includes(handler)) {
return;
}
this.listeners[name] = this.listeners[name]
.filter(listener => listener !== handler);
if (!this.listeners[name].length) {
delete this.listeners[name];
}
}
}
export default class Component extends TypedComponent<Component> {}

View File

@ -278,12 +278,9 @@ export function wrapReactWidgets<T extends TypedComponent<any>>(components: (T |
return wrappedResult;
}
type EventHandler = ((data: any) => void);
export class ReactWrappedWidget extends BasicWidget {
private el: VNode;
private listeners: Record<string, EventHandler[]> = {};
constructor(el: VNode) {
super();
@ -294,41 +291,4 @@ export class ReactWrappedWidget extends BasicWidget {
this.$widget = renderReactWidget(this, this.el);
}
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
if (!this.listeners[name]) {
return;
}
for (const listener of this.listeners[name]) {
listener(data);
}
super.handleEvent(name, data);
}
registerHandler<T extends EventNames>(name: T, handler: EventHandler) {
if (!this.listeners[name]) {
this.listeners[name] = [];
}
if (this.listeners[name].includes(handler)) {
return;
}
this.listeners[name].push(handler);
}
removeHandler<T extends EventNames>(name: T, handler: EventHandler) {
if (!this.listeners[name]?.includes(handler)) {
return;
}
this.listeners[name] = this.listeners[name]
.filter(listener => listener !== handler);
if (!this.listeners[name].length) {
delete this.listeners[name];
}
}
}

View File

@ -7,14 +7,14 @@ import openService from "../../services/open.js";
import { useState } from "preact/hooks";
import type { CSSProperties } from "preact/compat";
import type { AppInfo } from "@triliumnext/commons";
import { useTriliumEventBeta } from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
export default function AboutDialog() {
let [appInfo, setAppInfo] = useState<AppInfo | null>(null);
let [shown, setShown] = useState(false);
const forceWordBreak: CSSProperties = { wordBreak: "break-all" };
useTriliumEventBeta("openAboutDialog", () => setShown(true));
useTriliumEvent("openAboutDialog", () => setShown(true));
return (
<Modal className="about-dialog"

View File

@ -11,7 +11,7 @@ import { default as TextTypeWidget } from "../type_widgets/editable_text.js";
import { logError } from "../../services/ws";
import FormGroup from "../react/FormGroup.js";
import { refToJQuerySelector } from "../react/react_utils";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
type LinkType = "reference-link" | "external-link" | "hyper-link";
@ -24,7 +24,7 @@ export default function AddLinkDialog() {
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
const [ shown, setShown ] = useState(false);
useTriliumEventBeta("showAddLinkDialog", ( { textTypeWidget, text }) => {
useTriliumEvent("showAddLinkDialog", ( { textTypeWidget, text }) => {
setTextTypeWidget(textTypeWidget);
initialText.current = text;
setShown(true);

View File

@ -8,7 +8,7 @@ import froca from "../../services/froca.js";
import tree from "../../services/tree.js";
import Button from "../react/Button.jsx";
import FormGroup from "../react/FormGroup.js";
import { useTriliumEventBeta } from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
import FBranch from "../../entities/fbranch.js";
export default function BranchPrefixDialog() {
@ -17,7 +17,7 @@ export default function BranchPrefixDialog() {
const [ prefix, setPrefix ] = useState(branch?.prefix ?? "");
const branchInput = useRef<HTMLInputElement>(null);
useTriliumEventBeta("editBranchPrefix", async () => {
useTriliumEvent("editBranchPrefix", async () => {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (!notePath) {
return;

View File

@ -12,7 +12,7 @@ import toast from "../../services/toast";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function BulkActionsDialog() {
const [ selectedOrActiveNoteIds, setSelectedOrActiveNoteIds ] = useState<string[]>();
@ -22,7 +22,7 @@ export default function BulkActionsDialog() {
const [ existingActions, setExistingActions ] = useState<RenameNoteBulkAction[]>([]);
const [ shown, setShown ] = useState(false);
useTriliumEventBeta("openBulkActionsDialog", async ({ selectedOrActiveNoteIds }) => {
useTriliumEvent("openBulkActionsDialog", async ({ selectedOrActiveNoteIds }) => {
setSelectedOrActiveNoteIds(selectedOrActiveNoteIds);
setBulkActionNote(await froca.getNote("_bulkAction"));
setShown(true);
@ -46,7 +46,7 @@ export default function BulkActionsDialog() {
refreshExistingActions();
}, [refreshExistingActions]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((row) =>
row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) {
refreshExistingActions();

View File

@ -13,7 +13,7 @@ import tree from "../../services/tree";
import branches from "../../services/branches";
import toast from "../../services/toast";
import NoteList from "../react/NoteList";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function CloneToDialog() {
const [ clonedNoteIds, setClonedNoteIds ] = useState<string[]>();
@ -22,7 +22,7 @@ export default function CloneToDialog() {
const [ shown, setShown ] = useState(false);
const autoCompleteRef = useRef<HTMLInputElement>(null);
useTriliumEventBeta("cloneNoteIdsTo", ({ noteIds }) => {
useTriliumEvent("cloneNoteIdsTo", ({ noteIds }) => {
if (!noteIds || noteIds.length === 0) {
noteIds = [appContext.tabManager.getActiveContextNoteId() ?? ""];
}

View File

@ -3,7 +3,7 @@ import Button from "../react/Button";
import { t } from "../../services/i18n";
import { useState } from "preact/hooks";
import FormCheckbox from "../react/FormCheckbox";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
interface ConfirmDialogProps {
title?: string;
@ -27,8 +27,8 @@ export default function ConfirmDialog() {
setShown(true);
}
useTriliumEventBeta("showConfirmDialog", ({ message, callback }) => showDialog(null, message, callback, false));
useTriliumEventBeta("showConfirmDeleteNoteBoxWithNoteDialog", ({ title, callback }) => showDialog(title, t("confirm.are_you_sure_remove_note", { title: title }), callback, true));
useTriliumEvent("showConfirmDialog", ({ message, callback }) => showDialog(null, message, callback, false));
useTriliumEvent("showConfirmDeleteNoteBoxWithNoteDialog", ({ title, callback }) => showDialog(title, t("confirm.are_you_sure_remove_note", { title: title }), callback, true));
return (
<Modal

View File

@ -2,7 +2,6 @@ import { useRef, useState, useEffect } from "preact/hooks";
import { t } from "../../services/i18n.js";
import FormCheckbox from "../react/FormCheckbox.js";
import Modal from "../react/Modal.js";
import ReactBasicWidget from "../react/ReactBasicWidget.js";
import type { DeleteNotesPreview } from "@triliumnext/commons";
import server from "../../services/server.js";
import froca from "../../services/froca.js";
@ -10,7 +9,7 @@ import FNote from "../../entities/fnote.js";
import link from "../../services/link.js";
import Button from "../react/Button.jsx";
import Alert from "../react/Alert.jsx";
import useTriliumEvent, { useTriliumEventBeta } from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
export interface ResolveOptions {
proceed: boolean;
@ -39,7 +38,7 @@ export default function DeleteNotesDialog() {
const [ shown, setShown ] = useState(false);
const okButtonRef = useRef<HTMLButtonElement>(null);
useTriliumEventBeta("showDeleteNotesDialog", (opts) => {
useTriliumEvent("showDeleteNotesDialog", (opts) => {
setOpts(opts);
setShown(true);
})

View File

@ -11,7 +11,7 @@ import toastService, { ToastOptions } from "../../services/toast";
import utils from "../../services/utils";
import open from "../../services/open";
import froca from "../../services/froca";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
interface ExportDialogProps {
branchId?: string | null;
@ -27,7 +27,7 @@ export default function ExportDialog() {
const [ opmlVersion, setOpmlVersion ] = useState("2.0");
const [ shown, setShown ] = useState(false);
useTriliumEventBeta("showExportDialog", async ({ notePath, defaultType }) => {
useTriliumEvent("showExportDialog", async ({ notePath, defaultType }) => {
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
if (!parentNoteId) {
return;

View File

@ -5,11 +5,11 @@ import { CommandNames } from "../../components/app_context.js";
import RawHtml from "../react/RawHtml.jsx";
import { useEffect, useState } from "preact/hooks";
import keyboard_actions from "../../services/keyboard_actions.js";
import { useTriliumEventBeta } from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
export default function HelpDialog() {
const [ shown, setShown ] = useState(false);
useTriliumEventBeta("showCheatsheet", () => setShown(true));
useTriliumEvent("showCheatsheet", () => setShown(true));
return (
<Modal

View File

@ -9,7 +9,7 @@ import Modal from "../react/Modal";
import RawHtml from "../react/RawHtml";
import ReactBasicWidget from "../react/ReactBasicWidget";
import importService, { UploadFilesOptions } from "../../services/import";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function ImportDialog() {
const [ parentNoteId, setParentNoteId ] = useState<string>();
@ -23,7 +23,7 @@ export default function ImportDialog() {
const [ replaceUnderscoresWithSpaces, setReplaceUnderscoresWithSpaces ] = useState(true);
const [ shown, setShown ] = useState(false);
useTriliumEventBeta("showImportDialog", ({ noteId }) => {
useTriliumEvent("showImportDialog", ({ noteId }) => {
setParentNoteId(noteId);
tree.getNoteTitle(noteId).then(setNoteTitle);
setShown(true);

View File

@ -9,7 +9,7 @@ import { Suggestion, triggerRecentNotes } from "../../services/note_autocomplete
import tree from "../../services/tree";
import froca from "../../services/froca";
import EditableTextTypeWidget from "../type_widgets/editable_text";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function IncludeNoteDialog() {
const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();

View File

@ -4,12 +4,12 @@ import utils from "../../services/utils.js";
import Button from "../react/Button.js";
import Modal from "../react/Modal.js";
import { useState } from "preact/hooks";
import { useTriliumEventBeta } from "../react/hooks.jsx";
import { useTriliumEvent } from "../react/hooks.jsx";
export default function IncorrectCpuArchDialogComponent() {
const [ shown, setShown ] = useState(false);
const downloadButtonRef = useRef<HTMLButtonElement>(null);
useTriliumEventBeta("showCpuArchWarning", () => setShown(true));
useTriliumEvent("showCpuArchWarning", () => setShown(true));
return (
<Modal

View File

@ -4,14 +4,14 @@ import { t } from "../../services/i18n";
import Button from "../react/Button";
import { useRef, useState } from "preact/hooks";
import { RawHtmlBlock } from "../react/RawHtml";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function InfoDialog() {
const [ opts, setOpts ] = useState<EventData<"showInfoDialog">>();
const [ shown, setShown ] = useState(false);
const okButtonRef = useRef<HTMLButtonElement>(null);
useTriliumEventBeta("showInfoDialog", (opts) => {
useTriliumEvent("showInfoDialog", (opts) => {
setOpts(opts);
setShown(true);
});

View File

@ -7,7 +7,7 @@ import note_autocomplete, { Suggestion } from "../../services/note_autocomplete"
import appContext from "../../components/app_context";
import commandRegistry from "../../services/command_registry";
import { refToJQuerySelector } from "../react/react_utils";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
@ -50,8 +50,8 @@ export default function JumpToNoteDialogComponent() {
setLastOpenedTs(Date.now());
}
useTriliumEventBeta("jumpToNote", () => openDialog(false));
useTriliumEventBeta("commandPalette", () => openDialog(true));
useTriliumEvent("jumpToNote", () => openDialog(false));
useTriliumEvent("commandPalette", () => openDialog(true));
async function onItemSelected(suggestion?: Suggestion | null) {
if (!suggestion) {

View File

@ -6,7 +6,7 @@ import toast from "../../services/toast";
import utils from "../../services/utils";
import Modal from "../react/Modal";
import Button from "../react/Button";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
interface RenderMarkdownResponse {
htmlContent: string;
@ -32,8 +32,8 @@ export default function MarkdownImportDialog() {
}
}, []);
useTriliumEventBeta("importMarkdownInline", triggerImport);
useTriliumEventBeta("pasteMarkdownIntoText", triggerImport);
useTriliumEvent("importMarkdownInline", triggerImport);
useTriliumEvent("pasteMarkdownIntoText", triggerImport);
async function sendForm() {
await convertMarkdownToHtml(text);

View File

@ -11,7 +11,7 @@ import tree from "../../services/tree";
import froca from "../../services/froca";
import branches from "../../services/branches";
import toast from "../../services/toast";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function MoveToDialog() {
const [ movedBranchIds, setMovedBranchIds ] = useState<string[]>();
@ -19,7 +19,7 @@ export default function MoveToDialog() {
const [ shown, setShown ] = useState(false);
const autoCompleteRef = useRef<HTMLInputElement>(null);
useTriliumEventBeta("moveBranchIdsTo", ({ branchIds }) => {
useTriliumEvent("moveBranchIdsTo", ({ branchIds }) => {
setMovedBranchIds(branchIds);
setShown(true);
});

View File

@ -9,7 +9,7 @@ import { MenuCommandItem, MenuItem } from "../../menus/context_menu";
import { TreeCommandNames } from "../../menus/tree_context_menu";
import { Suggestion } from "../../services/note_autocomplete";
import Badge from "../react/Badge";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export interface ChooseNoteTypeResponse {
success: boolean;
@ -31,7 +31,7 @@ export default function NoteTypeChooserDialogComponent() {
const [ parentNote, setParentNote ] = useState<Suggestion | null>();
const [ noteTypes, setNoteTypes ] = useState<MenuItem<TreeCommandNames>[]>([]);
useTriliumEventBeta("chooseNoteType", ({ callback }) => {
useTriliumEvent("chooseNoteType", ({ callback }) => {
setCallback(() => callback);
setShown(true);
});

View File

@ -3,11 +3,11 @@ import { t } from "../../services/i18n";
import Button from "../react/Button";
import appContext from "../../components/app_context";
import { useState } from "preact/hooks";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function PasswordNotSetDialog() {
const [ shown, setShown ] = useState(false);
useTriliumEventBeta("showPasswordNotSet", () => setShown(true));
useTriliumEvent("showPasswordNotSet", () => setShown(true));
return (
<Modal

View File

@ -5,7 +5,7 @@ import Modal from "../react/Modal";
import FormTextBox from "../react/FormTextBox";
import FormGroup from "../react/FormGroup";
import { refToJQuerySelector } from "../react/react_utils";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
// JQuery here is maintained for compatibility with existing code.
interface ShownCallbackData {
@ -36,7 +36,7 @@ export default function PromptDialog() {
const [ shown, setShown ] = useState(false);
const submitValue = useRef<string>(null);
useTriliumEventBeta("showPromptDialog", (newOpts) => {
useTriliumEvent("showPromptDialog", (newOpts) => {
opts.current = newOpts;
setValue(newOpts.defaultValue ?? "");
setShown(true);

View File

@ -4,15 +4,15 @@ import Button from "../react/Button";
import FormTextBox from "../react/FormTextBox";
import Modal from "../react/Modal";
import protected_session from "../../services/protected_session";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function ProtectedSessionPasswordDialog() {
const [ shown, setShown ] = useState(false);
const [ password, setPassword ] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
useTriliumEventBeta("showProtectedSessionPasswordDialog", () => setShown(true));
useTriliumEventBeta("closeProtectedSessionPasswordDialog", () => setShown(false));
useTriliumEvent("showProtectedSessionPasswordDialog", () => setShown(true));
useTriliumEvent("closeProtectedSessionPasswordDialog", () => setShown(false));
return (
<Modal

View File

@ -13,7 +13,7 @@ import { formatDateTime } from "../../utils/formatters";
import link from "../../services/link";
import RawHtml from "../react/RawHtml";
import ws from "../../services/ws";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function RecentChangesDialog() {
const [ ancestorNoteId, setAncestorNoteId ] = useState<string>();
@ -21,7 +21,7 @@ export default function RecentChangesDialog() {
const [ needsRefresh, setNeedsRefresh ] = useState(false);
const [ shown, setShown ] = useState(false);
useTriliumEventBeta("showRecentChanges", ({ ancestorNoteId }) => {
useTriliumEvent("showRecentChanges", ({ ancestorNoteId }) => {
setNeedsRefresh(true);
setAncestorNoteId(ancestorNoteId ?? hoisted_note.getHoistedNoteId());
setShown(true);

View File

@ -8,7 +8,6 @@ import server from "../../services/server";
import toast from "../../services/toast";
import Button from "../react/Button";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import FormList, { FormListItem } from "../react/FormList";
import utils from "../../services/utils";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
@ -18,7 +17,7 @@ import type { CSSProperties } from "preact/compat";
import open from "../../services/open";
import ActionButton from "../react/ActionButton";
import options from "../../services/options";
import useTriliumEvent from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function RevisionsDialog() {
const [ note, setNote ] = useState<FNote>();

View File

@ -7,7 +7,7 @@ import FormTextBox from "../react/FormTextBox";
import Modal from "../react/Modal";
import server from "../../services/server";
import FormGroup from "../react/FormGroup";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function SortChildNotesDialog() {
const [ parentNoteId, setParentNoteId ] = useState<string>();
@ -18,7 +18,7 @@ export default function SortChildNotesDialog() {
const [ sortLocale, setSortLocale ] = useState("");
const [ shown, setShown ] = useState(false);
useTriliumEventBeta("sortChildNotes", ({ node }) => {
useTriliumEvent("sortChildNotes", ({ node }) => {
setParentNoteId(node.data.noteId);
setShown(true);
});

View File

@ -9,7 +9,7 @@ import ReactBasicWidget from "../react/ReactBasicWidget";
import options from "../../services/options";
import importService from "../../services/import.js";
import tree from "../../services/tree";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function UploadAttachmentsDialog() {
const [ parentNoteId, setParentNoteId ] = useState<string>();
@ -19,7 +19,7 @@ export default function UploadAttachmentsDialog() {
const [ description, setDescription ] = useState<string | undefined>(undefined);
const [ shown, setShown ] = useState(false);
useTriliumEventBeta("showUploadAttachmentsDialog", ({ noteId }) => {
useTriliumEvent("showUploadAttachmentsDialog", ({ noteId }) => {
setParentNoteId(noteId);
setShown(true);
});

View File

@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "preact/hooks";
import { t } from "../services/i18n";
import FormTextBox from "./react/FormTextBox";
import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEventBeta } from "./react/hooks";
import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEvent } from "./react/hooks";
import protected_session_holder from "../services/protected_session_holder";
import server from "../services/server";
import "./note_title.css";
@ -48,12 +48,12 @@ export default function NoteTitleWidget() {
useEffect(() => {
appContext.addBeforeUnloadListener(() => spacedUpdate.isAllSavedAndTriggerUpdate());
}, []);
useTriliumEventBeta([ "beforeNoteSwitch", "beforeNoteContextRemove" ], () => spacedUpdate.updateNowIfNecessary());
useTriliumEvent([ "beforeNoteSwitch", "beforeNoteContextRemove" ], () => spacedUpdate.updateNowIfNecessary());
// Manage focus.
const textBoxRef = useRef<HTMLInputElement>(null);
const isNewNote = useRef<boolean>();
useTriliumEventBeta([ "focusOnTitle", "focusAndSelectTitle" ], (e) => {
useTriliumEvent([ "focusOnTitle", "focusAndSelectTitle" ], (e) => {
if (noteContext?.isActive() && textBoxRef.current) {
textBoxRef.current.focus();
isNewNote.current = ("isNewNote" in e ? e.isNewNote : false);

View File

@ -5,94 +5,25 @@ import SpacedUpdate from "../../services/spaced_update";
import { OptionNames } from "@triliumnext/commons";
import options, { type OptionValue } from "../../services/options";
import utils, { reloadFrontendApp } from "../../services/utils";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import FBlob from "../../entities/fblob";
import NoteContextAwareWidget from "../note_context_aware_widget";
import { Ref, RefObject, VNode } from "preact";
import { RefObject, VNode } from "preact";
import { Tooltip } from "bootstrap";
import { CSSProperties } from "preact/compat";
type TriliumEventHandler<T extends EventNames> = (data: EventData<T>) => void;
const registeredHandlers: Map<Component, Map<EventNames, TriliumEventHandler<any>[]>> = new Map();
/**
* Allows a React component to react to Trilium events (e.g. `entitiesReloaded`). When the desired event is triggered, the handler is invoked with the event parameters.
*
* Under the hood, it works by altering the parent (Trilium) component of the React element to introduce the corresponding event.
*
* @param eventName the name of the Trilium event to listen for.
* @param handler the handler to be invoked when the event is triggered.
* @param enabled determines whether the event should be listened to or not. Useful to conditionally limit the listener based on a state (e.g. a modal being displayed).
*/
export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: TriliumEventHandler<T>, enabled = true) {
const parentWidget = useContext(ParentComponent);
if (!parentWidget) {
export function useTriliumEvent<T extends EventNames>(eventName: T | T[], handler: TriliumEventHandler<T>) {
const parentComponent = useContext(ParentComponent);
if (!parentComponent) {
console.error("React widget has no legacy parent component. Event handling will not work.", new Error().stack);
return;
}
const handlerName = `${eventName}Event`;
const customHandler = useMemo(() => {
return async (data: EventData<T>) => {
// Inform the attached event listeners.
const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName) ?? [];
for (const eventHandler of eventHandlers) {
eventHandler(data);
}
}
}, [ eventName, parentWidget ]);
useEffect(() => {
// Attach to the list of handlers.
let handlersByWidget = registeredHandlers.get(parentWidget);
if (!handlersByWidget) {
handlersByWidget = new Map();
registeredHandlers.set(parentWidget, handlersByWidget);
}
let handlersByWidgetAndEventName = handlersByWidget.get(eventName);
if (!handlersByWidgetAndEventName) {
handlersByWidgetAndEventName = [];
handlersByWidget.set(eventName, handlersByWidgetAndEventName);
}
if (!handlersByWidgetAndEventName.includes(handler)) {
handlersByWidgetAndEventName.push(handler);
}
// Apply the custom event handler.
if (parentWidget[handlerName] && parentWidget[handlerName] !== customHandler) {
console.warn(`Widget ${parentWidget.componentId} already had an event listener and it was replaced by the React one.`);
}
parentWidget[handlerName] = customHandler;
return () => {
const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName);
if (!eventHandlers || !eventHandlers.includes(handler)) {
return;
}
// Remove the event handler from the array.
const newEventHandlers = eventHandlers.filter(e => e !== handler);
if (newEventHandlers.length) {
registeredHandlers.get(parentWidget)?.set(eventName, newEventHandlers);
} else {
registeredHandlers.get(parentWidget)?.delete(eventName);
}
if (!registeredHandlers.get(parentWidget)?.size) {
registeredHandlers.delete(parentWidget);
}
};
}, [ eventName, parentWidget, handler ]);
}
export function useTriliumEventBeta<T extends EventNames>(eventName: T | T[], handler: TriliumEventHandler<T>) {
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
if (Array.isArray(eventName)) {
for (const eventSingleName of eventName) {
@ -185,7 +116,7 @@ export function useTriliumOptionBeta(name: OptionNames, needsRefresh?: boolean):
}
}, [ name, needsRefresh ]);
useTriliumEventBeta("entitiesReloaded", useCallback(({ loadResults }) => {
useTriliumEvent("entitiesReloaded", useCallback(({ loadResults }) => {
if (loadResults.getOptionNames().includes(name)) {
const newValue = options.get(name);
setValue(newValue);
@ -283,20 +214,20 @@ export function useNoteContext() {
setNote(noteContext?.note);
}, [ notePath ]);
useTriliumEventBeta("activeContextChanged", ({ noteContext }) => {
useTriliumEvent("activeContextChanged", ({ noteContext }) => {
setNoteContext(noteContext);
setNotePath(noteContext.notePath);
});
useTriliumEventBeta("setNoteContext", ({ noteContext }) => {
useTriliumEvent("setNoteContext", ({ noteContext }) => {
setNoteContext(noteContext);
});
useTriliumEventBeta("noteSwitchedAndActivated", ({ noteContext }) => {
useTriliumEvent("noteSwitchedAndActivated", ({ noteContext }) => {
setNoteContext(noteContext);
});
useTriliumEventBeta("noteSwitched", ({ noteContext, notePath }) => {
useTriliumEvent("noteSwitched", ({ noteContext, notePath }) => {
setNotePath(notePath);
});
useTriliumEventBeta("frocaReloaded", () => {
useTriliumEvent("frocaReloaded", () => {
setNote(noteContext?.note);
});
@ -336,7 +267,7 @@ export function useNoteProperty<T extends keyof FNote>(note: FNote | null | unde
useEffect(() => refreshValue(), [ note, note[property] ]);
// Watch for external changes.
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.isNoteReloaded(note.noteId, componentId)) {
refreshValue();
}
@ -349,7 +280,7 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: st
const [ relationValue, setRelationValue ] = useState<string | null | undefined>(note?.getRelationValue(relationName));
useEffect(() => setRelationValue(note?.getRelationValue(relationName) ?? null), [ note ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
for (const attr of loadResults.getAttributeRows()) {
if (attr.type === "relation" && attr.name === relationName && attributes.isAffecting(attr, note)) {
setRelationValue(attr.value ?? null);
@ -380,7 +311,7 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: string):
const [ labelValue, setLabelValue ] = useState<string | null | undefined>(note?.getLabelValue(labelName));
useEffect(() => setLabelValue(note?.getLabelValue(labelName) ?? null), [ note ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
for (const attr of loadResults.getAttributeRows()) {
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
setLabelValue(attr.value ?? null);
@ -409,7 +340,7 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: s
useEffect(() => setLabelValue(!!note?.hasLabel(labelName)), [ note ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
for (const attr of loadResults.getAttributeRows()) {
if (attr.type === "label" && attr.name === labelName && attributes.isAffecting(attr, note)) {
setLabelValue(!attr.isDeleted);
@ -442,7 +373,7 @@ export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | un
}
useEffect(refresh, [ note?.noteId ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.hasRevisionForNote(note.noteId)) {
refresh();
}

View File

@ -3,7 +3,7 @@ import Dropdown from "../react/Dropdown";
import { NOTE_TYPES } from "../../services/note_types";
import { FormDropdownDivider, FormListBadge, FormListItem } from "../react/FormList";
import { getAvailableLocales, t } from "../../services/i18n";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEventBeta, useTriliumOption, useTriliumOptionBeta, useTriliumOptionJson } from "../react/hooks";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption, useTriliumOptionBeta, useTriliumOptionJson } from "../react/hooks";
import mime_types from "../../services/mime_types";
import { Locale, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
import server from "../../services/server";
@ -179,7 +179,7 @@ function BookmarkSwitch({ note }: { note?: FNote | null }) {
}, [ note ]);
useEffect(() => refreshState(), [ note ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
}
@ -228,7 +228,7 @@ function SharedSwitch({ note }: { note?: FNote | null }) {
}, [ note ]);
useEffect(() => refreshState(), [ note ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.getBranchRows().find((b) => b.noteId === note.noteId)) {
refreshState();
}

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "preact/hooks";
import { TabContext } from "./ribbon-interface";
import FAttribute from "../../entities/fattribute";
import { useLegacyWidget, useTriliumEventBeta } from "../react/hooks";
import { useLegacyWidget, useTriliumEvent } from "../react/hooks";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import attribute_renderer from "../../services/attribute_renderer";
@ -29,7 +29,7 @@ export default function InheritedAttributesTab({ note, componentId }: TabContext
}
useEffect(refresh, [ note ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) {
refresh();
}

View File

@ -7,7 +7,7 @@ import Button from "../react/Button";
import { formatDateTime } from "../../utils/formatters";
import { formatSize } from "../../services/utils";
import LoadingSpinner from "../react/LoadingSpinner";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function NoteInfoTab({ note }: TabContext) {
const [ metadata, setMetadata ] = useState<MetadataResponse>();
@ -26,7 +26,7 @@ export default function NoteInfoTab({ note }: TabContext) {
}
useEffect(refresh, [ note?.noteId ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
const noteId = note?.noteId;
if (noteId && (loadResults.isNoteReloaded(noteId) || loadResults.isNoteContentReloaded(noteId))) {
refresh();

View File

@ -1,7 +1,7 @@
import { TabContext } from "./ribbon-interface";
import { t } from "../../services/i18n";
import Button from "../react/Button";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
import { useEffect, useMemo, useState } from "preact/hooks";
import { NotePathRecord } from "../../entities/fnote";
import NoteLink from "../react/NoteLink";
@ -18,7 +18,7 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
}
useEffect(refresh, [ note?.noteId ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
const noteId = note?.noteId;
if (!noteId) return;
if (loadResults.getBranchRows().find((branch) => branch.noteId === noteId)

View File

@ -8,7 +8,7 @@ import toast from "../../services/toast";
import froca from "../../services/froca";
import { useContext, useEffect, useState } from "preact/hooks";
import { ParentComponent } from "../react/react_utils";
import { useTriliumEventBeta } from "../react/hooks";
import { useTriliumEvent } from "../react/hooks";
import appContext from "../../components/app_context";
import server from "../../services/server";
import ws from "../../services/ws";
@ -65,7 +65,7 @@ export default function SearchDefinitionTab({ note, ntxId }: TabContext) {
// Refresh the list of available and active options.
useEffect(refreshOptions, [ note ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((attrRow) => attributes.isAffecting(attrRow, note))) {
refreshOptions();
}
@ -166,7 +166,7 @@ function BulkActionsList({ note }: { note: FNote }) {
// React to changes.
useEffect(refreshBulkActions, [ note ]);
useTriliumEventBeta("entitiesReloaded", ({loadResults}) => {
useTriliumEvent("entitiesReloaded", ({loadResults}) => {
if (loadResults.getAttributeRows().find(attr => attr.type === "label" && attr.name === "action" && attributes.isAffecting(attr, note))) {
refreshBulkActions();
}

View File

@ -4,7 +4,7 @@ import { t } from "../../../services/i18n";
import server from "../../../services/server";
import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete";
import CKEditor, { CKEditorApi } from "../../react/CKEditor";
import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEventBeta } from "../../react/hooks";
import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent } from "../../react/hooks";
import FAttribute from "../../../entities/fattribute";
import attribute_renderer from "../../../services/attribute_renderer";
import FNote from "../../../entities/fnote";
@ -215,7 +215,7 @@ export default function AttributeEditor({ note, componentId, notePath, ntxId }:
}
useEffect(() => refresh(), [ note ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) {
console.log("Trigger due to entities reloaded");
refresh();
@ -257,11 +257,11 @@ export default function AttributeEditor({ note, componentId, notePath, ntxId }:
}), []));
// Keyboard shortcuts
useTriliumEventBeta("addNewLabel", ({ ntxId: eventNtxId }) => {
useTriliumEvent("addNewLabel", ({ ntxId: eventNtxId }) => {
if (eventNtxId !== ntxId) return;
handleAddNewAttributeCommand("addNewLabel");
});
useTriliumEventBeta("addNewRelation", ({ ntxId: eventNtxId }) => {
useTriliumEvent("addNewRelation", ({ ntxId: eventNtxId }) => {
if (eventNtxId !== ntxId) return;
handleAddNewAttributeCommand("addNewRelation");
});

View File

@ -76,7 +76,6 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextA
*/
export default class ContentWidgetTypeWidget extends TypeWidget {
private $content!: JQuery<HTMLElement>;
private widget?: BasicWidget;
static getType() {
return "contentWidget";
@ -113,7 +112,6 @@ export default class ContentWidgetTypeWidget extends TypeWidget {
this.child(widget);
this.$content.append(widget.render());
this.widget = widget;
await widget.refresh();
}
return;

View File

@ -10,7 +10,7 @@ import toast from "../../../services/toast";
import dialog from "../../../services/dialog";
import { formatDateTime } from "../../../utils/formatters";
import ActionButton from "../../react/ActionButton";
import useTriliumEvent from "../../react/hooks";
import { useTriliumEvent } from "../../react/hooks";
type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>;
type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>;

View File

@ -11,7 +11,7 @@ import { useCallback, useEffect, useState } from "preact/hooks";
import server from "../../../services/server";
import options from "../../../services/options";
import dialog from "../../../services/dialog";
import useTriliumEvent from "../../react/hooks";
import { useTriliumEvent } from "../../react/hooks";
export default function ShortcutSettings() {
const [ keyboardShortcuts, setKeyboardShortcuts ] = useState<KeyboardShortcut[]>([]);