[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:
M. Palanikannan 2025-01-15 16:18:49 +05:30 committed by GitHub
parent 0345336d90
commit 996d11de12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 2250 additions and 1898 deletions

View file

@ -16,10 +16,10 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@hocuspocus/extension-database": "^2.11.3", "@hocuspocus/extension-database": "^2.15.0",
"@hocuspocus/extension-logger": "^2.11.3", "@hocuspocus/extension-logger": "^2.15.0",
"@hocuspocus/extension-redis": "^2.13.5", "@hocuspocus/extension-redis": "^2.15.0",
"@hocuspocus/server": "^2.11.3", "@hocuspocus/server": "^2.15.0",
"@plane/constants": "*", "@plane/constants": "*",
"@plane/editor": "*", "@plane/editor": "*",
"@plane/types": "*", "@plane/types": "*",
@ -40,9 +40,9 @@
"pino-http": "^10.3.0", "pino-http": "^10.3.0",
"pino-pretty": "^11.2.2", "pino-pretty": "^11.2.2",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"y-prosemirror": "^1.2.9", "y-prosemirror": "^1.2.15",
"y-protocols": "^1.0.6", "y-protocols": "^1.0.6",
"yjs": "^13.6.14" "yjs": "^13.6.20"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.25.6", "@babel/cli": "^7.25.6",

View file

@ -12,14 +12,12 @@
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.mts", "types": "./dist/index.d.mts",
"import": "./dist/index.mjs", "import": "./dist/index.mjs"
"module": "./dist/index.mjs"
}, },
"./lib": { "./lib": {
"require": "./dist/lib.js", "require": "./dist/lib.js",
"types": "./dist/lib.d.mts", "types": "./dist/lib.d.mts",
"import": "./dist/lib.mjs", "import": "./dist/lib.mjs"
"module": "./dist/lib.mjs"
} }
}, },
"scripts": { "scripts": {
@ -36,7 +34,7 @@
}, },
"dependencies": { "dependencies": {
"@floating-ui/react": "^0.26.4", "@floating-ui/react": "^0.26.4",
"@hocuspocus/provider": "^2.13.5", "@hocuspocus/provider": "^2.15.0",
"@plane/types": "*", "@plane/types": "*",
"@plane/ui": "*", "@plane/ui": "*",
"@plane/utils": "*", "@plane/utils": "*",
@ -67,12 +65,12 @@
"prosemirror-codemark": "^0.4.2", "prosemirror-codemark": "^0.4.2",
"prosemirror-utils": "^1.2.2", "prosemirror-utils": "^1.2.2",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.9", "tiptap-markdown": "^0.8.10",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"y-indexeddb": "^9.0.12", "y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.5", "y-prosemirror": "^1.2.15",
"y-protocols": "^1.0.6", "y-protocols": "^1.0.6",
"yjs": "^13.6.15" "yjs": "^13.6.20"
}, },
"devDependencies": { "devDependencies": {
"@plane/eslint-config": "*", "@plane/eslint-config": "*",

View file

@ -202,8 +202,7 @@ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
key: "image", key: "image",
name: "Image", name: "Image",
isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"), isActive: () => editor?.isActive("image") || editor?.isActive("imageComponent"),
command: ({ savedSelection }) => command: () => insertImage({ editor, event: "insert", pos: editor.state.selection.from }),
insertImage({ editor, event: "insert", pos: savedSelection?.from ?? editor.state.selection.from }),
icon: ImageIcon, icon: ImageIcon,
}); });

View file

