Skip to content

Instantly share code, notes, and snippets.

@Radiergummi
Created May 7, 2018 13:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Radiergummi/29ad8a344e384031da05266833dbe2fd to your computer and use it in GitHub Desktop.
Save Radiergummi/29ad8a344e384031da05266833dbe2fd to your computer and use it in GitHub Desktop.
Updated version
'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 {
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#39'
};
}
/**
* 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="&quot;上午/下午 &quot;hh&quot;時&quot;mm&quot;分&quot;ss&quot;秒 &quot;"/><numFmt numFmtId="60" formatCode="&quot;This is &quot;\\ 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;
@mix3d
Copy link

mix3d commented Sep 11, 2018

you have some typos in your error messages: zipclex vs zipcelx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment