mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 10:02:59 +08:00
refactor(client): fix type errors related to CKEditor
This commit is contained in:
parent
3bad43c50d
commit
aab762911b
@ -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<MentionItem[]>((res, rej) => {
|
||||
return await new Promise<MentionFeedObjectItem[]>((res, rej) => {
|
||||
autocompleteSource(
|
||||
queryText,
|
||||
(rows) => {
|
||||
|
113
apps/client/src/types.d.ts
vendored
113
apps/client/src/types.d.ts
vendored
@ -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
|
||||
*/
|
||||
|
@ -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*/`
|
||||
</div>
|
||||
`;
|
||||
|
||||
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<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
|
||||
minimumCharacters: 0
|
||||
},
|
||||
{
|
||||
marker: "#",
|
||||
feed: async (queryText) => {
|
||||
const names = await server.get<string[]>(`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<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
|
||||
minimumCharacters: 0
|
||||
},
|
||||
{
|
||||
marker: "~",
|
||||
feed: async (queryText) => {
|
||||
const names = await server.get<string[]>(`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);
|
||||
});
|
||||
}
|
||||
|
@ -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<CKFindResult>("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();
|
||||
|
@ -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*/`
|
||||
<div class="note-detail-editable-text note-detail-printable">
|
||||
@ -128,7 +126,7 @@ function buildListOfLanguages() {
|
||||
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
|
||||
private contentLanguage?: string | null;
|
||||
private watchdog!: EditorWatchdog<CKTextEditor>;
|
||||
private watchdog!: EditorWatchdog<ClassicEditor | PopupEditor>;
|
||||
|
||||
private $editor!: JQuery<HTMLElement>;
|
||||
|
||||
@ -158,7 +156,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
// display of $widget in both branches.
|
||||
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).
|
||||
// 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() {
|
||||
|
@ -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<void>;
|
||||
};
|
||||
|
||||
export type FindAndReplaceState = FindAndReplaceEditing["state"];
|
||||
export type FindCommandResult = ReturnType<FindCommand["execute"]>;
|
||||
|
||||
/**
|
||||
* The text editor that can be used for editing attributes and relations.
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user