├── .gitignore ├── JsonExcel.vue ├── LICENSE ├── README.md ├── dist ├── vue-json-excel.cjs.js ├── vue-json-excel.esm.js └── vue-json-excel.umd.js ├── example-multi-line.png ├── package.json └── rollup.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | package-lock.json -------------------------------------------------------------------------------- /JsonExcel.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 361 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pooya Parsa 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 | # JSON to Excel for VUE 2 2 | 3 | Download your JSON data as an Excel file directly from the browser. This component is based on the solution proposed on [this thread](https://stackoverflow.com/questions/17142427/javascript-to-export-html-table-to-excel) 4 | 5 | ### Important! Extra prompt in Microsoft Excel 6 | 7 | **The method implemented in this component uses HTML tables to draw the .xls files, Microsoft Excel no longer recognize HTML as native content so a warning message will be displayed before opening the file. The content will be rendered perfectly but the message can't be avoided.** 8 | 9 | ## Getting started 10 | 11 | Get the package: 12 | 13 | ```bash 14 | npm install vue-json-excel 15 | ``` 16 | 17 | Register JsonExcel in your vue app entry point: 18 | 19 | ```js 20 | import Vue from "vue"; 21 | import JsonExcel from "vue-json-excel"; 22 | 23 | Vue.component("downloadExcel", JsonExcel); 24 | ``` 25 | 26 | In your template 27 | 28 | ```html 29 | 30 | Download Data 31 | 32 | 33 | ``` 34 | 35 | ## Props List 36 | 37 | | Name | Type | Description | Default | 38 | | :--------------------------- | :----------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | 39 | | data | Array | Data to be exported. | 40 | | fields | Object | Fields inside the JSON Object that you want to export. If none provided, all properties in the JSON will be exported. | 41 | | export-fields (exportFields) | Object | Used to fix the problem with other components that use the variable fields, like vee-validate. exportFields works exactly like fields | 42 | | type | string | Mime type [xls, csv] | xls | 43 | | name | string | File name to export. | data.xls | 44 | | header | string/Array | Title(s) for the data. Can be a string (one title) or an array of strings (multiple titles). | 45 | | title(deprecated) | string/Array | same as header, title is maintained for retro-compatibility purposes but its use is not recommended due to the conflict with the HTML5 title attribute. | 46 | | footer | string/Array | Footer(s) for the data. Can be a string (one footer) or an array of strings (multiple footers). | 47 | | default-value (defaultValue) | string | Use as fallback when the row has no field values. | '' | 48 | | worksheet | string | Name of the worksheet tab. | 'Sheet1' | 49 | | fetch | Function | Callback to fetch data before download, if it's set it runs immediately after mouse pressed and before download process.
IMPORTANT: only works if no data prop is defined. | 50 | | before-generate | Function | Callback to call a method right before the generate / fetch data, eg:show loading progress | 51 | | before-finish | Function | Callback to call a method right before the download box pops out, eg:hide loading progress | 52 | | stringifyLongNum | Boolean | stringify long number and decimal(solve the problem of loss of digital accuracy), default: false | 53 | | escapeCsv | Boolean | This escapes CSV values in order to fix some excel problems with number fields. But this will wrap every csv data with **="** and **"**, to avoid that you have to set this prop to false. default: True | 54 | 55 | ## Example 56 | 57 | ```js 58 | import Vue from "vue"; 59 | import JsonExcel from "vue-json-excel"; 60 | 61 | Vue.component("downloadExcel", JsonExcel); 62 | 63 | const app = new Vue({ 64 | el: "#app", 65 | data: { 66 | json_fields: { 67 | "Complete name": "name", 68 | City: "city", 69 | Telephone: "phone.mobile", 70 | "Telephone 2": { 71 | field: "phone.landline", 72 | callback: (value) => { 73 | return `Landline Phone - ${value}`; 74 | }, 75 | }, 76 | }, 77 | json_data: [ 78 | { 79 | name: "Tony Peña", 80 | city: "New York", 81 | country: "United States", 82 | birthdate: "1978-03-15", 83 | phone: { 84 | mobile: "1-541-754-3010", 85 | landline: "(541) 754-3010", 86 | }, 87 | }, 88 | { 89 | name: "Thessaloniki", 90 | city: "Athens", 91 | country: "Greece", 92 | birthdate: "1987-11-23", 93 | phone: { 94 | mobile: "+1 855 275 5071", 95 | landline: "(2741) 2621-244", 96 | }, 97 | }, 98 | ], 99 | json_meta: [ 100 | [ 101 | { 102 | key: "charset", 103 | value: "utf-8", 104 | }, 105 | ], 106 | ], 107 | }, 108 | }); 109 | ``` 110 | 111 | In your HTML call it like 112 | 113 | ```html 114 | 121 | Download Excel (you can customize this with html code!) 122 | 123 | ``` 124 | 125 | REQUIRED 126 | 127 | - json_data: Contains the data you want to export. 128 | - json_fields: You can select what fields to export. Specify nested data and assign labels to the fields. The key is the label, the value is the JSON field. This will export the field data 'as is'. If you need to customize the the exported data you can define a callback function. Thanks to @gucastiliao. 129 | 130 | ```js 131 | let json_fields = { 132 | // regular field (exported data 'as is') 133 | fieldLabel: attributeName, // nested attribute supported 134 | // callback function for data formatting 135 | anotherFieldLabel: { 136 | field: anotherAttributeName, // nested attribute supported 137 | callback: (value) => { 138 | return `formatted value ${value}`; 139 | }, 140 | }, 141 | }; 142 | ``` 143 | 144 | json_fields is a object that represents which columns will be exported. If no object is provided, the component will be use the first object in your data array to extract the keys as columns names. Json field example: 145 | 146 | ``` 147 | :export-fields="{ 148 | 'Human friendly name': '_name_field_from_json', 149 | 'user's last name': '_last_name_text' 150 | }" 151 | ``` 152 | 153 | ## Export CSV 154 | 155 | To export JSON as a CSV file, just add the prop `type` with a value of "csv": 156 | 157 | ```html 158 | 165 | Download CSV (you can customize this with html code!) 166 | 167 | ``` 168 | 169 | ## Multi-line values will appear in a single cell 170 | 171 | A single text value in the data that contains newline characters will appear as a single cell in Excel. This avoids the undesired behavior of multi-line values getting split into multiple cells that must be merged before using data filters and pivot tables. 172 | 173 | For example: 174 | 175 | ```html 176 | 181 | 204 | ``` 205 | 206 | ![Example of Excel showing multi-line cell](example-multi-line.png) 207 | 208 | ## Fetch Data on Demand 209 | 210 | In case you need to fetch data from the server, you could use the fetch prop that allows you to define a callback function that is executed when your user click the download button. This function has to return a JSON value containing the data to export. A basic use case is: 211 | 212 | ```js 213 | 228 | 229 | 261 | 262 | ``` 263 | 264 | ## Using callbacks 265 | 266 | when using callback functions in the fields description, you have three option to retrieve data: 267 | 268 | - **field: 'path.to.nested.property'** you can retrieve a specific value using the nested property notation. 269 | 270 | ```js 271 | json_fields: { 272 | 'Complete name': 'name', 273 | 'City': 'city', 274 | 'Telephone': 'phone.mobile', 275 | 'Telephone 2' : { 276 | field: 'phone.landline', 277 | callback: (value) => { 278 | return `Landline Phone - ${value}`; 279 | } 280 | }, 281 | }, 282 | ``` 283 | 284 | - **field: 'define.nested.object'** you can retrieve a nested object too. 285 | 286 | ```js 287 | json_fields: {s 288 | 'Complete name': 'name', 289 | 'City': 'city', 290 | 'Telephone': 'phone.mobile', 291 | 'Telephone 2' : { 292 | field: 'phone', 293 | callback: (value) => { 294 | return `Landline Phone - ${value.landline}`; 295 | } 296 | }, 297 | }, 298 | ``` 299 | 300 | - Or **get the whole row** if field is undefined. 301 | 302 | ```js 303 | json_fields: { 304 | 'Complete name': 'name', 305 | 'City': 'city', 306 | 'Telephone': 'phone.mobile', 307 | 'Telephone 2' : { 308 | callback: (value) => { 309 | return `Landline Phone - ${value.phone.landline}`; 310 | } 311 | }, 312 | }, 313 | ``` 314 | 315 | ## License 316 | 317 | MIT 318 | 319 | #### Status 320 | 321 | This project is in an early stage of development. Any contribution is welcome :D 322 | -------------------------------------------------------------------------------- /dist/vue-json-excel.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 4 | 5 | function createCommonjsModule(fn, module) { 6 | return module = { exports: {} }, fn(module, module.exports), module.exports; 7 | } 8 | 9 | var download = createCommonjsModule(function (module, exports) { 10 | //download.js v4.2, by dandavis; 2008-2016. [MIT] see http://danml.com/download.html for tests/usage 11 | // v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime 12 | // v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs 13 | // v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling. 14 | // v4 adds AMD/UMD, commonJS, and plain browser support 15 | // v4.1 adds url download capability via solo URL argument (same domain/CORS only) 16 | // v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors 17 | // https://github.com/rndme/download 18 | 19 | (function (root, factory) { 20 | { 21 | // Node. Does not work with strict CommonJS, but 22 | // only CommonJS-like environments that support module.exports, 23 | // like Node. 24 | module.exports = factory(); 25 | } 26 | }(commonjsGlobal, function () { 27 | 28 | return function download(data, strFileName, strMimeType) { 29 | 30 | var self = window, // this script is only for browsers anyway... 31 | defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads 32 | mimeType = strMimeType || defaultMime, 33 | payload = data, 34 | url = !strFileName && !strMimeType && payload, 35 | anchor = document.createElement("a"), 36 | toString = function(a){return String(a);}, 37 | myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString), 38 | fileName = strFileName || "download", 39 | blob, 40 | reader; 41 | myBlob= myBlob.call ? myBlob.bind(self) : Blob ; 42 | 43 | if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback 44 | payload=[payload, mimeType]; 45 | mimeType=payload[0]; 46 | payload=payload[1]; 47 | } 48 | 49 | 50 | if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument 51 | fileName = url.split("/").pop().split("?")[0]; 52 | anchor.href = url; // assign href prop to temp anchor 53 | if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path: 54 | var ajax=new XMLHttpRequest(); 55 | ajax.open( "GET", url, true); 56 | ajax.responseType = 'blob'; 57 | ajax.onload= function(e){ 58 | download(e.target.response, fileName, defaultMime); 59 | }; 60 | setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return: 61 | return ajax; 62 | } // end if valid url? 63 | } // end if url? 64 | 65 | 66 | //go ahead and download dataURLs right away 67 | if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(payload)){ 68 | 69 | if(payload.length > (1024*1024*1.999) && myBlob !== toString ){ 70 | payload=dataUrlToBlob(payload); 71 | mimeType=payload.type || defaultMime; 72 | }else { 73 | return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs: 74 | navigator.msSaveBlob(dataUrlToBlob(payload), fileName) : 75 | saver(payload) ; // everyone else can save dataURLs un-processed 76 | } 77 | 78 | }else {//not data url, is it a string with special needs? 79 | if(/([\x80-\xff])/.test(payload)){ 80 | var i=0, tempUiArr= new Uint8Array(payload.length), mx=tempUiArr.length; 81 | for(i;i null, 193 | }, 194 | // this prop is used to fix the problem with other components that use the 195 | // variable fields, like vee-validate. exportFields works exactly like fields 196 | exportFields: { 197 | type: Object, 198 | default: () => null, 199 | }, 200 | // Use as fallback when the row has no field values 201 | defaultValue: { 202 | type: String, 203 | required: false, 204 | default: "", 205 | }, 206 | // Title(s) for the data, could be a string or an array of strings (multiple titles) 207 | header: { 208 | default: null, 209 | }, 210 | // Footer(s) for the data, could be a string or an array of strings (multiple footers) 211 | footer: { 212 | default: null, 213 | }, 214 | // filename to export 215 | name: { 216 | type: String, 217 | default: "data.xls", 218 | }, 219 | fetch: { 220 | type: Function, 221 | }, 222 | meta: { 223 | type: Array, 224 | default: () => [], 225 | }, 226 | worksheet: { 227 | type: String, 228 | default: "Sheet1", 229 | }, 230 | //event before generate was called 231 | beforeGenerate: { 232 | type: Function, 233 | }, 234 | //event before download pops up 235 | beforeFinish: { 236 | type: Function, 237 | }, 238 | // Determine if CSV Data should be escaped 239 | escapeCsv: { 240 | type: Boolean, 241 | default: true, 242 | }, 243 | // long number stringify 244 | stringifyLongNum: { 245 | type: Boolean, 246 | default: false, 247 | }, 248 | }, 249 | computed: { 250 | // unique identifier 251 | idName() { 252 | var now = new Date().getTime(); 253 | return "export_" + now; 254 | }, 255 | 256 | downloadFields() { 257 | if (this.fields) return this.fields; 258 | 259 | if (this.exportFields) return this.exportFields; 260 | }, 261 | }, 262 | methods: { 263 | async generate() { 264 | if (typeof this.beforeGenerate === "function") { 265 | await this.beforeGenerate(); 266 | } 267 | let data = this.data; 268 | if (typeof this.fetch === "function" || !data) data = await this.fetch(); 269 | 270 | if (!data || !data.length) { 271 | return; 272 | } 273 | 274 | let json = this.getProcessedJson(data, this.downloadFields); 275 | if (this.type === "html") { 276 | // this is mainly for testing 277 | return this.export( 278 | this.jsonToXLS(json), 279 | this.name.replace(".xls", ".html"), 280 | "text/html" 281 | ); 282 | } else if (this.type === "csv") { 283 | return this.export( 284 | this.jsonToCSV(json), 285 | this.name.replace(".xls", ".csv"), 286 | "application/csv" 287 | ); 288 | } 289 | return this.export( 290 | this.jsonToXLS(json), 291 | this.name, 292 | "application/vnd.ms-excel" 293 | ); 294 | }, 295 | /* 296 | Use downloadjs to generate the download link 297 | */ 298 | export: async function (data, filename, mime) { 299 | let blob = this.base64ToBlob(data, mime); 300 | if (typeof this.beforeFinish === "function") await this.beforeFinish(); 301 | download(blob, filename, mime); 302 | }, 303 | /* 304 | jsonToXLS 305 | --------------- 306 | Transform json data into an xml document with MS Excel format, sadly 307 | it shows a prompt when it opens, that is a default behavior for 308 | Microsoft office and cannot be avoided. It's recommended to use CSV format instead. 309 | */ 310 | jsonToXLS(data) { 311 | let xlsTemp = 312 | ' ${table}
'; 313 | let xlsData = ""; 314 | const colspan = Object.keys(data[0]).length; 315 | let _self = this; 316 | 317 | //Header 318 | const header = this.header || this.$attrs.title; 319 | if (header) { 320 | xlsData += this.parseExtraData( 321 | header, 322 | '${data}' 323 | ); 324 | } 325 | 326 | //Fields 327 | xlsData += ""; 328 | for (let key in data[0]) { 329 | xlsData += "" + key + ""; 330 | } 331 | xlsData += ""; 332 | xlsData += ""; 333 | 334 | //Data 335 | xlsData += ""; 336 | data.map(function (item, index) { 337 | xlsData += ""; 338 | for (let key in item) { 339 | xlsData += 340 | "" + 341 | _self.preprocessLongNum( 342 | _self.valueReformattedForMultilines(item[key]) 343 | ) + 344 | ""; 345 | } 346 | xlsData += ""; 347 | }); 348 | xlsData += ""; 349 | 350 | //Footer 351 | if (this.footer != null) { 352 | xlsData += ""; 353 | xlsData += this.parseExtraData( 354 | this.footer, 355 | '${data}' 356 | ); 357 | xlsData += ""; 358 | } 359 | 360 | return xlsTemp 361 | .replace("${table}", xlsData) 362 | .replace("${worksheet}", this.worksheet); 363 | }, 364 | /* 365 | jsonToCSV 366 | --------------- 367 | Transform json data into an CSV file. 368 | */ 369 | jsonToCSV(data) { 370 | let _self = this; 371 | var csvData = []; 372 | 373 | //Header 374 | const header = this.header || this.$attrs.title; 375 | if (header) { 376 | csvData.push(this.parseExtraData(header, "${data}\r\n")); 377 | } 378 | 379 | //Fields 380 | for (let key in data[0]) { 381 | csvData.push(key); 382 | csvData.push(","); 383 | } 384 | csvData.pop(); 385 | csvData.push("\r\n"); 386 | //Data 387 | data.map(function (item) { 388 | for (let key in item) { 389 | let escapedCSV = item[key] + ""; 390 | // Escaped CSV data to string to avoid problems with numbers or other types of values 391 | // this is controlled by the prop escapeCsv 392 | if (_self.escapeCsv) { 393 | escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string 394 | if (escapedCSV.match(/[,"\n]/)) { 395 | escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"'; 396 | } 397 | } 398 | csvData.push(escapedCSV); 399 | csvData.push(","); 400 | } 401 | csvData.pop(); 402 | csvData.push("\r\n"); 403 | }); 404 | //Footer 405 | if (this.footer != null) { 406 | csvData.push(this.parseExtraData(this.footer, "${data}\r\n")); 407 | } 408 | return csvData.join(""); 409 | }, 410 | /* 411 | getProcessedJson 412 | --------------- 413 | Get only the data to export, if no fields are set return all the data 414 | */ 415 | getProcessedJson(data, header) { 416 | let keys = this.getKeys(data, header); 417 | let newData = []; 418 | let _self = this; 419 | data.map(function (item, index) { 420 | let newItem = {}; 421 | for (let label in keys) { 422 | let property = keys[label]; 423 | newItem[label] = _self.getValue(property, item); 424 | } 425 | newData.push(newItem); 426 | }); 427 | 428 | return newData; 429 | }, 430 | getKeys(data, header) { 431 | if (header) { 432 | return header; 433 | } 434 | 435 | let keys = {}; 436 | for (let key in data[0]) { 437 | keys[key] = key; 438 | } 439 | return keys; 440 | }, 441 | /* 442 | parseExtraData 443 | --------------- 444 | Parse title and footer attribute to the csv format 445 | */ 446 | parseExtraData(extraData, format) { 447 | let parseData = ""; 448 | if (Array.isArray(extraData)) { 449 | for (var i = 0; i < extraData.length; i++) { 450 | if (extraData[i]) 451 | parseData += format.replace("${data}", extraData[i]); 452 | } 453 | } else { 454 | parseData += format.replace("${data}", extraData); 455 | } 456 | return parseData; 457 | }, 458 | 459 | getValue(key, item) { 460 | const field = typeof key !== "object" ? key : key.field; 461 | let indexes = typeof field !== "string" ? [] : field.split("."); 462 | let value = this.defaultValue; 463 | 464 | if (!field) value = item; 465 | else if (indexes.length > 1) 466 | value = this.getValueFromNestedItem(item, indexes); 467 | else value = this.parseValue(item[field]); 468 | 469 | if (key.hasOwnProperty("callback")) 470 | value = this.getValueFromCallback(value, key.callback); 471 | 472 | return value; 473 | }, 474 | 475 | /* 476 | convert values with newline \n characters into
477 | */ 478 | valueReformattedForMultilines(value) { 479 | if (typeof value == "string") return value.replace(/\n/gi, "
"); 480 | else return value; 481 | }, 482 | preprocessLongNum(value) { 483 | if (this.stringifyLongNum) { 484 | if (String(value).startsWith("0x")) { 485 | return value; 486 | } 487 | if (!isNaN(value) && value != "") { 488 | if (value > 99999999999 || value < 0.0000000000001) { 489 | return '="' + value + '"'; 490 | } 491 | } 492 | } 493 | return value; 494 | }, 495 | getValueFromNestedItem(item, indexes) { 496 | let nestedItem = item; 497 | for (let index of indexes) { 498 | if (nestedItem) nestedItem = nestedItem[index]; 499 | } 500 | return this.parseValue(nestedItem); 501 | }, 502 | 503 | getValueFromCallback(item, callback) { 504 | if (typeof callback !== "function") return this.defaultValue; 505 | const value = callback(item); 506 | return this.parseValue(value); 507 | }, 508 | parseValue(value) { 509 | return value || value === 0 || typeof value === "boolean" 510 | ? value 511 | : this.defaultValue; 512 | }, 513 | base64ToBlob(data, mime) { 514 | let base64 = window.btoa(window.unescape(encodeURIComponent(data))); 515 | let bstr = atob(base64); 516 | let n = bstr.length; 517 | let u8arr = new Uint8ClampedArray(n); 518 | while (n--) { 519 | u8arr[n] = bstr.charCodeAt(n); 520 | } 521 | return new Blob([u8arr], { type: mime }); 522 | }, 523 | }, // end methods 524 | }; 525 | 526 | function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier 527 | /* server only */ 528 | , shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { 529 | if (typeof shadowMode !== 'boolean') { 530 | createInjectorSSR = createInjector; 531 | createInjector = shadowMode; 532 | shadowMode = false; 533 | } // Vue.extend constructor export interop. 534 | 535 | 536 | var options = typeof script === 'function' ? script.options : script; // render functions 537 | 538 | if (template && template.render) { 539 | options.render = template.render; 540 | options.staticRenderFns = template.staticRenderFns; 541 | options._compiled = true; // functional template 542 | 543 | if (isFunctionalTemplate) { 544 | options.functional = true; 545 | } 546 | } // scopedId 547 | 548 | 549 | if (scopeId) { 550 | options._scopeId = scopeId; 551 | } 552 | 553 | var hook; 554 | 555 | if (moduleIdentifier) { 556 | // server build 557 | hook = function hook(context) { 558 | // 2.3 injection 559 | context = context || // cached call 560 | this.$vnode && this.$vnode.ssrContext || // stateful 561 | this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional 562 | // 2.2 with runInNewContext: true 563 | 564 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { 565 | context = __VUE_SSR_CONTEXT__; 566 | } // inject component styles 567 | 568 | 569 | if (style) { 570 | style.call(this, createInjectorSSR(context)); 571 | } // register component module identifier for async chunk inference 572 | 573 | 574 | if (context && context._registeredComponents) { 575 | context._registeredComponents.add(moduleIdentifier); 576 | } 577 | }; // used by ssr in case component is cached and beforeCreate 578 | // never gets called 579 | 580 | 581 | options._ssrRegister = hook; 582 | } else if (style) { 583 | hook = shadowMode ? function () { 584 | style.call(this, createInjectorShadow(this.$root.$options.shadowRoot)); 585 | } : function (context) { 586 | style.call(this, createInjector(context)); 587 | }; 588 | } 589 | 590 | if (hook) { 591 | if (options.functional) { 592 | // register for functional component in vue file 593 | var originalRender = options.render; 594 | 595 | options.render = function renderWithStyleInjection(h, context) { 596 | hook.call(context); 597 | return originalRender(h, context); 598 | }; 599 | } else { 600 | // inject component registration as beforeCreate hook 601 | var existing = options.beforeCreate; 602 | options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; 603 | } 604 | } 605 | 606 | return script; 607 | } 608 | 609 | var normalizeComponent_1 = normalizeComponent; 610 | 611 | /* script */ 612 | const __vue_script__ = script; 613 | 614 | /* template */ 615 | var __vue_render__ = function() { 616 | var _vm = this; 617 | var _h = _vm.$createElement; 618 | var _c = _vm._self._c || _h; 619 | return _c( 620 | "div", 621 | { attrs: { id: _vm.idName }, on: { click: _vm.generate } }, 622 | [_vm._t("default", [_vm._v(" Download " + _vm._s(_vm.name) + " ")])], 623 | 2 624 | ) 625 | }; 626 | var __vue_staticRenderFns__ = []; 627 | __vue_render__._withStripped = true; 628 | 629 | /* style */ 630 | const __vue_inject_styles__ = undefined; 631 | /* scoped */ 632 | const __vue_scope_id__ = undefined; 633 | /* module identifier */ 634 | const __vue_module_identifier__ = undefined; 635 | /* functional template */ 636 | const __vue_is_functional_template__ = false; 637 | /* style inject */ 638 | 639 | /* style inject SSR */ 640 | 641 | 642 | 643 | var JsonExcel = normalizeComponent_1( 644 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, 645 | __vue_inject_styles__, 646 | __vue_script__, 647 | __vue_scope_id__, 648 | __vue_is_functional_template__, 649 | __vue_module_identifier__, 650 | undefined, 651 | undefined 652 | ); 653 | 654 | module.exports = JsonExcel; 655 | -------------------------------------------------------------------------------- /dist/vue-json-excel.esm.js: -------------------------------------------------------------------------------- 1 | var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 2 | 3 | function createCommonjsModule(fn, module) { 4 | return module = { exports: {} }, fn(module, module.exports), module.exports; 5 | } 6 | 7 | var download = createCommonjsModule(function (module, exports) { 8 | //download.js v4.2, by dandavis; 2008-2016. [MIT] see http://danml.com/download.html for tests/usage 9 | // v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime 10 | // v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs 11 | // v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling. 12 | // v4 adds AMD/UMD, commonJS, and plain browser support 13 | // v4.1 adds url download capability via solo URL argument (same domain/CORS only) 14 | // v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors 15 | // https://github.com/rndme/download 16 | 17 | (function (root, factory) { 18 | { 19 | // Node. Does not work with strict CommonJS, but 20 | // only CommonJS-like environments that support module.exports, 21 | // like Node. 22 | module.exports = factory(); 23 | } 24 | }(commonjsGlobal, function () { 25 | 26 | return function download(data, strFileName, strMimeType) { 27 | 28 | var self = window, // this script is only for browsers anyway... 29 | defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads 30 | mimeType = strMimeType || defaultMime, 31 | payload = data, 32 | url = !strFileName && !strMimeType && payload, 33 | anchor = document.createElement("a"), 34 | toString = function(a){return String(a);}, 35 | myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString), 36 | fileName = strFileName || "download", 37 | blob, 38 | reader; 39 | myBlob= myBlob.call ? myBlob.bind(self) : Blob ; 40 | 41 | if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback 42 | payload=[payload, mimeType]; 43 | mimeType=payload[0]; 44 | payload=payload[1]; 45 | } 46 | 47 | 48 | if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument 49 | fileName = url.split("/").pop().split("?")[0]; 50 | anchor.href = url; // assign href prop to temp anchor 51 | if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path: 52 | var ajax=new XMLHttpRequest(); 53 | ajax.open( "GET", url, true); 54 | ajax.responseType = 'blob'; 55 | ajax.onload= function(e){ 56 | download(e.target.response, fileName, defaultMime); 57 | }; 58 | setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return: 59 | return ajax; 60 | } // end if valid url? 61 | } // end if url? 62 | 63 | 64 | //go ahead and download dataURLs right away 65 | if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(payload)){ 66 | 67 | if(payload.length > (1024*1024*1.999) && myBlob !== toString ){ 68 | payload=dataUrlToBlob(payload); 69 | mimeType=payload.type || defaultMime; 70 | }else { 71 | return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs: 72 | navigator.msSaveBlob(dataUrlToBlob(payload), fileName) : 73 | saver(payload) ; // everyone else can save dataURLs un-processed 74 | } 75 | 76 | }else {//not data url, is it a string with special needs? 77 | if(/([\x80-\xff])/.test(payload)){ 78 | var i=0, tempUiArr= new Uint8Array(payload.length), mx=tempUiArr.length; 79 | for(i;i null, 191 | }, 192 | // this prop is used to fix the problem with other components that use the 193 | // variable fields, like vee-validate. exportFields works exactly like fields 194 | exportFields: { 195 | type: Object, 196 | default: () => null, 197 | }, 198 | // Use as fallback when the row has no field values 199 | defaultValue: { 200 | type: String, 201 | required: false, 202 | default: "", 203 | }, 204 | // Title(s) for the data, could be a string or an array of strings (multiple titles) 205 | header: { 206 | default: null, 207 | }, 208 | // Footer(s) for the data, could be a string or an array of strings (multiple footers) 209 | footer: { 210 | default: null, 211 | }, 212 | // filename to export 213 | name: { 214 | type: String, 215 | default: "data.xls", 216 | }, 217 | fetch: { 218 | type: Function, 219 | }, 220 | meta: { 221 | type: Array, 222 | default: () => [], 223 | }, 224 | worksheet: { 225 | type: String, 226 | default: "Sheet1", 227 | }, 228 | //event before generate was called 229 | beforeGenerate: { 230 | type: Function, 231 | }, 232 | //event before download pops up 233 | beforeFinish: { 234 | type: Function, 235 | }, 236 | // Determine if CSV Data should be escaped 237 | escapeCsv: { 238 | type: Boolean, 239 | default: true, 240 | }, 241 | // long number stringify 242 | stringifyLongNum: { 243 | type: Boolean, 244 | default: false, 245 | }, 246 | }, 247 | computed: { 248 | // unique identifier 249 | idName() { 250 | var now = new Date().getTime(); 251 | return "export_" + now; 252 | }, 253 | 254 | downloadFields() { 255 | if (this.fields) return this.fields; 256 | 257 | if (this.exportFields) return this.exportFields; 258 | }, 259 | }, 260 | methods: { 261 | async generate() { 262 | if (typeof this.beforeGenerate === "function") { 263 | await this.beforeGenerate(); 264 | } 265 | let data = this.data; 266 | if (typeof this.fetch === "function" || !data) data = await this.fetch(); 267 | 268 | if (!data || !data.length) { 269 | return; 270 | } 271 | 272 | let json = this.getProcessedJson(data, this.downloadFields); 273 | if (this.type === "html") { 274 | // this is mainly for testing 275 | return this.export( 276 | this.jsonToXLS(json), 277 | this.name.replace(".xls", ".html"), 278 | "text/html" 279 | ); 280 | } else if (this.type === "csv") { 281 | return this.export( 282 | this.jsonToCSV(json), 283 | this.name.replace(".xls", ".csv"), 284 | "application/csv" 285 | ); 286 | } 287 | return this.export( 288 | this.jsonToXLS(json), 289 | this.name, 290 | "application/vnd.ms-excel" 291 | ); 292 | }, 293 | /* 294 | Use downloadjs to generate the download link 295 | */ 296 | export: async function (data, filename, mime) { 297 | let blob = this.base64ToBlob(data, mime); 298 | if (typeof this.beforeFinish === "function") await this.beforeFinish(); 299 | download(blob, filename, mime); 300 | }, 301 | /* 302 | jsonToXLS 303 | --------------- 304 | Transform json data into an xml document with MS Excel format, sadly 305 | it shows a prompt when it opens, that is a default behavior for 306 | Microsoft office and cannot be avoided. It's recommended to use CSV format instead. 307 | */ 308 | jsonToXLS(data) { 309 | let xlsTemp = 310 | ' ${table}
'; 311 | let xlsData = ""; 312 | const colspan = Object.keys(data[0]).length; 313 | let _self = this; 314 | 315 | //Header 316 | const header = this.header || this.$attrs.title; 317 | if (header) { 318 | xlsData += this.parseExtraData( 319 | header, 320 | '${data}' 321 | ); 322 | } 323 | 324 | //Fields 325 | xlsData += ""; 326 | for (let key in data[0]) { 327 | xlsData += "" + key + ""; 328 | } 329 | xlsData += ""; 330 | xlsData += ""; 331 | 332 | //Data 333 | xlsData += ""; 334 | data.map(function (item, index) { 335 | xlsData += ""; 336 | for (let key in item) { 337 | xlsData += 338 | "" + 339 | _self.preprocessLongNum( 340 | _self.valueReformattedForMultilines(item[key]) 341 | ) + 342 | ""; 343 | } 344 | xlsData += ""; 345 | }); 346 | xlsData += ""; 347 | 348 | //Footer 349 | if (this.footer != null) { 350 | xlsData += ""; 351 | xlsData += this.parseExtraData( 352 | this.footer, 353 | '${data}' 354 | ); 355 | xlsData += ""; 356 | } 357 | 358 | return xlsTemp 359 | .replace("${table}", xlsData) 360 | .replace("${worksheet}", this.worksheet); 361 | }, 362 | /* 363 | jsonToCSV 364 | --------------- 365 | Transform json data into an CSV file. 366 | */ 367 | jsonToCSV(data) { 368 | let _self = this; 369 | var csvData = []; 370 | 371 | //Header 372 | const header = this.header || this.$attrs.title; 373 | if (header) { 374 | csvData.push(this.parseExtraData(header, "${data}\r\n")); 375 | } 376 | 377 | //Fields 378 | for (let key in data[0]) { 379 | csvData.push(key); 380 | csvData.push(","); 381 | } 382 | csvData.pop(); 383 | csvData.push("\r\n"); 384 | //Data 385 | data.map(function (item) { 386 | for (let key in item) { 387 | let escapedCSV = item[key] + ""; 388 | // Escaped CSV data to string to avoid problems with numbers or other types of values 389 | // this is controlled by the prop escapeCsv 390 | if (_self.escapeCsv) { 391 | escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string 392 | if (escapedCSV.match(/[,"\n]/)) { 393 | escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"'; 394 | } 395 | } 396 | csvData.push(escapedCSV); 397 | csvData.push(","); 398 | } 399 | csvData.pop(); 400 | csvData.push("\r\n"); 401 | }); 402 | //Footer 403 | if (this.footer != null) { 404 | csvData.push(this.parseExtraData(this.footer, "${data}\r\n")); 405 | } 406 | return csvData.join(""); 407 | }, 408 | /* 409 | getProcessedJson 410 | --------------- 411 | Get only the data to export, if no fields are set return all the data 412 | */ 413 | getProcessedJson(data, header) { 414 | let keys = this.getKeys(data, header); 415 | let newData = []; 416 | let _self = this; 417 | data.map(function (item, index) { 418 | let newItem = {}; 419 | for (let label in keys) { 420 | let property = keys[label]; 421 | newItem[label] = _self.getValue(property, item); 422 | } 423 | newData.push(newItem); 424 | }); 425 | 426 | return newData; 427 | }, 428 | getKeys(data, header) { 429 | if (header) { 430 | return header; 431 | } 432 | 433 | let keys = {}; 434 | for (let key in data[0]) { 435 | keys[key] = key; 436 | } 437 | return keys; 438 | }, 439 | /* 440 | parseExtraData 441 | --------------- 442 | Parse title and footer attribute to the csv format 443 | */ 444 | parseExtraData(extraData, format) { 445 | let parseData = ""; 446 | if (Array.isArray(extraData)) { 447 | for (var i = 0; i < extraData.length; i++) { 448 | if (extraData[i]) 449 | parseData += format.replace("${data}", extraData[i]); 450 | } 451 | } else { 452 | parseData += format.replace("${data}", extraData); 453 | } 454 | return parseData; 455 | }, 456 | 457 | getValue(key, item) { 458 | const field = typeof key !== "object" ? key : key.field; 459 | let indexes = typeof field !== "string" ? [] : field.split("."); 460 | let value = this.defaultValue; 461 | 462 | if (!field) value = item; 463 | else if (indexes.length > 1) 464 | value = this.getValueFromNestedItem(item, indexes); 465 | else value = this.parseValue(item[field]); 466 | 467 | if (key.hasOwnProperty("callback")) 468 | value = this.getValueFromCallback(value, key.callback); 469 | 470 | return value; 471 | }, 472 | 473 | /* 474 | convert values with newline \n characters into
475 | */ 476 | valueReformattedForMultilines(value) { 477 | if (typeof value == "string") return value.replace(/\n/gi, "
"); 478 | else return value; 479 | }, 480 | preprocessLongNum(value) { 481 | if (this.stringifyLongNum) { 482 | if (String(value).startsWith("0x")) { 483 | return value; 484 | } 485 | if (!isNaN(value) && value != "") { 486 | if (value > 99999999999 || value < 0.0000000000001) { 487 | return '="' + value + '"'; 488 | } 489 | } 490 | } 491 | return value; 492 | }, 493 | getValueFromNestedItem(item, indexes) { 494 | let nestedItem = item; 495 | for (let index of indexes) { 496 | if (nestedItem) nestedItem = nestedItem[index]; 497 | } 498 | return this.parseValue(nestedItem); 499 | }, 500 | 501 | getValueFromCallback(item, callback) { 502 | if (typeof callback !== "function") return this.defaultValue; 503 | const value = callback(item); 504 | return this.parseValue(value); 505 | }, 506 | parseValue(value) { 507 | return value || value === 0 || typeof value === "boolean" 508 | ? value 509 | : this.defaultValue; 510 | }, 511 | base64ToBlob(data, mime) { 512 | let base64 = window.btoa(window.unescape(encodeURIComponent(data))); 513 | let bstr = atob(base64); 514 | let n = bstr.length; 515 | let u8arr = new Uint8ClampedArray(n); 516 | while (n--) { 517 | u8arr[n] = bstr.charCodeAt(n); 518 | } 519 | return new Blob([u8arr], { type: mime }); 520 | }, 521 | }, // end methods 522 | }; 523 | 524 | function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier 525 | /* server only */ 526 | , shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { 527 | if (typeof shadowMode !== 'boolean') { 528 | createInjectorSSR = createInjector; 529 | createInjector = shadowMode; 530 | shadowMode = false; 531 | } // Vue.extend constructor export interop. 532 | 533 | 534 | var options = typeof script === 'function' ? script.options : script; // render functions 535 | 536 | if (template && template.render) { 537 | options.render = template.render; 538 | options.staticRenderFns = template.staticRenderFns; 539 | options._compiled = true; // functional template 540 | 541 | if (isFunctionalTemplate) { 542 | options.functional = true; 543 | } 544 | } // scopedId 545 | 546 | 547 | if (scopeId) { 548 | options._scopeId = scopeId; 549 | } 550 | 551 | var hook; 552 | 553 | if (moduleIdentifier) { 554 | // server build 555 | hook = function hook(context) { 556 | // 2.3 injection 557 | context = context || // cached call 558 | this.$vnode && this.$vnode.ssrContext || // stateful 559 | this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional 560 | // 2.2 with runInNewContext: true 561 | 562 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { 563 | context = __VUE_SSR_CONTEXT__; 564 | } // inject component styles 565 | 566 | 567 | if (style) { 568 | style.call(this, createInjectorSSR(context)); 569 | } // register component module identifier for async chunk inference 570 | 571 | 572 | if (context && context._registeredComponents) { 573 | context._registeredComponents.add(moduleIdentifier); 574 | } 575 | }; // used by ssr in case component is cached and beforeCreate 576 | // never gets called 577 | 578 | 579 | options._ssrRegister = hook; 580 | } else if (style) { 581 | hook = shadowMode ? function () { 582 | style.call(this, createInjectorShadow(this.$root.$options.shadowRoot)); 583 | } : function (context) { 584 | style.call(this, createInjector(context)); 585 | }; 586 | } 587 | 588 | if (hook) { 589 | if (options.functional) { 590 | // register for functional component in vue file 591 | var originalRender = options.render; 592 | 593 | options.render = function renderWithStyleInjection(h, context) { 594 | hook.call(context); 595 | return originalRender(h, context); 596 | }; 597 | } else { 598 | // inject component registration as beforeCreate hook 599 | var existing = options.beforeCreate; 600 | options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; 601 | } 602 | } 603 | 604 | return script; 605 | } 606 | 607 | var normalizeComponent_1 = normalizeComponent; 608 | 609 | /* script */ 610 | const __vue_script__ = script; 611 | 612 | /* template */ 613 | var __vue_render__ = function() { 614 | var _vm = this; 615 | var _h = _vm.$createElement; 616 | var _c = _vm._self._c || _h; 617 | return _c( 618 | "div", 619 | { attrs: { id: _vm.idName }, on: { click: _vm.generate } }, 620 | [_vm._t("default", [_vm._v(" Download " + _vm._s(_vm.name) + " ")])], 621 | 2 622 | ) 623 | }; 624 | var __vue_staticRenderFns__ = []; 625 | __vue_render__._withStripped = true; 626 | 627 | /* style */ 628 | const __vue_inject_styles__ = undefined; 629 | /* scoped */ 630 | const __vue_scope_id__ = undefined; 631 | /* module identifier */ 632 | const __vue_module_identifier__ = undefined; 633 | /* functional template */ 634 | const __vue_is_functional_template__ = false; 635 | /* style inject */ 636 | 637 | /* style inject SSR */ 638 | 639 | 640 | 641 | var JsonExcel = normalizeComponent_1( 642 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, 643 | __vue_inject_styles__, 644 | __vue_script__, 645 | __vue_scope_id__, 646 | __vue_is_functional_template__, 647 | __vue_module_identifier__, 648 | undefined, 649 | undefined 650 | ); 651 | 652 | export default JsonExcel; 653 | -------------------------------------------------------------------------------- /dist/vue-json-excel.umd.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = global || self, global.JsonExcel = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 8 | 9 | function createCommonjsModule(fn, module) { 10 | return module = { exports: {} }, fn(module, module.exports), module.exports; 11 | } 12 | 13 | var download = createCommonjsModule(function (module, exports) { 14 | //download.js v4.2, by dandavis; 2008-2016. [MIT] see http://danml.com/download.html for tests/usage 15 | // v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime 16 | // v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs 17 | // v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling. 18 | // v4 adds AMD/UMD, commonJS, and plain browser support 19 | // v4.1 adds url download capability via solo URL argument (same domain/CORS only) 20 | // v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors 21 | // https://github.com/rndme/download 22 | 23 | (function (root, factory) { 24 | { 25 | // Node. Does not work with strict CommonJS, but 26 | // only CommonJS-like environments that support module.exports, 27 | // like Node. 28 | module.exports = factory(); 29 | } 30 | }(commonjsGlobal, function () { 31 | 32 | return function download(data, strFileName, strMimeType) { 33 | 34 | var self = window, // this script is only for browsers anyway... 35 | defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads 36 | mimeType = strMimeType || defaultMime, 37 | payload = data, 38 | url = !strFileName && !strMimeType && payload, 39 | anchor = document.createElement("a"), 40 | toString = function(a){return String(a);}, 41 | myBlob = (self.Blob || self.MozBlob || self.WebKitBlob || toString), 42 | fileName = strFileName || "download", 43 | blob, 44 | reader; 45 | myBlob= myBlob.call ? myBlob.bind(self) : Blob ; 46 | 47 | if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback 48 | payload=[payload, mimeType]; 49 | mimeType=payload[0]; 50 | payload=payload[1]; 51 | } 52 | 53 | 54 | if(url && url.length< 2048){ // if no filename and no mime, assume a url was passed as the only argument 55 | fileName = url.split("/").pop().split("?")[0]; 56 | anchor.href = url; // assign href prop to temp anchor 57 | if(anchor.href.indexOf(url) !== -1){ // if the browser determines that it's a potentially valid url path: 58 | var ajax=new XMLHttpRequest(); 59 | ajax.open( "GET", url, true); 60 | ajax.responseType = 'blob'; 61 | ajax.onload= function(e){ 62 | download(e.target.response, fileName, defaultMime); 63 | }; 64 | setTimeout(function(){ ajax.send();}, 0); // allows setting custom ajax headers using the return: 65 | return ajax; 66 | } // end if valid url? 67 | } // end if url? 68 | 69 | 70 | //go ahead and download dataURLs right away 71 | if(/^data:([\w+-]+\/[\w+.-]+)?[,;]/.test(payload)){ 72 | 73 | if(payload.length > (1024*1024*1.999) && myBlob !== toString ){ 74 | payload=dataUrlToBlob(payload); 75 | mimeType=payload.type || defaultMime; 76 | }else { 77 | return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs: 78 | navigator.msSaveBlob(dataUrlToBlob(payload), fileName) : 79 | saver(payload) ; // everyone else can save dataURLs un-processed 80 | } 81 | 82 | }else {//not data url, is it a string with special needs? 83 | if(/([\x80-\xff])/.test(payload)){ 84 | var i=0, tempUiArr= new Uint8Array(payload.length), mx=tempUiArr.length; 85 | for(i;i null, 197 | }, 198 | // this prop is used to fix the problem with other components that use the 199 | // variable fields, like vee-validate. exportFields works exactly like fields 200 | exportFields: { 201 | type: Object, 202 | default: () => null, 203 | }, 204 | // Use as fallback when the row has no field values 205 | defaultValue: { 206 | type: String, 207 | required: false, 208 | default: "", 209 | }, 210 | // Title(s) for the data, could be a string or an array of strings (multiple titles) 211 | header: { 212 | default: null, 213 | }, 214 | // Footer(s) for the data, could be a string or an array of strings (multiple footers) 215 | footer: { 216 | default: null, 217 | }, 218 | // filename to export 219 | name: { 220 | type: String, 221 | default: "data.xls", 222 | }, 223 | fetch: { 224 | type: Function, 225 | }, 226 | meta: { 227 | type: Array, 228 | default: () => [], 229 | }, 230 | worksheet: { 231 | type: String, 232 | default: "Sheet1", 233 | }, 234 | //event before generate was called 235 | beforeGenerate: { 236 | type: Function, 237 | }, 238 | //event before download pops up 239 | beforeFinish: { 240 | type: Function, 241 | }, 242 | // Determine if CSV Data should be escaped 243 | escapeCsv: { 244 | type: Boolean, 245 | default: true, 246 | }, 247 | // long number stringify 248 | stringifyLongNum: { 249 | type: Boolean, 250 | default: false, 251 | }, 252 | }, 253 | computed: { 254 | // unique identifier 255 | idName() { 256 | var now = new Date().getTime(); 257 | return "export_" + now; 258 | }, 259 | 260 | downloadFields() { 261 | if (this.fields) return this.fields; 262 | 263 | if (this.exportFields) return this.exportFields; 264 | }, 265 | }, 266 | methods: { 267 | async generate() { 268 | if (typeof this.beforeGenerate === "function") { 269 | await this.beforeGenerate(); 270 | } 271 | let data = this.data; 272 | if (typeof this.fetch === "function" || !data) data = await this.fetch(); 273 | 274 | if (!data || !data.length) { 275 | return; 276 | } 277 | 278 | let json = this.getProcessedJson(data, this.downloadFields); 279 | if (this.type === "html") { 280 | // this is mainly for testing 281 | return this.export( 282 | this.jsonToXLS(json), 283 | this.name.replace(".xls", ".html"), 284 | "text/html" 285 | ); 286 | } else if (this.type === "csv") { 287 | return this.export( 288 | this.jsonToCSV(json), 289 | this.name.replace(".xls", ".csv"), 290 | "application/csv" 291 | ); 292 | } 293 | return this.export( 294 | this.jsonToXLS(json), 295 | this.name, 296 | "application/vnd.ms-excel" 297 | ); 298 | }, 299 | /* 300 | Use downloadjs to generate the download link 301 | */ 302 | export: async function (data, filename, mime) { 303 | let blob = this.base64ToBlob(data, mime); 304 | if (typeof this.beforeFinish === "function") await this.beforeFinish(); 305 | download(blob, filename, mime); 306 | }, 307 | /* 308 | jsonToXLS 309 | --------------- 310 | Transform json data into an xml document with MS Excel format, sadly 311 | it shows a prompt when it opens, that is a default behavior for 312 | Microsoft office and cannot be avoided. It's recommended to use CSV format instead. 313 | */ 314 | jsonToXLS(data) { 315 | let xlsTemp = 316 | ' ${table}
'; 317 | let xlsData = ""; 318 | const colspan = Object.keys(data[0]).length; 319 | let _self = this; 320 | 321 | //Header 322 | const header = this.header || this.$attrs.title; 323 | if (header) { 324 | xlsData += this.parseExtraData( 325 | header, 326 | '${data}' 327 | ); 328 | } 329 | 330 | //Fields 331 | xlsData += ""; 332 | for (let key in data[0]) { 333 | xlsData += "" + key + ""; 334 | } 335 | xlsData += ""; 336 | xlsData += ""; 337 | 338 | //Data 339 | xlsData += ""; 340 | data.map(function (item, index) { 341 | xlsData += ""; 342 | for (let key in item) { 343 | xlsData += 344 | "" + 345 | _self.preprocessLongNum( 346 | _self.valueReformattedForMultilines(item[key]) 347 | ) + 348 | ""; 349 | } 350 | xlsData += ""; 351 | }); 352 | xlsData += ""; 353 | 354 | //Footer 355 | if (this.footer != null) { 356 | xlsData += ""; 357 | xlsData += this.parseExtraData( 358 | this.footer, 359 | '${data}' 360 | ); 361 | xlsData += ""; 362 | } 363 | 364 | return xlsTemp 365 | .replace("${table}", xlsData) 366 | .replace("${worksheet}", this.worksheet); 367 | }, 368 | /* 369 | jsonToCSV 370 | --------------- 371 | Transform json data into an CSV file. 372 | */ 373 | jsonToCSV(data) { 374 | let _self = this; 375 | var csvData = []; 376 | 377 | //Header 378 | const header = this.header || this.$attrs.title; 379 | if (header) { 380 | csvData.push(this.parseExtraData(header, "${data}\r\n")); 381 | } 382 | 383 | //Fields 384 | for (let key in data[0]) { 385 | csvData.push(key); 386 | csvData.push(","); 387 | } 388 | csvData.pop(); 389 | csvData.push("\r\n"); 390 | //Data 391 | data.map(function (item) { 392 | for (let key in item) { 393 | let escapedCSV = item[key] + ""; 394 | // Escaped CSV data to string to avoid problems with numbers or other types of values 395 | // this is controlled by the prop escapeCsv 396 | if (_self.escapeCsv) { 397 | escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string 398 | if (escapedCSV.match(/[,"\n]/)) { 399 | escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"'; 400 | } 401 | } 402 | csvData.push(escapedCSV); 403 | csvData.push(","); 404 | } 405 | csvData.pop(); 406 | csvData.push("\r\n"); 407 | }); 408 | //Footer 409 | if (this.footer != null) { 410 | csvData.push(this.parseExtraData(this.footer, "${data}\r\n")); 411 | } 412 | return csvData.join(""); 413 | }, 414 | /* 415 | getProcessedJson 416 | --------------- 417 | Get only the data to export, if no fields are set return all the data 418 | */ 419 | getProcessedJson(data, header) { 420 | let keys = this.getKeys(data, header); 421 | let newData = []; 422 | let _self = this; 423 | data.map(function (item, index) { 424 | let newItem = {}; 425 | for (let label in keys) { 426 | let property = keys[label]; 427 | newItem[label] = _self.getValue(property, item); 428 | } 429 | newData.push(newItem); 430 | }); 431 | 432 | return newData; 433 | }, 434 | getKeys(data, header) { 435 | if (header) { 436 | return header; 437 | } 438 | 439 | let keys = {}; 440 | for (let key in data[0]) { 441 | keys[key] = key; 442 | } 443 | return keys; 444 | }, 445 | /* 446 | parseExtraData 447 | --------------- 448 | Parse title and footer attribute to the csv format 449 | */ 450 | parseExtraData(extraData, format) { 451 | let parseData = ""; 452 | if (Array.isArray(extraData)) { 453 | for (var i = 0; i < extraData.length; i++) { 454 | if (extraData[i]) 455 | parseData += format.replace("${data}", extraData[i]); 456 | } 457 | } else { 458 | parseData += format.replace("${data}", extraData); 459 | } 460 | return parseData; 461 | }, 462 | 463 | getValue(key, item) { 464 | const field = typeof key !== "object" ? key : key.field; 465 | let indexes = typeof field !== "string" ? [] : field.split("."); 466 | let value = this.defaultValue; 467 | 468 | if (!field) value = item; 469 | else if (indexes.length > 1) 470 | value = this.getValueFromNestedItem(item, indexes); 471 | else value = this.parseValue(item[field]); 472 | 473 | if (key.hasOwnProperty("callback")) 474 | value = this.getValueFromCallback(value, key.callback); 475 | 476 | return value; 477 | }, 478 | 479 | /* 480 | convert values with newline \n characters into
481 | */ 482 | valueReformattedForMultilines(value) { 483 | if (typeof value == "string") return value.replace(/\n/gi, "
"); 484 | else return value; 485 | }, 486 | preprocessLongNum(value) { 487 | if (this.stringifyLongNum) { 488 | if (String(value).startsWith("0x")) { 489 | return value; 490 | } 491 | if (!isNaN(value) && value != "") { 492 | if (value > 99999999999 || value < 0.0000000000001) { 493 | return '="' + value + '"'; 494 | } 495 | } 496 | } 497 | return value; 498 | }, 499 | getValueFromNestedItem(item, indexes) { 500 | let nestedItem = item; 501 | for (let index of indexes) { 502 | if (nestedItem) nestedItem = nestedItem[index]; 503 | } 504 | return this.parseValue(nestedItem); 505 | }, 506 | 507 | getValueFromCallback(item, callback) { 508 | if (typeof callback !== "function") return this.defaultValue; 509 | const value = callback(item); 510 | return this.parseValue(value); 511 | }, 512 | parseValue(value) { 513 | return value || value === 0 || typeof value === "boolean" 514 | ? value 515 | : this.defaultValue; 516 | }, 517 | base64ToBlob(data, mime) { 518 | let base64 = window.btoa(window.unescape(encodeURIComponent(data))); 519 | let bstr = atob(base64); 520 | let n = bstr.length; 521 | let u8arr = new Uint8ClampedArray(n); 522 | while (n--) { 523 | u8arr[n] = bstr.charCodeAt(n); 524 | } 525 | return new Blob([u8arr], { type: mime }); 526 | }, 527 | }, // end methods 528 | }; 529 | 530 | function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier 531 | /* server only */ 532 | , shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { 533 | if (typeof shadowMode !== 'boolean') { 534 | createInjectorSSR = createInjector; 535 | createInjector = shadowMode; 536 | shadowMode = false; 537 | } // Vue.extend constructor export interop. 538 | 539 | 540 | var options = typeof script === 'function' ? script.options : script; // render functions 541 | 542 | if (template && template.render) { 543 | options.render = template.render; 544 | options.staticRenderFns = template.staticRenderFns; 545 | options._compiled = true; // functional template 546 | 547 | if (isFunctionalTemplate) { 548 | options.functional = true; 549 | } 550 | } // scopedId 551 | 552 | 553 | if (scopeId) { 554 | options._scopeId = scopeId; 555 | } 556 | 557 | var hook; 558 | 559 | if (moduleIdentifier) { 560 | // server build 561 | hook = function hook(context) { 562 | // 2.3 injection 563 | context = context || // cached call 564 | this.$vnode && this.$vnode.ssrContext || // stateful 565 | this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext; // functional 566 | // 2.2 with runInNewContext: true 567 | 568 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { 569 | context = __VUE_SSR_CONTEXT__; 570 | } // inject component styles 571 | 572 | 573 | if (style) { 574 | style.call(this, createInjectorSSR(context)); 575 | } // register component module identifier for async chunk inference 576 | 577 | 578 | if (context && context._registeredComponents) { 579 | context._registeredComponents.add(moduleIdentifier); 580 | } 581 | }; // used by ssr in case component is cached and beforeCreate 582 | // never gets called 583 | 584 | 585 | options._ssrRegister = hook; 586 | } else if (style) { 587 | hook = shadowMode ? function () { 588 | style.call(this, createInjectorShadow(this.$root.$options.shadowRoot)); 589 | } : function (context) { 590 | style.call(this, createInjector(context)); 591 | }; 592 | } 593 | 594 | if (hook) { 595 | if (options.functional) { 596 | // register for functional component in vue file 597 | var originalRender = options.render; 598 | 599 | options.render = function renderWithStyleInjection(h, context) { 600 | hook.call(context); 601 | return originalRender(h, context); 602 | }; 603 | } else { 604 | // inject component registration as beforeCreate hook 605 | var existing = options.beforeCreate; 606 | options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; 607 | } 608 | } 609 | 610 | return script; 611 | } 612 | 613 | var normalizeComponent_1 = normalizeComponent; 614 | 615 | /* script */ 616 | const __vue_script__ = script; 617 | 618 | /* template */ 619 | var __vue_render__ = function() { 620 | var _vm = this; 621 | var _h = _vm.$createElement; 622 | var _c = _vm._self._c || _h; 623 | return _c( 624 | "div", 625 | { attrs: { id: _vm.idName }, on: { click: _vm.generate } }, 626 | [_vm._t("default", [_vm._v(" Download " + _vm._s(_vm.name) + " ")])], 627 | 2 628 | ) 629 | }; 630 | var __vue_staticRenderFns__ = []; 631 | __vue_render__._withStripped = true; 632 | 633 | /* style */ 634 | const __vue_inject_styles__ = undefined; 635 | /* scoped */ 636 | const __vue_scope_id__ = undefined; 637 | /* module identifier */ 638 | const __vue_module_identifier__ = undefined; 639 | /* functional template */ 640 | const __vue_is_functional_template__ = false; 641 | /* style inject */ 642 | 643 | /* style inject SSR */ 644 | 645 | 646 | 647 | var JsonExcel = normalizeComponent_1( 648 | { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, 649 | __vue_inject_styles__, 650 | __vue_script__, 651 | __vue_scope_id__, 652 | __vue_is_functional_template__, 653 | __vue_module_identifier__, 654 | undefined, 655 | undefined 656 | ); 657 | 658 | return JsonExcel; 659 | 660 | }))); 661 | -------------------------------------------------------------------------------- /example-multi-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jecovier/vue-json-excel/b886d6fe9438cc8215348dc1c692c1ab22cc9955/example-multi-line.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-json-excel", 3 | "version": "0.3.0", 4 | "description": "Download your JSON as an excel or CSV file directly from the browser", 5 | "main": "dist/vue-json-excel.umd.js", 6 | "module": "dist/vue-json-excel.esm.js", 7 | "scripts": { 8 | "build:dist": "rollup -c ./rollup.config.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/jecovier/vue-json-excel.git" 14 | }, 15 | "keywords": [ 16 | "vue", 17 | "vuejs", 18 | "vue2", 19 | "Excel", 20 | "xls", 21 | "csv", 22 | "json", 23 | "export", 24 | "json excel", 25 | "download", 26 | "component" 27 | ], 28 | "author": "Jose Javier Espinoza", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/jecovier/vue-json-excel/issues" 32 | }, 33 | "homepage": "https://github.com/jecovier/vue-json-excel#readme", 34 | "dependencies": { 35 | "downloadjs": "^1.4.7" 36 | }, 37 | "devDependencies": { 38 | "rollup": "^1.7.4", 39 | "rollup-plugin-commonjs": "^9.2.2", 40 | "rollup-plugin-node-resolve": "^4.0.1", 41 | "rollup-plugin-vue": "^4.7.2", 42 | "vue-template-compiler": "^2.6.10" 43 | } 44 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import vue from 'rollup-plugin-vue'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | 5 | export default { 6 | input: 'JsonExcel.vue', 7 | output: [ 8 | { 9 | format: 'cjs', 10 | file: 'dist/vue-json-excel.cjs.js' 11 | }, 12 | { 13 | format: 'esm', 14 | file: 'dist/vue-json-excel.esm.js' 15 | }, 16 | { 17 | name: 'JsonExcel', 18 | format: 'umd', 19 | file: 'dist/vue-json-excel.umd.js' 20 | } 21 | ], 22 | plugins: [ 23 | vue(), 24 | commonjs(), 25 | resolve() 26 | ] 27 | } 28 | --------------------------------------------------------------------------------