From 9dbb453e4a64953e9ee1990a6c29dfe1c84a1504 Mon Sep 17 00:00:00 2001 From: indigane Date: Fri, 30 Aug 2024 11:10:40 +0300 Subject: [PATCH] Add drag and drop support for document form --- .../src/common/components/FileDropZone.tsx | 102 ++++++++++++++++++ .../components/GenericDocumentsPage.tsx | 55 +++++++--- frontend/src/common/components/index.ts | 1 + .../src/styles/components/_Documents.sass | 1 + .../src/styles/components/_FileDropZone.sass | 32 ++++++ frontend/src/styles/components/index.sass | 1 + frontend/tsconfig.json | 2 +- 7 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 frontend/src/common/components/FileDropZone.tsx create mode 100644 frontend/src/styles/components/_FileDropZone.sass diff --git a/frontend/src/common/components/FileDropZone.tsx b/frontend/src/common/components/FileDropZone.tsx new file mode 100644 index 000000000..ae045942a --- /dev/null +++ b/frontend/src/common/components/FileDropZone.tsx @@ -0,0 +1,102 @@ +import React, {useEffect} from "react"; + +interface FileDropZoneProps { + onDraggingChange?: (isDragging: boolean) => void; + onFileDrop: (files: FileList) => void; + helpText?: string; +} + +export default function FileDropZone({ + onDraggingChange = () => {}, + onFileDrop, + helpText, +}: FileDropZoneProps): React.JSX.Element { + useEffect(() => { + let isDragging = document.documentElement.classList.contains("is-dragging"); + let dragEndTimeout; + + const startDrag = () => { + if (!isDragging) { + isDragging = true; + document.documentElement.classList.add("is-dragging"); + onDraggingChange(true); + // Failsafe in case the dragleave is somehow missed and the user is no longer dragging + window.addEventListener("mousemove", handleMouseMove); + } + }; + + const endDrag = () => { + if (isDragging) { + isDragging = false; + document.documentElement.classList.remove("is-dragging"); + onDraggingChange(false); + document + .querySelector(".file-drop-zone.is-dragging-over-drop-zone") + ?.classList.remove("is-dragging-over-drop-zone"); + window.removeEventListener("mousemove", handleMouseMove); + } + }; + + const handleDragEnter = (event) => { + event.preventDefault(); + dragEndTimeout = clearTimeout(dragEndTimeout); + startDrag(); + if (event.target.classList.contains("file-drop-zone")) { + event.target.classList.add("is-dragging-over-drop-zone"); + } + }; + + const handleDragOver = (event) => { + event.preventDefault(); + dragEndTimeout = clearTimeout(dragEndTimeout); + }; + + const handleDragLeave = (event) => { + event.preventDefault(); + dragEndTimeout = setTimeout(endDrag, 200); + if (event.target.classList.contains("file-drop-zone")) { + event.target.classList.remove("is-dragging-over-drop-zone"); + } + }; + + const handleDrop = (event) => { + event.preventDefault(); + endDrag(); + if ( + event.target.classList.contains("file-drop-zone") && + event.dataTransfer.files && + event.dataTransfer.files.length > 0 + ) { + onFileDrop(event.dataTransfer.files); + } + }; + + const handleMouseMove = (event) => { + // Failsafe in case the dragleave is somehow missed and the user is no longer dragging + if (event.buttons === 0) { + endDrag(); + } + }; + + window.addEventListener("dragenter", handleDragEnter); + window.addEventListener("dragover", handleDragOver); + window.addEventListener("dragleave", handleDragLeave); + window.addEventListener("drop", handleDrop); + + return () => { + window.removeEventListener("dragenter", handleDragEnter); + window.removeEventListener("dragover", handleDragOver); + window.removeEventListener("dragleave", handleDragLeave); + window.removeEventListener("drop", handleDrop); + window.removeEventListener("mousemove", handleMouseMove); + }; + }, [onDraggingChange]); + + return ( + <> +
+
{helpText ?? "Pudota tiedostot tähän."}
+
+ + ); +} diff --git a/frontend/src/common/components/GenericDocumentsPage.tsx b/frontend/src/common/components/GenericDocumentsPage.tsx index 93c7074b0..4c650fee5 100644 --- a/frontend/src/common/components/GenericDocumentsPage.tsx +++ b/frontend/src/common/components/GenericDocumentsPage.tsx @@ -3,7 +3,7 @@ import {useCallback, useRef, useState} from "react"; import {SubmitHandler, useFieldArray, useForm, useFormContext} from "react-hook-form"; import {IApartmentDetails, IHousingCompanyDetails} from "../schemas"; import {FileInput, FormProviderForm, TextInput} from "./forms"; -import {ConfirmDialogModal, SaveButton} from "./index"; +import {ConfirmDialogModal, FileDropZone, SaveButton} from "./index"; import {zodResolver} from "@hookform/resolvers/zod"; import {useNavigate} from "react-router-dom"; @@ -135,6 +135,7 @@ const DocumentsListItems = ({name, remove}) => { formObject.setValue(`${name}.${index}.file_object`, filesArray[0], {shouldDirty: true}); }} required={!document.file_link} + defaultValue={document.file_object ? [document.file_object] : []} /> {!document.file_object && document.file_link && ( { ); }; -const DocumentFieldSets = ({apartment}) => { - return ( -
- -
- ); -}; - type IGenericDocumentsPage = { housingCompany: IHousingCompanyDetails; apartment?: IApartmentDetails; @@ -265,6 +255,16 @@ const GenericDocumentsPage = ({housingCompany, apartment}: IGenericDocumentsPage mode: "all", }); + const { + fields: documentFields, + append: appendDocument, + remove: removeDocument, + } = useFieldArray({ + name: "documents", + control: formObject.control, + }); + formObject.register("documents"); + // API Handling const [saveDocument, {isSaveLoading}] = saveDocumentHook(); const [deleteDocument, {isDeleteLoading}] = deleteDocumentHook(); @@ -335,8 +335,35 @@ const GenericDocumentsPage = ({housingCompany, apartment}: IGenericDocumentsPage ) : ( "" )} - - +
+
+
    + {documentFields.length ? ( + + ) : ( +
    Ei dokumentteja
    + )} +
  • + +
  • +
+
+ { + for (const file of files) { + appendDocument({ + ...emptyDocument, + key: uuidv4(), + file_object: file, + }); + } + }} + helpText="Pudota tiedostot tähän lisätäksesi ne dokumenteiksi." + /> +