working split

This commit is contained in:
Reece Browne 2025-08-22 18:40:31 +01:00
parent abed82cc6b
commit d5e1a3eccb
5 changed files with 106 additions and 398 deletions

View File

@ -1,335 +0,0 @@
import { Command, CommandSequence } from '../hooks/useUndoRedo';
import { PDFDocument, PDFPage } from '../types/pageEditor';
// Base class for page operations
abstract class PageCommand implements Command {
protected pdfDocument: PDFDocument;
protected setPdfDocument: (doc: PDFDocument) => void;
protected previousState: PDFDocument;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void
) {
this.pdfDocument = pdfDocument;
this.setPdfDocument = setPdfDocument;
this.previousState = JSON.parse(JSON.stringify(pdfDocument)); // Deep clone
}
abstract execute(): void;
abstract description: string;
undo(): void {
this.setPdfDocument(this.previousState);
}
}
// Rotate pages command
export class RotatePagesCommand extends PageCommand {
private pageIds: string[];
private rotation: number;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageIds: string[],
rotation: number
) {
super(pdfDocument, setPdfDocument);
this.pageIds = pageIds;
this.rotation = rotation;
}
execute(): void {
const updatedPages = this.pdfDocument.pages.map(page => {
if (this.pageIds.includes(page.id)) {
return { ...page, rotation: page.rotation + this.rotation };
}
return page;
});
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
get description(): string {
const direction = this.rotation > 0 ? 'right' : 'left';
return `Rotate ${this.pageIds.length} page(s) ${direction}`;
}
}
// Delete pages command
export class DeletePagesCommand extends PageCommand {
private pageIds: string[];
private deletedPages: PDFPage[];
private deletedPositions: Map<string, number>;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageIds: string[]
) {
super(pdfDocument, setPdfDocument);
this.pageIds = pageIds;
this.deletedPages = [];
this.deletedPositions = new Map();
}
execute(): void {
// Store deleted pages and their positions for undo
this.deletedPages = this.pdfDocument.pages.filter(page =>
this.pageIds.includes(page.id)
);
this.deletedPages.forEach(page => {
const index = this.pdfDocument.pages.findIndex(p => p.id === page.id);
this.deletedPositions.set(page.id, index);
});
const updatedPages = this.pdfDocument.pages
.filter(page => !this.pageIds.includes(page.id))
.map((page, index) => ({ ...page, pageNumber: index + 1 }));
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
undo(): void {
// Simply restore to the previous state (before deletion)
this.setPdfDocument(this.previousState);
}
get description(): string {
return `Delete ${this.pageIds.length} page(s)`;
}
}
// Move pages command
export class MovePagesCommand extends PageCommand {
private pageIds: string[];
private targetIndex: number;
private originalIndices: Map<string, number>;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageIds: string[],
targetIndex: number
) {
super(pdfDocument, setPdfDocument);
this.pageIds = pageIds;
this.targetIndex = targetIndex;
this.originalIndices = new Map();
}
execute(): void {
// Store original positions
this.pageIds.forEach(pageId => {
const index = this.pdfDocument.pages.findIndex(p => p.id === pageId);
this.originalIndices.set(pageId, index);
});
let newPages = [...this.pdfDocument.pages];
const pagesToMove = this.pageIds
.map(id => this.pdfDocument.pages.find(p => p.id === id))
.filter((page): page is PDFPage => page !== undefined);
// Remove pages to move
newPages = newPages.filter(page => !this.pageIds.includes(page.id));
// Insert pages at target position
newPages.splice(this.targetIndex, 0, ...pagesToMove);
// Update page numbers
newPages = newPages.map((page, index) => ({
...page,
pageNumber: index + 1
}));
this.setPdfDocument({
...this.pdfDocument,
pages: newPages,
totalPages: newPages.length
});
}
get description(): string {
return `Move ${this.pageIds.length} page(s)`;
}
}
// Reorder single page command (for drag-and-drop)
export class ReorderPageCommand extends PageCommand {
private pageId: string;
private targetIndex: number;
private originalIndex: number;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageId: string,
targetIndex: number
) {
super(pdfDocument, setPdfDocument);
this.pageId = pageId;
this.targetIndex = targetIndex;
this.originalIndex = pdfDocument.pages.findIndex(p => p.id === pageId);
}
execute(): void {
const newPages = [...this.pdfDocument.pages];
const [movedPage] = newPages.splice(this.originalIndex, 1);
newPages.splice(this.targetIndex, 0, movedPage);
// Update page numbers
const updatedPages = newPages.map((page, index) => ({
...page,
pageNumber: index + 1
}));
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
get description(): string {
return `Reorder page ${this.originalIndex + 1} to position ${this.targetIndex + 1}`;
}
}
// Toggle split markers command
export class ToggleSplitCommand extends PageCommand {
private pageIds: string[];
private previousSplitStates: Map<string, boolean>;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
pageIds: string[]
) {
super(pdfDocument, setPdfDocument);
this.pageIds = pageIds;
this.previousSplitStates = new Map();
}
execute(): void {
// Store previous split states
this.pageIds.forEach(pageId => {
const page = this.pdfDocument.pages.find(p => p.id === pageId);
if (page) {
this.previousSplitStates.set(pageId, !!page.splitBefore);
}
});
const updatedPages = this.pdfDocument.pages.map(page => {
if (this.pageIds.includes(page.id)) {
return { ...page, splitBefore: !page.splitBefore };
}
return page;
});
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
undo(): void {
const updatedPages = this.pdfDocument.pages.map(page => {
if (this.pageIds.includes(page.id)) {
const previousState = this.previousSplitStates.get(page.id);
return { ...page, splitBefore: previousState };
}
return page;
});
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
get description(): string {
return `Toggle split markers for ${this.pageIds.length} page(s)`;
}
}
// Add pages command (for inserting new files)
export class AddPagesCommand extends PageCommand {
private newPages: PDFPage[];
private insertIndex: number;
constructor(
pdfDocument: PDFDocument,
setPdfDocument: (doc: PDFDocument) => void,
newPages: PDFPage[],
insertIndex: number = -1 // -1 means append to end
) {
super(pdfDocument, setPdfDocument);
this.newPages = newPages;
this.insertIndex = insertIndex === -1 ? pdfDocument.pages.length : insertIndex;
}
execute(): void {
const newPagesArray = [...this.pdfDocument.pages];
newPagesArray.splice(this.insertIndex, 0, ...this.newPages);
// Update page numbers for all pages
const updatedPages = newPagesArray.map((page, index) => ({
...page,
pageNumber: index + 1
}));
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
undo(): void {
const updatedPages = this.pdfDocument.pages
.filter(page => !this.newPages.some(newPage => newPage.id === page.id))
.map((page, index) => ({ ...page, pageNumber: index + 1 }));
this.setPdfDocument({
...this.pdfDocument,
pages: updatedPages,
totalPages: updatedPages.length
});
}
get description(): string {
return `Add ${this.newPages.length} page(s)`;
}
}
// Command sequence for bulk operations
export class PageCommandSequence implements CommandSequence {
commands: Command[];
description: string;
constructor(commands: Command[], description?: string) {
this.commands = commands;
this.description = description || `Execute ${commands.length} operations`;
}
execute(): void {
this.commands.forEach(command => command.execute());
}
undo(): void {
// Undo in reverse order
[...this.commands].reverse().forEach(command => command.undo());
}
}

View File

@ -6,7 +6,7 @@ import styles from './PageEditor.module.css';
interface DragDropItem {
id: string;
splitBefore?: boolean;
splitAfter?: boolean;
}
interface DragDropGridProps<T extends DragDropItem> {
@ -128,14 +128,13 @@ const DragDropGrid = <T extends DragDropItem>({
justifyContent: 'flex-start',
height: '100%',
alignItems: 'center',
position: 'relative'
}}
>
{rowItems.map((item, itemIndex) => {
const actualIndex = startIndex + itemIndex;
return (
<React.Fragment key={item.id}>
{/* Split marker */}
{renderSplitMarker && item.splitBefore && actualIndex > 0 && renderSplitMarker(item, actualIndex)}
{/* Item */}
{renderItem(item, actualIndex, itemRefs)}
</React.Fragment>

View File

@ -365,9 +365,16 @@ const PageEditor = ({
const [isAnimating, setIsAnimating] = useState(false);
const [csvInput, setCsvInput] = useState('');
// Position-based split tracking (replaces page-based splitAfter)
const [splitPositions, setSplitPositions] = useState<Set<number>>(new Set());
// Grid container ref for positioning split indicators
const gridContainerRef = useRef<HTMLDivElement>(null);
// Export state
const [exportLoading, setExportLoading] = useState(false);
// DOM-first command handlers
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
pageIds.forEach(pageId => {
@ -445,30 +452,22 @@ const PageEditor = ({
}
class ToggleSplitCommand {
constructor(public pageIds: string[]) {}
constructor(public position: number) {}
execute() {
if (!displayDocument) return;
console.log('Toggle split:', this.pageIds);
console.log('Toggle split at position:', this.position);
// Create new pages array with toggled split markers
const newPages = displayDocument.pages.map(page => {
if (this.pageIds.includes(page.id)) {
return {
...page,
splitAfter: !page.splitAfter
};
// Toggle the split position in the splitPositions set
setSplitPositions(prev => {
const newPositions = new Set(prev);
if (newPositions.has(this.position)) {
newPositions.delete(this.position);
} else {
newPositions.add(this.position);
}
return page;
return newPositions;
});
// Update the document with new split markers
const updatedDocument: PDFDocument = {
...displayDocument,
pages: newPages,
};
setEditedDocument(updatedDocument);
}
}
@ -482,6 +481,7 @@ const PageEditor = ({
// Interface functions for parent component
const displayDocument = editedDocument || mergedPdfDocument;
const handleUndo = useCallback(() => {
undoManagerRef.current.undo();
}, []);
@ -510,18 +510,17 @@ const PageEditor = ({
const handleSplit = useCallback(() => {
if (!displayDocument || selectedPageNumbers.length === 0) return;
console.log('Toggle split markers at selected pages:', selectedPageNumbers);
console.log('Toggle split markers at selected page positions:', selectedPageNumbers);
// Get page IDs for selected pages
const selectedPageIds = selectedPageNumbers.map(pageNum => {
const page = displayDocument.pages.find(p => p.pageNumber === pageNum);
return page?.id || '';
}).filter(id => id);
if (selectedPageIds.length > 0) {
const command = new ToggleSplitCommand(selectedPageIds);
// Convert page numbers to positions (0-based indices)
selectedPageNumbers.forEach(pageNum => {
const pageIndex = displayDocument.pages.findIndex(p => p.pageNumber === pageNum);
if (pageIndex !== -1 && pageIndex < displayDocument.pages.length - 1) {
// Only allow splits before the last page
const command = new ToggleSplitCommand(pageIndex);
command.execute();
}
});
}, [selectedPageNumbers, displayDocument]);
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => {
@ -592,7 +591,8 @@ const PageEditor = ({
console.log('Applying DOM changes before export...');
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
mergedPdfDocument || displayDocument, // Original order
displayDocument // Current display order (includes reordering)
displayDocument, // Current display order (includes reordering)
splitPositions // Position-based splits
);
// For selected pages export, we work with the first document (or single document)
@ -620,7 +620,7 @@ const PageEditor = ({
console.error('Export failed:', error);
setExportLoading(false);
}
}, [displayDocument, selectedPageNumbers, mergedPdfDocument]);
}, [displayDocument, selectedPageNumbers, mergedPdfDocument, splitPositions]);
const onExportAll = useCallback(async () => {
if (!displayDocument) return;
@ -631,7 +631,8 @@ const PageEditor = ({
console.log('Applying DOM changes before export...');
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
mergedPdfDocument || displayDocument, // Original order
displayDocument // Current display order (includes reordering)
displayDocument, // Current display order (includes reordering)
splitPositions // Position-based splits
);
// Step 2: Check if we have multiple documents (splits) or single document
@ -676,7 +677,7 @@ const PageEditor = ({
console.error('Export failed:', error);
setExportLoading(false);
}
}, [displayDocument, mergedPdfDocument]);
}, [displayDocument, mergedPdfDocument, splitPositions]);
// Apply DOM changes to document state using dedicated service
const applyChanges = useCallback(() => {
@ -685,7 +686,8 @@ const PageEditor = ({
// Pass current display document (which includes reordering) to get both reordering AND DOM changes
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
mergedPdfDocument || displayDocument, // Original order
displayDocument // Current display order (includes reordering)
displayDocument, // Current display order (includes reordering)
splitPositions // Position-based splits
);
// For apply changes, we only set the first document if it's an array (splits shouldn't affect document state)
@ -693,7 +695,7 @@ const PageEditor = ({
setEditedDocument(documentToSet);
console.log('Changes applied to document');
}, [displayDocument, mergedPdfDocument]);
}, [displayDocument, mergedPdfDocument, splitPositions]);
const closePdf = useCallback(() => {
@ -767,7 +769,7 @@ const PageEditor = ({
)}
{displayDocument && (
<Box p={0}>
<Box ref={gridContainerRef} p={0} style={{ position: 'relative' }}>
{/* File name and basic controls */}
<Group mb="md" p="md" justify="space-between">
<TextInput
@ -805,6 +807,48 @@ const PageEditor = ({
/>
)}
{/* Split Lines Overlay */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
zIndex: 10
}}
>
{Array.from(splitPositions).map((position) => {
// Calculate the split line position based on grid layout
const ITEM_WIDTH = 320; // 20rem
const ITEM_HEIGHT = 340; // 20rem + gap
const ITEM_GAP = 24; // 1.5rem
const ITEMS_PER_ROW = 4; // Default, could be dynamic
const row = Math.floor(position / ITEMS_PER_ROW);
const col = position % ITEMS_PER_ROW;
// Position after the current item
const leftPosition = (col + 1) * (ITEM_WIDTH + ITEM_GAP) - ITEM_GAP / 2;
const topPosition = row * ITEM_HEIGHT + 100; // Offset for header controls
return (
<div
key={`split-${position}`}
style={{
position: 'absolute',
left: leftPosition,
top: topPosition,
width: '1px',
height: '320px', // Match item height
borderLeft: '1px dashed #3b82f6'
}}
/>
);
})}
</div>
{/* Pages Grid */}
<DragDropGrid
items={displayedPages}
@ -835,12 +879,15 @@ const PageEditor = ({
ToggleSplitCommand={ToggleSplitCommand}
pdfDocument={displayDocument}
setPdfDocument={setEditedDocument}
splitPositions={splitPositions}
/>
)}
/>
</Box>
)}
<NavigationWarningModal />
</Box>
);

View File

@ -39,6 +39,7 @@ interface PageThumbnailProps {
ToggleSplitCommand: any;
pdfDocument: PDFDocument;
setPdfDocument: (doc: PDFDocument) => void;
splitPositions: Set<number>;
}
const PageThumbnail: React.FC<PageThumbnailProps> = ({
@ -62,6 +63,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
ToggleSplitCommand,
pdfDocument,
setPdfDocument,
splitPositions,
}: PageThumbnailProps) => {
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
const [isDragging, setIsDragging] = useState(false);
@ -195,13 +197,14 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
const handleSplit = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
// Create a command to toggle split marker
const command = new ToggleSplitCommand([page.id]);
// Create a command to toggle split at this position
const command = new ToggleSplitCommand(index);
onExecuteCommand(command);
const action = page.splitAfter ? 'removed' : 'added';
onSetStatus(`Split marker ${action} after page ${page.pageNumber}`);
}, [page.pageNumber, page.id, page.splitAfter, onExecuteCommand, onSetStatus, ToggleSplitCommand]);
const hasSplit = splitPositions.has(index);
const action = hasSplit ? 'removed' : 'added';
onSetStatus(`Split marker ${action} after position ${index + 1}`);
}, [index, splitPositions, onExecuteCommand, onSetStatus, ToggleSplitCommand]);
return (
<div
@ -432,21 +435,6 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
</div>
{/* Split indicator - shows where document will be split */}
{page.splitAfter && (
<div
style={{
position: 'absolute',
right: '-8px',
top: '50%',
transform: 'translateY(-50%)',
width: '2px',
height: '60px',
backgroundColor: '#3b82f6',
zIndex: 5,
}}
/>
)}
</div>
);
};

View File

@ -9,17 +9,26 @@ export class DocumentManipulationService {
* Apply all DOM changes (rotations, splits, reordering) to document state
* Returns single document or multiple documents if splits are present
*/
applyDOMChangesToDocument(pdfDocument: PDFDocument, currentDisplayOrder?: PDFDocument): PDFDocument | PDFDocument[] {
applyDOMChangesToDocument(pdfDocument: PDFDocument, currentDisplayOrder?: PDFDocument, splitPositions?: Set<number>): PDFDocument | PDFDocument[] {
console.log('DocumentManipulationService: Applying DOM changes to document');
console.log('Original document page order:', pdfDocument.pages.map(p => p.pageNumber));
console.log('Current display order:', currentDisplayOrder?.pages.map(p => p.pageNumber) || 'none provided');
console.log('Split positions:', splitPositions ? Array.from(splitPositions).sort() : 'none');
// Use current display order (from React state) if provided, otherwise use original order
const baseDocument = currentDisplayOrder || pdfDocument;
console.log('Using page order:', baseDocument.pages.map(p => p.pageNumber));
// Apply DOM changes to each page (rotation, split markers)
const updatedPages = baseDocument.pages.map(page => this.applyPageChanges(page));
// Apply DOM changes to each page (rotation only now, splits are position-based)
let updatedPages = baseDocument.pages.map(page => this.applyPageChanges(page));
// Convert position-based splits to page-based splits for export
if (splitPositions && splitPositions.size > 0) {
updatedPages = updatedPages.map((page, index) => ({
...page,
splitAfter: splitPositions.has(index)
}));
}
// Create final document with reordered pages and applied changes
const finalDocument = {
@ -28,7 +37,7 @@ export class DocumentManipulationService {
};
// Check for splits and return multiple documents if needed
if (this.hasSplitMarkers(finalDocument)) {
if (splitPositions && splitPositions.size > 0) {
return this.createSplitDocuments(finalDocument);
}