| | '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); |
| |
|