chore(react/type_widget): save on change

This commit is contained in:
Elian Doran
2025-09-22 12:41:32 +03:00
parent 1e323de01b
commit f6631b7b9a
3 changed files with 55 additions and 27 deletions

View File

@@ -1,22 +1,30 @@
import { HTMLProps, useEffect, useRef } from "preact/compat"; import { HTMLProps, RefObject, useEffect, useRef, useState } from "preact/compat";
import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig } from "@triliumnext/ckeditor5"; import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor } from "@triliumnext/ckeditor5";
import { buildConfig, BuildEditorOptions } from "./config"; import { buildConfig, BuildEditorOptions } from "./config";
import { Editor } from "tabulator-tables";
interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "className" | "tabIndex"> { interface CKEditorWithWatchdogProps extends Pick<HTMLProps<HTMLDivElement>, "className" | "tabIndex"> {
content?: string;
isClassicEditor?: boolean; isClassicEditor?: boolean;
watchdogRef: RefObject<EditorWatchdog>;
watchdogConfig?: WatchdogConfig; watchdogConfig?: WatchdogConfig;
buildEditorOpts: Omit<BuildEditorOptions, "isClassicEditor">; buildEditorOpts: Omit<BuildEditorOptions, "isClassicEditor">;
onNotificationWarning?: (evt: any, data: any) => void; onNotificationWarning?: (evt: any, data: any) => void;
onWatchdogStateChange?: (watchdog: EditorWatchdog<any>) => void; onWatchdogStateChange?: (watchdog: EditorWatchdog<any>) => void;
onChange: () => void;
} }
export default function CKEditorWithWatchdog({ className, tabIndex, isClassicEditor, watchdogConfig, buildEditorOpts, onNotificationWarning, onWatchdogStateChange }: CKEditorWithWatchdogProps) { export default function CKEditorWithWatchdog({ content, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, buildEditorOpts, onNotificationWarning, onWatchdogStateChange, onChange }: CKEditorWithWatchdogProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const watchdogRef = useRef<EditorWatchdog>(null);
const [ editor, setEditor ] = useState<CKTextEditor>();
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;
if (!container) return; if (!container) return;
const watchdog = buildWatchdog(!!isClassicEditor, watchdogConfig); const watchdog = buildWatchdog(!!isClassicEditor, watchdogConfig);
watchdogRef.current = watchdog;
externalWatchdogRef.current = watchdog;
watchdog.setCreator(async () => { watchdog.setCreator(async () => {
const editor = await buildEditor(container, !!isClassicEditor, { const editor = await buildEditor(container, !!isClassicEditor, {
...buildEditorOpts, ...buildEditorOpts,
@@ -27,6 +35,8 @@ export default function CKEditorWithWatchdog({ className, tabIndex, isClassicEdi
editor.plugins.get("Notification").on("show:warning", onNotificationWarning); editor.plugins.get("Notification").on("show:warning", onNotificationWarning);
} }
setEditor(editor);
return editor; return editor;
}); });
@@ -39,6 +49,16 @@ export default function CKEditorWithWatchdog({ className, tabIndex, isClassicEdi
return () => watchdog.destroy(); return () => watchdog.destroy();
}, []); }, []);
// React to content changes.
useEffect(() => editor?.setData(content ?? ""), [ editor, content ]);
// React to on change listener.
useEffect(() => {
if (!editor) return;
editor.model.document.on("change:data", onChange);
return () => editor.model.document.off("change:data", onChange);
}, [ editor, onChange ]);
return ( return (
<div ref={containerRef} className={className} tabIndex={tabIndex}> <div ref={containerRef} className={className} tabIndex={tabIndex}>

View File

@@ -1,11 +1,12 @@
import { useRef, useState } from "preact/hooks";
import dialog from "../../../services/dialog"; import dialog from "../../../services/dialog";
import toast from "../../../services/toast"; import toast from "../../../services/toast";
import { isMobile } from "../../../services/utils"; import utils, { isMobile } from "../../../services/utils";
import { useNoteLabel, useTriliumOption } from "../../react/hooks"; import { useEditorSpacedUpdate, useNoteLabel, useTriliumOption } from "../../react/hooks";
import { TypeWidgetProps } from "../type_widget"; import { TypeWidgetProps } from "../type_widget";
import CKEditorWithWatchdog from "./CKEditorWithWatchdog"; import CKEditorWithWatchdog from "./CKEditorWithWatchdog";
import "./EditableText.css"; import "./EditableText.css";
import type { EditorWatchdog } from "@triliumnext/ckeditor5"; import { EditorWatchdog } from "@triliumnext/ckeditor5";
/** /**
* The editor can operate into two distinct modes: * The editor can operate into two distinct modes:
@@ -14,15 +15,40 @@ import type { EditorWatchdog } from "@triliumnext/ckeditor5";
* - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works. * - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works.
*/ */
export default function EditableText({ note }: TypeWidgetProps) { export default function EditableText({ note }: TypeWidgetProps) {
const [ content, setContent ] = useState<string>();
const watchdogRef = useRef<EditorWatchdog>(null);
const [ language ] = useNoteLabel(note, "language"); const [ language ] = useNoteLabel(note, "language");
const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType"); const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType");
const isClassicEditor = isMobile() || textNoteEditorType === "ckeditor-classic"; const isClassicEditor = isMobile() || textNoteEditorType === "ckeditor-classic";
const spacedUpdate = useEditorSpacedUpdate({
note,
getData() {
const editor = watchdogRef.current?.editor;
if (!editor) {
// There is nothing to save, most likely a result of the editor crashing and reinitializing.
return;
}
const content = editor.getData() ?? "";
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty,
// this is important when setting a new note to code
return {
content: utils.isHtmlEmpty(content) ? "" : content
};
},
onContentChange(newContent) {
setContent(newContent);
}
})
return ( return (
<div class="note-detail-editable-text note-detail-printable"> <div class="note-detail-editable-text note-detail-printable">
<CKEditorWithWatchdog {note && <CKEditorWithWatchdog
className="note-detail-editable-text-editor use-tn-links" tabIndex={300} className="note-detail-editable-text-editor use-tn-links" tabIndex={300}
content={content}
isClassicEditor={isClassicEditor} isClassicEditor={isClassicEditor}
watchdogRef={watchdogRef}
watchdogConfig={{ watchdogConfig={{
// An average number of milliseconds between the last editor errors (defaults to 5000). When the period of time between errors is lower than that and the crashNumberLimit is also reached, the watchdog changes its state to crashedPermanently, and it stops restarting the editor. This prevents an infinite restart loop. // An average number of milliseconds between the last editor errors (defaults to 5000). When the period of time between errors is lower than that and the crashNumberLimit is also reached, the watchdog changes its state to crashedPermanently, and it stops restarting the editor. This prevents an infinite restart loop.
minimumNonErrorTimePeriod: 5000, minimumNonErrorTimePeriod: 5000,
@@ -37,7 +63,8 @@ export default function EditableText({ note }: TypeWidgetProps) {
}} }}
onNotificationWarning={onNotificationWarning} onNotificationWarning={onNotificationWarning}
onWatchdogStateChange={onWatchdogStateChange} onWatchdogStateChange={onWatchdogStateChange}
/> onChange={() => spacedUpdate.scheduleUpdate()}
/>}
</div> </div>
) )
} }

View File

@@ -74,8 +74,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
} }
} }
editor.model.document.on("change:data", () => this.spacedUpdate.scheduleUpdate());
if (import.meta.env.VITE_CKEDITOR_ENABLE_INSPECTOR === "true") { if (import.meta.env.VITE_CKEDITOR_ENABLE_INSPECTOR === "true") {
const CKEditorInspector = (await import("@ckeditor/ckeditor5-inspector")).default; const CKEditorInspector = (await import("@ckeditor/ckeditor5-inspector")).default;
CKEditorInspector.attach(editor); CKEditorInspector.attach(editor);
@@ -102,27 +100,10 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const newContentLanguage = this.note?.getLabelValue("language"); const newContentLanguage = this.note?.getLabelValue("language");
if (this.contentLanguage !== newContentLanguage) { if (this.contentLanguage !== newContentLanguage) {
await this.reinitializeWithData(data); await this.reinitializeWithData(data);
} else {
this.watchdog.editor?.setData(data);
} }
}); });
} }
getData() {
if (!this.watchdog.editor) {
// There is nothing to save, most likely a result of the editor crashing and reinitializing.
return;
}
const content = this.watchdog.editor?.getData() ?? "";
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty,
// this is important when setting a new note to code
return {
content: utils.isHtmlEmpty(content) ? "" : content
};
}
focus() { focus() {
const editor = this.watchdog.editor; const editor = this.watchdog.editor;
if (editor) { if (editor) {