Page breaks

This commit is contained in:
Reece Browne 2025-08-25 17:11:23 +01:00
parent 7cf0806749
commit 2bcb506a7b
8 changed files with 256 additions and 34 deletions

View File

@ -112,6 +112,8 @@ export default function Workbench() {
onDelete={pageEditorFunctions.handleDelete}
onSplit={pageEditorFunctions.handleSplit}
onSplitAll={pageEditorFunctions.handleSplitAll}
onPageBreak={pageEditorFunctions.handlePageBreak}
onPageBreakAll={pageEditorFunctions.handlePageBreakAll}
onExportSelected={pageEditorFunctions.onExportSelected}
onExportAll={pageEditorFunctions.onExportAll}
exportLoading={pageEditorFunctions.exportLoading}

View File

@ -27,6 +27,8 @@ import {
BulkRotateCommand,
BulkSplitCommand,
SplitAllCommand,
PageBreakCommand,
BulkPageBreakCommand,
UndoManager
} from './commands/pageCommands';
import { usePageDocument } from './hooks/usePageDocument';
@ -42,6 +44,8 @@ export interface PageEditorProps {
handleDelete: () => void;
handleSplit: () => void;
handleSplitAll: () => void;
handlePageBreak: () => void;
handlePageBreakAll: () => void;
showExportPreview: (selectedOnly: boolean) => void;
onExportSelected: () => void;
onExportAll: () => void;
@ -266,6 +270,31 @@ const PageEditor = ({
undoManagerRef.current.executeCommand(splitAllCommand);
}, [displayDocument, splitPositions]);
const handlePageBreak = useCallback(() => {
if (!displayDocument || selectedPageNumbers.length === 0) return;
console.log('Insert page breaks after selected pages:', selectedPageNumbers);
const pageBreakCommand = new PageBreakCommand(
selectedPageNumbers,
() => displayDocument,
setEditedDocument,
setSelectedPageNumbers
);
undoManagerRef.current.executeCommand(pageBreakCommand);
}, [selectedPageNumbers, displayDocument]);
const handlePageBreakAll = useCallback(() => {
if (!displayDocument) return;
const pageBreakAllCommand = new BulkPageBreakCommand(
() => displayDocument,
setEditedDocument,
setSelectedPageNumbers
);
undoManagerRef.current.executeCommand(pageBreakAllCommand);
}, [displayDocument]);
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => {
if (!displayDocument) return;
@ -429,6 +458,8 @@ const PageEditor = ({
handleDelete,
handleSplit,
handleSplitAll,
handlePageBreak,
handlePageBreakAll,
showExportPreview: handleExportPreview,
onExportSelected,
onExportAll,
@ -443,8 +474,8 @@ const PageEditor = ({
}
}, [
onFunctionsReady, handleUndo, handleRedo, canUndo, canRedo, handleRotate, handleDelete, handleSplit, handleSplitAll,
handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading, selectionMode, selectedPageNumbers,
splitPositions, displayDocument?.pages.length, closePdf
handlePageBreak, handlePageBreakAll, handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading,
selectionMode, selectedPageNumbers, splitPositions, displayDocument?.pages.length, closePdf
]);
// Display all pages - use edited or original document

View File

@ -12,6 +12,7 @@ import RotateLeftIcon from "@mui/icons-material/RotateLeft";
import RotateRightIcon from "@mui/icons-material/RotateRight";
import DeleteIcon from "@mui/icons-material/Delete";
import CloseIcon from "@mui/icons-material/Close";
import InsertPageBreakIcon from "@mui/icons-material/InsertPageBreak";
interface PageEditorControlsProps {
// Close/Reset functions
@ -28,6 +29,8 @@ interface PageEditorControlsProps {
onDelete: () => void;
onSplit: () => void;
onSplitAll: () => void;
onPageBreak: () => void;
onPageBreakAll: () => void;
// Export functions
onExportSelected: () => void;
@ -53,6 +56,8 @@ const PageEditorControls = ({
onDelete,
onSplit,
onSplitAll,
onPageBreak,
onPageBreakAll,
onExportSelected,
onExportAll,
exportLoading,
@ -79,6 +84,16 @@ const PageEditorControls = ({
return hasAllSplits ? "Remove All Splits" : "Split All";
};
// Calculate page break tooltip text
const getPageBreakTooltip = () => {
if (selectionMode) {
return selectedPages.length > 0
? `Insert ${selectedPages.length} Page Break${selectedPages.length > 1 ? 's' : ''}`
: "Insert Page Breaks";
}
return "Insert Page Breaks After All Pages";
};
return (
<div
style={{
@ -183,6 +198,17 @@ const PageEditorControls = ({
<ContentCutIcon />
</ActionIcon>
</Tooltip>
<Tooltip label={getPageBreakTooltip()}>
<ActionIcon
onClick={selectionMode ? onPageBreak : onPageBreakAll}
disabled={selectionMode && selectedPages.length === 0}
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
color={selectionMode && selectedPages.length > 0 ? "orange" : undefined}
size="lg"
>
<InsertPageBreakIcon />
</ActionIcon>
</Tooltip>
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />

View File

@ -292,7 +292,10 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
pointerEvents: 'auto'
}}
onMouseDown={(e) => e.stopPropagation()}
onMouseDown={(e) => {
e.stopPropagation();
onTogglePage(page.pageNumber);
}}
onMouseUp={(e) => e.stopPropagation()}
onDragStart={(e) => {
e.preventDefault();
@ -302,9 +305,10 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
<Checkbox
checked={Array.isArray(selectedPages) ? selectedPages.includes(page.pageNumber) : false}
onChange={() => {
// onChange is handled by the parent div click
// Selection is handled by container mouseDown
}}
size="sm"
style={{ pointerEvents: 'none' }}
/>
</div>
)}
@ -323,7 +327,14 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
justifyContent: 'center'
}}
>
{thumbnailUrl ? (
{page.isBlankPage ? (
<div style={{
width: '100%',
height: '100%',
backgroundColor: 'white',
borderRadius: 4
}}></div>
) : thumbnailUrl ? (
<img
src={thumbnailUrl}
alt={`Page ${page.pageNumber}`}
@ -354,7 +365,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
position: 'absolute',
top: 5,
left: 5,
background: 'rgba(162, 201, 255, 0.8)',
background: page.isBlankPage ? 'rgba(255, 165, 0, 0.8)' : 'rgba(162, 201, 255, 0.8)',
padding: '6px 8px',
borderRadius: 8,
zIndex: 2,

View File

@ -396,6 +396,147 @@ export class SplitAllCommand extends DOMCommand {
}
}
export class PageBreakCommand extends DOMCommand {
private insertedPages: PDFPage[] = [];
private originalDocument: PDFDocument | null = null;
constructor(
private selectedPageNumbers: number[],
private getCurrentDocument: () => PDFDocument | null,
private setDocument: (doc: PDFDocument) => void,
private setSelectedPages: (pages: number[]) => void
) {
super();
}
execute(): void {
const currentDoc = this.getCurrentDocument();
if (!currentDoc || this.selectedPageNumbers.length === 0) return;
// Store original state for undo
this.originalDocument = {
...currentDoc,
pages: currentDoc.pages.map(page => ({...page}))
};
// Create new pages array with blank pages inserted
const newPages: PDFPage[] = [];
this.insertedPages = [];
let pageNumberCounter = 1;
currentDoc.pages.forEach((page, index) => {
// Add the current page
const updatedPage = { ...page, pageNumber: pageNumberCounter++ };
newPages.push(updatedPage);
// If this page is selected for page break insertion, add a blank page after it
if (this.selectedPageNumbers.includes(page.pageNumber)) {
const blankPage: PDFPage = {
id: `blank-${Date.now()}-${index}`,
pageNumber: pageNumberCounter++,
originalPageNumber: -1, // Mark as blank page
thumbnail: null,
rotation: 0,
selected: false,
splitAfter: false,
isBlankPage: true // Custom flag for blank pages
};
newPages.push(blankPage);
this.insertedPages.push(blankPage);
}
});
// Update document
const updatedDocument: PDFDocument = {
...currentDoc,
pages: newPages,
totalPages: newPages.length,
};
this.setDocument(updatedDocument);
this.setSelectedPages([]);
}
undo(): void {
if (!this.originalDocument) return;
this.setDocument(this.originalDocument);
}
get description(): string {
return `Insert ${this.selectedPageNumbers.length} page break(s)`;
}
}
export class BulkPageBreakCommand extends DOMCommand {
private insertedPages: PDFPage[] = [];
private originalDocument: PDFDocument | null = null;
constructor(
private getCurrentDocument: () => PDFDocument | null,
private setDocument: (doc: PDFDocument) => void,
private setSelectedPages: (pages: number[]) => void
) {
super();
}
execute(): void {
const currentDoc = this.getCurrentDocument();
if (!currentDoc) return;
// Store original state for undo
this.originalDocument = {
...currentDoc,
pages: currentDoc.pages.map(page => ({...page}))
};
// Create new pages array with blank pages inserted after each page (except the last)
const newPages: PDFPage[] = [];
this.insertedPages = [];
let pageNumberCounter = 1;
currentDoc.pages.forEach((page, index) => {
// Add the current page
const updatedPage = { ...page, pageNumber: pageNumberCounter++ };
newPages.push(updatedPage);
// Add blank page after each page except the last one
if (index < currentDoc.pages.length - 1) {
const blankPage: PDFPage = {
id: `blank-${Date.now()}-${index}`,
pageNumber: pageNumberCounter++,
originalPageNumber: -1,
thumbnail: null,
rotation: 0,
selected: false,
splitAfter: false,
isBlankPage: true
};
newPages.push(blankPage);
this.insertedPages.push(blankPage);
}
});
// Update document
const updatedDocument: PDFDocument = {
...currentDoc,
pages: newPages,
totalPages: newPages.length,
};
this.setDocument(updatedDocument);
this.setSelectedPages([]);
}
undo(): void {
if (!this.originalDocument) return;
this.setDocument(this.originalDocument);
}
get description(): string {
return `Insert page breaks after all pages`;
}
}
// Simple undo manager for DOM commands
export class UndoManager {
private undoStack: DOMCommand[] = [];

View File

@ -121,10 +121,10 @@ export class DocumentManipulationService {
*/
private getRotationFromDOM(pageElement: Element, originalPage: PDFPage): number {
const img = pageElement.querySelector('img');
if (img && img.style.rotate) {
// Parse rotation from DOM (e.g., "90deg" -> 90)
const rotationMatch = img.style.rotate.match(/-?\d+/);
const domRotation = rotationMatch ? parseInt(rotationMatch[0]) : 0;
if (img && img.style.transform) {
// Parse rotation from transform property (e.g., "rotate(90deg)" -> 90)
const rotationMatch = img.style.transform.match(/rotate\((-?\d+)deg\)/);
const domRotation = rotationMatch ? parseInt(rotationMatch[1]) : 0;
console.log(`Page ${originalPage.pageNumber}: DOM rotation = ${domRotation}°, original = ${originalPage.rotation}°`);
return domRotation;
@ -146,7 +146,7 @@ export class DocumentManipulationService {
const img = pageElement.querySelector('img');
if (img) {
// Reset rotation to match document state
img.style.rotate = `${page.rotation}deg`;
img.style.transform = `rotate(${page.rotation}deg)`;
}
}
});

View File

@ -50,19 +50,29 @@ export class PDFExportService {
const newDoc = await PDFLibDocument.create();
for (const page of pages) {
// Get the original page from source document using originalPageNumber
const sourcePageIndex = page.originalPageNumber - 1;
if (page.isBlankPage || page.originalPageNumber === -1) {
// Create a blank page
const blankPage = newDoc.addPage(PageSizes.A4);
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
// Copy the page
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
// Apply rotation
// Apply rotation if needed
if (page.rotation !== 0) {
copiedPage.setRotation(degrees(page.rotation));
blankPage.setRotation(degrees(page.rotation));
}
} else {
// Get the original page from source document using originalPageNumber
const sourcePageIndex = page.originalPageNumber - 1;
newDoc.addPage(copiedPage);
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
// Copy the page
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
// Apply rotation
if (page.rotation !== 0) {
copiedPage.setRotation(degrees(page.rotation));
}
newDoc.addPage(copiedPage);
}
}
}

View File

@ -6,6 +6,7 @@ export interface PDFPage {
rotation: number;
selected: boolean;
splitAfter?: boolean;
isBlankPage?: boolean;
}
export interface PDFDocument {