excel-builder-vanilla
Version:
An easy way of building Excel files with javascript
371 lines (332 loc) • 11.8 kB
text/typescript
import { uniqueId } from '../utilities/uniqueId.js';
import type { Drawings } from './Drawings.js';
import { Paths } from './Paths.js';
import { RelationshipManager } from './RelationshipManager.js';
import { SharedStrings } from './SharedStrings.js';
import { StyleSheet } from './StyleSheet.js';
import type { Table } from './Table.js';
import { Util } from './Util.js';
import { Worksheet } from './Worksheet.js';
import { XMLDOM } from './XMLDOM.js';
export interface MediaMeta {
id: string;
data: string;
fileName: string;
contentType: string | null;
extension: string;
rId?: string;
}
/**
* @module Excel/Workbook
*/
/* globals console: true */
export class Workbook {
id = uniqueId('Workbook');
styleSheet = new StyleSheet();
sharedStrings = new SharedStrings();
relations = new RelationshipManager();
worksheets: Worksheet[] = [];
tables: Table[] = [];
drawings: Drawings[] = [];
media: { [filename: string]: MediaMeta } = {};
printTitles: any;
constructor() {
this.initialize();
}
initialize() {
this.id = uniqueId('Workbook');
this.styleSheet = new StyleSheet();
this.sharedStrings = new SharedStrings();
this.relations = new RelationshipManager();
this.relations.addRelation(this.styleSheet, 'stylesheet');
this.relations.addRelation(this.sharedStrings, 'sharedStrings');
}
createWorksheet(config?: any) {
config = Object.assign({}, { name: 'Sheet '.concat(String(this.worksheets.length + 1)) }, config);
return new Worksheet(config);
}
getStyleSheet() {
return this.styleSheet;
}
addTable(table: Table) {
this.tables.push(table);
}
addDrawings(drawings: Drawings) {
this.drawings.push(drawings);
}
/**
* Set number of rows to repeat for this sheet.
*
* @param {String} sheet name
* @param {int} number of rows to repeat from the top
* @returns {undefined}
*/
setPrintTitleTop(inSheet: string, inRowCount: number) {
if (this.printTitles == null) {
this.printTitles = {};
}
if (this.printTitles[inSheet] == null) {
this.printTitles[inSheet] = {};
}
this.printTitles[inSheet].top = inRowCount;
}
/**
* Set number of rows to repeat for this sheet.
*
* @param {String} sheet name
* @param {int} number of columns to repeat from the left
* @returns {undefined}
*/
setPrintTitleLeft(inSheet: string, inRowCount: number) {
if (this.printTitles == null) {
this.printTitles = {};
}
if (this.printTitles[inSheet] == null) {
this.printTitles[inSheet] = {};
}
//WARN: this does not handle AA, AB, etc.
this.printTitles[inSheet].left = String.fromCharCode(64 + inRowCount);
}
addMedia(_type: string, fileName: string, fileData: any, contentType?: string | null) {
const fileNamePieces = fileName.split('.');
const extension = fileNamePieces[fileNamePieces.length - 1];
if (!contentType) {
switch (extension.toLowerCase()) {
case 'jpeg':
case 'jpg':
contentType = 'image/jpeg';
break;
case 'png':
contentType = 'image/png';
break;
case 'gif':
contentType = 'image/gif';
break;
default:
contentType = null;
break;
}
}
if (!this.media[fileName]) {
this.media[fileName] = {
id: fileName,
data: fileData,
fileName: fileName,
contentType: contentType,
extension: extension,
};
}
return this.media[fileName];
}
addWorksheet(worksheet: Worksheet) {
this.relations.addRelation(worksheet, 'worksheet');
worksheet.setSharedStringCollection(this.sharedStrings);
this.worksheets.push(worksheet);
}
createContentTypes() {
const doc = Util.createXmlDoc(Util.schemas.contentTypes, 'Types');
const types = doc.documentElement;
let i: number;
let l: number;
types.appendChild(
Util.createElement(doc, 'Default', [
['Extension', 'rels'],
['ContentType', 'application/vnd.openxmlformats-package.relationships+xml'],
]),
);
types.appendChild(
Util.createElement(doc, 'Default', [
['Extension', 'xml'],
['ContentType', 'application/xml'],
]),
);
const extensions: any = {};
for (const filename in this.media) {
if (filename in this.media) {
extensions[this.media[filename].extension] = this.media[filename].contentType;
}
}
for (const extension in extensions) {
if (extension in extensions) {
types.appendChild(
Util.createElement(doc, 'Default', [
['Extension', extension],
['ContentType', extensions[extension]],
]),
);
}
}
types.appendChild(
Util.createElement(doc, 'Override', [
['PartName', '/xl/workbook.xml'],
['ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'],
]),
);
types.appendChild(
Util.createElement(doc, 'Override', [
['PartName', '/xl/sharedStrings.xml'],
['ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'],
]),
);
types.appendChild(
Util.createElement(doc, 'Override', [
['PartName', '/xl/styles.xml'],
['ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'],
]),
);
for (i = 0, l = this.worksheets.length; i < l; i++) {
types.appendChild(
Util.createElement(doc, 'Override', [
['PartName', `/xl/worksheets/sheet${i + 1}.xml`],
['ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'],
]),
);
}
for (i = 0, l = this.tables.length; i < l; i++) {
types.appendChild(
Util.createElement(doc, 'Override', [
['PartName', `/xl/tables/table${i + 1}.xml`],
['ContentType', 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml'],
]),
);
}
for (i = 0, l = this.drawings.length; i < l; i++) {
types.appendChild(
Util.createElement(doc, 'Override', [
['PartName', `/xl/drawings/drawing${i + 1}.xml`],
['ContentType', 'application/vnd.openxmlformats-officedocument.drawing+xml'],
]),
);
}
return doc;
}
toXML() {
const doc = Util.createXmlDoc(Util.schemas.spreadsheetml, 'workbook');
const wb = doc.documentElement;
wb.setAttribute('xmlns:r', Util.schemas.relationships);
const maxWorksheetNameLength = 31;
const sheets = Util.createElement(doc, 'sheets');
for (let i = 0, l = this.worksheets.length; i < l; i++) {
const sheet = doc.createElement('sheet');
// Microsoft Excel (2007, 2013) do not allow worksheet names longer than 31 characters
// if the worksheet name is longer, Excel displays an "Excel found unreadable content..." popup when opening the file
if (typeof console !== 'undefined' && this.worksheets[i].name.length > maxWorksheetNameLength) {
console.log(
`Microsoft Excel requires work sheet names to be less than ${maxWorksheetNameLength + 1} characters long, work sheet name "${
this.worksheets[i].name
}" is ${this.worksheets[i].name.length} characters long`,
);
}
sheet.setAttribute('name', this.worksheets[i].name);
sheet.setAttribute('sheetId', i + 1);
sheet.setAttribute('r:id', this.relations.getRelationshipId(this.worksheets[i]));
sheets.appendChild(sheet);
}
wb.appendChild(sheets);
//now to add repeating rows
const definedNames = Util.createElement(doc, 'definedNames');
let ctr = 0;
for (const name in this.printTitles) {
if (name in this.printTitles) {
const entry = this.printTitles[name];
const definedName = doc.createElement('definedName');
definedName.setAttribute('name', '_xlnm.Print_Titles');
definedName.setAttribute('localSheetId', ctr++);
let value = '';
if (entry.top) {
value += `${name}!$1:$${entry.top}`;
if (entry.left) {
value += ',';
}
}
if (entry.left) {
value += `${name}!$A:$${entry.left}`;
}
definedName.appendChild(doc.createTextNode(value));
definedNames.appendChild(definedName);
}
}
wb.appendChild(definedNames);
return doc;
}
createWorkbookRelationship() {
const doc = Util.createXmlDoc(Util.schemas.relationshipPackage, 'Relationships');
const relationships = doc.documentElement;
relationships.appendChild(
Util.createElement(doc, 'Relationship', [
['Id', 'rId1'],
['Type', Util.schemas.officeDocument],
['Target', 'xl/workbook.xml'],
]),
);
return doc;
}
_generateCorePaths(files: any) {
let i: number;
let l: number;
Paths[this.styleSheet.id] = 'styles.xml';
Paths[this.sharedStrings.id] = 'sharedStrings.xml';
Paths[this.id] = '/xl/workbook.xml';
for (i = 0, l = this.tables.length; i < l; i++) {
files[`/xl/tables/table${i + 1}.xml`] = this.tables[i].toXML();
Paths[this.tables[i].id] = `/xl/tables/table${i + 1}.xml`;
}
for (const fileName in this.media) {
if (fileName in this.media) {
const media = this.media[fileName];
files[`/xl/media/${fileName}`] = media.data;
Paths[fileName] = `/xl/media/${fileName}`;
}
}
for (i = 0, l = this.drawings.length; i < l; i++) {
files[`/xl/drawings/drawing${i + 1}.xml`] = this.drawings[i].toXML();
Paths[this.drawings[i].id] = `/xl/drawings/drawing${i + 1}.xml`;
files[`/xl/drawings/_rels/drawing${i + 1}.xml.rels`] = this.drawings[i].relations.toXML();
}
}
_prepareFilesForPackaging(files: { [path: string]: XMLDOM | string }) {
Object.assign(files, {
'/[Content_Types].xml': this.createContentTypes(),
'/_rels/.rels': this.createWorkbookRelationship(),
'/xl/styles.xml': this.styleSheet.toXML(),
'/xl/workbook.xml': this.toXML(),
'/xl/sharedStrings.xml': this.sharedStrings.toXML(),
'/xl/_rels/workbook.xml.rels': this.relations.toXML(),
});
for (const [key, value] of Object.entries(files)) {
if (key.indexOf('.xml') !== -1 || key.indexOf('.rels') !== -1) {
if (value instanceof XMLDOM) {
files[key] = value.toString();
} else {
files[key] = (value as any).xml || new window.XMLSerializer().serializeToString(value as any);
}
let content = (files[key] as string).replace(/xmlns=""/g, '');
content = content.replace(/NS[\d]+:/g, '');
content = content.replace(/xmlns:NS[\d]+=""/g, '');
files[key] = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${content}`;
}
}
}
generateFiles(): Promise<{ [path: string]: string }> {
return new Promise(resolve => {
const files: any = {};
this._generateCorePaths(files);
for (let i = 0, l = this.worksheets.length; i < l; i++) {
const xml = this.worksheets[i].toXML();
files[`/xl/worksheets/sheet${i + 1}.xml`] = xml;
Paths[this.worksheets[i].id] = `worksheets/sheet${i + 1}.xml`;
files[`/xl/worksheets/_rels/sheet${i + 1}.xml.rels`] = this.worksheets[i].relations.toXML();
}
this._prepareFilesForPackaging(files);
return resolve(files);
});
}
/** Return workbook XML header */
serializeHeader(): string {
return '<?xml version="1.0" encoding="UTF-8"?><workbook>';
}
/** Return workbook XML footer */
serializeFooter(): string {
return '</workbook>';
}
}