import { useCallback, useEffect, useRef } from 'react'; const useDocumentDragHandlers = ({ selectedEntries, selectedDocumentIds, selectedFolderIds, applySelection, handleEntrySelection, documentLookup, setDraggedDocumentIds, setDraggedFolderId, resolveDocumentRowKey, resolveFolderRowKey, documentsViewMode, }) => { const dragPreviewRef = useRef(null); const destroyDragPreview = useCallback(() => { const node = dragPreviewRef.current; if (node && node.parentNode) { node.parentNode.removeChild(node); } dragPreviewRef.current = null; }, []); useEffect(() => destroyDragPreview, [destroyDragPreview]); const createDragPreview = useCallback( ({ documents = [], folders = [] } = {}) => { destroyDragPreview(); if (typeof document === 'undefined') { return null; } const docEntries = (documents || []).filter(Boolean); const folderEntries = (folders || []).filter(Boolean); const totalCount = docEntries.length + folderEntries.length; if (!totalCount) { return null; } const maxVisible = 4; const size = 64; const canvasSize = Math.round(size * 1.6); const visibleItems = []; docEntries.slice(0, maxVisible).forEach((doc) => { visibleItems.push({ type: 'document', payload: doc }); }); if (visibleItems.length < maxVisible) { folderEntries .slice(0, maxVisible - visibleItems.length) .forEach((folderId) => visibleItems.push({ type: 'folder', payload: folderId })); } const wrapper = document.createElement('div'); wrapper.className = 'document-drag-preview'; wrapper.style.setProperty('--drag-preview-size', `${canvasSize}px`); wrapper.style.width = `${canvasSize}px`; wrapper.style.height = `${canvasSize}px`; visibleItems.forEach((item, index) => { const layer = document.createElement('div'); layer.className = 'document-drag-preview__item'; layer.style.setProperty('--index', String(index)); const rotationMagnitude = Math.random() * 8 + 2; const rotation = (index % 2 === 0 ? 1 : -1) * rotationMagnitude; layer.style.setProperty('--rotation-deg', `${rotation}deg`); if (item.type === 'document') { const doc = item.payload; const rowEl = doc?.id ? document.getElementById(`document-row-${doc.id}`) || document.getElementById(`document-card-${doc.id}`) : null; const wrapperEl = rowEl?.querySelector('.document-thumbnail-wrapper'); const thumbnailEl = rowEl?.querySelector('.document-thumbnail'); const placeholderEl = rowEl?.querySelector('.thumb-placeholder'); const aspectAttr = wrapperEl?.dataset?.thumbnailAspect; const aspectRatio = aspectAttr ? parseFloat(aspectAttr) : null; let thumbWidth = size; let thumbHeight = size; if (Number.isFinite(aspectRatio) && aspectRatio > 0) { if (aspectRatio >= 1) { thumbWidth = size; thumbHeight = Math.max(size / aspectRatio, size * 0.5); } else { thumbHeight = size; thumbWidth = Math.max(size * aspectRatio, size * 0.5); } } layer.style.width = `${Math.round(thumbWidth)}px`; layer.style.height = `${Math.round(thumbHeight)}px`; const thumbSrc = thumbnailEl?.currentSrc || thumbnailEl?.src || null; if (thumbSrc) { layer.classList.add('document-drag-preview__item--image'); layer.style.backgroundImage = `url("${thumbSrc}")`; } else if (placeholderEl instanceof HTMLElement) { const clone = placeholderEl.cloneNode(true); clone.style.pointerEvents = 'none'; layer.appendChild(clone); } else { layer.textContent = doc?.title || 'Document'; } } else { const payload = item.payload; const folderId = typeof payload === 'string' ? payload : payload?.id; const rowEl = folderId ? document.getElementById(`folder-row-${folderId}`) || document.getElementById(`folder-card-${folderId}`) : null; const iconEl = rowEl?.querySelector('.thumb-icon, .folder-card__icon'); layer.style.width = `${size}px`; layer.style.height = `${size}px`; layer.classList.add('document-drag-preview__item--folder'); let content = null; if (iconEl instanceof HTMLElement) { const cloneSource = iconEl.classList.contains('folder-card__icon') ? iconEl.querySelector('svg') || iconEl : iconEl; content = cloneSource.cloneNode(true); content.classList.add('document-drag-preview__folder-thumb'); const svg = content.querySelector('svg'); if (svg) { svg.setAttribute('width', '48'); svg.setAttribute('height', '48'); } } if (!content) { content = document.createElement('div'); content.className = 'document-drag-preview__folder-placeholder'; content.textContent = 'Folder'; } layer.appendChild(content); } wrapper.appendChild(layer); }); if (totalCount > 1) { const badge = document.createElement('div'); badge.className = 'document-drag-preview__count'; badge.textContent = `${totalCount}`; wrapper.appendChild(badge); } document.body.appendChild(wrapper); dragPreviewRef.current = wrapper; return wrapper; }, [destroyDragPreview], ); const handleDocumentDragStart = useCallback( (event, documentOrId) => { const documentId = typeof documentOrId === 'string' ? documentOrId : documentOrId?.id; if (!documentId) { return; } const documentKey = resolveDocumentRowKey(documentId); if (!documentKey) { return; } const isGridView = documentsViewMode === 'grid'; const isAlreadySelected = selectedDocumentIds.includes(documentId); const selection = isAlreadySelected ? [...selectedDocumentIds] : isGridView ? [...selectedDocumentIds, documentId] : [documentId]; const folderSelection = []; if (!isAlreadySelected && !isGridView) { applySelection([documentKey], { anchor: documentKey, interactedKeys: [documentKey], }); } const previewDocs = selection.map((id) => documentLookup.get(id) || null).filter(Boolean); const previewNode = createDragPreview({ documents: previewDocs, folders: folderSelection, }); setDraggedDocumentIds(selection); if (folderSelection.length) { setDraggedFolderId(folderSelection[0] || null); } event.dataTransfer.effectAllowed = 'move'; try { event.dataTransfer.setData( 'application/x-papercrate-doc-list', JSON.stringify(selection), ); if (folderSelection.length) { event.dataTransfer.setData( 'application/x-papercrate-folder-list', JSON.stringify(folderSelection), ); if (folderSelection.length === 1) { event.dataTransfer.setData('application/x-papercrate-folder', folderSelection[0]); } } } catch (error) { console.warn('[documents] Failed to populate drag payload', error); } if (previewNode) { const width = previewNode.offsetWidth || 96; const height = previewNode.offsetHeight || 96; event.dataTransfer.setDragImage(previewNode, width / 2, height / 2); } event.currentTarget.classList.add('dragging'); }, [ selectedDocumentIds, applySelection, documentLookup, createDragPreview, setDraggedFolderId, setDraggedDocumentIds, documentsViewMode, resolveDocumentRowKey, ], ); const handleDocumentDragEnd = useCallback( (event) => { setDraggedDocumentIds([]); event.currentTarget.classList.remove('dragging'); destroyDragPreview(); setDraggedFolderId(null); }, [destroyDragPreview, setDraggedFolderId, setDraggedDocumentIds], ); const handleFolderDragStart = useCallback( (event, folderId) => { if (folderId === 'root') { return; } event.stopPropagation(); const folderKey = resolveFolderRowKey(folderId); const isAlreadySelected = folderKey ? selectedEntries.includes(folderKey) : false; let effectiveFolderSelection = selectedFolderIds; let effectiveDocumentSelection = selectedDocumentIds; if (!isAlreadySelected && folderKey) { effectiveFolderSelection = [folderId]; effectiveDocumentSelection = []; handleEntrySelection(folderKey, { preventDefault: () => {} }); } const uniqueFolders = effectiveFolderSelection.length ? Array.from(new Set(effectiveFolderSelection.filter(Boolean))) : [folderId]; setDraggedFolderId(folderId); if (effectiveDocumentSelection.length) { setDraggedDocumentIds(effectiveDocumentSelection); } event.dataTransfer.effectAllowed = 'move'; try { event.dataTransfer.setData( 'application/x-papercrate-folder-list', JSON.stringify(uniqueFolders), ); if (uniqueFolders.length === 1) { event.dataTransfer.setData('application/x-papercrate-folder', uniqueFolders[0]); } if (effectiveDocumentSelection.length) { event.dataTransfer.setData( 'application/x-papercrate-doc-list', JSON.stringify(effectiveDocumentSelection), ); } } catch (error) { console.warn('[documents] Failed to populate folder drag payload', error); } const previewNode = createDragPreview({ documents: effectiveDocumentSelection .map((id) => documentLookup.get(id) || null) .filter(Boolean), folders: uniqueFolders, }); event.currentTarget.classList.add('dragging'); if (previewNode) { const width = previewNode.offsetWidth || 96; const height = previewNode.offsetHeight || 96; event.dataTransfer.setDragImage(previewNode, width / 2, height / 2); } }, [ selectedFolderIds, selectedEntries, selectedDocumentIds, handleEntrySelection, setDraggedFolderId, setDraggedDocumentIds, documentLookup, createDragPreview, resolveFolderRowKey, ], ); const handleFolderDragEnd = useCallback( (event) => { if (event?.currentTarget) { event.currentTarget.classList.remove('dragging'); } setDraggedFolderId(null); setDraggedDocumentIds([]); destroyDragPreview(); }, [setDraggedFolderId, setDraggedDocumentIds, destroyDragPreview], ); return { handleDocumentDragStart, handleDocumentDragEnd, handleFolderDragStart, handleFolderDragEnd, }; }; export default useDocumentDragHandlers;