| 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> |
| ); |
| } |
|
|