diff --git a/docz/static/electron/ew.png b/docz/static/electron/ew.png index 8b9db9f..5fa2f3f 100644 Binary files a/docz/static/electron/ew.png and b/docz/static/electron/ew.png differ diff --git a/docz/static/electron/index.html b/docz/static/electron/index.html index 8fcafe1..78c6499 100644 --- a/docz/static/electron/index.html +++ b/docz/static/electron/index.html @@ -16,18 +16,18 @@

SheetJS Electron Demo

-

Load a spreadsheet file to see it's contents

+

Load a spreadsheet to view its contents

Drag and drop a file here

or

- - +
-
+
+

diff --git a/docz/static/electron/index.js b/docz/static/electron/index.js index 524953d..1011c16 100644 --- a/docz/static/electron/index.js +++ b/docz/static/electron/index.js @@ -1,132 +1,57 @@ -const XLSX = require('xlsx'); -const electron = require('@electron/remote'); +const XLSX = require("xlsx"); +const electron = require("@electron/remote"); // --- Supported Extensions --- -const EXTENSIONS = "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers".split("|"); +const EXTENSIONS = + "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers".split( + "|" + ); + +const dropContainer = document.getElementById("drop-container"); +const dropzone = document.getElementById("drop"); +const fileStatus = document.getElementById("fileStatus"); +const exportBtn = document.getElementById("exportBtn"); +const spinnerOverlay = document.getElementById("spinner-overlay"); +const htmlout = document.getElementById("htmlout"); + +// open external links in default browser +document.addEventListener("click", (e) => { + if (e.target.tagName === "A" && e.target.href.startsWith("http")) { + e.preventDefault(); + electron.shell.openExternal(e.target.href); + } +}); /** * Export current HTML table as a spreadsheet file using Electron API. */ async function exportFile() { - const HTMLOUT = document.getElementById('htmlout'); - const wb = XLSX.utils.table_to_book(HTMLOUT.getElementsByTagName("TABLE")[0]); + const wb = XLSX.utils.table_to_book(htmlout.getElementsByTagName("TABLE")[0]); const o = await electron.dialog.showSaveDialog({ - title: 'Save file as', - filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }] + title: "Save file as", + filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }], }); XLSX.writeFile(wb, o.filePath); - electron.dialog.showMessageBox({ message: "Exported data to " + o.filePath, buttons: ["OK"] }); -} -document.getElementById('exportBtn').addEventListener('click', exportFile, false); - -/** - * Create a element for a table from a header row array. - */ -function createTableHead(headerRow) { - const thead = document.createElement('thead'); - const headRow = document.createElement('tr'); - headerRow.forEach(cell => { - const th = document.createElement('th'); - th.textContent = cell !== undefined ? cell : ''; - headRow.appendChild(th); + electron.dialog.showMessageBox({ + message: "Exported data to " + o.filePath, + buttons: ["OK"], }); - thead.appendChild(headRow); - return thead; } -/** - * Create a element for a table from a 2D data array. - */ -function createTableBody(data) { - const tbody = document.createElement('tbody'); - for (let i = 1; i < data.length; ++i) { - const row = data[i]; - const tr = document.createElement('tr'); - row.forEach(cell => { - const td = document.createElement('td'); - td.textContent = cell !== undefined ? cell : ''; - tr.appendChild(td); - }); - tbody.appendChild(tr); - } - return tbody; -} +exportBtn.addEventListener("click", exportFile, false); -/** - * Wrap a table in a responsive container div. - */ -function createResponsiveDiv(table) { - const responsiveDiv = document.createElement('div'); - responsiveDiv.className = 'table-responsive'; - responsiveDiv.appendChild(table); - return responsiveDiv; -} - -// --- Main Table Processing --- -/** - * Render all sheets of a workbook as HTML tables with clickable tabs. - */ function renderWorkbookToTables(wb) { - const HTMLOUT = document.getElementById('htmlout'); - const exportBtn = document.getElementById('exportBtn'); - exportBtn.disabled = false; - HTMLOUT.innerHTML = ""; - - // Create tab container - const tabContainer = document.createElement('div'); - tabContainer.className = 'sheetjs-tab-container'; - - // Create content container - const contentContainer = document.createElement('div'); - contentContainer.className = 'sheetjs-tab-content'; - - // Store tables for each sheet - const tables = []; - - wb.SheetNames.forEach(function(sheetName, idx) { - // Create tab button - const tab = document.createElement('button'); - tab.className = 'sheetjs-tab-btn text-small'; - tab.textContent = sheetName; - tab.setAttribute('data-sheet-idx', idx); - if(idx === 0) tab.classList.add('active'); - tab.addEventListener('click', function() { - // Remove active from all tabs - Array.from(tabContainer.children).forEach(btn => btn.classList.remove('active')); - tab.classList.add('active'); - // Hide all tables and show the selected one - tables.forEach((tableDiv, tIdx) => { - tableDiv.style.display = (tIdx === idx) ? '' : 'none'; - }); - }); - tabContainer.appendChild(tab); - - // Create table for this sheet + htmlout.innerHTML = ""; + const sheetNames = wb.SheetNames; + sheetNames.forEach((sheetName) => { const sheet = wb.Sheets[sheetName]; - const data = XLSX.utils.sheet_to_json(sheet, { header: 1 }); - if (data.length === 0) { - const emptyDiv = document.createElement('div'); - emptyDiv.textContent = `Sheet '${sheetName}' is empty.`; - tables.push(emptyDiv); - contentContainer.appendChild(emptyDiv); - return; - } - const table = document.createElement('table'); - table.className = 'sheetjs-table'; - // Remove caption, handled by tab - table.appendChild(createTableHead(data[0])); - table.appendChild(createTableBody(data)); - const responsiveDiv = createResponsiveDiv(table); - if(idx !== 0) responsiveDiv.style.display = 'none'; - tables.push(responsiveDiv); - contentContainer.appendChild(responsiveDiv); + const sheetname = sheetName; + const table = XLSX.utils.sheet_to_html(sheet); + htmlout.innerHTML += `
+ ${sheetname} +
${table}
+
`; }); - - // Only show tabs if more than one sheet - if (wb.SheetNames.length > 1) { - HTMLOUT.appendChild(tabContainer); - } - HTMLOUT.appendChild(contentContainer); } // --- File Import Logic --- @@ -135,79 +60,81 @@ function renderWorkbookToTables(wb) { */ async function handleReadBtn() { const o = await electron.dialog.showOpenDialog({ - title: 'Select a file', + title: "Select a file", filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }], - properties: ['openFile'] + properties: ["openFile"], }); - if(o.filePaths.length == 0) throw new Error("No file was selected!"); + if (o.filePaths.length == 0) throw new Error("No file was selected!"); showSpinner(); // yield to event loop to render spinner - await new Promise(resolve => setTimeout(resolve, 200)); + await new Promise((resolve) => setTimeout(resolve, 200)); try { const filePath = o.filePaths[0]; const fileName = filePath.split(/[/\\]/).pop(); renderWorkbookToTables(XLSX.readFile(filePath)); showLoadedFileUI(fileName); + showExportBtn(); } finally { hideSpinner(); + hideDropUI(); } } // --- UI Templates and Helpers --- -/** - * Return HTML for the drag-and-drop area. - */ -function getDropAreaHTML() { - return `
- -

