refactor(client): fix type errors related to CKEditor

This commit is contained in:
Elian Doran 2025-05-10 01:52:42 +03:00
parent 3bad43c50d
commit aab762911b
No known key found for this signature in database
6 changed files with 100 additions and 200 deletions

View File

@ -3,6 +3,7 @@ import appContext from "../components/app_context.js";
import noteCreateService from "./note_create.js"; import noteCreateService from "./note_create.js";
import froca from "./froca.js"; import froca from "./froca.js";
import { t } from "./i18n.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 // this key needs to have this value, so it's hit by the tooltip
const SELECTED_NOTE_PATH_KEY = "data-note-path"; const SELECTED_NOTE_PATH_KEY = "data-note-path";
@ -43,7 +44,7 @@ interface Options {
} }
async function autocompleteSourceForCKEditor(queryText: string) { async function autocompleteSourceForCKEditor(queryText: string) {
return await new Promise<MentionItem[]>((res, rej) => { return await new Promise<MentionFeedObjectItem[]>((res, rej) => {
autocompleteSource( autocompleteSource(
queryText, queryText,
(rows) => { (rows) => {

View File

@ -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<string, unknown> | 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<string, string | null | undefined>);
}
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<MentionItem[]>;
itemRenderer?: (item: {
highlightedNotePathTitle: string
}) => void;
minimumCharacters: number;
}[];
}
/* /*
* Panzoom * Panzoom
*/ */

View File

@ -1,10 +1,10 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.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 server from "../../services/server.js";
import contextMenuService from "../../menus/context_menu.js"; import contextMenuService from "../../menus/context_menu.js";
import attributeParser, { type Attribute } from "../../services/attribute_parser.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 froca from "../../services/froca.js";
import attributeRenderer from "../../services/attribute_renderer.js"; import attributeRenderer from "../../services/attribute_renderer.js";
import noteCreateService from "../../services/note_create.js"; import noteCreateService from "../../services/note_create.js";
@ -84,57 +84,58 @@ const TPL = /*html*/`
</div> </div>
`; `;
const mentionSetup: MentionConfig = { const mentionSetup: MentionFeed[] = [
feeds: [ {
{ marker: "@",
marker: "@", feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), itemRenderer: (_item) => {
itemRenderer: (item) => { const item = _item as Suggestion;
const itemElement = document.createElement("button"); const itemElement = document.createElement("button");
itemElement.innerHTML = `${item.highlightedNotePathTitle} `; itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
return itemElement; return itemElement;
},
minimumCharacters: 0
}, },
{ minimumCharacters: 0
marker: "#", },
feed: async (queryText) => { {
const names = await server.get<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`); marker: "#",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
return names.map((name) => { return names.map((name) => {
return { return {
id: `#${name}`, id: `#${name}`,
name: name name: name
}; };
}); });
},
minimumCharacters: 0
}, },
{ minimumCharacters: 0
marker: "~", },
feed: async (queryText) => { {
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`); marker: "~",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
return names.map((name) => { return names.map((name) => {
return { return {
id: `~${name}`, id: `~${name}`,
name: name name: name
}; };
}); });
}, },
minimumCharacters: 0 minimumCharacters: 0
} }
] ];
};
const editorConfig = { const editorConfig: EditorConfig = {
toolbar: { toolbar: {
items: [] items: []
}, },
placeholder: t("attribute_editor.placeholder"), placeholder: t("attribute_editor.placeholder"),
mention: mentionSetup, mention: {
feeds: mentionSetup
},
licenseKey: "GPL" licenseKey: "GPL"
}; };
@ -334,7 +335,10 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
); );
// disable spellcheck for attribute editor // 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() { dataChanged() {
@ -411,18 +415,18 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
this.$editor.tooltip("show"); this.$editor.tooltip("show");
} }
getClickIndex(pos: TextPosition) { getClickIndex(pos: Position) {
let clickIndex = pos.offset - pos.textNode.startOffset; 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; curNode = curNode.previousSibling;
if (curNode.name === "reference") { if ((curNode as Element).name === "reference") {
clickIndex += curNode._attrs.get("notePath").length + 1; clickIndex += (curNode.getAttribute("notePath") as string).length + 1;
} else { } else if ("data" in curNode) {
clickIndex += curNode.data.length; clickIndex += (curNode.data as string).length;
} }
} }
@ -480,8 +484,12 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
this.$editor.trigger("focus"); this.$editor.trigger("focus");
this.textEditor.model.change((writer) => { 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); writer.setSelection(positionAt);
}); });
} }

View File

@ -1,3 +1,4 @@
import type { FindAndReplaceState, FindCommandResult } from "@triliumnext/ckeditor5";
import type { FindResult } from "./find.js"; import type { FindResult } from "./find.js";
import type FindWidget from "./find.js"; import type FindWidget from "./find.js";
@ -14,8 +15,8 @@ interface Match {
export default class FindInText { export default class FindInText {
private parent: FindWidget; private parent: FindWidget;
private findResult?: CKFindResult | null; private findResult?: FindCommandResult | null;
private editingState?: EditingState; private editingState?: FindAndReplaceState;
constructor(parent: FindWidget) { constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
@ -40,7 +41,7 @@ export default class FindInText {
// Clear // Clear
const findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing"); const findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
findAndReplaceEditing.state.clear(model); findAndReplaceEditing.state?.clear(model);
findAndReplaceEditing.stop(); findAndReplaceEditing.stop();
this.editingState = findAndReplaceEditing.state; this.editingState = findAndReplaceEditing.state;
if (searchTerm !== "") { if (searchTerm !== "") {
@ -52,14 +53,14 @@ export default class FindInText {
// let m = text.match(re); // let m = text.match(re);
// totalFound = m ? m.length : 0; // totalFound = m ? m.length : 0;
const options = { matchCase: matchCase, wholeWords: wholeWord }; const options = { matchCase: matchCase, wholeWords: wholeWord };
findResult = textEditor.execute<CKFindResult>("find", searchTerm, options); findResult = textEditor.execute("find", searchTerm, options);
totalFound = findResult.results.length; totalFound = findResult.results.length;
// Find the result beyond the cursor // Find the result beyond the cursor
const cursorPos = model.document.selection.getLastPosition(); const cursorPos = model.document.selection.getLastPosition();
for (let i = 0; i < findResult.results.length; ++i) { for (let i = 0; i < findResult.results.length; ++i) {
const marker = findResult.results.get(i).marker; const marker = findResult.results.get(i)?.marker;
const fromPos = marker.getStart(); const fromPos = marker?.getStart();
if (cursorPos && fromPos.compareWith(cursorPos) !== "before") { if (cursorPos && fromPos && fromPos.compareWith(cursorPos) !== "before") {
currentFound = i; currentFound = i;
break; break;
} }
@ -75,7 +76,7 @@ export default class FindInText {
// XXX Do this accessing the private data? // XXX Do this accessing the private data?
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js
for (let i = 0; i < currentFound; ++i) { 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 // Clear the markers and set the caret to the
// current occurrence // current occurrence
const model = textEditor.model; const model = textEditor.model;
const range = this.findResult?.results?.get(currentFound).marker.getRange(); const range = this.findResult?.results?.get(currentFound)?.marker?.getRange();
// From // From
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92 // 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 // XXX Roll our own since already done for codeEditor and
// will probably allow more refactoring? // will probably allow more refactoring?
let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing"); let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
findAndReplaceEditing.state.clear(model); findAndReplaceEditing.state?.clear(model);
findAndReplaceEditing.stop(); findAndReplaceEditing.stop();
if (range) { if (range) {
model.change((writer) => { model.change((writer) => {
writer.setSelection(range, 0); writer.setSelection(range);
}); });
} }
textEditor.editing.view.scrollToTheSelection(); textEditor.editing.view.scrollToTheSelection();

View File

@ -1,6 +1,6 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import libraryLoader from "../../services/library_loader.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 mimeTypesService from "../../services/mime_types.js";
import utils, { hasTouchBar } from "../../services/utils.js"; import utils, { hasTouchBar } from "../../services/utils.js";
import keyboardActionService from "../../services/keyboard_actions.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 { buildConfig, buildToolbarConfig } from "./ckeditor/config.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import { getMermaidConfig } from "../../services/mermaid.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"; import "@triliumnext/ckeditor5/index.css";
const ENABLE_INSPECTOR = false; const ENABLE_INSPECTOR = false;
const mentionSetup: MentionConfig = { const mentionSetup: MentionFeed[] = [
feeds: [ {
{ marker: "@",
marker: "@", feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText), itemRenderer: (item) => {
itemRenderer: (item) => { const itemElement = document.createElement("button");
const itemElement = document.createElement("button");
itemElement.innerHTML = `${item.highlightedNotePathTitle} `; itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `;
return itemElement; return itemElement;
}, },
minimumCharacters: 0 minimumCharacters: 0
} }
] ];
};
const TPL = /*html*/` const TPL = /*html*/`
<div class="note-detail-editable-text note-detail-printable"> <div class="note-detail-editable-text note-detail-printable">
@ -128,7 +126,7 @@ function buildListOfLanguages() {
export default class EditableTextTypeWidget extends AbstractTextTypeWidget { export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
private contentLanguage?: string | null; private contentLanguage?: string | null;
private watchdog!: EditorWatchdog<CKTextEditor>; private watchdog!: EditorWatchdog<ClassicEditor | PopupEditor>;
private $editor!: JQuery<HTMLElement>; private $editor!: JQuery<HTMLElement>;
@ -158,7 +156,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
// display of $widget in both branches. // display of $widget in both branches.
this.$widget.show(); this.$widget.show();
this.watchdog = new EditorWatchdog<CKTextEditor>(editorClass, { const config: WatchdogConfig = {
// An average number of milliseconds between the last editor errors (defaults to 5000). // 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 // 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 // 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). // 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. // Note that for large documents, this might impact the editor performance.
saveInterval: 5000 saveInterval: 5000
}); };
this.watchdog = isClassicEditor ? new EditorWatchdog(ClassicEditor, config) : new EditorWatchdog(PopupEditor, config);
this.watchdog.on("stateChange", () => { this.watchdog.on("stateChange", () => {
const currentState = this.watchdog.state; const currentState = this.watchdog.state;
@ -226,7 +225,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const editor = await editorClass.create(elementOrData, finalConfig); const editor = await editorClass.create(elementOrData, finalConfig);
const notificationsPlugin = editor.plugins.get("Notification"); 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 title = data.title;
const message = data.message.message; const message = data.message.message;
@ -447,10 +446,10 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
} }
if (callback) { if (callback) {
callback(this.watchdog.editor); callback(this.watchdog.editor as CKTextEditor);
} }
resolve(this.watchdog.editor); resolve(this.watchdog.editor as CKTextEditor);
} }
addLinkToTextCommand() { addLinkToTextCommand() {

View File

@ -1,7 +1,8 @@
import "ckeditor5/ckeditor5.css"; import "ckeditor5/ckeditor5.css";
import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins"; 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 { 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. * Short-hand for the CKEditor classes supported by Trilium for text editing.
@ -12,6 +13,9 @@ export type CKTextEditor = (ClassicEditor | PopupEditor) & {
removeSelection(): Promise<void>; removeSelection(): Promise<void>;
}; };
export type FindAndReplaceState = FindAndReplaceEditing["state"];
export type FindCommandResult = ReturnType<FindCommand["execute"]>;
/** /**
* The text editor that can be used for editing attributes and relations. * The text editor that can be used for editing attributes and relations.
*/ */