forked from sheetjs/docs.sheetjs.com
feat/docs: [Electron Demo] - full context isolation between processes.
This commit is contained in:
parent
603b49a9fd
commit
418f16a872
@ -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**
|
||||
|
||||
<details>
|
||||
<summary><b>Renderer Configuration</b> (click to show)</summary>
|
||||
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.
|
||||
|
||||
</details>
|
||||
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.
|
@ -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) => `<div class="file-loaded">
|
||||
<span class="file-name text-muted text-small">${fileName}</span>
|
||||
@ -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();
|
||||
|
@ -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(); });
|
@ -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": {
|
||||
|
20
docz/static/electron/preload.js
Normal file
20
docz/static/electron/preload.js
Normal file
@ -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,
|
||||
});
|
Loading…
Reference in New Issue
Block a user