version bump 0.11.19: browser `writeFile`

- IE6-9 ActiveX + VBScript shim
- `writeFile` supported in browser
- `oldie` demo for IE write strategies
This commit is contained in:
SheetJS 2018-02-03 15:46:32 -05:00
parent edf7150ca8
commit 75845a0ca7
88 changed files with 1098 additions and 416 deletions

View File

@ -36,7 +36,6 @@ CDNjs
CommonJS
Ethercalc
ExtendScript
FileSaver
IndexedDB
JavaScriptCore
LocalStorage
@ -58,6 +57,7 @@ webpack
weex
# Other terms
ActiveX
APIs
ArrayBuffer
Base64
@ -66,6 +66,7 @@ JS
NoSQL
README
UTF-16
VBScript
XHR
XMLHttpRequest
bundler

View File

@ -4,6 +4,10 @@ This log is intended to keep track of backwards-incompatible changes, including
but not limited to API changes and file location changes. Minor behavioral
changes may not be included if they are not expected to break existing code.
## 0.11.19
* Error on empty workbook
## 0.11.16 (2017-12-30)
* XLS ANSI/CP separation

View File

@ -215,6 +215,7 @@ The [`demos` directory](demos/) includes sample projects for:
- [`Headless Browsers`](demos/headless/)
- [`canvas-datagrid`](demos/datagrid/)
- [`Swift JSC and other engines`](demos/altjs/)
- [`internet explorer`](demos/oldie/)
### Optional Modules
@ -339,7 +340,7 @@ var worksheet = XLSX.read(htmlstr, {type:'string'});
<summary><b>Browser download file (ajax)</b> (click to show)</summary>
Note: for a more complete example that works in older browsers, check the demo
at <http://oss.sheetjs.com/js-xlsx/ajax.html>). The <demos/xhr/> directory also
at <http://oss.sheetjs.com/js-xlsx/ajax.html>). The [`xhr` demo](demos/xhr/)
includes more examples with `XMLHttpRequest` and `fetch`.
```js
@ -414,6 +415,8 @@ input_dom_element.addEventListener('change', handleFile, false);
</details>
More specialized cases, including mobile app file processing, are covered in the
[included demos](demos/)
### Parsing Examples
@ -590,8 +593,7 @@ Assuming `workbook` is a workbook object:
<details>
<summary><b>nodejs write a file</b> (click to show)</summary>
`writeFile` is only available in server environments. Browsers have no API for
writing arbitrary files given a path, so another strategy must be used.
`XLSX.writeFile` uses `fs.writeFileSync` in server environments:
```js
if(typeof require !== 'undefined') XLSX = require('xlsx');
@ -603,7 +605,7 @@ XLSX.writeFile(workbook, 'out.xlsb');
</details>
<details>
<summary><b>Browser add to web page</b> (click to show)</summary>
<summary><b>Browser add TABLE element to page</b> (click to show)</summary>
The `sheet_to_html` utility function generates HTML code that can be added to
any DOM element.
@ -614,29 +616,10 @@ var container = document.getElementById('tableau');
container.innerHTML = XLSX.utils.sheet_to_html(worksheet);
```
</details>
<details>
<summary><b>Browser save file</b> (click to show)</summary>
Note: browser generates binary blob and forces a "download" to client. This
example uses [FileSaver](https://github.com/eligrey/FileSaver.js/):
```js
/* bookType can be any supported output type */
var wopts = { bookType:'xlsx', bookSST:false, type:'array' };
var wbout = XLSX.write(workbook,wopts);
/* the saveAs call downloads a file on the local machine */
saveAs(new Blob([wbout],{type:"application/octet-stream"}), "test.xlsx");
```
</details>
<details>
<summary><b>Browser upload to server</b> (click to show)</summary>
<summary><b>Browser upload file (ajax)</b> (click to show)</summary>
A complete example using XHR is [included in the XHR demo](demos/xhr/), along
with examples for fetch and wrapper libraries. This example assumes the server
@ -658,6 +641,65 @@ req.send(formdata);
</details>
<details>
<summary><b>Browser save file</b> (click to show)</summary>
`XLSX.writeFile` wraps a few techniques for triggering a file save:
- `URL` browser API creates an object URL for the file, which the library uses
by creating a link and forcing a click. It is supported in modern browsers.
- `msSaveBlob` is an IE10+ API for triggering a file save.
- `IE_FileSave` uses VBScript and ActiveX to write a file in IE6+ for Windows
XP and Windows 7. The shim must be included in the containing HTML page.
There is no standard way to determine if the actual file has been downloaded.
```js
/* output format determined by filename */
XLSX.writeFile(workbook, 'out.xlsb');
/* at this point, out.xlsb will have been downloaded */
```
</details>
<details>
<summary><b>Browser save file (compatibility)</b> (click to show)</summary>
`XLSX.writeFile` techniques work for most modern browsers as well as older IE.
For much older browsers, there are workarounds implemented by wrapper libraries.
[`FileSaver.js`](https://github.com/eligrey/FileSaver.js/) implements `saveAs`.
Note: `XLSX.writeFile` will automatically call `saveAs` if available.
```js
/* bookType can be any supported output type */
var wopts = { bookType:'xlsx', bookSST:false, type:'array' };
var wbout = XLSX.write(workbook,wopts);
/* the saveAs call downloads a file on the local machine */
saveAs(new Blob([wbout],{type:"application/octet-stream"}), "test.xlsx");
```
[`Downloadify`](https://github.com/dcneiner/downloadify) uses a Flash SWF button
to generate local files, suitable for environments where ActiveX is unavailable:
```js
Downloadify.create(id,{
/* other options are required! read the downloadify docs for more info */
filename: "test.xlsx",
data: function() { return XLSX.write(wb, {bookType:"xlsx", type:'base64'}); },
append: false,
dataType: 'base64'
});
```
The [`oldie` demo](demos/oldie/) shows an IE-compatible fallback scenario.
</details>
The [included demos](demos/) cover mobile apps and other special deployments.
### Writing Examples
- <http://sheetjs.com/demos/table.html> exporting an HTML table
@ -705,7 +747,8 @@ Parse options are described in the [Parsing Options](#parsing-options) section.
`XLSX.write(wb, write_opts)` attempts to write the workbook `wb`
`XLSX.writeFile(wb, filename, write_opts)` attempts to write `wb` to `filename`
`XLSX.writeFile(wb, filename, write_opts)` attempts to write `wb` to `filename`.
In browser-based environments, it will attempt to force a client-side download.
`XLSX.writeFileAsync(filename, wb, o, cb)` attempts to write `wb` to `filename`.
If `o` is omitted, the writer will use the third argument as the callback.
@ -2015,6 +2058,7 @@ produces HTML output. The function takes an options argument:
| Option Name | Default | Description |
| :---------- | :------: | :-------------------------------------------------- |
|`id` | | Specify the `id` attribute for the `TABLE` element |
|`editable` | false | If true, set `contenteditable="true"` for every TD |
|`header` | | Override header (default `html body`) |
|`footer` | | Override footer (default `/body /html`) |

View File

@ -1 +1 @@
XLSX.version = '0.11.18';
XLSX.version = '0.11.19';

View File

@ -19,11 +19,16 @@ function s2ab(s/*:string*/) {
return buf;
}
function arr2str(data/*:any*/)/*:string*/ {
function a2s(data/*:any*/)/*:string*/ {
if(Array.isArray(data)) return data.map(_chr).join("");
var o/*:Array<string>*/ = []; for(var i = 0; i < data.length; ++i) o[i] = _chr(data[i]); return o.join("");
}
function a2u(data/*:Array<number>*/)/*:Uint8Array*/ {
if(typeof Uint8Array === 'undefined') throw new Error("Unsupported");
return new Uint8Array(data);
}
function ab2a(data/*:ArrayBuffer|Uint8Array*/)/*:Array<number>*/ {
if(typeof ArrayBuffer == 'undefined') throw new Error("Unsupported");
if(data instanceof ArrayBuffer) return ab2a(new Uint8Array(data));

37
bits/19_fsutils.js Normal file
View File

@ -0,0 +1,37 @@
var _fs;
if(typeof require !== 'undefined') try { _fs = require('fs'); } catch(e) {}
/* normalize data for blob ctor */
function blobify(data) {
if(typeof data === "string") return s2ab(data);
if(Array.isArray(data)) return a2u(data);
return data;
}
/* write or download file */
function write_dl(fname/*:string*/, payload/*:any*/, enc/*:?string*/) {
/*global IE_SaveFile, Blob, navigator, saveAs, URL, document */
if(typeof _fs !== 'undefined' && _fs.writeFileSync) return enc ? _fs.writeFileSync(fname, payload, enc) : _fs.writeFileSync(fname, payload);
var data = (enc == "utf8") ? utf8write(payload) : payload;
/*:: declare var IE_SaveFile: any; */
if(typeof IE_SaveFile !== 'undefined') return IE_SaveFile(data, fname);
if(typeof Blob !== 'undefined') {
var blob = new Blob([blobify(data)], {type:"application/octet-stream"});
/*:: declare var navigator: any; */
if(typeof navigator !== 'undefined' && navigator.msSaveBlob) return navigator.msSaveBlob(blob, fname);
/*:: declare var saveAs: any; */
if(typeof saveAs !== 'undefined') return saveAs(blob, fname);
if(typeof URL !== 'undefined' && typeof document !== 'undefined' && document.createElement && URL.createObjectURL) {
var a = document.createElement("a");
if(a.download != null) {
var url = URL.createObjectURL(blob);
/*:: if(document.body == null) throw new Error("unreachable"); */
a.download = fname; a.href = url; document.body.appendChild(a); a.click();
/*:: if(document.body == null) throw new Error("unreachable"); */ document.body.removeChild(a);
if(URL.revokeObjectURL && typeof setTimeout !== 'undefined') setTimeout(function() { URL.revokeObjectURL(url); }, 60000);
return url;
}
}
}
throw new Error("cannot initiate download");
}

View File

@ -51,14 +51,13 @@ function getzipstr(zip, file/*:string*/, safe/*:?boolean*/)/*:?string*/ {
try { return getzipstr(zip, file); } catch(e) { return null; }
}
var _fs, jszip;
var jszip;
/*:: declare var JSZip:any; */
/*global JSZip:true */
if(typeof JSZip !== 'undefined') jszip = JSZip;
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
if(typeof exports !== 'undefined') {
if(typeof module !== 'undefined' && module.exports) {
if(typeof jszip === 'undefined') jszip = require('./jszip.js');
try { _fs = require('fs'); } catch(e) { }
}
}

View File

@ -132,6 +132,7 @@ function check_wb_names(N) {
}
function check_wb(wb) {
if(!wb || !wb.SheetNames || !wb.Sheets) throw new Error("Invalid Workbook");
if(!wb.SheetNames.length) throw new Error("Workbook is empty");
check_wb_names(wb.SheetNames);
/* TODO: validate workbook */
}

View File

@ -833,7 +833,7 @@ function parse_xlml(data/*:RawBytes|string*/, opts)/*:Workbook*/ {
switch(opts.type||"base64") {
case "base64": return parse_xlml_xml(Base64.decode(data), opts);
case "binary": case "buffer": case "file": return parse_xlml_xml(data, opts);
case "array": return parse_xlml_xml(arr2str(data), opts);
case "array": return parse_xlml_xml(a2s(data), opts);
}
/*:: throw new Error("unsupported type " + opts.type); */
}

View File

@ -84,9 +84,9 @@ var HTML_ = (function() {
var preamble = "<tr>";
return preamble + oo.join("") + "</tr>";
}
function make_html_preamble(/*::ws:Worksheet, R:Range, o:Sheet2HTMLOpts*/)/*:string*/ {
function make_html_preamble(ws/*:Worksheet*/, R/*:Range*/, o/*:Sheet2HTMLOpts*/)/*:string*/ {
var out/*:Array<string>*/ = [];
return out.join("") + '<table>';
return out.join("") + '<table' + (o && o.id ? ' id="' + o.id + '"' : "") + '>';
}
var _BEGIN = '<html><head><meta charset="utf-8"/><title>SheetJS Table Export</title></head><body>';
var _END = '</body></html>';

View File

@ -100,7 +100,6 @@ function readSync(data/*:RawData*/, opts/*:?ParseOpts*/)/*:Workbook*/ {
case 0x0A: case 0x0D: case 0x20: return read_plaintext_raw(d, o);
}
if(n[2] <= 12 && n[3] <= 31) return DBF.to_workbook(d, o);
if(0x20>n[0]||n[0]>0x7F) throw new Error("Unsupported file " + n.join("|"));
return read_prn(data, d, o, str);
}

View File

@ -6,12 +6,12 @@ function write_zip_type(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:any*/ {
switch(o.type) {
case "base64": oopts.type = "base64"; break;
case "binary": oopts.type = "string"; break;
case "string": throw new Error("'string' output type invalid for '" + o.bookType + ' files');
case "string": throw new Error("'string' output type invalid for '" + o.bookType + "' files");
case "buffer":
case "file": oopts.type = "nodebuffer"; break;
case "file": oopts.type = has_buf ? "nodebuffer" : "string"; break;
default: throw new Error("Unrecognized type " + o.type);
}
if(o.type === "file") return _fs.writeFileSync(o.file, z.generate(oopts));
if(o.type === "file") return write_dl(o.file, z.generate(oopts));
var out = z.generate(oopts);
// $FlowIgnore
return o.type == "string" ? utf8read(out) : out;
@ -23,8 +23,8 @@ function write_cfb_type(wb/*:Workbook*/, opts/*:?WriteOpts*/)/*:any*/ {
switch(o.type) {
case "base64": case "binary": break;
case "buffer": case "array": o.type = ""; break;
case "file": return _fs.writeFileSync(o.file, CFB.write(cfb, {type:'buffer'}));
case "string": throw new Error("'string' output type invalid for '" + o.bookType + ' files');
case "file": return write_dl(o.file, CFB.write(cfb, {type:has_buf ? 'buffer' : ""}));
case "string": throw new Error("'string' output type invalid for '" + o.bookType + "' files");
default: throw new Error("Unrecognized type " + o.type);
}
return CFB.write(cfb, o);
@ -37,7 +37,7 @@ function write_string_type(out/*:string*/, opts/*:WriteOpts*/, bom/*:?string*/)/
case "base64": return Base64.encode(utf8write(o));
case "binary": return utf8write(o);
case "string": return out;
case "file": return _fs.writeFileSync(opts.file, o, 'utf8');
case "file": return write_dl(opts.file, o, 'utf8');
case "buffer": {
if(has_buf) return new Buffer(o, 'utf8');
else return write_string_type(o, {type:'binary'}).split("").map(function(c) { return c.charCodeAt(0); });
@ -51,7 +51,7 @@ function write_stxt_type(out/*:string*/, opts/*:WriteOpts*/)/*:any*/ {
case "base64": return Base64.encode(out);
case "binary": return out;
case "string": return out; /* override in sheet_to_txt */
case "file": return _fs.writeFileSync(opts.file, out, 'binary');
case "file": return write_dl(opts.file, out, 'binary');
case "buffer": {
if(has_buf) return new Buffer(out, 'binary');
else return out.split("").map(function(c) { return c.charCodeAt(0); });
@ -70,7 +70,7 @@ function write_binary_type(out, opts/*:WriteOpts*/)/*:any*/ {
// $FlowIgnore
for(var i = 0; i < out.length; ++i) bstr += String.fromCharCode(out[i]);
return opts.type == 'base64' ? Base64.encode(bstr) : opts.type == 'string' ? utf8read(bstr) : bstr;
case "file": return _fs.writeFileSync(opts.file, out);
case "file": return write_dl(opts.file, out);
case "buffer": return out;
default: throw new Error("Unrecognized type " + opts.type);
}

View File

@ -131,7 +131,7 @@ function sheet_to_txt(sheet/*:Worksheet*/, opts/*:?Sheet2CSVOpts*/) {
var s = sheet_to_csv(sheet, opts);
if(typeof cptable == 'undefined' || opts.type == 'string') return s;
var o = cptable.utils.encode(1200, s, 'str');
return "\xff\xfe" + o;
return String.fromCharCode(255) + String.fromCharCode(254) + o;
}
function sheet_to_formulae(sheet/*:Worksheet*/)/*:Array<string>*/ {

View File

@ -44,5 +44,6 @@ can be installed with Bash on Windows or with `cygwin`.
- [`Headless Browsers`](headless/)
- [`canvas-datagrid`](datagrid/)
- [`Swift JSC and other engines`](altjs/)
- [`internet explorer`](oldie/)
[![Analytics](https://ga-beacon.appspot.com/UA-36810333-1/SheetJS/js-xlsx?pixel)](https://github.com/SheetJS/js-xlsx)

View File

@ -81,18 +81,15 @@ var ws = XLSX.utils.json_to_sheet(data);
var wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Presidents");
/* write workbook (use type 'array' for ArrayBuffer) */
var wbout = XLSX.write(wb, {bookType:'xlsx', type:'array'});
/* generate a download */
saveAs(new Blob([wbout],{type:"application/octet-stream"}), "sheetjs.xlsx");
/* write workbook and force a download */
XLSX.writeFile(wb, "sheetjs.xlsx");
```
`SheetJSExportService` exposes export functions for `XLSB` and `XLSX`. Other
formats are easily supported by changing the `bookType` variable. It grabs
values from the grid, builds an array of arrays, generates a workbook and uses
FileSaver to generate a download. By setting the `filename` and `sheetname`
options in the ui-grid options, the output can be controlled.
values from the grid, builds an array of arrays, generates a workbook and forces
a download. By setting the `filename` and `sheetname` options in the ui-grid
options, the output can be controlled.
[![Analytics](https://ga-beacon.appspot.com/UA-36810333-1/SheetJS/js-xlsx?pixel)](https://github.com/SheetJS/js-xlsx)

View File

@ -12,8 +12,7 @@ function SheetJSExportService(uiGridExporterService) {
var wb = XLSX.utils.book_new(), ws = uigrid_to_sheet(data, columns);
XLSX.utils.book_append_sheet(wb, ws, sheetName);
var wbout = XLSX.write(wb, wopts);
saveAs(new Blob([wbout], { type: 'application/octet-stream' }), fileName);
XLSX.writeFile(wb, fileName);
}
var service = {};

View File

@ -13,10 +13,8 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-grid/4.0.0/ui-grid.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-grid/4.0.0/ui-grid.css"/>
<!-- FileSaver shim for exporting files -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js"></script>
<!-- SheetJS js-xlsx library -->
<script src="shim.js"></script>
<script src="xlsx.full.min.js"></script>
<!-- SheetJS Service -->

1
demos/angular/shim.js vendored Symbolic link
View File

@ -0,0 +1 @@
../../shim.js

View File

@ -2,24 +2,27 @@
angular:
# Test Angular2 build
cp package.json-angular2 package.json
rm -rf node_modules
npm install
if [ ! -e node_modules ]; then mkdir node_modules; fi
if [ ! -e node_modules/xlsx ]; then cd node_modules; ln -s ../../../ xlsx; cd -; fi
ng build
npm run build
# Test Angular4 build
cp package.json-angular4 package.json
rm -rf node_modules
npm install
if [ ! -e node_modules ]; then mkdir node_modules; fi
if [ ! -e node_modules/xlsx ]; then cd node_modules; ln -s ../../../ xlsx; cd -; fi
ng build
npm run build
# Test Angular5 build
cp package.json-angular5 package.json
rm -rf node_modules
npm install
if [ ! -e node_modules ]; then mkdir node_modules; fi
if [ ! -e node_modules/xlsx ]; then cd node_modules; ln -s ../../../ xlsx; cd -; fi
ng build
npm run build
.PHONY: ionic
ionic:

View File

@ -38,8 +38,7 @@ const wb: XLSX.WorkBook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
/* save to file */
const wbout: string = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
saveAs(new Blob([wbout]), 'SheetJS.xlsx');
XLSX.writeFile(wb, 'SheetJS.xlsx');
```
`sheet_to_json` with the option `header:1` makes importing simple:

View File

@ -5,7 +5,7 @@ if [ ! -e SheetJSIonic ]; then
ionic cordova platform add browser </dev/null
ionic cordova platform add ios </dev/null
ionic cordova plugin add cordova-plugin-file </dev/null
npm install --save @ionic-native/file file-saver
npm install --save @ionic-native/file
npm install --save xlsx
cp src/app/app.module.ts{,.bak}

View File

@ -5,7 +5,6 @@ import { Component } from '@angular/core';
import * as XLSX from 'xlsx';
import { File } from '@ionic-native/file';
import { saveAs } from 'file-saver';
type AOA = any[][];
@ -48,7 +47,7 @@ export class HomePage {
this.data = <AOA>(XLSX.utils.sheet_to_json(ws, {header: 1}));
};
write(): ArrayBuffer {
write(): XLSX.WorkBook {
/* generate worksheet */
const ws: XLSX.WorkSheet = XLSX.utils.aoa_to_sheet(this.data);
@ -56,9 +55,7 @@ export class HomePage {
const wb: XLSX.WorkBook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'SheetJS');
/* save to ArrayBuffer */
const wbout: ArrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
return wbout;
return wb;
};
/* File Input element for browser */
@ -91,17 +88,26 @@ export class HomePage {
/* Export button */
async export() {
const wbout: ArrayBuffer = this.write();
const wb: XLSX.WorkBook = this.write();
const filename: string = "SheetJSIonic.xlsx";
const blob: Blob = new Blob([wbout], {type: 'application/octet-stream'});
try {
/* generate Blob */
const wbout: ArrayBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
const blob: Blob = new Blob([wbout], {type: 'application/octet-stream'});
/* 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, blob, {replace: true});
alert(`Wrote to SheetJSIonic.xlsx in ${url}`);
} catch(e) {
if(e.message.match(/It was determined/)) saveAs(blob, filename);
if(e.message.match(/It was determined/)) {
/* in the browser, use writeFile */
XLSX.writeFile(wb, filename);
}
else alert(`Error: ${e.message}`);
}
};

View File

@ -24,11 +24,10 @@
"rxjs": "^5.5.2",
"zone.js": "^0.8.14",
"file-saver": "^1.3.3"
"zone.js": "^0.8.14"
},
"devDependencies": {
"@angular/cli": "1.5.0",
"@angular/cli": "^1.5.3",
"@angular/compiler-cli": "^5.0.0",
"@angular/language-service": "^5.0.0",
"@types/node": "~6.0.60",

View File

@ -24,8 +24,7 @@
"reflect-metadata": "^0.1.8",
"rxjs": "^5.0.2",
"systemjs": "0.19.40",
"zone.js": "^0.7.4",
"file-saver": "^1.3.3"
"zone.js": "^0.7.4"
},
"devDependencies": {
"@angular/cli": "1.1.2",

View File

@ -24,8 +24,7 @@
"rxjs": "^5.1.0",
"zone.js": "^0.8.4",
"file-saver": "^1.3.3"
"zone.js": "^0.8.4"
},
"devDependencies": {
"@angular/cli": "1.1.2",

View File

@ -24,11 +24,10 @@
"rxjs": "^5.5.2",
"zone.js": "^0.8.14",
"file-saver": "^1.3.3"
"zone.js": "^0.8.14"
},
"devDependencies": {
"@angular/cli": "1.5.0",
"@angular/cli": "^1.5.3",
"@angular/compiler-cli": "^5.0.0",
"@angular/language-service": "^5.0.0",
"@types/node": "~6.0.60",

View File

@ -4,8 +4,6 @@ import { Component } from '@angular/core';
import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';
type AOA = any[][];
@Component({
@ -57,7 +55,6 @@ export class SheetJSComponent {
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
/* save to file */
const wbout: ArrayBuffer = XLSX.write(wb, this.wopts);
saveAs(new Blob([wbout], { type: 'application/octet-stream' }), this.fileName);
XLSX.writeFile(wb, this.fileName);
}
}

View File

@ -9,42 +9,42 @@ var client = redis.createClient();
/* Sample data */
var init = [
["FLUSHALL", []],
["SADD", ["birdpowers", "flight", "pecking"]],
["SET", ["foo", "bar"]],
["SET", ["baz", 0]],
["RPUSH", ["friends", "sam", "alice", "bob"]],
["ZADD", ["hackers", 1906, 'Grace Hopper', 1912, 'Alan Turing', 1916, 'Claude Shannon', 1940, 'Alan Kay', 1953, 'Richard Stallman', 1957, 'Sophie Wilson', 1965, 'Yukihiro Matsumoto', 1969, 'Linus Torvalds']],
["SADD", ["superpowers", "flight", 'x-ray vision']],
["HMSET", ["user:1000", "name", 'John Smith', "email", 'john.smith@example.com', "password", "s3cret", "visits", 1]],
["HMSET", ["user:1001", "name", 'Mary Jones', "email", 'mjones@example.com', "password", "hidden"]]
["FLUSHALL", []],
["SADD", ["birdpowers", "flight", "pecking"]],
["SET", ["foo", "bar"]],
["SET", ["baz", 0]],
["RPUSH", ["friends", "sam", "alice", "bob"]],
["ZADD", ["hackers", 1906, 'Grace Hopper', 1912, 'Alan Turing', 1916, 'Claude Shannon', 1940, 'Alan Kay', 1953, 'Richard Stallman', 1957, 'Sophie Wilson', 1965, 'Yukihiro Matsumoto', 1969, 'Linus Torvalds']],
["SADD", ["superpowers", "flight", 'x-ray vision']],
["HMSET", ["user:1000", "name", 'John Smith', "email", 'john.smith@example.com', "password", "s3cret", "visits", 1]],
["HMSET", ["user:1001", "name", 'Mary Jones', "email", 'mjones@example.com', "password", "hidden"]]
];
const R = (()=>{
const Rcache = {};
const R_ = (n) => Rcache[n] || (Rcache[n] = util.promisify(client[n]).bind(client));
return (n) => R_(n.toLowerCase());
const Rcache = {};
const R_ = (n) => Rcache[n] || (Rcache[n] = util.promisify(client[n]).bind(client));
return (n) => R_(n.toLowerCase());
})();
(async () => {
for(var i = 0; i < init.length; ++i) await R(init[i][0])(init[i][1]);
for(var i = 0; i < init.length; ++i) await R(init[i][0])(init[i][1]);
/* Export database to XLSX */
var wb = await SheetJSRedis.redis_to_wb(R);
var wb = await SheetJSRedis.redis_to_wb(R);
XLSX.writeFile(wb, "redis.xlsx");
/* Import XLSX to database */
await R("flushall")();
await R("flushall")();
var wb2 = XLSX.readFile("redis.xlsx");
await SheetJSRedis.wb_to_redis(wb2, R);
await SheetJSRedis.wb_to_redis(wb2, R);
/* Verify */
assert.equal(await R("get")("foo"), "bar");
assert.equal(await R("lindex")("friends", 1), "alice");
assert.equal(await R("zscore")("hackers", "Claude Shannon"), 1916);
assert.equal(await R("hget")("user:1000", "name"), "John Smith");
assert.equal(await R("sismember")("superpowers", "flight"), "1");
assert.equal(await R("sismember")("birdpowers", "pecking"), "1");
/* Verify */
assert.equal(await R("get")("foo"), "bar");
assert.equal(await R("lindex")("friends", 1), "alice");
assert.equal(await R("zscore")("hackers", "Claude Shannon"), 1916);
assert.equal(await R("hget")("user:1000", "name"), "John Smith");
assert.equal(await R("sismember")("superpowers", "flight"), "1");
assert.equal(await R("sismember")("birdpowers", "pecking"), "1");
client.quit();
client.quit();
})();

View File

@ -6,68 +6,68 @@ const pair = (arr) => arr.map((x,i)=>!(i%2)&&[x,+arr[i+1]]).filter(x=>x);
const keyify = (obj) => Object.keys(obj).map(x => [x, obj[x]]);
async function redis_to_wb(R) {
var wb = XLSX.utils.book_new();
var manifest = [], strs = [];
var wb = XLSX.utils.book_new();
var manifest = [], strs = [];
/* store strings in strs and keep note of other objects in manifest */
var keys = await R("keys")("*"), type = "";
for(var i = 0; i < keys.length; ++i) {
type = await R("type")(keys[i]);
switch(type) {
case "string": strs.push({key:keys[i], value: await R("get")(keys[i])}); break;
case "list": case "zset": case "set": case "hash": manifest.push({key:keys[i], type:type}); break;
default: throw new Error("bad type " + type);
}
}
/* store strings in strs and keep note of other objects in manifest */
var keys = await R("keys")("*"), type = "";
for(var i = 0; i < keys.length; ++i) {
type = await R("type")(keys[i]);
switch(type) {
case "string": strs.push({key:keys[i], value: await R("get")(keys[i])}); break;
case "list": case "zset": case "set": case "hash": manifest.push({key:keys[i], type:type}); break;
default: throw new Error("bad type " + type);
}
}
/* add worksheets if relevant */
if(strs.length > 0) {
var wss = XLSX.utils.json_to_sheet(strs, {header: ["key", "value"], skipHeader:1});
XLSX.utils.book_append_sheet(wb, wss, "_strs");
}
if(manifest.length > 0) {
var wsm = XLSX.utils.json_to_sheet(manifest, {header: ["key", "type"]});
XLSX.utils.book_append_sheet(wb, wsm, "_manifest");
}
for(i = 0; i < manifest.length; ++i) {
var sn = "obj" + i;
var aoa, key = manifest[i].key;
switch((type=manifest[i].type)) {
case "list":
aoa = (await R("lrange")(key, 0, -1)).map(x => [x]); break;
case "set":
aoa = (await R("smembers")(key)).map(x => [x]); break;
case "zset":
aoa = pair(await R("zrange")(key, 0, -1, "withscores")); break;
case "hash":
aoa = keyify(await R("hgetall")(key)); break;
default: throw new Error("bad type " + type);
}
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), sn);
}
return wb;
/* add worksheets if relevant */
if(strs.length > 0) {
var wss = XLSX.utils.json_to_sheet(strs, {header: ["key", "value"], skipHeader:1});
XLSX.utils.book_append_sheet(wb, wss, "_strs");
}
if(manifest.length > 0) {
var wsm = XLSX.utils.json_to_sheet(manifest, {header: ["key", "type"]});
XLSX.utils.book_append_sheet(wb, wsm, "_manifest");
}
for(i = 0; i < manifest.length; ++i) {
var sn = "obj" + i;
var aoa, key = manifest[i].key;
switch((type=manifest[i].type)) {
case "list":
aoa = (await R("lrange")(key, 0, -1)).map(x => [x]); break;
case "set":
aoa = (await R("smembers")(key)).map(x => [x]); break;
case "zset":
aoa = pair(await R("zrange")(key, 0, -1, "withscores")); break;
case "hash":
aoa = keyify(await R("hgetall")(key)); break;
default: throw new Error("bad type " + type);
}
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), sn);
}
return wb;
}
/* convert worksheet aoa to specific redis type */
const aoa_to_redis = {
list: async (aoa, R, key) => await R("RPUSH")([key].concat(aoa.map(x=>x[0]))),
zset: async (aoa, R, key) => await R("ZADD" )([key].concat(aoa.reduce((acc,x)=>acc.concat([+x[1], x[0]]), []))),
hash: async (aoa, R, key) => await R("HMSET")([key].concat(aoa.reduce((acc,x)=>acc.concat(x), []))),
set: async (aoa, R, key) => await R("SADD" )([key].concat(aoa.map(x=>x[0])))
list: async (aoa, R, key) => await R("RPUSH")([key].concat(aoa.map(x=>x[0]))),
zset: async (aoa, R, key) => await R("ZADD" )([key].concat(aoa.reduce((acc,x)=>acc.concat([+x[1], x[0]]), []))),
hash: async (aoa, R, key) => await R("HMSET")([key].concat(aoa.reduce((acc,x)=>acc.concat(x), []))),
set: async (aoa, R, key) => await R("SADD" )([key].concat(aoa.map(x=>x[0])))
};
async function wb_to_redis(wb, R) {
if(wb.Sheets._strs) {
var strs = XLSX.utils.sheet_to_json(wb.Sheets._strs, {header:1});
for(var i = 0; i < strs.length; ++i) await R("SET")(strs[i]);
}
if(!wb.Sheets._manifest) return;
var M = XLSX.utils.sheet_to_json(wb.Sheets._manifest);
for(i = 0; i < M.length; ++i) {
var aoa = XLSX.utils.sheet_to_json(wb.Sheets["obj" + i], {header:1});
await aoa_to_redis[M[i].type](aoa, R, M[i].key);
}
if(wb.Sheets._strs) {
var strs = XLSX.utils.sheet_to_json(wb.Sheets._strs, {header:1});
for(var i = 0; i < strs.length; ++i) await R("SET")(strs[i]);
}
if(!wb.Sheets._manifest) return;
var M = XLSX.utils.sheet_to_json(wb.Sheets._manifest);
for(i = 0; i < M.length; ++i) {
var aoa = XLSX.utils.sheet_to_json(wb.Sheets["obj" + i], {header:1});
await aoa_to_redis[M[i].type](aoa, R, M[i].key);
}
}
module.exports = {
redis_to_wb,
wb_to_redis
redis_to_wb,
wb_to_redis
};

View File

@ -49,9 +49,9 @@ function sheet_to_sql(ws, sname, mode) {
var out = [];
var BT = mode == "PGSQL" ? "" : "`";
var Q = mode == "PGSQL" ? "'" : '"';
var R = mode == "PGSQL" ? /'/g : /"/g;
var BT = mode == "PGSQL" ? "" : "`";
var Q = mode == "PGSQL" ? "'" : '"';
var R = mode == "PGSQL" ? /'/g : /"/g;
out.push("DROP TABLE IF EXISTS " + BT + sname + BT );
out.push("CREATE TABLE " + BT + sname + BT + " (" + names.map(function(n, i) { return BT + n + BT + " " + (types[i]||"TEXT"); }).join(", ") + ");" );

View File

@ -82,7 +82,8 @@ var ws = XLSX.utils.aoa_to_sheet(prep(grid.data));
var wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'SheetJS');
/* .. generate download (see documentation for examples) .. */
/* generate download */
XLSX.writeFile(wb, "SheetJS.xlsx");
```
## Additional Features

View File

@ -40,8 +40,7 @@ Use readAsBinaryString: (when available) <input type="checkbox" name="userabs" c
<div id="htmlout"></div>
<br />
<script src="https://unpkg.com/canvas-datagrid/dist/canvas-datagrid.js"></script>
<script type="text/javascript" src="https://rawgit.com/eligrey/Blob.js/master/Blob.js"></script>
<script type="text/javascript" src="https://rawgit.com/eligrey/FileSaver.js/master/FileSaver.js"></script>
<script src="shim.js"></script>
<script src="xlsx.full.min.js"></script>
<script>
/*jshint browser:true */
@ -151,11 +150,7 @@ var export_xlsx = (function() {
XLSX.utils.book_append_sheet(new_wb, new_ws, 'SheetJS');
/* write file and trigger a download */
var wbout = XLSX.write(new_wb, {bookType:'xlsx', bookSST:true, type:'array'});
var fname = 'sheetjs.xlsx';
try {
saveAs(new Blob([wbout],{type:"application/octet-stream"}), fname);
} catch(e) { if(typeof console != 'undefined') console.log(e, wbout); }
XLSX.writeFile(new_wb, 'sheetjs.xlsx', {bookSST:true});
};
})();
</script>

1
demos/datagrid/shim.js Symbolic link
View File

@ -0,0 +1 @@
../../shim.js

View File

@ -6,9 +6,9 @@ start:
init:
if [ ! -e .meteor ]; then meteor create .; fi;
@npm install babel-runtime meteor-node-stubs
@meteor add pfafman:filesaver check
@meteor add check
@mkdir -p node_modules; cd node_modules; ln -s ../../../ xlsx; cd -
.PHONY: lint
lint:
@meteor npm run lint
@meteor npm run lint

View File

@ -61,13 +61,9 @@ const html = document.getElementById('out').innerHTML;
// SERVER SIDE
const wb = XLSX.read(html, { type: 'binary' });
// CLIENT SIDE
const o = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
saveAs(new Blob([o], {type:'application/octet-stream'}), 'sheetjs.xlsx');
XLSX.writeFile(wb, 'sheetjs.xlsx');
```
This demo uses the FileSaver library for writing files, installed through the
[`pfafman:filesaver` wrapper](https://atmospherejs.com/pfafman/filesaver).
## Setup
@ -76,7 +72,6 @@ This tree does not include the `.meteor` structure. Rebuild the project with:
```bash
meteor create .
npm install babel-runtime meteor-node-stubs xlsx
meteor add pfafman:filesaver
meteor
```

View File

@ -1,7 +1,5 @@
/* xlsx.js (C) 2013-present SheetJS -- http://sheetjs.com */
import XLSX from 'xlsx';
/* note: saveAs is made available via the smart package */
/* global saveAs */
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
@ -13,11 +11,12 @@ Template.sheetjs.events({
/* "Browser file upload form element" from SheetJS README */
const file = event.currentTarget.files[0];
const reader = new FileReader();
const rABS = !!reader.readAsBinaryString;
reader.onload = function(e) {
const data = e.target.result;
const name = file.name;
/* Meteor magic */
Meteor.call('upload', data, name, function(err, wb) {
Meteor.call(rABS ? 'uploadS' : 'uploadU', rABS ? data : new Uint8Array(data), name, function(err, wb) {
if (err) throw err;
/* load the first worksheet */
const ws = wb.Sheets[wb.SheetNames[0]];
@ -27,15 +26,14 @@ Template.sheetjs.events({
document.getElementById('dnload').disabled = false;
});
};
reader.readAsBinaryString(file);
if(rABS) reader.readAsBinaryString(file); else reader.readAsArrayBuffer(file);
},
'click button' () {
const html = document.getElementById('out').innerHTML;
Meteor.call('download', html, function(err, wb) {
if (err) throw err;
/* "Browser download file" from SheetJS README */
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
saveAs(new Blob([wbout], { type: 'application/octet-stream' }), 'sheetjs.xlsx');
XLSX.writeFile(wb, 'sheetjs.xlsx');
});
},
});

View File

@ -4,12 +4,17 @@ import { check } from 'meteor/check';
import XLSX from 'xlsx';
Meteor.methods({
upload: (bstr, name) => {
/* read the data and return the workbook object to the frontend */
/* read the data and return the workbook object to the frontend */
uploadS: (bstr, name) => {
check(bstr, String);
check(name, String);
return XLSX.read(bstr, { type: 'binary' });
},
uploadU: (ab, name) => {
check(ab, Uint8Array);
check(name, String);
return XLSX.read(ab, { type: 'array' });
},
download: (html) => {
check(html, String);
let wb;

70
demos/oldie/README.md Normal file
View File

@ -0,0 +1,70 @@
# Internet Explorer
Despite the efforts to deprecate the pertinent operating systems, IE is still
very popular, required for various government and corporate websites throughout
the world. The modern download strategies are not available in older versions
of IE, but there are alternative approaches.
## Strategies
#### IE10 and IE11 File API
As part of the File API implementation, IE10 and IE11 provide the `msSaveBlob`
and `msSaveOrOpenBlob` functions to save blobs to the client computer. This
approach is embedded in `XLSX.writeFile` and no additional shims are necessary.
#### Flash-based Download
It is possible to write to the file system using a SWF. `Downloadify` library
implements one solution. Since a genuine click is required, there is no way to
force a download. The demo generates a button for each desired output format.
#### ActiveX-based Download
Through the `Scripting.FileSystemObject` object model, a script in the VBScript
scripting language can write to an arbitrary path on the filesystem. The shim
includes a special `IE_SaveFile` function to write binary strings to file. It
attempts to write to the Downloads folder or Documents folder or Desktop.
This approach can be triggered, but it requires the user to enable ActiveX. It
is embedded as a strategy in `writeFile` and used only if the shim script is
included in the page and the relevant features are enabled on the target system.
## Demo
The included demo starts from an array of arrays, generating an editable HTML
table with `aoa_to_sheet` and adding it to the page:
```js
var ws = XLSX.utils.aoa_to_sheet(aoa);
var html_string = XLSX.utils.sheet_to_html(ws, { id: "table", editable: true });
document.getElementById("container").innerHTML = html_string;
```
The included download buttons use `table_to_book` to construct a new workbook
based on the table and `writeFile` to force a download:
```js
var elt = document.getElementById('table');
var wb = XLSX.utils.table_to_book(elt, { sheet: "Sheet JS" });
XLSX.writeFile(wb, filename);
```
The shim is included in the HTML page, unlocking the ActiveX pathway if enabled
in browser settings.
The corresponding SWF buttons are displayed in environments where Flash is
available and `Downloadify` is supported. The easiest solution involves writing
to a Base64 string and passing to the library:
```js
Downloadify.create(element_id, {
/* the demo includes the other options required by Downloadify */
filename: "test.xlsx",
data: function() { return XLSX.write(wb, {bookType:"xlsx", type:'base64'}); },
dataType: 'base64'
});
```
[![Analytics](https://ga-beacon.appspot.com/UA-36810333-1/SheetJS/js-xlsx?pixel)](https://github.com/SheetJS/js-xlsx)

1
demos/oldie/base64.min.js vendored Normal file
View File

@ -0,0 +1 @@
!function(){function t(t){this.message=t}var r="undefined"!=typeof exports?exports:self,e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";t.prototype=new Error,t.prototype.name="InvalidCharacterError",r.btoa||(r.btoa=function(r){for(var o,n,a=String(r),i=0,c=e,d="";a.charAt(0|i)||(c="=",i%1);d+=c.charAt(63&o>>8-i%1*8)){if(n=a.charCodeAt(i+=.75),n>255)throw new t("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");o=o<<8|n}return d}),r.atob||(r.atob=function(r){var o=String(r).replace(/=+$/,"");if(o.length%4==1)throw new t("'atob' failed: The string to be decoded is not correctly encoded.");for(var n,a,i=0,c=0,d="";a=o.charAt(c++);~a&&(n=i%4?64*n+a:a,i++%4)?d+=String.fromCharCode(255&n>>(-2*i&6)):0)a=e.indexOf(a);return d})}();

BIN
demos/oldie/download.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

3
demos/oldie/downloadify.min.js vendored Executable file
View File

@ -0,0 +1,3 @@
/* Downloadify 0.2 (c) 2009 by Douglas Neiner. Licensed under the MIT license */
/* See http://github.com/dcneiner/Downloadify for license and more info */
(function(){Downloadify=window.Downloadify={queue:{},uid:new Date().getTime(),getTextForSave:function(a){var b=Downloadify.queue[a];if(b)return b.getData();return""},getFileNameForSave:function(a){var b=Downloadify.queue[a];if(b)return b.getFilename();return""},getDataTypeForSave:function(a){var b=Downloadify.queue[a];if(b)return b.getDataType();return""},saveComplete:function(a){var b=Downloadify.queue[a];if(b)b.complete();return true},saveCancel:function(a){var b=Downloadify.queue[a];if(b)b.cancel();return true},saveError:function(a){var b=Downloadify.queue[a];if(b)b.error();return true},addToQueue:function(a){Downloadify.queue[a.queue_name]=a},getUID:function(a){if(a.id=="")a.id='downloadify_'+Downloadify.uid++;return a.id}};Downloadify.create=function(a,b){var c=(typeof(a)=="string"?document.getElementById(a):a);return new Downloadify.Container(c,b)};Downloadify.Container=function(d,e){var f=this;f.el=d;f.enabled=true;f.dataCallback=null;f.filenameCallback=null;f.data=null;f.filename=null;var g=function(){f.options=e;if(!f.options.append)f.el.innerHTML="";f.flashContainer=document.createElement('span');f.el.appendChild(f.flashContainer);f.queue_name=Downloadify.getUID(f.flashContainer);if(typeof(f.options.filename)==="function")f.filenameCallback=f.options.filename;else if(f.options.filename)f.filename=f.options.filename;if(typeof(f.options.data)==="function")f.dataCallback=f.options.data;else if(f.options.data)f.data=f.options.data;var a={queue_name:f.queue_name,width:f.options.width,height:f.options.height};var b={allowScriptAccess:'always'};var c={id:f.flashContainer.id,name:f.flashContainer.id};if(f.options.enabled===false)f.enabled=false;if(f.options.transparent===true)b.wmode="transparent";if(f.options.downloadImage)a.downloadImage=f.options.downloadImage;swfobject.embedSWF(f.options.swf,f.flashContainer.id,f.options.width,f.options.height,"10",null,a,b,c);Downloadify.addToQueue(f)};f.enable=function(){var a=document.getElementById(f.flashContainer.id);a.setEnabled(true);f.enabled=true};f.disable=function(){var a=document.getElementById(f.flashContainer.id);a.setEnabled(false);f.enabled=false};f.getData=function(){if(!f.enabled)return"";if(f.dataCallback)return f.dataCallback();else if(f.data)return f.data;else return""};f.getFilename=function(){if(f.filenameCallback)return f.filenameCallback();else if(f.filename)return f.filename;else return""};f.getDataType=function(){if(f.options.dataType)return f.options.dataType;return"string"};f.complete=function(){if(typeof(f.options.onComplete)==="function")f.options.onComplete()};f.cancel=function(){if(typeof(f.options.onCancel)==="function")f.options.onCancel()};f.error=function(){if(typeof(f.options.onError)==="function")f.options.onError()};g()};Downloadify.defaultOptions={swf:'media/downloadify.swf',downloadImage:'images/download.png',width:100,height:30,transparent:true,append:false,dataType:"string"}})();if(typeof(jQuery)!="undefined"){(function($){$.fn.downloadify=function(b){return this.each(function(){b=$.extend({},Downloadify.defaultOptions,b);var a=Downloadify.create(this,b);$(this).data('Downloadify',a)})}})(jQuery)};if(typeof(MooTools)!='undefined'){Element.implement({downloadify:function(a){a=$merge(Downloadify.defaultOptions,a);return this.store('Downloadify',Downloadify.create(this,a))}})};

BIN
demos/oldie/downloadify.swf Executable file

Binary file not shown.

122
demos/oldie/index.html Normal file
View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<!-- (C) 2013-present SheetJS http://sheetjs.com -->
<!-- vim: set ts=2: -->
<html>
<head>
<title>SheetJS JS-XLSX In-Browser HTML Table Export Demo</title>
<meta charset="utf-8" />
<style>
.xport, .btn {
display: inline;
text-align:center;
}
a { text-decoration: none }
#data-table, #data-table th, #data-table td { border: 1px solid black }
</style>
</head>
<body>
<!-- SheetJS js-xlsx library -->
<script type="text/javascript" src="shim.min.js"></script>
<script type="text/javascript" src="xlsx.full.min.js"></script>
<!-- Downloadify Flash fallback for IE 9 and below if ActiveX is unavailable -->
<!--[if lte IE 9]>
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript" src="downloadify.min.js"></script>
<script type="text/javascript" src="base64.min.js"></script>
<![endif]-->
<script>
function doit(type, fn, dl) {
var elt = document.getElementById('data-table');
var wb = XLSX.utils.table_to_book(elt, {sheet:"Sheet JS"});
return dl ?
XLSX.write(wb, {bookType:type, bookSST:true, type: 'base64'}) :
XLSX.writeFile(wb, fn || ('test.' + (type || 'xlsx')));
}
</script>
<pre>
<h3><a href="//sheetjs.com/">SheetJS</a> JS-XLSX In-Browser HTML Table Export Demo</h3>
<b>Compatibility notes:</b>
- Editable table leverages the HTML5 contenteditable feature, supported in most browsers.