diff --git a/docz/docs/03-demos/19-desktop/01-electron.md b/docz/docs/03-demos/19-desktop/01-electron.md
index 711e139..a021065 100644
--- a/docz/docs/03-demos/19-desktop/01-electron.md
+++ b/docz/docs/03-demos/19-desktop/01-electron.md
@@ -40,22 +40,47 @@ app to read and write workbooks. The app will look like the screenshots below:
## Integration Details
-The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be
-imported from the main or the renderer thread.
+Electron uses a multi-process architecture, with the main process handling system level operations and I/O, and the renderer process handling UI and web content.
-The SheetJS `readFile` and `writeFile` methods will use the Electron `fs` module
-where available.
+**Renderer Process Limitations**
-
- Renderer Configuration (click to show)
+The renderer process is sandboxed and cannot run any non-browser code.
-Electron 9 and later require the preference `nodeIntegration: true` in order to
-`require('xlsx')` in the renderer process.
+**Main Process Limitations**
-Electron 12 and later also require `worldSafeExecuteJavascript: true` and
-`contextIsolation: true`.
+The main process can run any NodeJS code, but it cannot access the DOM or any browser APIs.
-
+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. Such as the [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) which we will be using here.
+
+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');
+const XLSX = require('xlsx');
+
+// The contextBridge API allows us to expose APIs to the renderer process.
+// highlight-next-line
+contextBridge.exposeInMainWorld('SheetJSDemoAPI', {
+ // request OS file dialogs from the main process
+ openFile: (filters) => ipcRenderer.invoke('dialog:openFile', filters),
+ saveFile: (filters) => ipcRenderer.invoke('dialog:saveFile', filters),
+ message: (msg) => ipcRenderer.invoke('dialog:message', msg),
+ // open external links in the default browser
+ openExternal: (url) => shell.openExternal(url),
+ // listen for file open events from the main process
+ onFileOpened: (cb) => ipcRenderer.on('file-opened', (_e, fp) => cb(fp)),
+
+ // You can use this to expose nodejs APIs to the renderer process.
+ basename: (p) => path.basename(p),
+ extname: (p) => path.extname(p),
+
+ // Here for example we are exposing the sheetjs package to the renderer process.
+ // highlight-next-line
+ xlsx: XLSX,
+});
+```
### Reading Files
@@ -73,7 +98,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();
@@ -87,8 +112,9 @@ document.getElementById("xlf").addEventListener("change", handleFile, false);
**Drag and Drop**
-The [drag and drop snippet](/docs/solutions/input#example-user-submissions)
-applies to DIV elements on the page.
+In the demo the [drag and drop snippet](/docs/solutions/input#example-user-submissions)
+applies to the entire window via the `document.body` element. However it can easily be
+applied to any element on the page.
For example, assuming a DIV on the page:
@@ -98,7 +124,9 @@ 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"
+const XLSX = window.SheetJSDemoAPI.xlsx; // use xlsx package from bridge process
+
async function handleDrop(e) {
e.stopPropagation();
e.preventDefault();
@@ -117,88 +145,105 @@ document.getElementById("drop").addEventListener("drop", handleDrop, false);
[`XLSX.readFile`](/docs/api/parse-options) reads workbooks from the file system.
`showOpenDialog` shows a Save As dialog and returns the selected file name.
-Unlike the Web APIs, the `showOpenDialog` flow can be initiated by app code:
-```js
-/* from the renderer thread */
-const electron = require('@electron/remote');
+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 title="index.js -- renderer process"
+// our exposed bridge APIs are available as SheetJSDemoAPI on the window object
+const openFile = window.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 = window.SheetJSDemoAPI.xlsx;
/* this function will show the open dialog and try to parse the workbook */
async function importFile() {
- /* show Save As dialog */
- const result = await electron.dialog.showOpenDialog({
- title: 'Select a file',
- filters: [{
+ /* show open file dialog */
+ const result = await openFile([{
name: "Spreadsheets",
extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */]
- }]
- });
+ }]);
/* result.filePaths is an array of selected files */
if(result.filePaths.length == 0) throw new Error("No file was selected!");
// highlight-next-line
return XLSX.readFile(result.filePaths[0]);
}
```
+In order to interact with the file system, the `xlsx` package here depends on the Node.js. Which means we need to utilize the Bridge here and make it possible to call these methods from the renderer process. The appropriate IPC event can be found below.
-:::note pass
+```js title="main.js -- main process"
+const { ipcMain, dialog } = require('electron');
-`showOpenDialog` originally returned an array of paths:
-
-```js
-var dialog = require('electron').remote.dialog;
-
-function importFile(workbook) {
- var result = dialog.showOpenDialog({ properties: ['openFile'] });
- return XLSX.readFile(result[0]);
-}
+ipcMain.handle('dialog:openFile', (_e, filters) =>
+ dialog.showOpenDialog({ title: 'Select a file', filters, properties: ['openFile'] })
+);
```
-This method was renamed to `showOpenDialogSync` in Electron 6.
-
-:::
-
### Writing Files
[`XLSX.writeFile`](/docs/api/write-options) writes workbooks to the file system.
`showSaveDialog` shows a Save As dialog and returns the selected file name:
-```js
-/* from the renderer thread */
-const electron = require('@electron/remote');
+The implementation for saving files looks very similar to the one above thanks to our bridge API.
+```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;
-/* this function will show the save dialog and try to write the workbook */
async function exportFile(workbook) {
- /* show Save As dialog */
- const result = await electron.dialog.showSaveDialog({
- title: 'Save file as',
- filters: [{
+ const result = await saveFile([{
name: "Spreadsheets",
extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */]
- }]
- });
- /* write file */
- // highlight-next-line
- XLSX.writeFile(workbook, result.filePath);
+ }]);
+ if(result.filePaths.length == 0) throw new Error("No file was selected!");
+ XLSX.writeFile(workbook, result.filePaths[0]);
}
```
+And here is the implementation of the `saveFile` event listener in `main.js`:
+```js title="main.js -- main process"
+const { ipcMain, dialog } = require('electron');
-:::note pass
-
-`showSaveDialog` originally returned the selected path:
-
-```js
-var dialog = require('electron').remote.dialog;
-
-function exportFile(workbook) {
- var result = dialog.showSaveDialog();
- XLSX.writeFile(workbook, result);
-}
+ipcMain.handle('dialog:saveFile', (_e, filters) =>
+ dialog.showSaveDialog({ title: 'Save file as', filters })
+);
```
-This method was renamed to `showSaveDialogSync` in Electron 6.
+### Working with OS level file open events.
+Electron makes it possible to handle OS level file open events, such as the "open with" context menu or `open` CLI command.
+
+The example below shows the configuration required to register your application as a handler supporting such events for all file extensions SheetJS supports.
+
+:::caution
+
+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.
+
+**This action might not be supported by some file managers on Linux based systems.**
:::
+```json title="package.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 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 avoiding manual registration processes that differ across operating systems.
+
## Complete Example
:::note Tested Deployments
@@ -208,29 +253,26 @@ 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 |
:::
-This demo includes a drag-and-drop box as well as a file input box, mirroring
-the [SheetJS Data Preview Live Demo](https://oss.sheetjs.com/sheetjs/)
-
-The core data in this demo is an editable HTML table. The readers build up the
-table using `sheet_to_html` (with `editable:true` option) and the writers scrape
-the table using `table_to_book`.
-
The demo project is wired for `electron-forge` to build the standalone binary.
+You can also use `electron-builder` to build a packaged installer binary.
+
1) Download the demo files:
- [`package.json`](pathname:///electron/package.json) : project structure
- [`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
+- [`preload.js`](pathname:///electron/preload.js) : preload script (ContextBridge API worker)
+- [`styles.css`](pathname:///electron/styles.css) : stylesheet
:::caution pass
@@ -248,6 +290,8 @@ 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/preload.js
+curl -LO https://docs.sheetjs.com/electron/styles.css
```
:::note pass
@@ -265,6 +309,8 @@ 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/preload.js
+curl.exe -LO https://docs.sheetjs.com/electron/styles.css
```
:::
@@ -322,12 +368,12 @@ The program will run on ARM64 Windows.
#### Electron API
-7) Click "Click here to select a file from your computer". With the file picker,
+7) Click "Click here to select a file. With the file picker,
navigate to the Downloads folder and select `pres.numbers`.
-The application should show data in a table.
+The application should show a dropdown component for each worksheet contained in your file, clicking on it should display its data within a table.
-8) Click "Export Data!" and click "Save" in the popup. By default, it will try
+8) Click "Export" and click "Save" in the popup. By default, it will try
to write to `Untitled.xls` in the Downloads folder.
:::note pass
@@ -341,27 +387,51 @@ If there is no default name, enter `Untitled.xls` and click "Save".
The app will show a popup once the data is exported. Open the file in a
spreadsheet editor and compare the data to the table shown in the application.
-#### Drag and Drop
-
+#### Open with menu
9) Close the application, end the terminal process and re-launch (see step 6)
10) Open the Downloads folder in a file explorer or finder window.
-11) Click and drag the `pres.numbers` file from the Downloads folder to the
-bordered "Drop a spreadsheet file" box. The file data should be displayed.
+11) Right-click the `pres.numbers` file and select "Open with".
-#### File Input Element
+12) Select your application binary by navigating to the folder where the application was built (see step 4).
-12) Close the application, end the terminal process and re-launch (see step 6)
+:::info
+ On some Linux based systems, depending on the file manager in use selecting the binary directly may not be possible.
+:::
-13) Click "Choose File". With the file picker, navigate to the Downloads folder
+
+The application should show a dropdown component for each worksheet contained in your file, clicking on it should display its data within a table.
+
+#### Drag and Drop
+
+13) Close the application, end the terminal process and re-launch (see step 6)
+
+14) Open the Downloads folder in a file explorer or finder window.
+
+15) Click and drag the `pres.numbers` file from the Downloads folder
+into the application window.
+
+The application should show a dropdown component for each worksheet contained in your file, clicking on it should display its data within a table.
+
+:::info
+ On some Linux based systems, the experience can differ depending on the window manager / desktop environment in use.
+:::
+
+#### File Picker Element
+
+16) Close the application, end the terminal process and re-launch (see step 6)
+
+17) Click "Choose File". With the file picker, navigate to the Downloads folder
and select `pres.numbers`.
+The application should show a dropdown component for each worksheet contained in your file, clicking on it should display its data within a table.
+
## Electron Breaking Changes
The first version of this demo used Electron `1.7.5`. The current demo includes
-the required changes for Electron `35.1.2`.
+the required changes for Electron `36.1.0`.
There are no Electron-specific workarounds in the library, but Electron broke
backwards compatibility multiple times. A summary of changes is noted below.
@@ -389,4 +459,8 @@ Electron 14 and later must use `@electron/remote` instead of `remote`. An
:::
+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/ew.png b/docz/static/electron/ew.png
index 67c7b3e..e45ab15 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..cb3fd57 100644
--- a/docz/static/electron/index.html
+++ b/docz/static/electron/index.html
@@ -1,37 +1,43 @@
-