const XLSX = require("xlsx"); // TODO: Replace deprecated @electron/remote with contextBridge‑based IPC in production. const electron = require("@electron/remote"); const { ipcRenderer } = require("electron"); const path = require("path"); // --------------------------------------------------------------------------- // Supported file extensions // --------------------------------------------------------------------------- const EXTENSIONS = "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers".split( "|", ); // --------------------------------------------------------------------------- // DOM references // --------------------------------------------------------------------------- const dropContainer = document.getElementById("drop-container"); const fileStatus = document.getElementById("fileStatus"); const exportBtn = document.getElementById("exportBtn"); const spinnerOverlay = document.getElementById("spinner-overlay"); const htmlout = document.getElementById("htmlout"); const onError = document.getElementById("onError"); // --------------------------------------------------------------------------- // State & helpers // --------------------------------------------------------------------------- let currentWorkbook = null; // SheetJS workbook in memory const isSpreadsheet = (ext) => EXTENSIONS.includes(ext.toLowerCase()); const nextPaint = () => new Promise(requestAnimationFrame); // --------------------------------------------------------------------------- // Open external links in default browser (security) // --------------------------------------------------------------------------- document.addEventListener("click", (e) => { if (e.target.tagName === "A" && e.target.href.startsWith("http")) { e.preventDefault(); electron.shell.openExternal(e.target.href); } }); // --------------------------------------------------------------------------- // Export logic – uses cached workbook (no DOM traversal) // --------------------------------------------------------------------------- async function exportWorkbookAsFile() { if (!currentWorkbook) return displayError("No workbook loaded!"); // -- 1. use electron save as dialog to get file path const { filePath, canceled } = await electron.dialog.showSaveDialog({ title: "Save file as", filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }], }); // -- 2. if canceled or no file path, return if (canceled || !filePath) return; // -- 3. write workbook to file try { XLSX.writeFile(currentWorkbook, filePath); electron.dialog.showMessageBox({ message: `Exported to ${filePath}` }); } catch (err) { // -- 4. if error, display error displayError(`Failed to export: ${err.message}`); } } exportBtn.addEventListener("click", exportWorkbookAsFile); // --------------------------------------------------------------------------- // Render workbook --> HTML tables // --------------------------------------------------------------------------- function renderWorkbookToTables(wb) { // -- 1. convert each sheet to HTML const html = wb.SheetNames.map((name) => { const sheet = wb.Sheets[name]; const table = XLSX.utils.sheet_to_html(sheet, { id: `${name}-tbl` }); // -- 2. wrap in details element return `
${name}
${table}
`; }).join(""); // -- 3. join into single string // -- 4. render to DOM htmlout.innerHTML = html; // single write → single re‑flow of the DOM } // --------------------------------------------------------------------------- // Generic UI helpers // --------------------------------------------------------------------------- const displayError = (msg) => (onError ? ((onError.textContent = msg), (onError.hidden = false)) : console.error(msg)); const hideDropUI = () => dropContainer && (dropContainer.style.display = "none"); const showDropUI = () => dropContainer && (dropContainer.style.display = "block"); const hideExportBtn = () => (exportBtn.disabled = true); const showExportBtn = () => (exportBtn.disabled = false); const showSpinner = () => (spinnerOverlay.style.display = "flex"); const hideSpinner = () => (spinnerOverlay.style.display = "none"); const hideOutputUI = () => (htmlout.innerHTML = ""); const hideLoadedFileUI = () => (fileStatus.innerHTML = ""); const getLoadedFileUI = (fileName) => `
${fileName}
`; function showLoadedFileUI(fileName) { fileStatus.innerHTML = getLoadedFileUI(fileName); hideDropUI(); showExportBtn(); } // --------------------------------------------------------------------------- // Event delegation for unload button – avoids per‑render listener leaks // --------------------------------------------------------------------------- fileStatus.addEventListener("click", (e) => { if (e.target.classList.contains("unload-btn")) { hideLoadedFileUI(); hideExportBtn(); showDropUI(); hideOutputUI(); currentWorkbook = null; } }); // --------------------------------------------------------------------------- // File‑open dialog handler // --------------------------------------------------------------------------- async function handleReadBtn() { // -- 1. show file open dialog to get the file path const { filePaths, canceled } = await electron.dialog.showOpenDialog({ title: "Select a file", filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }], properties: ["openFile"], }); // -- 2. if canceled or no file path, return if (canceled || !filePaths.length) return; // -- 3. if multiple files selected, return error if (filePaths.length !== 1) return displayError("Please choose a single file."); showSpinner(); await nextPaint(); // ensure spinner paints try { // -- 4. read the first selected file const filePath = filePaths[0]; currentWorkbook = XLSX.readFile(filePath); renderWorkbookToTables(currentWorkbook); showLoadedFileUI(path.basename(filePath)); } finally { hideSpinner(); hideDropUI(); // -- 5. reset error UI state onError && (onError.hidden = true); } } // --------------------------------------------------------------------------- // Drag‑and‑drop + file input // --------------------------------------------------------------------------- function addListener(id, evt, fn) { const el = document.getElementById(id); if (el) el.addEventListener(evt, fn); } function attachFileListeners() { // file input element addListener("readIn", "change", (e) => { showSpinner(); nextPaint().then(() => readFile(e.target.files)); }); addListener("readBtn", "click", handleReadBtn); // drag‑and‑drop (applied to whole window for simplicity) const onDrag = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; }; ["dragenter", "dragover"].forEach((t) => document.body.addEventListener(t, onDrag, { passive: false }) ); document.body.addEventListener( "drop", (e) => { e.preventDefault(); readFile(e.dataTransfer.files).catch((err) => displayError(err.message)); }, { passive: false } ); } // --------------------------------------------------------------------------- // Read File from input or DnD // --------------------------------------------------------------------------- async function readFile(files) { // -- 1. if no files, return if (!files || !files.length) return; // -- 2. get the first file const file = files[0]; // -- 3. if not a spreadsheet, return error const ext = path.extname(file.name).slice(1); if (!isSpreadsheet(ext)) return displayError(`Unsupported file type .${ext}`); showSpinner(); try { // -- 4. read the file const data = await file.arrayBuffer(); currentWorkbook = XLSX.read(data); // -- 5. render the workbook to tables renderWorkbookToTables(currentWorkbook); // -- 6. show the loaded file UI showLoadedFileUI(file.name); } finally { hideSpinner(); // reset error UI state onError && (onError.hidden = true); } } // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- attachFileListeners(); // the file-opened event is sent from the main process when a file is opened using "open with" ipcRenderer.on("file-opened", async (_e, filePath) => { showSpinner(); await nextPaint(); // ensure spinner paints currentWorkbook = XLSX.readFile(filePath); renderWorkbookToTables(currentWorkbook); showLoadedFileUI(path.basename(filePath)); hideSpinner(); hideDropUI(); showExportBtn(); });