Responsive drag and drop of files using FullScreenDropZone Component
This commit is contained in:
@@ -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;
|
||||
@@ -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([]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user