All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 20s
150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
/**
|
|
* 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 <OnChangePlugin onChange={handleChange} />
|
|
}
|
|
|
|
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 (
|
|
<LexicalComposer initialConfig={editorConfig}>
|
|
<div className="editor-container">
|
|
<ToolbarPlugin />
|
|
<div className="editor-inner">
|
|
<RichTextPlugin
|
|
contentEditable={<ContentEditable className="editor-input" />}
|
|
placeholder={
|
|
<div className="editor-placeholder">
|
|
{placeholder || 'Start writing your blog post...'}
|
|
</div>
|
|
}
|
|
ErrorBoundary={LexicalErrorBoundary}
|
|
/>
|
|
<OnChangeWrapper onChange={onChange} />
|
|
<HistoryPlugin />
|
|
<ListPlugin />
|
|
<CheckListPlugin />
|
|
<LinkPlugin />
|
|
<AutoLinkPlugin matchers={MATCHERS} />
|
|
<TablePlugin />
|
|
<BlogImagePlugin />
|
|
<HashtagPlugin />
|
|
<MentionsPlugin />
|
|
<MarkdownPlugin />
|
|
</div>
|
|
</div>
|
|
</LexicalComposer>
|
|
)
|
|
}
|