[PE-210] feat: editor performance (#6269)
* bump: upgrade editor * fix: remove editor ref in use * fix: added editor state to reduce rerenders * fix: add editor rerendering optimization * fix: wrong condition in scroll summary * fix: removing ref usage internally in read only editor as well * fix: remove unused methods from read only editor * fix: add editable prop again * regression: added the types for onHeadingChange * fix: types * fix: improve the check condition
This commit is contained in:
parent
0345336d90
commit
996d11de12
8 changed files with 2250 additions and 1898 deletions
|
|
@ -16,10 +16,10 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@hocuspocus/extension-database": "^2.11.3",
|
||||
"@hocuspocus/extension-logger": "^2.11.3",
|
||||
"@hocuspocus/extension-redis": "^2.13.5",
|
||||
"@hocuspocus/server": "^2.11.3",
|
||||
"@hocuspocus/extension-database": "^2.15.0",
|
||||
"@hocuspocus/extension-logger": "^2.15.0",
|
||||
"@hocuspocus/extension-redis": "^2.15.0",
|
||||
"@hocuspocus/server": "^2.15.0",
|
||||
"@plane/constants": "*",
|
||||
"@plane/editor": "*",
|
||||
"@plane/types": "*",
|
||||
|
|
@ -40,9 +40,9 @@
|
|||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^11.2.2",
|
||||
"uuid": "^10.0.0",
|
||||
"y-prosemirror": "^1.2.9",
|
||||
"y-prosemirror": "^1.2.15",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.14"
|
||||
"yjs": "^13.6.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.25.6",
|
||||
|
|
|
|||
|
|
@ -12,14 +12,12 @@
|
|||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs"
|
||||
"import": "./dist/index.mjs"
|
||||
},
|
||||
"./lib": {
|
||||
"require": "./dist/lib.js",
|
||||
"types": "./dist/lib.d.mts",
|
||||
"import": "./dist/lib.mjs",
|
||||
"module": "./dist/lib.mjs"
|
||||
"import": "./dist/lib.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -36,7 +34,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.4",
|
||||
"@hocuspocus/provider": "^2.13.5",
|
||||
"@hocuspocus/provider": "^2.15.0",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/utils": "*",
|
||||
|
|
@ -67,12 +65,12 @@
|
|||
"prosemirror-codemark": "^0.4.2",
|
||||
"prosemirror-utils": "^1.2.2",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8.9",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"uuid": "^10.0.0",
|
||||
"y-indexeddb": "^9.0.12",
|
||||
"y-prosemirror": "^1.2.5",
|
||||
"y-prosemirror": "^1.2.15",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.15"
|
||||
"yjs": "^13.6.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "*",
|
||||
|
|
|
|||
|
|
@ -202,8 +202,7 @@ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
|
|||
key: "image",
|
||||
name: "Image",
|
||||
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
|
||||
command: ({ savedSelection }) =>
|
||||
insertImage({ editor, event: "insert", pos: savedSelection?.from ?? editor.state.selection.from }),
|
||||
command: () => insertImage({ editor, event: "insert", pos: editor.state.selection.from }),
|
||||
icon: ImageIcon,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,21 @@
|
|||
import { MutableRefObject } from "react";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
import { Editor } from "@tiptap/react";
|
||||
|
||||
export const insertContentAtSavedSelection = (
|
||||
editorRef: MutableRefObject<Editor | null>,
|
||||
content: string,
|
||||
savedSelection: Selection
|
||||
) => {
|
||||
if (!editorRef.current || editorRef.current.isDestroyed) {
|
||||
export const insertContentAtSavedSelection = (editor: Editor, content: string) => {
|
||||
if (!editor || editor.isDestroyed) {
|
||||
console.error("Editor reference is not available or has been destroyed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!savedSelection) {
|
||||
if (!editor.state.selection) {
|
||||
console.error("Saved selection is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
const docSize = editorRef.current.state.doc.content.size;
|
||||
const safePosition = Math.max(0, Math.min(savedSelection.anchor, docSize));
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const safePosition = Math.max(0, Math.min(editor.state.selection.anchor, docSize));
|
||||
|
||||
try {
|
||||
editorRef.current.chain().focus().insertContentAt(safePosition, content).run();
|
||||
editor.chain().focus().insertContentAt(safePosition, content).run();
|
||||
} catch (error) {
|
||||
console.error("An error occurred while inserting content at saved selection:", error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { DOMSerializer } from "@tiptap/pm/model";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { useEditor as useTiptapEditor, Editor, Extensions } from "@tiptap/react";
|
||||
import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react";
|
||||
import { useImperativeHandle, MutableRefObject, useEffect } from "react";
|
||||
import * as Y from "yjs";
|
||||
// components
|
||||
import { EditorMenuItem, getEditorMenuItems } from "@/components/menus";
|
||||
import { getEditorMenuItems } from "@/components/menus";
|
||||
// extensions
|
||||
import { CoreEditorExtensions } from "@/extensions";
|
||||
// helpers
|
||||
|
|
@ -71,14 +70,12 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
provider,
|
||||
autofocus = false,
|
||||
} = props;
|
||||
// states
|
||||
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
|
||||
// refs
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
const savedSelectionRef = useRef(savedSelection);
|
||||
|
||||
const editor = useTiptapEditor(
|
||||
{
|
||||
editable,
|
||||
immediatelyRender: false,
|
||||
shouldRerenderOnTransaction: false,
|
||||
autofocus,
|
||||
editorProps: {
|
||||
...CoreEditorProps({
|
||||
|
|
@ -100,8 +97,7 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
],
|
||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
||||
onCreate: () => handleEditorReady?.(true),
|
||||
onTransaction: ({ editor }) => {
|
||||
setSavedSelection(editor.state.selection);
|
||||
onTransaction: () => {
|
||||
onTransaction?.();
|
||||
},
|
||||
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
|
||||
|
|
@ -110,23 +106,17 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
[editable]
|
||||
);
|
||||
|
||||
// Update the ref whenever savedSelection changes
|
||||
useEffect(() => {
|
||||
savedSelectionRef.current = savedSelection;
|
||||
}, [savedSelection]);
|
||||
|
||||
// Effect for syncing SWR data
|
||||
useEffect(() => {
|
||||
// value is null when intentionally passed where syncing is not yet
|
||||
// supported and value is undefined when the data from swr is not populated
|
||||
if (value === null || value === undefined) return;
|
||||
if (value == null) return;
|
||||
if (editor && !editor.isDestroyed && !editor.storage.imageComponent.uploadInProgress) {
|
||||
try {
|
||||
editor.commands.setContent(value, false, { preserveWhitespace: "full" });
|
||||
const currentSavedSelection = savedSelectionRef.current;
|
||||
if (currentSavedSelection) {
|
||||
if (editor.state.selection) {
|
||||
const docLength = editor.state.doc.content.size;
|
||||
const relativePosition = Math.min(currentSavedSelection.from, docLength - 1);
|
||||
const relativePosition = Math.min(editor.state.selection.from, docLength - 1);
|
||||
editor.commands.setTextSelection(relativePosition);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -138,46 +128,40 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
useImperativeHandle(
|
||||
forwardedRef,
|
||||
() => ({
|
||||
blur: () => editorRef.current?.commands.blur(),
|
||||
blur: () => editor.commands.blur(),
|
||||
scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) {
|
||||
const resolvedPos = pos ?? savedSelection?.from;
|
||||
if (!editorRef.current || !resolvedPos) return;
|
||||
scrollToNodeViaDOMCoordinates(editorRef.current, resolvedPos, behavior);
|
||||
const resolvedPos = pos ?? editor.state.selection.from;
|
||||
if (!editor || !resolvedPos) return;
|
||||
scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
|
||||
},
|
||||
getCurrentCursorPosition: () => savedSelection?.from,
|
||||
getCurrentCursorPosition: () => editor.state.selection.from,
|
||||
clearEditor: (emitUpdate = false) => {
|
||||
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
},
|
||||
setEditorValueAtCursorPosition: (content: string) => {
|
||||
if (savedSelection) {
|
||||
insertContentAtSavedSelection(editorRef, content, savedSelection);
|
||||
if (editor.state.selection) {
|
||||
insertContentAtSavedSelection(editor, content);
|
||||
}
|
||||
},
|
||||
executeMenuItemCommand: (props) => {
|
||||
const { itemKey } = props;
|
||||
const editorItems = getEditorMenuItems(editorRef.current);
|
||||
const editorItems = getEditorMenuItems(editor);
|
||||
|
||||
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
|
||||
|
||||
const item = getEditorMenuItem(itemKey);
|
||||
if (item) {
|
||||
if (item.key === "image") {
|
||||
(item as EditorMenuItem<"image">).command({
|
||||
savedSelection: savedSelectionRef.current,
|
||||
});
|
||||
} else {
|
||||
item.command(props);
|
||||
}
|
||||
item.command(props);
|
||||
} else {
|
||||
console.warn(`No command found for item: ${itemKey}`);
|
||||
}
|
||||
},
|
||||
isMenuItemActive: (props) => {
|
||||
const { itemKey } = props;
|
||||
const editorItems = getEditorMenuItems(editorRef.current);
|
||||
const editorItems = getEditorMenuItems(editor);
|
||||
|
||||
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
|
||||
const item = getEditorMenuItem(itemKey);
|
||||
|
|
@ -187,20 +171,20 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
},
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
|
||||
// Subscribe to update event emitted from headers extension
|
||||
editorRef.current?.on("update", () => {
|
||||
callback(editorRef.current?.storage.headingList.headings);
|
||||
editor?.on("update", () => {
|
||||
callback(editor?.storage.headingList.headings);
|
||||
});
|
||||
// Return a function to unsubscribe to the continuous transactions of
|
||||
// the editor on unmounting the component that has subscribed to this
|
||||
// method
|
||||
return () => {
|
||||
editorRef.current?.off("update");
|
||||
editor?.off("update");
|
||||
};
|
||||
},
|
||||
getHeadings: () => editorRef?.current?.storage.headingList.headings,
|
||||
getHeadings: () => editor?.storage.headingList.headings,
|
||||
onStateChange: (callback: () => void) => {
|
||||
// Subscribe to editor state changes
|
||||
editorRef.current?.on("transaction", () => {
|
||||
editor?.on("transaction", () => {
|
||||
callback();
|
||||
});
|
||||
|
||||
|
|
@ -208,17 +192,17 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
// the editor on unmounting the component that has subscribed to this
|
||||
// method
|
||||
return () => {
|
||||
editorRef.current?.off("transaction");
|
||||
editor?.off("transaction");
|
||||
};
|
||||
},
|
||||
getMarkDown: (): string => {
|
||||
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
|
||||
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
||||
return markdownOutput;
|
||||
},
|
||||
getDocument: () => {
|
||||
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
|
||||
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editorRef.current?.getJSON() ?? null;
|
||||
const documentHTML = editor?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editor.getJSON() ?? null;
|
||||
|
||||
return {
|
||||
binary: documentBinary,
|
||||
|
|
@ -227,19 +211,19 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
};
|
||||
},
|
||||
scrollSummary: (marking: IMarking): void => {
|
||||
if (!editorRef.current) return;
|
||||
scrollSummary(editorRef.current, marking);
|
||||
if (!editor) return;
|
||||
scrollSummary(editor, marking);
|
||||
},
|
||||
isEditorReadyToDiscard: () => editorRef.current?.storage.imageComponent.uploadInProgress === false,
|
||||
isEditorReadyToDiscard: () => editor?.storage.imageComponent.uploadInProgress === false,
|
||||
setFocusAtPosition: (position: number) => {
|
||||
if (!editorRef.current || editorRef.current.isDestroyed) {
|
||||
if (!editor || editor.isDestroyed) {
|
||||
console.error("Editor reference is not available or has been destroyed.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const docSize = editorRef.current.state.doc.content.size;
|
||||
const docSize = editor.state.doc.content.size;
|
||||
const safePosition = Math.max(0, Math.min(position, docSize));
|
||||
editorRef.current
|
||||
editor
|
||||
.chain()
|
||||
.insertContentAt(safePosition, [{ type: "paragraph" }])
|
||||
.focus()
|
||||
|
|
@ -249,17 +233,17 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
}
|
||||
},
|
||||
getSelectedText: () => {
|
||||
if (!editorRef.current) return null;
|
||||
if (!editor) return null;
|
||||
|
||||
const { state } = editorRef.current;
|
||||
const { state } = editor;
|
||||
const { from, to, empty } = state.selection;
|
||||
|
||||
if (empty) return null;
|
||||
|
||||
const nodesArray: string[] = [];
|
||||
state.doc.nodesBetween(from, to, (node, _pos, parent) => {
|
||||
if (parent === state.doc && editorRef.current) {
|
||||
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema);
|
||||
if (parent === state.doc && editor) {
|
||||
const serializer = DOMSerializer.fromSchema(editor.schema);
|
||||
const dom = serializer.serializeNode(node);
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.appendChild(dom);
|
||||
|
|
@ -270,28 +254,21 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
return selection;
|
||||
},
|
||||
insertText: (contentHTML, insertOnNextLine) => {
|
||||
if (!editorRef.current) return;
|
||||
// get selection
|
||||
const { from, to, empty } = editorRef.current.state.selection;
|
||||
if (!editor) return;
|
||||
const { from, to, empty } = editor.state.selection;
|
||||
if (empty) return;
|
||||
if (insertOnNextLine) {
|
||||
// move cursor to the end of the selection and insert a new line
|
||||
editorRef.current
|
||||
.chain()
|
||||
.focus()
|
||||
.setTextSelection(to)
|
||||
.insertContent("<br />")
|
||||
.insertContent(contentHTML)
|
||||
.run();
|
||||
editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
|
||||
} else {
|
||||
// replace selected text with the content provided
|
||||
editorRef.current.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
|
||||
editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run();
|
||||
}
|
||||
},
|
||||
getDocumentInfo: () => ({
|
||||
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editorRef?.current?.state),
|
||||
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
|
||||
characters: editor?.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editor?.state),
|
||||
words: editor?.storage?.characterCount?.words?.() ?? 0,
|
||||
}),
|
||||
setProviderDocument: (value) => {
|
||||
const document = provider?.document;
|
||||
|
|
@ -301,16 +278,12 @@ export const useEditor = (props: CustomEditorProps) => {
|
|||
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
|
||||
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
|
||||
}),
|
||||
[editorRef, savedSelection]
|
||||
[editor]
|
||||
);
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// the editorRef is used to access the editor instance from outside the hook
|
||||
// and should only be used after editor is initialized
|
||||
editorRef.current = editor;
|
||||
|
||||
return editor;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
|
||||
import { HocuspocusProvider } from "@hocuspocus/provider";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { useEditor as useCustomEditor, Editor, Extensions } from "@tiptap/react";
|
||||
import { useEditor as useTiptapEditor, Extensions } from "@tiptap/react";
|
||||
import { useImperativeHandle, MutableRefObject, useEffect } from "react";
|
||||
import * as Y from "yjs";
|
||||
// extensions
|
||||
import { CoreReadOnlyEditorExtensions } from "@/extensions";
|
||||
|
|
@ -11,13 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
|
|||
// props
|
||||
import { CoreReadOnlyEditorProps } from "@/props";
|
||||
// types
|
||||
import type {
|
||||
EditorReadOnlyRefApi,
|
||||
TExtensions,
|
||||
TDocumentEventsServer,
|
||||
TFileHandler,
|
||||
TReadOnlyMentionHandler,
|
||||
} from "@/types";
|
||||
import type { EditorReadOnlyRefApi, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
|
||||
|
||||
interface CustomReadOnlyEditorProps {
|
||||
disabledExtensions: TExtensions[];
|
||||
|
|
@ -46,8 +40,10 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
|||
provider,
|
||||
} = props;
|
||||
|
||||
const editor = useCustomEditor({
|
||||
const editor = useTiptapEditor({
|
||||
editable: false,
|
||||
immediatelyRender: true,
|
||||
shouldRerenderOnTransaction: false,
|
||||
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
|
||||
editorProps: {
|
||||
...CoreReadOnlyEditorProps({
|
||||
|
|
@ -77,23 +73,21 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
|||
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" });
|
||||
}, [editor, initialValue]);
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
clearEditor: (emitUpdate = false) => {
|
||||
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
|
||||
},
|
||||
setEditorValue: (content: string) => {
|
||||
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
|
||||
},
|
||||
getMarkDown: (): string => {
|
||||
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
|
||||
const markdownOutput = editor?.storage.markdown.getMarkdown();
|
||||
return markdownOutput;
|
||||
},
|
||||
getDocument: () => {
|
||||
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
|
||||
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editorRef.current?.getJSON() ?? null;
|
||||
const documentHTML = editor?.getHTML() ?? "<p></p>";
|
||||
const documentJSON = editor?.getJSON() ?? null;
|
||||
|
||||
return {
|
||||
binary: documentBinary,
|
||||
|
|
@ -102,35 +96,22 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
|
|||
};
|
||||
},
|
||||
scrollSummary: (marking: IMarking): void => {
|
||||
if (!editorRef.current) return;
|
||||
scrollSummary(editorRef.current, marking);
|
||||
if (!editor) return;
|
||||
scrollSummary(editor, marking);
|
||||
},
|
||||
getDocumentInfo: () => ({
|
||||
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editorRef?.current?.state),
|
||||
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0,
|
||||
}),
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => {
|
||||
// Subscribe to update event emitted from headers extension
|
||||
editorRef.current?.on("update", () => {
|
||||
callback(editorRef.current?.storage.headingList.headings);
|
||||
});
|
||||
// Return a function to unsubscribe to the continuous transactions of
|
||||
// the editor on unmounting the component that has subscribed to this
|
||||
// method
|
||||
return () => {
|
||||
editorRef.current?.off("update");
|
||||
getDocumentInfo: () => {
|
||||
if (!editor) return;
|
||||
return {
|
||||
characters: editor.storage?.characterCount?.characters?.() ?? 0,
|
||||
paragraphs: getParagraphCount(editor.state),
|
||||
words: editor.storage?.characterCount?.words?.() ?? 0,
|
||||
};
|
||||
},
|
||||
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
|
||||
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
|
||||
getHeadings: () => editorRef?.current?.storage.headingList.headings,
|
||||
}));
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
editorRef.current = editor;
|
||||
return editor;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -86,10 +86,6 @@ export type EditorReadOnlyRefApi = {
|
|||
paragraphs: number;
|
||||
words: number;
|
||||
};
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
|
||||
getHeadings: () => IMarking[];
|
||||
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
|
||||
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
|
||||
};
|
||||
|
||||
export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||
|
|
@ -105,6 +101,10 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
|
|||
getSelectedText: () => string | null;
|
||||
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
|
||||
setProviderDocument: (value: Uint8Array) => void;
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
|
||||
getHeadings: () => IMarking[];
|
||||
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
|
||||
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
|
||||
}
|
||||
|
||||
// editor props
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue