Spaces:
Runtime error
Runtime error
| import { NodeViewWrapper } from "@tiptap/react"; | |
| import { useCallback, useRef, useState } from "react"; | |
| import type { NodeViewProps } from "@tiptap/react"; | |
| import { uploadImage } from "./upload"; | |
| export function ImageUploadView({ editor, getPos }: NodeViewProps) { | |
| const [dragging, setDragging] = useState(false); | |
| const [uploading, setUploading] = useState(false); | |
| const [urlMode, setUrlMode] = useState(false); | |
| const [urlValue, setUrlValue] = useState(""); | |
| const fileRef = useRef<HTMLInputElement>(null); | |
| const replaceWithImage = useCallback( | |
| (src: string) => { | |
| const pos = getPos(); | |
| if (pos === undefined) return; | |
| editor | |
| .chain() | |
| .focus() | |
| .deleteRange({ from: pos, to: pos + 1 }) | |
| .insertContentAt(pos, { | |
| type: "image", | |
| attrs: { src }, | |
| }) | |
| .run(); | |
| }, | |
| [editor, getPos], | |
| ); | |
| const handleFiles = useCallback( | |
| (files: FileList | File[]) => { | |
| const file = Array.from(files).find((f) => f.type.startsWith("image/")); | |
| if (!file) return; | |
| setUploading(true); | |
| uploadImage(file) | |
| .then((url) => replaceWithImage(url)) | |
| .catch((err) => { | |
| console.error("[image-upload]", err); | |
| setUploading(false); | |
| }); | |
| }, | |
| [replaceWithImage], | |
| ); | |
| const onDrop = useCallback( | |
| (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| setDragging(false); | |
| if (e.dataTransfer.files.length) handleFiles(e.dataTransfer.files); | |
| }, | |
| [handleFiles], | |
| ); | |
| const onDragOver = useCallback((e: React.DragEvent) => { | |
| e.preventDefault(); | |
| setDragging(true); | |
| }, []); | |
| const onDragLeave = useCallback(() => setDragging(false), []); | |
| const submitUrl = useCallback(() => { | |
| const src = urlValue.trim(); | |
| if (src) replaceWithImage(src); | |
| }, [urlValue, replaceWithImage]); | |
| return ( | |
| <NodeViewWrapper> | |
| <div | |
| className={`image-upload-card${dragging ? " dragging" : ""}`} | |
| onDrop={onDrop} | |
| onDragOver={onDragOver} | |
| onDragLeave={onDragLeave} | |
| contentEditable={false} | |
| > | |
| <input | |
| ref={fileRef} | |
| type="file" | |
| accept="image/*" | |
| style={{ display: "none" }} | |
| onChange={(e) => { | |
| if (e.target.files?.length) handleFiles(e.target.files); | |
| }} | |
| /> | |
| {uploading ? ( | |
| <> | |
| <div className="image-upload-card-icon" style={{ opacity: 0.5 }}> | |
| <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"> | |
| <rect x="3" y="3" width="18" height="18" rx="2" ry="2" /> | |
| <circle cx="8.5" cy="8.5" r="1.5" /> | |
| <polyline points="21 15 16 10 5 21" /> | |
| </svg> | |
| </div> | |
| <p className="image-upload-card-hint">Uploading...</p> | |
| </> | |
| ) : !urlMode ? ( | |
| <> | |
| <div className="image-upload-card-icon"> | |
| <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"> | |
| <rect x="3" y="3" width="18" height="18" rx="2" ry="2" /> | |
| <circle cx="8.5" cy="8.5" r="1.5" /> | |
| <polyline points="21 15 16 10 5 21" /> | |
| </svg> | |
| </div> | |
| <div className="image-upload-card-actions"> | |
| <button | |
| className="image-upload-btn primary" | |
| onClick={() => fileRef.current?.click()} | |
| > | |
| Upload image | |
| </button> | |
| <button | |
| className="image-upload-btn secondary" | |
| onClick={() => setUrlMode(true)} | |
| > | |
| Embed link | |
| </button> | |
| </div> | |
| <p className="image-upload-card-hint"> | |
| Drag & drop an image, or click to upload | |
| </p> | |
| </> | |
| ) : ( | |
| <div className="image-upload-card-url"> | |
| <input | |
| type="text" | |
| className="image-upload-url-input" | |
| placeholder="Paste image URL..." | |
| value={urlValue} | |
| onChange={(e) => setUrlValue(e.target.value)} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter") submitUrl(); | |
| if (e.key === "Escape") setUrlMode(false); | |
| }} | |
| autoFocus | |
| /> | |
| <div className="image-upload-card-actions"> | |
| <button className="image-upload-btn primary" onClick={submitUrl}> | |
| Embed | |
| </button> | |
| <button | |
| className="image-upload-btn secondary" | |
| onClick={() => setUrlMode(false)} | |
| > | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </NodeViewWrapper> | |
| ); | |
| } | |