Drag and drop a file here

-

or

- - -
`; -} -/** - * Return HTML for the file-loaded UI. - */ -function getFileLoadedHTML(fileName) { +const getLoadedFileUI = (fileName) => { return `
- ${fileName} - -
`; -} + ${fileName} + + `; +}; -/** - * Update the drop-container's inner HTML. - */ -function updateDropContainer(html) { - document.getElementById('drop-container').innerHTML = html; -} +const hideDropUI = () => { + if (dropContainer) dropContainer.style.display = "none"; +}; -/** - * Restore the original drag-and-drop UI and reset state. - */ -function restoreDropUI() { - updateDropContainer(getDropAreaHTML()); - // Remove the table/output - const htmlout = document.getElementById('htmlout'); - if (htmlout) htmlout.innerHTML = ''; - // Reset export button state - const exportBtn = document.getElementById('exportBtn'); - if (exportBtn) { - exportBtn.disabled = true; - exportBtn.classList.add('disabled'); +const showDropUI = () => { + if (dropContainer) dropContainer.style.display = "block"; +}; + +const hideLoadedFileUI = () => { + if (fileStatus) fileStatus.innerHTML = ""; +}; + +const hideOutputUI = () => { + if (htmlout) htmlout.innerHTML = ""; +}; + +const showLoadedFileUI = (fileName) => { + const loadedFileUI = getLoadedFileUI(fileName); + fileStatus.innerHTML = loadedFileUI; + const unloadBtn = fileStatus.querySelector("#unloadBtn"); + if (unloadBtn) { + unloadBtn.addEventListener("click", () => { + hideLoadedFileUI(); + hideExportBtn(); + showDropUI(); + hideOutputUI(); + }); } - // Re-attach event listeners after restoring UI - attachDropListeners(); + hideDropUI(); + showExportBtn(); +}; + +const hideExportBtn = () => { + if (exportBtn) exportBtn.disabled = true; +}; + +const showExportBtn = () => { + if (exportBtn) exportBtn.disabled = false; +}; + +function showSpinner() { + if (spinnerOverlay) spinnerOverlay.style.display = "flex"; } -/** - * Show UI for loaded file and attach unload handler. - */ -function showLoadedFileUI(fileName) { - updateDropContainer(getFileLoadedHTML(fileName)); - document.getElementById('unloadBtn').addEventListener('click', restoreDropUI); +function hideSpinner() { + if (spinnerOverlay) spinnerOverlay.style.display = "none"; } // --- Event Listener Helpers --- @@ -223,43 +150,38 @@ function addListener(id, event, handler) { * Attach drag-and-drop and file input listeners to the UI. */ function attachDropListeners() { - addListener('readIn', 'change', (e) => { + addListener("readIn", "change", (e) => { showSpinner(); // Defer to next tick to ensure spinner renders before heavy work setTimeout(() => readFile(e.target.files), 0); }); - addListener('readBtn', 'click', handleReadBtn); - const drop = document.getElementById('drop'); - const dropContainer = document.getElementById('drop-container'); - if (!drop || !dropContainer) return; + addListener("readBtn", "click", handleReadBtn); + if (!dropzone || !dropContainer) return; const handleDrag = (e) => { - e.stopPropagation(); e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; - dropContainer.classList.add('drag-over'); + e.stopPropagation(); + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + dropContainer.classList.add("drag-over"); }; const handleDragLeave = (e) => { - e.stopPropagation(); e.preventDefault(); - dropContainer.classList.remove('drag-over'); + e.stopPropagation(); + e.preventDefault(); + dropContainer.classList.remove("drag-over"); }; - drop.addEventListener('drop', (e) => { - e.stopPropagation(); e.preventDefault(); - dropContainer.classList.remove('drag-over'); - readFile(e.dataTransfer.files); - }, false); - drop.addEventListener('dragenter', handleDrag, false); - drop.addEventListener('dragover', handleDrag, false); - drop.addEventListener('dragleave', handleDragLeave, false); - drop.addEventListener('dragend', handleDragLeave, false); -} - -// --- Spinner Helpers --- -function showSpinner() { - const spinner = document.getElementById('spinner-overlay'); - if (spinner) spinner.style.display = 'flex'; -} -function hideSpinner() { - const spinner = document.getElementById('spinner-overlay'); - if (spinner) spinner.style.display = 'none'; + dropzone.addEventListener( + "drop", + (e) => { + e.stopPropagation(); + e.preventDefault(); + dropContainer.classList.remove("drag-over"); + readFile(e.dataTransfer.files); + }, + false + ); + dropzone.addEventListener("dragenter", handleDrag, false); + dropzone.addEventListener("dragover", handleDrag, false); + dropzone.addEventListener("dragleave", handleDragLeave, false); + dropzone.addEventListener("dragend", handleDragLeave, false); } // --- File Reader for Drag-and-Drop and Input --- diff --git a/docz/static/electron/styles.css b/docz/static/electron/styles.css index 99405ab..f9c01a4 100644 --- a/docz/static/electron/styles.css +++ b/docz/static/electron/styles.css @@ -85,7 +85,7 @@ a:hover { Header ===================== */ header { - margin: 4rem 0; + margin-top: 2rem; } /* ===================== @@ -197,13 +197,12 @@ footer li { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; - margin: 2rem 0; } /* ===================== SheetJS Table Styles ===================== */ -.sheetjs-table { +table { width: 80%; padding: 1rem; max-width: 900px; @@ -211,11 +210,10 @@ footer li { margin: 0 auto; font-family: "Roboto", "Roboto Condensed", sans-serif; overflow: hidden; + margin-bottom: 2rem; } -#htmlout { - padding: 1rem; -} -.sheetjs-table caption { + +table caption { caption-side: top; font-family: "Roboto Condensed", sans-serif; font-size: 1.2rem; @@ -223,35 +221,54 @@ footer li { padding: 0.5rem; letter-spacing: 0.05em; } -.sheetjs-table thead { + +table thead { background: var(--table-head); } -.sheetjs-table th, .sheetjs-table td { + +table th, table td { padding: 0.25rem 1rem; border: 1px solid var(--text-muted); text-align: left; font-size: 1rem; } -.sheetjs-table th { + +th { color: var(--text-base); font-family: "Roboto Condensed", sans-serif; font-weight: 600; letter-spacing: 0.03em; } -.sheetjs-table tbody tr:nth-child(even) { + +table tbody tr:nth-child(even) { background: var(--table-even); } -.sheetjs-table tbody tr:hover { + +table tbody tr:hover { background: var(--table-hover); } +.sheetjs-sheet-name { + font-family: "Roboto Condensed", sans-serif; + color: var(--text-accent); + padding: 0.5rem; + font-weight: 600; +} + +.sheetjs-sheet-container { + margin: 1rem auto; + width: 90%; + max-width: 900px; +} + + + /* ===================== File Status/Loaded/Unload ===================== */ .file-status { margin-top: 10px; font-size: 1rem; - color: var(--gray-888); min-height: 1.5em; transition: color 0.2s; } @@ -259,55 +276,14 @@ footer li { display: flex; gap: 1rem; align-items: center; - justify-content: space-between; - padding: 1em; + justify-content: center; + margin-bottom: 1rem; } .unload-btn:hover { background: var(--danger); color: var(--white); } -/* ===================== - Sheet Tabs - ===================== */ -.sheetjs-tab-container { - display: flex; - gap: 0.5rem; - margin: 1rem auto 0.5rem auto; - justify-content: center; -} -.sheetjs-tab-btn { - background: var(--button-primary); - border: none; - padding: 0.5rem 1.2rem; - border-radius: 6px 6px 0 0; - font-family: "Roboto Condensed", sans-serif; - font-size: 1rem; - color: var(--button-text); - cursor: pointer; - transition: background 0.18s, color 0.18s, box-shadow 0.18s; - outline: none; - position: relative; - top: 2px; - box-shadow: 0 2px 6px rgba(12, 146, 68, 0.05); - opacity: 0.94; -} -.sheetjs-tab-btn.active, .sheetjs-tab-btn:focus { - background: var(--text-accent); - color: var(--white); - z-index: 2; - opacity: 1; - box-shadow: 0 4px 12px rgba(12, 146, 68, 0.12); -} -.sheetjs-tab-btn:hover:not(.active) { - background: var(--button-primary-hover); - color: var(--white); - opacity: 1; -} -.sheetjs-tab-content { - width: 100%; -} - /* Spinner Styles */ .spinner-overlay { position: fixed; @@ -328,6 +304,7 @@ footer li { height: 48px; animation: spin 1s linear infinite; } + @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } @@ -337,11 +314,11 @@ footer li { Media Queries ===================== */ @media (max-width: 700px) { - .sheetjs-table th, .sheetjs-table td { + table th, table td { padding: 0.25rem 0.5rem; font-size: 0.9rem; } - .sheetjs-table caption { + table caption { font-size: 1rem; padding: 0.25rem; }