This commit is contained in:
SheetJS 2023-06-25 05:36:58 -04:00
parent 881a848c93
commit 86d7a8f06a
33 changed files with 727 additions and 130 deletions

@ -20,7 +20,7 @@ Each standalone release script is available at <https://cdn.sheetjs.com/>.
<script lang="javascript" src="https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js"></script>`}
</CodeBlock>
:::info
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when

@ -49,7 +49,7 @@ import { read, writeFileXLSX } from "xlsx";
The ["Bundlers" demo](/docs/demos/bundler) includes examples for specific tools.
:::info
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when

@ -39,7 +39,7 @@ yarn add https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`}
</TabItem>
</Tabs>
:::info
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when

@ -26,7 +26,7 @@ script as `xlsx.full.min`.
:::
:::info
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when

@ -22,7 +22,7 @@ After downloading the script, it can be directly referenced with `#include`:
#include "xlsx.extendscript.js"
```
:::info
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when

@ -31,7 +31,7 @@ be reported to the Deno project for further diagnosis.
:::
:::info
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when

@ -29,7 +29,7 @@ be reported to the Bun project for further diagnosis.
:::
:::info
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when

@ -20,7 +20,7 @@ read the installation instructions for your use case:
</li>);
})}</ul>
:::info
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when

@ -18,8 +18,15 @@ The discussion focuses on the problem solving mindset. API details are covered
in other parts of the documentation.
The goal of this example is to generate a XLSX workbook of US President names
and birthdays. [Click here](#live-demo) to jump to the live demo. The sequence
diagram below shows the process:
and birthdates. We will download and wrangle a JSON dataset using standard
JavaScript functions. Once we have a simple list of names and birthdates, we
will use SheetJS API functions to build a workbook object and export to XLSX.
The ["Live Demo"](#live-demo) section includes a working demo in this page!
["Run the Demo Locally"](#run-the-demo-locally) shows how to run the workflow in
iOS / Android apps, desktop apps, NodeJS scripts and other environments.
The follow sequence diagram shows the process:
```mermaid
sequenceDiagram
@ -31,17 +38,17 @@ sequenceDiagram
A->>P: raw data
Note over P: process data
Note over P: make workbook
Note over P: setup download
Note over P: export file
P->>U: download workbook
```
## Acquire Data
### Raw Data
The raw data is available in JSON form[^1]. It has been mirrored at
<https://sheetjs.com/data/executive.json>
[The raw data is available in JSON form](https://theunitedstates.io/congress-legislators/executive.json).
For convenience, it has been [mirrored here](https://sheetjs.com/data/executive.json)
### Raw Data
Acquiring the data is straightforward with `fetch`:
@ -50,23 +57,105 @@ const url = "https://sheetjs.com/data/executive.json";
const raw_data = await (await fetch(url)).json();
```
The raw data is an Array of objects. This is the data for John Adams:
<details><summary><b>Code Explanation</b> (click to show)</summary>
`fetch` is a low-level API for downloading data from an endpoint. It separates
the network step from the response parsing step.
**Network Step**
`fetch(url)` returns a `Promise` representing the network request. The browser
will attempt to download data from the URL. If the network request succeeded,
the `Promise` will "return" with a `Response` object.
Using modern syntax, inside an `async` function, code should `await` the fetch:
```js
const response = await fetch(url);
```
**Checking Status Code**
If the file is not available, the `fetch` will still succeed.
The status code, stored in the `status` property of the `Response` object, is a
standard HTTP status code number. Code should check the result.
Typically servers will return status `404` "File not Found" if the file is not
available. A successful request should have status `200` "OK".
**Extracting Data**
`Response#json` will try to parse the data using `JSON.parse`. Like `fetch`, the
`json` method returns a `Promise` that must be `await`-ed:
```js
const raw_data = await response.json();
```
:::note pass
The `Response` object has other useful methods. `Response#arrayBuffer` will
return the raw data as an `ArrayBuffer`, suitable for parsing workbook files.
:::
**Production Use**
Functions can test each part independently and report different errors:
```js
async function get_data_from_endpoint(url) {
/* perform network request */
let response;
try {
response = await fetch(url);
} catch(e) {
/* network error */
throw new Error(`Network Error: ${e.message}`);
}
/* check status code */
if(response.status == 404) {
/* server 404 error -- file not found */
throw new Error("File not found");
}
if(response.status != 200) {
/* for most servers, a successful response will have status 200 */
throw new Error(`Server status ${response.status}: ${response.statusText}`);
}
/* parse JSON */
let data;
try {
data = await response.json();
} catch(e) {
/* parsing error */
throw new Error(`Parsing Error: ${e.message}`);
}
return data;
}
```
</details>
The raw data is an Array of objects[^2]. For this discussion, the relevant data
for John Adams is shown below:
```js
{
"id": { /* (data omitted) */ },
"name": {
"first": "John", // <-- first name
"last": "Adams" // <-- last name
},
"bio": {
"birthday": "1735-10-19", // <-- birthday
"gender": "M"
},
"terms": [
{ "type": "viceprez", "start": "1789-04-21", /* (other fields omitted) */ },
{ "type": "viceprez", "start": "1793-03-04", /* (other fields omitted) */ },
{ "type": "prez", "start": "1797-03-04", /* (other fields omitted) */ }
"terms": [ // <-- array of presidential terms
{ "type": "viceprez", "start": "1789-04-21", },
{ "type": "viceprez", "start": "1793-03-04", },
{ "type": "prez", "start": "1797-03-04", } // <-- presidential term
]
}
```
@ -75,15 +164,62 @@ The raw data is an Array of objects. This is the data for John Adams:
The dataset includes Aaron Burr, a Vice President who was never President!
`Array#filter` creates a new array with the desired rows. A President served
at least one term with `type` set to `"prez"`. To test if a particular row has
at least one `"prez"` term, `Array#some` is another native JS function. The
complete filter would be:
The `terms` field of each object is an array of terms. A term is a Presidential
term if the `type` property is `"prez"`. We are interested in Presidents that
served at least one term. The following line creates an array of Presidents:
```js
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
```
:::caution pass
JavaScript code can be extremely concise. The "Code Explanation" blocks explain
the code in more detail.
:::
<details><summary><b>Code Explanation</b> (click to show)</summary>
**Verifying if a person was a US President**
`Array#some` takes a function and calls it on each element of an array in order.
If the function ever returns `true`, `Array#some` returns `true`. If each call
returns `false`, `Array#some` returns `false`.
The following function tests if a term is presidential:
```js
const term_is_presidential = term => term.type == "prez";
```
To test if a person was a President, that function should be tested against
every term in the `terms` array:
```js
const person_was_president = person => person.terms.some(term => term.type == "prez");
```
**Creating a list of US Presidents**
`Array#filter` takes a function and returns an array. The function is called on
each element in order. If the function returns `true`, the element is added to
the final array. If the function returns false, the element is not added.
Using the previous function, this line filters the dataset for Presidents:
```js
const prez = raw_data.filter(row => person_was_president(row));
```
Placing the `person_was_president` function in-line, the final code is:
```js
const prez = raw_data.filter(row => row.terms.some(term => term.type == "prez"));
```
</details>
### Sorting by First Term
The dataset is sorted in chronological order by the first presidential or vice
@ -94,21 +230,136 @@ data point appears first. The goal is to sort the presidents in order of their
presidential term.
The first step is adding the first presidential term start date to the dataset.
`Array#find` will find the first value in an array that matches a criterion.
The following code looks at each president and creates a `"start"` property that
The following code looks at each president and creates a `start` property that
represents the start of the first presidential term.
```js
prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start);
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
```
`Array#sort` will sort the array. Since the `start` properties are strings, the
recommended approach is to use `String#localeCompare` to compare strings:
<details><summary><b>Code Explanation</b> (click to show)</summary>
**Finding the first presidential term**
`Array#find` will find the first value in an array that matches a criterion.
The first presidential term can be found with the following function:
```js
const first_prez_term = prez => prez.terms.find(term => term.type === "prez");
```
:::note
If no element in the array matches the criterion, `Array#find` does not return
a value. In this case, since `prez` was created by filtering for people that
served at least one presidential term, the code assumes a term exists.
:::
The start of a President's first Presidential term is therefore
```js
const first_prez_term_start = prez => first_prez_term(prez).start;
```
**Adding the first start date to one row**
The following function creates the desired `start` property:
```js
const prez_add_start = prez => prez.start = first_prez_term_start(prez);
```
**Adding the first start date to each row**
`Array#forEach` takes a function and calls it for every element in the array.
Any modifications to objects affect the objects in the original array.
The previous function can be used directly:
```js
prez.forEach(row => prez_add_start(row));
```
Working in reverse, each partial function can be inserted in place. These lines
of code are equivalent:
```js
/* start */
prez.forEach(row => prez_add_start(row));
/* put `prez_add_start` definition into the line */
prez.forEach(row => row.start = first_prez_term_start(row));
/* put `first_prez_term_start` definition into the line */
prez.forEach(row => row.start = first_prez_term(row).start);
/* put `first_prez_term` definition into the line */
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
```
</details>
At this point, each row in the `prez` array has a `start` property. Since the
`start` properties are strings, the following line sorts the array:
```js
prez.sort((l,r) => l.start.localeCompare(r.start));
```
<details><summary><b>Code Explanation</b> (click to show)</summary>
**Comparator Functions and Relative Ordering in JavaScript**
A comparator takes two arguments and returns a number that represents the
relative ordering. `comparator(a,b)` should return a negative number if `a`
should be placed before `b`. If `b` should be placed before `a`, the comparator
should return a positive number.
If the `start` properties were numbers, the following comparator would suffice:
```js
const comparator_numbers = (a,b) => a - b;
```
For strings, JavaScript comparison operators can work:
```js
const comparator_string_simple = (a,b) => a == b ? 0 : a < b ? -1 : 1;
```
However, that comparator does not handle diacritics. For example, `"z" < "é"`.
It is strongly recommended to use `String#localeCompare` to compare strings:
```js
const comparator_string = (a,b) => a.localeCompare(b);
```
**Comparing two Presidents**
The `start` properties of the Presidents should be compared:
```js
const compare_prez = (a,b) => (a.start).localeCompare(b.start);
```
**Sorting the Array**
`Array#sort` takes a comparator function and sorts the array in place. Using
the Presidential comparator:
```js
prez.sort((l,r) => compare_prez(l,r));
```
Placing the `compare_prez` function in the body:
```js
prez.sort((l,r) => l.start.localeCompare(r.start));
```
</details>
### Reshaping the Array
For this example, the name will be the first name combined with the last name
@ -122,6 +373,86 @@ const rows = prez.map(row => ({
}));
```
<details><summary><b>Code Explanation</b> (click to show)</summary>
**Wrangling One Data Row**
The key fields for John Adams are shown below:
```js
{
"name": {
"first": "John", // <-- first name
"last": "Adams" // <-- last name
},
"bio": {
"birthday": "1735-10-19", // <-- birthday
}
}
```
If `row` is the object, then
- `row.name.first` is the first name ("John")
- `row.name.last` is the last name ("Adams")
- `row.bio.birthday` is the birthday ("1735-10-19")
The desired object has a `name` and `birthday` field:
```js
function get_data(row) {
var name = row.name.first + " " + row.name.last;
var birthday = row.bio.birthday;
return ({
name: name,
birthday: birthday
});
}
```
This can be shortened by adding the fields to the object directly:
```js
function get_data(row) {
return ({
name: row.name.first + " " + row.name.last,
birthday: row.bio.birthday
});
}
```
When writing an arrow function that returns an object, parentheses are required:
```js
// open paren required --V
const get_data = row => ({
name: row.name.first + " " + row.name.last,
birthday: row.bio.birthday
});
// ^-- close paren required
```
**Wrangling the entire dataset**
`Array#map` calls a function on each element of an array and returns a new array
with the return values of each function.
Using the previous method:
```js
const rows = prez.map(row => get_data(row));
```
The `get_data` function can be added in place:
```js
const rows = prez.map(row => ({
name: row.name.first + " " + row.name.last,
birthday: row.bio.birthday
}));
```
</details>
The result is an array of "simple" objects with no nesting:
```js
@ -134,13 +465,13 @@ The result is an array of "simple" objects with no nesting:
## Create a Workbook
With the cleaned dataset, `XLSX.utils.json_to_sheet` generates a worksheet:
With the cleaned dataset, `XLSX.utils.json_to_sheet`[^3] generates a worksheet:
```js
const worksheet = XLSX.utils.json_to_sheet(rows);
```
`XLSX.utils.book_new` creates a new workbook and `XLSX.utils.book_append_sheet`
`XLSX.utils.book_new`[^4] creates a new workbook and `XLSX.utils.book_append_sheet`[^5]
appends a worksheet to the workbook. The new worksheet will be called "Dates":
```js
@ -163,7 +494,7 @@ additional styling options like cell styling and frozen rows.
By default, `json_to_sheet` creates a worksheet with a header row. In this case,
the headers come from the JS object keys: "name" and "birthday".
The headers are in cells `A1` and `B1`. `XLSX.utils.sheet_add_aoa` can write
The headers are in cells `A1` and `B1`. `XLSX.utils.sheet_add_aoa`[^6] can write
text values to the existing worksheet starting at cell `A1`:
```js
@ -175,7 +506,7 @@ XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
<details><summary><b>Changing Column Widths</b> (click to show)</summary>
Some of the names are longer than the default column width. Column widths are
set by setting the `"!cols"` worksheet property.
set by setting the `"!cols"` worksheet property.[^7]
The following line sets the width of column A to approximately 10 characters:
@ -198,9 +529,9 @@ After cleanup, the generated workbook looks like the screenshot below:
## Export a File
`XLSX.writeFile` creates a spreadsheet file and tries to write it to the system.
In the browser, it will try to prompt the user to download the file. In NodeJS,
it will write to the local directory.
`XLSX.writeFile`[^8] creates a spreadsheet file and tries to write it to the
system. In the browser, it will try to prompt the user to download the file. In
NodeJS, it will write to the local directory.
```js
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
@ -221,7 +552,7 @@ function Presidents() { return ( <button onClick={async () => {
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
/* sort by first presidential term */
prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start);
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
prez.sort((l,r) => l.start.localeCompare(r.start));
/* flatten objects */
@ -269,7 +600,7 @@ Save the following script to `SheetJSStandaloneDemo.html`:
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
\n\
/* sort by first presidential term */
prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start);
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
prez.sort((l,r) => l.start.localeCompare(r.start));
\n\
/* flatten objects */
@ -330,7 +661,7 @@ const XLSX = require("xlsx");
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
/* sort by first presidential term */
prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start);
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
prez.sort((l,r) => l.start.localeCompare(r.start));
/* flatten objects */
@ -395,7 +726,7 @@ const axios = require("axios");
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
/* sort by first presidential term */
prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start);
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
prez.sort((l,r) => l.start.localeCompare(r.start));
/* flatten objects */
@ -452,7 +783,7 @@ const raw_data = await (await fetch(url)).json();
const prez = raw_data.filter((row: any) => row.terms.some((term: any) => term.type === "prez"));
\n\
/* sort by first presidential term */
prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start);
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
prez.sort((l,r) => l.start.localeCompare(r.start));
\n\
/* flatten objects */
@ -509,7 +840,7 @@ const raw_data = await (await fetch(url)).json();
const prez = raw_data.filter((row) => row.terms.some((term) => term.type === "prez"));
/* sort by first presidential term */
prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start);
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
prez.sort((l,r) => l.start.localeCompare(r.start));
/* flatten objects */
@ -566,7 +897,7 @@ Save the following script to `SheetJSNW.html`:
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
\n\
/* sort by first presidential term */
prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start);
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
prez.sort((l,r) => l.start.localeCompare(r.start));
\n\
/* flatten objects */
@ -654,7 +985,7 @@ const make_workbook = async() => {
const prez = raw_data.filter(row => row.terms.some(term => term.type === "prez"));
/* sort by first presidential term */
prez.forEach(prez => prez.start = prez.terms.find(term => term.type === "prez").start);
prez.forEach(row => row.start = row.terms.find(term => term.type === "prez").start);
prez.sort((l,r) => l.start.localeCompare(r.start));
/* flatten objects */
@ -816,4 +1147,16 @@ see a preview of the data. The Numbers app can open the file.
</Tabs>
</TabItem>
</Tabs>
</Tabs>
[^1]: <https://theunitedstates.io/congress-legislators/executive.json> is the
original location of the example dataset. The contributors to the dataset
dedicated the content to the public domain.
[^2]: See ["The Executive Branch](https://github.com/unitedstates/congress-legislators#the-executive-branch)
in the dataset documentation.
[^3]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
[^4]: See [`book_new` in "Utilities"](/docs/api/utilities/wb)
[^5]: See [`book_append_sheet` in "Utilities"](/docs/api/utilities/wb)
[^6]: See [`sheet_add_aoa` in "Utilities"](/docs/api/utilities/array#array-of-arrays-input)
[^7]: See ["Row and Column Properties"](/docs/csf/features/#row-and-column-properties)
[^8]: See [`writeFile` in "Writing Files"](/docs/api/write-options)

@ -31,7 +31,7 @@ read the installation instructions for your use case:
});
})}</ul>
:::info
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when

