diff --git a/apps/client/src/services/note_autocomplete.ts b/apps/client/src/services/note_autocomplete.ts index c9e39785e..4ffff8594 100644 --- a/apps/client/src/services/note_autocomplete.ts +++ b/apps/client/src/services/note_autocomplete.ts @@ -3,6 +3,7 @@ import appContext from "../components/app_context.js"; import noteCreateService from "./note_create.js"; import froca from "./froca.js"; import { t } from "./i18n.js"; +import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5"; // this key needs to have this value, so it's hit by the tooltip const SELECTED_NOTE_PATH_KEY = "data-note-path"; @@ -43,7 +44,7 @@ interface Options { } async function autocompleteSourceForCKEditor(queryText: string) { - return await new Promise((res, rej) => { + return await new Promise((res, rej) => { autocompleteSource( queryText, (rows) => { diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index b699f8b04..0a466825a 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -209,119 +209,6 @@ declare global { }); } - interface Range { - toJSON(): object; - getItems(): TextNode[]; - } - interface Writer { - setAttribute(name: string, value: string, el: CKNode); - createPositionAt(el: CKNode, opt?: "end" | number); - setSelection(pos: number, pos2?: number); - insertText(text: string, opts: Record | undefined | TextPosition, position?: TextPosition); - addMarker(name: string, opts: { - range: Range; - usingOperation: boolean; - }); - removeMarker(name: string); - createRange(start: number, end: number): Range; - createElement(type: string, opts: Record); - } - interface TextNode { - previousSibling?: TextNode; - name: string; - data: string; - startOffset: number; - _attrs: { - get(key: string): { - length: number - } - } - } - interface TextPosition { - textNode: TextNode; - offset: number; - compareWith(pos: TextPosition): string; - } - - interface TextRange { - - } - - interface Marker { - name: string; - } - - interface CKNode { - _children: CKNode[]; - name: string; - childCount: number; - isEmpty: boolean; - toJSON(): object; - is(type: string, name?: string); - getAttribute(name: string): string; - getChild(index: number): CKNode; - data: string; - startOffset: number; - root: { - document: { - model: { - createRangeIn(el: CKNode): TextRange; - markers: { - getMarkersIntersectingRange(range: TextRange): Marker[]; - } - } - } - }; - } - - interface CKEvent { - stop(): void; - } - - interface PluginEventData { - title: string; - message: { - message: string; - }; - } - - interface EditingState { - highlightedResult: string; - results: unknown[]; - } - - interface CKFindResult { - results: { - get(number): { - marker: { - getStart(): TextPosition; - getRange(): number; - }; - } - } & []; - } - - interface MentionItem { - action?: string; - noteTitle?: string; - id: string; - name: string; - link?: string; - notePath?: string; - highlightedNotePathTitle?: string; - } - - interface MentionConfig { - feeds: { - marker: string; - feed: (queryText: string) => MentionItem[] | Promise; - itemRenderer?: (item: { - highlightedNotePathTitle: string - }) => void; - minimumCharacters: number; - }[]; - } - /* * Panzoom */ diff --git a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts b/apps/client/src/widgets/attribute_widgets/attribute_editor.ts index 205541716..1f3da0096 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_editor.ts @@ -1,10 +1,10 @@ import { t } from "../../services/i18n.js"; import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import noteAutocompleteService from "../../services/note_autocomplete.js"; +import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js"; import server from "../../services/server.js"; import contextMenuService from "../../menus/context_menu.js"; import attributeParser, { type Attribute } from "../../services/attribute_parser.js"; -import { AttributeEditor } from "@triliumnext/ckeditor5"; +import { AttributeEditor, type EditorConfig, type Element, type MentionFeed, type Node, type Position } from "@triliumnext/ckeditor5"; import froca from "../../services/froca.js"; import attributeRenderer from "../../services/attribute_renderer.js"; import noteCreateService from "../../services/note_create.js"; @@ -84,57 +84,58 @@ const TPL = /*html*/` `; -const mentionSetup: MentionConfig = { - feeds: [ - { - marker: "@", - feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), - itemRenderer: (item) => { - const itemElement = document.createElement("button"); +const mentionSetup: MentionFeed[] = [ + { + marker: "@", + feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), + itemRenderer: (_item) => { + const item = _item as Suggestion; + const itemElement = document.createElement("button"); - itemElement.innerHTML = `${item.highlightedNotePathTitle} `; + itemElement.innerHTML = `${item.highlightedNotePathTitle} `; - return itemElement; - }, - minimumCharacters: 0 + return itemElement; }, - { - marker: "#", - feed: async (queryText) => { - const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); + minimumCharacters: 0 + }, + { + marker: "#", + feed: async (queryText) => { + const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); - return names.map((name) => { - return { - id: `#${name}`, - name: name - }; - }); - }, - minimumCharacters: 0 + return names.map((name) => { + return { + id: `#${name}`, + name: name + }; + }); }, - { - marker: "~", - feed: async (queryText) => { - const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); + minimumCharacters: 0 + }, + { + marker: "~", + feed: async (queryText) => { + const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); - return names.map((name) => { - return { - id: `~${name}`, - name: name - }; - }); - }, - minimumCharacters: 0 - } - ] -}; + return names.map((name) => { + return { + id: `~${name}`, + name: name + }; + }); + }, + minimumCharacters: 0 + } +]; -const editorConfig = { +const editorConfig: EditorConfig = { toolbar: { items: [] }, placeholder: t("attribute_editor.placeholder"), - mention: mentionSetup, + mention: { + feeds: mentionSetup + }, licenseKey: "GPL" }; @@ -334,7 +335,10 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem ); // disable spellcheck for attribute editor - this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", this.textEditor.editing.view.document.getRoot())); + const documentRoot = this.textEditor.editing.view.document.getRoot(); + if (documentRoot) { + this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", documentRoot)); + } } dataChanged() { @@ -411,18 +415,18 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem this.$editor.tooltip("show"); } - getClickIndex(pos: TextPosition) { - let clickIndex = pos.offset - pos.textNode.startOffset; + getClickIndex(pos: Position) { + let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0); - let curNode = pos.textNode; + let curNode: Node | Text | Element | null = pos.textNode; - while (curNode.previousSibling) { + while (curNode?.previousSibling) { curNode = curNode.previousSibling; - if (curNode.name === "reference") { - clickIndex += curNode._attrs.get("notePath").length + 1; - } else { - clickIndex += curNode.data.length; + if ((curNode as Element).name === "reference") { + clickIndex += (curNode.getAttribute("notePath") as string).length + 1; + } else if ("data" in curNode) { + clickIndex += (curNode.data as string).length; } } @@ -480,8 +484,12 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem this.$editor.trigger("focus"); this.textEditor.model.change((writer) => { - const positionAt = writer.createPositionAt(this.textEditor.model.document.getRoot(), "end"); + const documentRoot = this.textEditor.editing.model.document.getRoot(); + if (!documentRoot) { + return; + } + const positionAt = writer.createPositionAt(documentRoot, "end"); writer.setSelection(positionAt); }); } diff --git a/apps/client/src/widgets/find_in_text.ts b/apps/client/src/widgets/find_in_text.ts index b5fa3a02c..248c0bc0b 100644 --- a/apps/client/src/widgets/find_in_text.ts +++ b/apps/client/src/widgets/find_in_text.ts @@ -1,3 +1,4 @@ +import type { FindAndReplaceState, FindCommandResult } from "@triliumnext/ckeditor5"; import type { FindResult } from "./find.js"; import type FindWidget from "./find.js"; @@ -14,8 +15,8 @@ interface Match { export default class FindInText { private parent: FindWidget; - private findResult?: CKFindResult | null; - private editingState?: EditingState; + private findResult?: FindCommandResult | null; + private editingState?: FindAndReplaceState; constructor(parent: FindWidget) { this.parent = parent; @@ -40,7 +41,7 @@ export default class FindInText { // Clear const findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing"); - findAndReplaceEditing.state.clear(model); + findAndReplaceEditing.state?.clear(model); findAndReplaceEditing.stop(); this.editingState = findAndReplaceEditing.state; if (searchTerm !== "") { @@ -52,14 +53,14 @@ export default class FindInText { // let m = text.match(re); // totalFound = m ? m.length : 0; const options = { matchCase: matchCase, wholeWords: wholeWord }; - findResult = textEditor.execute("find", searchTerm, options); + findResult = textEditor.execute("find", searchTerm, options); totalFound = findResult.results.length; // Find the result beyond the cursor const cursorPos = model.document.selection.getLastPosition(); for (let i = 0; i < findResult.results.length; ++i) { - const marker = findResult.results.get(i).marker; - const fromPos = marker.getStart(); - if (cursorPos && fromPos.compareWith(cursorPos) !== "before") { + const marker = findResult.results.get(i)?.marker; + const fromPos = marker?.getStart(); + if (cursorPos && fromPos && fromPos.compareWith(cursorPos) !== "before") { currentFound = i; break; } @@ -75,7 +76,7 @@ export default class FindInText { // XXX Do this accessing the private data? // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js for (let i = 0; i < currentFound; ++i) { - textEditor?.execute("findNext", searchTerm); + textEditor?.execute("findNext"); } } @@ -109,17 +110,17 @@ export default class FindInText { // Clear the markers and set the caret to the // current occurrence const model = textEditor.model; - const range = this.findResult?.results?.get(currentFound).marker.getRange(); + const range = this.findResult?.results?.get(currentFound)?.marker?.getRange(); // From // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 // XXX Roll our own since already done for codeEditor and // will probably allow more refactoring? let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing"); - findAndReplaceEditing.state.clear(model); + findAndReplaceEditing.state?.clear(model); findAndReplaceEditing.stop(); if (range) { model.change((writer) => { - writer.setSelection(range, 0); + writer.setSelection(range); }); } textEditor.editing.view.scrollToTheSelection(); diff --git a/apps/client/src/widgets/type_widgets/editable_text.ts b/apps/client/src/widgets/type_widgets/editable_text.ts index 90661a66d..2f49533ca 100644 --- a/apps/client/src/widgets/type_widgets/editable_text.ts +++ b/apps/client/src/widgets/type_widgets/editable_text.ts @@ -1,6 +1,6 @@ import { t } from "../../services/i18n.js"; import libraryLoader from "../../services/library_loader.js"; -import noteAutocompleteService from "../../services/note_autocomplete.js"; +import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js"; import mimeTypesService from "../../services/mime_types.js"; import utils, { hasTouchBar } from "../../services/utils.js"; import keyboardActionService from "../../services/keyboard_actions.js"; @@ -17,27 +17,25 @@ import { buildSelectedBackgroundColor } from "../../components/touch_bar.js"; import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js"; import type FNote from "../../entities/fnote.js"; import { getMermaidConfig } from "../../services/mermaid.js"; -import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor } from "@triliumnext/ckeditor5"; +import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig } from "@triliumnext/ckeditor5"; import "@triliumnext/ckeditor5/index.css"; const ENABLE_INSPECTOR = false; -const mentionSetup: MentionConfig = { - feeds: [ - { - marker: "@", - feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), - itemRenderer: (item) => { - const itemElement = document.createElement("button"); +const mentionSetup: MentionFeed[] = [ + { + marker: "@", + feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), + itemRenderer: (item) => { + const itemElement = document.createElement("button"); - itemElement.innerHTML = `${item.highlightedNotePathTitle} `; + itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `; - return itemElement; - }, - minimumCharacters: 0 - } - ] -}; + return itemElement; + }, + minimumCharacters: 0 + } +]; const TPL = /*html*/`
@@ -128,7 +126,7 @@ function buildListOfLanguages() { export default class EditableTextTypeWidget extends AbstractTextTypeWidget { private contentLanguage?: string | null; - private watchdog!: EditorWatchdog; + private watchdog!: EditorWatchdog; private $editor!: JQuery; @@ -158,7 +156,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { // display of $widget in both branches. this.$widget.show(); - this.watchdog = new EditorWatchdog(editorClass, { + 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 @@ -173,7 +171,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { // 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; @@ -226,7 +225,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { const editor = await editorClass.create(elementOrData, finalConfig); const notificationsPlugin = editor.plugins.get("Notification"); - notificationsPlugin.on("show:warning", (evt: CKEvent, data: PluginEventData) => { + notificationsPlugin.on("show:warning", (evt, data) => { const title = data.title; const message = data.message.message; @@ -447,10 +446,10 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { } if (callback) { - callback(this.watchdog.editor); + callback(this.watchdog.editor as CKTextEditor); } - resolve(this.watchdog.editor); + resolve(this.watchdog.editor as CKTextEditor); } addLinkToTextCommand() { diff --git a/packages/ckeditor5/src/index.ts b/packages/ckeditor5/src/index.ts index 57fd3373f..1a614f8e8 100644 --- a/packages/ckeditor5/src/index.ts +++ b/packages/ckeditor5/src/index.ts @@ -1,7 +1,8 @@ import "ckeditor5/ckeditor5.css"; import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins"; -import { BalloonEditor, DecoupledEditor } from "ckeditor5"; +import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5"; export { EditorWatchdog } from "ckeditor5"; +export type { EditorConfig, MentionFeed, MentionFeedObjectItem, Node, Position, Element, WatchdogConfig } from "ckeditor5"; /** * Short-hand for the CKEditor classes supported by Trilium for text editing. @@ -12,6 +13,9 @@ export type CKTextEditor = (ClassicEditor | PopupEditor) & { removeSelection(): Promise; }; +export type FindAndReplaceState = FindAndReplaceEditing["state"]; +export type FindCommandResult = ReturnType; + /** * The text editor that can be used for editing attributes and relations. */