├── .babelrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── webpack.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── bower.json ├── dist ├── excellentexport.d.ts ├── excellentexport.js ├── excellentexport.js.LICENSE.txt ├── format.d.ts └── utils.d.ts ├── index.bigtable.html ├── index.filters.html ├── index.format.html ├── index.html ├── index.noanchor.html ├── index.require.html ├── index.rtl.html ├── jest.config.ts ├── package-lock.json ├── package.json ├── scripts └── postinstall.js ├── src ├── excellentexport.ts ├── format.ts └── utils.ts ├── test ├── checkversion.test.ts ├── convert-filters.test.ts ├── convert-table.test.ts ├── convert.format.ts ├── convert.test.ts ├── fixdata.test.ts ├── negative.test.ts ├── simple.test.ts ├── utils.test.ts ├── utils_fixdata.test.ts └── utils_removeColumns.test.ts ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" ], 4 | "plugins": [ 5 | "@babel/plugin-proposal-class-properties", 6 | "@babel/plugin-transform-flow-strip-types" 7 | ] 8 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jmaister 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 0 8 | -------------------------------------------------------------------------------- /.github/workflows/webpack.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [22.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | 29 | - run: npm install 30 | 31 | - run: npm test 32 | 33 | - name: Upload coverage reports to Codecov 34 | uses: codecov/codecov-action@v5 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject 2 | /.project 3 | node_modules 4 | .tmp 5 | .idea 6 | 7 | coverage/* 8 | /.cache 9 | 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2025 Jordi Burgos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Node CI](https://github.com/jmaister/excellentexport/actions/workflows/webpack.yml/badge.svg?branch=master)](https://github.com/jmaister/excellentexport/actions/workflows/webpack.yml) 3 | [![](https://data.jsdelivr.com/v1/package/npm/excellentexport/badge)](https://www.jsdelivr.com/package/npm/excellentexport) 4 | [![codecov](https://codecov.io/gh/jmaister/excellentexport/graph/badge.svg?token=CMhCN0GnmY)](https://codecov.io/gh/jmaister/excellentexport) 5 | 6 | # ExcellentExport.js 7 | 8 | - [:heart: Sponsor ExcellentExport.js project:heart:](https://github.com/sponsors/jmaister) 9 | 10 | - JavaScript export to Excel or CSV. 11 | 12 | - A quick JavaScript library to create export to Excel/CSV from HTML tables in the browser. No server required. 13 | 14 | - As part of the new version 3.0.0+, there is support for _XLSX_. The drawback is that the library is 200+ KB. 15 | 16 | - Check My Blog Page for Testing : 17 | [JavaScript export to Excel](http://jordiburgos.com/post/2013/javascript-export-to-excel.html) 18 | 19 | [ExcellentExport.js update: JavaScript export to Excel and CSV](http://jordiburgos.com/post/2017/excellentexport-javascript-export-to-excel-csv.html) 20 | 21 | # Revision history: 22 | 23 | ### 3.9.9 24 | 25 | * Configure codecov to track code coverage. 26 | * Removed coveralls, the library used is too old, not updated in years and has many vulnerabilities. 27 | * Remove unused dependencies (there was a lot of them). 28 | 29 | ### 3.9.8 30 | 31 | * _Update npm dependencies to fix vulnerabilities_ 32 | * Moving to npm build (package-lock.json), yarn does not support "audit fix" command to fix dependencies vulnerabilities automatically. 33 | 34 | ### 3.9.7 35 | 36 | * _Update npm dependencies to fix vulnerabilities_ 37 | * xlsx package loaded from CDN [SheetJS CDN](https://cdn.sheetjs.com/) 38 | 39 | ### 3.9.6 40 | 41 | * Remove references to openbase.io 42 | * Fix typos 43 | * _Update npm dependencies to fix vulnerabilities_ 44 | 45 | ### 3.9.5 46 | 47 | * _Update npm dependencies to fix vulnerabilities_ 48 | 49 | ### 3.9.4 50 | 51 | * _Update npm dependencies to fix vulnerabilities_ 52 | 53 | ### 3.9.3 54 | 55 | * Fix TypeScript exported types 56 | 57 | ### 3.9.0 58 | 59 | * Cell types and formats!!! Now you can define the cell type and format. For example, you can define a cell as a date or a number. You can also define the format of the cell. For example, you can define a cell as a date with the format "dd/mm/yyyy" or a number with the format "#,##0.00". 60 | 61 | ### 3.8.1 62 | 63 | * Activate XLSX compression by default. The example of index.bigtable.html went from 18Mb to 3Mb. 64 | * _Update npm dependencies to fix vulnerabilities_ 65 | * Update to latest version of TypeScript 66 | * Reduced size of the library from 912 KB to 277 KB!!! 67 | 68 | ### 3.8.0 69 | 70 | * Allow RTL options on the whole file or sheet. 71 | * _Update npm dependencies to fix vulnerabilities_ 72 | 73 | ### 3.7.3 74 | 75 | * Fix (#591) remove columns parameter. Now it is not affected by repeated column numbers nor its order. 76 | 77 | ### 3.7.2 78 | 79 | * _Update npm dependencies to fix vulnerabilities_ 80 | 81 | ### 3.7.1 82 | 83 | * _Update npm dependencies to fix vulnerabilities_ 84 | * Start using Dependabot and get rid of Dependabot-preview 85 | 86 | ### 3.7.0 87 | 88 | * Added option `openAsDownload: boolean`. Use this option to download as a file without using an anchor tag. So download can be triggered from a button. 89 | * _Update npm dependencies to fix vulnerabilities_ 90 | 91 | ### 3.6.0 92 | 93 | * Added sponsor link to the project [:heart: Sponsor ExcellentExport.js project:heart:](https://github.com/sponsors/jmaister) 94 | * Transform the project from JavaScript to TypeScript 95 | * Configure Jest as test runner 96 | * _Update npm dependencies to fix vulnerabilities_ 97 | 98 | ### 3.5.0 99 | 100 | * Add fixValue and fixArray functions to configuration: these configuration functions can be used to manipulate the values of the cells. 101 | * _Update npm dependencies to fix vulnerabilities_ 102 | 103 | ### 3.4.3 104 | 105 | * _Update npm dependencies to fix vulnerabilities_ 106 | 107 | ### 3.4.2 108 | 109 | * Remove ES6 function syntax to support IE11 110 | * _Update npm dependencies to fix vulnerabilities_ 111 | 112 | ### 3.4.0 113 | 114 | * Configure TravisCI on GitHub 115 | * Update npm dependencies to fix vulnerabilities 116 | 117 | ### 3.3.0 118 | 119 | * Remove columns by index 120 | * Filter rows by value 121 | * _Updated build to Webpack 4.x.x_ 122 | 123 | ### 3.2.1 124 | 125 | * _Update npm dependencies to fix vulnerabilities_ 126 | 127 | ### 3.2.0 128 | 129 | * _Update npm dependencies to fix vulnerabilities_ 130 | 131 | ### 3.1.0 132 | 133 | * Fix old API for base64 and escaping problem. 134 | 135 | ### 3.0.0 136 | 137 | * XLSX support. This bumps the build size to 640 KB. 138 | * New API : ExcellentExport.convert(...) 139 | * Autogenerate download filename. 140 | * Data input from arrays or HTML Tables. 141 | * Multiple sheets for XLS or XLSX formats. 142 | 143 | ### 2.1.0 144 | 145 | * Add Webpack build. 146 | * Create UMD JavaScript module. Library can be loaded as a module (import, RequireJS, AMD, etc...) or standalone as window.ExcellentExport. 147 | 148 | ### 2.0.3 149 | 150 | * Fix export as a module. 151 | * Changed minifier to UglifyJS. 152 | 153 | ### 2.0.2 154 | 155 | * Fix CSV Chinese characters and other special characters display error in Windows Excel. 156 | * Fix URL.createObjectURL(...) on Firefox. 157 | 158 | 159 | ### 2.0.0 160 | 161 | * Now it can export to big files +2MB. 162 | * Minimum IE 11. 163 | * Links open with URL.createObjectURL(...). 164 | * NPM package available. 165 | * Using Semantic versioning (2.0.0 instead of 2.0). 166 | * Module can be loaded standalone or with RequireJS. 167 | * Change license to MIT. 168 | 169 | ### 1.5 170 | 171 | * Possibility to select a CSV delimiter. 172 | * Bower package available. 173 | * Compose package available. 174 | 175 | ### 1.4 176 | 177 | * _Add LICENSE.txt with GPL v3_ 178 | * UTF-8 characters fixed. 179 | 180 | ### 1.3 181 | 182 | * _Added minified version_ 183 | 184 | ### 1.1 185 | 186 | * _Added CSV data export_ 187 | 188 | ### 1.0 189 | 190 | * _Added Excel data export_ 191 | 192 | ## Compatibility 193 | 194 | - Firefox 195 | - Chrome 196 | - Internet Explorer 11+ 197 | 198 | # Install 199 | 200 | ## npm 201 | 202 | npm install excellentexport --save 203 | 204 | ## yarn 205 | 206 | yarn add excellentexport 207 | 208 | ## bower 209 | 210 | bower install excellentexport 211 | 212 | # Load 213 | 214 | 215 | **Include script in your HTML:** 216 | 217 | 218 | 219 | **Include script in your HTML using CDN:** 220 | 221 | 222 | 223 | 224 | **Require.js** 225 | 226 | 227 | 232 | 233 | **ES6 import** 234 | 235 | import ExcellentExport from 'excellentexport'; 236 | 237 | 238 | # Usage 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 |
100 200 300
400 500 600
248 | 249 | Export to Excel 250 | Export to CSV 251 | 252 | Export to CSV 253 | 254 | # API 255 | 256 | ExcellentExport.convert(options, sheets); 257 | 258 | Options: 259 | { 260 | anchor: String or HTML Element, 261 | format: 'xlsx' or 'xls' or 'csv', 262 | filename: String, 263 | rtl: Use Right-to-left characters, boolean (optional) 264 | } 265 | 266 | Sheets must be an array of sheet configuration objects. Sheet description: 267 | [ 268 | { 269 | name: 'Sheet 1', // Sheet name 270 | from: { 271 | table: String/Element, // Table ID or table element 272 | array: [...] // Array with the data. Array where each element is a row. Every row is an array of the cells. 273 | }, 274 | removeColumns: [...], // Array of column indexes (from 0) 275 | filterRowFn: function(row) {return true}, // Function to decide which rows are returned 276 | fixValue: function(value, row, column) {return fixedValue} // Function to fix values, receiving value, row num, column num 277 | fixArray: function(array) {return array} // Function to manipulate the whole data array 278 | rtl: Use Right-to-left characters, boolean (optional) 279 | formats: [...] // Array of formats for each column. See formats below. 280 | ... 281 | }, 282 | { 283 | ... 284 | }, ... 285 | ] 286 | 287 | ## fixValue example 288 | 289 | This is an example for the _fixValue function_ to handle HTML tags inside a table cell. 290 | It transforms BR to line breaks and then strips all the HTML tags. 291 | 292 | fixValue: (value, row, col) => { 293 | let v = value.replace(/
/gi, "\n"); 294 | let strippedString = v.replace(/(<([^>]+)>)/gi, ""); 295 | return strippedString; 296 | } 297 | 298 | ## Formats 299 | 300 | You can specify an array with the formats for a specific cell range (i.e. A1:A100, A1:D100, A1:H1, etc). 301 | 302 | Each element in the format array consists on: 303 | 304 | ```typescript 305 | const sheet01 = { 306 | "range": "A1:A100", // Range of cells to apply the format, mandatory 307 | "format": { 308 | "type": "", // Type of format, mandatory 309 | "pattern": "" // Pattern, optional 310 | } 311 | } 312 | ``` 313 | 314 | Example: 315 | 316 | ```typescript 317 | formats: [ 318 | { 319 | range: "C2:C20", 320 | format: { 321 | type: "n", 322 | pattern: "0.00", 323 | }, 324 | }, 325 | { 326 | range: "C2:C20", 327 | format: ExcellentExport.formats.NUMBER, 328 | } 329 | ] 330 | ``` 331 | 332 | `format` can be used from one of the predefined types if you use TypeScript 333 | 334 | 335 | `cell_type` can be one of the followint: 336 | 337 | 's': String 338 | 'n': Number 339 | 'd': Date 340 | 'b': Boolean 341 | 342 | `pattern` is a string with the format pattern used in Excel. For example: 343 | 344 | '0' // Integer 345 | '0.00' // 2 decimals 346 | 'dd/mm/yyyy' // Date 347 | 'dd/mm/yyyy hh:mm:ss' // Date and time 348 | '0.00%' // Percentage 349 | '0.00e+00' // Scientific notation 350 | '@' // Text 351 | 352 | # Notes 353 | 354 | - IE8 or lower do not support *data:* url schema. 355 | - IE9 does not support *data:* url schema on links. 356 | - IE10 and above and Edge are supported via the Microsoft-specific `msOpenOrSaveBlob` method. 357 | 358 | # Test 359 | 360 | python 2.x: 361 | python -m SimpleHTTPServer 8000 362 | 363 | python 3.x: 364 | python -m http.server 8000 365 | 366 | # Build 367 | 368 | **Install dependencies:** 369 | 370 | npm install 371 | 372 | **Build development version dist/excellentexport.js** 373 | 374 | npm run build 375 | 376 | **Build publish version of dist/excellentexport.js** 377 | 378 | npm run prod 379 | 380 | **Publish** 381 | 382 | npm publish 383 | 384 | ## Dependencies 385 | 386 | - XLSX is not available from NPM anymore. Use https://cdn.sheetjs.com/ to install it. 387 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "excellentexport", 3 | "main": "excellentexport.js", 4 | "homepage": "http://jordiburgos.com/post/2014/excellentexport-javascript-export-to-excel-csv.html", 5 | "authors": [ 6 | "Jordi Burgos " 7 | ], 8 | "description": "Client side JavaScript export to Excel or CSV", 9 | "moduleType": [ 10 | "globals" 11 | ], 12 | "keywords": [ 13 | "excel", 14 | "export", 15 | "csv", 16 | "javascript", 17 | "client", 18 | "side" 19 | ], 20 | "license": "MIT", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "git://github.com/jmaister/excellentexport.git" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dist/excellentexport.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ExcellentExport 3.9.9 3 | * A client side Javascript export to Excel. 4 | * 5 | * @author: Jordi Burgos (jordiburgos@gmail.com) 6 | * @url: https://github.com/jmaister/excellentexport 7 | * 8 | */ 9 | import { CellTypes, FormatDefinition, CellFormats, CellPatterns } from './format'; 10 | declare global { 11 | interface Navigator { 12 | msSaveBlob?: (blob: any, defaultName?: string) => boolean; 13 | } 14 | } 15 | export interface ConvertOptions { 16 | anchor?: (string | HTMLAnchorElement); 17 | openAsDownload?: boolean; 18 | format: ('csv' | 'xls' | 'xlsx'); 19 | filename?: string; 20 | rtl?: boolean; 21 | } 22 | export interface FromOptions { 23 | table?: (string | HTMLTableElement); 24 | array?: any[][]; 25 | } 26 | export interface SheetOptions { 27 | name: string; 28 | from: FromOptions; 29 | removeColumns?: number[]; 30 | filterRowFn?(row: any[]): boolean; 31 | fixValue?(value: any, row: number, column: number): any; 32 | fixArray?(array: any[][]): any[][]; 33 | rtl?: boolean; 34 | formats?: (FormatDefinition | null)[]; 35 | } 36 | declare const ExcellentExport: { 37 | version: () => string; 38 | excel: (anchor: (HTMLAnchorElement | string), table: HTMLTableElement, name: string) => boolean; 39 | csv: (anchor: (HTMLAnchorElement | string), table: HTMLTableElement, delimiter?: string, newLine?: string) => boolean; 40 | convert: (options: ConvertOptions, sheets: SheetOptions[]) => string | false; 41 | formats: CellFormats; 42 | cellTypes: typeof CellTypes; 43 | cellPatterns: typeof CellPatterns; 44 | }; 45 | export default ExcellentExport; 46 | -------------------------------------------------------------------------------- /dist/excellentexport.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! sheetjs (C) 2013-present SheetJS -- http://sheetjs.com */ 2 | 3 | /*! xlsx.js (C) 2013-present SheetJS -- http://sheetjs.com */ 4 | -------------------------------------------------------------------------------- /dist/format.d.ts: -------------------------------------------------------------------------------- 1 | export declare enum CellTypes { 2 | TEXT = "s", 3 | NUMBER = "n", 4 | DATE = "d", 5 | BOOLEAN = "b" 6 | } 7 | export declare enum CellPatterns { 8 | INTEGER = "0", 9 | DECIMAL = "0.00", 10 | DATE = "dd/mm/yyyy", 11 | TIME = "hh:mm:ss", 12 | DATETIME = "dd/mm/yyyy hh:mm:ss", 13 | CURRENCY = "[$$-409]#,##0.00;[RED]-[$$-409]#,##0.00", 14 | PERCENTAGE = "0.00%", 15 | EXPONENT = "0.00E+00", 16 | TEXT = "@" 17 | } 18 | export type CellType = 's' | 'n' | 'd' | 'b'; 19 | export interface CellFormat { 20 | type: CellType; 21 | pattern?: string; 22 | } 23 | export interface CellFormats { 24 | [key: string]: CellFormat; 25 | } 26 | export declare const PredefinedFormat: CellFormats; 27 | export interface FormatDefinition { 28 | range: string; 29 | format?: CellFormat; 30 | } 31 | -------------------------------------------------------------------------------- /dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare const b64toBlob: (b64Data: string, contentType: string, sliceSize?: number) => Blob; 2 | export declare const templates: { 3 | excel: string; 4 | }; 5 | /** 6 | * Convert a string to Base64. 7 | */ 8 | export declare const base64: (s: string) => string; 9 | export declare const format: (s: string, context: any) => string; 10 | /** 11 | * Get element by ID. 12 | * @param {*} element 13 | */ 14 | export declare const getTable: (element: (HTMLTableElement | string)) => HTMLTableElement; 15 | /** 16 | * Get element by ID. 17 | * @param {*} element 18 | */ 19 | export declare const getAnchor: (element: (HTMLAnchorElement | string)) => HTMLAnchorElement; 20 | /** 21 | * Encode a value for CSV. 22 | * @param {*} value 23 | */ 24 | export declare const fixCSVField: (value: string, csvDelimiter: string) => string; 25 | export declare const tableToArray: (table: HTMLTableElement) => any[][]; 26 | export declare const tableToCSV: (table: HTMLTableElement, csvDelimiter?: string, csvNewLine?: string) => string; 27 | export declare const createDownloadLink: (anchor: HTMLAnchorElement, base64data: string, exporttype: string, filename: string) => boolean; 28 | export declare const string2ArrayBuffer: (s: string) => ArrayBuffer; 29 | export declare const removeColumns: (dataArray: any[][], columnIndexes: number[]) => void; 30 | export declare const hasContent: (value: any) => boolean; 31 | -------------------------------------------------------------------------------- /index.bigtable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Export to excel test 5 | 6 | 11 | 25 | 26 | 27 |