@ -197,28 +197,28 @@ the table element works with the SheetJS DOM methods after patching the object.
This example fetches [a sample table](pathname:///dom/SheetJSTable.html):
```ts title="SheetJSDenoDOM.ts"
// @deno-types="https://cdn.sheetjs.com/xlsx-0.19.3/package/types/index.d.ts"
import * as XLSX from 'https://cdn.sheetjs.com/xlsx-0.19.3/package/xlsx.mjs';
<CodeBlock language="ts" title="SheetJSDenoDOM.ts">{`\
// @deno-types="https://cdn.sheetjs.com/xlsx-${current}/package/types/index.d.ts"
import * as XLSX from 'https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs';
\n\
import { DOMParser } from 'https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts';
\n\
const doc = new DOMParser().parseFromString(
await (await fetch('https://docs.sheetjs.com/dom/SheetJSTable.html')).text(),
await (await fetch('https://docs.sheetjs.com/dom/SheetJSTable.html')).text(),
"text/html",
)!;
// highlight-start
const tbl = doc.querySelector("table");
\n\
/* patch DenoDOM element */
tbl.rows = tbl.querySelectorAll("tr");
tbl.rows.forEach(row => row.cells = row.querySelectorAll("td, th"))
\n\
/* generate workbook */
const workbook = XLSX.utils.table_to_book(tbl);
// highlight-end
XLSX.writeFile(workbook, "SheetJSDenoDOM.xlsx");
```
XLSX.writeFile(workbook, "SheetJSDenoDOM.xlsx");`}
</CodeBlock>
<details open><summary><b>Complete Demo</b> (click to hide)</summary>