@ -1,27 +1,21 @@
import { MutableRefObject } from "react";
import { Selection } from "@tiptap/pm/state";
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
export const insertContentAtSavedSelection = ( export const insertContentAtSavedSelection = (editor: Editor, content: string) => {
editorRef: MutableRefObject<Editor | null>, if (!editor || editor.isDestroyed) {
content: string,
savedSelection: Selection
) => {
if (!editorRef.current || editorRef.current.isDestroyed) {
console.error("Editor reference is not available or has been destroyed."); console.error("Editor reference is not available or has been destroyed.");
return; return;
} }
if (!savedSelection) { if (!editor.state.selection) {
console.error("Saved selection is invalid."); console.error("Saved selection is invalid.");
return; return;
} }
const docSize = editorRef.current.state.doc.content.size; const docSize = editor.state.doc.content.size;
const safePosition = Math.max(0, Math.min(savedSelection.anchor, docSize)); const safePosition = Math.max(0, Math.min(editor.state.selection.anchor, docSize));
try { try {
editorRef.current.chain().focus().insertContentAt(safePosition, content).run(); editor.chain().focus().insertContentAt(safePosition, content).run();
} catch (error) { } catch (error) {
console.error("An error occurred while inserting content at saved selection:", error); console.error("An error occurred while inserting content at saved selection:", error);
} }

View file

@ -1,12 +1,11 @@
import { useImperativeHandle, useRef, MutableRefObject, useState, useEffect } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider"; import { HocuspocusProvider } from "@hocuspocus/provider";
import { DOMSerializer } from "@tiptap/pm/model"; import { DOMSerializer } from "@tiptap/pm/model";
import { Selection } from "@tiptap/pm/state";
import { EditorProps } from "@tiptap/pm/view"; 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"; import * as Y from "yjs";
// components // components
import { EditorMenuItem, getEditorMenuItems } from "@/components/menus"; import { getEditorMenuItems } from "@/components/menus";
// extensions // extensions
import { CoreEditorExtensions } from "@/extensions"; import { CoreEditorExtensions } from "@/extensions";
// helpers // helpers
@ -71,14 +70,12 @@ export const useEditor = (props: CustomEditorProps) => {
provider, provider,
autofocus = false, autofocus = false,
} = props; } = props;
// states
const [savedSelection, setSavedSelection] = useState<Selection | null>(null);
// refs
const editorRef: MutableRefObject<Editor | null> = useRef(null);
const savedSelectionRef = useRef(savedSelection);
const editor = useTiptapEditor( const editor = useTiptapEditor(
{ {
editable, editable,
immediatelyRender: false,
shouldRerenderOnTransaction: false,
autofocus, autofocus,
editorProps: { editorProps: {
...CoreEditorProps({ ...CoreEditorProps({
@ -100,8 +97,7 @@ export const useEditor = (props: CustomEditorProps) => {
], ],
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>", content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
onCreate: () => handleEditorReady?.(true), onCreate: () => handleEditorReady?.(true),
onTransaction: ({ editor }) => { onTransaction: () => {
setSavedSelection(editor.state.selection);
onTransaction?.(); onTransaction?.();
}, },
onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()), onUpdate: ({ editor }) => onChange?.(editor.getJSON(), editor.getHTML()),
@ -110,23 +106,17 @@ export const useEditor = (props: CustomEditorProps) => {
[editable] [editable]
); );
// Update the ref whenever savedSelection changes
useEffect(() => {
savedSelectionRef.current = savedSelection;
}, [savedSelection]);
// Effect for syncing SWR data // Effect for syncing SWR data
useEffect(() => { useEffect(() => {
// value is null when intentionally passed where syncing is not yet // value is null when intentionally passed where syncing is not yet
// supported and value is undefined when the data from swr is not populated // 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) { if (editor && !editor.isDestroyed && !editor.storage.imageComponent.uploadInProgress) {
try { try {
editor.commands.setContent(value, false, { preserveWhitespace: "full" }); editor.commands.setContent(value, false, { preserveWhitespace: "full" });
const currentSavedSelection = savedSelectionRef.current; if (editor.state.selection) {
if (currentSavedSelection) {
const docLength = editor.state.doc.content.size; 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); editor.commands.setTextSelection(relativePosition);
} }
} catch (error) { } catch (error) {
@ -138,46 +128,40 @@ export const useEditor = (props: CustomEditorProps) => {
useImperativeHandle( useImperativeHandle(
forwardedRef, forwardedRef,
() => ({ () => ({
blur: () => editorRef.current?.commands.blur(), blur: () => editor.commands.blur(),
scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) { scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) {
const resolvedPos = pos ?? savedSelection?.from; const resolvedPos = pos ?? editor.state.selection.from;
if (!editorRef.current || !resolvedPos) return; if (!editor || !resolvedPos) return;
scrollToNodeViaDOMCoordinates(editorRef.current, resolvedPos, behavior); scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
}, },
getCurrentCursorPosition: () => savedSelection?.from, getCurrentCursorPosition: () => editor.state.selection.from,
clearEditor: (emitUpdate = false) => { clearEditor: (emitUpdate = false) => {
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
}, },
setEditorValue: (content: string) => { setEditorValue: (content: string) => {
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" }); editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
}, },
setEditorValueAtCursorPosition: (content: string) => { setEditorValueAtCursorPosition: (content: string) => {
if (savedSelection) { if (editor.state.selection) {
insertContentAtSavedSelection(editorRef, content, savedSelection); insertContentAtSavedSelection(editor, content);
} }
}, },
executeMenuItemCommand: (props) => { executeMenuItemCommand: (props) => {
const { itemKey } = props; const { itemKey } = props;
const editorItems = getEditorMenuItems(editorRef.current); const editorItems = getEditorMenuItems(editor);
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
const item = getEditorMenuItem(itemKey); const item = getEditorMenuItem(itemKey);
if (item) { if (item) {
if (item.key === "image") {
(item as EditorMenuItem<"image">).command({
savedSelection: savedSelectionRef.current,
});
} else {
item.command(props); item.command(props);
}
} else { } else {
console.warn(`No command found for item: ${itemKey}`); console.warn(`No command found for item: ${itemKey}`);
} }
}, },
isMenuItemActive: (props) => { isMenuItemActive: (props) => {
const { itemKey } = props; const { itemKey } = props;
const editorItems = getEditorMenuItems(editorRef.current); const editorItems = getEditorMenuItems(editor);
const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey);
const item = getEditorMenuItem(itemKey); const item = getEditorMenuItem(itemKey);
@ -187,20 +171,20 @@ export const useEditor = (props: CustomEditorProps) => {
}, },
onHeadingChange: (callback: (headings: IMarking[]) => void) => { onHeadingChange: (callback: (headings: IMarking[]) => void) => {
// Subscribe to update event emitted from headers extension // Subscribe to update event emitted from headers extension
editorRef.current?.on("update", () => { editor?.on("update", () => {
callback(editorRef.current?.storage.headingList.headings); callback(editor?.storage.headingList.headings);
}); });
// Return a function to unsubscribe to the continuous transactions of // Return a function to unsubscribe to the continuous transactions of
// the editor on unmounting the component that has subscribed to this // the editor on unmounting the component that has subscribed to this
// method // method
return () => { return () => {
editorRef.current?.off("update"); editor?.off("update");
}; };
}, },
getHeadings: () => editorRef?.current?.storage.headingList.headings, getHeadings: () => editor?.storage.headingList.headings,
onStateChange: (callback: () => void) => { onStateChange: (callback: () => void) => {
// Subscribe to editor state changes // Subscribe to editor state changes
editorRef.current?.on("transaction", () => { editor?.on("transaction", () => {
callback(); callback();
}); });
@ -208,17 +192,17 @@ export const useEditor = (props: CustomEditorProps) => {
// the editor on unmounting the component that has subscribed to this // the editor on unmounting the component that has subscribed to this
// method // method
return () => { return () => {
editorRef.current?.off("transaction"); editor?.off("transaction");
}; };
}, },
getMarkDown: (): string => { getMarkDown: (): string => {
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown(); const markdownOutput = editor?.storage.markdown.getMarkdown();
return markdownOutput; return markdownOutput;
}, },
getDocument: () => { getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null; const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>"; const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editorRef.current?.getJSON() ?? null; const documentJSON = editor.getJSON() ?? null;
return { return {
binary: documentBinary, binary: documentBinary,
@ -227,19 +211,19 @@ export const useEditor = (props: CustomEditorProps) => {
}; };
}, },
scrollSummary: (marking: IMarking): void => { scrollSummary: (marking: IMarking): void => {
if (!editorRef.current) return; if (!editor) return;
scrollSummary(editorRef.current, marking); scrollSummary(editor, marking);
}, },
isEditorReadyToDiscard: () => editorRef.current?.storage.imageComponent.uploadInProgress === false, isEditorReadyToDiscard: () => editor?.storage.imageComponent.uploadInProgress === false,
setFocusAtPosition: (position: number) => { setFocusAtPosition: (position: number) => {
if (!editorRef.current || editorRef.current.isDestroyed) { if (!editor || editor.isDestroyed) {
console.error("Editor reference is not available or has been destroyed."); console.error("Editor reference is not available or has been destroyed.");
return; return;
} }
try { 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)); const safePosition = Math.max(0, Math.min(position, docSize));
editorRef.current editor
.chain() .chain()
.insertContentAt(safePosition, [{ type: "paragraph" }]) .insertContentAt(safePosition, [{ type: "paragraph" }])
.focus() .focus()
@ -249,17 +233,17 @@ export const useEditor = (props: CustomEditorProps) => {
} }
}, },
getSelectedText: () => { getSelectedText: () => {
if (!editorRef.current) return null; if (!editor) return null;
const { state } = editorRef.current; const { state } = editor;
const { from, to, empty } = state.selection; const { from, to, empty } = state.selection;
if (empty) return null; if (empty) return null;
const nodesArray: string[] = []; const nodesArray: string[] = [];
state.doc.nodesBetween(from, to, (node, _pos, parent) => { state.doc.nodesBetween(from, to, (node, _pos, parent) => {
if (parent === state.doc && editorRef.current) { if (parent === state.doc && editor) {
const serializer = DOMSerializer.fromSchema(editorRef.current?.schema); const serializer = DOMSerializer.fromSchema(editor.schema);
const dom = serializer.serializeNode(node); const dom = serializer.serializeNode(node);
const tempDiv = document.createElement("div"); const tempDiv = document.createElement("div");
tempDiv.appendChild(dom); tempDiv.appendChild(dom);
@ -270,28 +254,21 @@ export const useEditor = (props: CustomEditorProps) => {
return selection; return selection;
}, },
insertText: (contentHTML, insertOnNextLine) => { insertText: (contentHTML, insertOnNextLine) => {
if (!editorRef.current) return; if (!editor) return;
// get selection const { from, to, empty } = editor.state.selection;
const { from, to, empty } = editorRef.current.state.selection;
if (empty) return; if (empty) return;
if (insertOnNextLine) { if (insertOnNextLine) {
// move cursor to the end of the selection and insert a new line // move cursor to the end of the selection and insert a new line
editorRef.current editor.chain().focus().setTextSelection(to).insertContent("<br />").insertContent(contentHTML).run();
.chain()
.focus()
.setTextSelection(to)
.insertContent("<br />")
.insertContent(contentHTML)
.run();
} else { } else {
// replace selected text with the content provided // 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: () => ({ getDocumentInfo: () => ({
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0, characters: editor?.storage?.characterCount?.characters?.() ?? 0,
paragraphs: getParagraphCount(editorRef?.current?.state), paragraphs: getParagraphCount(editor?.state),
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0, words: editor?.storage?.characterCount?.words?.() ?? 0,
}), }),
setProviderDocument: (value) => { setProviderDocument: (value) => {
const document = provider?.document; const document = provider?.document;
@ -301,16 +278,12 @@ export const useEditor = (props: CustomEditorProps) => {
emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message), emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message),
listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) }, listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) },
}), }),
[editorRef, savedSelection] [editor]
); );
if (!editor) { if (!editor) {
return null; 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; return editor;
}; };

View file

@ -1,7 +1,7 @@
import { useImperativeHandle, useRef, MutableRefObject, useEffect } from "react";
import { HocuspocusProvider } from "@hocuspocus/provider"; import { HocuspocusProvider } from "@hocuspocus/provider";
import { EditorProps } from "@tiptap/pm/view"; 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"; import * as Y from "yjs";
// extensions // extensions
import { CoreReadOnlyEditorExtensions } from "@/extensions"; import { CoreReadOnlyEditorExtensions } from "@/extensions";
@ -11,13 +11,7 @@ import { IMarking, scrollSummary } from "@/helpers/scroll-to-node";
// props // props
import { CoreReadOnlyEditorProps } from "@/props"; import { CoreReadOnlyEditorProps } from "@/props";
// types // types
import type { import type { EditorReadOnlyRefApi, TExtensions, TFileHandler, TReadOnlyMentionHandler } from "@/types";
EditorReadOnlyRefApi,
TExtensions,
TDocumentEventsServer,
TFileHandler,
TReadOnlyMentionHandler,
} from "@/types";
interface CustomReadOnlyEditorProps { interface CustomReadOnlyEditorProps {
disabledExtensions: TExtensions[]; disabledExtensions: TExtensions[];
@ -46,8 +40,10 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
provider, provider,
} = props; } = props;
const editor = useCustomEditor({ const editor = useTiptapEditor({
editable: false, editable: false,
immediatelyRender: true,
shouldRerenderOnTransaction: false,
content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>", content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "<p></p>",
editorProps: { editorProps: {
...CoreReadOnlyEditorProps({ ...CoreReadOnlyEditorProps({
@ -77,23 +73,21 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" }); if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: "full" });
}, [editor, initialValue]); }, [editor, initialValue]);
const editorRef: MutableRefObject<Editor | null> = useRef(null);
useImperativeHandle(forwardedRef, () => ({ useImperativeHandle(forwardedRef, () => ({
clearEditor: (emitUpdate = false) => { clearEditor: (emitUpdate = false) => {
editorRef.current?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run(); editor?.chain().setMeta("skipImageDeletion", true).clearContent(emitUpdate).run();
}, },
setEditorValue: (content: string) => { setEditorValue: (content: string) => {
editorRef.current?.commands.setContent(content, false, { preserveWhitespace: "full" }); editor?.commands.setContent(content, false, { preserveWhitespace: "full" });
}, },
getMarkDown: (): string => { getMarkDown: (): string => {
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown(); const markdownOutput = editor?.storage.markdown.getMarkdown();
return markdownOutput; return markdownOutput;
}, },
getDocument: () => { getDocument: () => {
const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null; const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null;
const documentHTML = editorRef.current?.getHTML() ?? "<p></p>"; const documentHTML = editor?.getHTML() ?? "<p></p>";
const documentJSON = editorRef.current?.getJSON() ?? null; const documentJSON = editor?.getJSON() ?? null;
return { return {
binary: documentBinary, binary: documentBinary,
@ -102,35 +96,22 @@ export const useReadOnlyEditor = (props: CustomReadOnlyEditorProps) => {
}; };
}, },
scrollSummary: (marking: IMarking): void => { scrollSummary: (marking: IMarking): void => {
if (!editorRef.current) return; if (!editor) return;
scrollSummary(editorRef.current, marking); scrollSummary(editor, marking);
}, },
getDocumentInfo: () => ({ getDocumentInfo: () => {
characters: editorRef?.current?.storage?.characterCount?.characters?.() ?? 0, if (!editor) return;
paragraphs: getParagraphCount(editorRef?.current?.state), return {
words: editorRef?.current?.storage?.characterCount?.words?.() ?? 0, characters: editor.storage?.characterCount?.characters?.() ?? 0,
}), paragraphs: getParagraphCount(editor.state),
onHeadingChange: (callback: (headings: IMarking[]) => void) => { words: editor.storage?.characterCount?.words?.() ?? 0,
// 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");
}; };
}, },
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) { if (!editor) {
return null; return null;
} }
editorRef.current = editor;
return editor; return editor;
}; };

View file

@ -86,10 +86,6 @@ export type EditorReadOnlyRefApi = {
paragraphs: number; paragraphs: number;
words: number; words: number;
}; };
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
}; };
export interface EditorRefApi extends EditorReadOnlyRefApi { export interface EditorRefApi extends EditorReadOnlyRefApi {
@ -105,6 +101,10 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
getSelectedText: () => string | null; getSelectedText: () => string | null;
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void; insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
setProviderDocument: (value: Uint8Array) => void; setProviderDocument: (value: Uint8Array) => void;
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
getHeadings: () => IMarking[];
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined;
} }
// editor props // editor props

3911
yarn.lock

File diff suppressed because it is too large Load diff