feat: 💨
This commit is contained in:
parent
9969ab2414
commit
c6a5157fe7
10 changed files with 236 additions and 258 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import mediaUpload from '@/services/media-upload.service'
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { EditorView } from '@tiptap/pm/view'
|
||||
import { Slice } from '@tiptap/pm/model'
|
||||
import { Plugin, TextSelection } from 'prosemirror-state'
|
||||
|
||||
const DRAGOVER_CLASS_LIST = [
|
||||
|
|
@ -13,12 +12,9 @@ const DRAGOVER_CLASS_LIST = [
|
|||
]
|
||||
|
||||
export interface ClipboardAndDropHandlerOptions {
|
||||
onUploadStart?: (file: File) => void
|
||||
onUploadSuccess?: (file: File, result: any) => void
|
||||
onUploadError?: (file: File, error: any) => void
|
||||
onUploadEnd?: () => void
|
||||
onUploadStart?: (file: File, cancel: () => void) => void
|
||||
onUploadEnd?: (file: File) => void
|
||||
onUploadProgress?: (file: File, progress: number) => void
|
||||
onProvideCancel?: (cancel: () => void) => void
|
||||
}
|
||||
|
||||
export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerOptions>({
|
||||
|
|
@ -57,7 +53,7 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
|
|||
return true
|
||||
}
|
||||
},
|
||||
handleDrop(view: EditorView, event: DragEvent, _slice: Slice, _moved: boolean) {
|
||||
handleDrop(view: EditorView, event: DragEvent) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
view.dom.classList.remove(...DRAGOVER_CLASS_LIST)
|
||||
|
|
@ -68,7 +64,7 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
|
|||
)
|
||||
if (!mediaFiles.length) return false
|
||||
|
||||
uploadFile(view, mediaFiles, options)
|
||||
uploadFiles(view, mediaFiles, options)
|
||||
return true
|
||||
},
|
||||
handlePaste(view, event) {
|
||||
|
|
@ -82,7 +78,7 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
|
|||
) {
|
||||
const file = item.getAsFile()
|
||||
if (file) {
|
||||
uploadFile(view, [file], options)
|
||||
uploadFiles(view, [file], options)
|
||||
handled = true
|
||||
}
|
||||
} else if (item.kind === 'string' && item.type === 'text/plain') {
|
||||
|
|
@ -116,16 +112,22 @@ export const ClipboardAndDropHandler = Extension.create<ClipboardAndDropHandlerO
|
|||
}
|
||||
})
|
||||
|
||||
async function uploadFile(
|
||||
async function uploadFiles(
|
||||
view: EditorView,
|
||||
files: File[],
|
||||
options: ClipboardAndDropHandlerOptions
|
||||
) {
|
||||
const abortControllers = new Map<File, AbortController>()
|
||||
files.forEach((file) => {
|
||||
const abortController = new AbortController()
|
||||
abortControllers.set(file, abortController)
|
||||
options.onUploadStart?.(file, () => abortController.abort())
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000))
|
||||
|
||||
for (const file of files) {
|
||||
const name = file.name
|
||||
|
||||
options.onUploadStart?.(file)
|
||||
|
||||
const placeholder = `[Uploading "${name}"...]`
|
||||
const uploadingNode = view.state.schema.text(placeholder)
|
||||
const hardBreakNode = view.state.schema.nodes.hardBreak.create()
|
||||
|
|
@ -133,16 +135,15 @@ async function uploadFile(
|
|||
tr = tr.insert(tr.selection.from, hardBreakNode)
|
||||
view.dispatch(tr)
|
||||
|
||||
const abortController = new AbortController()
|
||||
options.onProvideCancel?.(() => abortController.abort())
|
||||
const abortController = abortControllers.get(file)
|
||||
|
||||
mediaUpload
|
||||
.upload(file, {
|
||||
onProgress: (p) => options.onUploadProgress?.(file, p),
|
||||
signal: abortController.signal
|
||||
signal: abortController?.signal
|
||||
})
|
||||
.then((result) => {
|
||||
options.onUploadSuccess?.(file, result)
|
||||
options.onUploadEnd?.(file)
|
||||
const urlNode = view.state.schema.text(result.url)
|
||||
|
||||
const tr = view.state.tr
|
||||
|
|
@ -175,11 +176,10 @@ async function uploadFile(
|
|||
insertTr.setSelection(TextSelection.near(insertTr.doc.resolve(newPos)))
|
||||
view.dispatch(insertTr)
|
||||
}
|
||||
options.onUploadEnd?.()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Upload failed:', error)
|
||||
options.onUploadError?.(file, error)
|
||||
options.onUploadEnd?.(file)
|
||||
|
||||
const tr = view.state.tr
|
||||
let didReplace = false
|
||||
|
|
@ -200,7 +200,6 @@ async function uploadFile(
|
|||
if (didReplace) {
|
||||
view.dispatch(tr)
|
||||
}
|
||||
options.onUploadEnd?.()
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { EditorContent, useEditor } from '@tiptap/react'
|
|||
import { Event } from 'nostr-tools'
|
||||
import { Dispatch, forwardRef, SetStateAction, useImperativeHandle } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePostEditor } from '../PostEditorProvider'
|
||||
import { ClipboardAndDropHandler } from './ClipboardAndDropHandler'
|
||||
import CustomMention from './CustomMention'
|
||||
import Preview from './Preview'
|
||||
|
|
@ -33,124 +32,123 @@ const PostTextarea = forwardRef<
|
|||
parentEvent?: Event
|
||||
onSubmit?: () => void
|
||||
className?: string
|
||||
onUploadStart?: (file: File) => void
|
||||
onUploadProgress?: (progress: number, file: File) => void
|
||||
onUploadEnd?: () => void
|
||||
onProvideCancel?: (cancel: () => void) => void
|
||||
onUploadStart?: (file: File, cancel: () => void) => void
|
||||
onUploadProgress?: (file: File, progress: number) => void
|
||||
onUploadEnd?: (file: File) => void
|
||||
}
|
||||
>(({
|
||||
text = '',
|
||||
setText,
|
||||
defaultContent,
|
||||
parentEvent,
|
||||
onSubmit,
|
||||
className,
|
||||
onUploadStart,
|
||||
onUploadProgress,
|
||||
onUploadEnd,
|
||||
onProvideCancel
|
||||
}, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { setUploadingFiles } = usePostEditor()
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
History,
|
||||
HardBreak,
|
||||
Placeholder.configure({
|
||||
placeholder: t('Write something...') + ' (' + t('Paste or drop media files to upload') + ')'
|
||||
}),
|
||||
CustomMention.configure({
|
||||
suggestion
|
||||
}),
|
||||
ClipboardAndDropHandler.configure({
|
||||
onUploadStart: (file) => {
|
||||
setUploadingFiles((prev) => prev + 1)
|
||||
onUploadStart?.(file)
|
||||
>(
|
||||
(
|
||||
{
|
||||
text = '',
|
||||
setText,
|
||||
defaultContent,
|
||||
parentEvent,
|
||||
onSubmit,
|
||||
className,
|
||||
onUploadStart,
|
||||
onUploadProgress,
|
||||
onUploadEnd
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
History,
|
||||
HardBreak,
|
||||
Placeholder.configure({
|
||||
placeholder:
|
||||
t('Write something...') + ' (' + t('Paste or drop media files to upload') + ')'
|
||||
}),
|
||||
CustomMention.configure({
|
||||
suggestion
|
||||
}),
|
||||
ClipboardAndDropHandler.configure({
|
||||
onUploadStart: (file, cancel) => {
|
||||
onUploadStart?.(file, cancel)
|
||||
},
|
||||
onUploadEnd: (file) => onUploadEnd?.(file),
|
||||
onUploadProgress: (file, p) => onUploadProgress?.(file, p)
|
||||
})
|
||||
],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: cn(
|
||||
'border rounded-lg p-3 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
className
|
||||
)
|
||||
},
|
||||
onUploadSuccess: () => setUploadingFiles((prev) => prev - 1),
|
||||
onUploadError: () => setUploadingFiles((prev) => prev - 1),
|
||||
onUploadEnd: () => onUploadEnd?.(),
|
||||
onUploadProgress: (file, p) => onUploadProgress?.(p, file),
|
||||
onProvideCancel: (cancel) => onProvideCancel?.(cancel)
|
||||
})
|
||||
],
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: cn(
|
||||
'border rounded-lg p-3 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
className
|
||||
)
|
||||
},
|
||||
handleKeyDown: (_view, event) => {
|
||||
// Handle Ctrl+Enter or Cmd+Enter for submit
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
onSubmit?.()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
content: postEditorCache.getPostContentCache({ defaultContent, parentEvent }),
|
||||
onUpdate(props) {
|
||||
setText(parseEditorJsonToText(props.editor.getJSON()))
|
||||
postEditorCache.setPostContentCache({ defaultContent, parentEvent }, props.editor.getJSON())
|
||||
},
|
||||
onCreate(props) {
|
||||
setText(parseEditorJsonToText(props.editor.getJSON()))
|
||||
}
|
||||
})
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
appendText: (text: string, addNewline = false) => {
|
||||
if (editor) {
|
||||
let chain = editor
|
||||
.chain()
|
||||
.focus()
|
||||
.command(({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
const endPos = tr.doc.content.size
|
||||
const selection = TextSelection.create(tr.doc, endPos)
|
||||
tr.setSelection(selection)
|
||||
dispatch(tr)
|
||||
}
|
||||
handleKeyDown: (_view, event) => {
|
||||
// Handle Ctrl+Enter or Cmd+Enter for submit
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
onSubmit?.()
|
||||
return true
|
||||
})
|
||||
.insertContent(text)
|
||||
if (addNewline) {
|
||||
chain = chain.setHardBreak()
|
||||
}
|
||||
return false
|
||||
}
|
||||
chain.run()
|
||||
},
|
||||
content: postEditorCache.getPostContentCache({ defaultContent, parentEvent }),
|
||||
onUpdate(props) {
|
||||
setText(parseEditorJsonToText(props.editor.getJSON()))
|
||||
postEditorCache.setPostContentCache({ defaultContent, parentEvent }, props.editor.getJSON())
|
||||
},
|
||||
onCreate(props) {
|
||||
setText(parseEditorJsonToText(props.editor.getJSON()))
|
||||
}
|
||||
},
|
||||
insertText: (text: string) => {
|
||||
if (editor) {
|
||||
editor.chain().focus().insertContent(text).run()
|
||||
})
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
appendText: (text: string, addNewline = false) => {
|
||||
if (editor) {
|
||||
let chain = editor
|
||||
.chain()
|
||||
.focus()
|
||||
.command(({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
const endPos = tr.doc.content.size
|
||||
const selection = TextSelection.create(tr.doc, endPos)
|
||||
tr.setSelection(selection)
|
||||
dispatch(tr)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.insertContent(text)
|
||||
if (addNewline) {
|
||||
chain = chain.setHardBreak()
|
||||
}
|
||||
chain.run()
|
||||
}
|
||||
},
|
||||
insertText: (text: string) => {
|
||||
if (editor) {
|
||||
editor.chain().focus().insertContent(text).run()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
}))
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
return (
|
||||
<Tabs defaultValue="edit" className="space-y-2">
|
||||
<TabsList>
|
||||
<TabsTrigger value="edit">{t('Edit')}</TabsTrigger>
|
||||
<TabsTrigger value="preview">{t('Preview')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="edit">
|
||||
<EditorContent className="tiptap" editor={editor} />
|
||||
</TabsContent>
|
||||
<TabsContent value="preview">
|
||||
<Preview content={text} className={className} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="edit" className="space-y-2">
|
||||
<TabsList>
|
||||
<TabsTrigger value="edit">{t('Edit')}</TabsTrigger>
|
||||
<TabsTrigger value="preview">{t('Preview')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="edit">
|
||||
<EditorContent className="tiptap" editor={editor} />
|
||||
</TabsContent>
|
||||
<TabsContent value="preview">
|
||||
<Preview content={text} className={className} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
})
|
||||
)
|
||||
PostTextarea.displayName = 'PostTextarea'
|
||||
export default PostTextarea
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue