| 
									
										
										
										
											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"; | 
					
						
							| 
									
										
										
										
											2025-08-14 23:54:32 +03:00
										 |  |  | import options, { type OptionValue } 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"; | 
					
						
							| 
									
										
										
										
											2025-08-15 11:21:19 +03:00
										 |  |  | import server from "../../services/server"; | 
					
						
							| 
									
										
										
										
											2025-08-14 21:05:24 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | 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-18 09:34:16 +03:00
										 |  |  | /** | 
					
						
							|  |  |  |  * Allows a React component to read and write a Trilium option, while also watching for external changes. | 
					
						
							|  |  |  |  *  | 
					
						
							|  |  |  |  * Conceptually, `useTriliumOption` works just like `useState`, but the value is also automatically updated if | 
					
						
							|  |  |  |  * the option is changed somewhere else in the client. | 
					
						
							|  |  |  |  *  | 
					
						
							|  |  |  |  * @param name the name of the option to listen for. | 
					
						
							|  |  |  |  * @param needsRefresh whether to reload the frontend whenever the value is changed. | 
					
						
							|  |  |  |  * @returns an array where the first value is the current option value and the second value is the setter. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2025-08-14 23:54:32 +03:00
										 |  |  | export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [string, (newValue: OptionValue) => 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(() => { | 
					
						
							| 
									
										
										
										
											2025-08-14 23:54:32 +03:00
										 |  |  |         return async (newValue: OptionValue) => { | 
					
						
							| 
									
										
										
										
											2025-08-14 19:55:45 +03:00
										 |  |  |             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-18 09:34:16 +03:00
										 |  |  | export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean): [boolean, (newValue: boolean) => Promise<void>] { | 
					
						
							|  |  |  |     const [ value, setValue ] = useTriliumOption(name, needsRefresh); | 
					
						
							| 
									
										
										
										
											2025-08-14 18:26:22 +03:00
										 |  |  |     return [ | 
					
						
							|  |  |  |         (value === "true"), | 
					
						
							|  |  |  |         (newValue) => setValue(newValue ? "true" : "false") | 
					
						
							|  |  |  |     ] | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-14 23:54:32 +03:00
										 |  |  | export function useTriliumOptionInt(name: OptionNames): [number, (newValue: number) => Promise<void>] { | 
					
						
							|  |  |  |     const [ value, setValue ] = useTriliumOption(name); | 
					
						
							|  |  |  |     return [ | 
					
						
							|  |  |  |         (parseInt(value, 10)), | 
					
						
							|  |  |  |         (newValue) => setValue(newValue) | 
					
						
							|  |  |  |     ] | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-15 10:26:25 +03:00
										 |  |  | export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) => Promise<void> ] { | 
					
						
							|  |  |  |     const [ value, setValue ] = useTriliumOption(name); | 
					
						
							|  |  |  |     return [ | 
					
						
							|  |  |  |         (JSON.parse(value) as T), | 
					
						
							|  |  |  |         (newValue => setValue(JSON.stringify(newValue))) | 
					
						
							|  |  |  |     ]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-08-15 11:21:19 +03:00
										 |  |  | export function useTriliumOptions<T extends OptionNames>(...names: T[]) { | 
					
						
							|  |  |  |     const values: Record<string, string> = {}; | 
					
						
							|  |  |  |     for (const name of names) { | 
					
						
							|  |  |  |         values[name] = options.get(name); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const setValue = (newValues: Record<T, string>) => server.put<void>("options", newValues); | 
					
						
							|  |  |  |     return [ | 
					
						
							|  |  |  |         values as Record<T, string>, | 
					
						
							|  |  |  |         setValue | 
					
						
							|  |  |  |     ] as const; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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
										 |  |  | } |