Improve PDF export with multi-page support and better UX
- Fixed multi-page PDF generation - now properly splits long documents across pages - Added isExportingPDF state for loading feedback - Replaced generic download icon with document/PDF icon - Button shows spinning loader during PDF generation - Button changes to blue background while exporting - Added success alert with filename and download location - Button disabled during export to prevent multiple clicks - Improved visual feedback throughout export process
This commit is contained in:
@@ -27,6 +27,7 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
const [titleManuallyEdited, setTitleManuallyEdited] = useState(false);
|
const [titleManuallyEdited, setTitleManuallyEdited] = useState(false);
|
||||||
|
const [isExportingPDF, setIsExportingPDF] = useState(false);
|
||||||
const previousNoteIdRef = useRef<number | null>(null);
|
const previousNoteIdRef = useRef<number | null>(null);
|
||||||
|
|
||||||
// Notify parent component when unsaved changes state changes
|
// Notify parent component when unsaved changes state changes
|
||||||
@@ -141,10 +142,15 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
|
|||||||
const handleExportPDF = async () => {
|
const handleExportPDF = async () => {
|
||||||
if (!note || !editor) return;
|
if (!note || !editor) return;
|
||||||
|
|
||||||
|
setIsExportingPDF(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the editor content element
|
// Get the editor content element
|
||||||
const editorElement = document.querySelector('.ProseMirror');
|
const editorElement = document.querySelector('.ProseMirror');
|
||||||
if (!editorElement) return;
|
if (!editorElement) {
|
||||||
|
setIsExportingPDF(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a temporary container with better styling for PDF
|
// Create a temporary container with better styling for PDF
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
@@ -167,6 +173,7 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
|
|||||||
const contentClone = editorElement.cloneNode(true) as HTMLElement;
|
const contentClone = editorElement.cloneNode(true) as HTMLElement;
|
||||||
contentClone.style.fontSize = '12px';
|
contentClone.style.fontSize = '12px';
|
||||||
contentClone.style.lineHeight = '1.6';
|
contentClone.style.lineHeight = '1.6';
|
||||||
|
contentClone.style.color = '#000000';
|
||||||
container.appendChild(contentClone);
|
container.appendChild(contentClone);
|
||||||
|
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
@@ -181,25 +188,46 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
|
|||||||
// Remove temporary container
|
// Remove temporary container
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
|
|
||||||
// Create PDF
|
// Create PDF with multi-page support
|
||||||
const imgData = canvas.toDataURL('image/png');
|
|
||||||
const pdf = new jsPDF({
|
const pdf = new jsPDF({
|
||||||
orientation: 'portrait',
|
orientation: 'portrait',
|
||||||
unit: 'mm',
|
unit: 'mm',
|
||||||
format: 'a4',
|
format: 'a4',
|
||||||
});
|
});
|
||||||
|
|
||||||
const imgWidth = 210; // A4 width in mm
|
const pageWidth = 210; // A4 width in mm
|
||||||
|
const pageHeight = 297; // A4 height in mm
|
||||||
|
const imgWidth = pageWidth;
|
||||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||||
|
|
||||||
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
|
let heightLeft = imgHeight;
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
// Add first page
|
||||||
|
pdf.addImage(canvas.toDataURL('image/png'), 'PNG', 0, position, imgWidth, imgHeight);
|
||||||
|
heightLeft -= pageHeight;
|
||||||
|
|
||||||
|
// Add additional pages if needed
|
||||||
|
while (heightLeft > 0) {
|
||||||
|
position = heightLeft - imgHeight;
|
||||||
|
pdf.addPage();
|
||||||
|
pdf.addImage(canvas.toDataURL('image/png'), 'PNG', 0, position, imgWidth, imgHeight);
|
||||||
|
heightLeft -= pageHeight;
|
||||||
|
}
|
||||||
|
|
||||||
// Save the PDF
|
// Save the PDF
|
||||||
const fileName = `${localTitle || 'note'}.pdf`;
|
const fileName = `${localTitle || 'note'}.pdf`;
|
||||||
pdf.save(fileName);
|
pdf.save(fileName);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
setTimeout(() => {
|
||||||
|
alert(`PDF exported successfully!\n\nFile: ${fileName}\nLocation: Downloads folder`);
|
||||||
|
setIsExportingPDF(false);
|
||||||
|
}, 500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PDF export failed:', error);
|
console.error('PDF export failed:', error);
|
||||||
alert('Failed to export PDF. Please try again.');
|
alert('Failed to export PDF. Please try again.');
|
||||||
|
setIsExportingPDF(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -285,12 +313,26 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleExportPDF}
|
onClick={handleExportPDF}
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors text-gray-700 dark:text-gray-300"
|
disabled={isExportingPDF}
|
||||||
title="Export as PDF"
|
className={`p-2 rounded-lg transition-colors ${
|
||||||
|
isExportingPDF
|
||||||
|
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 cursor-wait'
|
||||||
|
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||||
|
}`}
|
||||||
|
title={isExportingPDF ? "Generating PDF..." : "Export as PDF"}
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
{isExportingPDF ? (
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
</svg>
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/>
|
||||||
|
<path d="M14 2v6h6"/>
|
||||||
|
<path fill="white" d="M8 13h8v1H8v-1zm0 2h8v1H8v-1zm0 2h5v1H8v-1z"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user