From 8c4bd369c458ba75a5f211e6e82ef54d00aa563a Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Sat, 3 May 2025 15:53:11 +0200 Subject: [PATCH] chore/docs: [Electron Demo] - improve code comments and documentation. --- docz/docs/03-demos/19-desktop/01-electron.md | 96 ++++++++++---------- docz/static/electron/index.js | 18 ++-- 2 files changed, 54 insertions(+), 60 deletions(-) diff --git a/docz/docs/03-demos/19-desktop/01-electron.md b/docz/docs/03-demos/19-desktop/01-electron.md index 6e0efee..63d9da3 100644 --- a/docz/docs/03-demos/19-desktop/01-electron.md +++ b/docz/docs/03-demos/19-desktop/01-electron.md @@ -52,8 +52,9 @@ The main process can run any NodeJS code, but it cannot access the DOM or any br To allow communication between the main and renderer processes, Electron recommends building a [context bridge](https://www.electronjs.org/docs/latest/api/context-bridge) to expose low-level system calls and NodeJS APIs to the renderer process. -```js -// preload.js +Exposed APIs are available as `SheetJSDemoAPI` on the window object and proxied from the main process. + +```js title="preload.js -- contextBridge API" const { contextBridge, ipcRenderer, shell } = require('electron'); // import nodejs modules we wish to expose APIs from. const path = require('path'); @@ -95,7 +96,7 @@ For example, assuming a file input element on the page: The event handler would process the event as if it were a web event: -```js +```js title="index.js -- renderer process" async function handleFile(e) { const file = e.target.files[0]; const data = await file.arrayBuffer(); @@ -121,7 +122,7 @@ For example, assuming a DIV on the page: The event handler would process the event as if it were a web event: -```js +```js title="index.js -- renderer process" async function handleDrop(e) { e.stopPropagation(); e.preventDefault(); @@ -143,12 +144,11 @@ document.getElementById("drop").addEventListener("drop", handleDrop, false); We can now use the exposed APIs from our preload script above to show the open dialog and try to parse the workbook from within the renderer process. -```js -// index.js - renderer process - +```js title="index.js -- renderer process" // our exposed bridge APIs are available as SheetJSDemoAPI on the window object const openFile = SheetJSDemoAPI.openFile; // request the open file dialog from the main process // We can also access the SheetJS package from the exposed bridge APIs +// highlight-next-line const XLSX = SheetJSDemoAPI.xlsx; /* this function will show the open dialog and try to parse the workbook */ @@ -164,10 +164,10 @@ async function importFile() { return XLSX.readFile(result.filePaths[0]); } ``` +The xslx package here is being proxied from the main process via the bridge API. The actual implementation of the `openFile` function is handled within the main process in `main.js`. -```js -// main.js - main process +```js title="main.js -- main process" const { ipcMain, dialog } = require('electron'); ipcMain.handle('dialog:openFile', (_e, filters) => @@ -181,8 +181,7 @@ ipcMain.handle('dialog:openFile', (_e, filters) => `showSaveDialog` shows a Save As dialog and returns the selected file name: The implementation for saving files looks very similar to the one above thanks to our bridge API. -```js -// index.js - renderer process +```js title="index.js -- renderer process" // our exposed bridge APIs are available as SheetJSDemoAPI on the window object const saveFile = window.SheetJSDemoAPI.saveFile; // request the save file dialog from the main process const XLSX = window.SheetJSDemoAPI.xlsx; @@ -197,8 +196,7 @@ async function exportFile(workbook) { } ``` And here is the implementation of the `saveFile` function in `main.js`: -```js -// main.js - main process +```js title="main.js -- main process" const { ipcMain, dialog } = require('electron'); ipcMain.handle('dialog:saveFile', (_e, filters) => @@ -206,6 +204,38 @@ ipcMain.handle('dialog:saveFile', (_e, filters) => ); ``` +### Working with OS level file open events. + +The demo has been preconfigured to handle OS level file open events, such as the "open with" context menu or `open` CLI command for all file types SheetJS supports. +In order to pre-register your application as a handler for any other file types, it is necessary to modify the `package.json` file as such. + +```json +// ...existing content + "build": { + "appId": "com.sheetjs.electron", + "fileAssociations": [ + { + "ext": [ // supported extensions to register with the OS. + "xls","xlsx","xlsm","xlsb","xml","csv","txt","dif", + "sylk","slk","prn","ods","fods","htm","html","numbers" + ], + "name": "Spreadsheet / Delimited File", + "description": "Spreadsheets and delimited text files opened by SheetJS-Electron", + "role": "Editor" + } + ], + "mac": { "target": "dmg" }, + "win": { "target": "nsis" }, + "linux": { "target": "deb" } + }, +``` +this snippet makes it possible to generate installers for MacOS, Windows and Linux which will automatically register the application as a handler for the specified file types. + +:::info pass + +It is also possible to open files using the "open with" context menu without registering the application as a handler for the specified file types. This however, requires manually selecting the application binary as a target to open the file with. + +::: ## Complete Example :::note Tested Deployments @@ -215,8 +245,8 @@ This demo was tested in the following environments: | OS and Version | Architecture | Electron | Date | |:---------------|:-------------|:---------|:-----------| | macOS 15.3 | `darwin-x64` | `35.1.2` | 2025-03-31 | -| macOS 14.5 | `darwin-arm` | `35.1.2` | 2025-03-30 | -| Windows 11 | `win11-x64` | `33.2.1` | 2025-02-11 | +| macOS 15.4 | `darwin-arm` | `36.1.0` | 2025-05-03 | +| Windows 11 | `win11-x64` | `36.1.0` | 2025-05-03 | | Windows 11 | `win11-arm` | `33.2.1` | 2025-02-23 | | Linux (HoloOS) | `linux-x64` | `33.2.1` | 2025-01-02 | | Linux (Debian) | `linux-arm` | `33.2.1` | 2025-02-16 | @@ -318,38 +348,6 @@ The program will run on ARM64 Windows. ::: -### Working with OS level file open events. - -The demo has been preconfigured to handle OS level file open events, such as the "open with" context menu or `open` CLI command for all file types SheetJS supports. -In order to pre-register your application as a handler for any other file types, it is necessary to modify the `package.json` file as such. - -```json -// ...existing content - "build": { - "appId": "com.sheetjs.electron", - "fileAssociations": [ - { - "ext": [ // supported extensions to register with the OS. - "xls","xlsx","xlsm","xlsb","xml","csv","txt","dif", - "sylk","slk","prn","ods","fods","htm","html","numbers" - ], - "name": "Spreadsheet / Delimited File", - "description": "Spreadsheets and delimited text files opened by SheetJS-Electron", - "role": "Editor" - } - ], - "mac": { "target": "dmg" }, - "win": { "target": "nsis" }, - "linux": { "target": "deb" } - }, -``` -this snippet makes it possible to generate installers for MacOS, Windows and Linux which will automatically register the application as a handler for the specified file types. - -:::info pass - -It is also possible to open files using the "open with" context menu without registering the application as a handler for the specified file types. This however, requires manually selecting the application binary as a target to open the file with. - -::: ### Testing 5) Download [the test file `pres.numbers`](https://docs.sheetjs.com/pres.numbers) @@ -445,10 +443,10 @@ Electron 12 and later also require `worldSafeExecuteJavascript: true` and Electron 14 and later must use `@electron/remote` instead of `remote`. An `initialize` call is required to enable Developer Tools in the window. +::: + For demos built on top of Electron 36 and later we isolate the processes entirely and the demo no longer requires `@electron/remote`. However, `nodeIntegration: false` by default now means that the renderer process no longer has access to NodeJS APIs. To expose NodeJS APIs to the renderer process, we use the contextBridge API to expose APIs from the main process to the renderer process. [See more](https://www.electronjs.org/docs/latest/api/context-bridge). This has been best practice since Electron 25. -::: - [^1]: See ["Makers"](https://www.electronforge.io/config/makers) in the Electron Forge documentation. On Linux, the demo generates `rpm` and `deb` distributables. On Arch Linux and the Steam Deck, `sudo pacman -Syu rpm-tools dpkg fakeroot` installed required packages. On Debian and Ubuntu, `sudo apt-get install rpm` sufficed. \ No newline at end of file diff --git a/docz/static/electron/index.js b/docz/static/electron/index.js index 365cd91..3b9f870 100644 --- a/docz/static/electron/index.js +++ b/docz/static/electron/index.js @@ -50,14 +50,12 @@ async function exportWorkbookAsFile() { extensions: EXTENSIONS, }, ]); - // -- 2. if canceled or no file path, return if (canceled || !filePath) return; - // -- 3. write workbook to file + // -- 2. write workbook to file try { XLSX.writeFile(currentWorkbook, filePath); window.SheetJSDemoAPI.message(`Exported to ${filePath}`); } catch (err) { - // -- 4. if error, display error displayError(`Failed to export: ${err.message}`); } } @@ -67,18 +65,19 @@ exportBtn.addEventListener("click", exportWorkbookAsFile); // Render workbook --> HTML tables // --------------------------------------------------------------------------- function renderWorkbookToTables(wb) { - // -- 1. convert each sheet to HTML + // -- 1. map through each sheet const html = wb.SheetNames.map((name) => { const sheet = wb.Sheets[name]; + // -- 2. convert sheet to HTML 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 + }).join(""); + // CAUTION!: in production environments please sanitize the HTML output to prevent XSS attacks from maliciously crafted spreadsheets. htmlout.innerHTML = html; // single write → single re‑flow of the DOM + } // --------------------------------------------------------------------------- @@ -133,24 +132,21 @@ async function handleReadBtn() { extensions: EXTENSIONS, }, ]); - // -- 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]; + // -- 2. read the first selected file currentWorkbook = XLSX.readFile(filePath); renderWorkbookToTables(currentWorkbook); showLoadedFileUI(basename(filePath)); } finally { hideSpinner(); hideDropUI(); - // -- 5. reset error UI state onError && (onError.hidden = true); } }