diff --git a/apps/client/src/widgets/react/NoteStore.ts b/apps/client/src/widgets/react/NoteStore.ts new file mode 100644 index 000000000..6f34c6d23 --- /dev/null +++ b/apps/client/src/widgets/react/NoteStore.ts @@ -0,0 +1,33 @@ +type Listener = () => void; + +class NoteSavedDataStore { + private data = new Map(); + private listeners = new Map>(); + + get(noteId: string) { + return this.data.get(noteId); + } + + set(noteId: string, value: string) { + this.data.set(noteId, value); + this.listeners.get(noteId)?.forEach(l => l()); + } + + subscribe(noteId: string, listener: Listener) { + let set = this.listeners.get(noteId); + if (!set) { + set = new Set(); + this.listeners.set(noteId, set); + } + set.add(listener); + + return () => { + set!.delete(listener); + if (set!.size === 0) { + this.listeners.delete(noteId); + } + }; + } +} + +export const noteSavedDataStore = new NoteSavedDataStore(); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index c5e9711d4..19233cded 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -3,7 +3,7 @@ import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } f import { Tooltip } from "bootstrap"; import Mark from "mark.js"; import { RefObject, VNode } from "preact"; -import { CSSProperties } from "preact/compat"; +import { CSSProperties, useSyncExternalStore } from "preact/compat"; import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; import appContext, { EventData, EventNames } from "../../components/app_context"; @@ -27,6 +27,8 @@ import ws from "../../services/ws"; import BasicWidget, { ReactWrappedWidget } from "../basic_widget"; import NoteContextAwareWidget from "../note_context_aware_widget"; import { DragData } from "../note_tree"; +import CKEditor from "./CKEditor"; +import { noteSavedDataStore } from "./NoteStore"; import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils"; export function useTriliumEvent(eventName: T, handler: (data: EventData) => void) { @@ -114,6 +116,7 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha protected_session_holder.touchProtectedSessionIfNecessary(note); await server.put(`notes/${note.noteId}/data`, data, parentComponent?.componentId); + noteSavedDataStore.set(note.noteId, data.content); dataSaved?.(data); }; }, [ note, getData, dataSaved ]); @@ -122,6 +125,7 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha // React to note/blob changes. useEffect(() => { if (!blob) return; + noteSavedDataStore.set(note.noteId, blob.content); spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob.content)); }, [ blob ]); @@ -153,6 +157,14 @@ export function useEditorSpacedUpdate({ note, noteContext, getData, onContentCha return spacedUpdate; } +export function useNoteSavedData(noteId: string | undefined) { + return useSyncExternalStore( + (cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {}, + () => noteId ? noteSavedDataStore.get(noteId) : undefined + ); +} + + /** * Allows a React component to read and write a Trilium option, while also watching for external changes. *