diff --git a/src/js/config/pdf-tools.ts b/src/js/config/pdf-tools.ts new file mode 100644 index 0000000..27e3e78 --- /dev/null +++ b/src/js/config/pdf-tools.ts @@ -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', +]; \ No newline at end of file diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index 6b7038d..6684a46 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -1,5 +1,3 @@ -// FILE: js/handlers/fileHandler.js - import { state } from '../state.js'; import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js'; import { readFileAsArrayBuffer } from '../utils/helpers.js'; @@ -9,12 +7,14 @@ import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.j import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { icons, createIcons } from 'lucide'; 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...'); try { const pdfBytes = await readFileAsArrayBuffer(file); - state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { + state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { ignoreEncryption: true }); hideLoader(); @@ -30,8 +30,7 @@ async function handleSinglePdfUpload(toolId: any, file: any) { const processBtn = document.getElementById('process-btn'); 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.disabled = false; + (processBtn as HTMLButtonElement).disabled = false; processBtn.classList.remove('hidden'); const logic = toolLogic[toolId]; 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)) { - document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount(); + document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString(); } 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 rotateAllRightBtn = document.getElementById('rotate-all-right-btn'); - // Show the buttons 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 => { - // @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.dataset.rotation || '0'); - // Calculate new rotation, ensuring it wraps around 0-270 degrees + const currentRotation = parseInt((item as HTMLElement).dataset.rotation || '0'); const newRotation = (currentRotation + (direction * 90) + 360) % 360; - - // @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; - + (item as HTMLElement).dataset.rotation = newRotation.toString(); const thumbnail = item.querySelector('canvas, img'); if (thumbnail) { - // @ts-expect-error TS(2339) FIXME: Property 'style' does not exist on type 'Element'. - thumbnail.style.transform = `rotate(${newRotation}deg)`; + (thumbnail as HTMLElement).style.transform = `rotate(${newRotation}deg)`; } }); }; - - rotateAllLeftBtn.onclick = () => rotateAll(-1); // -1 for counter-clockwise - rotateAllRightBtn.onclick = () => rotateAll(1); // 1 for clockwise + rotateAllLeftBtn.onclick = () => rotateAll(-1); + rotateAllRightBtn.onclick = () => rotateAll(1); } - } if (toolId === 'duplicate-organize') { @@ -93,9 +83,7 @@ async function handleSinglePdfUpload(toolId: any, file: any) { try { const pdfBytes = await readFileAsArrayBuffer(state.files[0]); - // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'. - const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise; - + const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes as ArrayBuffer }).promise; const [metadata, fieldObjects] = await Promise.all([ pdfjsDoc.getMetadata(), pdfjsDoc.getFieldObjects() @@ -103,7 +91,34 @@ async function handleSinglePdfUpload(toolId: any, file: any) { 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; try { const year = pdfDate.substring(2, 6); @@ -113,91 +128,49 @@ async function handleSinglePdfUpload(toolId: any, file: any) { const minute = pdfDate.substring(12, 14); const second = pdfDate.substring(14, 16); return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString(); - } catch { - return pdfDate; - } + } catch { return pdfDate; } }; - let htmlContent = ''; - - htmlContent += ` -
-

Info Dictionary

-
`; - - htmlContent += ` -
-

Interactive Form Fields

-
`; - - htmlContent += ` -
-

XMP Metadata (Raw XML)

-
`; + resultsDiv.appendChild(fieldsSection.wrapper); + 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) { - const escapedXml = rawXmpString.replace(/&/g, '&').replace(//g, '>'); - htmlContent += `
${escapedXml}
`; + const pre = document.createElement('pre'); + pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all'; + pre.textContent = String(rawXmpString); + xmpContainer.appendChild(pre); } else { - htmlContent += `

- No XMP metadata found -

`; + xmpContainer.innerHTML = `

- No XMP metadata found -

`; } - htmlContent += `
`; + xmpSection.wrapper.appendChild(xmpContainer); + resultsDiv.appendChild(xmpSection.wrapper); - resultsDiv.innerHTML = htmlContent; resultsDiv.classList.remove('hidden'); } 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 = ` - - - - `; - container.appendChild(fieldWrapper); - - createIcons({icons}); // Re-render icons - }; - - createIcons({icons}); - } if (toolId === 'edit-metadata') { const form = document.getElementById('metadata-form'); const container = document.getElementById('custom-metadata-container'); const addBtn = document.getElementById('add-custom-meta-btn'); - // Helper to format Date objects - const formatDateForInput = (date: any) => { + const formatDateForInput = (date) => { 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())}`; }; - - - // Comprehensive decoder for PDF values - const decodePDFValue = (valueNode: any) => { - if (!valueNode) return ''; - - // @ts-expect-error TS(2339) FIXME: Property 'PDFLib' does not exist on type 'Window &... Remove this comment to see the full error message - const { PDFHexString, PDFString, PDFName, PDFNumber } = window.PDFLib; - - 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()); + (document.getElementById('meta-title') as HTMLInputElement).value = state.pdfDoc.getTitle() || ''; + (document.getElementById('meta-author') as HTMLInputElement).value = state.pdfDoc.getAuthor() || ''; + (document.getElementById('meta-subject') as HTMLInputElement).value = state.pdfDoc.getSubject() || ''; + (document.getElementById('meta-keywords') as HTMLInputElement).value = state.pdfDoc.getKeywords() || ''; + (document.getElementById('meta-creator') as HTMLInputElement).value = state.pdfDoc.getCreator() || ''; + (document.getElementById('meta-producer') as HTMLInputElement).value = state.pdfDoc.getProducer() || ''; + (document.getElementById('meta-creation-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getCreationDate()); + (document.getElementById('meta-mod-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getModificationDate()); addBtn.onclick = () => { - const newFieldId = `custom-field-${Date.now()}`; const fieldWrapper = document.createElement('div'); - fieldWrapper.id = newFieldId; fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper'; - fieldWrapper.innerHTML = ` - - - - `; - container.appendChild(fieldWrapper); - createIcons({icons}); + 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 = ''; + removeBtn.addEventListener('click', () => fieldWrapper.remove()); + + fieldWrapper.append(keyInput, valueInput, removeBtn); + container.appendChild(fieldWrapper); + createIcons({ icons }); }; form.classList.remove('hidden'); - - createIcons({icons}); // Render all icons after dynamic changes + createIcons({ icons }); } 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'); 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.disabled = false; + (processBtn as HTMLButtonElement).disabled = false; const logic = toolLogic[toolId]; if (logic) { const func = typeof logic.process === 'function' ? logic.process : logic; @@ -447,15 +263,23 @@ function handleMultiFileUpload(toolId: any) { toolLogic.merge.setup(); } else if (toolId === 'image-to-pdf') { const imageList = document.getElementById('image-list'); - - imageList.innerHTML = ''; + imageList.textContent = ''; // Clear safely state.files.forEach(file => { const url = URL.createObjectURL(file); const li = document.createElement('li'); li.className = "relative group cursor-move"; li.dataset.fileName = file.name; - li.innerHTML = `

${file.name}

`; + + 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); }); @@ -463,14 +287,12 @@ function handleMultiFileUpload(toolId: any) { } } - -export function setupFileInputHandler(toolId: any) { +export function setupFileInputHandler(toolId) { 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); let isFirstUpload = true; - const processFiles = async (newFiles: any) => { + const processFiles = async (newFiles) => { if (newFiles.length === 0) return; if (!isMultiFileTool || isFirstUpload) { @@ -478,7 +300,7 @@ export function setupFileInputHandler(toolId: any) { } else { state.files = [...state.files, ...newFiles]; } - isFirstUpload = false; + isFirstUpload = false; const fileDisplayArea = document.getElementById('file-display-area'); if (fileDisplayArea) { @@ -488,20 +310,9 @@ export function setupFileInputHandler(toolId: any) { const fileControls = document.getElementById('file-controls'); if (fileControls) { 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) { handleMultiFileUpload(toolId); } else if (singlePdfLoadTools.includes(toolId)) { @@ -512,8 +323,7 @@ export function setupFileInputHandler(toolId: any) { if (optionsDiv) optionsDiv.classList.remove('hidden'); const processBtn = document.getElementById('process-btn'); 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.disabled = false; + (processBtn as HTMLButtonElement).disabled = false; processBtn.onclick = () => { const logic = toolLogic[toolId]; if (logic) { @@ -522,29 +332,25 @@ export function setupFileInputHandler(toolId: any) { } }; } - } - else if (toolId === 'edit') { + } else if (toolId === 'edit') { const file = state.files[0]; if (!file) return; const pdfWrapper = document.getElementById('embed-pdf-wrapper'); const pdfContainer = document.getElementById('embed-pdf-container'); - pdfContainer.innerHTML = ''; + pdfContainer.textContent = ''; // Clear safely if (state.currentPdfUrl) { URL.revokeObjectURL(state.currentPdfUrl); } - pdfWrapper.classList.remove('hidden'); - const fileURL = URL.createObjectURL(file); - state.currentPdfUrl = fileURL; const script = document.createElement('script'); script.type = 'module'; - script.innerHTML = ` + script.textContent = ` import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js'; EmbedPDF.init({ type: 'container', @@ -558,22 +364,19 @@ export function setupFileInputHandler(toolId: any) { const backBtn = document.getElementById('back-to-grid'); const urlRevoker = () => { URL.revokeObjectURL(fileURL); - state.currentPdfUrl = null; // Clear from state as well + state.currentPdfUrl = null; backBtn.removeEventListener('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.files))); + fileInput.addEventListener('change', (e) => processFiles(Array.from((e.target as HTMLInputElement).files || []))); const setupAddMoreButton = () => { const addMoreBtn = document.getElementById('add-more-btn'); if (addMoreBtn) { - addMoreBtn.addEventListener('click', () => { - fileInput.click(); - }); + addMoreBtn.addEventListener('click', () => fileInput.click()); } }; @@ -583,31 +386,28 @@ export function setupFileInputHandler(toolId: any) { clearBtn.addEventListener('click', () => { state.files = []; 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.value = ''; - + (fileInput as HTMLInputElement).value = ''; + const fileDisplayArea = document.getElementById('file-display-area'); - if (fileDisplayArea) fileDisplayArea.innerHTML = ''; - + if (fileDisplayArea) fileDisplayArea.textContent = ''; + const fileControls = document.getElementById('file-controls'); if (fileControls) fileControls.classList.add('hidden'); - - // Clear tool-specific UI + const toolSpecificUI = ['file-list', 'page-merge-preview', 'image-list']; toolSpecificUI.forEach(id => { const el = document.getElementById(id); - if (el) el.innerHTML = ''; + if (el) el.textContent = ''; }); - + 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.disabled = true; + if (processBtn) (processBtn as HTMLButtonElement).disabled = true; }); } }; - + setTimeout(() => { setupAddMoreButton(); setupClearButton(); }, 100); -} \ No newline at end of file +} diff --git a/src/js/logic/compare-pdfs.ts b/src/js/logic/compare-pdfs.ts index d7c1d42..ae711b3 100644 --- a/src/js/logic/compare-pdfs.ts +++ b/src/js/logic/compare-pdfs.ts @@ -6,7 +6,7 @@ const state = { pdfDoc1: null, pdfDoc2: null, currentPage: 1, - viewMode: 'overlay', + viewMode: 'overlay', isSyncScroll: true, }; @@ -19,7 +19,7 @@ const state = { */ async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any) { const page = await pdfDoc.getPage(pageNum); - + // Calculate scale to fit the container width. const containerWidth = container.clientWidth - 2; // Subtract border width const viewport = page.getViewport({ scale: 1.0 }); @@ -38,15 +38,15 @@ async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any async function renderBothPages() { if (!state.pdfDoc1 || !state.pdfDoc2) return; - + showLoader(`Loading page ${state.currentPage}...`); - + const canvas1 = document.getElementById('canvas-compare-1'); const canvas2 = document.getElementById('canvas-compare-2'); const panel1 = document.getElementById('panel-1'); const panel2 = document.getElementById('panel-2'); const wrapper = document.getElementById('compare-viewer-wrapper'); - + // Determine the correct container based on the view mode const container1 = state.viewMode === 'overlay' ? wrapper : panel1; const container2 = state.viewMode === 'overlay' ? wrapper : panel2; @@ -55,7 +55,7 @@ async function renderBothPages() { renderPage(state.pdfDoc1, Math.min(state.currentPage, state.pdfDoc1.numPages), canvas1, container1), renderPage(state.pdfDoc2, Math.min(state.currentPage, state.pdfDoc2.numPages), canvas2, container2) ]); - + updateNavControls(); hideLoader(); } @@ -78,11 +78,26 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) { const handleFile = async (file: any) => { if (!file || file.type !== 'application/pdf') return showAlert('Invalid File', 'Please select a valid PDF file.'); - + const displayDiv = document.getElementById(displayId); - displayDiv.innerHTML = `

${file.name}

`; - createIcons({icons}); - + 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 }); + try { showLoader(`Loading ${file.name}...`); const pdfBytes = await readFileAsArrayBuffer(file); @@ -174,7 +189,7 @@ export function setupCompareTool() { const syncToggle = document.getElementById('sync-scroll-toggle'); // @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message syncToggle.addEventListener('change', () => { state.isSyncScroll = syncToggle.checked; }); - + let scrollingPanel: any = null; panel1.addEventListener('scroll', () => { if (state.isSyncScroll && scrollingPanel !== panel2) { diff --git a/src/js/logic/duplicate-organize.ts b/src/js/logic/duplicate-organize.ts index 1a189d6..429e877 100644 --- a/src/js/logic/duplicate-organize.ts +++ b/src/js/logic/duplicate-organize.ts @@ -82,7 +82,7 @@ export async function renderDuplicateOrganizeThumbnails() { // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'. const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise; - grid.innerHTML = ''; + grid.textContent = ''; for (let i = 1; i <= pdfjsDoc.numPages; 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'. wrapper.dataset.originalPageIndex = i - 1; - wrapper.innerHTML = ` -
- -
- ${i} -
- - -
-`; + const imgContainer = document.createElement('div'); + imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600'; + + const img = document.createElement('img'); + img.src = canvas.toDataURL(); + img.className = 'max-w-full max-h-full object-contain'; + imgContainer.appendChild(img); + + const pageNumberSpan = document.createElement('span'); + 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'; + pageNumberSpan.textContent = i.toString(); + + 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); attachEventListeners(wrapper); } diff --git a/src/js/logic/form-filler.ts b/src/js/logic/form-filler.ts index af079c7..596c5c3 100644 --- a/src/js/logic/form-filler.ts +++ b/src/js/logic/form-filler.ts @@ -1,15 +1,15 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; -import { - PDFDocument as PDFLibDocument, - PDFTextField, - PDFCheckBox, - PDFRadioGroup, - PDFDropdown, - PDFButton, - PDFSignature, - PDFOptionList +import { + PDFDocument as PDFLibDocument, + PDFTextField, + PDFCheckBox, + PDFRadioGroup, + PDFDropdown, + PDFButton, + PDFSignature, + PDFOptionList } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; @@ -22,7 +22,7 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( let pdfJsDoc: any = null; let currentPageNum = 1; let pdfRendering = false; -let renderTimeout: any = null; +let renderTimeout: any = null; const formState = { scale: 2, fields: [], @@ -41,14 +41,14 @@ async function renderPage() { const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement; const context = canvas.getContext('2d'); - + if (!context) { console.error('Could not get canvas context'); pdfRendering = false; hideLoader(); return; } - + canvas.height = viewport.height; canvas.width = viewport.width; @@ -75,9 +75,8 @@ async function renderPage() { } else if (field instanceof PDFDropdown) { field.select(fieldValues[fieldName]); } else if (field instanceof PDFOptionList) { - // Handle multi-select list box if (Array.isArray(fieldValues[fieldName])) { - fieldValues[fieldName].forEach((option: any) => field.select(option)); + (fieldValues[fieldName] as any[]).forEach(option => field.select(option)); } } } catch (e) { @@ -89,7 +88,6 @@ async function renderPage() { const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes }).promise; const tempPage = await tempPdfJsDoc.getPage(currentPageNum); - // Use the newer PDF.js render API await tempPage.render({ canvasContext: context, viewport: viewport @@ -104,15 +102,11 @@ async function renderPage() { if (totalPagesDisplay) totalPagesDisplay.textContent = String(pdfJsDoc.numPages); if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1; if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages; - + pdfRendering = false; hideLoader(); } -/** - * Navigates to the next or previous page. - * @param {number} offset 1 for next, -1 for previous. - */ async function changePage(offset: number) { const newPageNum = currentPageNum + offset; 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) { formState.scale = factor; await renderPage(); @@ -134,82 +124,137 @@ function handleFormChange(event: Event) { const input = event.target as HTMLInputElement | HTMLSelectElement; const name = input.name; let value: any; - + if (input instanceof HTMLInputElement && input.type === 'checkbox') { value = input.checked ? 'on' : 'off'; } else if (input instanceof HTMLSelectElement && input.multiple) { - // Handle multi-select list box value = Array.from(input.options) - .filter(option => option.selected) - .map(option => option.value); + .filter(option => option.selected) + .map(option => option.value); } else { value = input.value; } - + fieldValues[name] = value; - + clearTimeout(renderTimeout); renderTimeout = setTimeout(() => { renderPage(); }, 350); } -function createFormFieldHtml(field: any) { +function createFormFieldHtml(field: any): HTMLElement { const name = field.getName(); const isRequired = field.isRequired(); - - const labelText = name.replace(/[_-]/g, ' '); - const labelHtml = ``; + const labelText = name.replace(/[_-]/g, ' '); + + 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; - let inputHtml = ''; - if (field instanceof PDFTextField) { fieldValues[name] = field.getText() || ''; - inputHtml = ``; + 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) { fieldValues[name] = field.isChecked() ? 'on' : 'off'; - inputHtml = ``; + 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) { fieldValues[name] = field.getSelected(); const options = field.getOptions(); - inputHtml = options.map((opt: any) => ` - - `).join(''); - } else if (field instanceof PDFDropdown) { - fieldValues[name] = field.getSelected(); - const dropdownOptions = field.getOptions(); - inputHtml = ``; - } else if (field instanceof PDFOptionList) { + const fragment = document.createDocumentFragment(); + options.forEach((opt: string) => { + const optionLabel = document.createElement('label'); + optionLabel.className = 'flex items-center gap-2'; + + const radio = document.createElement('input'); + radio.type = 'radio'; + radio.name = name; + radio.value = opt; + radio.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500'; + if (opt === field.getSelected()) radio.checked = true; + + 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(); fieldValues[name] = selectedValues; - const listOptions = field.getOptions(); - inputHtml = ``; - } else if (field instanceof PDFSignature) { - inputHtml = `

Signature field: Not supported for direct editing.

`; - } else if (field instanceof PDFButton) { - inputHtml = ``; + const options = field.getOptions(); + + const select = document.createElement('select'); + select.id = `field-${name}`; + select.name = name; + select.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5'; + + if (field instanceof PDFOptionList) { + select.multiple = true; + select.size = Math.min(10, options.length); + select.classList.add('h-auto'); + } + + options.forEach((opt: string) => { + const option = document.createElement('option'); + option.value = opt; + option.textContent = opt; + if (selectedValues.includes(opt)) option.selected = true; + select.appendChild(option); + }); + inputElement = select; } else { - return `

Unsupported field type: ${field.constructor.name}

`; + 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; } - return ` -
- ${labelHtml} - ${inputHtml} -
- `; + wrapper.appendChild(inputElement); + return wrapper; } export async function setupFormFiller() { if (!state.pdfDoc) return; - + showLoader('Analyzing form fields...'); const formContainer = document.getElementById('form-fields-container'); const processBtn = document.getElementById('process-btn'); @@ -225,28 +270,37 @@ export async function setupFormFiller() { const fields = form.getFields(); formState.fields = fields; + formContainer.textContent = ''; + if (fields.length === 0) { - formContainer.innerHTML = '

This PDF contains no form fields.

'; + formContainer.innerHTML = '

This PDF contains no form fields.

'; processBtn.classList.add('hidden'); } else { - let formHtml = ''; - fields.forEach((field: any) => { try { - formHtml += createFormFieldHtml(field); + const fieldElement = createFormFieldHtml(field); + formContainer.appendChild(fieldElement); } catch (e: any) { console.error(`Error processing field "${field.getName()}":`, e); - formHtml += `

Unsupported field: ${field.getName()}

${e.message}

`; + 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'); - formContainer.addEventListener('change', handleFormChange); formContainer.addEventListener('input', handleFormChange); } - + const pdfBytes = await state.pdfDoc.save(); pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise; currentPageNum = 1; @@ -257,13 +311,13 @@ export async function setupFormFiller() { const prevPageBtn = document.getElementById('prev-page'); const nextPageBtn = document.getElementById('next-page'); - if (zoomInBtn) zoomInBtn.onclick = () => setZoom(formState.scale + 0.25); - if (zoomOutBtn) zoomOutBtn.onclick = () => setZoom(Math.max(0.25, formState.scale - 0.25)); - if (prevPageBtn) prevPageBtn.onclick = () => changePage(-1); - if (nextPageBtn) nextPageBtn.onclick = () => changePage(1); - + if (zoomInBtn) zoomInBtn.addEventListener('click', () => setZoom(formState.scale + 0.25)); + if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => setZoom(Math.max(1, formState.scale - 0.25))); + if (prevPageBtn) prevPageBtn.addEventListener('click', () => changePage(-1)); + if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1)); + hideLoader(); - + const formFillerOptions = document.getElementById('form-filler-options'); if (formFillerOptions) formFillerOptions.classList.remove('hidden'); @@ -278,7 +332,7 @@ export async function processAndDownloadForm() { showLoader('Applying form data...'); try { const form = state.pdfDoc.getForm(); - + Object.keys(fieldValues).forEach(fieldName => { try { const field = form.getField(fieldName); @@ -297,7 +351,6 @@ export async function processAndDownloadForm() { } else if (field instanceof PDFDropdown) { field.select(value); } else if (field instanceof PDFOptionList) { - // Handle multi-select list box if (Array.isArray(value)) { value.forEach(option => field.select(option)); } @@ -306,10 +359,10 @@ export async function processAndDownloadForm() { console.error(`Error processing field "${fieldName}" during download:`, e); } }); - + const newPdfBytes = await state.pdfDoc.save(); downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'filled-form.pdf'); - + showAlert('Success', 'Form has been filled and downloaded.'); } catch (e) { diff --git a/src/js/main.ts b/src/js/main.ts index fd2a60b..41d83b6 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -115,5 +115,4 @@ const init = () => { console.log('Please share our tool and share the love!'); }; -// --- START THE APP --- document.addEventListener('DOMContentLoaded', init); \ No newline at end of file