Files
pdf-tools/src/components/ReorderPanel.tsx
2025-11-26 18:40:03 +01:00

322 lines
9.4 KiB
TypeScript

import React, { useEffect, useState } from 'react';
interface ReorderPanelProps {
pageCount: number;
thumbnails: string[] | null;
isBusy: boolean;
hasPdf: boolean;
rotations: Record<number, number>;
onRotate: (pageIndex: number) => void;
onExportReordered: (order: number[]) => void;
reorderDownloadUrl: string | null;
reorderFilename: string | null;
}
const ReorderPanel: React.FC<ReorderPanelProps> = ({
pageCount,
thumbnails,
isBusy,
hasPdf,
rotations,
onRotate,
onExportReordered,
reorderDownloadUrl,
reorderFilename,
}) => {
const [order, setOrder] = useState<number[]>([]);
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
const [dropIndex, setDropIndex] = useState<number | null>(null); // slot 0..order.length
useEffect(() => {
if (pageCount > 0) {
setOrder(Array.from({ length: pageCount }, (_, i) => i));
} else {
setOrder([]);
setDraggingIndex(null);
setDropIndex(null);
}
}, [pageCount]);
const handleDragStart = (index: number) => (e: React.DragEvent) => {
setDraggingIndex(index);
setDropIndex(index); // initial assumption: before itself
e.dataTransfer.effectAllowed = 'move';
// Firefox needs some data
e.dataTransfer.setData('text/plain', String(index));
};
const handleDragEnd = () => {
setDraggingIndex(null);
setDropIndex(null);
};
const handleCardDragOver = (cardIndex: number) => (e: React.DragEvent) => {
if (draggingIndex == null) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
const x = e.clientX - rect.left;
// left half => slot BEFORE this card
// right half => slot AFTER this card
const slot = x < rect.width / 2 ? cardIndex : cardIndex + 1;
setDropIndex(slot);
};
const handleEndSlotDragOver = (e: React.DragEvent) => {
if (draggingIndex == null) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setDropIndex(order.length); // slot at the very end
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
if (draggingIndex == null || dropIndex == null) return;
setOrder((prev) => {
const updated = [...prev];
const [moved] = updated.splice(draggingIndex, 1);
const adjustedSlot = dropIndex > draggingIndex ? dropIndex - 1 : dropIndex;
updated.splice(adjustedSlot, 0, moved);
return updated;
});
setDraggingIndex(null);
setDropIndex(null);
};
const handleDelete = (visualIndex: number) => () => {
setOrder((prev) => prev.filter((_, idx) => idx !== visualIndex));
setDraggingIndex(null);
setDropIndex(null);
};
const handleRotateClick = (pageIndex: number) => () => {
onRotate(pageIndex);
};
const handleExport = () => {
if (!hasPdf || order.length === 0) return;
onExportReordered(order);
};
if (!hasPdf) {
return (
<div className="card">
<h2>Reorder pages</h2>
<p>Load a PDF first to reorder, delete, or rotate its pages.</p>
</div>
);
}
const showLeftLine = (cardIndex: number) =>
dropIndex !== null && dropIndex === cardIndex && draggingIndex !== null;
const showRightLine = (cardIndex: number) =>
dropIndex !== null && dropIndex === cardIndex + 1 && draggingIndex !== null;
const showEndLine = () =>
dropIndex !== null && dropIndex === order.length && draggingIndex !== null;
return (
<div className="card">
<h2>Reorder / delete / rotate</h2>
<p>
Drag pages to reorder them. A vertical blue line shows where the page
will be inserted. Use rotate and delete controls below each thumbnail.
</p>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
alignItems: 'flex-start',
marginBottom: '0.75rem',
}}
onDrop={handleDrop}
>
{order.map((pageIndex, visualIndex) => {
const thumb = thumbnails?.[pageIndex];
const isDragging = visualIndex === draggingIndex;
const rotation = rotations[pageIndex] ?? 0;
return (
<div
key={`${pageIndex}-${visualIndex}`}
draggable
onDragStart={handleDragStart(visualIndex)}
onDragEnd={handleDragEnd}
onDragOver={handleCardDragOver(visualIndex)}
style={{
position: 'relative',
width: '130px',
padding: '0.4rem',
borderRadius: '0.5rem',
border: isDragging ? '2px solid #2563eb' : '1px solid #e5e7eb',
background: isDragging ? '#dbeafe' : '#f9fafb',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.25rem',
cursor: 'grab',
}}
>
{/* left drop indicator */}
{showLeftLine(visualIndex) && (
<div
style={{
position: 'absolute',
left: '-4px',
top: '4px',
bottom: '4px',
width: '3px',
borderRadius: '999px',
background: '#2563eb',
}}
/>
)}
{/* right drop indicator */}
{showRightLine(visualIndex) && (
<div
style={{
position: 'absolute',
right: '-4px',
top: '4px',
bottom: '4px',
width: '3px',
borderRadius: '999px',
background: '#2563eb',
}}
/>
)}
{thumb ? (
<img
src={thumb}
alt={`Page ${pageIndex + 1}`}
style={{
maxHeight: '90px',
width: 'auto',
borderRadius: '0.25rem',
border: '1px solid #e5e7eb',
background: 'white',
}}
/>
) : (
<div
style={{
width: '60px',
height: '80px',
borderRadius: '0.25rem',
border: '1px dashed #d1d5db',
background: '#f3f4f6',
}}
/>
)}
<span style={{ fontSize: '0.8rem' }}>Page {pageIndex + 1}</span>
<span style={{ fontSize: '0.7rem', color: '#6b7280' }}>
Pos {visualIndex + 1} · Rot {rotation}°
</span>
<div
style={{
display: 'flex',
gap: '0.25rem',
marginTop: '0.25rem',
}}
>
<button
type="button"
onClick={handleRotateClick(pageIndex)}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.15rem 0.4rem',
fontSize: '0.75rem',
background: '#e5e7eb',
cursor: 'pointer',
}}
>
90°
</button>
<button
type="button"
onClick={handleDelete(visualIndex)}
style={{
border: 'none',
borderRadius: '999px',
padding: '0.15rem 0.4rem',
fontSize: '0.75rem',
background: '#fecaca',
color: '#b91c1c',
cursor: 'pointer',
}}
title="Remove this page from the exported PDF"
>
</button>
</div>
</div>
);
})}
{/* invisible end slot to allow dropping after the last card */}
<div
onDragOver={handleEndSlotDragOver}
onDrop={handleDrop}
style={{
width: '20px',
height: '120px',
position: 'relative',
alignSelf: 'stretch',
}}
>
{showEndLine() && (
<div
style={{
position: 'absolute',
left: '8px',
top: '4px',
bottom: '4px',
width: '3px',
borderRadius: '999px',
background: '#2563eb',
}}
/>
)}
</div>
</div>
<div className="button-row">
<button
className="primary"
disabled={!hasPdf || isBusy || order.length === 0}
onClick={handleExport}
>
{isBusy ? 'Exporting…' : 'Export reordered PDF'}
</button>
</div>
{reorderDownloadUrl && reorderFilename && (
<div style={{ marginTop: '0.5rem', fontSize: '0.9rem' }}>
<strong>Reordered result:</strong>{' '}
<a
href={reorderDownloadUrl}
download={reorderFilename}
className="download-link"
>
Download {reorderFilename}
</a>
</div>
)}
</div>
);
};
export default ReorderPanel;