2025-08-14 19:55:45 +03:00
|
|
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
2025-08-08 20:08:06 +03:00
|
|
|
import { EventData, EventNames } from "../../components/app_context";
|
|
|
|
|
import { ParentComponent } from "./ReactBasicWidget";
|
2025-08-08 23:23:07 +03:00
|
|
|
import SpacedUpdate from "../../services/spaced_update";
|
2025-08-14 17:36:11 +03:00
|
|
|
import { OptionNames } from "@triliumnext/commons";
|
|
|
|
|
import options from "../../services/options";
|
2025-08-14 18:18:45 +03:00
|
|
|
import utils, { reloadFrontendApp } from "../../services/utils";
|
2025-08-14 21:05:24 +03:00
|
|
|
import Component from "../../components/component";
|
|
|
|
|
|
|
|
|
|
type TriliumEventHandler<T extends EventNames> = (data: EventData<T>) => void;
|
|
|
|
|
const registeredHandlers: Map<Component, Map<EventNames, TriliumEventHandler<any>[]>> = new Map();
|
2025-08-08 20:08:06 +03:00
|
|
|
|
2025-08-10 15:11:43 +03:00
|
|
|
/**
|
|
|
|
|
* 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).
|
|
|
|
|
*/
|
2025-08-14 21:05:24 +03:00
|
|
|
export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: TriliumEventHandler<T>, enabled = true) {
|
2025-08-08 20:08:06 +03:00
|
|
|
const parentWidget = useContext(ParentComponent);
|
2025-08-14 21:05:24 +03:00
|
|
|
if (!parentWidget) {
|
|
|
|
|
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);
|
2025-08-08 20:08:06 +03:00
|
|
|
}
|
2025-08-14 21:05:24 +03:00
|
|
|
}
|
|
|
|
|
}, [ 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);
|
|
|
|
|
}
|
2025-08-08 20:08:06 +03:00
|
|
|
|
2025-08-14 21:05:24 +03:00
|
|
|
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;
|
|
|
|
|
|
2025-08-08 20:08:06 +03:00
|
|
|
return () => {
|
2025-08-14 21:05:24 +03:00
|
|
|
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);
|
|
|
|
|
registeredHandlers.get(parentWidget)?.set(eventName, newEventHandlers);
|
2025-08-08 20:08:06 +03:00
|
|
|
};
|
2025-08-14 21:05:24 +03:00
|
|
|
}, [ eventName, parentWidget, handler ]);
|
2025-08-08 23:23:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) {
|
2025-08-09 09:15:54 +03:00
|
|
|
const callbackRef = useRef(callback);
|
|
|
|
|
const spacedUpdateRef = useRef<SpacedUpdate>();
|
2025-08-08 23:23:07 +03:00
|
|
|
|
2025-08-09 09:15:54 +03:00
|
|
|
// Update callback ref when it changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
callbackRef.current = callback;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create SpacedUpdate instance only once
|
|
|
|
|
if (!spacedUpdateRef.current) {
|
|
|
|
|
spacedUpdateRef.current = new SpacedUpdate(
|
|
|
|
|
() => callbackRef.current(),
|
|
|
|
|
interval
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update interval if it changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
spacedUpdateRef.current?.setUpdateInterval(interval);
|
|
|
|
|
}, [interval]);
|
|
|
|
|
|
|
|
|
|
return spacedUpdateRef.current;
|
2025-08-14 17:36:11 +03:00
|
|
|
}
|
|
|
|
|
|
2025-08-14 18:18:45 +03:00
|
|
|
export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [string, (newValue: string) => Promise<void>] {
|
2025-08-14 17:36:11 +03:00
|
|
|
const initialValue = options.get(name);
|
|
|
|
|
const [ value, setValue ] = useState(initialValue);
|
|
|
|
|
|
2025-08-14 19:55:45 +03:00
|
|
|
const wrappedSetValue = useMemo(() => {
|
|
|
|
|
return async (newValue: string) => {
|
|
|
|
|
await options.save(name, newValue);
|
2025-08-14 18:18:45 +03:00
|
|
|
|
2025-08-14 19:55:45 +03:00
|
|
|
if (needsRefresh) {
|
|
|
|
|
reloadFrontendApp(`option change: ${name}`);
|
|
|
|
|
}
|
2025-08-14 18:18:45 +03:00
|
|
|
}
|
2025-08-14 19:55:45 +03:00
|
|
|
}, [ name, needsRefresh ]);
|
2025-08-14 17:47:45 +03:00
|
|
|
|
2025-08-14 19:55:45 +03:00
|
|
|
useTriliumEvent("entitiesReloaded", useCallback(({ loadResults }) => {
|
2025-08-14 17:47:45 +03:00
|
|
|
if (loadResults.getOptionNames().includes(name)) {
|
|
|
|
|
const newValue = options.get(name);
|
|
|
|
|
setValue(newValue);
|
|
|
|
|
}
|
2025-08-14 19:55:45 +03:00
|
|
|
}, [ name ]));
|
2025-08-14 17:47:45 +03:00
|
|
|
|
2025-08-14 17:36:11 +03:00
|
|
|
return [
|
|
|
|
|
value,
|
2025-08-14 17:47:45 +03:00
|
|
|
wrappedSetValue
|
2025-08-14 17:36:11 +03:00
|
|
|
]
|
2025-08-14 17:53:24 +03:00
|
|
|
}
|
|
|
|
|
|
2025-08-14 18:26:22 +03:00
|
|
|
export function useTriliumOptionBool(name: OptionNames): [boolean, (newValue: boolean) => Promise<void>] {
|
|
|
|
|
const [ value, setValue ] = useTriliumOption(name);
|
|
|
|
|
return [
|
|
|
|
|
(value === "true"),
|
|
|
|
|
(newValue) => setValue(newValue ? "true" : "false")
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-14 17:53:24 +03:00
|
|
|
/**
|
|
|
|
|
* Generates a unique name via a random alphanumeric string of a fixed length.
|
|
|
|
|
*
|
|
|
|
|
* <p>
|
|
|
|
|
* Generally used to assign names to inputs that are unique, especially useful for widgets inside tabs.
|
|
|
|
|
*
|
|
|
|
|
* @param prefix a prefix to add to the unique name.
|
|
|
|
|
* @returns a name with the given prefix and a random alpanumeric string appended to it.
|
|
|
|
|
*/
|
|
|
|
|
export function useUniqueName(prefix: string) {
|
|
|
|
|
return useMemo(() => prefix + utils.randomString(10), [ prefix]);
|
2025-08-08 20:08:06 +03:00
|
|
|
}
|