ExcellentExport.js

28 | 29 | Check on jordiburgos.com and GitHub. 30 | 31 |

Big Table Test page

32 | 33 | 34 | Export to XLSX very big table
35 | Export to XLS very big table
36 | Export to CSV very big table 37 | 38 |
39 |
40 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /index.filters.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Export to excel test 5 | 6 | 11 | 30 | 31 | 32 |

ExcellentExport.js

33 | 34 | Check on jordiburgos.com and GitHub. 35 | 36 |

Test page

37 | 38 | Test table: 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
Column 1Column 2Column 3Column 4
hello
world
text in spanabcdef
53 | 54 |
55 | 56 | Export to XLSX from array 57 |
58 | Export to XLS from array 59 |
60 | Export to CSV from array 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /index.format.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Export to excel test 6 | 7 | 12 | 36 | 37 | 38 |

ExcellentExport.js

39 | 40 |

Test page

41 | 42 | Test table: 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
IDNameBirthdateSalaryActiveBig number
1John1980-12-1098762000.551987654321987654
2Peter1978-01-2398762500.430876543219987654
3George1985-11-3098761800.981765432198987654
EndEndEndEndEndEnd
9876543.21
93 | 94 |
95 | 96 | Export Excel 97 | Export CSV 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Export to excel test 5 | 6 | 11 | 61 | 62 | 63 |

