This commit is contained in:
SheetJS 2022-08-19 02:42:18 -04:00
parent 913538b2ed
commit 08b7493c34
13 changed files with 526 additions and 34 deletions

@ -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

@ -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 */

@ -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.

@ -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.
<details><summary><b>Complete Example</b> (click to show)</summary>
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
```
</details>

@ -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<President[]>([]);
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<President>(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 (<table><thead><th>Name</th><th>Index</th></thead><tbody>
{ /* generate row for each president */
// highlight-start
@ -101,7 +108,9 @@ export default function SheetJSReactAoO() {
</tr>))
// highlight-end
}
</tbody></table>);
</tbody><tfoot><td colSpan={2}>
<button onClick={exportFile}>Export XLSX</button>
</td></tfoot></table>);
}
```
@ -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<string>("");
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 ( <>
<button onClick={exportFile}>Export XLSX</button>
// highlight-next-line
return ( <div dangerouslySetInnerHTML={{ __html: html }} />);
<div ref={tbl} dangerouslySetInnerHTML={{ __html: html }} />
</>);
}
```

@ -74,7 +74,7 @@ a TABLE with a row for each President:
```html title="src/SheetJSVueAoO.vue"
<script setup>
import { ref, onMounted } from "vue";
import { read, utils } from 'xlsx';
import { read, utils, writeFileXLSX } from 'xlsx';
const rows = ref([]);
@ -89,6 +89,14 @@ onMounted(async() => {
/* update data */
rows.value = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
});
/* get state data and export to XLSX */
function exportFile() {
const ws = utils.json_to_sheet(rows.value);
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "Data");
writeFileXLSX(wb, "SheetJSVueAoO.xlsx");
}
</script>
<template>
@ -97,7 +105,9 @@ onMounted(async() => {
<td>{{ row.Name }}</td>
<td>{{ row.Index }}</td>
</tr>
</tbody></table>
</tbody><tfoot><td colSpan={2}>
<button @click="exportFile">Export XLSX</button>
</td></tfoot></table>
</template>
```
@ -114,9 +124,10 @@ attribute, effectively inserting the code into the page:
```html title="src/SheetJSVueHTML.vue"
<script setup>
import { ref, onMounted } from "vue";
import { read, utils } from 'xlsx';
import { read, utils, writeFileXLSX } from 'xlsx';
const html = ref("");
const tableau = ref();
onMounted(async() => {
/* Download from https://sheetjs.com/pres.numbers */
@ -129,10 +140,17 @@ onMounted(async() => {
/* update data */
html.value = utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]);
});
/* get live table and export to XLSX */
function exportFile() {
const wb = utils.table_to_book(tableau.value.getElementsByTagName("TABLE")[0])
writeFileXLSX(wb, "SheetJSVueHTML.xlsx");
}
</script>
<template>
<div v-html="html"></div>
<button @click="exportFile">Export XLSX</button>
</template>
```

@ -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 };
<td>{{row.Index}}</td>
</tr>
// highlight-end
</tbody>
</tbody><tfoot>
<button (click)="onSave()">Export XLSX</button>
</tfoot>
</table></div>
`
})
@ -122,6 +124,14 @@ export class AppComponent {
this.rows = utils.sheet_to_json<President>(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: `<div class="content" role="main" [innerHTML]="html"></div>`
template: `<div class="content" role="main" [innerHTML]="html" #tableau></div>
<button (click)="onSave()">Export XLSX</button>`
})
export class AppComponent {
// highlight-start
constructor(private sanitizer: DomSanitizer) {}
html: SafeHtml = "";
@ViewChild('tableau') tabeller!: ElementRef<HTMLDivElement>;
// 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");
}
}
```

@ -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"]; // <input type="file" id="upload" name="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 <https://s2c.sheetjs.com> 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 <input type="file" id="upload" name="upload">
// highlight-next-line
const file = request.bodyParam<Drash.Types.BodyFile>("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);
}
}
```
<details><summary><b>Complete Example</b> (click to show)</summary>
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<Drash.Types.BodyFile>("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(`\
<!DOCTYPE html>
<html>
<head>
<title>SheetJS Spreadsheet to HTML Conversion Service</title>
<meta charset="utf-8" />
</head>
<body>
<pre><h3><a href="//sheetjs.com/">SheetJS</a> Spreadsheet Conversion Service</h3>
<b>API</b>
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.
<b>Try it out!</b><form action="/" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
Use the file input element to select a file, then click "Submit"
<button type="submit">Submit</button>
</form>
</pre>
</body>
</html>`,
);
}
}
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 <https://sheetjs.com/pres.numbers>
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.
</details>

@ -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)

@ -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` |

@ -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

@ -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);

@ -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: `
<ion-header>
<ion-toolbar>
<ion-title>SheetJS Ionic Demo</ion-title>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title>SheetJS Demo</ion-title>
</ion-toolbar>
</ion-header>
<ion-grid>
<ion-row *ngFor="let row of data">
<ion-col *ngFor="let val of row">
{{val}}
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
<ion-footer padding>
<input type="file" (change)="onFileChange($event)" multiple="false" />
<button ion-button color="secondary" (click)="import()">Import Data</button>
<button ion-button color="secondary" (click)="export()">Export Data</button>
</ion-footer>
`
})
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}`);
}
}
};
}