返回 JoyAI-Echo
useClipboardAndDrop.ts
根目录 / echo_longvideo / Director_Agent / webui / src / hooks / useClipboardAndDrop.ts
1 import { useCallback, useRef, useState } from "react";
2
3 /** Extract image ``File``s from a paste / drop event.
4 *
5 * Deliberate behaviour:
6 * - Only items whose ``kind === "file"`` and ``type`` starts with
7 * ``image/`` are returned; ``<img>`` tags inside HTML fragments are
8 * ignored (defending against remote URL fetch + XSS surfaces).
9 * - Plain text pasted alongside images is *not* consumed by this helper,
10 * so the caller can still let the textarea receive it naturally.
11 */
12 export function extractImageFilesFromPaste(
13 event: ClipboardEvent | React.ClipboardEvent,
14 ): File[] {
15 const clipboard = (event as ClipboardEvent).clipboardData
16 ?? (event as React.ClipboardEvent).clipboardData;
17 if (!clipboard) return [];
18 const files: File[] = [];
19 for (const item of Array.from(clipboard.items)) {
20 if (item.kind !== "file") continue;
21 if (!item.type.startsWith("image/")) continue;
22 const file = item.getAsFile();
23 if (file) files.push(file);
24 }
25 return files;
26 }
27
28 /** Extract dropped image files, mirroring ``extractImageFilesFromPaste``. */
29 export function extractImageFilesFromDrop(
30 event: DragEvent | React.DragEvent,
31 ): File[] {
32 const dt = (event as DragEvent).dataTransfer
33 ?? (event as React.DragEvent).dataTransfer;
34 if (!dt) return [];
35 const files: File[] = [];
36 for (const item of Array.from(dt.files)) {
37 if (item.type.startsWith("image/")) files.push(item);
38 }
39 return files;
40 }
41
42 export interface UseClipboardAndDropApi {
43 /** Whether a drag is currently hovering the drop zone (toggle dragover UI). */
44 isDragging: boolean;
45 onPaste: (
46 event: React.ClipboardEvent,
47 ) => void;
48 onDragEnter: (event: React.DragEvent) => void;
49 onDragOver: (event: React.DragEvent) => void;
50 onDragLeave: (event: React.DragEvent) => void;
51 onDrop: (event: React.DragEvent) => void;
52 }
53
54 /** Wire paste + drag-and-drop to a callback.
55 *
56 * The hook owns ``isDragging`` state and the refcount that keeps it accurate
57 * across nested ``dragenter`` / ``dragleave`` events (a known DOM gotcha: the
58 * text cursor inside a textarea fires ``dragleave`` on entry, flicking the
59 * highlight off otherwise). */
60 export function useClipboardAndDrop(
61 onImageFiles: (files: File[]) => void,
62 ): UseClipboardAndDropApi {
63 const [isDragging, setIsDragging] = useState(false);
64 const dragDepth = useRef(0);
65
66 const onPaste = useCallback(
67 (event: React.ClipboardEvent) => {
68 const files = extractImageFilesFromPaste(event);
69 if (files.length === 0) return;
70 // Consume only when an image is actually present; plain-text paste still
71 // reaches the textarea unmolested.
72 event.preventDefault();
73 onImageFiles(files);
74 },
75 [onImageFiles],
76 );
77
78 const onDragEnter = useCallback((event: React.DragEvent) => {
79 if (!Array.from(event.dataTransfer.types ?? []).includes("Files")) return;
80 event.preventDefault();
81 dragDepth.current += 1;
82 setIsDragging(true);
83 }, []);
84
85 const onDragOver = useCallback((event: React.DragEvent) => {
86 if (!Array.from(event.dataTransfer.types ?? []).includes("Files")) return;
87 event.preventDefault();
88 event.dataTransfer.dropEffect = "copy";
89 }, []);
90
91 const onDragLeave = useCallback((event: React.DragEvent) => {
92 if (!Array.from(event.dataTransfer.types ?? []).includes("Files")) return;
93 event.preventDefault();
94 dragDepth.current = Math.max(0, dragDepth.current - 1);
95 if (dragDepth.current === 0) setIsDragging(false);
96 }, []);
97
98 const onDrop = useCallback(
99 (event: React.DragEvent) => {
100 dragDepth.current = 0;
101 setIsDragging(false);
102 const files = extractImageFilesFromDrop(event);
103 if (files.length === 0) return;
104 event.preventDefault();
105 onImageFiles(files);
106 },
107 [onImageFiles],
108 );
109
110 return { isDragging, onPaste, onDragEnter, onDragOver, onDragLeave, onDrop };
111 }
112
112 lines TYPESCRIPT