ExcellentExport.js

64 | 65 | Check on jordiburgos.com and GitHub. 66 | 67 |

Test page

68 | 69 | Test table: 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 95 | 96 |
Column 1Column "cool" 2Column 3Column 4
100,111200030áéíóú
400500Chinese chars: 解决导出csv中文乱码问题àèìòù
TextMore textText with 93 | new lineç ñ ÄËÏÖÜ äëïöü
97 | 98 |
99 | 100 | Export to Excel 101 |
102 | 103 | Export to CSV - UTF8 104 |
105 | Export to CSV - Using semicolon ";" separator - UTF8 106 |
107 |

NEW API!

108 | Export to Excel: XLS format 109 |
110 | Export to Excel: XLSX format 111 |
112 | Export to CSV 113 |
114 | Export to Excel: XLSX filtering columns and rows 115 |
116 |
117 | 118 |

NEW API from Arrays!

119 | 120 | Export to XLSX from array 121 |
122 | Export to XLS from array 123 |
124 | Export to CSV from array 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /index.noanchor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Export to excel test 5 | 6 | 11 | 21 | 22 | 23 |

ExcellentExport.js

24 | 25 | Check on jordiburgos.com and GitHub. 26 | 27 |

Test without anchor tab

28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /index.require.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Export to excel test 5 | 6 | 11 | 16 | 17 | 18 |

ExcellentExport.js

19 | 20 | Check on jordiburgos.com and GitHub. 21 | 22 |

Test page

23 | 24 |
25 | 26 | Export to Excel 27 |
28 | 29 | Export to CSV - UTF8 30 |
31 | Export to CSV - Using semicolon ";" separator - UTF8 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 |
Column 1Column "cool" 2Column 3Column 4
100,111200300áéíóú
400500Chinese chars: 解决导出csv中文乱码问题àèìòù
TextMore textText with 57 | new lineç ñ ÄËÏÖÜ äëïöü
61 | 62 | 63 | -------------------------------------------------------------------------------- /index.rtl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Export to excel test 5 | 6 | 11 | 26 | 27 | 28 |

ExcellentExport.js RTL text

29 | 30 | Check on jordiburgos.com and GitHub. 31 | 32 |

Test page

