Responsive drag and drop of files using FullScreenDropZone Component

This commit is contained in:
david_bai
2025-07-02 23:53:22 +08:00
parent c9227baaf1
commit 6e90a78a4b
4 changed files with 147 additions and 80 deletions
+78 -1
View File
@@ -1,5 +1,5 @@
"use client";
import React, { useState, useRef, useCallback } from "react";
import React, { useState, useRef, useCallback, useEffect } from "react";
import { Button } from "@/components/ui/button";
import useRichTextToPlainText from "../hooks/useRichTextToPlainText";
import QRCodeComponent from "./ClipboardApp/ShareCard";
@@ -11,6 +11,8 @@ import { useWebRTCConnection } from "@/hooks/useWebRTCConnection";
import { useFileTransferHandler } from "@/hooks/useFileTransferHandler";
import { SendTabPanel } from "./ClipboardApp/SendTabPanel";
import { RetrieveTabPanel } from "./ClipboardApp/RetrieveTabPanel";
import FullScreenDropZone from "./ClipboardApp/FullScreenDropZone";
import { traverseFileTree } from "@/lib/fileUtils";
const ClipboardApp = () => {
const { shareMessage, retrieveMessage, putMessageInMs } =
@@ -18,6 +20,8 @@ const ClipboardApp = () => {
const [retrieveRoomIdInput, setRetrieveRoomIdInput] = useState("");
const [activeTab, setActiveTab] = useState<"send" | "retrieve">("send");
const [isDragging, setIsDragging] = useState(false);
const dragCounter = useRef(0);
const retrieveJoinRoomBtnRef = useRef<HTMLButtonElement>(null); // Ref for the receiver's "Join Room" button
const { messages, isLoadingMessages } = usePageSetup({
@@ -89,6 +93,78 @@ const ClipboardApp = () => {
broadcastDataToAllPeers(shareContent, sendFiles),
});
const handleFileDrop = useCallback(
(items: DataTransferItemList) => {
if (activeTab !== "send") return;
const itemsArray = Array.from(items);
Promise.all(
itemsArray.map((item) => {
const entry = item.webkitGetAsEntry();
if (entry) {
return traverseFileTree(entry);
}
return Promise.resolve([]);
})
).then((results) => {
const allFiles = results.flat();
if (allFiles.length > 0) {
addFilesToSend(allFiles);
}
});
},
[activeTab, addFilesToSend]
);
useEffect(() => {
const handleDragEnter = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (activeTab !== "send") return;
dragCounter.current++;
if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
setIsDragging(true);
}
};
const handleDragLeave = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (activeTab !== "send") return;
dragCounter.current--;
if (dragCounter.current === 0) {
setIsDragging(false);
}
};
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (activeTab !== "send") return;
if (e.dataTransfer?.items) {
handleFileDrop(e.dataTransfer.items);
}
dragCounter.current = 0;
setIsDragging(false);
};
window.addEventListener("dragenter", handleDragEnter);
window.addEventListener("dragleave", handleDragLeave);
window.addEventListener("dragover", handleDragOver);
window.addEventListener("drop", handleDrop);
return () => {
window.removeEventListener("dragenter", handleDragEnter);
window.removeEventListener("dragleave", handleDragLeave);
window.removeEventListener("dragover", handleDragOver);
window.removeEventListener("drop", handleDrop);
};
}, [activeTab, handleFileDrop]);
if (isLoadingMessages || !messages) {
// Use a skeleton screen placeholder to replace the simple text loading prompt.
// The height of this placeholder is similar to the height of the component that is finally loaded,
@@ -105,6 +181,7 @@ const ClipboardApp = () => {
return (
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
<FullScreenDropZone isDragging={isDragging} messages={messages} />
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-4">
<Button
variant={activeTab === "send" ? "default" : "outline"}
@@ -28,49 +28,6 @@ import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
import { en } from "@/constants/messages/en"; // Import English dictionary as default
const traverseFileTree = async (
item: FileSystemEntry,
path = ""
): Promise<CustomFile[]> => {
return new Promise((resolve) => {
// console.log('path',path)//path in ['','test/','test/sub/']
if (item.isFile) {
(item as FileSystemFileEntry).file((file: File) => {
// console.log('file.name',file.name)//file.name in ['Gmail-773240713232313363.txt','link.txt','cvat-serverless部署踩坑及部署模型测试 (1).docx','images.jpg']
// console.log('fullName',path + file.name,'folderName',path.split('/')[0])
const customFile: CustomFile = Object.assign(file, {
fullName: path + file.name,
folderName: path.split("/")[0],
});
resolve([customFile]);
});
} else if (item.isDirectory) {
const dirReader = (item as FileSystemDirectoryEntry).createReader();
let entries: FileSystemEntry[] = [];
const readEntries = () => {
dirReader.readEntries(async (results) => {
if (results.length) {
entries = entries.concat(Array.from(results));
readEntries();
} else {
const newPath = path + item.name + "/";
const subResults = await Promise.all(
entries.map((entry) => traverseFileTree(entry, newPath))
);
// console.log('subResults',subResults)
const files: CustomFile[] = subResults.flat();
// console.log('files',files)
resolve(files); // Removed conditional judgment, directly return processed files
}
});
};
readEntries();
}
});
};
function formatFileChosen(
template: string,
fileNum: number,
@@ -91,7 +48,6 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
const locale = useLocale();
const [messages, setMessages] = useState<Messages>(en); // Use English dictionary as initial value
const dropZoneRef = useRef<HTMLDivElement>(null); // Drag and drop files to attachments -- support
const folderInputRef = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// File selector -- message prompt
@@ -135,42 +91,13 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
if (folderInputRef.current) {
folderInputRef.current.value = "";
}
},
[messages, onFilePicked]
);
// Drag and drop folder upload response processing
const handleDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const items = e.dataTransfer.items;
if (items) {
const itemsArray = Array.from(items);
Promise.all(
itemsArray.map((item) => {
const entry = item.webkitGetAsEntry();
if (entry) {
return traverseFileTree(entry);
}
return Promise.resolve([]);
})
).then((results) => {
const allFiles = results.flat();
handleFileChange(allFiles);
});
}
},
[handleFileChange]
);
/* Define a callback function handleDragOver to handle the drag-over event.
In handleDragOver, prevent default behavior and event propagation to ensure custom handling.
There is no dependency array, which means the handleDragOver function will only be created once when the component first renders, and will not be re-created in subsequent renders.
*/
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
}, []);
// Click to upload file processing
const handleFileInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
@@ -233,9 +160,6 @@ const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
return (
<>
<div
ref={dropZoneRef}
onDrop={handleDrop}
onDragOver={handleDragOver}
className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer"
onClick={handleZoneClick}
>
@@ -0,0 +1,26 @@
import React from "react";
import { Upload } from "lucide-react";
import type { Messages } from "@/types/messages";
interface FullScreenDropZoneProps {
isDragging: boolean;
messages: Messages;
}
const FullScreenDropZone: React.FC<FullScreenDropZoneProps> = ({
isDragging,
messages,
}) => {
if (!isDragging) return null;
return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-black bg-opacity-70 backdrop-blur-sm">
<Upload className="h-24 w-24 text-white animate-bounce" />
<p className="mt-6 text-2xl font-bold text-white">
{messages.text.fileUploadHandler.Drag_tips}
</p>
</div>
);
};
export default FullScreenDropZone;
+40
View File
@@ -35,3 +35,43 @@ export const downloadAs = async (
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
export const traverseFileTree = async (
item: FileSystemEntry,
path = ""
): Promise<CustomFile[]> => {
return new Promise((resolve) => {
if (item.isFile) {
(item as FileSystemFileEntry).file((file: File) => {
const customFile: CustomFile = Object.assign(file, {
fullName: path + file.name,
folderName: path.split("/")[0],
});
resolve([customFile]);
});
} else if (item.isDirectory) {
const dirReader = (item as FileSystemDirectoryEntry).createReader();
let entries: FileSystemEntry[] = [];
const readEntries = () => {
dirReader.readEntries(async (results) => {
if (results.length) {
entries = entries.concat(Array.from(results));
readEntries();
} else {
const newPath = path + item.name + "/";
const subResults = await Promise.all(
entries.map((entry) => traverseFileTree(entry, newPath))
);
const files: CustomFile[] = subResults.flat();
resolve(files);
}
});
};
readEntries();
} else {
resolve([]);
}
});
};