|
|
'use client'; |
|
|
|
|
|
import { exampleSetup } from 'prosemirror-example-setup'; |
|
|
import { inputRules } from 'prosemirror-inputrules'; |
|
|
import { EditorState } from 'prosemirror-state'; |
|
|
import { EditorView } from 'prosemirror-view'; |
|
|
import React, { memo, useEffect, useRef } from 'react'; |
|
|
|
|
|
import type { Suggestion } from '@/lib/db/schema'; |
|
|
import { |
|
|
documentSchema, |
|
|
handleTransaction, |
|
|
headingRule, |
|
|
} from '@/lib/editor/config'; |
|
|
import { |
|
|
buildContentFromDocument, |
|
|
buildDocumentFromContent, |
|
|
createDecorations, |
|
|
} from '@/lib/editor/functions'; |
|
|
import { |
|
|
projectWithPositions, |
|
|
suggestionsPlugin, |
|
|
suggestionsPluginKey, |
|
|
} from '@/lib/editor/suggestions'; |
|
|
|
|
|
type EditorProps = { |
|
|
content: string; |
|
|
onSaveContent: (updatedContent: string, debounce: boolean) => void; |
|
|
status: 'streaming' | 'idle'; |
|
|
isCurrentVersion: boolean; |
|
|
currentVersionIndex: number; |
|
|
suggestions: Array<Suggestion>; |
|
|
}; |
|
|
|
|
|
function PureEditor({ |
|
|
content, |
|
|
onSaveContent, |
|
|
suggestions, |
|
|
status, |
|
|
}: EditorProps) { |
|
|
const containerRef = useRef<HTMLDivElement>(null); |
|
|
const editorRef = useRef<EditorView | null>(null); |
|
|
|
|
|
useEffect(() => { |
|
|
if (containerRef.current && !editorRef.current) { |
|
|
const state = EditorState.create({ |
|
|
doc: buildDocumentFromContent(content), |
|
|
plugins: [ |
|
|
...exampleSetup({ schema: documentSchema, menuBar: false }), |
|
|
inputRules({ |
|
|
rules: [ |
|
|
headingRule(1), |
|
|
headingRule(2), |
|
|
headingRule(3), |
|
|
headingRule(4), |
|
|
headingRule(5), |
|
|
headingRule(6), |
|
|
], |
|
|
}), |
|
|
suggestionsPlugin, |
|
|
], |
|
|
}); |
|
|
|
|
|
editorRef.current = new EditorView(containerRef.current, { |
|
|
state, |
|
|
}); |
|
|
} |
|
|
|
|
|
return () => { |
|
|
if (editorRef.current) { |
|
|
editorRef.current.destroy(); |
|
|
editorRef.current = null; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
|
if (editorRef.current) { |
|
|
editorRef.current.setProps({ |
|
|
dispatchTransaction: (transaction) => { |
|
|
handleTransaction({ |
|
|
transaction, |
|
|
editorRef, |
|
|
onSaveContent, |
|
|
}); |
|
|
}, |
|
|
}); |
|
|
} |
|
|
}, [onSaveContent]); |
|
|
|
|
|
useEffect(() => { |
|
|
if (editorRef.current && content) { |
|
|
const currentContent = buildContentFromDocument( |
|
|
editorRef.current.state.doc, |
|
|
); |
|
|
|
|
|
if (status === 'streaming') { |
|
|
const newDocument = buildDocumentFromContent(content); |
|
|
|
|
|
const transaction = editorRef.current.state.tr.replaceWith( |
|
|
0, |
|
|
editorRef.current.state.doc.content.size, |
|
|
newDocument.content, |
|
|
); |
|
|
|
|
|
transaction.setMeta('no-save', true); |
|
|
editorRef.current.dispatch(transaction); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (currentContent !== content) { |
|
|
const newDocument = buildDocumentFromContent(content); |
|
|
|
|
|
const transaction = editorRef.current.state.tr.replaceWith( |
|
|
0, |
|
|
editorRef.current.state.doc.content.size, |
|
|
newDocument.content, |
|
|
); |
|
|
|
|
|
transaction.setMeta('no-save', true); |
|
|
editorRef.current.dispatch(transaction); |
|
|
} |
|
|
} |
|
|
}, [content, status]); |
|
|
|
|
|
useEffect(() => { |
|
|
if (editorRef.current?.state.doc && content) { |
|
|
const projectedSuggestions = projectWithPositions( |
|
|
editorRef.current.state.doc, |
|
|
suggestions, |
|
|
).filter( |
|
|
(suggestion) => suggestion.selectionStart && suggestion.selectionEnd, |
|
|
); |
|
|
|
|
|
const decorations = createDecorations( |
|
|
projectedSuggestions, |
|
|
editorRef.current, |
|
|
); |
|
|
|
|
|
const transaction = editorRef.current.state.tr; |
|
|
transaction.setMeta(suggestionsPluginKey, { decorations }); |
|
|
editorRef.current.dispatch(transaction); |
|
|
} |
|
|
}, [suggestions, content]); |
|
|
|
|
|
return ( |
|
|
<div className="relative prose dark:prose-invert" ref={containerRef} /> |
|
|
); |
|
|
} |
|
|
|
|
|
function areEqual(prevProps: EditorProps, nextProps: EditorProps) { |
|
|
return ( |
|
|
prevProps.suggestions === nextProps.suggestions && |
|
|
prevProps.currentVersionIndex === nextProps.currentVersionIndex && |
|
|
prevProps.isCurrentVersion === nextProps.isCurrentVersion && |
|
|
!(prevProps.status === 'streaming' && nextProps.status === 'streaming') && |
|
|
prevProps.content === nextProps.content && |
|
|
prevProps.onSaveContent === nextProps.onSaveContent |
|
|
); |
|
|
} |
|
|
|
|
|
export const Editor = memo(PureEditor, areEqual); |
|
|
|