feat: upgrade tiptap and handle clipboard uploads
This commit is contained in:
parent
fa9efdb5af
commit
281ecd5f6f
5 changed files with 487 additions and 219 deletions
|
|
@ -19,6 +19,7 @@ export function Dropzone({
|
|||
currentFileCount = 0,
|
||||
currentTotalBytes = 0,
|
||||
disabled = false,
|
||||
onRegisterUploadHandler,
|
||||
}: {
|
||||
onUploaded?: (files: Uploaded[]) => void;
|
||||
maxFiles?: number;
|
||||
|
|
@ -28,6 +29,7 @@ export function Dropzone({
|
|||
currentFileCount?: number;
|
||||
currentTotalBytes?: number;
|
||||
disabled?: boolean;
|
||||
onRegisterUploadHandler?: (handler: ((files: File[]) => Promise<void>) | null) => void;
|
||||
}) {
|
||||
const generateUrl = useAction(api.files.generateUploadUrl);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -122,6 +124,15 @@ export function Dropzone({
|
|||
if (uploaded.length) onUploaded?.(uploaded);
|
||||
}, [disabled, generateUrl, maxFiles, maxSize, normalizedFileCount, onUploaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onRegisterUploadHandler) return;
|
||||
const handler = (files: File[]) => startUpload(files);
|
||||
onRegisterUploadHandler(handler);
|
||||
return () => {
|
||||
onRegisterUploadHandler(null);
|
||||
};
|
||||
}, [onRegisterUploadHandler, startUpload]);
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-3", className)}>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import StarterKit from "@tiptap/starter-kit"
|
|||
import Placeholder from "@tiptap/extension-placeholder"
|
||||
import Mention from "@tiptap/extension-mention"
|
||||
import TiptapLink from "@tiptap/extension-link"
|
||||
import { Markdown } from "@tiptap/markdown"
|
||||
import { ReactRenderer } from "@tiptap/react"
|
||||
import tippy, { type Instance, type Props as TippyProps } from "tippy.js"
|
||||
// Nota: o CSS do Tippy não é obrigatório, mas melhora muito a renderização
|
||||
|
|
@ -51,6 +52,8 @@ type RichTextEditorProps = {
|
|||
ticketMention?: {
|
||||
enabled?: boolean
|
||||
}
|
||||
onEditorReady?: (editor: Editor | null) => void
|
||||
onPasteFiles?: (files: File[]) => void
|
||||
}
|
||||
|
||||
type TicketMentionItem = {
|
||||
|
|
@ -117,6 +120,24 @@ type TicketMentionAttributes = Record<string, unknown>
|
|||
const TICKET_MENTION_FALLBACK_STATUS = "PENDING"
|
||||
const TICKET_MENTION_FALLBACK_PRIORITY = "MEDIUM"
|
||||
|
||||
const MAX_PASTED_IMAGE_FILES = 10
|
||||
|
||||
function extractImageFilesFromClipboard(event: ClipboardEvent): File[] {
|
||||
const clipboard = event.clipboardData
|
||||
if (!clipboard) return []
|
||||
|
||||
const filesFromItems = Array.from(clipboard.items ?? [])
|
||||
.filter((item) => item.kind === "file")
|
||||
.map((item) => item.getAsFile())
|
||||
.filter((file): file is File => Boolean(file && file.type.startsWith("image/")))
|
||||
|
||||
if (filesFromItems.length > 0) {
|
||||
return filesFromItems
|
||||
}
|
||||
|
||||
return Array.from(clipboard.files ?? []).filter((file) => file.type.startsWith("image/"))
|
||||
}
|
||||
|
||||
function toPlainString(value: unknown): string {
|
||||
if (value === null || value === undefined) return ""
|
||||
return String(value)
|
||||
|
|
@ -767,6 +788,8 @@ export function RichTextEditor({
|
|||
disabled,
|
||||
minHeight = 120,
|
||||
ticketMention,
|
||||
onEditorReady,
|
||||
onPasteFiles,
|
||||
}: RichTextEditorProps) {
|
||||
const normalizedInitialContent = useMemo(() => {
|
||||
if (!ticketMention?.enabled) {
|
||||
|
|
@ -790,6 +813,7 @@ export function RichTextEditor({
|
|||
protocols: ["http", "https", "mailto"],
|
||||
HTMLAttributes: { rel: "noopener noreferrer", target: "_blank" },
|
||||
}),
|
||||
Markdown,
|
||||
Placeholder.configure({ placeholder }),
|
||||
]
|
||||
|
||||
|
|
@ -797,14 +821,21 @@ export function RichTextEditor({
|
|||
}, [placeholder, ticketMention?.enabled])
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
...extensions,
|
||||
],
|
||||
extensions,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
"prose prose-sm max-w-none focus:outline-none text-foreground",
|
||||
},
|
||||
handlePaste(_, event) {
|
||||
if (!onPasteFiles) return false
|
||||
const imageFiles = extractImageFilesFromClipboard(event)
|
||||
if (imageFiles.length === 0) return false
|
||||
event.preventDefault()
|
||||
const limited = imageFiles.slice(0, MAX_PASTED_IMAGE_FILES)
|
||||
onPasteFiles(limited)
|
||||
return true
|
||||
},
|
||||
},
|
||||
content: normalizedInitialContent,
|
||||
onUpdate({ editor }) {
|
||||
|
|
@ -817,6 +848,14 @@ export function RichTextEditor({
|
|||
immediatelyRender: false,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return
|
||||
onEditorReady?.(editor)
|
||||
return () => {
|
||||
onEditorReady?.(null)
|
||||
}
|
||||
}, [editor, onEditorReady])
|
||||
|
||||
const [linkPopoverOpen, setLinkPopoverOpen] = useState(false)
|
||||
const [linkUrl, setLinkUrl] = useState("")
|
||||
const linkInputRef = useRef<HTMLInputElement>(null)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue