/** * Blog Editor Component * Extends the base RichTextEditor with blog-specific functionality */ import { LexicalComposer } from '@lexical/react/LexicalComposer' import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' import { ContentEditable } from '@lexical/react/LexicalContentEditable' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { HeadingNode, QuoteNode } from '@lexical/rich-text' import { CodeNode, CodeHighlightNode } from '@lexical/code' import { ListItemNode, ListNode } from '@lexical/list' import { ListPlugin } from '@lexical/react/LexicalListPlugin' import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin' import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode' import { TableCellNode, TableNode, TableRowNode } from '@lexical/table' import { TablePlugin } from '@lexical/react/LexicalTablePlugin' import { LinkNode, AutoLinkNode } from '@lexical/link' import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin' import { AutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin' import { HashtagNode } from '@lexical/hashtag' import { $generateHtmlFromNodes } from '@lexical/html' import type { EditorState } from 'lexical' import { ImageNode } from '../../editor/nodes/ImageNode' import { MentionNode } from '../../editor/nodes/MentionNode' import ToolbarPlugin from '../../editor/plugins/ToolbarPlugin' import MarkdownPlugin from '../../editor/plugins/MarkdownShortcutPlugin' import HashtagPlugin from '../../editor/plugins/HashtagPlugin' import MentionsPlugin from '../../editor/plugins/MentionsPlugin' import editorTheme from '../../editor/themes/EditorTheme' import { BlogImagePlugin } from './BlogImagePlugin' import '../../editor/styles/editor.css' const URL_MATCHER = /((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/ const EMAIL_MATCHER = /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/ const MATCHERS = [ (text: string) => { const match = URL_MATCHER.exec(text) if (match === null) { return null } const fullMatch = match[0] return { index: match.index, length: fullMatch.length, text: fullMatch, url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`, } }, (text: string) => { const match = EMAIL_MATCHER.exec(text) if (match === null) { return null } const fullMatch = match[0] return { index: match.index, length: fullMatch.length, text: fullMatch, url: `mailto:${fullMatch}`, } }, ] interface BlogEditorProps { placeholder?: string onChange?: (html: string) => void } /** * OnChange wrapper component that has access to editor context */ function OnChangeWrapper({ onChange }: { onChange?: (html: string) => void }) { const [editor] = useLexicalComposerContext() const handleChange = (editorState: EditorState) => { editorState.read(() => { const html = $generateHtmlFromNodes(editor) onChange?.(html) }) } return } export default function BlogEditor({ placeholder, onChange }: BlogEditorProps) { const editorConfig = { namespace: 'BlogEditor', theme: editorTheme, onError(error: Error) { console.error(error) }, nodes: [ HeadingNode, QuoteNode, CodeNode, CodeHighlightNode, ListNode, ListItemNode, HorizontalRuleNode, TableNode, TableRowNode, TableCellNode, LinkNode, AutoLinkNode, ImageNode, HashtagNode, MentionNode, ], } return ( } placeholder={ {placeholder || 'Start writing your blog post...'} } ErrorBoundary={LexicalErrorBoundary} /> ) }