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 { ContentEditable } from '@lexical/react/LexicalContentEditable'
|
||||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
|
||||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
|
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 { HeadingNode, QuoteNode } from '@lexical/rich-text'
|
||||||
import { CodeNode, CodeHighlightNode } from '@lexical/code'
|
import { CodeNode, CodeHighlightNode } from '@lexical/code'
|
||||||
import { ListItemNode, ListNode } from '@lexical/list'
|
import { ListItemNode, ListNode } from '@lexical/list'
|
||||||
@@ -20,12 +22,13 @@ import { LinkNode, AutoLinkNode } from '@lexical/link'
|
|||||||
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'
|
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'
|
||||||
import { AutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin'
|
import { AutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin'
|
||||||
import { HashtagNode } from '@lexical/hashtag'
|
import { HashtagNode } from '@lexical/hashtag'
|
||||||
|
import { $generateHtmlFromNodes } from '@lexical/html'
|
||||||
|
import type { EditorState } from 'lexical'
|
||||||
|
|
||||||
import { ImageNode } from '../../editor/nodes/ImageNode'
|
import { ImageNode } from '../../editor/nodes/ImageNode'
|
||||||
import { MentionNode } from '../../editor/nodes/MentionNode'
|
import { MentionNode } from '../../editor/nodes/MentionNode'
|
||||||
import ToolbarPlugin from '../../editor/plugins/ToolbarPlugin'
|
import ToolbarPlugin from '../../editor/plugins/ToolbarPlugin'
|
||||||
import MarkdownPlugin from '../../editor/plugins/MarkdownShortcutPlugin'
|
import MarkdownPlugin from '../../editor/plugins/MarkdownShortcutPlugin'
|
||||||
import DragDropPastePlugin from '../../editor/plugins/DragDropPastePlugin'
|
|
||||||
import HashtagPlugin from '../../editor/plugins/HashtagPlugin'
|
import HashtagPlugin from '../../editor/plugins/HashtagPlugin'
|
||||||
import MentionsPlugin from '../../editor/plugins/MentionsPlugin'
|
import MentionsPlugin from '../../editor/plugins/MentionsPlugin'
|
||||||
import editorTheme from '../../editor/themes/EditorTheme'
|
import editorTheme from '../../editor/themes/EditorTheme'
|
||||||
@@ -69,9 +72,26 @@ const MATCHERS = [
|
|||||||
|
|
||||||
interface BlogEditorProps {
|
interface BlogEditorProps {
|
||||||
placeholder?: string
|
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 = {
|
const editorConfig = {
|
||||||
namespace: 'BlogEditor',
|
namespace: 'BlogEditor',
|
||||||
theme: editorTheme,
|
theme: editorTheme,
|
||||||
@@ -111,6 +131,7 @@ export default function BlogEditor({ placeholder }: BlogEditorProps) {
|
|||||||
}
|
}
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
/>
|
/>
|
||||||
|
<OnChangeWrapper onChange={onChange} />
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<ListPlugin />
|
<ListPlugin />
|
||||||
<CheckListPlugin />
|
<CheckListPlugin />
|
||||||
@@ -118,7 +139,6 @@ export default function BlogEditor({ placeholder }: BlogEditorProps) {
|
|||||||
<AutoLinkPlugin matchers={MATCHERS} />
|
<AutoLinkPlugin matchers={MATCHERS} />
|
||||||
<TablePlugin />
|
<TablePlugin />
|
||||||
<BlogImagePlugin />
|
<BlogImagePlugin />
|
||||||
<DragDropPastePlugin />
|
|
||||||
<HashtagPlugin />
|
<HashtagPlugin />
|
||||||
<MentionsPlugin />
|
<MentionsPlugin />
|
||||||
<MarkdownPlugin />
|
<MarkdownPlugin />
|
||||||
|
|||||||
@@ -4,14 +4,28 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
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 { 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 type { LexicalCommand } from 'lexical'
|
||||||
import { $createImageNode, ImageNode } from '../../editor/nodes/ImageNode'
|
import { $createImageNode, ImageNode } from '../../editor/nodes/ImageNode'
|
||||||
import { uploadFile, getDownloadPresignedURL } from '../api'
|
import { uploadFile, getDownloadPresignedURL } from '../api'
|
||||||
|
|
||||||
export const INSERT_BLOG_IMAGE_COMMAND: LexicalCommand<File> = createCommand('INSERT_BLOG_IMAGE_COMMAND')
|
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 {
|
interface UploadingImage {
|
||||||
id: string
|
id: string
|
||||||
file: File
|
file: File
|
||||||
@@ -27,6 +41,7 @@ export function BlogImagePlugin() {
|
|||||||
throw new Error('BlogImagePlugin: ImageNode not registered on editor')
|
throw new Error('BlogImagePlugin: ImageNode not registered on editor')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register command to insert blog images
|
||||||
return editor.registerCommand(
|
return editor.registerCommand(
|
||||||
INSERT_BLOG_IMAGE_COMMAND,
|
INSERT_BLOG_IMAGE_COMMAND,
|
||||||
(file: File) => {
|
(file: File) => {
|
||||||
@@ -55,6 +70,28 @@ export function BlogImagePlugin() {
|
|||||||
)
|
)
|
||||||
}, [editor])
|
}, [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) => {
|
const handleImageUpload = async (file: File, uploadId: string, localUrl: string) => {
|
||||||
try {
|
try {
|
||||||
// Upload file and get fileKey
|
// 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user