From 2a46c1423bd2bc7a063b7a7a4156a92c3cf9c4ab Mon Sep 17 00:00:00 2001 From: cialloo Date: Sat, 25 Oct 2025 20:04:14 +0800 Subject: [PATCH] feat(blog): add onChange functionality to BlogEditor and enhance BlogImagePlugin with drag-and-drop support --- src/blog/editor/BlogEditor.tsx | 26 ++++++++++-- src/blog/editor/BlogImagePlugin.tsx | 66 +++++++++++++++++------------ 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/blog/editor/BlogEditor.tsx b/src/blog/editor/BlogEditor.tsx index f8fbbce..edf3438 100644 --- a/src/blog/editor/BlogEditor.tsx +++ b/src/blog/editor/BlogEditor.tsx @@ -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 +} + +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} /> + @@ -118,7 +139,6 @@ export default function BlogEditor({ placeholder }: BlogEditorProps) { - diff --git a/src/blog/editor/BlogImagePlugin.tsx b/src/blog/editor/BlogImagePlugin.tsx index 1eed2f7..a0323cc 100644 --- a/src/blog/editor/BlogImagePlugin.tsx +++ b/src/blog/editor/BlogImagePlugin.tsx @@ -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 = 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 }