diff --git a/docz/docs/03-demos/19-desktop/01-electron.md b/docz/docs/03-demos/19-desktop/01-electron.md
index 8c3c2f2..6e0efee 100644
--- a/docz/docs/03-demos/19-desktop/01-electron.md
+++ b/docz/docs/03-demos/19-desktop/01-electron.md
@@ -40,22 +40,44 @@ 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.
+
+```js
+// preload.js
+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.
+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.
+ xlsx: XLSX,
+});
+```
### Reading Files
@@ -118,88 +140,72 @@ 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:
+
+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
-/* from the renderer thread */
-const electron = require('@electron/remote');
+// 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
+const XLSX = 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]);
}
```
-
-:::note pass
-
-`showOpenDialog` originally returned an array of paths:
+The actual implementation of the `openFile` function is handled within the main process in `main.js`.
```js
-var dialog = require('electron').remote.dialog;
+// main.js - main process
+const { ipcMain, dialog } = require('electron');
-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:
+The implementation for saving files looks very similar to the one above thanks to our bridge API.
```js
-/* from the renderer thread */
-const electron = require('@electron/remote');
+// 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]);
}
```
-
-:::note pass
-
-`showSaveDialog` originally returned the selected path:
-
+And here is the implementation of the `saveFile` function in `main.js`:
```js
-var dialog = require('electron').remote.dialog;
+// main.js - main process
+const { ipcMain, dialog } = require('electron');
-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.
-
-:::
-
## Complete Example
:::note Tested Deployments
@@ -217,21 +223,17 @@ This demo was tested in the following environments:
:::
-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
@@ -250,6 +252,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/preload.js
curl -LO https://docs.sheetjs.com/electron/styles.css
```
@@ -268,6 +271,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/preload.js
curl.exe -LO https://docs.sheetjs.com/electron/styles.css
```
@@ -293,8 +297,13 @@ The app will run.
```bash
npm run make
```
+or
+```bash
+npm run dist
+```
+if you want to generate an installer binary.
-This will create a package in the `out\make` folder and a standalone binary.
+This will create a package in the `out\make` folder and a standalone binary, or an installer binary in `/dist` if you used `npm run dist`.
:::caution pass
@@ -308,9 +317,11 @@ When the demo was last tested on Windows ARM, the generated binary targeted x64.
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 register your application as a handler for any other file types, it is necessary to modify the `package.json` file as such.
+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
@@ -334,10 +345,6 @@ In order to register your application as a handler for any other file types, it
```
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.
-```sh
-npm run dist # generate installers for macos, windows and linux
-```
-
:::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.
@@ -365,7 +372,7 @@ navigate to the Downloads folder and select `pres.numbers`.
The application should show data in 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
@@ -379,27 +386,40 @@ 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
+11) Right-click the `pres.numbers` file and select "Open with".
+
+12) Select your application binary by navigating to the folder where the application was built (see step 4).
+
+
+
+The application should show data in 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 to the
bordered "Drop a spreadsheet file" box. The file data should be displayed.
#### File Input Element
-12) Close the application, end the terminal process and re-launch (see step 6)
+16) Close the application, end the terminal process and re-launch (see step 6)
-13) Click "Choose File". With the file picker, navigate to the Downloads folder
+17) Click "Choose File". With the file picker, navigate to the Downloads folder
and select `pres.numbers`.
## 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.
@@ -425,6 +445,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 9cf5f8c..365cd91 100644
--- a/docz/static/electron/index.js
+++ b/docz/static/electron/index.js
@@ -1,33 +1,32 @@
-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");
+const XLSX = window.SheetJSDemoAPI.xlsx;
+const basename = window.SheetJSDemoAPI.basename;
+const extname = window.SheetJSDemoAPI.extname;
+const onFileOpened = window.SheetJSDemoAPI.onFileOpened;
// ---------------------------------------------------------------------------
// 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 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");
+const htmlout = document.getElementById("htmlout");
+const onError = document.getElementById("onError");
// ---------------------------------------------------------------------------
// State & helpers
// ---------------------------------------------------------------------------
-let currentWorkbook = null; // SheetJS workbook in memory
+let currentWorkbook = null; // SheetJS workbook in memory
const isSpreadsheet = (ext) => EXTENSIONS.includes(ext.toLowerCase());
-const nextPaint = () => new Promise(requestAnimationFrame);
+const nextPaint = () => new Promise(requestAnimationFrame);
// ---------------------------------------------------------------------------
// Open external links in default browser (security)
@@ -35,7 +34,7 @@ const nextPaint = () => new Promise(requestAnimationFrame);
document.addEventListener("click", (e) => {
if (e.target.tagName === "A" && e.target.href.startsWith("http")) {
e.preventDefault();
- electron.shell.openExternal(e.target.href);
+ window.SheetJSDemoAPI.openExternal(e.target.href);
}
});
@@ -45,16 +44,18 @@ document.addEventListener("click", (e) => {
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 }],
- });
+ const { filePath, canceled } = await window.SheetJSDemoAPI.saveFile([
+ {
+ 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}` });
+ window.SheetJSDemoAPI.message(`Exported to ${filePath}`);
} catch (err) {
// -- 4. if error, display error
displayError(`Failed to export: ${err.message}`);
@@ -83,14 +84,19 @@ function renderWorkbookToTables(wb) {
// ---------------------------------------------------------------------------
// 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 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}
@@ -121,24 +127,26 @@ fileStatus.addEventListener("click", (e) => {
// ---------------------------------------------------------------------------
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"],
- });
+ const { filePaths, canceled } = await window.SheetJSDemoAPI.openFile([
+ {
+ name: "Spreadsheets",
+ 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.");
+ if (filePaths.length !== 1)
+ return displayError("Please choose a single file.");
showSpinner();
- await nextPaint(); // ensure spinner paints
+ 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));
+ showLoadedFileUI(basename(filePath));
} finally {
hideSpinner();
hideDropUI();
@@ -192,7 +200,7 @@ async function readFile(files) {
// -- 2. get the first file
const file = files[0];
// -- 3. if not a spreadsheet, return error
- const ext = path.extname(file.name).slice(1);
+ const ext = extname(file.name).slice(1);
if (!isSpreadsheet(ext)) return displayError(`Unsupported file type .${ext}`);
showSpinner();
@@ -216,10 +224,10 @@ async function readFile(files) {
// ---------------------------------------------------------------------------
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) => {
+onFileOpened(async (_e, filePath) => {
showSpinner();
await nextPaint(); // ensure spinner paints
- currentWorkbook = XLSX.readFile(filePath);
+ currentWorkbook = XLSX.readFile(filePath);
renderWorkbookToTables(currentWorkbook);
showLoadedFileUI(path.basename(filePath));
hideSpinner();
diff --git a/docz/static/electron/main.js b/docz/static/electron/main.js
index a5ed378..874ff8d 100644
--- a/docz/static/electron/main.js
+++ b/docz/static/electron/main.js
@@ -1,85 +1,120 @@
/* sheetjs (C) 2013-present SheetJS -- https://sheetjs.com */
-var electron = require('electron');
-var XLSX = require('xlsx');
-var app = electron.app;
-require('@electron/remote/main').initialize(); // required for Electron 14+
-
-var win = null;
+const { app, BrowserWindow, ipcMain, dialog } = require('electron');
+const path = require('path');
+const XLSX = require('xlsx');
const EXT_REGEX = /\.(xls|xlsx|xlsm|xlsb|xml|csv|txt|dif|sylk|slk|prn|ods|fods|htm|html|numbers)$/i;
-const pendingPaths = []; // any paths that arrive before the window exists
+const pendingPaths = []; // paths of files that were opened before the window was created. These are queued and loaded once the window is created.
+let win = null; // reference to the main window, to make sure we don't create multiple windows.
-// send file opening events to renderer
-function sendToRenderer(filePath) {
- if (win && win.webContents) {
- win.webContents.send('file-opened', filePath);
- } else {
- pendingPaths.push(filePath);
- }
+/* In electron, the main process is the only process that can directly interface with the operating system.
+ The renderer process is sandboxed and cannot run any non-browser code.
+ To allow the renderer process to interface with the operating system, we use the contextBridge API to expose the API to the renderer process.
+ https://www.electronjs.org/docs/latest/api/context-bridge
+*/
+
+/* ----------------------------------------------------------------------------- */
+/* IPC handlers that allow communication between main and renderer processes */
+/* ----------------------------------------------------------------------------- */
+
+/* These three functions can be used to interface with the operating system from the renderer process.
+ the ipcMain.handle() function is used to register a handler for a specific event.
+ when the renderer process calls the corresponding function, the main process will receive the event and execute the handler.
+
+ In this case, we are listening to events which allow the renderer process to open/save file dialogs and show modal messages.
+*/
+ipcMain.handle('dialog:openFile', (_e, filters) =>
+ dialog.showOpenDialog({ title: 'Select a file', filters, properties: ['openFile'] })
+);
+
+ipcMain.handle('dialog:saveFile', (_e, filters) =>
+ dialog.showSaveDialog({ title: 'Save file as', filters })
+);
+
+ipcMain.handle('dialog:message', (_e, msg) =>
+ dialog.showMessageBox({ message: msg, buttons: ['OK'] })
+);
+
+/* ----------------------------------------------------------------------------- */
+/* Utility functions */
+/* ----------------------------------------------------------------------------- */
+function sendToRenderer(fp) {
+ if (win && win.webContents) win.webContents.send('file-opened', fp);
+ else pendingPaths.push(fp);
}
+/*
+ On Windows and Linux, opening a file using the "open with" menu option or `open` command will pass the file path as a startup argument to the app.
+ We need to parse it, and test if it is a spreadsheet file.
+*/
function firstSpreadsheetFromArgv() {
- const args = process.defaultApp // dev: electron .
- ? process.argv.slice(2) // skip electron executable & dir
- : process.argv.slice(1); // skip packaged exe
- return args.find(a => EXT_REGEX.test(a)); // undefined if none
+ const args = process.defaultApp ? process.argv.slice(2) : process.argv.slice(1);
+ return args.find((a) => EXT_REGEX.test(a));
}
-/* ---- single-instance guard (needed for Windows / Linux) ---- */
-// on windows and linux, opening a file opens a new instance of the app, this prevents that.
-// https://www.electronjs.org/docs/latest/api/app#event-second-instance
-const gotLock = app.requestSingleInstanceLock();
-if (!gotLock) app.quit();
+/* ----------------------------------------------------------------------------- */
+/* Single-instance guard */
+/* ----------------------------------------------------------------------------- */
+
+// Windows and Linux only: If the app is already running, we need to prevent a new instance from launching when opening a file via the "open with" menu option or `open` command.
+if (!app.requestSingleInstanceLock()) app.quit();
else {
- app.on('second-instance', (_e, argv) => { // emitted in *primary* instance
- const fp = argv.find(a => EXT_REGEX.test(a));
+ app.on('second-instance', (_e, argv) => {
+ const fp = argv.find((a) => EXT_REGEX.test(a));
if (fp) sendToRenderer(fp);
if (win) { win.show(); win.focus(); }
});
}
-/* ---- platform-specific “open file” hooks (macOS) ---- */
-// https://www.electronjs.org/docs/latest/api/app#event-open-file-macos
-app.on('open-file', (event, fp) => { // macOS Dock / Finder
- event.preventDefault();
- sendToRenderer(fp);
-});
-// https://www.electronjs.org/docs/latest/api/app#event-open-url-macos
-app.on('open-url', (event, url) => { // you can add a custom protocol if you want to handle URLs
- event.preventDefault();
- sendToRenderer(url.replace('file://', '')); // crude, adjust if you keep deep-links
-});
+// macOS file / url events
+app.on('open-file', (evt, fp) => { evt.preventDefault(); sendToRenderer(fp); });
+app.on('open-url', (evt, url) => { evt.preventDefault(); sendToRenderer(url.replace('file://', '')); });
-/* ---- normal start-up, harvest argv (Windows & Linux) ---- */
-app.whenReady().then(() => {
- const fp = firstSpreadsheetFromArgv(); // Windows & Linux first launch
- if (fp) pendingPaths.push(fp);
- createWindow();
-});
-
-/* ---- create the window ---- */
+/* ----------------------------------------------------------------------------- */
+/* Create the window */
+/* ----------------------------------------------------------------------------- */
function createWindow() {
if (win) return;
- win = new electron.BrowserWindow({
- width: 800, height: 600,
+
+ win = new BrowserWindow({
+ width: 800,
+ height: 600,
webPreferences: {
- worldSafeExecuteJavaScript: true, // required for Electron 12+
- contextIsolation: false, // required for Electron 12+
- nodeIntegration: true,
- enableRemoteModule: true
+ preload: path.join(__dirname, './preload.js'), // preload script that will be executed in the renderer process before the page is loaded and act as a bridge between the main and renderer processes within a worker thread.
+ contextIsolation: true, // isolate and enable bridge, keeping the renderer process sandboxed and separated from the main process.
+ nodeIntegration: false, // no Node.js in renderer process.
+ nodeIntegrationInWorker: true, // enable Node.js in worker threads.
+ worldSafeExecuteJavaScript: true
}
});
- win.loadURL("file://" + __dirname + "/index.html");
- require('@electron/remote/main').enable(win.webContents); // required for Electron 14
- if (process.env.NODE_ENV === 'development') win.webContents.openDevTools(); // only open devtools in development
- win.on('closed', function () { win = null; });
+
+ win.loadFile('index.html');
+ if (process.env.NODE_ENV === 'development') win.webContents.openDevTools();
+
+ win.on('closed', () => { win = null; });
+
win.webContents.once('did-finish-load', () => {
pendingPaths.splice(0).forEach(sendToRenderer);
});
}
-if (app.setAboutPanelOptions) app.setAboutPanelOptions({ applicationName: 'sheetjs-electron', applicationVersion: "XLSX " + XLSX.version, copyright: "(C) 2017-present SheetJS LLC" });
-app.on('ready', createWindow);
+/* ----------------------------------------------------------------------------- */
+/* App lifecycle */
+/* ----------------------------------------------------------------------------- */
+app.whenReady().then(() => {
+ const fp = firstSpreadsheetFromArgv();
+ if (fp) pendingPaths.push(fp);
+ createWindow();
+});
+
+if (app.setAboutPanelOptions) {
+ app.setAboutPanelOptions({
+ applicationName: 'sheetjs-electron',
+ applicationVersion: `XLSX ${XLSX.version}`,
+ copyright: '(C) 2017‑present SheetJS LLC'
+ });
+}
+
app.on('activate', createWindow);
-app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit(); });
+app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
\ No newline at end of file
diff --git a/docz/static/electron/package.json b/docz/static/electron/package.json
index a8fcee0..84c5a00 100644
--- a/docz/static/electron/package.json
+++ b/docz/static/electron/package.json
@@ -6,7 +6,6 @@
"version": "0.0.0",
"main": "main.js",
"dependencies": {
- "@electron/remote": "2.1.2",
"xlsx": "https://sheet.lol/balls/xlsx-0.20.3.tgz"
},
"scripts": {
diff --git a/docz/static/electron/preload.js b/docz/static/electron/preload.js
new file mode 100644
index 0000000..cc91b6b
--- /dev/null
+++ b/docz/static/electron/preload.js
@@ -0,0 +1,20 @@
+const { contextBridge, ipcRenderer, shell } = require('electron');
+const path = require('path');
+const XLSX = require('xlsx');
+
+// Because the main process is sandboxed, we need to use the contextBridge API to expose the API to the renderer process.
+// https://www.electronjs.org/docs/latest/api/context-bridge
+contextBridge.exposeInMainWorld('SheetJSDemoAPI', {
+ openFile: (filters) => ipcRenderer.invoke('dialog:openFile', filters),
+ saveFile: (filters) => ipcRenderer.invoke('dialog:saveFile', filters),
+ message: (msg) => ipcRenderer.invoke('dialog:message', msg),
+ openExternal: (url) => shell.openExternal(url),
+ // expose file-opened event
+ onFileOpened: (cb) => ipcRenderer.on('file-opened', (_e, fp) => cb(fp)),
+ // expose basename from path package
+ basename: (p) => path.basename(p),
+ // expose extname from path package
+ extname: (p) => path.extname(p),
+ // expose sheetjs package functions
+ xlsx: XLSX,
+});
\ No newline at end of file