Fix drag and drop on collapsed folders

This commit is contained in:
Richard Roberson 2025-02-13 15:49:15 -07:00
parent a1f069c0ae
commit df8772de37
4 changed files with 49 additions and 52 deletions

View file

@ -1,11 +1,11 @@
import { Fragment } from 'react'; import { Fragment, KeyboardEvent } from 'react';
import { Dialog, DialogPanel, DialogTitle, Input, Transition, TransitionChild } from '@headlessui/react'; import { Dialog, DialogPanel, DialogTitle, Input, Transition, TransitionChild } from '@headlessui/react';
interface CreateFolderDialogProps { interface CreateFolderDialogProps {
isOpen: boolean; isOpen: boolean;
folderName: string; folderName: string;
onFolderNameChange: (name: string) => void; onFolderNameChange: (name: string) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void; onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
onClose: () => void; onClose: () => void;
} }

View file

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, DragEvent } from 'react';
import { Button } from '@headlessui/react'; import { Button } from '@headlessui/react';
import { DocumentListItem } from './DocumentListItem'; import { DocumentListItem } from './DocumentListItem';
import { Folder, DocumentListDocument } from '@/types/documents'; import { Folder, DocumentListDocument } from '@/types/documents';
@ -11,12 +11,9 @@ interface DocumentFolderProps {
sortedDocuments: DocumentListDocument[]; sortedDocuments: DocumentListDocument[];
onDocumentDelete: (doc: DocumentListDocument) => void; onDocumentDelete: (doc: DocumentListDocument) => void;
draggedDoc: DocumentListDocument | null; draggedDoc: DocumentListDocument | null;
dropTargetDoc: DocumentListDocument | null;
onDragStart: (doc: DocumentListDocument) => void; onDragStart: (doc: DocumentListDocument) => void;
onDragEnd: () => void; onDragEnd: () => void;
onDragOver: (e: React.DragEvent, doc: DocumentListDocument) => void; onDrop: (e: DragEvent, folderId: string) => void;
onDragLeave: () => void;
onDrop: (e: React.DragEvent, doc: DocumentListDocument) => void;
} }
const ChevronIcon = ({ className = "w-5 h-5" }) => ( const ChevronIcon = ({ className = "w-5 h-5" }) => (
@ -37,38 +34,33 @@ export function DocumentFolder({
sortedDocuments, sortedDocuments,
onDocumentDelete, onDocumentDelete,
draggedDoc, draggedDoc,
dropTargetDoc,
onDragStart, onDragStart,
onDragEnd, onDragEnd,
onDragOver,
onDragLeave,
onDrop, onDrop,
}: DocumentFolderProps) { }: DocumentFolderProps) {
const [isHovering, setIsHovering] = useState(false); const [isHovering, setIsHovering] = useState(false);
const isDropTarget = isHovering && draggedDoc && !draggedDoc.folderId; const isDropTarget = isHovering && draggedDoc && !draggedDoc.folderId && draggedDoc.id !== folder.id;
return ( return (
<div <div
onDragOver={(e) => { onDragOver={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsHovering(true);
if (draggedDoc && !draggedDoc.folderId) { if (draggedDoc && !draggedDoc.folderId) {
onDragOver(e, { ...draggedDoc, folderId: folder.id }); setIsHovering(true);
} }
}} }}
onDragLeave={(e) => { onDragLeave={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsHovering(false); setIsHovering(false);
onDragLeave();
}} }}
onDrop={(e) => { onDrop={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setIsHovering(false); setIsHovering(false);
if (!draggedDoc || draggedDoc.folderId) return; if (!draggedDoc || draggedDoc.folderId) return;
onDrop(e, { ...draggedDoc, folderId: folder.id }); onDrop(e, folder.id);
}} }}
className={`rounded-lg p-2 transition-all bg-offbase shadow hover:shadow-md ${ className={`rounded-lg p-2 transition-all bg-offbase shadow hover:shadow-md ${
isDropTarget ? 'ring-2 ring-accent bg-primary/10' : '' isDropTarget ? 'ring-2 ring-accent bg-primary/10' : ''
@ -105,13 +97,10 @@ export function DocumentFolder({
key={`${doc.type}-${doc.id}`} key={`${doc.type}-${doc.id}`}
doc={doc} doc={doc}
onDelete={onDocumentDelete} onDelete={onDocumentDelete}
dragEnabled={true} dragEnabled={false} // Documents in folders can't be dragged to other documents
onDragStart={onDragStart} onDragStart={onDragStart}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
onDragOver={onDragOver} isDropTarget={false}
onDragLeave={onDragLeave}
onDrop={onDrop}
isDropTarget={dropTargetDoc?.id === doc.id}
/> />
))} ))}
</div> </div>

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useState, useEffect } from 'react'; import { useCallback, useState, useEffect, DragEvent, KeyboardEvent } from 'react';
import { useDocuments } from '@/contexts/DocumentContext'; import { useDocuments } from '@/contexts/DocumentContext';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
@ -172,7 +172,7 @@ export function DocumentList() {
setDropTargetDoc(null); setDropTargetDoc(null);
}, []); }, []);
const handleDragOver = useCallback((e: React.DragEvent, doc: DocumentListDocument) => { const handleDragOver = useCallback((e: DragEvent, doc: DocumentListDocument) => {
e.preventDefault(); e.preventDefault();
if (draggedDoc && draggedDoc.id !== doc.id && !draggedDoc.folderId) { if (draggedDoc && draggedDoc.id !== doc.id && !draggedDoc.folderId) {
// Only highlight target if neither document is in a folder // Only highlight target if neither document is in a folder
@ -186,26 +186,14 @@ export function DocumentList() {
setDropTargetDoc(null); setDropTargetDoc(null);
}, []); }, []);
const handleDrop = useCallback((e: React.DragEvent, targetDoc: DocumentListDocument) => { const handleDrop = useCallback((e: DragEvent, targetDoc: DocumentListDocument) => {
e.preventDefault(); e.preventDefault();
console.log('Dropped', draggedDoc?.name, 'on', targetDoc.name);
if (!draggedDoc || draggedDoc.id === targetDoc.id || draggedDoc.folderId) return; if (!draggedDoc || draggedDoc.id === targetDoc.id || draggedDoc.folderId) return;
// If target has a folderId, we're dropping into an existing folder // If target doc is unfoldered, create a new folder
if (targetDoc.folderId) { if (!targetDoc.folderId) {
const targetFolder = folders.find(f => f.id === targetDoc.folderId);
if (targetFolder && !targetFolder.documents.some(d => d.id === draggedDoc.id)) {
setFolders(prev => prev.map(f => {
if (f.id === targetDoc.folderId) {
return {
...f,
documents: [...f.documents, { ...draggedDoc, folderId: f.id }]
};
}
return f;
}));
}
} else {
// Create new folder when dropping on an unfoldered document
setPendingFolderDocs({ setPendingFolderDocs({
source: draggedDoc, source: draggedDoc,
target: targetDoc target: targetDoc
@ -213,6 +201,25 @@ export function DocumentList() {
setNewFolderName(''); setNewFolderName('');
} }
setDraggedDoc(null);
setDropTargetDoc(null);
}, [draggedDoc]);
const handleFolderDrop = useCallback((e: DragEvent, folderId: string) => {
e.preventDefault();
if (!draggedDoc || draggedDoc.folderId) return;
// Add document to existing folder
setFolders(folders.map(f => {
if (f.id === folderId && !f.documents.some(d => d.id === draggedDoc.id)) {
return {
...f,
documents: [...f.documents, { ...draggedDoc, folderId }]
};
}
return f;
}));
setDraggedDoc(null); setDraggedDoc(null);
setDropTargetDoc(null); setDropTargetDoc(null);
}, [draggedDoc, folders]); }, [draggedDoc, folders]);
@ -255,7 +262,7 @@ export function DocumentList() {
setShowHint(false); setShowHint(false);
}, [pendingFolderDocs, newFolderName]); }, [pendingFolderDocs, newFolderName]);
const handleFolderNameKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => { const handleFolderNameKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
createFolder(); createFolder();
@ -316,16 +323,12 @@ export function DocumentList() {
sortedDocuments={sortDocuments(folder.documents)} sortedDocuments={sortDocuments(folder.documents)}
onDocumentDelete={setDocumentToDelete} onDocumentDelete={setDocumentToDelete}
draggedDoc={draggedDoc} draggedDoc={draggedDoc}
dropTargetDoc={dropTargetDoc}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onDragOver={handleDragOver} onDrop={handleFolderDrop}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
/> />
))} ))}
{sortDocuments(unfolderedDocuments).map(doc => ( {sortDocuments(unfolderedDocuments).map(doc => (
<DocumentListItem <DocumentListItem
key={`${doc.type}-${doc.id}`} key={`${doc.type}-${doc.id}`}

View file

@ -1,4 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import { DragEvent } from 'react';
import { Button } from '@headlessui/react'; import { Button } from '@headlessui/react';
import { PDFIcon, EPUBIcon } from '@/components/icons/Icons'; import { PDFIcon, EPUBIcon } from '@/components/icons/Icons';
import { DocumentListDocument } from '@/types/documents'; import { DocumentListDocument } from '@/types/documents';
@ -9,9 +10,9 @@ interface DocumentListItemProps {
dragEnabled?: boolean; dragEnabled?: boolean;
onDragStart?: (doc: DocumentListDocument) => void; onDragStart?: (doc: DocumentListDocument) => void;
onDragEnd?: () => void; onDragEnd?: () => void;
onDragOver?: (e: React.DragEvent, doc: DocumentListDocument) => void; onDragOver?: (e: DragEvent, doc: DocumentListDocument) => void;
onDragLeave?: () => void; onDragLeave?: () => void;
onDrop?: (e: React.DragEvent, doc: DocumentListDocument) => void; onDrop?: (e: DragEvent, doc: DocumentListDocument) => void;
isDropTarget?: boolean; isDropTarget?: boolean;
} }
@ -26,17 +27,21 @@ export function DocumentListItem({
onDrop, onDrop,
isDropTarget = false, isDropTarget = false,
}: DocumentListItemProps) { }: DocumentListItemProps) {
// Only allow drag and drop interactions for documents not in folders
const isDraggable = dragEnabled && !doc.folderId;
const allowDropTarget = !doc.folderId;
return ( return (
<div <div
draggable={dragEnabled && !doc.folderId} draggable={isDraggable}
onDragStart={() => onDragStart?.(doc)} onDragStart={() => onDragStart?.(doc)}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
onDragOver={(e) => onDragOver?.(e, doc)} onDragOver={(e) => allowDropTarget && onDragOver?.(e, doc)}
onDragLeave={onDragLeave} onDragLeave={() => allowDropTarget && onDragLeave?.()}
onDrop={(e) => onDrop?.(e, doc)} onDrop={(e) => allowDropTarget && onDrop?.(e, doc)}
className={` className={`
w-full w-full
${!doc.folderId && isDropTarget ? 'ring-2 ring-accent bg-primary/10' : ''} ${allowDropTarget && isDropTarget ? 'ring-2 ring-accent bg-primary/10' : ''}
bg-background rounded-lg p-2 shadow hover:shadow-md transition-shadow bg-background rounded-lg p-2 shadow hover:shadow-md transition-shadow
`} `}
> >