@ -375,7 +375,7 @@ wails build
At the end, it will print the path to the generated program. Run the program!
[^1]: See ["How does it Work?"](https://wails.io/docs/howdoesitwork) in the Wails documentation.
[^2]: See [`read` in "Parsing Options"](/docs/api/parse-options)
[^2]: See [`read` in "Reading Files"](/docs/api/parse-options)
[^3]: See [`sheet_to_html` in "Utilities"](/docs/api/utilities/html#html-table-output)
[^4]: See [`OpenFileDialog`](https://wails.io/docs/reference/runtime/dialog#openfiledialog) in the Wails documentation.
[^5]: See [`ReadFile`](https://pkg.go.dev/os#ReadFile) in the Go documentation

@ -1,5 +1,7 @@
---
title: Tauri
sidebar_label: Tauri
description: Build data-intensive desktop apps using Tauri. Seamlessly integrate spreadsheets into your app using SheetJS. Modernize Excel-powered business processes with confidence.
pagination_prev: demos/mobile/index
pagination_next: demos/data/index
sidebar_position: 4
@ -7,15 +9,25 @@ sidebar_custom_props:
summary: Webview + Rust Backend
---
# Data Wranging in Tauri Apps
import current from '/version.js';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
The [NodeJS Module](/docs/getting-started/installation/nodejs) can be imported
from JavaScript code.
[Tauri](https://tauri.app/) is a modern toolkit for building desktop apps. Tauri
apps leverage platform-native browser engines to build lightweight programs.
The "Complete Example" creates an app that looks like the screenshot:
[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
data from spreadsheets.
This demo uses Tauri and SheetJS to pull data from a spreadsheet and display the
data in the app. We'll explore how to load SheetJS in a Tauri app and exchange
file data between the JavaScript frontend and Rust backend.
The ["Complete Example"](#complete-example) section covers a complete desktop
app to read and write workbooks. The app will look like the screenshots below:
<table><thead><tr>
<th><a href="#complete-example">Win10</a></th>
@ -37,33 +49,74 @@ The "Complete Example" creates an app that looks like the screenshot:
## Integration Details
:::note
The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be
installed and imported from JavaScript code.
:::note pass
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`, `dialog`, and `fs` must be explicitly allowed in `tauri.conf.json`:
For security reasons, Tauri apps must explicitly enable system features.[^1]
They are enabled in `src-tauri/tauri.conf.json` in the `allowlist` subsection of
the `tauri` section of the config.
- The `fs` entitlement[^2] enables reading and writing file data.
```js title="src-tauri/tauri.conf.json"
"tauri": {
"allowlist": {
//highlight-start
"fs": {
"all": true
}
// highlight-end
```
- The `dialog` entitlement[^3] enables the open and save dialog methods.
```js title="src-tauri/tauri.conf.json"
"tauri": {
"allowlist": {
//highlight-start
"dialog": {
"all": true
}
// highlight-end
```
- The `http` entitlement[^4] enables downloading files. Note that `http` is not
needed for reading or writing files in the local filesystem.
```json title="src-tauri/tauri.conf.json"
"tauri": {
"allowlist": {
//highlight-start
"http": {
"all": true,
"request": true,
"scope": ["https://**"]
},
"dialog": {
"all": true
},
"fs": {
"all": true
}
// highlight-end
```
### Reading Files
There are two steps to reading files: obtaining a path and reading binary data:
There are three steps to reading files:
1) Show an open file dialog to allow users to select a path. The `open` method
in `@tauri-apps/api/dialog`[^5] simplifies this process.
2) Read raw data from the selected file using the `readBinaryFile` method in
`@tauri-apps/api/fs`[^6]. This method resolves to a standard `Uint8Array`
3) Parse the data with the SheetJS `read` method[^7]. This method returns a
SheetJS workbook object.
The following code example defines a single function `openFile` that performs
all three steps and returns a SheetJS workbook object:
```js
import { read } from 'xlsx';
@ -95,9 +148,47 @@ async function openFile() {
}
```
At this point, standard SheetJS utility functions[^8] can extract data from the
workbook object. The demo includes a button that calls `sheet_to_json`[^9] to
generate an array of arrays of data. The following snippet uses VueJS framework
but the same logic works with ReactJS and other front-end frameworks:
```js
import { utils } from 'xlsx';
import { shallowRef } from 'vue';
const data = shallowRef([[]]); // update data by setting `data.value`
const open_button_callback = async() => {
const wb = await openFile();
/* get the first worksheet */
// highlight-start
const ws = wb.Sheets[wb.SheetNames[0]];
// highlight-end
/* get data from the first worksheet */
// highlight-start
const array = utils.sheet_to_json(ws, { header: 1 });
// highlight-end
data.value = array;
};
```
### Writing Files
There are two steps to writing files: obtaining a path and writing binary data:
There are three steps to writing files:
1) Show a save file dialog to allow users to select a path. The `save` method
in `@tauri-apps/api/dialog`[^10] simplifies this process.
2) Write the data with the SheetJS `write` method[^11]. The output book type can
be inferred from the selected file path. Using the `buffer` output type[^12],
the method will return a `Uint8Array` object that plays nice with Tauri.
3) Write the data using `writeBinaryFile` in `@tauri-apps/api/fs`[^13].
The following code example defines a single function `saveFile` that performs
all three steps starting from a SheetJS workbook object:
```js
import { write } from 'xlsx';
@ -128,15 +219,82 @@ async function saveFile(wb) {
}
```
The demo includes a button that calls `aoa_to_sheet`[^14] to generate a sheet
from array of arrays of data. A workbook is constructed using `book_new` and
`book_append_sheet`[^15]. The following snippet uses VueJS framework but the
same logic works with ReactJS and other front-end frameworks:
```js
import { utils } from 'xlsx';
import { shallowRef } from 'vue';
const data = shallowRef([[]]); // `data.value` is an array of arrays
const save_button_callback = async() => {
/* generate worksheet from the data */
// highlight-start
const ws = utils.aoa_to_sheet(data.value);
// highlight-end
/* create a new workbook object */
// highlight-start
const wb = utils.book_new();
// highlight-end
/* append the worksheet to the workbook using the sheet name "SheetJSTauri" */
// highlight-start
utils.book_append_sheet(wb, ws, "SheetJSTauri");
// highlight-end
await saveFile(wb);
}
```
## Complete Example
:::note
:::note pass
This demo was tested against Tauri `v1.2.3` on 2023 March 18.
This demo was tested in the following environments:
| OS and Version | Arch | Tauri | Date |
|:---------------|:-----|:---------|:-----------|
| macOS 13.4.0 | x64 | `v1.4.0` | 2023-06-25 |
| Windows 10 | x64 | `v1.2.3` | 2023-03-18 |
| Linux (HoloOS) | x64 | `v1.2.3` | 2023-03-18 |
:::
0) [Read Tauri "Getting Started" guide and install dependencies.](https://tauri.app/v1/guides/getting-started/prerequisites)
0) Read Tauri "Getting Started" guide and install prerequisites.[^16]
<details><summary><b>Installation Notes</b> (click to show)</summary>
At a high level, the following software is required for building Tauri apps:
- a native platform-specific C/C++ compiler (for example, macOS requires Xcode)
- a browser engine integration (for example, linux requires `webkit2gtk`)
- [Rust](https://www.rust-lang.org/tools/install)
The platform configuration can be verified by running:
```bash
npx @tauri-apps/cli info
```
If required dependencies are installed, the output will show a checkmark next to
"Environment". The output from the most recent macOS test is shown below:
```
[✔] Environment
- OS: Mac OS 13.4.0 X64
✔ Xcode Command Line Tools: installed
✔ rustc: 1.70.0 (90c541806 2023-05-31)
✔ Cargo: 1.70.0 (ec8a8a0ca 2023-04-25)
✔ rustup: 1.26.0 (5af9b9484 2023-04-05)
✔ Rust toolchain: stable-x86_64-apple-darwin (default)
- node: 18.16.1
- npm: 9.5.1
```
</details>
1) Create a new Tauri app:
@ -161,8 +319,8 @@ npm i --save @tauri-apps/api
npm i --save-dev @tauri-apps/cli`}
</CodeBlock>
3) Enable operations by adding the highlighted lines to `tauri.conf.json` in
the `tauri.allowlist` section:
3) Add the highlighted lines to `src-tauri/tauri.conf.json` in the
`tauri.allowlist` section:
```json title="src-tauri/tauri.conf.json"
"tauri": {
@ -206,4 +364,30 @@ curl -L -o src/App.vue https://docs.sheetjs.com/tauri/App.vue
npm run tauri build
```
At the end, it will print the path to the generated program. Run the program!
At the end, it will print the path to the generated program.
6) Run the program. The following features should be manually verified:
- When it is opened, the app will download <https://sheetjs.com/pres.numbers>
and display the data in a table.
- Clicking "Save Data" will show a save dialog. After selecting a path and name,
the app will write a file. That file can be opened in a spreadsheet editor.
- Edit the file in a spreadsheet editor, then click "Load Data" and select the
edited file. The table will refresh with new contents.
[^1]: See ["Security"](https://tauri.app/v1/references/architecture/security#allowing-api) in the Tauri documentation
[^2]: See [`FsAllowlistConfig`](https://tauri.app/v1/api/config/#fsallowlistconfig) in the Tauri documentation
[^3]: See [`DialogAllowlistConfig`](https://tauri.app/v1/api/config/#dialogallowlistconfig) in the Tauri documentation
[^4]: See [`HttpAllowlistConfig`](https://tauri.app/v1/api/config/#httpallowlistconfig) in the Tauri documentation
[^5]: See [`dialog`](https://tauri.app/v1/api/js/dialog/#open) in the Tauri documentation
[^6]: See [`fs`](https://tauri.app/v1/api/js/fs#readbinaryfile) in the Tauri documentation
[^7]: See [`read` in "Reading Files"](/docs/api/parse-options)
[^8]: See ["Utility Functions"](/docs/api/utilities/)
[^9]: See ["Array Output" in "Utility Functions"](/docs/api/utilities/array#array-output)
[^10]: See [`dialog`](https://tauri.app/v1/api/js/dialog/#save) in the Tauri documentation
[^11]: See [`write` in "Writing Files"](/docs/api/write-options)
[^12]: See ["Supported Output Formats"](/docs/api/write-options#supported-output-formats)
[^13]: See [`fs`](https://tauri.app/v1/api/js/fs#writebinaryfile) in the Tauri documentation
[^14]: See ["Array of Arrays Input" in "Utility Functions"](/docs/api/utilities/array#array-of-arrays-input)
[^15]: See ["Workbook Helpers" in "Utility Functions"](/docs/api/utilities/wb)
[^16]: See ["Prerequisites"](https://tauri.app/v1/guides/getting-started/prerequisites) in the Tauri documentation

@ -692,12 +692,8 @@ _Create a new Workbook_
var workbook = XLSX.utils.book_new();
```
The `book_new` utility function creates an empty workbook with no worksheets.
Spreadsheet software generally require at least one worksheet and enforce the
requirement in the user interface. This library enforces the requirement at
write time, throwing errors if an empty workbook is passed to write functions.
The [`book_new` utility function](/docs/api/utilities/wb) creates an empty
workbook with no worksheets.
#### API

@ -66,10 +66,8 @@ _Append a Worksheet to a Workbook_
XLSX.utils.book_append_sheet(workbook, worksheet, sheet_name);
```
The `book_append_sheet` utility function appends a worksheet to the workbook.
The third argument specifies the desired worksheet name. Multiple worksheets can
be added to a workbook by calling the function multiple times. If the worksheet
name is already used in the workbook, it will throw an error.
The [`book_append_sheet`](/docs/api/utilities/wb) utility function appends a
worksheet to the workbook.
_Append a Worksheet to a Workbook and find a unique name_
@ -77,16 +75,8 @@ _Append a Worksheet to a Workbook and find a unique name_
var new_name = XLSX.utils.book_append_sheet(workbook, worksheet, name, true);
```
If the fourth argument is `true`, the function will start with the specified
worksheet name. If the sheet name exists in the workbook, a new worksheet name
will be chosen by finding the name stem and incrementing the counter:
```js
XLSX.utils.book_append_sheet(workbook, sheetA, "Sheet2", true); // Sheet2
XLSX.utils.book_append_sheet(workbook, sheetB, "Sheet2", true); // Sheet3
XLSX.utils.book_append_sheet(workbook, sheetC, "Sheet2", true); // Sheet4
XLSX.utils.book_append_sheet(workbook, sheetD, "Sheet2", true); // Sheet5
```
If the fourth argument is `true`, the function will try to find a new worksheet
name in case of a collision.
#### Examples

@ -29,10 +29,10 @@ include options for specifying the date system
| XLSX (Strict ISO) | ✔ | ✔ | ✔ | Relative Date | 1900 + 1904 |
| XLSB | ✔ | ✔ | ✔ | Number | 1900 + 1904 |
| XLML | ✔ | ✔ | ✔ | Relative Date | 1900 + 1904 |
| XLS (BIFF5/8) | ✔ | ✔ | ✔ | Relative Date | 1900 + 1904 |
| XLS (BIFF2/3/4) | ✔ | ✔ | ✔ | Relative Date | 1900 + 1904 |
| XLR (Works) | ✔ | ✔ | ✔ | Relative Date | 1900 + 1904 |
| ET (WPS 电子表格) | ✔ | ✔ | ✔ | Relative Date | 1900 + 1904 |
| XLS (BIFF5/8) | ✔ | ✔ | ✔ | Number | 1900 + 1904 |
| XLS (BIFF2/3/4) | ✔ | ✔ | ✔ | Number | 1900 + 1904 |
| XLR (Works) | ✔ | ✔ | ✔ | Number | 1900 + 1904 |
| ET (WPS 电子表格) | ✔ | ✔ | ✔ | Number | 1900 + 1904 |
| ODS / FODS / UOS | ✔ | ✔ | ✔ | ISO Duration or Date | Arbitrary |
| HTML | ✔ | ✔ | ✔ | Plaintext | Calendar |
| CSV / TSV / Text | ✔ | ✔ | ✔ | Plaintext | Calendar |

@ -7,6 +7,9 @@ sidebar_position: 6
<details>
<summary><b>File Format Support</b> (click to show)</summary>
[Date and Time support](/docs/csf/features/dates) requires limited number format
support to distinguish date or time codes from standard numeric data.
Legacy formats like CSV mix "content" and "presentation". There is no true
concept of a "number format" distinct from the number itself. For specific
formats, the library will guess the number format.
@ -20,8 +23,14 @@ formats, the library will guess the number format.
| SYLK | R | Number Format Code |
| ODS / FODS / UOS | ✔ | XML |
| NUMBERS | | Binary encoding |
| WK\* | | Binary encoding |
| WQ\* / WB\* / QPW | | Binary encoding |
| WK1 | + | Fixed set of formats |
| WK3 / WK4 | | Binary encoding |
| WKS Lotus | + | Fixed set of formats |
| WKS Works | + | Fixed set of formats |
| WQ1 | + | Fixed set of formats |
| WQ2 | | Binary encoding |
| WB1 / WB2 / WB3 | | Binary encoding |
| QPW | + | Binary encoding |
| DBF | | Implied by field types |
| HTML | * | Special override |
| CSV | * | N/A |
@ -29,6 +38,10 @@ formats, the library will guess the number format.
| DIF | * | N/A |
| RTF | * | N/A |
(+) mark formats with limited support. The QPW (Quattro Pro Workbooks) parser
supports the built-in date and built-in time formats but does not support
custom number formats.
Asterisks (*) mark formats that mix content and presentation. Writers will use
formatted values if cell objects include formatted text or number formats.
Parsers may guess number formats for special values.

@ -83,12 +83,12 @@ Strings can be interpreted in multiple ways. The `type` parameter for `read`
tells the library how to parse the data argument:
| `type` | expected input |
|------------|-----------------------------------------------------------------|
|:-----------|:----------------------------------------------------------------|
| `"base64"` | string: Base64 encoding of the file |
| `"binary"` | string: binary string (byte `n` is `data.charCodeAt(n)`) |
| `"string"` | string: JS string (only appropriate for UTF-8 text formats) |
| `"buffer"` | nodejs Buffer |
| `"array"` | array: array of 8-bit unsigned int (byte `n` is `data[n]`) |
| `"array"` | array: array of 8-bit unsigned integers (byte `n` is `data[n]`) |
| `"file"` | string: path of file that will be read (nodejs only) |
Some common types are automatically deduced from the data input type, including

@ -76,9 +76,12 @@ The write functions accept an options argument:
| :---------- | -------: | :------------------------------------------------- |
|`type` | | Output data encoding (see Output Type below) |
|`cellDates` | `false` | Store dates as type `d` (default is `n`) |
|`cellStyles` | `false` | Save style/theme info to the `.s` field |
|`codepage` | | If specified, use code page when appropriate ** |
|`bookSST` | `false` | Generate Shared String Table ** |
|`bookType` | `"xlsx"` | Type of Workbook (see below for supported formats) |
|`bookVBA` | | Add VBA blob from workbook object to the file ** |
|`WTF` | `false` | If true, throw errors on unexpected features ** |
|`sheet` | `""` | Name of Worksheet for single-sheet formats ** |
|`compression`| `false` | Use ZIP compression for ZIP-based formats ** |
|`Props` | | Override workbook properties when writing ** |
@ -106,6 +109,9 @@ The write functions accept an options argument:
files to ignore the error by default. Set `ignoreEC` to `false` to suppress.
- `FS` and `RS` apply to CSV and Text output formats. The options are discussed
in ["CSV and Text"](/docs/api/utilities/csv#delimiter-separated-output)
- `bookVBA` only applies to supported formats. ["VBA"](/docs/csf/features/vba)
section explains the feature in more detail.
- `WTF` is mainly for development.
<details open>
<summary><b>Exporting NUMBERS files</b> (click to show)</summary>

@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 1
title: Arrays of Data
---

@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 3
title: HTML
---

@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 5
title: CSV and Text
---

@ -1,7 +1,6 @@
---
sidebar_position: 9
sidebar_position: 7
title: Array of Formulae
pagination_next: miscellany/formats
---
**Extract all formulae from a worksheet**

@ -0,0 +1,51 @@
---
sidebar_position: 8
title: Workbook Helpers
---
Many utility functions return worksheet objects. Worksheets cannot be written to
workbook file formats directly. They must be added to a workbook object.
**Create a new workbook**
```js
var workbook = XLSX.utils.book_new();
```
The `book_new` utility function creates an empty workbook with no worksheets.
Spreadsheet software generally require at least one worksheet and enforce the
requirement in the user interface. For example, if the last worksheet is deleted
in the program, Apple Numbers will automatically create a new blank sheet.
The SheetJS [write functions](/docs/api/write-options) enforce the requirement.
They will throw errors when trying to export empty worksheets.
**Append a Worksheet to a Workbook**
```js
XLSX.utils.book_append_sheet(workbook, worksheet, sheet_name);
```
The `book_append_sheet` utility function appends a worksheet to the workbook.
The third argument specifies the desired worksheet name. Multiple worksheets can
be added to a workbook by calling the function multiple times. If the worksheet
name is already used in the workbook, it will throw an error.
_Append a Worksheet to a Workbook and find a unique name_
```js
var new_name = XLSX.utils.book_append_sheet(workbook, worksheet, name, true);
```
If the fourth argument is `true`, the function will start with the specified
worksheet name. If the sheet name exists in the workbook, a new worksheet name
will be chosen by finding the name stem and incrementing the counter:
```js
XLSX.utils.book_append_sheet(workbook, sheetA, "Sheet2", true); // Sheet2
XLSX.utils.book_append_sheet(workbook, sheetB, "Sheet2", true); // Sheet3
XLSX.utils.book_append_sheet(workbook, sheetC, "Sheet2", true); // Sheet4
XLSX.utils.book_append_sheet(workbook, sheetD, "Sheet2", true); // Sheet5
```

@ -54,7 +54,7 @@ flowchart LR
var arr = XLSX.utils.sheet_to_json(ws, opts);
```
[**This functions is described in a dedicated page**](/docs/api/utilities/array#array-of-objects-input)
[**This function is described in a dedicated page**](/docs/api/utilities/array#array-of-objects-input)
## Array of Arrays Input

@ -102,13 +102,15 @@ _Exporting Formulae:_
- `sheet_to_formulae` generates a list of formulae or cell value assignments.
**[Utility Functions](/docs/api/utilities)**
**["Workbook Helpers" section of "Utility Functions"](/docs/api/utilities/wb)**
_Workbook Operations:_
- `book_new` creates an empty workbook
- `book_append_sheet` adds a worksheet to a workbook
**[Utility Functions](/docs/api/utilities)**
_Miscellaneous_
- `format_cell` generates the text value for a cell (using number formats).

@ -52,7 +52,7 @@ range limits will be silently truncated:
|:------------------------------------------|:-----------|---------:|---------:|
| Excel 2007+ XML Formats (XLSX/XLSM) |`XFD1048576`| 16384 | 1048576 |
| Excel 2007+ Binary Format (XLSB BIFF12) |`XFD1048576`| 16384 | 1048576 |
| Numbers 12.1 (NUMBERS) |`ALL1000000`| 1000 | 1000000 |
| Numbers 13.1 (NUMBERS) |`ALL1000000`| 1000 | 1000000 |
| Quattro Pro 9+ (QPW) |`IV1000000 `| 256 | 1000000 |
| Excel 97-2004 (XLS BIFF8) |`IV65536 `| 256 | 65536 |
| Excel 5.0/95 (XLS BIFF5) |`IV16384 `| 256 | 16384 |
@ -175,7 +175,7 @@ XLR also includes a `WksSSWorkBook` stream similar to Lotus FM3/FMT files.
iWork 2013 (Numbers 3.0 / Pages 5.0 / Keynote 6.0) switched from a proprietary
XML-based format to the current file format based on the iWork Archive (IWA).
This format has been used up through the current release (Numbers 12.1) as well
This format has been used up through the current release (Numbers 13.1) as well
as the iCloud.com web interface to Numbers.
The parser focuses on extracting raw data from tables. Numbers technically

@ -6,4 +6,14 @@ hide_table_of_contents: true
The official source code repository is <https://git.sheetjs.com/sheetjs/sheetjs>
Issues should be raised at <https://git.sheetjs.com/sheetjs/sheetjs/issues>
Issues should be raised at <https://git.sheetjs.com/sheetjs/sheetjs/issues>
The official changelog can be found [in the source code repository](https://git.sheetjs.com/sheetjs/sheetjs/raw/branch/master/CHANGELOG.md)
:::tip pass
[Watch the repo](https://git.sheetjs.com/SheetJS/sheetjs) or subscribe to the
[RSS feed](https://git.sheetjs.com/sheetjs/sheetjs/tags.rss) to be notified when
new versions are released!
:::

@ -275,28 +275,28 @@ git checkout -- .
### Reproduce official builds
4) Run `git log` and search for the commit that matches a particular release
version. For example, version `0.19.3` can be found with:
version. For example, version `0.20.0` can be found with:
```bash
git log | grep -B4 "version bump 0.19.3"
git log | grep -B4 "version bump 0.20.0"
```
The output should look like:
```bash
$ git log | grep -B4 "version bump 0.19.3"
$ git log | grep -B4 "version bump 0.20.0"
# highlight-next-line
commit 333e4e40f9c5603bd22a811f54c61c20bc9e17ab <-- this is the commit hash
commit 955543147dac0274d20307057c5a9f3e3e5d5307 <-- this is the commit hash
Author: SheetJS <dev@sheetjs.com>
Date: Mon Apr 17 23:39:28 2023 -0400
Date: Fri Jun 23 05:48:47 2023 -0400
version bump 0.19.3
version bump 0.20.0
```
5) Switch to that commit:
```bash
git checkout 333e4e40f9c5603bd22a811f54c61c20bc9e17ab
git checkout 955543147dac0274d20307057c5a9f3e3e5d5307
```
6) Run the full build sequence
@ -314,14 +314,14 @@ The local checksum for the browser script can be computed with:
```bash
$ md5sum dist/xlsx.full.min.js
f5c73b5ddc4b431c909d11c2e1d7a8e0 dist/xlsx.full.min.js
0b2f539797f92d35c6394274818f2c22 dist/xlsx.full.min.js
```
The checksum for the CDN version can be computed with:
```bash
$ curl -L https://cdn.sheetjs.com/xlsx-0.19.3/package/dist/xlsx.full.min.js | md5sum -
f5c73b5ddc4b431c909d11c2e1d7a8e0 -
$ curl -L https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js | md5sum -
0b2f539797f92d35c6394274818f2c22 -
```
The two hashes should match.

@ -102,7 +102,7 @@ support for CSS styling and rich text.
</details>
### Download and Preview a Numbers workbook
### Download and Preview Apple Numbers Workbooks
<details><summary><b>How to add to your site</b> (click to show)</summary>
@ -141,7 +141,9 @@ support for CSS styling and rich text.
</details>
<details><summary><b>Live Example</b> (click to show)</summary>
<details open><summary><b>Live Example</b> (click to hide)</summary>
This demo processes <https://sheetjs.com/pres.numbers>
```jsx live
/* The live editor requires this function wrapper */

@ -1,6 +1,6 @@
// @deno-types="https://cdn.sheetjs.com/xlsx-0.19.3/package/types/index.d.ts"
import { read, utils, set_cptable, version } from 'https://cdn.sheetjs.com/xlsx-0.19.3/package/xlsx.mjs';
import * as cptable from 'https://cdn.sheetjs.com/xlsx-0.19.3/package/dist/cpexcel.full.mjs';
// @deno-types="https://cdn.sheetjs.com/xlsx-0.20.0/package/types/index.d.ts"
import { read, utils, set_cptable, version } from 'https://cdn.sheetjs.com/xlsx-0.20.0/package/xlsx.mjs';
import * as cptable from 'https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/cpexcel.full.mjs';
set_cptable(cptable);
import * as Drash from "https://deno.land/x/drash@v2.5.4/mod.ts";

@ -4,6 +4,7 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="robots" content="noindex">
<title>SheetJS Live Demo</title>
<style>
#drop{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 204 KiB