diff --git a/app/components/Editor.tsx b/app/components/Editor.tsx index d5f3f450a..b426203e5 100644 --- a/app/components/Editor.tsx +++ b/app/components/Editor.tsx @@ -2,9 +2,11 @@ import { formatDistanceToNow } from "date-fns"; import { deburr, sortBy } from "lodash"; import { TextSelection } from "prosemirror-state"; import * as React from "react"; +import mergeRefs from "react-merge-refs"; import { Optional } from "utility-types"; import insertFiles from "@shared/editor/commands/insertFiles"; import embeds from "@shared/editor/embeds"; +import { Heading } from "@shared/editor/lib/getHeadings"; import { supportedImageMimeTypes } from "@shared/utils/files"; import getDataTransferFiles from "@shared/utils/getDataTransferFiles"; import parseDocumentSlug from "@shared/utils/parseDocumentSlug"; @@ -45,12 +47,13 @@ export type Props = Optional< shareId?: string | undefined; embedsDisabled?: boolean; grow?: boolean; + onHeadingsChange?: (headings: Heading[]) => void; onSynced?: () => Promise; onPublish?: (event: React.MouseEvent) => any; }; function Editor(props: Props, ref: React.RefObject | null) { - const { id, shareId } = props; + const { id, shareId, onChange, onHeadingsChange } = props; const { documents } = useStores(); const { showToast } = useToasts(); const dictionary = useDictionary(); @@ -58,6 +61,7 @@ function Editor(props: Props, ref: React.RefObject | null) { activeLinkEvent, setActiveLinkEvent, ] = React.useState(null); + const previousHeadings = React.useRef(null); const handleLinkActive = React.useCallback((event: MouseEvent) => { setActiveLinkEvent(event); @@ -216,11 +220,43 @@ function Editor(props: Props, ref: React.RefObject | null) { [] ); + // Calculate if headings have changed and trigger callback if so + const updateHeadings = React.useCallback(() => { + if (onHeadingsChange) { + const headings = ref?.current?.getHeadings(); + if ( + headings && + headings.map((h) => h.level + h.title).join("") !== + previousHeadings.current?.map((h) => h.level + h.title).join("") + ) { + previousHeadings.current = headings; + onHeadingsChange(headings); + } + } + }, [ref, onHeadingsChange]); + + const handleChange = React.useCallback( + (event) => { + onChange?.(event); + updateHeadings(); + }, + [onChange, updateHeadings] + ); + + const handleRefChanged = React.useCallback( + (node: SharedEditor | null) => { + if (node && !previousHeadings.current) { + updateHeadings(); + } + }, + [updateHeadings] + ); + return ( <> | null) { onHoverLink={handleLinkActive} onClickLink={onClickLink} onSearchLink={handleSearchLink} + onChange={handleChange} placeholder={props.placeholder || ""} defaultValue={props.defaultValue || ""} /> diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index f934aca1b..c48a473ed 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -75,7 +75,7 @@ type Props = WithTranslation & @observer class DocumentScene extends React.Component { @observable - editor: TEditor | null; + editor = React.createRef(); @observable isUploading = false; @@ -157,7 +157,7 @@ class DocumentScene extends React.Component { } replaceDocument = (template: Document | Revision) => { - const editorRef = this.editor; + const editorRef = this.editor.current; if (!editorRef) { return; @@ -194,7 +194,7 @@ class DocumentScene extends React.Component { const { toasts, history, location, t } = this.props; const restore = location.state?.restore; const revisionId = location.state?.revisionId; - const editorRef = this.editor; + const editorRef = this.editor.current; if (!editorRef || !restore) { return; @@ -379,17 +379,8 @@ class DocumentScene extends React.Component { const { document, auth } = this.props; this.getEditorText = getEditorText; - // Keep headings in sync for table of contents - const headings = this.editor?.getHeadings() ?? []; - if ( - headings.map((h) => h.level + h.title).join("") !== - this.headings.map((h) => h.level + h.title).join("") - ) { - this.headings = headings; - } - // Keep derived task list in sync - const tasks = this.editor?.getTasks(); + const tasks = this.editor.current?.getTasks(); const total = tasks?.length ?? 0; const completed = tasks?.filter((t) => t.completed).length ?? 0; document.updateTasks(total, completed); @@ -414,6 +405,10 @@ class DocumentScene extends React.Component { } }; + onHeadingsChange = (headings: Heading[]) => { + this.headings = headings; + }; + onChangeTitle = action((value: string) => { this.title = value; this.props.document.title = value; @@ -427,11 +422,6 @@ class DocumentScene extends React.Component { } }; - handleRef = (ref: TEditor | null) => { - this.editor = ref; - this.headings = this.editor?.getHeadings() ?? []; - }; - render() { const { document, @@ -586,7 +576,7 @@ class DocumentScene extends React.Component { { onCreateLink={this.props.onCreateLink} onChangeTitle={this.onChangeTitle} onChange={this.onChange} + onHeadingsChange={this.onHeadingsChange} onSave={this.onSave} onPublish={this.onPublish} onCancel={this.goBack} diff --git a/package.json b/package.json index 4cec5aba8..119e1f796 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "react-helmet": "^6.1.0", "react-i18next": "^11.16.6", "react-medium-image-zoom": "^3.1.3", + "react-merge-refs": "^1.1.0", "react-portal": "^4.2.0", "react-router-dom": "^5.2.0", "react-table": "^7.7.0", diff --git a/yarn.lock b/yarn.lock index fbdbdf820..73e446f7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12459,6 +12459,11 @@ react-medium-image-zoom@^3.1.3: resolved "https://registry.yarnpkg.com/react-medium-image-zoom/-/react-medium-image-zoom-3.1.3.tgz#b1470abc5a342d65c23021c01bafa8c731821478" integrity sha512-5CoU8whSCz5Xz2xNeGD34dDfZ6jaf/pybdfZh8HNUmA9mbXbLfj0n6bQWfEUwkq9lsNg1sEkyeIJq2tcvZY8bw== +react-merge-refs@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06" + integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ== + react-portal@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.1.tgz#12c1599238c06fb08a9800f3070bea2a3f78b1a6"