diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index 415d73904..d4fd9d22a 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -33,6 +33,7 @@ const TYPE_MAPPINGS: Record Promise<{ default: TypeWidge "attachmentList": async () => (await import("./type_widgets/Attachment")).AttachmentList, "attachmentDetail": async () => (await import("./type_widgets/Attachment")).AttachmentDetail, "readOnlyText": () => import("./type_widgets/text/ReadOnlyText"), + "editableText": () => import("./type_widgets/text/EditableText"), "render": () => import("./type_widgets/Render"), "canvas": () => import("./type_widgets/Canvas") // TODO: finalize the record. diff --git a/apps/client/src/widgets/type_widgets/text/CKEditorWithWatchdog.tsx b/apps/client/src/widgets/type_widgets/text/CKEditorWithWatchdog.tsx new file mode 100644 index 000000000..3773c121b --- /dev/null +++ b/apps/client/src/widgets/type_widgets/text/CKEditorWithWatchdog.tsx @@ -0,0 +1,56 @@ +import { HTMLProps, useEffect, useRef } from "preact/compat"; +import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig } from "@triliumnext/ckeditor5"; +import { buildConfig, BuildEditorOptions } from "./config"; + +interface CKEditorWithWatchdogProps extends Pick, "className" | "tabIndex"> { + isClassicEditor?: boolean; + watchdogConfig?: WatchdogConfig; + buildEditorOpts: Omit; +} + +export default function CKEditorWithWatchdog({ className, tabIndex, isClassicEditor, watchdogConfig, buildEditorOpts }: CKEditorWithWatchdogProps) { + const containerRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + const watchdog = buildWatchdog(!!isClassicEditor, watchdogConfig); + watchdog.setCreator(async () => { + const editor = buildEditor(container, !!isClassicEditor, { + ...buildEditorOpts, + isClassicEditor: !!isClassicEditor + }); + return editor; + }); + watchdog.create(container); + }, []); + + return ( +
+ +
+ ); +} + +function buildWatchdog(isClassicEditor: boolean, watchdogConfig?: WatchdogConfig) { + if (isClassicEditor) { + return new EditorWatchdog(ClassicEditor, watchdogConfig); + } else { + return new EditorWatchdog(PopupEditor, watchdogConfig); + } +} + +async function buildEditor(element: HTMLElement, isClassicEditor: boolean, opts: BuildEditorOptions) { + const editorClass = isClassicEditor ? ClassicEditor : PopupEditor; + let config = await buildConfig(opts); + let editor = await editorClass.create(element, config); + + if (editor.isReadOnly) { + editor.destroy(); + + opts.forceGplLicense = true; + config = await buildConfig(opts); + editor = await editorClass.create(element, config); + } + return editor; +} diff --git a/apps/client/src/widgets/type_widgets/text/EditableText.css b/apps/client/src/widgets/type_widgets/text/EditableText.css new file mode 100644 index 000000000..48aa06dde --- /dev/null +++ b/apps/client/src/widgets/type_widgets/text/EditableText.css @@ -0,0 +1,53 @@ +.note-detail-editable-text { + font-family: var(--detail-font-family); + padding-left: 14px; + padding-top: 10px; + height: 100%; +} + +/* Workaround for #1327 */ +body.desktop.electron .note-detail-editable-text { + letter-spacing: -0.01px; +} + +body.mobile .note-detail-editable-text { + padding-left: 4px; +} + +.note-detail-editable-text a:hover { + cursor: pointer; +} + +.note-detail-editable-text a[href^="http://"], .note-detail-editable-text a[href^="https://"] { + cursor: text !important; +} + +.note-detail-editable-text *:not(figure, .include-note, hr):first-child { + margin-top: 0 !important; +} + +.note-detail-editable-text h2 { font-size: 1.6em; } +.note-detail-editable-text h3 { font-size: 1.4em; } +.note-detail-editable-text h4 { font-size: 1.2em; } +.note-detail-editable-text h5 { font-size: 1.1em; } +.note-detail-editable-text h6 { font-size: 1.0em; } + +body.heading-style-markdown .note-detail-editable-text h2::before { content: "##\\2004"; color: var(--muted-text-color); } +body.heading-style-markdown .note-detail-editable-text h3::before { content: "###\\2004"; color: var(--muted-text-color); } +body.heading-style-markdown .note-detail-editable-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); } +body.heading-style-markdown .note-detail-editable-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); } +body.heading-style-markdown .note-detail-editable-text h6::before { content: "######\\2004"; color: var(--muted-text-color); } + +body.heading-style-underline .note-detail-editable-text h2 { border-bottom: 1px solid var(--main-border-color); } +body.heading-style-underline .note-detail-editable-text h3 { border-bottom: 1px solid var(--main-border-color); } +body.heading-style-underline .note-detail-editable-text h4:not(.include-note-title) { border-bottom: 1px solid var(--main-border-color); } +body.heading-style-underline .note-detail-editable-text h5 { border-bottom: 1px solid var(--main-border-color); } +body.heading-style-underline .note-detail-editable-text h6 { border-bottom: 1px solid var(--main-border-color); } + +.note-detail-editable-text-editor { + padding-top: 10px; + border: 0 !important; + box-shadow: none !important; + min-height: 50px; + height: 100%; +} \ No newline at end of file diff --git a/apps/client/src/widgets/type_widgets/text/EditableText.tsx b/apps/client/src/widgets/type_widgets/text/EditableText.tsx new file mode 100644 index 000000000..d995cceb5 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/text/EditableText.tsx @@ -0,0 +1,38 @@ +import { isMobile } from "../../../services/utils"; +import { useNoteLabel, useTriliumOption } from "../../react/hooks"; +import { TypeWidgetProps } from "../type_widget"; +import CKEditorWithWatchdog from "./CKEditorWithWatchdog"; +import "./EditableText.css"; + +/** + * The editor can operate into two distinct modes: + * + * - Ballon block mode, in which there is a floating toolbar for the selected text, but another floating button for the entire block (i.e. paragraph). + * - 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) { + const [ language ] = useNoteLabel(note, "language"); + const [ textNoteEditorType ] = useTriliumOption("textNoteEditorType"); + const isClassicEditor = isMobile() || textNoteEditorType === "ckeditor-classic"; + + return ( +
+ +
+ ) +} diff --git a/apps/client/src/widgets/type_widgets_old/ckeditor/config.ts b/apps/client/src/widgets/type_widgets/text/config.ts similarity index 100% rename from apps/client/src/widgets/type_widgets_old/ckeditor/config.ts rename to apps/client/src/widgets/type_widgets/text/config.ts diff --git a/apps/client/src/widgets/type_widgets_old/ckeditor/snippets.ts b/apps/client/src/widgets/type_widgets/text/snippets.ts similarity index 100% rename from apps/client/src/widgets/type_widgets_old/ckeditor/snippets.ts rename to apps/client/src/widgets/type_widgets/text/snippets.ts diff --git a/apps/client/src/widgets/type_widgets_old/ckeditor/toolbar.spec.ts b/apps/client/src/widgets/type_widgets/text/toolbar.spec.ts similarity index 100% rename from apps/client/src/widgets/type_widgets_old/ckeditor/toolbar.spec.ts rename to apps/client/src/widgets/type_widgets/text/toolbar.spec.ts diff --git a/apps/client/src/widgets/type_widgets_old/ckeditor/toolbar.ts b/apps/client/src/widgets/type_widgets/text/toolbar.ts similarity index 100% rename from apps/client/src/widgets/type_widgets_old/ckeditor/toolbar.ts rename to apps/client/src/widgets/type_widgets/text/toolbar.ts diff --git a/apps/client/src/widgets/type_widgets_old/editable_text.ts b/apps/client/src/widgets/type_widgets_old/editable_text.ts index a092ae696..921aa1897 100644 --- a/apps/client/src/widgets/type_widgets_old/editable_text.ts +++ b/apps/client/src/widgets/type_widgets_old/editable_text.ts @@ -16,74 +16,6 @@ import { updateTemplateCache } from "./ckeditor/snippets.js"; export type BoxSize = "small" | "medium" | "full"; -const TPL = /*html*/` -
- - - -
-`; - -/** - * The editor can operate into two distinct modes: - * - * - Ballon block mode, in which there is a floating toolbar for the selected text, but another floating button for the entire block (i.e. paragraph). - * - 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 class EditableTextTypeWidget extends AbstractTextTypeWidget { private contentLanguage?: string | null; @@ -91,10 +23,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { private $editor!: JQuery; - static getType() { - return "editableText"; - } - doRender() { this.$widget = $(TPL); this.$editor = this.$widget.find(".note-detail-editable-text-editor"); @@ -109,31 +37,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { } async initEditor() { - const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic"; - - // CKEditor since version 12 needs the element to be visible before initialization. At the same time, - // we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate - // display of $widget in both branches. - this.$widget.show(); - - const config: 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. - minimumNonErrorTimePeriod: 5000, - // A threshold specifying the number of errors (defaults to 3). - // After this limit is reached and the time between last errors - // is shorter than minimumNonErrorTimePeriod, the watchdog changes - // its state to crashedPermanently, and it stops restarting the editor. - // This prevents an infinite restart loop. - crashNumberLimit: 10, - // A minimum number of milliseconds between saving the editor data internally (defaults to 5000). - // Note that for large documents, this might impact the editor performance. - saveInterval: 5000 - }; - this.watchdog = isClassicEditor ? new EditorWatchdog(ClassicEditor, config) : new EditorWatchdog(PopupEditor, config); - this.watchdog.on("stateChange", () => { const currentState = this.watchdog.state; logInfo(`CKEditor state changed to ${currentState}`); @@ -154,16 +57,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { this.watchdog.setCreator(async (_, editorConfig) => { logInfo("Creating new CKEditor"); - const contentLanguage = this.note?.getLabelValue("language"); - this.contentLanguage = contentLanguage ?? null; - - const opts: BuildEditorOptions = { - contentLanguage: this.contentLanguage, - forceGplLicense: false, - isClassicEditor - }; - const editor = await buildEditor(this.$editor[0], isClassicEditor, opts); - const notificationsPlugin = editor.plugins.get("Notification"); notificationsPlugin.on("show:warning", (evt, data) => { const title = data.title; @@ -232,10 +125,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { await this.createEditor(); } - async createEditor() { - await this.watchdog.create(this.$editor[0]); - } - async doRefresh(note: FNote) { const blob = await note.getBlob(); @@ -592,19 +481,3 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { } } - -async function buildEditor(element: HTMLElement, isClassicEditor: boolean, opts: BuildEditorOptions) { - const editorClass = isClassicEditor ? ClassicEditor : PopupEditor; - let config = await buildConfig(opts); - let editor = await editorClass.create(element, config); - - if (editor.isReadOnly) { - editor.destroy(); - - opts.forceGplLicense = true; - config = await buildConfig(opts); - editor = await editorClass.create(element, config); - } - return editor; - -}