feat(blog): add onChange functionality to BlogEditor and enhance BlogImagePlugin with drag-and-drop support
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 20s
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 20s
This commit is contained in:
@@ -8,6 +8,8 @@ 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'
|
||||
@@ -20,12 +22,13 @@ 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 DragDropPastePlugin from '../../editor/plugins/DragDropPastePlugin'
|
||||
import HashtagPlugin from '../../editor/plugins/HashtagPlugin'
|
||||
import MentionsPlugin from '../../editor/plugins/MentionsPlugin'
|
||||
import editorTheme from '../../editor/themes/EditorTheme'
|
||||
@@ -69,9 +72,26 @@ const MATCHERS = [
|
||||
|
||||
interface BlogEditorProps {
|
||||
placeholder?: string
|
||||
onChange?: (html: string) => void
|
||||
}
|
||||
|
||||
export default function BlogEditor({ placeholder }: BlogEditorProps) {
|
||||
/**
|
||||
* 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,
|
||||
@@ -111,6 +131,7 @@ export default function BlogEditor({ placeholder }: BlogEditorProps) {
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<OnChangeWrapper onChange={onChange} />
|
||||
<HistoryPlugin />
|
||||
<ListPlugin />
|
||||
<CheckListPlugin />
|
||||
@@ -118,7 +139,6 @@ export default function BlogEditor({ placeholder }: BlogEditorProps) {
|
||||
<AutoLinkPlugin matchers={MATCHERS} />
|
||||
<TablePlugin />
|
||||
<BlogImagePlugin />
|
||||
<DragDropPastePlugin />
|
||||
<HashtagPlugin />
|
||||
<MentionsPlugin />
|
||||
<MarkdownPlugin />
|
||||
|
||||
@@ -4,14 +4,28 @@
|
||||
*/
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { DRAG_DROP_PASTE } from '@lexical/rich-text'
|
||||
import { isMimeType, mediaFileReader } from '@lexical/utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
|
||||
import { $insertNodes, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_LOW, createCommand } from 'lexical'
|
||||
import type { LexicalCommand } from 'lexical'
|
||||
import { $createImageNode, ImageNode } from '../../editor/nodes/ImageNode'
|
||||
import { uploadFile, getDownloadPresignedURL } from '../api'
|
||||
|
||||
export const INSERT_BLOG_IMAGE_COMMAND: LexicalCommand<File> = createCommand('INSERT_BLOG_IMAGE_COMMAND')
|
||||
|
||||
const ACCEPTABLE_IMAGE_TYPES = [
|
||||
'image/',
|
||||
'image/heic',
|
||||
'image/heif',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/svg+xml',
|
||||
]
|
||||
|
||||
interface UploadingImage {
|
||||
id: string
|
||||
file: File
|
||||
@@ -27,6 +41,7 @@ export function BlogImagePlugin() {
|
||||
throw new Error('BlogImagePlugin: ImageNode not registered on editor')
|
||||
}
|
||||
|
||||
// Register command to insert blog images
|
||||
return editor.registerCommand(
|
||||
INSERT_BLOG_IMAGE_COMMAND,
|
||||
(file: File) => {
|
||||
@@ -55,6 +70,28 @@ export function BlogImagePlugin() {
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
// Handle drag and drop / paste events
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
DRAG_DROP_PASTE,
|
||||
(files) => {
|
||||
(async () => {
|
||||
const filesResult = await mediaFileReader(
|
||||
files,
|
||||
[ACCEPTABLE_IMAGE_TYPES].flatMap((x) => x),
|
||||
)
|
||||
for (const { file } of filesResult) {
|
||||
if (isMimeType(file, ACCEPTABLE_IMAGE_TYPES)) {
|
||||
editor.dispatchCommand(INSERT_BLOG_IMAGE_COMMAND, file)
|
||||
}
|
||||
}
|
||||
})()
|
||||
return true
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
const handleImageUpload = async (file: File, uploadId: string, localUrl: string) => {
|
||||
try {
|
||||
// Upload file and get fileKey
|
||||
@@ -109,33 +146,6 @@ export function BlogImagePlugin() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle paste events
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
// @ts-ignore - PASTE_COMMAND exists
|
||||
editor.PASTE_COMMAND || 'PASTE_COMMAND',
|
||||
(event: ClipboardEvent) => {
|
||||
const items = event.clipboardData?.items
|
||||
if (!items) return false
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
event.preventDefault()
|
||||
const file = item.getAsFile()
|
||||
if (file) {
|
||||
editor.dispatchCommand(INSERT_BLOG_IMAGE_COMMAND, file)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
COMMAND_PRIORITY_EDITOR
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user