diff --git a/docz/docs/03-demos/03-database.md b/docz/docs/03-demos/03-database.md index 0642c5a..2a79707 100644 --- a/docz/docs/03-demos/03-database.md +++ b/docz/docs/03-demos/03-database.md @@ -417,7 +417,7 @@ for(var i = 0; i < stmts.length; ++i) await new Promise((res, rej) => { The result of a SQL SELECT statement is a `SQLResultSet`. The `rows` property is a `SQLResultSetRowList`. It is an "array-like" structure that has `length` -and properies like `0`, `1`, etc. However, this is not a real Array object. +and properties like `0`, `1`, etc. However, this is not a real Array object. A real Array can be created using `Array.from`: ```js diff --git a/docz/docs/03-demos/08-ml.mdx b/docz/docs/03-demos/08-ml.mdx index c951ad7..4ce4429 100644 --- a/docz/docs/03-demos/08-ml.mdx +++ b/docz/docs/03-demos/08-ml.mdx @@ -223,9 +223,7 @@ var dataset = Float32Array.from(column); `XLSX.utils.aoa_to_sheet` can generate a worksheet from an array of arrays. ML libraries typically provide APIs to pull an array of arrays, but it will -be transponsed -a row-major array of arrays. To export multiple data -sets, "transpose" the data: +be transposed. To export multiple data sets, manually "transpose" the data: ```js /* assuming data is an array of typed arrays */ diff --git a/docz/docs/03-demos/12-legacy.md b/docz/docs/03-demos/12-legacy.md index 80150ea..a94154f 100644 --- a/docz/docs/03-demos/12-legacy.md +++ b/docz/docs/03-demos/12-legacy.md @@ -7,7 +7,7 @@ import current from '/version.js'; Over the years, many frameworks have been released. Some were popular years ago but have waned in recent years. There are still many deployments using these -frameworks and it is oftentimes esasier to continue maintenance than to rewrite +frameworks and it is oftentimes easier to continue maintenance than to rewrite using modern web techniques. SheetJS libraries strive to maintain broad browser and JS engine compatibility. diff --git a/docz/docs/03-demos/19-mobile.md b/docz/docs/03-demos/19-mobile.md index afac178..2708582 100644 --- a/docz/docs/03-demos/19-mobile.md +++ b/docz/docs/03-demos/19-mobile.md @@ -1370,6 +1370,15 @@ id,content ## Ionic +:::note + +This demo was tested on an Intel Mac on 2022 August 18 with Cordova backend. +The file integration uses `@ionic-native/file` version `5.36.0`. + +The iOS simulator runs iOS 15.5 on an iPod Touch 7th Gen. + +::: + :::warning Telemetry Before starting this demo, manually disable telemetry. On Linux and macOS: @@ -1400,8 +1409,7 @@ npx @capacitor/cli telemetry :::caution -The latest version of Ionic uses CapacitorJS. These notes are for older apps -using Cordova +The latest version of Ionic uses CapacitorJS. These notes are for Cordova apps. ::: @@ -1432,3 +1440,77 @@ let blob = new Blob([wbout], {type: 'application/octet-stream'}); this.file.writeFile(url, filename, blob, {replace: true}); ``` +### Demo + +The demo uses Cordova. + +
Complete Example (click to show) + +0) Disable telemetry as noted in the warning. + +Install required global dependencies: + +```bash +npm i -g cordova-res @angular/cli native-run +``` + +Follow the [React Native demo](#demo) to ensure iOS and Android sims are ready. + + +1) Create a new project: + +```bash +npx @ionic/cli start SheetJSIonic blank --type angular --cordova --quiet --no-git --no-link --confirm +``` + +If a prompt discusses Cordova and Capacitor, enter `Yes` to continue. + +If a prompt asks about creating an Ionic account, enter `N` to opt out. + +2) Set up Cordova: + +```bash +npx @ionic/cli cordova platform add ios --confirm +npx @ionic/cli cordova plugin add cordova-plugin-file +npm install --save @ionic-native/core @ionic-native/file @ionic/cordova-builders +``` + +3) Install dependencies: + +```bash +npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz +``` + +4) Add `@ionic-native/file` to the module. Differences highlighted below: + +```ts title="src/app/app.module.ts" +import { AppComponent } from './app.component'; +import { AppRoutingModule } from './app-routing.module'; + +// highlight-next-line +import { File } from '@ionic-native/file/ngx'; + +@NgModule({ + declarations: [AppComponent], + imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule], + + // highlight-next-line + providers: [File, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +5) Download [`home.page.ts`](pathname:///ionic/home.page.ts) and replace: + +```bash +curl -o src/app/home/home.page.ts -L https://docs.sheetjs.com/ionic/home.page.ts +``` + +6) Test the app: + +```bash +npx @ionic/cli cordova emulate ios +``` + +
\ No newline at end of file diff --git a/docz/docs/03-demos/21-react.md b/docz/docs/03-demos/21-react.md index 9823f80..e56147a 100644 --- a/docz/docs/03-demos/21-react.md +++ b/docz/docs/03-demos/21-react.md @@ -71,15 +71,13 @@ console.log(data); A component will typically map over the data. The following example generates a TABLE with a row for each President: -```tsx title="src/SheetJSReactAoO.tsx" -import React, { useEffect, useState } from "react"; -import { read, utils } from 'xlsx'; - -interface President { Name: string; Index: number; } +```jsx title="src/SheetJSReactAoO.js" +import React, { useCallback, useEffect, useState } from "react"; +import { read, utils, writeFileXLSX } from 'xlsx'; export default function SheetJSReactAoO() { /* the component state is an array of presidents */ - const [pres, setPres] = useState([]); + const [pres, setPres] = useState([]); /* Fetch and update the state once */ useEffect(() => { (async() => { @@ -87,11 +85,20 @@ export default function SheetJSReactAoO() { // highlight-start const wb = read(f); // parse the array buffer const ws = wb.Sheets[wb.SheetNames[0]]; // get the first worksheet - const data = utils.sheet_to_json(ws); // generate objects + const data = utils.sheet_to_json(ws); // generate objects setPres(data); // update state // highlight-end })(); }, []); + /* get state data and export to XLSX */ + const exportFile = useCallback(() => { + // highlight-next-line + const ws = utils.json_to_sheet(pres); + const wb = utils.book_new(); + utils.book_append_sheet(wb, ws, "Data"); + writeFileXLSX(wb, "SheetJSReactAoO.xlsx"); + }, [pres]); + return ( { /* generate row for each president */ // highlight-start @@ -101,7 +108,9 @@ export default function SheetJSReactAoO() { )) // highlight-end } -
NameIndex
); + + + ); } ``` @@ -115,13 +124,15 @@ The `sheet_to_html` function generates HTML that is aware of merges and other worksheet features. React `dangerouslySetInnerHTML` attribute allows code to set the `innerHTML` attribute, effectively inserting the code into the page: -```tsx title="src/SheetJSReactHTML.tsx" -import React, { useEffect, useState } from "react"; -import { read, utils } from 'xlsx'; +```jsx title="src/SheetJSReactHTML.js" +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { read, utils, writeFileXLSX } from 'xlsx'; export default function SheetJSReactHTML() { /* the component state is an HTML string */ - const [html, setHtml] = useState(""); + const [html, setHtml] = useState(""); + /* the ref is used in export */ + const tbl = useRef(null); /* Fetch and update the state once */ useEffect(() => { (async() => { @@ -134,8 +145,20 @@ export default function SheetJSReactHTML() { // highlight-end })(); }, []); + /* get live table and export to XLSX */ + const exportFile = useCallback(() => { + // highlight-start + const elt = tbl.current.getElementsByTagName("TABLE")[0]; + const wb = utils.table_to_book(elt); + // highlight-end + writeFileXLSX(wb, "SheetJSReactHTML.xlsx"); + }, [tbl]); + + return ( <> + // highlight-next-line - return (
); +
+ ); } ``` diff --git a/docz/docs/03-demos/22-vue.md b/docz/docs/03-demos/22-vue.md index 925c4c8..837d77f 100644 --- a/docz/docs/03-demos/22-vue.md +++ b/docz/docs/03-demos/22-vue.md @@ -74,7 +74,7 @@ a TABLE with a row for each President: ```html title="src/SheetJSVueAoO.vue" ``` @@ -114,9 +124,10 @@ attribute, effectively inserting the code into the page: ```html title="src/SheetJSVueHTML.vue" ``` diff --git a/docz/docs/03-demos/23-angular.md b/docz/docs/03-demos/23-angular.md index 2974eb4..661d56e 100644 --- a/docz/docs/03-demos/23-angular.md +++ b/docz/docs/03-demos/23-angular.md @@ -16,7 +16,7 @@ and TypeScript familiarity is assumed. Other demos cover general Angular deployments, including: - [iOS and Android applications powered by NativeScript](./mobile#nativescript) -- [iOS and Android applications powered by ionic](./mobile#nativescript) +- [iOS and Android applications powered by ionic](./mobile#ionic) :::warning @@ -80,12 +80,12 @@ console.log(data); ] ``` -A component will typically loop over the data uaing `*ngFor`. The following +A component will typically loop over the data using `*ngFor`. The following example generates a TABLE with a row for each President: ```ts title="src/app/app.component.ts" import { Component } from '@angular/core'; -import { read, utils } from 'xlsx'; +import { read, utils, writeFileXLSX } from 'xlsx'; interface President { Name: string; Index: number }; @@ -101,7 +101,9 @@ interface President { Name: string; Index: number }; {{row.Index}} // highlight-end - + + +
` }) @@ -122,6 +124,14 @@ export class AppComponent { this.rows = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); })(); } + /* get state data and export to XLSX */ + onSave(): void { + // highlight-next-line + const ws = utils.json_to_sheet(this.rows); + const wb = utils.book_new(); + utils.book_append_sheet(wb, ws, "Data"); + writeFileXLSX(wb, "SheetJSAngularAoO.xlsx"); + } } ``` @@ -137,20 +147,22 @@ and should therefore be safe to pass to an `innerHTML`-bound variable, but the `DomSanitizer` approach is strongly recommended: ```ts title="src/app/app.component.ts" -import { Component } from '@angular/core'; +import { Component, ElementRef, ViewChild } from '@angular/core'; // highlight-next-line import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; -import { read, utils } from 'xlsx'; +import { read, utils, writeFileXLSX } from 'xlsx'; @Component({ selector: 'app-root', // highlight-next-line - template: `
` + template: `
+ ` }) export class AppComponent { // highlight-start constructor(private sanitizer: DomSanitizer) {} html: SafeHtml = ""; + @ViewChild('tableau') tabeller!: ElementRef; // highlight-end ngOnInit(): void { (async() => { /* Download from https://sheetjs.com/pres.numbers */ @@ -166,6 +178,14 @@ export class AppComponent { this.html = this.sanitizer.bypassSecurityTrustHtml(h); // highlight-end })(); } + /* get live table and export to XLSX */ + onSave(): void { + // highlight-start + const elt = this.tabeller.nativeElement.getElementsByTagName("TABLE")[0]; + const wb = utils.table_to_book(elt); + // highlight-end + writeFileXLSX(wb, "SheetJSAngularHTML.xlsx"); + } } ``` diff --git a/docz/docs/03-demos/24-server.md b/docz/docs/03-demos/24-server.md new file mode 100644 index 0000000..1690076 --- /dev/null +++ b/docz/docs/03-demos/24-server.md @@ -0,0 +1,225 @@ +--- +sidebar_position: 23 +title: HTTP Server Processing +--- + +Server-Side JS platforms like NodeJS and Deno have built-in APIs for listening +on network interfaces. They provide wrappers for requests and responses. + +## Overview + +#### Reading Data + +Typically servers receive form data with content type `multipart/form-data` or +`application/x-www-form-urlencoded`. The platforms themselves typically do not +provide "body parsing" functions, instead leaning on the community to supply +modules to take the encoded data and split into form fields and files. + +NodeJS servers typically use a parser like `formidable`. In the example below, +`formidable` will write to file and `XLSX.readFile` will read the file: + +```js +var XLSX = require("xlsx"); // This is using the CommonJS build +var formidable = require("formidable"); + +require("http").createServer(function(req, res) { + if(req.method !== "POST") return res.end(""); + + /* parse body and implement logic in callback */ + // highlight-next-line + (new formidable.IncomingForm()).parse(req, function(err, fields, files) { + /* if successful, files is an object whose keys are param names */ + // highlight-next-line + var file = files["upload"]; // + /* file.path is a location in the filesystem, usually in a temp folder */ + // highlight-next-line + var wb = XLSX.readFile(file.filepath); + // print the first worksheet back as a CSV + res.end(XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]])); + }); +}).listen(process.env.PORT || 3000); +``` + +`XLSX.read` will accept NodeJS buffers as well as `Uint8Array`, Base64 strings, +binary strings, and plain Arrays of bytes. This covers the interface types of +a wide variety of frameworks. + +#### Writing Data + +Typically server libraries use a response API that accepts `Uint8Array` data. +`XLSX.write` with the option `type: "buffer"` will generate data. To force the +response to be treated as an attachment, set the `Content-Disposition` header: + +```js +var XLSX = require("xlsx"); // This is using the CommonJS build + +require("http").createServer(function(req, res) { + if(req.method !== "GET") return res.end(""); + var wb = XLSX.read("S,h,e,e,t,J,S\n5,4,3,3,7,9,5", {type: "binary"}); + // highlight-start + res.setHeader('Content-Disposition', 'attachment; filename="SheetJS.xlsx"'); + res.end(XLSX.write(wb, {type:"buffer", bookType: "xlsx"})); + // highlight-end +}).listen(process.env.PORT || 3000); +``` + +## Deno + +:::warning + +Many hosted services like Deno Deploy do not offer filesystem access. + +This breaks web frameworks that use the filesystem in body parsing. + +::: + +Deno provides the basic elements to implement a server. It does not provide a +body parser out of the box. + +### Drash + +In testing, [Drash](https://drash.land/drash/) had an in-memory body parser +which could handle file uploads on hosted services like Deno Deploy. + +The service is hosted on Deno Deploy using Drash! + +_Reading Data_ + +`Request#bodyParam` reads body parameters. For uploaded files, the `content` +property is a `Uint8Array`: + +```ts +// @deno-types="https://cdn.sheetjs.com/xlsx-latest/package/types/index.d.ts" +import { read, utils } from 'https://cdn.sheetjs.com/xlsx-latest/package/xlsx.mjs'; + +import * as Drash from "https://deno.land/x/drash@v2.5.4/mod.ts"; + +class ParseResource extends Drash.Resource { + public paths = ["/"]; + + public POST(request: Drash.Request, response: Drash.Response) { + // assume a form upload like + // highlight-next-line + const file = request.bodyParam("upload"); + if (!file) throw new Error("File is required!"); + // highlight-next-line + var wb = read(file.content, {type: "buffer"}); + return response.html( utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]])); + } +} +``` + +_Writing Data_ + +Headers are manually set with `Response#headers.set` while the raw body is set +with `Response#send`: + +```ts +// @deno-types="https://cdn.sheetjs.com/xlsx-latest/package/types/index.d.ts" +import { read, utils } from 'https://cdn.sheetjs.com/xlsx-latest/package/xlsx.mjs'; + +import * as Drash from "https://deno.land/x/drash@v2.5.4/mod.ts"; + +class WriteResource extends Drash.Resource { + public paths = ["/export"]; + + public GET(request: Drash.Request, response: Drash.Response): void { + // create some fixed workbook + const data = ["SheetJS".split(""), [5,4,3,3,7,9,5]]; + const ws = utils.aoa_to_sheet(data); + const wb = utils.book_new(); utils.book_append_sheet(wb, ws, "data"); + // write the workbook to XLSX as a Uint8Array + // highlight-next-line + const file = write(wb, { bookType: "xlsx", type: "buffer"}); + // set headers + response.headers.set("Content-Disposition", 'attachment; filename="SheetJSDrash.xlsx"'); + // send data + // highlight-next-line + return response.send("application/vnd.ms-excel", file); + } +} +``` + +
Complete Example (click to show) + +1) Save the following script to `SheetJSDrash.ts`: + +```ts title="SheetJSDrash.ts" +/*! sheetjs (C) 2013-present SheetJS -- http://sheetjs.com */ +// @deno-types="https://cdn.sheetjs.com/xlsx-latest/package/types/index.d.ts" +import { read, utils, set_cptable } from 'https://cdn.sheetjs.com/xlsx-latest/package/xlsx.mjs'; +import * as cptable from 'https://cdn.sheetjs.com/xlsx-latest/package/dist/cpexcel.full.mjs'; +set_cptable(cptable); + +import * as Drash from "https://deno.land/x/drash@v2.5.4/mod.ts"; + +class ParseResource extends Drash.Resource { + public paths = ["/"]; + + public POST(request: Drash.Request, response: Drash.Response) { + const file = request.bodyParam("file"); + if (!file) throw new Error("File is required!"); + var wb = read(file.content, {type: "buffer"}); + return response.html( utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]])); + } + + public GET(request: Drash.Request, response: Drash.Response): void { + return response.html(`\ + + + + SheetJS Spreadsheet to HTML Conversion Service + + + +

SheetJS Spreadsheet Conversion Service

+API + +Send a POST request to https://s2c.sheetjs.com/ with the file in the "file" body parameter: + +$ curl -X POST -F"file=@test.xlsx" https://s2c.sheetjs.com/ + +The response will be an HTML TABLE generated from the first worksheet. + +Try it out!
+ + + +Use the file input element to select a file, then click "Submit" + + +
+
+ +`, + ); + } +} + +const server = new Drash.Server({ + hostname: "", + port: 3000, + protocol: "http", + resources: [ ParseResource ], +}); + +server.run(); + +console.log(`Server running at ${server.address}.`); +``` + +2) Run the server: + +```bash +deno run --allow-net SheetJSDrash.ts +``` + +3) Download the test file + +4) Open http://localhost:3000/ in your browser. + +Click "Choose File" and select `pres.numbers`. Then click "Submit" + +The page should show the contents of the file as an HTML table. + +
\ No newline at end of file diff --git a/docz/docs/03-demos/index.md b/docz/docs/03-demos/index.md index add0fb3..62ade1d 100644 --- a/docz/docs/03-demos/index.md +++ b/docz/docs/03-demos/index.md @@ -37,6 +37,7 @@ The demo projects include small runnable examples and short explainers. - [`Command-Line Tools`](./cli) - [`iOS and Android Mobile Applications`](./mobile) - [`NodeJS Server-Side Processing`](https://github.com/SheetJS/SheetJS/tree/master/demos/server/) +- [`Deno Server-Side Processing`](./server#deno) - [`Content Management and Static Sites`](./content) - [`Electron`](./desktop#electron) - [`NW.js`](./desktop#nwjs) diff --git a/docz/docs/07-csf/01-general.md b/docz/docs/07-csf/01-general.md index 40ce9e6..2545b62 100644 --- a/docz/docs/07-csf/01-general.md +++ b/docz/docs/07-csf/01-general.md @@ -48,7 +48,7 @@ then `AAA`. Some sample values, along with SheetJS column indices, are listed: | Second | `B` | `1` | | 26th | `Z` | `25` | | 27th | `AA` | `26` | -| 702st | `ZZ` | `701` | +| 702nd | `ZZ` | `701` | | 703rd | `AAA` | `702` | | 16384th | `XFD` | `16383` | diff --git a/docz/docs/07-csf/07-features/index.md b/docz/docs/07-csf/07-features/index.md index 51c9d1e..9727755 100644 --- a/docz/docs/07-csf/07-features/index.md +++ b/docz/docs/07-csf/07-features/index.md @@ -98,8 +98,8 @@ pixels. When the pixel and character counts do not align, Excel rounds values. XLSX internally stores column widths in a nebulous "Max Digit Width" form. The Max Digit Width is the width of the largest digit when rendered (generally the "0" character is the widest). The internal width must be an integer multiple of -the the width divided by 256. ECMA-376 describes a formula for converting -between pixels and the internal width. This represents a hybrid approach. +the width divided by 256. ECMA-376 describes a formula for converting between +pixels and the internal width. This represents a hybrid approach. Read functions attempt to populate all three properties. Write functions will try to cycle specified values to the desired type. In order to avoid potential diff --git a/docz/docs/08-api/09-utilities.md b/docz/docs/08-api/09-utilities.md index 0740ba3..35dd415 100644 --- a/docz/docs/08-api/09-utilities.md +++ b/docz/docs/08-api/09-utilities.md @@ -269,7 +269,7 @@ function SheetJSHeaderOrder() { ### HTML Table Input -**Create a worksheet or workbook from an HTML DOM TABLE** +**Create a worksheet or workbook from a HTML DOM TABLE** ```js var ws = XLSX.utils.table_to_sheet(elt, opts); diff --git a/docz/static/ionic/home.page.ts b/docz/static/ionic/home.page.ts new file mode 100644 index 0000000..9493109 --- /dev/null +++ b/docz/static/ionic/home.page.ts @@ -0,0 +1,125 @@ +/* sheetjs (C) 2013-present SheetJS -- http://sheetjs.com */ +/* vim: set ts=2: */ +import { Component } from '@angular/core'; +import { File } from '@ionic-native/file/ngx'; +import * as XLSX from 'xlsx'; + +type AOA = any[][]; + +@Component({ + selector: 'app-home', + //templateUrl: 'home.page.html', + styleUrls: ['home.page.scss'], + template: ` + + + SheetJS Ionic Demo + + + + + + + SheetJS Demo + + + + + + + {{val}} + + + + + + + + + + +` +}) + +export class HomePage { + data: any[][] = [[1,2,3],[4,5,6]]; + constructor(public file: File) {} + + read(ab: ArrayBuffer) { + /* read workbook */ + const wb: XLSX.WorkBook = XLSX.read(new Uint8Array(ab), {type: 'array'}); + + /* grab first sheet */ + const wsname: string = wb.SheetNames[0]; + const ws: XLSX.WorkSheet = wb.Sheets[wsname]; + + /* save data */ + this.data = (XLSX.utils.sheet_to_json(ws, {header: 1}) as AOA); + }; + + write(): XLSX.WorkBook { + /* generate worksheet */ + const ws: XLSX.WorkSheet = XLSX.utils.aoa_to_sheet(this.data); + + /* generate workbook and add the worksheet */ + const wb: XLSX.WorkBook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, 'SheetJS'); + + return wb; + }; + + /* File Input element for browser */ + onFileChange(evt: any) { + /* wire up file reader */ + const target: DataTransfer = (evt.target as DataTransfer); + if (target.files.length !== 1) { throw new Error('Cannot use multiple files'); } + const reader: FileReader = new FileReader(); + reader.onload = (e: any) => { + const ab: ArrayBuffer = e.target.result; + this.read(ab); + }; + reader.readAsArrayBuffer(target.files[0]); + }; + + /* Import button for mobile */ + async import() { + try { + const target: string = this.file.documentsDirectory || this.file.externalDataDirectory || this.file.dataDirectory || ''; + const dentry = await this.file.resolveDirectoryUrl(target); + const url: string = dentry.nativeURL || ''; + alert(`Attempting to read SheetJSIonic.xlsx from ${url}`); + const ab: ArrayBuffer = await this.file.readAsArrayBuffer(url, 'SheetJSIonic.xlsx'); + this.read(ab); + } catch(e) { + const m: string = e.message; + alert(m.match(/It was determined/) ? 'Use File Input control' : `Error: ${m}`); + } + }; + + /* Export button */ + async export() { + const wb: XLSX.WorkBook = this.write(); + const filename = 'SheetJSIonic.xlsx'; + try { + /* generate Blob */ + const wbout: ArrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' }); + + /* find appropriate path for mobile */ + const target: string = this.file.documentsDirectory || this.file.externalDataDirectory || this.file.dataDirectory || ''; + const dentry = await this.file.resolveDirectoryUrl(target); + const url: string = dentry.nativeURL || ''; + + /* attempt to save blob to file */ + await this.file.writeFile(url, filename, wbout, {replace: true}); + alert(`Wrote to SheetJSIonic.xlsx in ${url}`); + } catch(e) { + if(e.message.match(/It was determined/)) { + /* in the browser, use writeFile */ + XLSX.writeFile(wb, filename); + } else { + alert(`Error: ${e.message}`); + } + } + }; +} +