app/composables/usePdfExportSafe.ts

186 lines
6.6 KiB
TypeScript

export const usePdfExportSafe = () => {
const exportToPDF = async (elementSelector: string, filename: string) => {
// Only run on client side
if (process.server || typeof window === "undefined") {
throw new Error("PDF generation is only available on the client side");
}
try {
// Dynamic import for client-side only
const html2pdf = (await import("html2pdf.js")).default;
// Get the element to export
const element = document.querySelector(elementSelector);
if (!element) {
throw new Error(`Element with selector "${elementSelector}" not found`);
}
// Create a completely clean version of the content
const createCleanContent = () => {
const cleanDiv = document.createElement("div");
cleanDiv.style.cssText = `
width: 8.5in;
padding: 0.5in;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
line-height: 1.5;
color: #000000;
background: #ffffff;
`;
// Extract text content and basic structure
const extractContent = (sourceEl: Element, targetEl: HTMLElement) => {
const children = sourceEl.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
// Skip export controls
if (
child.classList.contains("export-controls") ||
child.classList.contains("no-pdf") ||
child.classList.contains("no-print")
) {
continue;
}
let newEl: HTMLElement;
// Create appropriate element based on tag
switch (child.tagName.toLowerCase()) {
case "h1":
newEl = document.createElement("h1");
newEl.style.cssText =
"font-size: 24px; font-weight: bold; margin: 20px 0 10px 0; color: #000;";
break;
case "h2":
newEl = document.createElement("h2");
newEl.style.cssText =
"font-size: 20px; font-weight: bold; margin: 16px 0 8px 0; color: #000;";
break;
case "h3":
newEl = document.createElement("h3");
newEl.style.cssText =
"font-size: 16px; font-weight: bold; margin: 12px 0 6px 0; color: #000;";
break;
case "p":
newEl = document.createElement("p");
newEl.style.cssText = "margin: 8px 0; color: #000;";
break;
case "ul":
newEl = document.createElement("ul");
newEl.style.cssText =
"margin: 8px 0; padding-left: 20px; color: #000;";
break;
case "ol":
newEl = document.createElement("ol");
newEl.style.cssText =
"margin: 8px 0; padding-left: 20px; color: #000;";
break;
case "li":
newEl = document.createElement("li");
newEl.style.cssText = "margin: 4px 0; color: #000;";
break;
case "input":
newEl = document.createElement("span");
const inputEl = child as HTMLInputElement;
newEl.textContent = inputEl.value || "[_____]";
newEl.style.cssText =
"border-bottom: 1px solid #000; padding: 2px; color: #000;";
break;
case "textarea":
newEl = document.createElement("span");
const textareaEl = child as HTMLTextAreaElement;
newEl.textContent = textareaEl.value || "[_____]";
newEl.style.cssText =
"border: 1px solid #000; padding: 4px; display: inline-block; color: #000;";
break;
case "select":
newEl = document.createElement("span");
const selectEl = child as HTMLSelectElement;
newEl.textContent = selectEl.value || "[_____]";
newEl.style.cssText =
"border: 1px solid #000; padding: 2px; color: #000;";
break;
default:
newEl = document.createElement("div");
newEl.style.cssText = "color: #000;";
}
// Set text content for elements that should have it
if (
child.tagName.toLowerCase() !== "input" &&
child.tagName.toLowerCase() !== "textarea" &&
child.tagName.toLowerCase() !== "select"
) {
// Get only direct text content, not from children
const directText = Array.from(child.childNodes)
.filter((node) => node.nodeType === Node.TEXT_NODE)
.map((node) => node.textContent)
.join("");
if (directText.trim()) {
newEl.appendChild(document.createTextNode(directText));
}
}
targetEl.appendChild(newEl);
// Recursively process children
if (child.children.length > 0) {
extractContent(child, newEl);
}
}
};
extractContent(element, cleanDiv);
return cleanDiv;
};
const cleanElement = createCleanContent();
// Temporarily add to DOM
cleanElement.style.position = "absolute";
cleanElement.style.left = "-9999px";
cleanElement.style.top = "-9999px";
document.body.appendChild(cleanElement);
// Simple options - no complex CSS processing
const options = {
margin: 0.5,
filename: filename,
image: { type: "jpeg", quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
allowTaint: false,
logging: false,
backgroundColor: "#ffffff",
},
jsPDF: {
unit: "in",
format: "letter",
orientation: "portrait",
},
};
console.log("Generating PDF with clean content...");
try {
// Generate PDF from the clean element
await html2pdf().set(options).from(cleanElement).save();
console.log("PDF generated successfully!");
} finally {
// Clean up
if (cleanElement.parentNode) {
document.body.removeChild(cleanElement);
}
}
} catch (error: any) {
console.error("PDF generation error:", error);
throw new Error(`PDF generation failed: ${error.message || error}`);
}
};
return { exportToPDF };
};