diff --git a/docz/docs/03-demos/03-desktop.md b/docz/docs/03-demos/03-desktop.md deleted file mode 100644 index 930f28f..0000000 --- a/docz/docs/03-demos/03-desktop.md +++ /dev/null @@ -1,1682 +0,0 @@ ---- -title: Desktop Applications ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -Web technologies like JavaScript and HTML have been adapted to the traditional -app space. Typically these frameworks bundle a JavaScript engine as well as a -windowing framework. SheetJS is compatible with many app frameworks. - -## NW.js - -The [Standalone scripts](/docs/getting-started/installation/standalone) can be -referenced in a `SCRIPT` tag from the entry point HTML page. - -This demo was tested against NW.js 0.66.0. - -
Complete Example (click to show) - -1) Create a `package.json` file that specifies the entry point: - -```json title="package.json" -{ - "name": "sheetjs-nwjs", - "author": "sheetjs", - "version": "0.0.0", - "main": "index.html", - "dependencies": { - "nw": "~0.66.0", - "xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz" - } -} -``` - -2) Download [`index.html`](pathname:///nwjs/index.html) into the same folder. - -:::caution - -Right-click the link and select "Save Link As...". Left-clicking the link will -try to load the page in your browser. The goal is to save the file contents. - -::: - -3) Run `npm install` to install dependencies - -4) To verify the app works, run in the test environment: - -``` -npx nw . -``` - -The app will show and you should be able to verify reading and writing by using -the file input element to select a spreadsheet and clicking the export button. - -5) To build a standalone app, run the builder: - -``` -npx -p nw-builder nwbuild --mode=build . -``` - -This will generate the standalone app in the `build\sheetjs-nwjs\` folder. - -
- -### Reading data - -The standard HTML5 `FileReader` techniques from the browser apply to NW.js! - -NW.js handles the OS minutiae for dragging files into app windows. The -[drag and drop snippet](/docs/solutions/input#example-user-submissions) apply -to DIV elements on the page. - -Similarly, file input elements automatically map to standard Web APIs. - -For example, assuming a file input element on the page: - -```html - -``` - -The event handler would process the event as if it were a web event: - -```js -async function handleFile(e) { - const file = e.target.files[0]; - const data = await file.arrayBuffer(); - /* data is an ArrayBuffer */ - const workbook = XLSX.read(data); - - /* DO SOMETHING WITH workbook HERE */ -} -document.getElementById("xlf").addEventListener("change", handleFile, false); -``` - -### Writing data - -File input elements with the attribute `nwsaveas` show UI for saving a file. The -standard trick is to generate a hidden file input DOM element and "click" it. -Since NW.js does not present a `writeFileSync` in the `fs` package, a manual -step is required: - -```js -/* pre-build the hidden nwsaveas input element */ -var input = document.createElement('input'); -input.style.display = 'none'; -input.setAttribute('nwsaveas', 'SheetJSNWDemo.xlsx'); -input.setAttribute('type', 'file'); -document.body.appendChild(input); - -/* show a message if the save is canceled */ -input.addEventListener('cancel',function(){ alert("Save was canceled!"); }); - -/* write to a file on the 'change' event */ -input.addEventListener('change',function(e){ - /* the `value` is the path that the program will write */ - var filename = this.value; - - /* use XLSX.write with type "buffer" to generate a buffer" */ - /* highlight-next-line */ - var wbout = XLSX.write(workbook, {type:'buffer', bookType:"xlsx"}); - /* highlight-next-line */ - fs.writeFile(filename, wbout, function(err) { - if(!err) return alert("Saved to " + filename); - alert("Error: " + (err.message || err)); - }); -}); - -input.click(); -``` - -## Electron - -The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported -from the main or the renderer thread. - -Electron presents a `fs` module. The `require('xlsx')` call loads the CommonJS -module, so `XLSX.readFile` and `XLSX.writeFile` work in the renderer thread. - -This demo was tested on 2022 November 07 with Electron 21.2.2 on `darwin-x64`. - -
Complete Example (click to show) - -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. - -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 - -:::caution - -Right-click each link and select "Save Link As...". Left-clicking a link will -try to load the page in your browser. The goal is to save the file contents. - -::: - -2) Run `npm install` to install dependencies. - -3) To verify the app works, run in the test environment: - -```bash -npx -y electron . -``` - -The app will show and you should be able to verify reading and writing by using -the relevant buttons to open files and clicking the export button. - -4) To build a standalone app, run the builder: - -```bash -npm run make -``` - -This will generate the standalone app in the `out\sheetjs-electron-...` folder. -For a recent Intel Mac, the path will be `out/sheetjs-electron-darwin-x64/` - -
- -### 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'); - -/* 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: [{ - name: "Spreadsheets", - extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */] - }] - }); - /* write file */ - // highlight-next-line - XLSX.writeFile(workbook, result.filePath); -} -``` - -:::note - -In older versions of Electron, `showSaveDialog` returned the path directly: - -```js -var dialog = require('electron').remote.dialog; - -function exportFile(workbook) { - var result = dialog.showSaveDialog(); - XLSX.writeFile(workbook, result); -} -``` - -::: - -### Reading Files - -Electron offers 3 different ways to read files, two of which use Web APIs. - -**File Input Element** - -File input elements automatically map to standard Web APIs. - -For example, assuming a file input element on the page: - -```html - -``` - -The event handler would process the event as if it were a web event: - -```js -async function handleFile(e) { - const file = e.target.files[0]; - const data = await file.arrayBuffer(); - /* data is an ArrayBuffer */ - const workbook = XLSX.read(data); - - /* DO SOMETHING WITH workbook HERE */ -} -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. - -For example, assuming a DIV on the page: - -```html -
Drop a spreadsheet file here to see sheet data
-``` - -The event handler would process the event as if it were a web event: - -```js -async function handleDrop(e) { - e.stopPropagation(); - e.preventDefault(); - - const file = e.dataTransfer.files[0]; - const data = await file.arrayBuffer(); - /* data is an ArrayBuffer */ - const workbook = XLSX.read(data); - - /* DO SOMETHING WITH workbook HERE */ -} -document.getElementById("drop").addEventListener("drop", handleDrop, false); -``` - -**Electron API** - -[`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'); - -/* 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: [{ - 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 - -In older versions of Electron, `showOpenDialog` returned the path directly: - -```js -var dialog = require('electron').remote.dialog; - -function importFile(workbook) { - var result = dialog.showOpenDialog({ properties: ['openFile'] }); - return XLSX.readFile(result[0]); -} -``` - -::: - -### Electron Breaking Changes - -The first version of this demo used Electron 1.7.5. The current demo includes -the required changes for Electron 19.2.2. - -There are no Electron-specific workarounds in the library, but Electron broke -backwards compatibility multiple times. A summary of changes is noted below. - -:::caution - -Electron 6.x changed the `dialog` API. Methods like `showSaveDialog` originally -returned an array of strings, but now returns a `Promise`. This change was not -documented. - -Electron 9.0.0 and later require the preference `nodeIntegration: true` in order -to `require('xlsx')` in the renderer process. - -Electron 12.0.0 and later also require `worldSafeExecuteJavascript: true` and -`contextIsolation: true`. - -Electron 14+ must use `@electron/remote` instead of `remote`. An `initialize` -call is required to enable Developer Tools in the window. - -::: - -## Tauri - -The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported -from JavaScript code. - -This demo was tested against Tauri 1.0.5 on 2022 August 13. - -:::note - -Tauri currently does not provide the equivalent of NodeJS `fs` module. The raw -`@tauri-apps/api` methods used in the examples are not expected to change. - -::: - -`http` and `dialog` must be explicitly allowed in `tauri.conf.json`: - -```json title="tauri.conf.json" - "allowlist": { - "all": true, - "http": { - "all": true, - "request": true, - "scope": ["https://**"] - }, - "dialog": { - "all": true - } -``` - -The "Complete Example" creates an app that looks like the screenshot: - -![SheetJS Tauri MacOS screenshot](pathname:///tauri/macos.png) - -
Complete Example (click to show) - -0) [Read Tauri "Getting Started" guide and install dependencies.](https://tauri.app/v1/guides/getting-started/prerequisites) - -1) Create a new Tauri app: - -```bash -npm create tauri-app -``` - -When prompted: - -- App Name: `SheetJSTauri` -- Window Title: `SheetJS + Tauri` -- UI recipe: `create-vite` -- Add "@tauri-apps/api": `Y` -- ViteJS template: `vue-ts` - -2) Enter the directory: - -```bash -cd SheetJSTauri -``` - -Open `package.json` with a text editor and add the highlighted lines: - -```json title="package.json" -{ - "name": "SheetJSTauri", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vue-tsc --noEmit && vite build", - "preview": "vite preview", - "tauri": "tauri" - }, - "dependencies": { -// highlight-next-line - "@tauri-apps/api": "^1.0.2", - "vue": "^3.2.37", -// highlight-next-line - "xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz" - }, - "devDependencies": { -// highlight-next-line - "@tauri-apps/cli": "^1.0.5", - "@vitejs/plugin-vue": "^3.0.3", - "typescript": "^4.6.4", - "vite": "^3.0.7", - "vue-tsc": "^0.39.5" - } -} -``` - -3) Install dependencies: - -```bash -npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz -``` - -4) Enable operations by adding the highlighted lines to `tauri.conf.json`: - -```json title="src-tauri/tauri.conf.json" - "tauri": { - "allowlist": { -// highlight-start - "http": { - "all": true, - "request": true, - "scope": ["https://**"] - }, - "dialog": { - "all": true - }, -// highlight-end - "all": true - } -``` - -In the same file, look for the `"identifier"` key and replace the value with `com.sheetjs.tauri`: - -```json title="src-tauri/tauri.conf.json" - "icons/icon.ico" - ], - // highlight-next-line - "identifier": "com.sheetjs.tauri", - "longDescription": "", -``` - - -5) Download [`App.vue`](pathname:///tauri/App.vue) and replace `src/App.vue` - with the downloaded script. - -6) Build the app with - -```bash -npm run tauri build -``` - -At the end, it will print the path to the generated program. Run the program! - -
- -### Reading Files - -There are two steps to reading files: obtaining a path and reading binary data: - -```js -import { read } from 'xlsx'; -import { open } from '@tauri-apps/api/dialog'; -import { readBinaryFile } from '@tauri-apps/api/fs'; - -const filters = [ - {name: "Excel Binary Workbook", extensions: ["xlsb"]}, - {name: "Excel Workbook", extensions: ["xlsx"]}, - {name: "Excel 97-2004 Workbook", extensions: ["xls"]}, - // ... other desired formats ... -]; - -async function openFile() { - /* show open file dialog */ - const selected = await open({ - title: "Open Spreadsheet", - multiple: false, - directory: false, - filters - }); - - /* read data into a Uint8Array */ - const d = await readBinaryFile(selected); - - /* parse with SheetJS */ - const wb = read(d); - return wb; -} -``` - -### Writing Files - -There are two steps to writing files: obtaining a path and writing binary data: - -```js -import { write } from 'xlsx'; -import { save } from '@tauri-apps/api/dialog'; -import { writeBinaryFile } from '@tauri-apps/api/fs'; - -const filters = [ - {name: "Excel Binary Workbook", extensions: ["xlsb"]}, - {name: "Excel Workbook", extensions: ["xlsx"]}, - {name: "Excel 97-2004 Workbook", extensions: ["xls"]}, - // ... other desired formats ... -]; - -async function saveFile(wb) { - /* show save file dialog */ - const selected = await save({ - title: "Save to Spreadsheet", - filters - }); - - /* Generate workbook */ - const bookType = selected.slice(selected.lastIndexOf(".") + 1); - const d = write(wb, {type: "buffer", bookType}); - - /* save data to file */ - await writeBinaryFile(selected, d); -} -``` - -## Wails - -The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported -from JavaScript code. - -This demo was tested against Wails `v2.0.0-beta.44.2` on 2022 August 31 using -the Svelte TypeScript starter. - -:::caution - -Wails currently does not provide the equivalent of NodeJS `fs` module. - -The HTML File Input Element does not show a file picker. This is a known bug. - -All raw file operations must be performed in Go code. - -::: - - -The "Complete Example" creates an app that looks like the screenshot: - -![SheetJS Wails MacOS screenshot](pathname:///wails/macos.png) - -
Complete Example (click to show) - -0) [Read Wails "Getting Started" guide and install dependencies.](https://wails.io/docs/gettingstarted/installation) - -1) Create a new Wails app: - -```bash -wails init -n sheetjs-wails -t svelte-ts -``` - -2) Enter the directory: - -```bash -cd sheetjs-wails -``` - -3) Install front-end dependencies: - -```bash -cd frontend -curl -L -o src/assets/logo.png https://sheetjs.com/sketch1024.png -npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz -cd .. -``` - -4) Download source files: - -- Download [`app.go`](pathname:///wails/app.go) and replace `app.go` -- Download [`App.svelte`](pathname:///wails/App.svelte) and replace - `frontend/src/App.svelte` - -5) Build the app with - -```bash -wails build -``` - -At the end, it will print the path to the generated program. Run the program! - -
- -All operations must be run from Go code. This example passes Base64 strings. - -### Reading Files - -The file picker and reading operations can be combined in one Go function. - -#### Go - -```go -import ( - "context" -// highlight-start - "encoding/base64" - "io/ioutil" - "github.com/wailsapp/wails/v2/pkg/runtime" -// highlight-end -) - -type App struct { - ctx context.Context -} - -// ReadFile shows an open file dialog and returns the data as Base64 string -func (a *App) ReadFile() string { - // highlight-next-line - selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ - Title: "Select File", - Filters: []runtime.FileFilter{ - { DisplayName: "Excel Workbooks (*.xlsx)", Pattern: "*.xlsx", }, - // ... more filters for more file types - }, - }) - if err != nil { return "" } // The demo app shows an error message - // highlight-next-line - data, err := ioutil.ReadFile(selection) - if err != nil { return "" } // The demo app shows an error message - // highlight-next-line - return base64.StdEncoding.EncodeToString(data) -} -``` - -#### JS - -Wails will automatically create `window.go.main.App.ReadFile` for use in JS: - -```js title="frontend/src/App.svelte" -import { read, utils } from 'xlsx'; - -async function importFile(evt) { -// highlight-start - const b64 = window['go']['main']['App']['ReadFile'](); - const wb = read(b64, { type: "base64" }); -// highlight-end - const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet - html = utils.sheet_to_html(ws); // generate HTML and update state -} -``` - -### Writing Files - -There is a multi-part dance since the library needs the file extension. - -1) Show the save file picker in Go, pass back to JS - -2) Generate the file data in JS, pass the data back to Go - -3) Write to file in Go - -##### Go - -Two Go functions will be exposed. - -- `SaveFile` will show the file picker and return the path: - -```go -import ( - "context" -// highlight-start - "github.com/wailsapp/wails/v2/pkg/runtime" -// highlight-end -) - -type App struct { - ctx context.Context -} - -func (a *App) SaveFile() string { -// highlight-next-line - selection, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ - Title: "Select File", - DefaultFilename: "SheetJSWails.xlsx", - Filters: []runtime.FileFilter{ - { DisplayName: "Excel Workbooks (*.xlsx)", Pattern: "*.xlsx", }, - // ... more filters for more file types - }, - }) - if err != nil { return "" } // The demo app shows an error message - return selection -} -``` - -- `WriteFile` performs the file write given a Base64 string and file path: - -```go -import ( - "context" -// highlight-start - "encoding/base64" - "io/ioutil" -// highlight-end -) - -type App struct { - ctx context.Context -} - -func (a *App) WriteFile(b64 string, path string) { - // highlight-start - buf, _ := base64.StdEncoding.DecodeString(b64); - _ = ioutil.WriteFile(path, buf, 0644); - // highlight-end -} -``` - -#### JS - -Wails will automatically create bindings for use in JS: - -```js -import { utils, write } from 'xlsx'; - -async function exportFile(wb) { - /* generate workbook */ - const elt = tbl.getElementsByTagName("TABLE")[0]; - const wb = utils.table_to_book(elt); - - /* show save picker and get path */ - const path = await window['go']['main']['App']['SaveFile'](); - - /* generate base64 string based on the path */ - const b64 = write(wb, { bookType: path.slice(path.lastIndexOf(".")+1), type: "base64" }); - - /* write to file */ - await window['go']['main']['App']['WriteFile'](b64, path); - // The demo shows a success message at this point -} -``` - -## NeutralinoJS - -The [Standalone build](/docs/getting-started/installation/standalone) can be added -to the entry `index.html` - -This demo was tested against "binaries" `4.7.0` and "client" `3.6.0` - -:::note - -NeutralinoJS currently does not provide the equivalent of NodeJS `fs` module. -The raw `Neutralino.filesystem` and `Neutralino.os` methods are used. - -::: - -The `os` and `filesystem` modules must be enabled in `neutralino.conf.json`. -The starter already enables `os` so typically one line must be added: - -```json title="neutralino.config.json" - "nativeAllowList": [ - "app.*", - "os.*", -// highlight-next-line - "filesystem.*", - "debug.log" - ], -``` - -The "Complete Example" creates an app that looks like the screenshot: - -![SheetJS NeutralinoJS MacOS screenshot](pathname:///neu/macos.png) - -:::caution - -At the time of writing, `filters` did not work as expected on MacOS. They have -been omitted in the example and commented in the code snippets - -::: - -
Complete Example (click to show) - -The app core state will be the HTML table. Reading files will add the table to -the window. Writing files will parse the table into a spreadsheet. - -1) Create a new NeutralinoJS app: - -```bash -npx @neutralinojs/neu create sheetjs-neu -cd sheetjs-neu -``` - -2) Download the standalone script and place in `resources/js/main.js`: - -```bash -curl -L -o resources/js/xlsx.full.min.js https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js -``` - -3) Add the highlighted lines to `neutralino.conf.json` in `nativeAllowList`: - -```json title="neutralino.config.json" - "nativeAllowList": [ - "app.*", -// highlight-start - "os.*", - "filesystem.*", -// highlight-end - "debug.log" - ], -``` - -4) Set up skeleton app and print version info: - -- Edit `resources/index.html` and replace the `` with the code below: - -```html title="resources/index.html" - -
-

SheetJS × NeutralinoJS

- - -
-
- - - - - -``` - -- Append the following code to `resources/styles.css` to center the table: - -```css title="resources/styles.css" -#info { - width:100%; - text-align: unset; -} -table { - margin: 0 auto; -} -``` - -- Print the version number in the `showInfo` method of `resources/js/main.js`: - -```js title="resources/js/main.js" - ${NL_APPID} is running on port ${NL_PORT} inside ${NL_OS} -

- server: v${NL_VERSION} . client: v${NL_CVERSION} -// highlight-start -

- SheetJS version ${XLSX.version} -// highlight-end - `; -``` - -5) Run the app: - -```bash -npx @neutralinojs/neu run -``` - -You should see `SheetJS Version ` followed by the library version number. - -6) Add the following code to the bottom of `resources/js/main.js`: - -```js -(async() => { - const ab = await (await fetch("https://sheetjs.com/pres.numbers")).arrayBuffer(); - const wb = XLSX.read(ab); - const ws = wb.Sheets[wb.SheetNames[0]]; - document.getElementById('info').innerHTML = XLSX.utils.sheet_to_html(ws); -})(); -``` - -Save the source file, close the app and re-run the command from step 5. - -When the app loads, a table should show in the main screen. - -7) Add `importFile` and `exportFile` to the bottom of `resources/js/main.js`: - -```js -async function importData() { - /* show open dialog */ - const [filename] = await Neutralino.os.showOpenDialog('Open a spreadsheet'); - - /* read data */ - const ab = await Neutralino.filesystem.readBinaryFile(filename); - const wb = XLSX.read(ab); - - /* make table */ - const ws = wb.Sheets[wb.SheetNames[0]]; - document.getElementById('info').innerHTML = XLSX.utils.sheet_to_html(ws); -} - -async function exportData() { - /* show save dialog */ - const filename = await Neutralino.os.showSaveDialog('Save to file'); - - /* make workbook */ - const tbl = document.getElementById('info').querySelector("table"); - const wb = XLSX.utils.table_to_book(tbl); - - /* make file */ - const bookType = filename.slice(filename.lastIndexOf(".") + 1); - const data = XLSX.write(wb, { bookType, type: "buffer" }); - await Neutralino.filesystem.writeBinaryFile(filename, data); -} -``` - -Save the source file, close the app and re-run the command from step 5. - -When the app loads, click the "Import File" button and select a spreadsheet to -see the contents. Click "Export File" and enter `SheetJSNeu.xlsx` to write. - -8) Build production apps: - -```bash -npx @neutralinojs/neu run -``` - -Platform-specific programs will be created in the `dist` folder. - -
- -### Reading Files - -There are two steps to reading files: obtaining a path and reading binary data: - -```js -const filters = [ - {name: "Excel Binary Workbook", extensions: ["xlsb"]}, - {name: "Excel Workbook", extensions: ["xlsx"]}, -] - -async function openFile() { - /* show open file dialog */ - const [filename] = await Neutralino.os.showOpenDialog( - 'Open a spreadsheet', - { /* filters, */ multiSelections: false } - ); - - /* read data into an ArrayBuffer */ - const ab = await Neutralino.filesystem.readBinaryFile(filename); - - /* parse with SheetJS */ - const wb = XLSX.read(ab); - return wb; -} -``` - -This method can be called from a button click or other event. - -### Writing Files - -There are two steps to writing files: obtaining a path and writing binary data: - -```js -const filters = [ - {name: "Excel Binary Workbook", extensions: ["xlsb"]}, - {name: "Excel Workbook", extensions: ["xlsx"]}, -] - -async function saveFile(wb) { - /* show save file dialog */ - const filename = await Neutralino.os.showSaveDialog( - 'Save to file', - { /* filters */ } - ); - - /* Generate workbook */ - const bookType = filename.slice(filename.lastIndexOf(".") + 1); - const data = XLSX.write(wb, { bookType, type: "buffer" }); - - /* save data to file */ - await Neutralino.filesystem.writeBinaryFile(filename, data); -} -``` - -## React Native Windows - -The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported -from the main app script. File operations must be written in native code. - -This demo was tested against `v0.69.6` on 2022 September 07 in Windows 10. - -:::warning - -There is no simple standalone executable file at the end of the process. - -[The official documentation describes distribution strategies](https://microsoft.github.io/react-native-windows/docs/native-code#distribution) - -::: - -React Native Windows use [Turbo Modules](https://reactnative.dev/docs/the-new-architecture/pillars-turbomodules) - -
Complete Example (click to show) - -:::note - -React Native Windows supports writing native code in C++ or C#. This demo has -been tested against both application types. - -::: - -0) Follow the ["Getting Started" guide](https://microsoft.github.io/react-native-windows/docs/getting-started) - -1) Create a new project using React Native `0.69`: - -```powershell -npx react-native init SheetJSWin --template react-native@^0.69.0 -cd .\SheetJSWin\ -``` - -Create the Windows part of the application: - - - - -```powershell -npx react-native-windows-init --no-telemetry --overwrite --language=cs -``` - - - - -```powershell -npx react-native-windows-init --no-telemetry --overwrite -``` - - - - -Install library: - -```powershell -npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz -``` - -To ensure that the app works, launch the app: - -```powershell -npx react-native run-windows --no-telemetry -``` - - - - -2) Create the file `windows\SheetJSWin\DocumentPicker.cs` with the following: - -```csharp title="windows\SheetJSWin\DocumentPicker.cs" -using System; -using Microsoft.ReactNative.Managed; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Windows.ApplicationModel.Core; -using Windows.Security.Cryptography; -using Windows.Storage; -using Windows.Storage.Pickers; -using Windows.UI.Core; - -namespace SheetJSWin { - [ReactModule] - class DocumentPicker { - private ReactContext context; - [ReactInitializer] - public void Initialize(ReactContext reactContext) { context = reactContext; } - - [ReactMethod("PickAndRead")] - public async void PickAndRead(IReactPromise result) { - context.Handle.UIDispatcher.Post(async() => { try { - var picker = new FileOpenPicker(); - picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; - picker.FileTypeFilter.Add(".xlsx"); - picker.FileTypeFilter.Add(".xls"); - - var file = await picker.PickSingleFileAsync(); - if(file == null) throw new Exception("File not found"); - - var buf = await FileIO.ReadBufferAsync(file); - result.Resolve(CryptographicBuffer.EncodeToBase64String(buf)); - } catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }}); - } - } -} -``` - -3) Add the highlighted line to `windows\SheetJSWin\SheetJSWin.csproj`. Look for -the `ItemGroup` that contains `ReactPackageProvider.cs`: - -```xml title="windows\SheetJSWin\SheetJSWin.csproj" - - - - -``` - - - - -4) Create the file `windows\SheetJSWin\DocumentPicker.h` with the following: - -```cpp title="windows\SheetJSWin\DocumentPicker.h" -#pragma once - -#include "pch.h" -#include -#include -#include "JSValue.h" -#include "NativeModules.h" - -using namespace winrt::Microsoft::ReactNative; -using namespace winrt::Windows::Foundation; -using namespace winrt::Windows::Storage; -using namespace winrt::Windows::Storage::Pickers; -using namespace winrt::Windows::Security::Cryptography; - -namespace SheetJSWin { - REACT_MODULE(DocumentPicker); - struct DocumentPicker { - REACT_INIT(Initialize); - void Initialize(const ReactContext& reactContext) noexcept { - context = reactContext; - } - - REACT_METHOD(PickAndRead); - void PickAndRead(ReactPromise promise) noexcept { - auto prom = promise; - context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget { - auto p = prom; - winrt::Windows::Storage::Pickers::FileOpenPicker picker; - picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary); - picker.FileTypeFilter().Append(L".xlsx"); - picker.FileTypeFilter().Append(L".xls"); - - StorageFile file = co_await picker.PickSingleFileAsync(); - if(file == nullptr) { p.Reject("File not Found"); co_return; } - - auto buf = co_await FileIO::ReadBufferAsync(file); - p.Resolve(CryptographicBuffer::EncodeToBase64String(buf)); - co_return; - }); - } - - private: - ReactContext context{nullptr}; - }; -} -``` - -5) Add the highlighted line to `windows\SheetJSWin\ReactPackageProvider.cpp`: - -```cpp title="windows\SheetJSWin\ReactPackageProvider.cpp" -#include "ReactPackageProvider.h" -// highlight-next-line -#include "DocumentPicker.h" -#include "NativeModules.h" -``` - - - - -Now the native module will be added to the app. - -6) Remove `App.js` and save the following to `App.tsx`: - -```tsx title="App.tsx" -import React, { useState, type Node } from 'react'; -import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native'; -import { read, utils, version } from 'xlsx'; -import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; -const DocumentPicker = getEnforcing('DocumentPicker'); - -const App: () => Node = () => { - - const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]); - - return ( - - SheetJS × React Native Windows {version} - { - try { - const b64 = await DocumentPicker.PickAndRead(); - const wb = read(b64); - setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } )); - } catch(err) { alert(`Error: ${err.message}`); } - }}>Click here to Open File! - - {aoa.map((row,R) => ( - {row.map((cell,C) => ( - {cell} - ))} - ))} - - - ); -}; - -const styles = StyleSheet.create({ - cell: { flex: 4 }, - row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', }, - table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', }, - outer: { marginTop: 32, paddingHorizontal: 24, }, - title: { fontSize: 24, fontWeight: '600', }, - button: { marginTop: 8, fontSize: 18, fontWeight: '400', }, -}); - -export default App; -``` - -7) Test the app again: - -```powershell -npx react-native run-windows --no-telemetry -``` - -Download , then click on "open file". Use the -file picker to select the `pres.xlsx` file and the app will show the data. - -
- -### Reading Files - -Only the main UI thread can show file pickers. This is similar to Web Worker -DOM access limitations in the Web platform. - -This example defines a `PickAndRead` function that will show the file picker, -read the file contents, and return a Base64 string. - - - - -```csharp -namespace SheetJSWin { - [ReactModule] - class DocumentPicker { - /* The context must be stored when the module is initialized */ - private ReactContext context; - [ReactInitializer] - public void Initialize(ReactContext ctx) { context = ctx; } - - [ReactMethod("PickAndRead")] - public async void PickAndRead(IReactPromise result) { - /* perform file picker action in the UI thread */ - // highlight-next-line - context.Handle.UIDispatcher.Post(async() => { try { - /* create file picker */ - var picker = new FileOpenPicker(); - picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; - picker.FileTypeFilter.Add(".xlsx"); - picker.FileTypeFilter.Add(".xls"); - - /* show file picker */ - // highlight-next-line - var file = await picker.PickSingleFileAsync(); - if(file == null) throw new Exception("File not found"); - - /* read data and return base64 string */ - var buf = await FileIO.ReadBufferAsync(file); - // highlight-next-line - result.Resolve(CryptographicBuffer.EncodeToBase64String(buf)); - } catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }}); - } - } -} -``` - - - - -```cpp -namespace SheetJSWin -{ - REACT_MODULE(DocumentPicker); - struct DocumentPicker - { - /* The context must be stored when the module is initialized */ - REACT_INIT(Initialize); - void Initialize(const ReactContext& reactContext) noexcept { - context = reactContext; - } - - REACT_METHOD(PickAndRead); - void PickAndRead(ReactPromise promise) noexcept { - auto prom = promise; - /* perform file picker action in the UI thread */ - // highlight-next-line - context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget { - auto p = prom; // promise -> prom -> p dance avoids promise destruction - - /* create file picker */ - winrt::Windows::Storage::Pickers::FileOpenPicker picker; - picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary); - picker.FileTypeFilter().Append(L".xlsx"); - picker.FileTypeFilter().Append(L".xls"); - - /* show file picker */ - // highlight-next-line - StorageFile file = co_await picker.PickSingleFileAsync(); - if(file == nullptr) { p.Reject("File not Found"); co_return; } - - /* read data and return base64 string */ - auto buf = co_await FileIO::ReadBufferAsync(file); - // highlight-next-line - p.Resolve(CryptographicBuffer::EncodeToBase64String(buf)); - co_return; - }); - } - - private: - ReactContext context{nullptr}; - }; -} -``` - - - - -This module can be referenced from the Turbo Module Registry: - -```js -import { read } from 'xlsx'; -import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; -const DocumentPicker = getEnforcing('DocumentPicker'); - - -/* ... in some event handler ... */ -async() => { - const b64 = await DocumentPicker.PickAndRead(); - const wb = read(b64); - // DO SOMETHING WITH `wb` HERE -} -``` - -## React Native MacOS - -The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported -from the main app script. File operations must be written in native code. - -This demo was tested against `v0.64.30` on 2022 September 10 in MacOS 12.4 - -:::note - -At the time of writing, the latest supported React Native version was `v0.64.3` - -::: - -
Complete Example (click to show) - -0) Follow the [React Native](https://reactnative.dev/docs/environment-setup) - guide for React Native CLI on MacOS. - -:::caution - -NodeJS `v16` is required. There are OS-specific tools for downgrading: - -- [`nvm-windows`](https://github.com/coreybutler/nvm-windows/releases) Windows -- [`n`](https://github.com/tj/n/) Linux, MacOS, WSL, etc. - -::: - -1) Create a new project using React Native `0.64`: - -```bash -npx react-native init SheetJSmacOS --template react-native@^0.64.0 -cd SheetJSmacOS -``` - -Create the MacOS part of the application: - -```bash -npx react-native-macos-init --no-telemetry -``` - -Install Library: - -``` -npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz -``` - -To ensure that the app works, launch the app: - -```powershell -npx react-native run-macos -``` - -Close the running app from the dock and close the Metro terminal window. - -2) Create the file `macos/SheetJSmacOS-macOS/RCTDocumentPicker.h`: - -```objc title="macos/SheetJSmacOS-macOS/RCTDocumentPicker.h" -#import -@interface RCTDocumentPicker : NSObject -@end -``` - -Create the file `macos/SheetJSmacOS-macOS/RCTDocumentPicker.m`: - -```objc title="macos/SheetJSmacOS-macOS/RCTDocumentPicker.m" -#import -#import - -#import "RCTDocumentPicker.h" - -@implementation RCTDocumentPicker - -RCT_EXPORT_MODULE(); - -RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) -{ - RCTExecuteOnMainQueue(^{ - NSOpenPanel *panel = [NSOpenPanel openPanel]; - [panel setCanChooseDirectories:NO]; - [panel setAllowsMultipleSelection:NO]; - [panel setMessage:@"Select a spreadsheet to read"]; - - [panel beginWithCompletionHandler:^(NSInteger result){ - if (result == NSModalResponseOK) { - NSURL *selected = [[panel URLs] objectAtIndex:0]; - NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil]; - if(hFile) { - NSData *data = [hFile readDataToEndOfFile]; - resolve([data base64EncodedStringWithOptions:0]); - } else reject(@"read_failure", @"Could not read selected file!", nil); - } else reject(@"select_failure", @"No file selected!", nil); - }]; - }); -} -@end -``` - -3) Edit the project file `macos/SheetJSmacOS.xcodeproj/project.pbxproj`. - -There are four places where lines must be added: - -A) Immediately after `/* Begin PBXBuildFile section */` - -```plist -/* Begin PBXBuildFile section */ -// highlight-next-line - 4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */; }; - 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; -``` - -B) Immediately after `/* Begin PBXFileReference section */` - -```plist -/* Begin PBXFileReference section */ -// highlight-start - 4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTDocumentPicker.h; path = "SheetJSMacOS-macOS/RCTDocumentPicker.h"; sourceTree = ""; }; - 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RCTDocumentPicker.m; path = "SheetJSMacOS-macOS/RCTDocumentPicker.m"; sourceTree = ""; }; -// highlight-end - 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; -``` - -C) The goal is to add a reference to the `PBXSourcesBuildPhase` block for the -`macOS` target. To determine this, look in the `PBXNativeTarget section` for -a block with the comment `SheetJSmacOS-macOS`: - -```plist -/* Begin PBXNativeTarget section */ -... - productType = "com.apple.product-type.application"; - }; -// highlight-next-line - 514201482437B4B30078DB4F /* SheetJSmacOS-macOS */ = { - isa = PBXNativeTarget; -... -/* End PBXNativeTarget section */ -``` - -Within the block, look for `buildPhases` and find the hex string for `Sources`: - -```plist - buildPhases = ( - 1A938104A937498D81B3BD3B /* [CP] Check Pods Manifest.lock */, - 381D8A6F24576A6C00465D17 /* Start Packager */, -// highlight-next-line - 514201452437B4B30078DB4F /* Sources */, - 514201462437B4B30078DB4F /* Frameworks */, - 514201472437B4B30078DB4F /* Resources */, - 381D8A6E24576A4E00465D17 /* Bundle React Native code and images */, - 3689826CA944E2EF44FCBC17 /* [CP] Copy Pods Resources */, - ); -``` - -Search for that hex string (`514201452437B4B30078DB4F` in our example) in the -file and it should show up in a `PBXSourcesBuildPhase` section. Within `files`, -add the highlighted line: - -```plist - 514201452437B4B30078DB4F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( -// highlight-next-line - 4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */, - 514201502437B4B30078DB4F /* ViewController.m in Sources */, - 514201582437B4B40078DB4F /* main.m in Sources */, - 5142014D2437B4B30078DB4F /* AppDelegate.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -``` - -D) The goal is to add file references to the "main group". Search for -`/* Begin PBXProject section */` and there should be one Project object. -Within the project object, look for `mainGroup`: - -```plist -/* Begin PBXProject section */ - 83CBB9F71A601CBA00E9B192 /* Project object */ = { - isa = PBXProject; -... - Base, - ); -// highlight-next-line - mainGroup = 83CBB9F61A601CBA00E9B192; - productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; -... -/* End PBXProject section */ -``` - -Search for that hex string (`83CBB9F61A601CBA00E9B192` in our example) in the -file and it should show up in a `PBXGroup` section. Within `children`, add the -highlighted lines: - -```plist - 83CBB9F61A601CBA00E9B192 = { - isa = PBXGroup; - children = ( -// highlight-start - 4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */, - 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */, -// highlight-end - 5142014A2437B4B30078DB4F /* SheetJSmacOS-macOS */, - 13B07FAE1A68108700A75B9A /* SheetJSmacOS-iOS */, -``` - -4) Replace `App.js` with the following: - -```tsx title="App.js" -import React, { useState, type Node } from 'react'; -import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native'; -import { read, utils, version } from 'xlsx'; -import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; -const DocumentPicker = getEnforcing('DocumentPicker'); - -const App: () => Node = () => { - - const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]); - - return ( - - SheetJS × React Native MacOS {version} - { - try { - const b64 = await DocumentPicker.PickAndRead(); - const wb = read(b64); - setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } )); - } catch(err) { alert(`Error: ${err.message}`); } - }}>Click here to Open File! - - {aoa.map((row,R) => ( - {row.map((cell,C) => ( - {cell} - ))} - ))} - - - ); -}; - -const styles = StyleSheet.create({ - cell: { flex: 4 }, - row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', }, - table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', }, - outer: { marginTop: 32, paddingHorizontal: 24, }, - title: { fontSize: 24, fontWeight: '600', }, - button: { marginTop: 8, fontSize: 18, fontWeight: '400', }, -}); - -export default App; -``` - -5) Test the app: - -```bash -npx react-native run-macos -``` - -Download , then click on "open file". Use the -file picker to select the `pres.xlsx` file and the app will show the data. - -6) Make a release build: - -```bash -xcodebuild -workspace macos/SheetJSmacOS.xcworkspace -scheme SheetJSmacOS-macOS -config Release -``` - -
- -### Reading Files - -Only the main UI thread can show file pickers. This is similar to Web Worker -DOM access limitations in the Web platform. - -This example defines a `PickAndRead` function that will show the file picker, -read the file contents, and return a Base64 string. - -```objc -/* the resolve/reject is projected on the JS side as a Promise */ -RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) -{ - /* perform file picker action in the UI thread */ - // highlight-next-line - RCTExecuteOnMainQueue(^{ - /* create file picker */ - NSOpenPanel *panel = [NSOpenPanel openPanel]; - [panel setCanChooseDirectories:NO]; - [panel setAllowsMultipleSelection:NO]; - [panel setMessage:@"Select a spreadsheet to read"]; - - /* show file picker */ - // highlight-next-line - [panel beginWithCompletionHandler:^(NSInteger result){ - if (result == NSModalResponseOK) { - /* read data and return base64 string */ - NSURL *selected = [[panel URLs] objectAtIndex:0]; - NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil]; - if(hFile) { - NSData *data = [hFile readDataToEndOfFile]; - // highlight-next-line - resolve([data base64EncodedStringWithOptions:0]); - } else reject(@"read_failure", @"Could not read selected file!", nil); - } else reject(@"select_failure", @"No file selected!", nil); - }]; - }); -} -``` - -This module is referenced in the same way as the React Native Windows example: - -```js -import { read } from 'xlsx'; -import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; -const DocumentPicker = getEnforcing('DocumentPicker'); - - -/* ... in some event handler ... */ -async() => { - const b64 = await DocumentPicker.PickAndRead(); - const wb = read(b64); - // DO SOMETHING WITH `wb` HERE -} -``` \ No newline at end of file diff --git a/docz/docs/03-demos/03-desktop/01-electron.md b/docz/docs/03-demos/03-desktop/01-electron.md new file mode 100644 index 0000000..fd74a51 --- /dev/null +++ b/docz/docs/03-demos/03-desktop/01-electron.md @@ -0,0 +1,229 @@ +--- +title: Electron +pagination_prev: demos/mobile +pagination_next: demos/grid +sidebar_position: 1 +sidebar_custom_props: + summary: Embedded NodeJS + Chromium +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported +from the main or the renderer thread. + +Electron presents a `fs` module. The `require('xlsx')` call loads the CommonJS +module, so `XLSX.readFile` and `XLSX.writeFile` work in the renderer thread. + +This demo was tested on 2022 November 07 with Electron 21.2.2 on `darwin-x64`. + +
Complete Example (click to show) + +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. + +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 + +:::caution + +Right-click each link and select "Save Link As...". Left-clicking a link will +try to load the page in your browser. The goal is to save the file contents. + +::: + +2) Run `npm install` to install dependencies. + +3) To verify the app works, run in the test environment: + +```bash +npx -y electron . +``` + +The app will show and you should be able to verify reading and writing by using +the relevant buttons to open files and clicking the export button. + +4) To build a standalone app, run the builder: + +```bash +npm run make +``` + +This will generate the standalone app in the `out\sheetjs-electron-...` folder. +For a recent Intel Mac, the path will be `out/sheetjs-electron-darwin-x64/` + +
+ +### 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'); + +/* 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: [{ + name: "Spreadsheets", + extensions: ["xlsx", "xls", "xlsb", /* ... other formats ... */] + }] + }); + /* write file */ + // highlight-next-line + XLSX.writeFile(workbook, result.filePath); +} +``` + +:::note + +In older versions of Electron, `showSaveDialog` returned the path directly: + +```js +var dialog = require('electron').remote.dialog; + +function exportFile(workbook) { + var result = dialog.showSaveDialog(); + XLSX.writeFile(workbook, result); +} +``` + +::: + +### Reading Files + +Electron offers 3 different ways to read files, two of which use Web APIs. + +**File Input Element** + +File input elements automatically map to standard Web APIs. + +For example, assuming a file input element on the page: + +```html + +``` + +The event handler would process the event as if it were a web event: + +```js +async function handleFile(e) { + const file = e.target.files[0]; + const data = await file.arrayBuffer(); + /* data is an ArrayBuffer */ + const workbook = XLSX.read(data); + + /* DO SOMETHING WITH workbook HERE */ +} +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. + +For example, assuming a DIV on the page: + +```html +
Drop a spreadsheet file here to see sheet data
+``` + +The event handler would process the event as if it were a web event: + +```js +async function handleDrop(e) { + e.stopPropagation(); + e.preventDefault(); + + const file = e.dataTransfer.files[0]; + const data = await file.arrayBuffer(); + /* data is an ArrayBuffer */ + const workbook = XLSX.read(data); + + /* DO SOMETHING WITH workbook HERE */ +} +document.getElementById("drop").addEventListener("drop", handleDrop, false); +``` + +**Electron API** + +[`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'); + +/* 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: [{ + 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 + +In older versions of Electron, `showOpenDialog` returned the path directly: + +```js +var dialog = require('electron').remote.dialog; + +function importFile(workbook) { + var result = dialog.showOpenDialog({ properties: ['openFile'] }); + return XLSX.readFile(result[0]); +} +``` + +::: + +### Electron Breaking Changes + +The first version of this demo used Electron 1.7.5. The current demo includes +the required changes for Electron 19.2.2. + +There are no Electron-specific workarounds in the library, but Electron broke +backwards compatibility multiple times. A summary of changes is noted below. + +:::caution + +Electron 6.x changed the `dialog` API. Methods like `showSaveDialog` originally +returned an array of strings, but now returns a `Promise`. This change was not +documented. + +Electron 9.0.0 and later require the preference `nodeIntegration: true` in order +to `require('xlsx')` in the renderer process. + +Electron 12.0.0 and later also require `worldSafeExecuteJavascript: true` and +`contextIsolation: true`. + +Electron 14+ must use `@electron/remote` instead of `remote`. An `initialize` +call is required to enable Developer Tools in the window. + +::: diff --git a/docz/docs/03-demos/03-desktop/02-nwjs.md b/docz/docs/03-demos/03-desktop/02-nwjs.md new file mode 100644 index 0000000..ff143ae --- /dev/null +++ b/docz/docs/03-demos/03-desktop/02-nwjs.md @@ -0,0 +1,129 @@ +--- +title: NW.js +pagination_prev: demos/mobile +pagination_next: demos/grid +sidebar_position: 2 +sidebar_custom_props: + summary: Embedded Chromium + NodeJS +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The [Standalone scripts](/docs/getting-started/installation/standalone) can be +referenced in a `SCRIPT` tag from the entry point HTML page. + +This demo was tested against NW.js 0.66.0. + +
Complete Example (click to show) + +1) Create a `package.json` file that specifies the entry point: + +```json title="package.json" +{ + "name": "sheetjs-nwjs", + "author": "sheetjs", + "version": "0.0.0", + "main": "index.html", + "dependencies": { + "nw": "~0.66.0", + "xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz" + } +} +``` + +2) Download [`index.html`](pathname:///nwjs/index.html) into the same folder. + +:::caution + +Right-click the link and select "Save Link As...". Left-clicking the link will +try to load the page in your browser. The goal is to save the file contents. + +::: + +3) Run `npm install` to install dependencies + +4) To verify the app works, run in the test environment: + +``` +npx nw . +``` + +The app will show and you should be able to verify reading and writing by using +the file input element to select a spreadsheet and clicking the export button. + +5) To build a standalone app, run the builder: + +``` +npx -p nw-builder nwbuild --mode=build . +``` + +This will generate the standalone app in the `build\sheetjs-nwjs\` folder. + +
+ +### Reading data + +The standard HTML5 `FileReader` techniques from the browser apply to NW.js! + +NW.js handles the OS minutiae for dragging files into app windows. The +[drag and drop snippet](/docs/solutions/input#example-user-submissions) apply +to DIV elements on the page. + +Similarly, file input elements automatically map to standard Web APIs. + +For example, assuming a file input element on the page: + +```html + +``` + +The event handler would process the event as if it were a web event: + +```js +async function handleFile(e) { + const file = e.target.files[0]; + const data = await file.arrayBuffer(); + /* data is an ArrayBuffer */ + const workbook = XLSX.read(data); + + /* DO SOMETHING WITH workbook HERE */ +} +document.getElementById("xlf").addEventListener("change", handleFile, false); +``` + +### Writing data + +File input elements with the attribute `nwsaveas` show UI for saving a file. The +standard trick is to generate a hidden file input DOM element and "click" it. +Since NW.js does not present a `writeFileSync` in the `fs` package, a manual +step is required: + +```js +/* pre-build the hidden nwsaveas input element */ +var input = document.createElement('input'); +input.style.display = 'none'; +input.setAttribute('nwsaveas', 'SheetJSNWDemo.xlsx'); +input.setAttribute('type', 'file'); +document.body.appendChild(input); + +/* show a message if the save is canceled */ +input.addEventListener('cancel',function(){ alert("Save was canceled!"); }); + +/* write to a file on the 'change' event */ +input.addEventListener('change',function(e){ + /* the `value` is the path that the program will write */ + var filename = this.value; + + /* use XLSX.write with type "buffer" to generate a buffer" */ + /* highlight-next-line */ + var wbout = XLSX.write(workbook, {type:'buffer', bookType:"xlsx"}); + /* highlight-next-line */ + fs.writeFile(filename, wbout, function(err) { + if(!err) return alert("Saved to " + filename); + alert("Error: " + (err.message || err)); + }); +}); + +input.click(); +``` diff --git a/docz/docs/03-demos/03-desktop/03-wails.md b/docz/docs/03-demos/03-desktop/03-wails.md new file mode 100644 index 0000000..c15dd98 --- /dev/null +++ b/docz/docs/03-demos/03-desktop/03-wails.md @@ -0,0 +1,221 @@ +--- +title: Wails +pagination_prev: demos/mobile +pagination_next: demos/grid +sidebar_position: 3 +sidebar_custom_props: + summary: Webview + Go Backend +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported +from JavaScript code. + +This demo was tested against Wails `v2.0.0-beta.44.2` on 2022 August 31 using +the Svelte TypeScript starter. + +:::caution + +Wails currently does not provide the equivalent of NodeJS `fs` module. + +The HTML File Input Element does not show a file picker. This is a known bug. + +All raw file operations must be performed in Go code. + +::: + + +The "Complete Example" creates an app that looks like the screenshot: + +![SheetJS Wails MacOS screenshot](pathname:///wails/macos.png) + +
Complete Example (click to show) + +0) [Read Wails "Getting Started" guide and install dependencies.](https://wails.io/docs/gettingstarted/installation) + +1) Create a new Wails app: + +```bash +wails init -n sheetjs-wails -t svelte-ts +``` + +2) Enter the directory: + +```bash +cd sheetjs-wails +``` + +3) Install front-end dependencies: + +```bash +cd frontend +curl -L -o src/assets/logo.png https://sheetjs.com/sketch1024.png +npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz +cd .. +``` + +4) Download source files: + +- Download [`app.go`](pathname:///wails/app.go) and replace `app.go` +- Download [`App.svelte`](pathname:///wails/App.svelte) and replace + `frontend/src/App.svelte` + +5) Build the app with + +```bash +wails build +``` + +At the end, it will print the path to the generated program. Run the program! + +
+ +All operations must be run from Go code. This example passes Base64 strings. + +### Reading Files + +The file picker and reading operations can be combined in one Go function. + +#### Go + +```go +import ( + "context" +// highlight-start + "encoding/base64" + "io/ioutil" + "github.com/wailsapp/wails/v2/pkg/runtime" +// highlight-end +) + +type App struct { + ctx context.Context +} + +// ReadFile shows an open file dialog and returns the data as Base64 string +func (a *App) ReadFile() string { + // highlight-next-line + selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "Select File", + Filters: []runtime.FileFilter{ + { DisplayName: "Excel Workbooks (*.xlsx)", Pattern: "*.xlsx", }, + // ... more filters for more file types + }, + }) + if err != nil { return "" } // The demo app shows an error message + // highlight-next-line + data, err := ioutil.ReadFile(selection) + if err != nil { return "" } // The demo app shows an error message + // highlight-next-line + return base64.StdEncoding.EncodeToString(data) +} +``` + +#### JS + +Wails will automatically create `window.go.main.App.ReadFile` for use in JS: + +```js title="frontend/src/App.svelte" +import { read, utils } from 'xlsx'; + +async function importFile(evt) { +// highlight-start + const b64 = window['go']['main']['App']['ReadFile'](); + const wb = read(b64, { type: "base64" }); +// highlight-end + const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet + html = utils.sheet_to_html(ws); // generate HTML and update state +} +``` + +### Writing Files + +There is a multi-part dance since the library needs the file extension. + +1) Show the save file picker in Go, pass back to JS + +2) Generate the file data in JS, pass the data back to Go + +3) Write to file in Go + +##### Go + +Two Go functions will be exposed. + +- `SaveFile` will show the file picker and return the path: + +```go +import ( + "context" +// highlight-start + "github.com/wailsapp/wails/v2/pkg/runtime" +// highlight-end +) + +type App struct { + ctx context.Context +} + +func (a *App) SaveFile() string { +// highlight-next-line + selection, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: "Select File", + DefaultFilename: "SheetJSWails.xlsx", + Filters: []runtime.FileFilter{ + { DisplayName: "Excel Workbooks (*.xlsx)", Pattern: "*.xlsx", }, + // ... more filters for more file types + }, + }) + if err != nil { return "" } // The demo app shows an error message + return selection +} +``` + +- `WriteFile` performs the file write given a Base64 string and file path: + +```go +import ( + "context" +// highlight-start + "encoding/base64" + "io/ioutil" +// highlight-end +) + +type App struct { + ctx context.Context +} + +func (a *App) WriteFile(b64 string, path string) { + // highlight-start + buf, _ := base64.StdEncoding.DecodeString(b64); + _ = ioutil.WriteFile(path, buf, 0644); + // highlight-end +} +``` + +#### JS + +Wails will automatically create bindings for use in JS: + +```js +import { utils, write } from 'xlsx'; + +async function exportFile(wb) { + /* generate workbook */ + const elt = tbl.getElementsByTagName("TABLE")[0]; + const wb = utils.table_to_book(elt); + + /* show save picker and get path */ + const path = await window['go']['main']['App']['SaveFile'](); + + /* generate base64 string based on the path */ + const b64 = write(wb, { bookType: path.slice(path.lastIndexOf(".")+1), type: "base64" }); + + /* write to file */ + await window['go']['main']['App']['WriteFile'](b64, path); + // The demo shows a success message at this point +} +``` diff --git a/docz/docs/03-demos/03-desktop/04-tauri.md b/docz/docs/03-demos/03-desktop/04-tauri.md new file mode 100644 index 0000000..4abbedb --- /dev/null +++ b/docz/docs/03-demos/03-desktop/04-tauri.md @@ -0,0 +1,213 @@ +--- +title: Tauri +pagination_prev: demos/mobile +pagination_next: demos/grid +sidebar_position: 4 +sidebar_custom_props: + summary: Webview + Rust Backend +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported +from JavaScript code. + +This demo was tested against Tauri 1.0.5 on 2022 August 13. + +:::note + +Tauri currently does not provide the equivalent of NodeJS `fs` module. The raw +`@tauri-apps/api` methods used in the examples are not expected to change. + +::: + +`http` and `dialog` must be explicitly allowed in `tauri.conf.json`: + +```json title="tauri.conf.json" + "allowlist": { + "all": true, + "http": { + "all": true, + "request": true, + "scope": ["https://**"] + }, + "dialog": { + "all": true + } +``` + +The "Complete Example" creates an app that looks like the screenshot: + +![SheetJS Tauri MacOS screenshot](pathname:///tauri/macos.png) + +
Complete Example (click to show) + +0) [Read Tauri "Getting Started" guide and install dependencies.](https://tauri.app/v1/guides/getting-started/prerequisites) + +1) Create a new Tauri app: + +```bash +npm create tauri-app +``` + +When prompted: + +- App Name: `SheetJSTauri` +- Window Title: `SheetJS + Tauri` +- UI recipe: `create-vite` +- Add "@tauri-apps/api": `Y` +- ViteJS template: `vue-ts` + +2) Enter the directory: + +```bash +cd SheetJSTauri +``` + +Open `package.json` with a text editor and add the highlighted lines: + +```json title="package.json" +{ + "name": "SheetJSTauri", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { +// highlight-next-line + "@tauri-apps/api": "^1.0.2", + "vue": "^3.2.37", +// highlight-next-line + "xlsx": "https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz" + }, + "devDependencies": { +// highlight-next-line + "@tauri-apps/cli": "^1.0.5", + "@vitejs/plugin-vue": "^3.0.3", + "typescript": "^4.6.4", + "vite": "^3.0.7", + "vue-tsc": "^0.39.5" + } +} +``` + +3) Install dependencies: + +```bash +npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz +``` + +4) Enable operations by adding the highlighted lines to `tauri.conf.json`: + +```json title="src-tauri/tauri.conf.json" + "tauri": { + "allowlist": { +// highlight-start + "http": { + "all": true, + "request": true, + "scope": ["https://**"] + }, + "dialog": { + "all": true + }, +// highlight-end + "all": true + } +``` + +In the same file, look for the `"identifier"` key and replace the value with `com.sheetjs.tauri`: + +```json title="src-tauri/tauri.conf.json" + "icons/icon.ico" + ], + // highlight-next-line + "identifier": "com.sheetjs.tauri", + "longDescription": "", +``` + + +5) Download [`App.vue`](pathname:///tauri/App.vue) and replace `src/App.vue` + with the downloaded script. + +6) Build the app with + +```bash +npm run tauri build +``` + +At the end, it will print the path to the generated program. Run the program! + +
+ +### Reading Files + +There are two steps to reading files: obtaining a path and reading binary data: + +```js +import { read } from 'xlsx'; +import { open } from '@tauri-apps/api/dialog'; +import { readBinaryFile } from '@tauri-apps/api/fs'; + +const filters = [ + {name: "Excel Binary Workbook", extensions: ["xlsb"]}, + {name: "Excel Workbook", extensions: ["xlsx"]}, + {name: "Excel 97-2004 Workbook", extensions: ["xls"]}, + // ... other desired formats ... +]; + +async function openFile() { + /* show open file dialog */ + const selected = await open({ + title: "Open Spreadsheet", + multiple: false, + directory: false, + filters + }); + + /* read data into a Uint8Array */ + const d = await readBinaryFile(selected); + + /* parse with SheetJS */ + const wb = read(d); + return wb; +} +``` + +### Writing Files + +There are two steps to writing files: obtaining a path and writing binary data: + +```js +import { write } from 'xlsx'; +import { save } from '@tauri-apps/api/dialog'; +import { writeBinaryFile } from '@tauri-apps/api/fs'; + +const filters = [ + {name: "Excel Binary Workbook", extensions: ["xlsb"]}, + {name: "Excel Workbook", extensions: ["xlsx"]}, + {name: "Excel 97-2004 Workbook", extensions: ["xls"]}, + // ... other desired formats ... +]; + +async function saveFile(wb) { + /* show save file dialog */ + const selected = await save({ + title: "Save to Spreadsheet", + filters + }); + + /* Generate workbook */ + const bookType = selected.slice(selected.lastIndexOf(".") + 1); + const d = write(wb, {type: "buffer", bookType}); + + /* save data to file */ + await writeBinaryFile(selected, d); +} +``` diff --git a/docz/docs/03-demos/03-desktop/05-neutralino.md b/docz/docs/03-demos/03-desktop/05-neutralino.md new file mode 100644 index 0000000..9fc8fd1 --- /dev/null +++ b/docz/docs/03-demos/03-desktop/05-neutralino.md @@ -0,0 +1,246 @@ +--- +title: NeutralinoJS +pagination_prev: demos/mobile +pagination_next: demos/grid +sidebar_position: 5 +sidebar_custom_props: + summary: Webview + Lightweight Extensions +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The [Standalone build](/docs/getting-started/installation/standalone) can be added +to the entry `index.html` + +This demo was tested against "binaries" `4.7.0` and "client" `3.6.0` + +:::note + +NeutralinoJS currently does not provide the equivalent of NodeJS `fs` module. +The raw `Neutralino.filesystem` and `Neutralino.os` methods are used. + +::: + +The `os` and `filesystem` modules must be enabled in `neutralino.conf.json`. +The starter already enables `os` so typically one line must be added: + +```json title="neutralino.config.json" + "nativeAllowList": [ + "app.*", + "os.*", +// highlight-next-line + "filesystem.*", + "debug.log" + ], +``` + +The "Complete Example" creates an app that looks like the screenshot: + +![SheetJS NeutralinoJS MacOS screenshot](pathname:///neu/macos.png) + +:::caution + +At the time of writing, `filters` did not work as expected on MacOS. They have +been omitted in the example and commented in the code snippets + +::: + +
Complete Example (click to show) + +The app core state will be the HTML table. Reading files will add the table to +the window. Writing files will parse the table into a spreadsheet. + +1) Create a new NeutralinoJS app: + +```bash +npx @neutralinojs/neu create sheetjs-neu +cd sheetjs-neu +``` + +2) Download the standalone script and place in `resources/js/main.js`: + +```bash +curl -L -o resources/js/xlsx.full.min.js https://cdn.sheetjs.com/xlsx-latest/package/dist/xlsx.full.min.js +``` + +3) Add the highlighted lines to `neutralino.conf.json` in `nativeAllowList`: + +```json title="neutralino.config.json" + "nativeAllowList": [ + "app.*", +// highlight-start + "os.*", + "filesystem.*", +// highlight-end + "debug.log" + ], +``` + +4) Set up skeleton app and print version info: + +- Edit `resources/index.html` and replace the `` with the code below: + +```html title="resources/index.html" + +
+

SheetJS × NeutralinoJS

+ + +
+
+ + + + + +``` + +- Append the following code to `resources/styles.css` to center the table: + +```css title="resources/styles.css" +#info { + width:100%; + text-align: unset; +} +table { + margin: 0 auto; +} +``` + +- Print the version number in the `showInfo` method of `resources/js/main.js`: + +```js title="resources/js/main.js" + ${NL_APPID} is running on port ${NL_PORT} inside ${NL_OS} +

+ server: v${NL_VERSION} . client: v${NL_CVERSION} +// highlight-start +

+ SheetJS version ${XLSX.version} +// highlight-end + `; +``` + +5) Run the app: + +```bash +npx @neutralinojs/neu run +``` + +You should see `SheetJS Version ` followed by the library version number. + +6) Add the following code to the bottom of `resources/js/main.js`: + +```js +(async() => { + const ab = await (await fetch("https://sheetjs.com/pres.numbers")).arrayBuffer(); + const wb = XLSX.read(ab); + const ws = wb.Sheets[wb.SheetNames[0]]; + document.getElementById('info').innerHTML = XLSX.utils.sheet_to_html(ws); +})(); +``` + +Save the source file, close the app and re-run the command from step 5. + +When the app loads, a table should show in the main screen. + +7) Add `importFile` and `exportFile` to the bottom of `resources/js/main.js`: + +```js +async function importData() { + /* show open dialog */ + const [filename] = await Neutralino.os.showOpenDialog('Open a spreadsheet'); + + /* read data */ + const ab = await Neutralino.filesystem.readBinaryFile(filename); + const wb = XLSX.read(ab); + + /* make table */ + const ws = wb.Sheets[wb.SheetNames[0]]; + document.getElementById('info').innerHTML = XLSX.utils.sheet_to_html(ws); +} + +async function exportData() { + /* show save dialog */ + const filename = await Neutralino.os.showSaveDialog('Save to file'); + + /* make workbook */ + const tbl = document.getElementById('info').querySelector("table"); + const wb = XLSX.utils.table_to_book(tbl); + + /* make file */ + const bookType = filename.slice(filename.lastIndexOf(".") + 1); + const data = XLSX.write(wb, { bookType, type: "buffer" }); + await Neutralino.filesystem.writeBinaryFile(filename, data); +} +``` + +Save the source file, close the app and re-run the command from step 5. + +When the app loads, click the "Import File" button and select a spreadsheet to +see the contents. Click "Export File" and enter `SheetJSNeu.xlsx` to write. + +8) Build production apps: + +```bash +npx @neutralinojs/neu run +``` + +Platform-specific programs will be created in the `dist` folder. + +
+ +### Reading Files + +There are two steps to reading files: obtaining a path and reading binary data: + +```js +const filters = [ + {name: "Excel Binary Workbook", extensions: ["xlsb"]}, + {name: "Excel Workbook", extensions: ["xlsx"]}, +] + +async function openFile() { + /* show open file dialog */ + const [filename] = await Neutralino.os.showOpenDialog( + 'Open a spreadsheet', + { /* filters, */ multiSelections: false } + ); + + /* read data into an ArrayBuffer */ + const ab = await Neutralino.filesystem.readBinaryFile(filename); + + /* parse with SheetJS */ + const wb = XLSX.read(ab); + return wb; +} +``` + +This method can be called from a button click or other event. + +### Writing Files + +There are two steps to writing files: obtaining a path and writing binary data: + +```js +const filters = [ + {name: "Excel Binary Workbook", extensions: ["xlsb"]}, + {name: "Excel Workbook", extensions: ["xlsx"]}, +] + +async function saveFile(wb) { + /* show save file dialog */ + const filename = await Neutralino.os.showSaveDialog( + 'Save to file', + { /* filters */ } + ); + + /* Generate workbook */ + const bookType = filename.slice(filename.lastIndexOf(".") + 1); + const data = XLSX.write(wb, { bookType, type: "buffer" }); + + /* save data to file */ + await Neutralino.filesystem.writeBinaryFile(filename, data); +} +``` + diff --git a/docz/docs/03-demos/03-desktop/06-reactnative.md b/docz/docs/03-demos/03-desktop/06-reactnative.md new file mode 100644 index 0000000..10c107f --- /dev/null +++ b/docz/docs/03-demos/03-desktop/06-reactnative.md @@ -0,0 +1,683 @@ +--- +title: React Native for Desktop +pagination_prev: demos/mobile +pagination_next: demos/grid +sidebar_position: 6 +sidebar_custom_props: + summary: Native Components with React +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +:::note + +This section covers React Native for desktop applications. For iOS and Android +applications, [check the mobile demo](/docs/demos/mobile) + +::: + +React Native for Windows + macOS is a backend for React Native that supports +native apps. The Windows backend builds apps for use on Windows 10 / 11, Xbox, +and other supported platforms. The macOS backend supports macOS 10.14 SDK + +The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported +from the main app script. File operations must be written in native code. + +The "Complete Example" creates an app that looks like the screenshots below: + + + + +
WindowsmacOS
+ +![Windows screenshot](pathname:///reactnative/rnw.png) + + + +![macOS screenshot](pathname:///reactnative/rnm.png) + +
+ +## Native Modules + +:::caution + +As with the mobile versions of React Native, file operations are not provided +by the base SDK. The examples include native code for both Windows and macOS. + +The Windows demo assumes some familiarity with C++ / C# and the macOS demo +assumes some familiarity with Objective-C. + +::: + +React Native for Windows + macOS use [Turbo Modules](https://reactnative.dev/docs/the-new-architecture/pillars-turbomodules) +for effortless integration with native libraries and code. + +The demos define a native module named `DocumentPicker`. + +### Reading Files + +The native modules in the demos define a `PickAndRead` function that will show +the file picker, read the file contents, and return a Base64 string. + +Only the main UI thread can show file pickers. This is similar to Web Worker +DOM access limitations in the Web platform. + +_Integration_ + +This module can be referenced from the Turbo Module Registry: + +```js +import { read } from 'xlsx'; +import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; +const DocumentPicker = getEnforcing('DocumentPicker'); + + +/* ... in some event handler ... */ +async() => { + const b64 = await DocumentPicker.PickAndRead(); + const wb = read(b64); + // DO SOMETHING WITH `wb` HERE +} +``` + +_Native Module_ + + + + +React Native Windows supports C++ and C# projects. + + + + +```csharp +[ReactMethod("PickAndRead")] +public async void PickAndRead(IReactPromise result) { + /* perform file picker action in the UI thread */ + // highlight-next-line + context.Handle.UIDispatcher.Post(async() => { try { + /* create file picker */ + var picker = new FileOpenPicker(); + picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; + picker.FileTypeFilter.Add(".xlsx"); + picker.FileTypeFilter.Add(".xls"); + + /* show file picker */ + // highlight-next-line + var file = await picker.PickSingleFileAsync(); + if(file == null) throw new Exception("File not found"); + + /* read data and return base64 string */ + var buf = await FileIO.ReadBufferAsync(file); + // highlight-next-line + result.Resolve(CryptographicBuffer.EncodeToBase64String(buf)); + } catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }}); +} +``` + + + + +```cpp +REACT_METHOD(PickAndRead); +void PickAndRead(ReactPromise promise) noexcept { + auto prom = promise; + /* perform file picker action in the UI thread */ + // highlight-next-line + context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget { + auto p = prom; // promise -> prom -> p dance avoids promise destruction + + /* create file picker */ + FileOpenPicker picker; + picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary); + picker.FileTypeFilter().Append(L".xlsx"); + picker.FileTypeFilter().Append(L".xls"); + + /* show file picker */ + // highlight-next-line + StorageFile file = co_await picker.PickSingleFileAsync(); + if(file == nullptr) { p.Reject("File not Found"); co_return; } + + /* read data and return base64 string */ + auto buf = co_await FileIO::ReadBufferAsync(file); + // highlight-next-line + p.Resolve(CryptographicBuffer::EncodeToBase64String(buf)); + co_return; + }); +} +``` + + + + + + + +React Native macOS supports Objective-C modules + +```objc +/* the resolve/reject is projected on the JS side as a Promise */ +RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + /* perform file picker action in the UI thread */ + // highlight-next-line + RCTExecuteOnMainQueue(^{ + /* create file picker */ + NSOpenPanel *panel = [NSOpenPanel openPanel]; + [panel setCanChooseDirectories:NO]; + [panel setAllowsMultipleSelection:NO]; + [panel setMessage:@"Select a spreadsheet to read"]; + + /* show file picker */ + // highlight-next-line + [panel beginWithCompletionHandler:^(NSInteger result){ + if (result == NSModalResponseOK) { + /* read data and return base64 string */ + NSURL *selected = [[panel URLs] objectAtIndex:0]; + NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil]; + if(hFile) { + NSData *data = [hFile readDataToEndOfFile]; + // highlight-next-line + resolve([data base64EncodedStringWithOptions:0]); + } else reject(@"read_failure", @"Could not read selected file!", nil); + } else reject(@"select_failure", @"No file selected!", nil); + }]; + }); +} +``` + + + + + +## Windows Demo + +This demo was tested against `v0.70.10` on 2023 January 04 in Windows 10. + +:::warning + +There is no simple standalone executable file at the end of the process. + +[The official documentation describes distribution strategies](https://microsoft.github.io/react-native-windows/docs/native-code#distribution) + +::: + +:::note + +React Native Windows supports writing native code in C++ or C#. This demo has +been tested against both application types. + +::: + +0) Follow the ["Getting Started" guide](https://microsoft.github.io/react-native-windows/docs/getting-started) + +:::caution + +NodeJS `v16` is required. There are OS-specific tools for downgrading: + +- [`nvm-windows`](https://github.com/coreybutler/nvm-windows/releases) Windows +- [`n`](https://github.com/tj/n/) Linux, MacOS, WSL, etc. + +::: + +1) Create a new project using React Native `0.70`: + +```powershell +npx react-native init SheetJSWin --template react-native@^0.70.0 +cd .\SheetJSWin\ +``` + +Create the Windows part of the application: + + + + +```powershell +npx react-native-windows-init --no-telemetry --overwrite --language=cs +``` + + + + +```powershell +npx react-native-windows-init --no-telemetry --overwrite +``` + + + + +Install library: + +```powershell +npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz +``` + +To ensure that the app works, launch the app: + +```powershell +npx react-native run-windows --no-telemetry +``` + + + + +2) Create the file `windows\SheetJSWin\DocumentPicker.cs` with the following: + +```csharp title="windows\SheetJSWin\DocumentPicker.cs" +using System; +using System.Threading.Tasks; +using Windows.Security.Cryptography; +using Windows.Storage; +using Windows.Storage.Pickers; +using Microsoft.ReactNative.Managed; + +namespace SheetJSWin { + [ReactModule] + class DocumentPicker { + private ReactContext context; + [ReactInitializer] + public void Initialize(ReactContext reactContext) { context = reactContext; } + + [ReactMethod("PickAndRead")] + public async void PickAndRead(IReactPromise result) { + context.Handle.UIDispatcher.Post(async() => { try { + var picker = new FileOpenPicker(); + picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; + picker.FileTypeFilter.Add(".xlsx"); + picker.FileTypeFilter.Add(".xls"); + + var file = await picker.PickSingleFileAsync(); + if(file == null) throw new Exception("File not found"); + + var buf = await FileIO.ReadBufferAsync(file); + result.Resolve(CryptographicBuffer.EncodeToBase64String(buf)); + } catch(Exception e) { result.Reject(new ReactError { Message = e.Message }); }}); + } + } +} +``` + +3) Add the highlighted line to `windows\SheetJSWin\SheetJSWin.csproj`. Look for +the `ItemGroup` that contains `ReactPackageProvider.cs`: + +```xml title="windows\SheetJSWin\SheetJSWin.csproj" + + + + +``` + + + + +2) Create the file `windows\SheetJSWin\DocumentPicker.h` with the following: + +```cpp title="windows\SheetJSWin\DocumentPicker.h" +#pragma once + +#include +#include +#include "NativeModules.h" + +using namespace winrt::Microsoft::ReactNative; +using namespace winrt::Windows::Storage; +using namespace winrt::Windows::Storage::Pickers; +using namespace winrt::Windows::Security::Cryptography; + +namespace SheetJSWin { + REACT_MODULE(DocumentPicker); + struct DocumentPicker { + REACT_INIT(Initialize); + void Initialize(const ReactContext& reactContext) noexcept { + context = reactContext; + } + + REACT_METHOD(PickAndRead); + void PickAndRead(ReactPromise promise) noexcept { + auto prom = promise; + context.UIDispatcher().Post([prom = std::move(prom)]()->winrt::fire_and_forget { + auto p = prom; + FileOpenPicker picker; + picker.SuggestedStartLocation(PickerLocationId::DocumentsLibrary); + picker.FileTypeFilter().Append(L".xlsx"); + picker.FileTypeFilter().Append(L".xls"); + + StorageFile file = co_await picker.PickSingleFileAsync(); + if(file == nullptr) { p.Reject("File not Found"); co_return; } + + auto buf = co_await FileIO::ReadBufferAsync(file); + p.Resolve(CryptographicBuffer::EncodeToBase64String(buf)); + co_return; + }); + } + + private: + ReactContext context{nullptr}; + }; +} +``` + +3) Add the highlighted line to `windows\SheetJSWin\ReactPackageProvider.cpp`: + +```cpp title="windows\SheetJSWin\ReactPackageProvider.cpp" +#include "ReactPackageProvider.h" +// highlight-next-line +#include "DocumentPicker.h" +#include "NativeModules.h" +``` + + + + +Now the native module will be added to the app. + +4) Remove `App.js` and save the following to `App.tsx`: + +```tsx title="App.tsx" +import React, { useState, type Node } from 'react'; +import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native'; +import { read, utils, version } from 'xlsx'; +import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; +const DocumentPicker = getEnforcing('DocumentPicker'); + +const App: () => Node = () => { + + const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]); + + return ( + + SheetJS × React Native Windows {version} + { + try { + const b64 = await DocumentPicker.PickAndRead(); + const wb = read(b64); + setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } )); + } catch(err) { alert(`Error: ${err.message}`); } + }}>Click here to Open File! + + {aoa.map((row,R) => ( + {row.map((cell,C) => ( + {cell} + ))} + ))} + + + ); +}; + +const styles = StyleSheet.create({ + cell: { flex: 4 }, + row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', }, + table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', }, + outer: { marginTop: 32, paddingHorizontal: 24, }, + title: { fontSize: 24, fontWeight: '600', }, + button: { marginTop: 8, fontSize: 18, fontWeight: '400', }, +}); + +export default App; +``` + +5) Test the app again: + +```powershell +npx react-native run-windows --no-telemetry +``` + +Download , then click on "open file". Use the +file picker to select the `pres.xlsx` file and the app will show the data. + +## macOS Demo + +This demo was tested against `v0.64.30` on 2023 January 04 in MacOS 12.4 + +0) Follow the [React Native](https://reactnative.dev/docs/environment-setup) + guide for React Native CLI on MacOS. + +:::caution + +NodeJS `v16` is required. There are OS-specific tools for downgrading: + +- [`nvm-windows`](https://github.com/coreybutler/nvm-windows/releases) Windows +- [`n`](https://github.com/tj/n/) Linux, MacOS, WSL, etc. + +::: + +1) Create a new project using React Native `0.64`: + +```bash +npx react-native init SheetJSmacOS --template react-native@^0.64.0 +cd SheetJSmacOS +``` + +Create the MacOS part of the application: + +```bash +npx react-native-macos-init --no-telemetry +``` + +Install Library: + +``` +npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz +``` + +To ensure that the app works, launch the app: + +```powershell +npx react-native run-macos +``` + +Close the running app from the dock and close the Metro terminal window. + +2) Create the file `macos/SheetJSmacOS-macOS/RCTDocumentPicker.h`: + +```objc title="macos/SheetJSmacOS-macOS/RCTDocumentPicker.h" +#import +@interface RCTDocumentPicker : NSObject +@end +``` + +Create the file `macos/SheetJSmacOS-macOS/RCTDocumentPicker.m`: + +```objc title="macos/SheetJSmacOS-macOS/RCTDocumentPicker.m" +#import +#import + +#import "RCTDocumentPicker.h" + +@implementation RCTDocumentPicker + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(PickAndRead:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +{ + RCTExecuteOnMainQueue(^{ + NSOpenPanel *panel = [NSOpenPanel openPanel]; + [panel setCanChooseDirectories:NO]; + [panel setAllowsMultipleSelection:NO]; + [panel setMessage:@"Select a spreadsheet to read"]; + + [panel beginWithCompletionHandler:^(NSInteger result){ + if (result == NSModalResponseOK) { + NSURL *selected = [[panel URLs] objectAtIndex:0]; + NSFileHandle *hFile = [NSFileHandle fileHandleForReadingFromURL:selected error:nil]; + if(hFile) { + NSData *data = [hFile readDataToEndOfFile]; + resolve([data base64EncodedStringWithOptions:0]); + } else reject(@"read_failure", @"Could not read selected file!", nil); + } else reject(@"select_failure", @"No file selected!", nil); + }]; + }); +} +@end +``` + +3) Edit the project file `macos/SheetJSmacOS.xcodeproj/project.pbxproj`. + +There are four places where lines must be added: + +A) Immediately after `/* Begin PBXBuildFile section */` + +```plist +/* Begin PBXBuildFile section */ +// highlight-next-line + 4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; +``` + +B) Immediately after `/* Begin PBXFileReference section */` + +```plist +/* Begin PBXFileReference section */ +// highlight-start + 4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RCTDocumentPicker.h; path = "SheetJSMacOS-macOS/RCTDocumentPicker.h"; sourceTree = ""; }; + 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RCTDocumentPicker.m; path = "SheetJSMacOS-macOS/RCTDocumentPicker.m"; sourceTree = ""; }; +// highlight-end + 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; +``` + +C) The goal is to add a reference to the `PBXSourcesBuildPhase` block for the +`macOS` target. To determine this, look in the `PBXNativeTarget section` for +a block with the comment `SheetJSmacOS-macOS`: + +```plist +/* Begin PBXNativeTarget section */ +... + productType = "com.apple.product-type.application"; + }; +// highlight-next-line + 514201482437B4B30078DB4F /* SheetJSmacOS-macOS */ = { + isa = PBXNativeTarget; +... +/* End PBXNativeTarget section */ +``` + +Within the block, look for `buildPhases` and find the hex string for `Sources`: + +```plist + buildPhases = ( + 1A938104A937498D81B3BD3B /* [CP] Check Pods Manifest.lock */, + 381D8A6F24576A6C00465D17 /* Start Packager */, +// highlight-next-line + 514201452437B4B30078DB4F /* Sources */, + 514201462437B4B30078DB4F /* Frameworks */, + 514201472437B4B30078DB4F /* Resources */, + 381D8A6E24576A4E00465D17 /* Bundle React Native code and images */, + 3689826CA944E2EF44FCBC17 /* [CP] Copy Pods Resources */, + ); +``` + +Search for that hex string (`514201452437B4B30078DB4F` in our example) in the +file and it should show up in a `PBXSourcesBuildPhase` section. Within `files`, +add the highlighted line: + +```plist + 514201452437B4B30078DB4F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( +// highlight-next-line + 4717DC6A28CC499A00A9BE56 /* RCTDocumentPicker.m in Sources */, + 514201502437B4B30078DB4F /* ViewController.m in Sources */, + 514201582437B4B40078DB4F /* main.m in Sources */, + 5142014D2437B4B30078DB4F /* AppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +``` + +D) The goal is to add file references to the "main group". Search for +`/* Begin PBXProject section */` and there should be one Project object. +Within the project object, look for `mainGroup`: + +```plist +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; +... + Base, + ); +// highlight-next-line + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; +... +/* End PBXProject section */ +``` + +Search for that hex string (`83CBB9F61A601CBA00E9B192` in our example) in the +file and it should show up in a `PBXGroup` section. Within `children`, add the +highlighted lines: + +```plist + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( +// highlight-start + 4717DC6828CC495400A9BE56 /* RCTDocumentPicker.h */, + 4717DC6928CC499A00A9BE56 /* RCTDocumentPicker.m */, +// highlight-end + 5142014A2437B4B30078DB4F /* SheetJSmacOS-macOS */, + 13B07FAE1A68108700A75B9A /* SheetJSmacOS-iOS */, +``` + +4) Replace `App.js` with the following: + +```tsx title="App.js" +import React, { useState, type Node } from 'react'; +import { SafeAreaView, ScrollView, StyleSheet, Text, TouchableHighlight, View } from 'react-native'; +import { read, utils, version } from 'xlsx'; +import { getEnforcing } from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; +const DocumentPicker = getEnforcing('DocumentPicker'); + +const App: () => Node = () => { + + const [ aoa, setAoA ] = useState(["SheetJS".split(""), "5433795".split("")]); + + return ( + + SheetJS × React Native MacOS {version} + { + try { + const b64 = await DocumentPicker.PickAndRead(); + const wb = read(b64); + setAoA(utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { header: 1 } )); + } catch(err) { alert(`Error: ${err.message}`); } + }}>Click here to Open File! + + {aoa.map((row,R) => ( + {row.map((cell,C) => ( + {cell} + ))} + ))} + + + ); +}; + +const styles = StyleSheet.create({ + cell: { flex: 4 }, + row: { flexDirection: 'row', justifyContent: 'space-evenly', padding: 10, backgroundColor: 'white', }, + table: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', }, + outer: { marginTop: 32, paddingHorizontal: 24, }, + title: { fontSize: 24, fontWeight: '600', }, + button: { marginTop: 8, fontSize: 18, fontWeight: '400', }, +}); + +export default App; +``` + +5) Test the app: + +```bash +npx react-native run-macos +``` + +Download , then click on "open file". Use the +file picker to select the `pres.xlsx` file and the app will show the data. + +6) Make a release build: + +```bash +xcodebuild -workspace macos/SheetJSmacOS.xcworkspace -scheme SheetJSmacOS-macOS -config Release +``` diff --git a/docz/docs/03-demos/03-desktop/_category_.json b/docz/docs/03-demos/03-desktop/_category_.json new file mode 100644 index 0000000..74aac8f --- /dev/null +++ b/docz/docs/03-demos/03-desktop/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Desktop Applications", + "position": 3 +} diff --git a/docz/docs/03-demos/03-desktop/index.md b/docz/docs/03-demos/03-desktop/index.md new file mode 100644 index 0000000..54007b1 --- /dev/null +++ b/docz/docs/03-demos/03-desktop/index.md @@ -0,0 +1,37 @@ +--- +title: Desktop Applications +pagination_prev: demos/mobile +pagination_next: demos/grid +--- + +import DocCardList from '@theme/DocCardList'; +import {useCurrentSidebarCategory} from '@docusaurus/theme-common'; + +Web technologies like JavaScript and HTML have been adapted to the traditional +app space. Typically these frameworks bundle a JavaScript engine as well as a +windowing framework. SheetJS is compatible with many app frameworks. + +Demos for common tools are included in separate pages: + +
    {useCurrentSidebarCategory().items.map((item, index) => { + const listyle = (item.customProps?.icon) ? { + listStyleImage: `url("${item.customProps.icon}")` + } : {}; + return (
  • + {item.label}{item.customProps?.summary && (" - " + item.customProps.summary)} +
  • ); +})}
+ +:::note Recommendation + +Electron is the most established and widely-used framework. With deep support +for NodeJS modules and consistent user interfaces, it is the recommended choice +for new projects and for web developers. + +Frameworks like Wails are compelling alternatives for teams with experience in +other programming languages. + +Frameworks like React Native generate applications that use native UI elements. + +::: + diff --git a/docz/docs/03-demos/04-grid.md b/docz/docs/03-demos/04-grid.md index ffbcf2b..b362438 100644 --- a/docz/docs/03-demos/04-grid.md +++ b/docz/docs/03-demos/04-grid.md @@ -1,5 +1,6 @@ --- title: Data Grids and Tables +pagination_prev: demos/desktop/index --- Various JavaScript UI components provide a more interactive editing experience. diff --git a/docz/docs/03-demos/index.md b/docz/docs/03-demos/index.md index ff851a9..18139f2 100644 --- a/docz/docs/03-demos/index.md +++ b/docz/docs/03-demos/index.md @@ -19,7 +19,7 @@ run in the web browser, demos will include interactive examples. - [`Web SQL Database`](/docs/demos/database#websql) - [`IndexedDB`](/docs/demos/database#indexeddb) -### Frameworks +### Web Frameworks - [`Angular`](/docs/demos/angular) - [`React`](/docs/demos/react) @@ -38,6 +38,15 @@ run in the web browser, demos will include interactive examples. - [`angular-ui-grid`](/docs/demos/grid#angular-ui-grid) - [`material ui`](/docs/demos/grid#material-ui-table) +### Desktop App Frameworks + +- [`Electron`](/docs/demos/desktop/electron) +- [`NW.js`](/docs/demos/desktop/nwjs) +- [`Wails`](/docs/demos/desktop/wails) +- [`Tauri`](/docs/demos/desktop/tauri) +- [`NeutralinoJS`](/docs/demos/desktop/neutralino) +- [`React Native for Desktop`](/docs/demos/desktop/reactnative) + ### Platforms and Integrations - [`Command-Line Tools`](/docs/demos/cli) @@ -45,10 +54,6 @@ run in the web browser, demos will include interactive examples. - [`NodeJS Server-Side Processing`](/docs/demos/server#nodejs) - [`Deno Server-Side Processing`](/docs/demos/server#deno) - [`Content Management and Static Sites`](/docs/demos/content) -- [`Electron`](/docs/demos/desktop#electron) -- [`NW.js`](/docs/demos/desktop#nwjs) -- [`Tauri`](/docs/demos/desktop#tauri) -- [`Wails`](/docs/demos/desktop#wails) - [`Chrome and Chromium Extensions`](/docs/demos/chromium) - [`Google Sheets API`](/docs/demos/gsheet) - [`ExtendScript for Adobe Apps`](/docs/demos/extendscript) diff --git a/docz/static/reactnative/rnm.png b/docz/static/reactnative/rnm.png new file mode 100644 index 0000000..3759114 Binary files /dev/null and b/docz/static/reactnative/rnm.png differ diff --git a/docz/static/reactnative/rnw.png b/docz/static/reactnative/rnw.png new file mode 100644 index 0000000..b6e40a4 Binary files /dev/null and b/docz/static/reactnative/rnw.png differ