refactor(file-handler): replace innerHTML with DOM methods for safer rendering
refactor(form-filler): improve form field creation with DOM methods refactor(compare-pdfs): use textContent instead of innerHTML refactor(duplicate-organize): replace innerHTML with DOM methods style(main): remove commented start app line feat(config): add pdf-tools config file with tool categories
This commit is contained in:
17
src/js/config/pdf-tools.ts
Normal file
17
src/js/config/pdf-tools.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const singlePdfLoadTools = [
|
||||||
|
'split', 'organize', 'rotate', 'add-page-numbers',
|
||||||
|
'pdf-to-jpg', 'pdf-to-png', 'pdf-to-webp', 'compress', 'pdf-to-greyscale',
|
||||||
|
'edit-metadata', 'remove-metadata', 'flatten', 'delete-pages', 'add-blank-page',
|
||||||
|
'extract-pages', 'add-watermark', 'add-header-footer', 'invert-colors', 'view-metadata',
|
||||||
|
'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', 'pdf-to-tiff', 'split-in-half',
|
||||||
|
'page-dimensions', 'n-up', 'duplicate-organize', 'combine-single-page', 'fix-dimensions', 'change-background-color',
|
||||||
|
'change-text-color', 'ocr-pdf', 'sign-pdf', 'remove-annotations', 'cropper', 'form-filler',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const simpleTools = [
|
||||||
|
'encrypt', 'decrypt', 'change-permissions', 'pdf-to-markdown', 'word-to-pdf',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const multiFileTools = [
|
||||||
|
'merge', 'pdf-to-zip', 'jpg-to-pdf', 'png-to-pdf', 'webp-to-pdf', 'image-to-pdf', 'svg-to-pdf', 'bmp-to-pdf', 'heic-to-pdf', 'tiff-to-pdf',
|
||||||
|
];
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// FILE: js/handlers/fileHandler.js
|
|
||||||
|
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js';
|
import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js';
|
||||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
@@ -9,8 +7,10 @@ import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.j
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
import { multiFileTools, simpleTools, singlePdfLoadTools } from '../config/pdf-tools.js';
|
||||||
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
async function handleSinglePdfUpload(toolId: any, file: any) {
|
async function handleSinglePdfUpload(toolId, file) {
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
@@ -30,8 +30,7 @@ async function handleSinglePdfUpload(toolId: any, file: any) {
|
|||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
processBtn.disabled = false;
|
|
||||||
processBtn.classList.remove('hidden');
|
processBtn.classList.remove('hidden');
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
@@ -41,7 +40,7 @@ async function handleSinglePdfUpload(toolId: any, file: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (['split', 'delete-pages', 'add-blank-page', 'extract-pages', 'add-header-footer'].includes(toolId)) {
|
if (['split', 'delete-pages', 'add-blank-page', 'extract-pages', 'add-header-footer'].includes(toolId)) {
|
||||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount();
|
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'organize' || toolId === 'rotate') {
|
if (toolId === 'organize' || toolId === 'rotate') {
|
||||||
@@ -52,32 +51,23 @@ async function handleSinglePdfUpload(toolId: any, file: any) {
|
|||||||
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
||||||
const rotateAllRightBtn = document.getElementById('rotate-all-right-btn');
|
const rotateAllRightBtn = document.getElementById('rotate-all-right-btn');
|
||||||
|
|
||||||
// Show the buttons
|
|
||||||
rotateAllControls.classList.remove('hidden');
|
rotateAllControls.classList.remove('hidden');
|
||||||
createIcons({icons}); // Render the new icons
|
createIcons({ icons });
|
||||||
|
|
||||||
const rotateAll = (direction: any) => {
|
const rotateAll = (direction) => {
|
||||||
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
const currentRotation = parseInt((item as HTMLElement).dataset.rotation || '0');
|
||||||
const currentRotation = parseInt(item.dataset.rotation || '0');
|
|
||||||
// Calculate new rotation, ensuring it wraps around 0-270 degrees
|
|
||||||
const newRotation = (currentRotation + (direction * 90) + 360) % 360;
|
const newRotation = (currentRotation + (direction * 90) + 360) % 360;
|
||||||
|
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
|
||||||
item.dataset.rotation = newRotation;
|
|
||||||
|
|
||||||
const thumbnail = item.querySelector('canvas, img');
|
const thumbnail = item.querySelector('canvas, img');
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'style' does not exist on type 'Element'.
|
(thumbnail as HTMLElement).style.transform = `rotate(${newRotation}deg)`;
|
||||||
thumbnail.style.transform = `rotate(${newRotation}deg)`;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
rotateAllLeftBtn.onclick = () => rotateAll(-1);
|
||||||
rotateAllLeftBtn.onclick = () => rotateAll(-1); // -1 for counter-clockwise
|
rotateAllRightBtn.onclick = () => rotateAll(1);
|
||||||
rotateAllRightBtn.onclick = () => rotateAll(1); // 1 for clockwise
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'duplicate-organize') {
|
if (toolId === 'duplicate-organize') {
|
||||||
@@ -93,9 +83,7 @@ async function handleSinglePdfUpload(toolId: any, file: any) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes as ArrayBuffer }).promise;
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
|
||||||
|
|
||||||
const [metadata, fieldObjects] = await Promise.all([
|
const [metadata, fieldObjects] = await Promise.all([
|
||||||
pdfjsDoc.getMetadata(),
|
pdfjsDoc.getMetadata(),
|
||||||
pdfjsDoc.getFieldObjects()
|
pdfjsDoc.getFieldObjects()
|
||||||
@@ -103,7 +91,34 @@ async function handleSinglePdfUpload(toolId: any, file: any) {
|
|||||||
|
|
||||||
const { info, metadata: rawXmpString } = metadata;
|
const { info, metadata: rawXmpString } = metadata;
|
||||||
|
|
||||||
const parsePdfDate = (pdfDate: any) => {
|
resultsDiv.textContent = ''; // Clear safely
|
||||||
|
|
||||||
|
const createSection = (title) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'mb-4';
|
||||||
|
const h3 = document.createElement('h3');
|
||||||
|
h3.className = 'text-lg font-semibold text-white mb-2';
|
||||||
|
h3.textContent = title;
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||||
|
wrapper.append(h3, ul);
|
||||||
|
return { wrapper, ul };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createListItem = (key, value) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'flex flex-col sm:flex-row';
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.className = 'w-40 flex-shrink-0 text-gray-400';
|
||||||
|
strong.textContent = key;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex-grow text-white break-all';
|
||||||
|
div.textContent = value;
|
||||||
|
li.append(strong, div);
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsePdfDate = (pdfDate) => {
|
||||||
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) return pdfDate;
|
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) return pdfDate;
|
||||||
try {
|
try {
|
||||||
const year = pdfDate.substring(2, 6);
|
const year = pdfDate.substring(2, 6);
|
||||||
@@ -113,91 +128,49 @@ async function handleSinglePdfUpload(toolId: any, file: any) {
|
|||||||
const minute = pdfDate.substring(12, 14);
|
const minute = pdfDate.substring(12, 14);
|
||||||
const second = pdfDate.substring(14, 16);
|
const second = pdfDate.substring(14, 16);
|
||||||
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
|
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
|
||||||
} catch {
|
} catch { return pdfDate; }
|
||||||
return pdfDate;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let htmlContent = '';
|
const infoSection = createSection('Info Dictionary');
|
||||||
|
|
||||||
htmlContent += `
|
|
||||||
<div class="mb-4">
|
|
||||||
<h3 class="text-lg font-semibold text-white mb-2">Info Dictionary</h3>
|
|
||||||
<ul class="space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700">`;
|
|
||||||
|
|
||||||
if (info && Object.keys(info).length > 0) {
|
if (info && Object.keys(info).length > 0) {
|
||||||
for (const key in info) {
|
for (const key in info) {
|
||||||
let value = info[key];
|
let value = info[key] || '- Not Set -';
|
||||||
let displayValue;
|
if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
|
||||||
|
value = parsePdfDate(value);
|
||||||
if (value === null || value === undefined || String(value).trim() === '') {
|
|
||||||
displayValue = `<span class="text-gray-500 italic">- Not Set -</span>`;
|
|
||||||
} else if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
|
|
||||||
displayValue = `<span class="text-white break-all">${parsePdfDate(value)}</span>`;
|
|
||||||
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
||||||
try {
|
|
||||||
let nestedList = '<ul class="mt-2 space-y-1 pl-4 border-l border-gray-600">';
|
|
||||||
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
|
||||||
nestedList += `<li class="flex"><strong class="w-32 flex-shrink-0 text-gray-500">${nestedKey}:</strong> <span class="text-white break-all">${nestedValue}</span></li>`;
|
|
||||||
}
|
}
|
||||||
nestedList += '</ul>';
|
infoSection.ul.appendChild(createListItem(key, String(value)));
|
||||||
displayValue = nestedList;
|
|
||||||
} catch (e) {
|
|
||||||
displayValue = `<span class="text-white break-all">[Unserializable Object]</span>`;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback for simple values (strings, numbers, arrays)
|
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||||
displayValue = `<span class="text-white break-all">${String(value)}</span>`;
|
|
||||||
}
|
}
|
||||||
htmlContent += `<li class="flex flex-col sm:flex-row"><strong class="w-40 flex-shrink-0 text-gray-400">${key}</strong><div class="flex-grow">${displayValue}</div></li>`;
|
resultsDiv.appendChild(infoSection.wrapper);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
htmlContent += `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
|
||||||
}
|
|
||||||
htmlContent += `</ul></div>`;
|
|
||||||
|
|
||||||
htmlContent += `
|
|
||||||
<div class="mb-4">
|
|
||||||
<h3 class="text-lg font-semibold text-white mb-2">Interactive Form Fields</h3>
|
|
||||||
<ul class="space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700">`;
|
|
||||||
|
|
||||||
|
const fieldsSection = createSection('Interactive Form Fields');
|
||||||
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
||||||
const getFriendlyFieldType = (type: any) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'Tx': return 'Text'; case 'Btn': return 'Button/Checkbox';
|
|
||||||
case 'Ch': return 'Choice/Dropdown'; case 'Sig': return 'Signature';
|
|
||||||
default: return type || 'Unknown';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (const fieldName in fieldObjects) {
|
for (const fieldName in fieldObjects) {
|
||||||
const field = fieldObjects[fieldName][0];
|
const field = fieldObjects[fieldName][0];
|
||||||
const fieldType = getFriendlyFieldType(field.fieldType);
|
const value = (field as any).fieldValue || '- Not Set -';
|
||||||
const fieldValue = field.fieldValue ? `<span class="text-white break-all">${field.fieldValue}</span>` : `<span class="text-gray-500 italic">- Not Set -</span>`;
|
fieldsSection.ul.appendChild(createListItem(fieldName, String(value)));
|
||||||
htmlContent += `
|
|
||||||
<li class="flex flex-col sm:flex-row">
|
|
||||||
<strong class="w-40 flex-shrink-0 text-gray-400 font-semibold">${fieldName}</strong>
|
|
||||||
<div><span class="text-gray-300 mr-2">[${fieldType}]</span>${fieldValue}</div>
|
|
||||||
</li>`;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
htmlContent += `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
||||||
}
|
}
|
||||||
htmlContent += `</ul></div>`;
|
resultsDiv.appendChild(fieldsSection.wrapper);
|
||||||
|
|
||||||
htmlContent += `
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-semibold text-white mb-2">XMP Metadata (Raw XML)</h3>
|
|
||||||
<div class="bg-gray-900 p-4 rounded-lg border border-gray-700">`;
|
|
||||||
|
|
||||||
|
const xmpSection = createSection('XMP Metadata (Raw XML)');
|
||||||
|
const xmpContainer = document.createElement('div');
|
||||||
|
xmpContainer.className = 'bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||||
if (rawXmpString) {
|
if (rawXmpString) {
|
||||||
const escapedXml = rawXmpString.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
const pre = document.createElement('pre');
|
||||||
htmlContent += `<pre class="text-xs text-gray-300 whitespace-pre-wrap break-all">${escapedXml}</pre>`;
|
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||||
|
pre.textContent = String(rawXmpString);
|
||||||
|
xmpContainer.appendChild(pre);
|
||||||
} else {
|
} else {
|
||||||
htmlContent += `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
||||||
}
|
}
|
||||||
htmlContent += `</div></div>`;
|
xmpSection.wrapper.appendChild(xmpContainer);
|
||||||
|
resultsDiv.appendChild(xmpSection.wrapper);
|
||||||
|
|
||||||
resultsDiv.innerHTML = htmlContent;
|
|
||||||
resultsDiv.classList.remove('hidden');
|
resultsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -208,209 +181,53 @@ async function handleSinglePdfUpload(toolId: any, file: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'edit-metadata') {
|
|
||||||
const form = document.getElementById('metadata-form');
|
|
||||||
const formatDateForInput = (date: any) => {
|
|
||||||
if (!date) return '';
|
|
||||||
const pad = (num: any) => num.toString().padStart(2, '0');
|
|
||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-title').value = state.pdfDoc.getTitle() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-author').value = state.pdfDoc.getAuthor() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-subject').value = state.pdfDoc.getSubject() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-keywords').value = state.pdfDoc.getKeywords() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-creator').value = state.pdfDoc.getCreator() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-producer').value = state.pdfDoc.getProducer() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-creation-date').value = formatDateForInput(state.pdfDoc.getCreationDate());
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-mod-date').value = formatDateForInput(state.pdfDoc.getModificationDate());
|
|
||||||
|
|
||||||
form.classList.remove('hidden');
|
|
||||||
|
|
||||||
const addBtn = document.getElementById('add-custom-meta-btn');
|
|
||||||
const container = document.getElementById('custom-metadata-container');
|
|
||||||
|
|
||||||
addBtn.onclick = () => {
|
|
||||||
const newFieldId = `custom-field-${Date.now()}`;
|
|
||||||
const fieldWrapper = document.createElement('div');
|
|
||||||
fieldWrapper.id = newFieldId;
|
|
||||||
fieldWrapper.className = 'flex items-center gap-2';
|
|
||||||
fieldWrapper.innerHTML = `
|
|
||||||
<input type="text" placeholder="Key (e.g., Department)" class="custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2">
|
|
||||||
<input type="text" placeholder="Value (e.g., Marketing)" class="custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2">
|
|
||||||
<button type="button" class="btn p-2 text-red-500 hover:bg-gray-700 rounded-full" onclick="document.getElementById('${newFieldId}').remove()">
|
|
||||||
<i data-lucide="trash-2"></i>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
container.appendChild(fieldWrapper);
|
|
||||||
|
|
||||||
createIcons({icons}); // Re-render icons
|
|
||||||
};
|
|
||||||
|
|
||||||
createIcons({icons});
|
|
||||||
}
|
|
||||||
if (toolId === 'edit-metadata') {
|
if (toolId === 'edit-metadata') {
|
||||||
const form = document.getElementById('metadata-form');
|
const form = document.getElementById('metadata-form');
|
||||||
const container = document.getElementById('custom-metadata-container');
|
const container = document.getElementById('custom-metadata-container');
|
||||||
const addBtn = document.getElementById('add-custom-meta-btn');
|
const addBtn = document.getElementById('add-custom-meta-btn');
|
||||||
|
|
||||||
// Helper to format Date objects
|
const formatDateForInput = (date) => {
|
||||||
const formatDateForInput = (date: any) => {
|
|
||||||
if (!date) return '';
|
if (!date) return '';
|
||||||
const pad = (num: any) => num.toString().padStart(2, '0');
|
const pad = (num) => num.toString().padStart(2, '0');
|
||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
(document.getElementById('meta-title') as HTMLInputElement).value = state.pdfDoc.getTitle() || '';
|
||||||
|
(document.getElementById('meta-author') as HTMLInputElement).value = state.pdfDoc.getAuthor() || '';
|
||||||
// Comprehensive decoder for PDF values
|
(document.getElementById('meta-subject') as HTMLInputElement).value = state.pdfDoc.getSubject() || '';
|
||||||
const decodePDFValue = (valueNode: any) => {
|
(document.getElementById('meta-keywords') as HTMLInputElement).value = state.pdfDoc.getKeywords() || '';
|
||||||
if (!valueNode) return '';
|
(document.getElementById('meta-creator') as HTMLInputElement).value = state.pdfDoc.getCreator() || '';
|
||||||
|
(document.getElementById('meta-producer') as HTMLInputElement).value = state.pdfDoc.getProducer() || '';
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'PDFLib' does not exist on type 'Window &... Remove this comment to see the full error message
|
(document.getElementById('meta-creation-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getCreationDate());
|
||||||
const { PDFHexString, PDFString, PDFName, PDFNumber } = window.PDFLib;
|
(document.getElementById('meta-mod-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getModificationDate());
|
||||||
|
|
||||||
try {
|
|
||||||
// Handle PDFHexString
|
|
||||||
if (valueNode instanceof PDFHexString) {
|
|
||||||
// Try the built-in decoder first
|
|
||||||
try {
|
|
||||||
return valueNode.decodeText();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Built-in decodeText failed for PDFHexString, trying manual decode');
|
|
||||||
// Manual hex decoding
|
|
||||||
const hexStr = valueNode.toString();
|
|
||||||
return decodeHexStringManual(hexStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle PDFString
|
|
||||||
if (valueNode instanceof PDFString) {
|
|
||||||
try {
|
|
||||||
return valueNode.decodeText();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Built-in decodeText failed for PDFString, using toString');
|
|
||||||
return valueNode.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle other types
|
|
||||||
if (valueNode instanceof PDFName) {
|
|
||||||
return valueNode.decodeText ? valueNode.decodeText() : valueNode.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueNode instanceof PDFNumber) {
|
|
||||||
return valueNode.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback - check if the toString() result needs hex decoding
|
|
||||||
const strValue = valueNode.toString();
|
|
||||||
|
|
||||||
// Check for various hex encoding patterns
|
|
||||||
if (strValue.includes('#')) {
|
|
||||||
// Pattern like "helllo#20h"
|
|
||||||
return strValue.replace(/#([0-9A-Fa-f]{2})/g, (match: any, hex: any) => {
|
|
||||||
return String.fromCharCode(parseInt(hex, 16));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a hex string in angle brackets like <48656C6C6C6F20>
|
|
||||||
if (strValue.match(/^<[0-9A-Fa-f\s]+>$/)) {
|
|
||||||
return decodeHexStringManual(strValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a parentheses-wrapped string
|
|
||||||
if (strValue.match(/^\([^)]*\)$/)) {
|
|
||||||
return strValue.slice(1, -1); // Remove parentheses
|
|
||||||
}
|
|
||||||
|
|
||||||
return strValue;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error decoding PDF value:', error);
|
|
||||||
return valueNode.toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Manual hex string decoder
|
|
||||||
const decodeHexStringManual = (hexStr: any) => {
|
|
||||||
try {
|
|
||||||
// Remove angle brackets if present
|
|
||||||
let cleanHex = hexStr.replace(/^<|>$/g, '');
|
|
||||||
// Remove any whitespace
|
|
||||||
cleanHex = cleanHex.replace(/\s/g, '');
|
|
||||||
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
||||||
const hexPair = cleanHex.substr(i, 2);
|
|
||||||
if (hexPair.length === 2 && /^[0-9A-Fa-f]{2}$/.test(hexPair)) {
|
|
||||||
const charCode = parseInt(hexPair, 16);
|
|
||||||
// Only add printable characters or common whitespace
|
|
||||||
if (charCode >= 32 && charCode <= 126) {
|
|
||||||
result += String.fromCharCode(charCode);
|
|
||||||
} else if (charCode === 10 || charCode === 13 || charCode === 9) {
|
|
||||||
result += String.fromCharCode(charCode);
|
|
||||||
} else {
|
|
||||||
// For non-printable characters, you might want to skip or use a placeholder
|
|
||||||
result += String.fromCharCode(charCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Manual hex decode failed:', error);
|
|
||||||
return hexStr;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- 1. Pre-fill Standard Fields ---
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-title').value = state.pdfDoc.getTitle() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-author').value = state.pdfDoc.getAuthor() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-subject').value = state.pdfDoc.getSubject() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-keywords').value = state.pdfDoc.getKeywords() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-creator').value = state.pdfDoc.getCreator() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-producer').value = state.pdfDoc.getProducer() || '';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-creation-date').value = formatDateForInput(state.pdfDoc.getCreationDate());
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
document.getElementById('meta-mod-date').value = formatDateForInput(state.pdfDoc.getModificationDate());
|
|
||||||
|
|
||||||
container.querySelectorAll('.custom-field-wrapper').forEach(el => el.remove());
|
|
||||||
|
|
||||||
addBtn.onclick = () => {
|
addBtn.onclick = () => {
|
||||||
const newFieldId = `custom-field-${Date.now()}`;
|
|
||||||
const fieldWrapper = document.createElement('div');
|
const fieldWrapper = document.createElement('div');
|
||||||
fieldWrapper.id = newFieldId;
|
|
||||||
fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper';
|
fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper';
|
||||||
fieldWrapper.innerHTML = `
|
|
||||||
<input type="text" placeholder="Key (e.g., Department)" class="custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2">
|
|
||||||
<input type="text" placeholder="Value (e.g., Marketing)" class="custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2">
|
|
||||||
<button type="button" class="btn p-2 text-red-500 hover:bg-gray-700 rounded-full" onclick="document.getElementById('${newFieldId}').remove()">
|
|
||||||
<i data-lucide="trash-2"></i>
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
container.appendChild(fieldWrapper);
|
|
||||||
|
|
||||||
|
const keyInput = document.createElement('input');
|
||||||
|
keyInput.type = 'text';
|
||||||
|
keyInput.placeholder = 'Key (e.g., Department)';
|
||||||
|
keyInput.className = 'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||||
|
|
||||||
|
const valueInput = document.createElement('input');
|
||||||
|
valueInput.type = 'text';
|
||||||
|
valueInput.placeholder = 'Value (e.g., Marketing)';
|
||||||
|
valueInput.className = 'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.type = 'button';
|
||||||
|
removeBtn.className = 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
|
||||||
|
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
||||||
|
removeBtn.addEventListener('click', () => fieldWrapper.remove());
|
||||||
|
|
||||||
|
fieldWrapper.append(keyInput, valueInput, removeBtn);
|
||||||
|
container.appendChild(fieldWrapper);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
};
|
};
|
||||||
|
|
||||||
form.classList.remove('hidden');
|
form.classList.remove('hidden');
|
||||||
|
createIcons({ icons });
|
||||||
createIcons({icons}); // Render all icons after dynamic changes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'cropper') {
|
if (toolId === 'cropper') {
|
||||||
@@ -431,11 +248,10 @@ async function handleSinglePdfUpload(toolId: any, file: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMultiFileUpload(toolId: any) {
|
function handleMultiFileUpload(toolId) {
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
processBtn.disabled = false;
|
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||||
@@ -447,15 +263,23 @@ function handleMultiFileUpload(toolId: any) {
|
|||||||
toolLogic.merge.setup();
|
toolLogic.merge.setup();
|
||||||
} else if (toolId === 'image-to-pdf') {
|
} else if (toolId === 'image-to-pdf') {
|
||||||
const imageList = document.getElementById('image-list');
|
const imageList = document.getElementById('image-list');
|
||||||
|
imageList.textContent = ''; // Clear safely
|
||||||
imageList.innerHTML = '';
|
|
||||||
|
|
||||||
state.files.forEach(file => {
|
state.files.forEach(file => {
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = "relative group cursor-move";
|
li.className = "relative group cursor-move";
|
||||||
li.dataset.fileName = file.name;
|
li.dataset.fileName = file.name;
|
||||||
li.innerHTML = `<img src="${url}" class="w-full h-full object-cover rounded-md border-2 border-gray-600"><p class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1">${file.name}</p>`;
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = url;
|
||||||
|
img.className = "w-full h-full object-cover rounded-md border-2 border-gray-600";
|
||||||
|
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.className = "absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1";
|
||||||
|
p.textContent = file.name; // Safe insertion
|
||||||
|
|
||||||
|
li.append(img, p);
|
||||||
imageList.appendChild(li);
|
imageList.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -463,14 +287,12 @@ function handleMultiFileUpload(toolId: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setupFileInputHandler(toolId) {
|
||||||
export function setupFileInputHandler(toolId: any) {
|
|
||||||
const fileInput = document.getElementById('file-input');
|
const fileInput = document.getElementById('file-input');
|
||||||
const multiFileTools = ['merge', 'pdf-to-zip', 'jpg-to-pdf', 'png-to-pdf', 'webp-to-pdf', 'image-to-pdf', 'svg-to-pdf', 'bmp-to-pdf', 'heic-to-pdf', 'tiff-to-pdf'];
|
|
||||||
const isMultiFileTool = multiFileTools.includes(toolId);
|
const isMultiFileTool = multiFileTools.includes(toolId);
|
||||||
let isFirstUpload = true;
|
let isFirstUpload = true;
|
||||||
|
|
||||||
const processFiles = async (newFiles: any) => {
|
const processFiles = async (newFiles) => {
|
||||||
if (newFiles.length === 0) return;
|
if (newFiles.length === 0) return;
|
||||||
|
|
||||||
if (!isMultiFileTool || isFirstUpload) {
|
if (!isMultiFileTool || isFirstUpload) {
|
||||||
@@ -488,20 +310,9 @@ export function setupFileInputHandler(toolId: any) {
|
|||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
if (fileControls) {
|
if (fileControls) {
|
||||||
fileControls.classList.remove('hidden');
|
fileControls.classList.remove('hidden');
|
||||||
|
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
const singlePdfLoadTools = ['split', 'organize', 'rotate', 'add-page-numbers',
|
|
||||||
'pdf-to-jpg', 'pdf-to-png', 'pdf-to-webp', 'compress', 'pdf-to-greyscale',
|
|
||||||
'edit-metadata', 'remove-metadata', 'flatten', 'delete-pages', 'add-blank-page',
|
|
||||||
'extract-pages', 'add-watermark', 'add-header-footer', 'invert-colors', 'view-metadata',
|
|
||||||
'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', 'pdf-to-tiff', 'split-in-half',
|
|
||||||
'page-dimensions', 'n-up', 'duplicate-organize', 'combine-single-page', 'fix-dimensions', 'change-background-color',
|
|
||||||
'change-text-color', 'ocr-pdf', 'sign-pdf', 'remove-annotations', 'cropper', 'form-filler',
|
|
||||||
];
|
|
||||||
const simpleTools = ['encrypt', 'decrypt', 'change-permissions', 'pdf-to-markdown', 'word-to-pdf'];
|
|
||||||
|
|
||||||
if (isMultiFileTool) {
|
if (isMultiFileTool) {
|
||||||
handleMultiFileUpload(toolId);
|
handleMultiFileUpload(toolId);
|
||||||
} else if (singlePdfLoadTools.includes(toolId)) {
|
} else if (singlePdfLoadTools.includes(toolId)) {
|
||||||
@@ -512,8 +323,7 @@ export function setupFileInputHandler(toolId: any) {
|
|||||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
processBtn.disabled = false;
|
|
||||||
processBtn.onclick = () => {
|
processBtn.onclick = () => {
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
@@ -522,29 +332,25 @@ export function setupFileInputHandler(toolId: any) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
} else if (toolId === 'edit') {
|
||||||
else if (toolId === 'edit') {
|
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
|
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
|
||||||
const pdfContainer = document.getElementById('embed-pdf-container');
|
const pdfContainer = document.getElementById('embed-pdf-container');
|
||||||
|
|
||||||
pdfContainer.innerHTML = '';
|
pdfContainer.textContent = ''; // Clear safely
|
||||||
|
|
||||||
if (state.currentPdfUrl) {
|
if (state.currentPdfUrl) {
|
||||||
URL.revokeObjectURL(state.currentPdfUrl);
|
URL.revokeObjectURL(state.currentPdfUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfWrapper.classList.remove('hidden');
|
pdfWrapper.classList.remove('hidden');
|
||||||
|
|
||||||
const fileURL = URL.createObjectURL(file);
|
const fileURL = URL.createObjectURL(file);
|
||||||
|
|
||||||
state.currentPdfUrl = fileURL;
|
state.currentPdfUrl = fileURL;
|
||||||
|
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.type = 'module';
|
script.type = 'module';
|
||||||
script.innerHTML = `
|
script.textContent = `
|
||||||
import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js';
|
import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js';
|
||||||
EmbedPDF.init({
|
EmbedPDF.init({
|
||||||
type: 'container',
|
type: 'container',
|
||||||
@@ -558,22 +364,19 @@ export function setupFileInputHandler(toolId: any) {
|
|||||||
const backBtn = document.getElementById('back-to-grid');
|
const backBtn = document.getElementById('back-to-grid');
|
||||||
const urlRevoker = () => {
|
const urlRevoker = () => {
|
||||||
URL.revokeObjectURL(fileURL);
|
URL.revokeObjectURL(fileURL);
|
||||||
state.currentPdfUrl = null; // Clear from state as well
|
state.currentPdfUrl = null;
|
||||||
backBtn.removeEventListener('click', urlRevoker);
|
backBtn.removeEventListener('click', urlRevoker);
|
||||||
};
|
};
|
||||||
backBtn.addEventListener('click', urlRevoker);
|
backBtn.addEventListener('click', urlRevoker);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
fileInput.addEventListener('change', (e) => processFiles(Array.from((e.target as HTMLInputElement).files || [])));
|
||||||
fileInput.addEventListener('change', (e) => processFiles(Array.from(e.target.files)));
|
|
||||||
|
|
||||||
const setupAddMoreButton = () => {
|
const setupAddMoreButton = () => {
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
if (addMoreBtn) {
|
if (addMoreBtn) {
|
||||||
addMoreBtn.addEventListener('click', () => {
|
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -583,25 +386,22 @@ export function setupFileInputHandler(toolId: any) {
|
|||||||
clearBtn.addEventListener('click', () => {
|
clearBtn.addEventListener('click', () => {
|
||||||
state.files = [];
|
state.files = [];
|
||||||
isFirstUpload = true;
|
isFirstUpload = true;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
(fileInput as HTMLInputElement).value = '';
|
||||||
fileInput.value = '';
|
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
if (fileDisplayArea) fileDisplayArea.textContent = '';
|
||||||
|
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
if (fileControls) fileControls.classList.add('hidden');
|
if (fileControls) fileControls.classList.add('hidden');
|
||||||
|
|
||||||
// Clear tool-specific UI
|
|
||||||
const toolSpecificUI = ['file-list', 'page-merge-preview', 'image-list'];
|
const toolSpecificUI = ['file-list', 'page-merge-preview', 'image-list'];
|
||||||
toolSpecificUI.forEach(id => {
|
toolSpecificUI.forEach(id => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.innerHTML = '';
|
if (el) el.textContent = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
if (processBtn) (processBtn as HTMLButtonElement).disabled = true;
|
||||||
if (processBtn) processBtn.disabled = true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,7 +80,22 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
|||||||
if (!file || file.type !== 'application/pdf') return showAlert('Invalid File', 'Please select a valid PDF file.');
|
if (!file || file.type !== 'application/pdf') return showAlert('Invalid File', 'Please select a valid PDF file.');
|
||||||
|
|
||||||
const displayDiv = document.getElementById(displayId);
|
const displayDiv = document.getElementById(displayId);
|
||||||
displayDiv.innerHTML = `<i data-lucide="check-circle" class="w-10 h-10 mb-3 text-green-500"></i><p class="text-sm text-gray-300 truncate">${file.name}</p>`;
|
displayDiv.textContent = '';
|
||||||
|
|
||||||
|
// 2. Create the icon element
|
||||||
|
const icon = document.createElement('i');
|
||||||
|
icon.setAttribute('data-lucide', 'check-circle');
|
||||||
|
icon.className = 'w-10 h-10 mb-3 text-green-500';
|
||||||
|
|
||||||
|
// 3. Create the paragraph element for the file name
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.className = 'text-sm text-gray-300 truncate';
|
||||||
|
|
||||||
|
// 4. Set the file name safely using textContent
|
||||||
|
p.textContent = file.name;
|
||||||
|
|
||||||
|
// 5. Append the safe elements to the container
|
||||||
|
displayDiv.append(icon, p);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
|
|
||||||
grid.innerHTML = '';
|
grid.textContent = '';
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||||
const page = await pdfjsDoc.getPage(i);
|
const page = await pdfjsDoc.getPage(i);
|
||||||
@@ -97,20 +97,39 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
wrapper.dataset.originalPageIndex = i - 1;
|
wrapper.dataset.originalPageIndex = i - 1;
|
||||||
|
|
||||||
wrapper.innerHTML = `
|
const imgContainer = document.createElement('div');
|
||||||
<div class="w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600">
|
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||||
<img src="${canvas.toDataURL()}" class="max-w-full max-h-full object-contain">
|
|
||||||
</div>
|
const img = document.createElement('img');
|
||||||
<span class="page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1">${i}</span>
|
img.src = canvas.toDataURL();
|
||||||
<div class="flex items-center justify-center gap-4">
|
img.className = 'max-w-full max-h-full object-contain';
|
||||||
<button class="duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center" title="Duplicate Page">
|
imgContainer.appendChild(img);
|
||||||
<i data-lucide="copy-plus" class="w-5 h-5"></i>
|
|
||||||
</button>
|
const pageNumberSpan = document.createElement('span');
|
||||||
<button class="delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center" title="Delete Page">
|
pageNumberSpan.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||||
<i data-lucide="x-circle" class="w-5 h-5"></i>
|
pageNumberSpan.textContent = i.toString();
|
||||||
</button>
|
|
||||||
</div>
|
const controlsDiv = document.createElement('div');
|
||||||
`;
|
controlsDiv.className = 'flex items-center justify-center gap-4';
|
||||||
|
|
||||||
|
const duplicateBtn = document.createElement('button');
|
||||||
|
duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||||
|
duplicateBtn.title = 'Duplicate Page';
|
||||||
|
const duplicateIcon = document.createElement('i');
|
||||||
|
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
||||||
|
duplicateIcon.className = 'w-5 h-5';
|
||||||
|
duplicateBtn.appendChild(duplicateIcon);
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||||
|
deleteBtn.title = 'Delete Page';
|
||||||
|
const deleteIcon = document.createElement('i');
|
||||||
|
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
||||||
|
deleteIcon.className = 'w-5 h-5';
|
||||||
|
deleteBtn.appendChild(deleteIcon);
|
||||||
|
|
||||||
|
controlsDiv.append(duplicateBtn, deleteBtn);
|
||||||
|
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
|
||||||
grid.appendChild(wrapper);
|
grid.appendChild(wrapper);
|
||||||
attachEventListeners(wrapper);
|
attachEventListeners(wrapper);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,9 +75,8 @@ async function renderPage() {
|
|||||||
} else if (field instanceof PDFDropdown) {
|
} else if (field instanceof PDFDropdown) {
|
||||||
field.select(fieldValues[fieldName]);
|
field.select(fieldValues[fieldName]);
|
||||||
} else if (field instanceof PDFOptionList) {
|
} else if (field instanceof PDFOptionList) {
|
||||||
// Handle multi-select list box
|
|
||||||
if (Array.isArray(fieldValues[fieldName])) {
|
if (Array.isArray(fieldValues[fieldName])) {
|
||||||
fieldValues[fieldName].forEach((option: any) => field.select(option));
|
(fieldValues[fieldName] as any[]).forEach(option => field.select(option));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -89,7 +88,6 @@ async function renderPage() {
|
|||||||
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes }).promise;
|
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes }).promise;
|
||||||
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
||||||
|
|
||||||
// Use the newer PDF.js render API
|
|
||||||
await tempPage.render({
|
await tempPage.render({
|
||||||
canvasContext: context,
|
canvasContext: context,
|
||||||
viewport: viewport
|
viewport: viewport
|
||||||
@@ -109,10 +107,6 @@ async function renderPage() {
|
|||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to the next or previous page.
|
|
||||||
* @param {number} offset 1 for next, -1 for previous.
|
|
||||||
*/
|
|
||||||
async function changePage(offset: number) {
|
async function changePage(offset: number) {
|
||||||
const newPageNum = currentPageNum + offset;
|
const newPageNum = currentPageNum + offset;
|
||||||
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
|
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
|
||||||
@@ -121,10 +115,6 @@ async function changePage(offset: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the zoom level of the PDF viewer.
|
|
||||||
* @param {number} factor The zoom factor.
|
|
||||||
*/
|
|
||||||
async function setZoom(factor: number) {
|
async function setZoom(factor: number) {
|
||||||
formState.scale = factor;
|
formState.scale = factor;
|
||||||
await renderPage();
|
await renderPage();
|
||||||
@@ -138,7 +128,6 @@ function handleFormChange(event: Event) {
|
|||||||
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
|
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
|
||||||
value = input.checked ? 'on' : 'off';
|
value = input.checked ? 'on' : 'off';
|
||||||
} else if (input instanceof HTMLSelectElement && input.multiple) {
|
} else if (input instanceof HTMLSelectElement && input.multiple) {
|
||||||
// Handle multi-select list box
|
|
||||||
value = Array.from(input.options)
|
value = Array.from(input.options)
|
||||||
.filter(option => option.selected)
|
.filter(option => option.selected)
|
||||||
.map(option => option.value);
|
.map(option => option.value);
|
||||||
@@ -154,57 +143,113 @@ function handleFormChange(event: Event) {
|
|||||||
}, 350);
|
}, 350);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFormFieldHtml(field: any) {
|
function createFormFieldHtml(field: any): HTMLElement {
|
||||||
const name = field.getName();
|
const name = field.getName();
|
||||||
const isRequired = field.isRequired();
|
const isRequired = field.isRequired();
|
||||||
|
|
||||||
const labelText = name.replace(/[_-]/g, ' ');
|
const labelText = name.replace(/[_-]/g, ' ');
|
||||||
const labelHtml = `<label for="field-${name}" class="block text-sm font-medium text-gray-300 capitalize mb-1">${labelText} ${isRequired ? '<span class="text-red-500">*</span>' : ''}</label>`;
|
|
||||||
|
|
||||||
let inputHtml = '';
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.htmlFor = `field-${name}`;
|
||||||
|
label.className = 'block text-sm font-medium text-gray-300 capitalize mb-1';
|
||||||
|
label.textContent = labelText;
|
||||||
|
|
||||||
|
if (isRequired) {
|
||||||
|
const requiredSpan = document.createElement('span');
|
||||||
|
requiredSpan.className = 'text-red-500';
|
||||||
|
requiredSpan.textContent = ' *';
|
||||||
|
label.appendChild(requiredSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.appendChild(label);
|
||||||
|
|
||||||
|
let inputElement: HTMLElement | DocumentFragment;
|
||||||
|
|
||||||
if (field instanceof PDFTextField) {
|
if (field instanceof PDFTextField) {
|
||||||
fieldValues[name] = field.getText() || '';
|
fieldValues[name] = field.getText() || '';
|
||||||
inputHtml = `<input type="text" id="field-${name}" name="${name}" value="${fieldValues[name]}" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">`;
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.id = `field-${name}`;
|
||||||
|
input.name = name;
|
||||||
|
input.value = fieldValues[name];
|
||||||
|
input.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||||
|
inputElement = input;
|
||||||
} else if (field instanceof PDFCheckBox) {
|
} else if (field instanceof PDFCheckBox) {
|
||||||
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
||||||
inputHtml = `<input type="checkbox" id="field-${name}" name="${name}" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500" ${field.isChecked() ? 'checked' : ''}>`;
|
const input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.id = `field-${name}`;
|
||||||
|
input.name = name;
|
||||||
|
input.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||||
|
input.checked = field.isChecked();
|
||||||
|
inputElement = input;
|
||||||
} else if (field instanceof PDFRadioGroup) {
|
} else if (field instanceof PDFRadioGroup) {
|
||||||
fieldValues[name] = field.getSelected();
|
fieldValues[name] = field.getSelected();
|
||||||
const options = field.getOptions();
|
const options = field.getOptions();
|
||||||
inputHtml = options.map((opt: any) => `
|
const fragment = document.createDocumentFragment();
|
||||||
<label class="flex items-center gap-2">
|
options.forEach((opt: string) => {
|
||||||
<input type="radio" name="${name}" value="${opt}" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500" ${opt === field.getSelected() ? 'checked' : ''}>
|
const optionLabel = document.createElement('label');
|
||||||
<span class="text-gray-300 text-sm">${opt}</span>
|
optionLabel.className = 'flex items-center gap-2';
|
||||||
</label>
|
|
||||||
`).join('');
|
const radio = document.createElement('input');
|
||||||
} else if (field instanceof PDFDropdown) {
|
radio.type = 'radio';
|
||||||
fieldValues[name] = field.getSelected();
|
radio.name = name;
|
||||||
const dropdownOptions = field.getOptions();
|
radio.value = opt;
|
||||||
inputHtml = `<select id="field-${name}" name="${name}" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
|
radio.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||||
${dropdownOptions.map((opt: any) => `<option value="${opt}" ${opt === field.getSelected() ? 'selected' : ''}>${opt}</option>`).join('')}
|
if (opt === field.getSelected()) radio.checked = true;
|
||||||
</select>`;
|
|
||||||
} else if (field instanceof PDFOptionList) {
|
const span = document.createElement('span');
|
||||||
|
span.className = 'text-gray-300 text-sm';
|
||||||
|
span.textContent = opt;
|
||||||
|
|
||||||
|
optionLabel.append(radio, span);
|
||||||
|
fragment.appendChild(optionLabel);
|
||||||
|
});
|
||||||
|
inputElement = fragment;
|
||||||
|
} else if (field instanceof PDFDropdown || field instanceof PDFOptionList) {
|
||||||
const selectedValues = field.getSelected();
|
const selectedValues = field.getSelected();
|
||||||
fieldValues[name] = selectedValues;
|
fieldValues[name] = selectedValues;
|
||||||
const listOptions = field.getOptions();
|
const options = field.getOptions();
|
||||||
inputHtml = `<select id="field-${name}" name="${name}" multiple size="${Math.min(10, listOptions.length)}" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 h-auto">
|
|
||||||
${listOptions.map((opt: any) => `<option value="${opt}" ${selectedValues.includes(opt) ? 'selected' : ''}>${opt}</option>`).join('')}
|
const select = document.createElement('select');
|
||||||
</select>`;
|
select.id = `field-${name}`;
|
||||||
} else if (field instanceof PDFSignature) {
|
select.name = name;
|
||||||
inputHtml = `<div class="p-4 bg-gray-800 rounded-lg border border-gray-700"><p class="text-sm text-gray-400">Signature field: Not supported for direct editing.</p></div>`;
|
select.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||||
} else if (field instanceof PDFButton) {
|
|
||||||
inputHtml = `<button type="button" id="field-${name}" class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg">Button: ${labelText}</button>`;
|
if (field instanceof PDFOptionList) {
|
||||||
} else {
|
select.multiple = true;
|
||||||
return `<div class="p-4 bg-gray-800 rounded-lg border border-gray-700"><p class="text-sm text-gray-500">Unsupported field type: ${field.constructor.name}</p></div>`;
|
select.size = Math.min(10, options.length);
|
||||||
|
select.classList.add('h-auto');
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
options.forEach((opt: string) => {
|
||||||
<div class="form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700">
|
const option = document.createElement('option');
|
||||||
${labelHtml}
|
option.value = opt;
|
||||||
${inputHtml}
|
option.textContent = opt;
|
||||||
</div>
|
if (selectedValues.includes(opt)) option.selected = true;
|
||||||
`;
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
inputElement = select;
|
||||||
|
} else {
|
||||||
|
const unsupportedDiv = document.createElement('div');
|
||||||
|
unsupportedDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.className = 'text-sm text-gray-400';
|
||||||
|
if (field instanceof PDFSignature) {
|
||||||
|
p.textContent = 'Signature field: Not supported for direct editing.';
|
||||||
|
} else if (field instanceof PDFButton) {
|
||||||
|
p.textContent = `Button: ${labelText}`;
|
||||||
|
} else {
|
||||||
|
p.textContent = `Unsupported field type: ${field.constructor.name}`;
|
||||||
|
}
|
||||||
|
unsupportedDiv.appendChild(p);
|
||||||
|
return unsupportedDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.appendChild(inputElement);
|
||||||
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupFormFiller() {
|
export async function setupFormFiller() {
|
||||||
@@ -225,24 +270,33 @@ export async function setupFormFiller() {
|
|||||||
const fields = form.getFields();
|
const fields = form.getFields();
|
||||||
formState.fields = fields;
|
formState.fields = fields;
|
||||||
|
|
||||||
|
formContainer.textContent = '';
|
||||||
|
|
||||||
if (fields.length === 0) {
|
if (fields.length === 0) {
|
||||||
formContainer.innerHTML = '<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
formContainer.innerHTML = '<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
||||||
processBtn.classList.add('hidden');
|
processBtn.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
let formHtml = '';
|
|
||||||
|
|
||||||
fields.forEach((field: any) => {
|
fields.forEach((field: any) => {
|
||||||
try {
|
try {
|
||||||
formHtml += createFormFieldHtml(field);
|
const fieldElement = createFormFieldHtml(field);
|
||||||
|
formContainer.appendChild(fieldElement);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`Error processing field "${field.getName()}":`, e);
|
console.error(`Error processing field "${field.getName()}":`, e);
|
||||||
formHtml += `<div class="p-4 bg-gray-800 rounded-lg border border-gray-700"><p class="text-sm text-gray-500">Unsupported field: ${field.getName()}</p><p class="text-xs text-gray-500">${e.message}</p></div>`;
|
const errorDiv = document.createElement('div');
|
||||||
|
errorDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
|
// Sanitize error message display
|
||||||
|
const p1 = document.createElement('p');
|
||||||
|
p1.className = 'text-sm text-gray-500';
|
||||||
|
p1.textContent = `Unsupported field: ${field.getName()}`;
|
||||||
|
const p2 = document.createElement('p');
|
||||||
|
p2.className = 'text-xs text-gray-500';
|
||||||
|
p2.textContent = e.message;
|
||||||
|
errorDiv.append(p1, p2);
|
||||||
|
formContainer.appendChild(errorDiv);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
formContainer.innerHTML = formHtml;
|
|
||||||
processBtn.classList.remove('hidden');
|
processBtn.classList.remove('hidden');
|
||||||
|
|
||||||
formContainer.addEventListener('change', handleFormChange);
|
formContainer.addEventListener('change', handleFormChange);
|
||||||
formContainer.addEventListener('input', handleFormChange);
|
formContainer.addEventListener('input', handleFormChange);
|
||||||
}
|
}
|
||||||
@@ -257,10 +311,10 @@ export async function setupFormFiller() {
|
|||||||
const prevPageBtn = document.getElementById('prev-page');
|
const prevPageBtn = document.getElementById('prev-page');
|
||||||
const nextPageBtn = document.getElementById('next-page');
|
const nextPageBtn = document.getElementById('next-page');
|
||||||
|
|
||||||
if (zoomInBtn) zoomInBtn.onclick = () => setZoom(formState.scale + 0.25);
|
if (zoomInBtn) zoomInBtn.addEventListener('click', () => setZoom(formState.scale + 0.25));
|
||||||
if (zoomOutBtn) zoomOutBtn.onclick = () => setZoom(Math.max(0.25, formState.scale - 0.25));
|
if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => setZoom(Math.max(1, formState.scale - 0.25)));
|
||||||
if (prevPageBtn) prevPageBtn.onclick = () => changePage(-1);
|
if (prevPageBtn) prevPageBtn.addEventListener('click', () => changePage(-1));
|
||||||
if (nextPageBtn) nextPageBtn.onclick = () => changePage(1);
|
if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1));
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
@@ -297,7 +351,6 @@ export async function processAndDownloadForm() {
|
|||||||
} else if (field instanceof PDFDropdown) {
|
} else if (field instanceof PDFDropdown) {
|
||||||
field.select(value);
|
field.select(value);
|
||||||
} else if (field instanceof PDFOptionList) {
|
} else if (field instanceof PDFOptionList) {
|
||||||
// Handle multi-select list box
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach(option => field.select(option));
|
value.forEach(option => field.select(option));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,5 +115,4 @@ const init = () => {
|
|||||||
console.log('Please share our tool and share the love!');
|
console.log('Please share our tool and share the love!');
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- START THE APP ---
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
Reference in New Issue
Block a user