Created
May 7, 2018 13:26
-
-
Save Radiergummi/29ad8a344e384031da05266833dbe2fd to your computer and use it in GitHub Desktop.
Updated version
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
import FileSaver from 'file-saver'; | |
import JSZip from 'jszip'; | |
class Spreadsheet { | |
/** | |
* Holds all replacement strings to escape | |
* | |
* @returns {Object<string>} | |
*/ | |
static get escapeReplacements () { | |
return { | |
'&': '&', | |
'<': '<', | |
'>': '>', | |
'"': '"', | |
'\'': ''' | |
}; | |
} | |
/** | |
* Regex to test for unescaped HTML | |
* | |
* @returns {RegExp} | |
*/ | |
static get escapeRegex () { | |
return /[&<>"']/g; | |
} | |
/** | |
* Holds all error messages | |
* @returns {{missing_key_filename: string, invalid_type_filename: string, invalid_type_sheet: string, invalid_type_sheet_data: string}} | |
*/ | |
static get errors () { | |
return { | |
missing_key_filename: 'zipclex config missing property filename', | |
invalid_type_filename: 'zipclex filename can only be of type string', | |
invalid_type_sheet: 'zipcelx sheet data is not of type array', | |
invalid_type_sheet_data: 'zipclex sheet data childs is not of type array' | |
}; | |
} | |
/** | |
* Holds all static XLSX template strings | |
* @returns {{workbookXML: String, workbookXMLRels: String, rels: String, contentTypes: String, sheet: String}} | |
*/ | |
static get templates () { | |
return { | |
workbookXML: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><workbookPr/><sheets><sheet name="Sheet" sheetId="1" r:id="rId1"/></sheets></workbook>`, | |
WorkbookXMLRels: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>`, | |
styles: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><numFmts count="2"><numFmt numFmtId="56" formatCode=""上午/下午 "hh"時"mm"分"ss"秒 ""/><numFmt numFmtId="60" formatCode=""This is "\\ 0.0"/></numFmts><fonts count="1"><font><sz val="12"/><color theme="1"/><name val="Calibri"/><family val="2"/><scheme val="minor"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="4"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/><xf numFmtId="9" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/><xf numFmtId="60" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/><xf numFmtId="14" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles><dxfs count="0"/><tableStyles count="0" defaultTableStyle="TableStyleMedium9" defaultPivotStyle="PivotStyleMedium4"/></styleSheet>`, | |
rels: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`, | |
contentTypes: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"> | |
<Default ContentType="application/xml" Extension="xml"/> | |
<Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/> | |
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" PartName="/xl/worksheets/sheet1.xml"/> | |
<Override ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" PartName="/xl/workbook.xml"/> | |
</Types>`, | |
sheet: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheetData>{placeholder}</sheetData></worksheet>` | |
}; | |
} | |
/** | |
* Creates a new spreadsheet object and analyzes the data | |
* | |
* @param {Array} data | |
*/ | |
constructor ( data ) { | |
this.raw = data; | |
this.data = this.constructor._convert( data ); | |
this.analyze(); | |
} | |
/** | |
* Analyzes the sheet data | |
* | |
* @throws {Error} | |
*/ | |
analyze () { | |
if ( !Array.isArray( this.data ) ) { | |
throw new Error( this.constructor.errors.invalid_type_sheet ); | |
} | |
if ( !this.constructor._validateChildren( this.data ) ) { | |
throw new Error( this.constructor.errors.invalid_type_sheet_data ); | |
} | |
this.constructor._validateChildren( this.data ); | |
} | |
/** | |
* Builds the sheet markup | |
*/ | |
build () { | |
const XMLRows = this.constructor._generateRows( this.data ); | |
this.sheet = this.constructor.templates.sheet.replace( '{placeholder}', XMLRows ); | |
} | |
/** | |
* Zips the sheet and downloads it | |
* | |
* @param {String} [filename] | |
* @returns {Promise<*>} | |
*/ | |
async download ( filename = this.constructor._generateRandomName() ) { | |
if ( !this.sheet ) { | |
this.build(); | |
} | |
const zip = new JSZip(); | |
const xl = zip.folder( 'xl' ); | |
xl.file( 'workbook.xml', this.constructor.templates.workbookXML ); | |
xl.file( 'styles.xml', this.constructor.templates.styles ); | |
xl.file( '_rels/workbook.xml.rels', this.constructor.templates.workbookXMLRels ); | |
zip.file( '_rels/.rels', this.constructor.templates.rels ); | |
zip.file( '[Content_Types].xml', this.constructor.templates.contentTypes ); | |
xl.file( 'worksheets/sheet1.xml', this.sheet ); | |
return FileSaver.saveAs( | |
await zip.generateAsync( { type: 'blob' } ), | |
`${filename}.xlsx` | |
); | |
} | |
/** | |
* Generates a random string for use as a download name. | |
* | |
* @param {Number} [length] desired name length. defaults to 12. | |
* @returns {string} | |
* @private | |
*/ | |
static _generateRandomName ( length = 12 ) { | |
return Array( length + 1 ) | |
.join( ( Math.random().toString( 36 ) + '00000000000000000' ).slice( 2, 18 ) ) | |
.slice( 0, length ); | |
} | |
static _validateChildren ( parent ) { | |
return parent.every( child => Array.isArray( child ) ); | |
} | |
static _generateColumnLetter ( colIndex ) { | |
if ( typeof colIndex !== 'number' ) { | |
return ''; | |
} | |
const prefix = Math.floor( colIndex / 26 ); | |
const letter = String.fromCharCode( 97 + ( colIndex % 26 ) ).toUpperCase(); | |
if ( prefix === 0 ) { | |
return letter; | |
} | |
return this._generateColumnLetter( prefix - 1 ) + letter; | |
} | |
static _generateCellNumber ( index, rowNumber ) { | |
return `${this._generateColumnLetter( index )}${rowNumber}`; | |
} | |
static _generateStringCell ( index, value, rowIndex ) { | |
return `<c r="${this._generateCellNumber( | |
index, | |
rowIndex | |
)}" t="inlineStr"><is><t>${this._escape( value )}</t></is></c>`; | |
} | |
static _generateBooleanCell ( index, value, rowIndex ) { | |
return `<c r="${this._generateCellNumber( | |
index, | |
rowIndex | |
)}" t="b"><f>${value.toString().toUpperCase()}</f><v>${Number( value )}</v></c>`; | |
} | |
static _generateNumberCell ( index, value, rowIndex ) { | |
return `<c r="${this._generateCellNumber( index, rowIndex )}"><v>${value}</v></c>`; | |
} | |
static _generateDateCell ( index, value, rowIndex ) { | |
const basedate = new Date( 1899, 11, 31, 0, 0, 0 ); | |
const dnthresh = basedate.getTime(); | |
const base1904 = new Date( 1900, 2, 1, 0, 0, 0 ); | |
const formatDate = date => { | |
let epoch = date.getTime(); | |
if ( date >= base1904 ) { | |
epoch += 24 * 60 * 60 * 1000; | |
} | |
return ( epoch - ( dnthresh + ( date.getTimezoneOffset() - basedate.getTimezoneOffset() ) * 60000 ) ) / ( 24 * 60 * 60 * 1000 ); | |
}; | |
// TODO: To format the cell as a date, we'd need to specify s="3" here, but that causes | |
// Excel to warn about the file being broken. | |
return `<c r="${this._generateCellNumber( | |
index, | |
rowIndex | |
)}"><v>${formatDate( value )}</v></c>`; | |
} | |
static _formatCell ( cell, index, rowIndex ) { | |
switch ( cell.type ) { | |
case Number: | |
return this._generateNumberCell( index, Number( cell.value ), rowIndex ); | |
case Boolean: | |
return this._generateBooleanCell( index, Boolean( cell.value ), rowIndex ); | |
case Date: | |
return this._generateDateCell( index, new Date( cell.value ), rowIndex ); | |
default: | |
return this._generateStringCell( index, String( cell.value ), rowIndex ); | |
} | |
} | |
static _formatRow ( row, index ) { | |
// To ensure the row number starts as in excel. | |
const rowIndex = index + 1; | |
const rowCells = row | |
.map( ( cell, cellIndex ) => this._formatCell( cell, cellIndex, rowIndex ) ) | |
.join( '' ); | |
return `<row r="${rowIndex}">${rowCells}</row>`; | |
} | |
static _generateRows ( rows ) { | |
return rows | |
.map( ( row, index ) => this._formatRow( row, index ) ) | |
.join( '' ); | |
} | |
/** | |
* Escapes a string. Stolen from lodash | |
* | |
* @param {string} data | |
* @returns {string} | |
* @private | |
* @see https://github.com/lodash/lodash/blob/master/escape.js | |
*/ | |
static _escape ( data ) { | |
return ( data && RegExp( this.escapeRegex.source ).test( data ) ) | |
? data.replace( this.escapeRegex, ( char ) => this.escapeReplacements[ char ] ) | |
: data; | |
} | |
/** | |
* Converts an array of objects into a tabular array representation. This methods assumes | |
* all objects have the same keys. | |
* | |
* @param {Object} data | |
* @private | |
*/ | |
static _convert ( data ) { | |
const keyMapper = item => Object | |
.keys( item ) | |
.map( key => ( { value: key, type: this._matchType( key ) } ) ); | |
const valueMapper = item => Object | |
.values( item ) | |
.map( value => ( { value, type: this._matchType( value ) } ) ); | |
if ( !Array.isArray( data ) ) { | |
return [ | |
keyMapper( data ), | |
valueMapper( data ) | |
]; | |
} | |
const first = data[ 0 ]; | |
const keys = keyMapper( first ); | |
const values = data.map( item => valueMapper( item ) ); | |
return [ keys, ...values ]; | |
} | |
/** | |
* Matches the type of a value to all supported types | |
* | |
* @param {String|Number|Boolean|Date} data | |
* @returns {StringConstructor|NumberConstructor|BooleanConstructor|Date} | |
* @private | |
*/ | |
static _matchType ( data ) { | |
if ( data === true || data === false ) { | |
return Boolean; | |
} | |
if ( data instanceof Date ) { | |
return Date; | |
} | |
if ( !isNaN( data ) ) { | |
return Number; | |
} | |
return String; | |
} | |
} | |
export default Spreadsheet; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
you have some typos in your error messages:
zipclex
vszipcelx