33 | 34 | Test table 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
االلغةعدد الأحرفCountrymiص-ص
العربية٢٨Earthن0ن
العبرية٢٢IsrealضSض
59 | 60 |
61 | 62 | Export to Excel: XLS format 63 |
64 | Export to Excel: XLSX format 65 |
66 | Export to CSV 67 | 68 | 69 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/tmp/jest_rs", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: "coverage", 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: "v8", 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | preset: 'jest-puppeteer', 98 | 99 | // Run tests from one or more projects 100 | // projects: undefined, 101 | 102 | // Use this configuration option to add custom reporters to Jest 103 | // reporters: undefined, 104 | 105 | // Automatically reset mock state between every test 106 | // resetMocks: false, 107 | 108 | // Reset the module registry before running each individual test 109 | // resetModules: false, 110 | 111 | // A path to a custom resolver 112 | // resolver: undefined, 113 | 114 | // Automatically restore mock state between every test 115 | // restoreMocks: false, 116 | 117 | // The root directory that Jest should scan for tests and modules within 118 | // rootDir: undefined, 119 | 120 | // A list of paths to directories that Jest should use to search for files in 121 | // roots: [ 122 | // "" 123 | // ], 124 | roots: [ 125 | "src", 126 | "test" 127 | ], 128 | 129 | // Allows you to use a custom runner instead of Jest's default test runner 130 | // runner: "jest-runner", 131 | 132 | // The paths to modules that run some code to configure or set up the testing environment before each test 133 | // setupFiles: [], 134 | 135 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 136 | // setupFilesAfterEnv: [], 137 | 138 | // The number of seconds after which a test is considered as slow and reported as such in the results. 139 | // slowTestThreshold: 5, 140 | 141 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 142 | // snapshotSerializers: [], 143 | 144 | // The test environment that will be used for testing 145 | // testEnvironment: "jest-environment-jsdom", 146 | testEnvironment: "jsdom", 147 | 148 | // Options that will be passed to the testEnvironment 149 | // testEnvironmentOptions: {}, 150 | 151 | // Adds a location field to test results 152 | // testLocationInResults: false, 153 | 154 | // The glob patterns Jest uses to detect test files 155 | // testMatch: [ 156 | // "**/__tests__/**/*.[jt]s?(x)", 157 | // "**/?(*.)+(spec|test).[tj]s?(x)" 158 | // ], 159 | 160 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 161 | // testPathIgnorePatterns: [ 162 | // "/node_modules/" 163 | // ], 164 | 165 | // The regexp pattern or array of patterns that Jest uses to detect test files 166 | // testRegex: [], 167 | 168 | // This option allows the use of a custom results processor 169 | // testResultsProcessor: undefined, 170 | 171 | // This option allows use of a custom test runner 172 | // testRunner: "jasmine2", 173 | 174 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 175 | // testURL: "http://localhost", 176 | 177 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 178 | // timers: "real", 179 | 180 | // A map from regular expressions to paths to transformers 181 | // transform: undefined, 182 | transform: { 183 | "^.+\\.ts?$": "ts-jest" 184 | }, 185 | 186 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 187 | // transformIgnorePatterns: [ 188 | // "/node_modules/", 189 | // "\\.pnp\\.[^\\/]+$" 190 | // ], 191 | 192 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 193 | // unmockedModulePathPatterns: undefined, 194 | 195 | // Indicates whether each individual test should be reported during the run 196 | // verbose: undefined, 197 | 198 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 199 | // watchPathIgnorePatterns: [], 200 | 201 | // Whether to use watchman for file crawling 202 | // watchman: true, 203 | }; 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "excellentexport", 3 | "version": "3.9.9", 4 | "description": "Client side JavaScript export to Excel or CSV", 5 | "license": "MIT", 6 | "homepage": "https://jordiburgos.com", 7 | "author": "Jordi Burgos ", 8 | "bugs": "https://github.com/jmaister/excellentexport/issues", 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/jmaister/excellentexport.git" 12 | }, 13 | "keywords": [ 14 | "excel", 15 | "export", 16 | "csv", 17 | "javascript", 18 | "client", 19 | "side" 20 | ], 21 | "scripts": { 22 | "build": "webpack --config webpack.config.js --progress --mode development --watch", 23 | "prod": "webpack --config webpack.config.js --progress --mode production", 24 | "test": "jest --coverage", 25 | "watch": "jest --watch", 26 | "postinstall": "node scripts/postinstall.js" 27 | }, 28 | "main": "dist/excellentexport.js", 29 | "devDependencies": { 30 | "@types/jest": "29.5.14", 31 | "@types/jest-environment-puppeteer": "5.0.6", 32 | "@types/node": "22.12.0", 33 | "jest": "29.7.0", 34 | "jest-environment-jsdom": "29.7.0", 35 | "jest-puppeteer": "11.0.0", 36 | "puppeteer": "24.1.1", 37 | "ts-jest": "29.2.5", 38 | "ts-loader": "9.5.2", 39 | "typescript": "5.7.3", 40 | "webpack": "5.97.1", 41 | "webpack-cli": "6.0.1", 42 | "webpack-dev-server": "5.2.0", 43 | "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/postinstall.js: -------------------------------------------------------------------------------- 1 | const color = '\x1b[33m%s\x1b[0m'; 2 | console.log(color, "Thank you for installing ExcellentExport!"); 3 | console.log(color, "If you like ExcellentExport, please consider a donation: https://github.com/sponsors/jmaister"); 4 | -------------------------------------------------------------------------------- /src/excellentexport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ExcellentExport 3.9.9 3 | * A client side Javascript export to Excel. 4 | * 5 | * @author: Jordi Burgos (jordiburgos@gmail.com) 6 | * @url: https://github.com/jmaister/excellentexport 7 | * 8 | */ 9 | 10 | import * as XLSX from 'xlsx'; 11 | import { CellTypes, FormatDefinition, PredefinedFormat, CellFormats, CellPatterns } from './format'; 12 | 13 | import * as utils from './utils'; 14 | 15 | // Fix for IE11: https://stackoverflow.com/questions/69485778/new-typescript-version-does-not-include-window-navigator-mssaveblob 16 | declare global { 17 | interface Navigator { 18 | msSaveBlob?: (blob: any, defaultName?: string) => boolean 19 | } 20 | } 21 | 22 | export interface ConvertOptions { 23 | anchor?: (string|HTMLAnchorElement), 24 | openAsDownload?: boolean, 25 | format: ('csv' | 'xls' | 'xlsx'), 26 | filename?: string, 27 | rtl?: boolean, 28 | } 29 | export interface FromOptions { 30 | table?: (string|HTMLTableElement), 31 | array?: any[][], 32 | } 33 | 34 | export interface SheetOptions { 35 | name: string, 36 | from: FromOptions, 37 | removeColumns?: number[], 38 | filterRowFn?(row:any[]): boolean , 39 | fixValue?(value:any, row:number, column:number): any, 40 | fixArray?(array:any[][]): any[][], 41 | rtl?: boolean, 42 | formats?: (FormatDefinition | null)[], 43 | } 44 | 45 | /* 46 | export type ExcellentExportType = { 47 | version: () => string, 48 | formats: CellFormats, 49 | excel: (anchor:(HTMLAnchorElement|string), table:HTMLTableElement, name:string) => void, 50 | csv: (anchor:(HTMLAnchorElement|string), table:HTMLTableElement, delimiter?:string, newLine?:string) => void, 51 | convert: (options:ConvertOptions, sheets:SheetOptions[]) => void, 52 | } 53 | */ 54 | 55 | const ExcellentExport = function() { 56 | 57 | const version = "3.9.9"; 58 | 59 | /* 60 | ExcellentExport.convert(options, sheets); 61 | 62 | Options: 63 | { 64 | anchor: String or HTML Element, 65 | openAsDownload: boolean, // Use this options if not using an anchor tag 66 | format: 'xlsx' or 'xls' or 'csv', 67 | filename: String, 68 | rtl: boolean (optional), specify if all the workbook has text in RTL mode 69 | } 70 | 71 | Sheets must be an array of sheet configuration objects. Sheet description: 72 | [ 73 | { 74 | name: 'Sheet 1', // Sheet name 75 | from: { 76 | table: String/Element, // Table ID or table element 77 | array: [...] // Array with the data. Array where each element is a row. Every row is an array of the cells. 78 | }, 79 | removeColumns: [...], // Array of column indexes (from 0) 80 | filterRowFn: function(row) {return true}, // Function to decide which rows are returned 81 | fixValue: function(value, row, column) {return fixedValue} // Function to fix values, receiving value, row num, column num 82 | fixArray: function(array) {return array} // Function to manipulate the whole data array 83 | rtl: boolean // optional: specify if the sheet has text in RTL mode 84 | ... 85 | }, 86 | { 87 | ... 88 | }, ... 89 | ] 90 | */ 91 | const convert = function(options:ConvertOptions, sheets:SheetOptions[]) { 92 | const workbook = { 93 | SheetNames: [], 94 | Sheets: {}, 95 | Views: [] 96 | }; 97 | 98 | if (!options.format) { 99 | throw new Error("'format' option must be defined"); 100 | } 101 | if (options.format === 'csv' && sheets.length > 1) { 102 | throw new Error("'csv' format only supports one sheet"); 103 | } 104 | 105 | sheets.forEach(function(sheetConf:SheetOptions, index:number) { 106 | const name = sheetConf.name; 107 | if (!name) { 108 | throw new Error('Sheet ' + index + ' must have the property "name".'); 109 | } 110 | 111 | // Select data source 112 | let dataArray: any[][]; 113 | if (sheetConf.from && sheetConf.from.table) { 114 | dataArray = utils.tableToArray(utils.getTable(sheetConf.from.table)); 115 | } else if(sheetConf.from && sheetConf.from.array) { 116 | dataArray = sheetConf.from.array 117 | } else { 118 | throw new Error('No data for sheet: [' + name + ']'); 119 | } 120 | 121 | // Filter rows 122 | if (sheetConf.filterRowFn) { 123 | if (sheetConf.filterRowFn instanceof Function) { 124 | dataArray = dataArray.filter(sheetConf.filterRowFn); 125 | } else { 126 | throw new Error('Parameter "filterRowFn" must be a function.'); 127 | } 128 | } 129 | // Filter columns 130 | if (sheetConf.removeColumns) { 131 | utils.removeColumns(dataArray, sheetConf.removeColumns); 132 | } 133 | 134 | // Convert data. Function applied to each value independently, receiving (value, rownum, colnum) 135 | if (sheetConf.fixValue && typeof sheetConf.fixValue === 'function') { 136 | const fn = sheetConf.fixValue; 137 | dataArray.map((r, rownum) => { 138 | r.map((value, colnum) => { 139 | dataArray[rownum][colnum] = fn(value, rownum, colnum); 140 | }); 141 | }); 142 | } 143 | 144 | // Convert data, whole array 145 | if (sheetConf.fixArray && typeof sheetConf.fixArray === 'function') { 146 | const fn = sheetConf.fixArray; 147 | dataArray = fn(dataArray); 148 | } 149 | 150 | // Create sheet 151 | workbook.SheetNames.push(name); 152 | const worksheet = XLSX.utils.aoa_to_sheet(dataArray, {sheet: name} as XLSX.AOA2SheetOpts); 153 | 154 | // Apply format 155 | if (sheetConf.formats) { 156 | sheetConf.formats.forEach(f => { 157 | const range = XLSX.utils.decode_range(f.range); 158 | for (let R = range.s.r; R <= range.e.r; ++R) { 159 | for (let C = range.s.c; C <= range.e.c; ++C) { 160 | const cell = worksheet[XLSX.utils.encode_cell({r: R, c: C})]; 161 | if (cell && utils.hasContent(cell.v)) { 162 | // type 163 | cell.t = f.format.type; 164 | 165 | // type fix 166 | if (f.format?.type == CellTypes.BOOLEAN) { 167 | const v = cell.v.toString().toLowerCase(); 168 | if (v == 'true' || v == '1') cell.v = true; 169 | if (v == 'false' || v == '0') cell.v = false; 170 | } 171 | // pattern 172 | if (f.format?.pattern) { 173 | cell.z = f.format.pattern; 174 | } 175 | } 176 | } 177 | } 178 | }); 179 | } 180 | 181 | 182 | workbook.Sheets[name] = worksheet; 183 | workbook.Views.push({RTL: options.rtl || sheetConf.rtl || false}); 184 | }); 185 | 186 | const wbOut:string = XLSX.write(workbook, {bookType: options.format, bookSST:true, type: 'binary', compression: true}); 187 | try { 188 | const blob = new Blob([utils.string2ArrayBuffer(wbOut)], { type: "application/octet-stream" }); 189 | const filename = (options.filename || 'download') + '.' + options.format; 190 | // Support for IE. 191 | if (window.navigator.msSaveBlob) { 192 | window.navigator.msSaveBlob(blob, filename); 193 | return false; 194 | } 195 | if (options.anchor) { 196 | const anchor = utils.getAnchor(options.anchor); 197 | anchor.href = window.URL.createObjectURL(blob); 198 | anchor.download = filename; 199 | } else if (options.openAsDownload) { 200 | const a = document.createElement("a"); 201 | a.href = URL.createObjectURL(blob); 202 | a.download = filename; 203 | document.body.appendChild(a); 204 | a.click(); 205 | document.body.removeChild(a); 206 | } else { 207 | throw new Error('Options should specify an anchor or openAsDownload=true.') 208 | } 209 | 210 | } catch(e) { 211 | throw new Error('Error converting to '+ options.format + '. ' + e); 212 | } 213 | return wbOut; 214 | 215 | }; 216 | 217 | return { 218 | version: function(): string { 219 | return version; 220 | }, 221 | excel: function(anchor:(HTMLAnchorElement|string), table:HTMLTableElement, name:string) { 222 | table = utils.getTable(table); 223 | anchor = utils.getAnchor(anchor); 224 | const ctx = {worksheet: name || 'Worksheet', table: table.innerHTML}; 225 | const b64 = utils.base64(utils.format(utils.templates.excel, ctx)); 226 | return utils.createDownloadLink(anchor, b64, 'application/vnd.ms-excel','export.xls'); 227 | }, 228 | csv: function(anchor:(HTMLAnchorElement|string), table:HTMLTableElement, delimiter?:string, newLine?:string) { 229 | let csvDelimiter = ","; 230 | let csvNewLine = "\r\n"; 231 | 232 | if (delimiter !== undefined && delimiter) { 233 | csvDelimiter = delimiter; 234 | } 235 | if (newLine !== undefined && newLine) { 236 | csvNewLine = newLine; 237 | } 238 | 239 | table = utils.getTable(table); 240 | anchor = utils.getAnchor(anchor); 241 | const csvData = "\uFEFF" + utils.tableToCSV(table, csvDelimiter, csvNewLine); 242 | const b64 = utils.base64(csvData); 243 | return utils.createDownloadLink(anchor, b64, 'application/csv', 'export.csv'); 244 | }, 245 | convert: function(options:ConvertOptions, sheets:SheetOptions[]) { 246 | return convert(options, sheets); 247 | }, 248 | formats: PredefinedFormat, 249 | cellTypes: CellTypes, 250 | cellPatterns: CellPatterns, 251 | }; 252 | }(); 253 | 254 | export default ExcellentExport; 255 | -------------------------------------------------------------------------------- /src/format.ts: -------------------------------------------------------------------------------- 1 | 2 | // Constants for cell types 3 | export enum CellTypes { 4 | TEXT = 's', 5 | NUMBER = 'n', 6 | DATE = 'd', 7 | BOOLEAN = 'b', 8 | } 9 | 10 | // Constants for cell patterns 11 | export enum CellPatterns { 12 | INTEGER = '0', 13 | DECIMAL = '0.00', 14 | DATE = 'dd/mm/yyyy', 15 | TIME = 'hh:mm:ss', 16 | DATETIME = 'dd/mm/yyyy hh:mm:ss', 17 | CURRENCY = '[$$-409]#,##0.00;[RED]-[$$-409]#,##0.00', 18 | PERCENTAGE = '0.00%', 19 | EXPONENT = '0.00E+00', 20 | TEXT = '@', 21 | } 22 | 23 | export type CellType = 's' | 'n' | 'd' | 'b'; 24 | 25 | export interface CellFormat { 26 | type: CellType, 27 | pattern?: string, 28 | } 29 | 30 | // Define structure for predefined formats 31 | export interface CellFormats { 32 | [key: string]: CellFormat 33 | } 34 | export const PredefinedFormat : CellFormats = { 35 | NUMBER: { type: CellTypes.NUMBER}, 36 | INTEGER: { type: CellTypes.NUMBER, pattern: CellPatterns.INTEGER }, 37 | DECIMAL: { type: CellTypes.NUMBER, pattern: CellPatterns.DECIMAL }, 38 | CURRENCY: { type: CellTypes.NUMBER, pattern: CellPatterns.CURRENCY }, 39 | PERCENTAGE: { type: CellTypes.NUMBER, pattern: CellPatterns.PERCENTAGE }, 40 | EXPONENT: { type: CellTypes.NUMBER, pattern: CellPatterns.EXPONENT }, 41 | 42 | DATE: { type: CellTypes.DATE, pattern: CellPatterns.DATE }, 43 | 44 | TIME: { type: CellTypes.DATE, pattern: CellPatterns.TIME }, 45 | DATETIME: { type: CellTypes.DATE, pattern: CellPatterns.DATETIME }, 46 | 47 | TEXT: { type: CellTypes.TEXT, pattern: CellPatterns.TEXT }, 48 | 49 | BOOLEAN: { type: CellTypes.BOOLEAN }, 50 | } 51 | 52 | export interface FormatDefinition { 53 | range: string, 54 | format?: CellFormat, 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export const b64toBlob = function (b64Data:string, contentType:string, sliceSize?:number): Blob { 3 | // function taken from http://stackoverflow.com/a/16245768/2591950 4 | // author Jeremy Banks http://stackoverflow.com/users/1114/jeremy-banks 5 | contentType = contentType || ''; 6 | sliceSize = sliceSize || 512; 7 | 8 | const byteCharacters = atob(b64Data); 9 | const byteArrays = []; 10 | 11 | for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { 12 | const slice = byteCharacters.slice(offset, offset + sliceSize); 13 | 14 | const byteNumbers = new Array(slice.length); 15 | for (let i = 0; i < slice.length; i = i + 1) { 16 | byteNumbers[i] = slice.charCodeAt(i); 17 | } 18 | 19 | const byteArray = new Uint8Array(byteNumbers); 20 | 21 | byteArrays.push(byteArray); 22 | } 23 | 24 | return new Blob(byteArrays, { 25 | type: contentType 26 | }); 27 | }; 28 | 29 | export const templates = {excel: ' {table}
'}; 30 | 31 | /** 32 | * Convert a string to Base64. 33 | */ 34 | export const base64 = function(s:string) : string { 35 | return btoa(unescape(encodeURIComponent(s))); 36 | }; 37 | 38 | export const format = function(s:string, context:any) : string { 39 | return s.replace(new RegExp("{(\\w+)}", "g"), function(m, p) { 40 | return context[p] || "{" + p + "}"; 41 | }); 42 | }; 43 | 44 | /** 45 | * Get element by ID. 46 | * @param {*} element 47 | */ 48 | export const getTable = function(element :(HTMLTableElement|string)) : HTMLTableElement { 49 | if (typeof element === 'string') { 50 | return document.getElementById(element) as HTMLTableElement; 51 | } 52 | return element; 53 | }; 54 | 55 | /** 56 | * Get element by ID. 57 | * @param {*} element 58 | */ 59 | export const getAnchor = function(element :(HTMLAnchorElement|string)) : HTMLAnchorElement { 60 | if (typeof element === 'string') { 61 | return document.getElementById(element) as HTMLAnchorElement; 62 | } 63 | return element; 64 | }; 65 | 66 | /** 67 | * Encode a value for CSV. 68 | * @param {*} value 69 | */ 70 | export const fixCSVField = function(value:string, csvDelimiter:string) : string { 71 | let fixedValue = value; 72 | const addQuotes = (value.indexOf(csvDelimiter) !== -1) || (value.indexOf('\r') !== -1) || (value.indexOf('\n') !== -1); 73 | const replaceDoubleQuotes = (value.indexOf('"') !== -1); 74 | 75 | if (replaceDoubleQuotes) { 76 | fixedValue = fixedValue.replace(/"/g, '""'); 77 | } 78 | if (addQuotes || replaceDoubleQuotes) { 79 | fixedValue = '"' + fixedValue + '"'; 80 | } 81 | 82 | return fixedValue; 83 | }; 84 | 85 | export const tableToArray = function(table:HTMLTableElement) : any[][] { 86 | let tableInfo = Array.prototype.map.call(table.querySelectorAll('tr'), function(tr) { 87 | return Array.prototype.map.call(tr.querySelectorAll('th,td'), function(td) { 88 | return td.innerHTML; 89 | }); 90 | }); 91 | return tableInfo; 92 | }; 93 | 94 | export const tableToCSV = function(table:HTMLTableElement, csvDelimiter:string = ',', csvNewLine:string = '\n') : string { 95 | let data = ""; 96 | for (let i = 0; i < table.rows.length; i=i+1) { 97 | const row = table.rows[i]; 98 | for (let j = 0; j < row.cells.length; j=j+1) { 99 | const col = row.cells[j]; 100 | data = data + (j ? csvDelimiter : '') + fixCSVField(col.textContent.trim(), csvDelimiter); 101 | } 102 | data = data + csvNewLine; 103 | } 104 | return data; 105 | }; 106 | 107 | export const createDownloadLink = function(anchor:HTMLAnchorElement, base64data:string, exporttype:string, filename:string) : boolean { 108 | if (window.navigator.msSaveBlob) { 109 | const blob = b64toBlob(base64data, exporttype); 110 | window.navigator.msSaveBlob(blob, filename); 111 | return false; 112 | } else if (window.URL.createObjectURL) { 113 | const blob = b64toBlob(base64data, exporttype); 114 | anchor.href = window.URL.createObjectURL(blob); 115 | } else { 116 | anchor.download = filename; 117 | anchor.href = "data:" + exporttype + ";base64," + base64data; 118 | } 119 | 120 | // Return true to allow the link to work 121 | return true; 122 | }; 123 | 124 | // String to ArrayBuffer 125 | export const string2ArrayBuffer = function (s:string): ArrayBuffer { 126 | let buf = new ArrayBuffer(s.length); 127 | let view = new Uint8Array(buf); 128 | for (let i=0; i !== s.length; ++i) { 129 | view[i] = s.charCodeAt(i) & 0xFF; 130 | } 131 | return buf; 132 | }; 133 | 134 | export const removeColumns = function(dataArray:any[][], columnIndexes:number[]) { 135 | const uniqueIndexes = [...new Set(columnIndexes)].sort().reverse(); 136 | uniqueIndexes.forEach(function(columnIndex) { 137 | dataArray.forEach(function(row) { 138 | row.splice(columnIndex, 1); 139 | }); 140 | }); 141 | }; 142 | 143 | export const hasContent = function(value:any) : boolean { 144 | return value !== undefined && value !== null && value !== ""; 145 | } -------------------------------------------------------------------------------- /test/checkversion.test.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | import ExcellentExport from '../src/excellentexport'; 4 | const pkg = require('../package.json'); 5 | 6 | describe('version() API', function() { 7 | describe('get version', function() { 8 | it('should get the current version number', function() { 9 | const version = ExcellentExport.version(); 10 | 11 | assert.ok(version, 'Version must be returned'); 12 | assert.equal(pkg.version, version, "Version must be " + pkg.version); 13 | }); 14 | }); 15 | }); 16 | 17 | -------------------------------------------------------------------------------- /test/convert-filters.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import 'expect-puppeteer'; 3 | 4 | import ExcellentExport, { ConvertOptions, SheetOptions } from '../src/excellentexport'; 5 | 6 | 7 | describe('convert() API', function() { 8 | describe('test sheet options', function() { 9 | 10 | beforeEach(() => { 11 | window.URL.createObjectURL = () => "blob:fake_URL"; 12 | 13 | document.body.innerHTML = ''; 14 | const element = document.createElement("div"); 15 | element.innerHTML = 16 | '
firstsecond
1234123.56
' + 17 | 'Link'; 18 | 19 | document.body.appendChild(element); 20 | }); 21 | 22 | test('filterRowFn', function() { 23 | const options = { 24 | anchor: 'anchor', 25 | filename: 'data_from_table', 26 | format: 'xlsx' 27 | } as ConvertOptions; 28 | 29 | const sheets = [ 30 | { 31 | name: 'Sheet Name Here 1', 32 | from: { 33 | table: 'sometable' 34 | }, 35 | filterRowFn: (row) => { 36 | if (row[0] === 'first') { 37 | return true; 38 | } 39 | } 40 | } 41 | ] as SheetOptions[]; 42 | 43 | const workbook = ExcellentExport.convert(options, sheets); 44 | expect(workbook).not.toBeNull(); 45 | }); 46 | 47 | test('removeColumns', function() { 48 | const options = { 49 | anchor: 'anchor', 50 | filename: 'data_from_table', 51 | format: 'xlsx' 52 | } as ConvertOptions; 53 | 54 | const sheets = [ 55 | { 56 | name: 'Sheet Name Here 1', 57 | from: { 58 | table: 'sometable' 59 | }, 60 | removeColumns: [1] 61 | } 62 | ] as SheetOptions[]; 63 | 64 | const workbook = ExcellentExport.convert(options, sheets); 65 | expect(workbook).not.toBeNull(); 66 | }); 67 | 68 | }); 69 | }); 70 | 71 | -------------------------------------------------------------------------------- /test/convert-table.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import 'expect-puppeteer'; 3 | 4 | import ExcellentExport, { ConvertOptions, SheetOptions } from '../src/excellentexport'; 5 | 6 | 7 | describe('convert() API', function() { 8 | describe('convert from HTML table', function() { 9 | 10 | beforeEach(() => { 11 | window.URL.createObjectURL = () => "blob:fake_URL"; 12 | 13 | document.body.innerHTML = ''; 14 | const element = document.createElement("div"); 15 | element.innerHTML = 16 | '
firstsecond
1234123.56
' + 17 | 'Link'; 18 | 19 | document.body.appendChild(element); 20 | }); 21 | 22 | test('should create a XLSX from HTML table by #id', function() { 23 | const options = { 24 | anchor: 'anchor', 25 | filename: 'data_from_table', 26 | format: 'xlsx' 27 | } as ConvertOptions; 28 | 29 | const sheets = [ 30 | { 31 | name: 'Sheet Name Here 1', 32 | from: { 33 | table: 'sometable' 34 | } 35 | } 36 | ] as SheetOptions[]; 37 | 38 | const workbook = ExcellentExport.convert(options, sheets); 39 | expect(workbook).not.toBeNull(); 40 | 41 | const anchor = document.getElementById('anchor') as HTMLAnchorElement; 42 | expect(anchor.href).not.toBeNull(); 43 | expect(anchor.href).toMatch(/blob:/); 44 | }); 45 | 46 | test('should create a XLSX from HTML table by DOM element', function() { 47 | const options = { 48 | anchor: document.getElementById('anchor'), 49 | filename: 'data_from_table', 50 | format: 'xlsx' 51 | } as ConvertOptions; 52 | 53 | const sheets = [ 54 | { 55 | name: 'Sheet Name Here 1', 56 | from: { 57 | table: document.getElementById('sometable') 58 | } 59 | } 60 | ] as SheetOptions[]; 61 | 62 | const workbook = ExcellentExport.convert(options, sheets); 63 | expect(workbook).not.toBeNull(); 64 | 65 | const anchor = document.getElementById('anchor') as HTMLAnchorElement; 66 | expect(anchor.href).not.toBeNull(); 67 | expect(anchor.href).toMatch(/blob:/); 68 | }); 69 | }); 70 | }); 71 | 72 | -------------------------------------------------------------------------------- /test/convert.format.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | import ExcellentExport, { ConvertOptions, SheetOptions } from '../src/excellentexport'; 4 | import { PredefinedFormat } from '../src/format'; 5 | 6 | 7 | describe('convert() API with column formats', function() { 8 | 9 | beforeEach(() => { 10 | window.URL.createObjectURL = () => "blob:fake_URL"; 11 | 12 | document.body.innerHTML = ''; 13 | const element = document.createElement("div"); 14 | element.innerHTML = 'Link'; 15 | 16 | document.body.appendChild(element); 17 | }); 18 | 19 | it('should create a XLSX with types', function() { 20 | const options = { 21 | anchor: 'anchor', 22 | filename: 'data_from_array', 23 | format: 'xlsx' 24 | } as ConvertOptions; 25 | 26 | const sheets = [ 27 | { 28 | name: 'People', 29 | from: { 30 | array: [ 31 | ["ID", "Name", "Birthdate", "Active", "Salary"], 32 | [11, "John", "1980-01-01", true, 1000.98], 33 | [22, "Mary", "1985-02-02", false, 2000.88], 34 | [33, "Peter", "1990-03-03", true, 3000.32], 35 | ] 36 | }, 37 | formats: [ 38 | { range: 'A2:A10', format: PredefinedFormat.INTEGER }, 39 | { range: 'C2:C10', format: PredefinedFormat.DATE }, 40 | { range: 'D2:D10', format: PredefinedFormat.BOOLEAN }, 41 | { range: 'E2:E10', format: PredefinedFormat.DECIMAL }, 42 | ] 43 | }, 44 | 45 | ] as SheetOptions[]; 46 | 47 | const workbook = ExcellentExport.convert(options, sheets); 48 | 49 | assert.ok(workbook, 'Result must not be null'); 50 | 51 | const anchor = document.getElementById('anchor') as HTMLAnchorElement; 52 | assert.ok(anchor.href, 'Element must have href'); 53 | assert.ok(anchor.href.indexOf('blob:') === 0, 'Element href myst be a blob:'); 54 | }); 55 | }); 56 | 57 | -------------------------------------------------------------------------------- /test/convert.test.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | import ExcellentExport, { ConvertOptions, SheetOptions } from '../src/excellentexport'; 4 | 5 | 6 | describe('convert() API', function() { 7 | describe('convert from array', function() { 8 | 9 | beforeEach(() => { 10 | window.URL.createObjectURL = () => "blob:fake_URL"; 11 | 12 | document.body.innerHTML = ''; 13 | const element = document.createElement("div"); 14 | element.innerHTML = 'Link'; 15 | 16 | document.body.appendChild(element); 17 | }); 18 | 19 | it('should create a XLSX from array', function() { 20 | const options = { 21 | anchor: 'anchor', 22 | filename: 'data_from_array', 23 | format: 'xlsx' 24 | } as ConvertOptions; 25 | 26 | const sheets = [ 27 | { 28 | name: 'Sheet Name Here 1', 29 | from: { 30 | array: [ 31 | [1, 2, 3], 32 | ['hello', '2200', 'bye'], 33 | ['quo"te', 'dobl"e qu"ote', 'singl\'e quote'] 34 | ] 35 | } 36 | }, 37 | { 38 | name: 'Sheet Number 2', 39 | from: { 40 | array: [ 41 | [6666, 7777, 8888], 42 | ['lorem', 'ipsum', 'dolor'] 43 | ] 44 | } 45 | }, 46 | 47 | ] as SheetOptions[]; 48 | 49 | const workbook = ExcellentExport.convert(options, sheets); 50 | 51 | assert.ok(workbook, 'Result must not be null'); 52 | 53 | const anchor = document.getElementById('anchor') as HTMLAnchorElement; 54 | assert.ok(anchor.href, 'Element must have href'); 55 | assert.ok(anchor.href.indexOf('blob:') === 0, 'Element href myst be a blob:'); 56 | }); 57 | }); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /test/fixdata.test.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | import ExcellentExport, { ConvertOptions } from '../src/excellentexport'; 4 | 5 | 6 | describe('Fix data', function() { 7 | beforeEach(() => { 8 | window.URL.createObjectURL = () => "blob:fake_URL"; 9 | 10 | document.body.innerHTML = ''; 11 | const element = document.createElement("div"); 12 | element.innerHTML = 'Link'; 13 | 14 | document.body.appendChild(element); 15 | }); 16 | 17 | it('should fix values', function() { 18 | const options = { 19 | anchor: 'anchor', 20 | filename: 'data_from_array', 21 | format: 'xlsx' 22 | } as ConvertOptions; 23 | 24 | const sheets = [ 25 | { 26 | name: 'Sheet Name Here 1', 27 | from: { 28 | array: [ 29 | ['hello', 'hello', 'bye'], 30 | ] 31 | }, 32 | fixValue: (value, row, col) => { 33 | let v = value.replace(/
/gi, "\n"); 34 | let strippedString = v.replace(/(<([^>]+)>)/gi, ""); 35 | return strippedString; 36 | } 37 | } 38 | ]; 39 | 40 | const workbook = ExcellentExport.convert(options, sheets); 41 | assert.ok(workbook, 'Result must not be null'); 42 | }); 43 | 44 | it('should process the whole array', function() { 45 | const options = { 46 | anchor: 'anchor', 47 | filename: 'data_from_array', 48 | format: 'xlsx' 49 | } as ConvertOptions; 50 | 51 | const sheets = [ 52 | { 53 | name: 'Sheet Name Here 1', 54 | from: { 55 | array: [ 56 | ['hello', 'hello', 'bye'], 57 | ] 58 | }, 59 | fixData: (array) => { 60 | return array.map(r => { 61 | return r.map(v => { 62 | return "fixed-" + v; 63 | }) 64 | }); 65 | } 66 | } 67 | ]; 68 | 69 | const workbook = ExcellentExport.convert(options, sheets); 70 | assert.ok(workbook, 'Result must not be null'); 71 | }); 72 | 73 | }); 74 | 75 | -------------------------------------------------------------------------------- /test/negative.test.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | import ExcellentExport, { ConvertOptions, SheetOptions } from '../src/excellentexport'; 4 | 5 | 6 | describe('convert() API', function() { 7 | describe('Negative tests', function() { 8 | 9 | beforeEach(() => { 10 | window.URL.createObjectURL = () => "blob:fake_URL"; 11 | 12 | const element = document.createElement("div"); 13 | element.innerHTML = 'Link'; 14 | 15 | document.body.appendChild(element); 16 | }); 17 | 18 | it('should fail if CSV has more than one sheet', function() { 19 | const options = { 20 | anchor: 'anchor', 21 | filename: 'data_from_array', 22 | format: 'csv' 23 | } as ConvertOptions; 24 | 25 | const sheets = [{ 26 | name: 'Sheet Name Here 1', 27 | from: {} 28 | }, { 29 | name: 'Sheet Number 2', 30 | from: {} 31 | }]; 32 | 33 | assert.throws(() => { 34 | ExcellentExport.convert(options, sheets) 35 | }, Error); 36 | 37 | }); 38 | 39 | it('should fail if sheet does not have name', function() { 40 | const options = { 41 | anchor: 'anchor', 42 | filename: 'data_from_array', 43 | format: 'csv' 44 | } as ConvertOptions; 45 | 46 | const sheets = [{ 47 | // name: 'Sheet Name Here 1', 48 | from: {} 49 | }] as SheetOptions[]; 50 | 51 | assert.throws(() => { 52 | ExcellentExport.convert(options, sheets) 53 | }, Error); 54 | 55 | }); 56 | 57 | it('should fail if sheet does not have data', function() { 58 | const options = { 59 | anchor: 'anchor', 60 | filename: 'data_from_array', 61 | format: 'csv' 62 | } as ConvertOptions; 63 | 64 | const sheets = [{ 65 | name: 'Sheet Name Here 1', 66 | // from: {} 67 | }] as SheetOptions[]; 68 | 69 | assert.throws(() => { 70 | ExcellentExport.convert(options, sheets) 71 | }, Error); 72 | 73 | }); 74 | 75 | it('should fail if there is not format defined', function() { 76 | const options = { 77 | anchor: 'anchor', 78 | filename: 'data_from_array', 79 | //format: 'csv' 80 | } as ConvertOptions; 81 | 82 | const sheets = [{ 83 | name: 'Sheet Name Here 1', 84 | from: {} 85 | }]; 86 | 87 | assert.throws(() => { 88 | ExcellentExport.convert(options, sheets) 89 | }, Error); 90 | 91 | }); 92 | 93 | it('should fail if anchor is not defined/valid', function() { 94 | const options = { 95 | anchor: 'anchor1235d5d5d5d_invalid', 96 | filename: 'data_from_array', 97 | format: 'csv' 98 | } as ConvertOptions; 99 | 100 | const sheets = [{ 101 | name: 'Sheet Name Here 1', 102 | from: {} 103 | }]; 104 | 105 | assert.throws(() => { 106 | ExcellentExport.convert(options, sheets) 107 | }, Error); 108 | 109 | }); 110 | 111 | it('should fail if no anchor and not openAsDownload', function() { 112 | const options = { 113 | filename: 'data_from_array', 114 | format: 'csv' 115 | } as ConvertOptions; 116 | 117 | const sheets = [{ 118 | name: 'Sheet Name Here 1', 119 | from: {} 120 | }]; 121 | 122 | assert.throws(() => { 123 | ExcellentExport.convert(options, sheets) 124 | }, Error); 125 | 126 | }); 127 | }); 128 | }); 129 | 130 | -------------------------------------------------------------------------------- /test/simple.test.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | describe('Array', function() { 4 | describe('#indexOf()', function() { 5 | it('should return -1 when the value is not present', function() { 6 | assert.equal(-1, [1,2,3].indexOf(4)); 7 | }); 8 | }); 9 | }); 10 | 11 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | import * as utils from '../src/utils'; 4 | 5 | 6 | describe('Test utility functions', () => { 7 | 8 | describe('base64', () => { 9 | it('should encode a string to base64', () => { 10 | assert.equal(utils.base64('test'), 'dGVzdA=='); 11 | }); 12 | 13 | it('should encode a unicode string to base64', () => { 14 | assert.equal(utils.base64('test\u00A9'), "dGVzdMKp"); 15 | }); 16 | 17 | }); 18 | 19 | 20 | describe('test format function', () => { 21 | it('should format a string', () => { 22 | assert.equal(utils.format('aaaa {a} bbbb {b} cccc', {a:'1', b:'2', c:'3', d:'4'}), 'aaaa 1 bbbb 2 cccc'); 23 | }); 24 | 25 | it('should not replace if no data is provided', () => { 26 | assert.equal(utils.format('aaaa {a} bbbb {b} cccc', {}), 'aaaa {a} bbbb {b} cccc'); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/utils_fixdata.test.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | import { fixCSVField } from '../src/utils'; 4 | 5 | 6 | describe('Test utility functions: csv functions', () => { 7 | 8 | it('should keep the value if not delimiter found', () => { 9 | assert.equal(fixCSVField('test', ','), 'test'); 10 | }); 11 | 12 | it('should fix a string with double quotes', () => { 13 | const str = 'aaa"bbb'; 14 | const result = fixCSVField(str, "\""); 15 | assert.equal(result, '\"aaa\"\"bbb\"'); 16 | }); 17 | 18 | it('should fix a field with space delimiter', () => { 19 | const str = 'aaa bbb'; 20 | const result = fixCSVField(str, " "); 21 | assert.equal(result, '\"aaa bbb\"'); 22 | }); 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /test/utils_removeColumns.test.ts: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | import { removeColumns } from '../src/utils'; 4 | 5 | 6 | describe('Test utility functions: removeColumns', () => { 7 | 8 | it('should remove one column correctly', function() { 9 | const columns = [ 10 | ['a', 'b', 'c'], 11 | ['d', 'e', 'f'], 12 | ['g', 'h', 'i'], 13 | ]; 14 | 15 | removeColumns(columns, [0]); 16 | expect(columns.length).toEqual(3) 17 | expect(columns[0]).toStrictEqual(['b', 'c']); 18 | expect(columns[1]).toStrictEqual(['e', 'f']); 19 | expect(columns[2]).toStrictEqual(['h', 'i']); 20 | }); 21 | 22 | it('should remove two columns', () => { 23 | const columns = [ 24 | ['a', 'b', 'c'], 25 | ['d', 'e', 'f'], 26 | ['g', 'h', 'i'], 27 | ]; 28 | 29 | removeColumns(columns, [0, 1]); 30 | expect(columns.length).toEqual(3) 31 | expect(columns[0]).toStrictEqual(['c']); 32 | expect(columns[1]).toStrictEqual(['f']); 33 | }); 34 | 35 | it('should remove the last column', () => { 36 | const columns = [ 37 | ['a', 'b', 'c'], 38 | ['d', 'e', 'f'], 39 | ['g', 'h', 'i'], 40 | ]; 41 | 42 | removeColumns(columns, [2]); 43 | expect(columns.length).toEqual(3) 44 | expect(columns[0]).toStrictEqual(['a', 'b']); 45 | expect(columns[1]).toStrictEqual(['d', 'e']); 46 | expect(columns[2]).toStrictEqual(['g', 'h']); 47 | }); 48 | 49 | it('should skip if remove out of range', () => { 50 | const columns = [ 51 | ['a', 'b', 'c'], 52 | ['d', 'e', 'f'], 53 | ['g', 'h', 'i'], 54 | ]; 55 | 56 | removeColumns(columns, [99]); 57 | expect(columns.length).toEqual(3); 58 | expect(columns[0]).toEqual(['a', 'b', 'c']); 59 | }); 60 | 61 | it('should skip if remove out of range', () => { 62 | const columns = [ 63 | ['a', 'b', 'c'], 64 | ['d', 'e', 'f'], 65 | ['g', 'h', 'i'], 66 | ]; 67 | 68 | removeColumns(columns, [0, 99]); 69 | expect(columns.length).toEqual(3); 70 | expect(columns[0]).toEqual(['b', 'c']); 71 | }); 72 | 73 | it('should not remove repeated columns', () => { 74 | const columns = [ 75 | ['a', 'b', 'c'], 76 | ['d', 'e', 'f'], 77 | ['g', 'h', 'i'], 78 | ]; 79 | 80 | removeColumns(columns, [0, 0]); 81 | expect(columns.length).toEqual(3); 82 | expect(columns[0]).toEqual(['b', 'c']); 83 | }); 84 | 85 | it('should remove columns in the correct order', () => { 86 | const columns = [ 87 | ['a', 'b', 'c'], 88 | ['d', 'e', 'f'], 89 | ['g', 'h', 'i'], 90 | ]; 91 | 92 | removeColumns(columns, [0, 2]); 93 | expect(columns.length).toEqual(3); 94 | expect(columns[0]).toEqual(['b']); 95 | expect(columns[1]).toEqual(['e']); 96 | }); 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "allowJs": true, 5 | "target": "ESNext", 6 | "sourceMap": true, 7 | "removeComments": false, 8 | "declaration": true, 9 | "rootDir": "src", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts", 15 | "test/**/*.{ts,js}" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/excellentexport.ts', 5 | // devtool: "inline-source-map", 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'excellentexport.js', 9 | library: 'ExcellentExport', 10 | libraryTarget: 'umd', 11 | libraryExport: 'default', 12 | auxiliaryComment: 'ExcellentExport.js' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | exclude: /(node_modules)/, 19 | use: { 20 | loader: 'ts-loader' 21 | } 22 | } 23 | ] 24 | }, 25 | performance: { 26 | hints: false 27 | }, 28 | resolve: { 29 | extensions: ['.ts', '.js'] 30 | }, 31 | }; 32 | --------------------------------------------------------------------------------