import React, { useRef, useState } from 'react'; import type { PageRef } from '../pdf/pdfTypes'; import CopyPagesDialog from './PageWorkspace/CopyPagesDialog'; import PageGrid from './PageWorkspace/PageGrid'; import PageSelectionToolbar from './PageWorkspace/PageSelectionToolbar'; interface ReorderPanelProps { pages: PageRef[]; thumbnails: Record; isBusy: boolean; hasPdf: boolean; selectedPageIds: string[]; onRotateClockwise: (pageId: string) => void; onRotateCounterclockwise: (pageId: string) => void; onDelete: (pageId: string) => void; onReorder: (newPages: PageRef[]) => void; onCopyPagesToSlot: (pageIds: string[], insertSlot: number) => void; onToggleSelect: ( pageId: string, visualIndex: number, e: React.MouseEvent ) => void; onSelectAll: () => void; onOpenPreview: (pageId: string) => void; onClearSelection: () => void; onDeleteSelected: () => void; } const ReorderPanel: React.FC = ({ pages, thumbnails, isBusy, hasPdf, selectedPageIds, onRotateClockwise, onRotateCounterclockwise, onDelete, onReorder, onCopyPagesToSlot, onToggleSelect, onSelectAll, onOpenPreview, onClearSelection, onDeleteSelected, }) => { const [draggingIndex, setDraggingIndex] = useState(null); const [dropIndex, setDropIndex] = useState(null); const [isCopyDragging, setIsCopyDragging] = useState(false); const [copyDialogOpen, setCopyDialogOpen] = useState(false); const [copyTargetPosition, setCopyTargetPosition] = useState(''); const [copyDialogError, setCopyDialogError] = useState(null); const dragGhostRef = useRef(null); const cleanupDragGhost = () => { if (dragGhostRef.current && dragGhostRef.current.parentNode) { dragGhostRef.current.parentNode.removeChild(dragGhostRef.current); } dragGhostRef.current = null; }; const isCopyModifierPressed = (e: React.DragEvent) => { return e.ctrlKey || e.metaKey; }; const getDraggedPages = (visualIndex: number): PageRef[] => { const draggedPage = pages[visualIndex]; if (!draggedPage) return []; const selectedInVisualOrder = pages.filter((page) => selectedPageIds.includes(page.id) ); const draggingIsSelected = selectedInVisualOrder.length > 0 && selectedInVisualOrder.some((page) => page.id === draggedPage.id); return draggingIsSelected ? selectedInVisualOrder : [draggedPage]; }; const createDragGhost = (e: React.DragEvent, count: number) => { cleanupDragGhost(); const ghost = document.createElement('div'); ghost.textContent = count === 1 ? '1 page' : `${count} pages`; ghost.style.position = 'fixed'; ghost.style.top = '0'; ghost.style.left = '0'; ghost.style.padding = '4px 8px'; ghost.style.borderRadius = '999px'; ghost.style.background = '#111827'; ghost.style.color = '#e5e7eb'; ghost.style.fontSize = '12px'; ghost.style.fontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; ghost.style.zIndex = '9999'; document.body.appendChild(ghost); dragGhostRef.current = ghost; const rect = ghost.getBoundingClientRect(); e.dataTransfer.setDragImage(ghost, rect.width / 2, rect.height / 2); }; const resetDragState = () => { cleanupDragGhost(); setDraggingIndex(null); setDropIndex(null); setIsCopyDragging(false); }; const handleDragStart = (visualIndex: number) => (e: React.DragEvent) => { setDraggingIndex(visualIndex); setDropIndex(visualIndex); const copying = isCopyModifierPressed(e); setIsCopyDragging(copying); e.dataTransfer.effectAllowed = 'copyMove'; e.dataTransfer.dropEffect = copying ? 'copy' : 'move'; e.dataTransfer.setData('text/plain', String(visualIndex)); const draggedPages = getDraggedPages(visualIndex); createDragGhost(e, draggedPages.length); }; const handleDragEnd = () => { resetDragState(); }; const handleCardDragOver = (visualIndex: number) => (e: React.DragEvent) => { if (draggingIndex == null) return; e.preventDefault(); const copying = isCopyModifierPressed(e); setIsCopyDragging(copying); e.dataTransfer.dropEffect = copying ? 'copy' : 'move'; const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect(); const x = e.clientX - rect.left; const slot = x < rect.width / 2 ? visualIndex : visualIndex + 1; setDropIndex(slot); }; const handleEndSlotDragOver = (e: React.DragEvent) => { if (draggingIndex == null) return; e.preventDefault(); const copying = isCopyModifierPressed(e); setIsCopyDragging(copying); e.dataTransfer.dropEffect = copying ? 'copy' : 'move'; setDropIndex(pages.length); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); cleanupDragGhost(); if (draggingIndex == null || dropIndex == null) return; const draggedPages = getDraggedPages(draggingIndex); if (draggedPages.length === 0) return; const shouldCopy = isCopyModifierPressed(e) || isCopyDragging; if (shouldCopy) { onCopyPagesToSlot( draggedPages.map((page) => page.id), dropIndex ); setDraggingIndex(null); setDropIndex(null); setIsCopyDragging(false); return; } const indexMap = new Map(); pages.forEach((page, idx) => indexMap.set(page.id, idx)); const countBefore = draggedPages.reduce((count, page) => { const idx = indexMap.get(page.id); if (idx != null && idx < dropIndex) return count + 1; return count; }, 0); const adjustedSlot = dropIndex - countBefore; const movingSet = new Set(draggedPages.map((page) => page.id)); const remaining = pages.filter((page) => !movingSet.has(page.id)); const newPages = [ ...remaining.slice(0, adjustedSlot), ...draggedPages, ...remaining.slice(adjustedSlot), ]; onReorder(newPages); setDraggingIndex(null); setDropIndex(null); setIsCopyDragging(false); }; const handleDeleteClick = (pageId: string) => { onDelete(pageId); setDraggingIndex(null); setDropIndex(null); }; const handleCheckboxClick = (pageId: string, visualIndex: number) => (e: React.MouseEvent) => { e.stopPropagation(); onToggleSelect(pageId, visualIndex, e); }; const handleCopySelectedClick = () => { if (selectedPageIds.length === 0) return; setCopyTargetPosition(String(pages.length + 1)); setCopyDialogError(null); setCopyDialogOpen(true); }; const handleCopyDialogCancel = () => { setCopyDialogOpen(false); setCopyDialogError(null); }; const handleCopyTargetPositionChange = (value: string) => { setCopyTargetPosition(value); setCopyDialogError(null); }; const handleCopyDialogConfirm = (e?: React.FormEvent) => { e?.preventDefault(); if (selectedPageIds.length === 0) { setCopyDialogError('No pages selected.'); return; } const maxPosition = pages.length + 1; const parsed = Number.parseInt(copyTargetPosition.trim(), 10); if (!Number.isFinite(parsed) || parsed < 1 || parsed > maxPosition) { setCopyDialogError(`Please enter a number between 1 and ${maxPosition}.`); return; } onCopyPagesToSlot(selectedPageIds, parsed - 1); setCopyDialogOpen(false); setCopyDialogError(null); }; if (!hasPdf) { return (

Pages

Open a PDF file to reorder, rotate, or delete its pages.

); } const draggingPage = draggingIndex != null ? pages[draggingIndex] : null; const draggingSelectionActive = draggingPage != null && selectedPageIds.length > 0 && selectedPageIds.includes(draggingPage.id); const dropIndicatorColor = isCopyDragging ? '#16a34a' : '#2563eb'; return ( <>

Pages

Tap/click a page to preview it. Use the checkbox to select pages (Shift for ranges). Drag to reorder; dragging a selected page moves the whole selection. Hold Ctrl/⌘ while dropping to copy instead of move. Shortcuts: Ctrl/⌘+A selects all, Delete removes selected pages, Esc clears selection.

{copyDialogOpen && ( )} ); }; export default ReorderPanel;