Files
pdf-tools/src/components/ReorderPanel.tsx

343 lines
9.8 KiB
TypeScript

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<string, string>;
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<HTMLButtonElement>
) => void;
onSelectAll: () => void;
onOpenPreview: (pageId: string) => void;
onClearSelection: () => void;
onDeleteSelected: () => void;
}
const ReorderPanel: React.FC<ReorderPanelProps> = ({
pages,
thumbnails,
isBusy,
hasPdf,
selectedPageIds,
onRotateClockwise,
onRotateCounterclockwise,
onDelete,
onReorder,
onCopyPagesToSlot,
onToggleSelect,
onSelectAll,
onOpenPreview,
onClearSelection,
onDeleteSelected,
}) => {
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
const [dropIndex, setDropIndex] = useState<number | null>(null);
const [isCopyDragging, setIsCopyDragging] = useState(false);
const [copyDialogOpen, setCopyDialogOpen] = useState(false);
const [copyTargetPosition, setCopyTargetPosition] = useState('');
const [copyDialogError, setCopyDialogError] = useState<string | null>(null);
const dragGhostRef = useRef<HTMLDivElement | null>(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<string, number>();
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<HTMLButtonElement>) => {
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 (
<div className="card">
<h2>Pages</h2>
<p>Open a PDF file to reorder, rotate, or delete its pages.</p>
</div>
);
}
const draggingPage = draggingIndex != null ? pages[draggingIndex] : null;
const draggingSelectionActive =
draggingPage != null &&
selectedPageIds.length > 0 &&
selectedPageIds.includes(draggingPage.id);
const dropIndicatorColor = isCopyDragging ? '#16a34a' : '#2563eb';
return (
<>
<div className="card">
<h2>Pages</h2>
<p style={{ fontSize: '0.85rem', color: '#6b7280' }}>
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.
</p>
<PageSelectionToolbar
selectedCount={selectedPageIds.length}
onCopySelected={handleCopySelectedClick}
onDeleteSelected={onDeleteSelected}
onSelectAll={onSelectAll}
onClearSelection={onClearSelection}
/>
<PageGrid
pages={pages}
thumbnails={thumbnails}
selectedPageIds={selectedPageIds}
isBusy={isBusy}
draggingIndex={draggingIndex}
dropIndex={dropIndex}
draggingSelectionActive={draggingSelectionActive}
isCopyDragging={isCopyDragging}
dropIndicatorColor={dropIndicatorColor}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onCardDragOver={handleCardDragOver}
onEndSlotDragOver={handleEndSlotDragOver}
onDrop={handleDrop}
onOpenPreview={onOpenPreview}
onToggleSelect={handleCheckboxClick}
onRotateClockwise={onRotateClockwise}
onRotateCounterclockwise={onRotateCounterclockwise}
onDelete={handleDeleteClick}
/>
</div>
{copyDialogOpen && (
<CopyPagesDialog
selectedCount={selectedPageIds.length}
pageCount={pages.length}
targetPosition={copyTargetPosition}
error={copyDialogError}
onTargetPositionChange={handleCopyTargetPositionChange}
onCancel={handleCopyDialogCancel}
onConfirm={handleCopyDialogConfirm}
/>
)}
</>
);
};
export default ReorderPanel;