diff --git a/docz/docs/03-demos/19-desktop/01-electron.md b/docz/docs/03-demos/19-desktop/01-electron.md index 711e139..344803e 100644 --- a/docz/docs/03-demos/19-desktop/01-electron.md +++ b/docz/docs/03-demos/19-desktop/01-electron.md @@ -231,6 +231,7 @@ The demo project is wired for `electron-forge` to build the standalone binary. - [`main.js`](pathname:///electron/main.js) : main process script - [`index.html`](pathname:///electron/index.html) : window page - [`index.js`](pathname:///electron/index.js) : script loaded in render context +- [`styles.css`](pathname:///electron/styles.css) : stylesheet :::caution pass @@ -248,6 +249,7 @@ curl -LO https://docs.sheetjs.com/electron/package.json curl -LO https://docs.sheetjs.com/electron/main.js curl -LO https://docs.sheetjs.com/electron/index.html curl -LO https://docs.sheetjs.com/electron/index.js +curl -LO https://docs.sheetjs.com/electron/styles.css ``` :::note pass @@ -265,6 +267,7 @@ curl.exe -LO https://docs.sheetjs.com/electron/package.json curl.exe -LO https://docs.sheetjs.com/electron/main.js curl.exe -LO https://docs.sheetjs.com/electron/index.html curl.exe -LO https://docs.sheetjs.com/electron/index.js +curl.exe -LO https://docs.sheetjs.com/electron/styles.css ``` ::: diff --git a/docz/static/electron/ew.png b/docz/static/electron/ew.png index 67c7b3e..8b9db9f 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 f20cfe4..8fcafe1 100644 --- a/docz/static/electron/index.html +++ b/docz/static/electron/index.html @@ -7,31 +7,37 @@ SheetJS Electron Demo - + + + + -
-SheetJS Electron Demo
-
-
-
Drop a spreadsheet file here to see sheet data
- ... or click here to select a file - -
-

-
-
- + +
+

SheetJS Electron Demo

+

Load a spreadsheet file to see it's contents

+
+
+
+ +

Drag and drop a file here

+

or

+ + +
+
+
+
+

+
+ + + diff --git a/docz/static/electron/index.js b/docz/static/electron/index.js index 35327a2..524953d 100644 --- a/docz/static/electron/index.js +++ b/docz/static/electron/index.js @@ -1,73 +1,283 @@ -/* sheetjs (C) 2013-present SheetJS -- https://sheetjs.com */ const XLSX = require('xlsx'); const electron = require('@electron/remote'); -/* list of supported extensions */ +// --- Supported Extensions --- const EXTENSIONS = "xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers".split("|"); -/* write file with Electron API */ +/** + * 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 o = await electron.dialog.showSaveDialog({ title: 'Save file as', - filters: [{ - name: "Spreadsheets", - extensions: EXTENSIONS - }] + 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); -/* common handler to create HTML tables on the page */ -function process_wb(wb) { - const HTMLOUT = document.getElementById('htmlout'); - const XPORT = document.getElementById('exportBtn'); - XPORT.disabled = false; - HTMLOUT.innerHTML = ""; - wb.SheetNames.forEach(function(sheetName) { - const htmlstr = XLSX.utils.sheet_to_html(wb.Sheets[sheetName],{editable:true}); - HTMLOUT.innerHTML += htmlstr; +/** + * 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); }); + thead.appendChild(headRow); + return thead; } -/* read file with Electron API */ +/** + * 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; +} + +/** + * 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 + 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); + }); + + // Only show tabs if more than one sheet + if (wb.SheetNames.length > 1) { + HTMLOUT.appendChild(tabContainer); + } + HTMLOUT.appendChild(contentContainer); +} + +// --- File Import Logic --- +/** + * Handle file selection dialog and render the selected spreadsheet. + */ async function handleReadBtn() { const o = await electron.dialog.showOpenDialog({ title: 'Select a file', - filters: [{ - name: "Spreadsheets", - extensions: EXTENSIONS - }], + filters: [{ name: "Spreadsheets", extensions: EXTENSIONS }], properties: ['openFile'] }); if(o.filePaths.length == 0) throw new Error("No file was selected!"); - process_wb(XLSX.readFile(o.filePaths[0])); + showSpinner(); + // yield to event loop to render spinner + await new Promise(resolve => setTimeout(resolve, 200)); + try { + const filePath = o.filePaths[0]; + const fileName = filePath.split(/[/\\]/).pop(); + renderWorkbookToTables(XLSX.readFile(filePath)); + showLoadedFileUI(fileName); + } finally { + hideSpinner(); + } } -document.getElementById('readBtn').addEventListener('click', handleReadBtn, false); -/* read file with Web APIs */ +// --- 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) { + return `
+ ${fileName} + +
`; +} + +/** + * Update the drop-container's inner HTML. + */ +function updateDropContainer(html) { + document.getElementById('drop-container').innerHTML = html; +} + +/** + * 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'); + } + // Re-attach event listeners after restoring UI + attachDropListeners(); +} + +/** + * Show UI for loaded file and attach unload handler. + */ +function showLoadedFileUI(fileName) { + updateDropContainer(getFileLoadedHTML(fileName)); + document.getElementById('unloadBtn').addEventListener('click', restoreDropUI); +} + +// --- Event Listener Helpers --- +/** + * Add an event listener to an element if it exists. + */ +function addListener(id, event, handler) { + const el = document.getElementById(id); + if (el) el.addEventListener(event, handler, false); +} + +/** + * Attach drag-and-drop and file input listeners to the UI. + */ +function attachDropListeners() { + 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; + const handleDrag = (e) => { + 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'); + }; + 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'; +} + +// --- File Reader for Drag-and-Drop and Input --- +/** + * Read file(s) from input or drag-and-drop and render as table. + */ async function readFile(files) { + if (!files || files.length === 0) return; const f = files[0]; - const data = await f.arrayBuffer(); - process_wb(XLSX.read(data)); + showSpinner(); + try { + const data = await f.arrayBuffer(); + renderWorkbookToTables(XLSX.read(data)); + showLoadedFileUI(f.name); + } finally { + hideSpinner(); + } } -// file input element -document.getElementById('readIn').addEventListener('change', (e) => { readFile(e.target.files); }, false); - -// drag and drop -const drop = document.getElementById('drop'); -drop.addEventListener('drop', (e) => { - e.stopPropagation(); e.preventDefault(); - readFile(e.dataTransfer.files); -}, false); - -const handleDrag = (e) => { - e.stopPropagation(); e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; -}; -drop.addEventListener('dragenter', handleDrag, false); -drop.addEventListener('dragover', handleDrag, false); +// --- Initial Setup --- +attachDropListeners(); diff --git a/docz/static/electron/styles.css b/docz/static/electron/styles.css new file mode 100644 index 0000000..99405ab --- /dev/null +++ b/docz/static/electron/styles.css @@ -0,0 +1,350 @@ +/* ===================== + Root Variables + ===================== */ +:root { + --text-base: #212529; + --text-muted: #666363; + --text-accent: #0c9244; + --button-primary: #212529; + --button-text: #fff; + --button-primary-hover: #0c9244; + --button-primary-active: #075025; + --button-primary-disabled: #6c757d; + --white: #fff; + --black: #000; + --table-even: #f6f6f6; + --table-hover: #88dda98f; + --table-head: #f8f9faaf; + --danger: #c0392b; +} + +/* ===================== + Global Styles & Reset + ===================== */ +body, html { + width: 100%; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + min-height: 100vh; + width: 100%; + margin: 0; + box-sizing: border-box; + font-family: "Roboto", sans-serif; + font-optical-sizing: auto; + font-style: normal; + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; +} + +li { + list-style: none; +} + +/* ===================== + Typography + ===================== */ +h1, p { + margin: 0; + color: var(--text-base); + text-align: center; +} + +.text-muted { + color: var(--text-muted) !important; +} + +.text-condensed { + font-family: "Roboto Condensed", sans-serif; + font-optical-sizing: auto; + font-style: normal; +} + +.text-small { + font-size: 0.875rem !important; +} + +/* ===================== + Links + ===================== */ +a { + text-decoration: none; + color: var(--text-base); +} +a:hover { + text-decoration: underline; + color: var(--text-accent); +} + +/* ===================== + Header + ===================== */ +header { + margin: 4rem 0; +} + +/* ===================== + Buttons & Inputs + ===================== */ +button, input[type="submit"] { + background-color: var(--button-primary); + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease; + color: var(--button-text); +} +button:hover, input[type="submit"]:hover { + background-color: var(--button-primary-hover); +} +button:active, input[type="submit"]:active { + background-color: var(--button-primary-active); +} +button:disabled, input[type="submit"]:disabled { + background-color: var(--button-primary-disabled); + cursor: not-allowed; + opacity: 0.3; +} +input[type="file"] { + display: none; +} + +button:focus, input[type="submit"]:focus, input[type="file"]:focus { + outline: 3px solid var(--text-accent); + outline-offset: 2px; + box-shadow: 0 0 0 2px var(--text-accent); + z-index: 2; +} + +/* ===================== + File Upload + ===================== */ +.file-upload { + display: flex; + margin: 1rem auto; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + border: 1px dashed var(--text-muted); + padding: 1rem; + width: 75%; + max-width: 600px; + border-radius: 4px; + background-color: var(--white); +} +.file-upload input[type="file"] { + display: none; +} +.file-upload.drag-over { + border-color: var(--text-accent); + background-color: #e6f8ee; + box-shadow: 0 0 0 2px var(--text-accent); + transition: border-color 0.2s, background-color 0.2s, box-shadow 0.2s; +} + +#drop { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +/* ===================== + Export Section + ===================== */ +.export { + margin: 2rem 0; + flex-grow: 1; +} + +/* ===================== + Footer + ===================== */ +footer { + background-color: #eee; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; + position: relative; + bottom: 0; +} +footer ul { + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + width: 100%; +} +footer li { + list-style: none; +} + +/* ===================== + Responsive Table Container + ===================== */ +.table-responsive { + width: 100%; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + margin: 2rem 0; +} + +/* ===================== + SheetJS Table Styles + ===================== */ +.sheetjs-table { + width: 80%; + padding: 1rem; + max-width: 900px; + border-collapse: collapse; + margin: 0 auto; + font-family: "Roboto", "Roboto Condensed", sans-serif; + overflow: hidden; +} +#htmlout { + padding: 1rem; +} +.sheetjs-table caption { + caption-side: top; + font-family: "Roboto Condensed", sans-serif; + font-size: 1.2rem; + color: var(--text-accent); + padding: 0.5rem; + letter-spacing: 0.05em; +} +.sheetjs-table thead { + background: var(--table-head); +} +.sheetjs-table th, .sheetjs-table td { + padding: 0.25rem 1rem; + border: 1px solid var(--text-muted); + text-align: left; + font-size: 1rem; +} +.sheetjs-table 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) { + background: var(--table-even); +} +.sheetjs-table tbody tr:hover { + background: var(--table-hover); +} + +/* ===================== + File Status/Loaded/Unload + ===================== */ +.file-status { + margin-top: 10px; + font-size: 1rem; + color: var(--gray-888); + min-height: 1.5em; + transition: color 0.2s; +} +.file-loaded { + display: flex; + gap: 1rem; + align-items: center; + justify-content: space-between; + padding: 1em; +} +.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; + top: 0; left: 0; + width: 100vw; + height: 100vh; + background: rgba(255,255,255,0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; +} +.spinner { + border: 6px solid #f3f3f3; + border-top: 6px solid var(--text-accent); + border-radius: 50%; + width: 48px; + height: 48px; + animation: spin 1s linear infinite; +} +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* ===================== + Media Queries + ===================== */ +@media (max-width: 700px) { + .sheetjs-table th, .sheetjs-table td { + padding: 0.25rem 0.5rem; + font-size: 0.9rem; + } + .sheetjs-table caption { + font-size: 1rem; + padding: 0.25rem; + } +} + +