excel-builder
Version:
An easy way of building Excel files with javascript
623 lines (562 loc) • 24.1 kB
JavaScript
"use strict";
var _ = require('lodash');
var util = require('./util');
var RelationshipManager = require('./RelationshipManager');
var SheetView = require('./SheetView');
/**
* This module represents an excel worksheet in its basic form - no tables, charts, etc. Its purpose is
* to hold data, the data's link to how it should be styled, and any links to other outside resources.
*
* @module Excel/Worksheet
*/
var Worksheet = function (config) {
this.relations = null;
this.columnFormats = [];
this.data = [];
this.mergedCells = [];
this.columns = [];
this.sheetProtection = false;
this._headers = [];
this._footers = [];
this._tables = [];
this._drawings = [];
this._rowInstructions = {};
this._freezePane = {};
this.hyperlinks = [];
this.sheetView = config.sheetView || new SheetView();
this.showZeros = null;
this.initialize(config);
};
_.extend(Worksheet.prototype, {
initialize: function (config) {
config = config || {};
this.name = config.name;
this.id = _.uniqueId('Worksheet');
this._timezoneOffset = new Date().getTimezoneOffset() * 60 * 1000;
if(config.columns) {
this.setColumns(config.columns);
}
this.relations = new RelationshipManager();
},
/**
* Returns an object that can be consumed by a WorksheetExportWorker
* @returns {Object}
*/
exportData: function () {
return {
relations: this.relations.exportData(),
columnFormats: this.columnFormats,
data: this.data,
columns: this.columns,
mergedCells: this.mergedCells,
_headers: this._headers,
_footers: this._footers,
_tables: this._tables,
_rowInstructions: this._rowInstructions,
_freezePane: this._freezePane,
name: this.name,
id: this.id
};
},
/**
* Imports data - to be used while inside of a WorksheetExportWorker.
* @param {Object} data
*/
importData: function (data) {
this.relations.importData(data.relations);
delete data.relations;
_.extend(this, data);
},
setSharedStringCollection: function (stringCollection) {
this.sharedStrings = stringCollection;
},
addTable: function (table) {
this._tables.push(table);
this.relations.addRelation(table, 'table');
},
addDrawings: function (table) {
this._drawings.push(table);
this.relations.addRelation(table, 'drawingRelationship');
},
setRowInstructions: function (rowIndex, instructions) {
this._rowInstructions[rowIndex] = instructions;
},
/**
* Expects an array length of three.
*
* @see Excel/Worksheet compilePageDetailPiece
* @see <a href='/cookbook/addingHeadersAndFooters.html'>Adding headers and footers to a worksheet</a>
*
* @param {Array} headers [left, center, right]
*/
setHeader: function (headers) {
if(!_.isArray(headers)) {
throw "Invalid argument type - setHeader expects an array of three instructions";
}
this._headers = headers;
},
/**
* Expects an array length of three.
*
* @see Excel/Worksheet compilePageDetailPiece
* @see <a href='/cookbook/addingHeadersAndFooters.html'>Adding headers and footers to a worksheet</a>
*
* @param {Array} footers [left, center, right]
*/
setFooter: function (footers) {
if(!_.isArray(footers)) {
throw "Invalid argument type - setFooter expects an array of three instructions";
}
this._footers = footers;
},
/**
* Turns page header/footer details into the proper format for Excel.
* @param {type} data
* @returns {String}
*/
compilePageDetailPackage: function (data) {
data = data || "";
return [
"&L", this.compilePageDetailPiece(data[0] || ""),
"&C", this.compilePageDetailPiece(data[1] || ""),
"&R", this.compilePageDetailPiece(data[2] || "")
].join('');
},
/**
* Turns instructions on page header/footer details into something
* usable by Excel.
*
* @param {type} data
* @returns {String|@exp;_@call;reduce}
*/
compilePageDetailPiece: function (data) {
if(_.isString(data)) {
return '&"-,Regular"'.concat(data);
}
if(_.isObject(data) && !_.isArray(data)) {
var string = "";
if(data.font || data.bold) {
var weighting = data.bold ? "Bold" : "Regular";
string += '&"' + (data.font || '-');
string += ',' + weighting + '"';
} else {
string += '&"-,Regular"';
}
if(data.underline) {
string += "&U";
}
if(data.fontSize) {
string += "&"+data.fontSize;
}
string += data.text;
return string;
}
if(_.isArray(data)) {
var self = this;
return _.reduce(data, function (m, v) {
return m.concat(self.compilePageDetailPiece(v));
}, "");
}
},
/**
* Creates the header node.
*
* @todo implement the ability to do even/odd headers
* @param {XML Doc} doc
* @returns {XML Node}
*/
exportHeader: function (doc) {
var oddHeader = doc.createElement('oddHeader');
oddHeader.appendChild(doc.createTextNode(this.compilePageDetailPackage(this._headers)));
return oddHeader;
},
/**
* Creates the footer node.
*
* @todo implement the ability to do even/odd footers
* @param {XML Doc} doc
* @returns {XML Node}
*/
exportFooter: function (doc) {
var oddFooter = doc.createElement('oddFooter');
oddFooter.appendChild(doc.createTextNode(this.compilePageDetailPackage(this._footers)));
return oddFooter;
},
/**
* This creates some nodes ahead of time, which cuts down on generation time due to
* most cell definitions being essentially the same, but having multiple nodes that need
* to be created. Cloning takes less time than creation.
*
* @private
* @param {XML Doc} doc
* @returns {_L8.Anonym$0._buildCache.Anonym$2}
*/
_buildCache: function (doc) {
var numberNode = doc.createElement('c');
var value = doc.createElement('v');
value.appendChild(doc.createTextNode("--temp--"));
numberNode.appendChild(value);
var formulaNode = doc.createElement('c');
var formulaValue = doc.createElement('f');
formulaValue.appendChild(doc.createTextNode("--temp--"));
formulaNode.appendChild(formulaValue);
var stringNode = doc.createElement('c');
stringNode.setAttribute('t', 's');
var stringValue = doc.createElement('v');
stringValue.appendChild(doc.createTextNode("--temp--"));
stringNode.appendChild(stringValue);
return {
number: numberNode,
date: numberNode,
string: stringNode,
formula: formulaNode
};
},
/**
* Runs through the XML document and grabs all of the strings that will
* be sent to the 'shared strings' document.
*
* @returns {Array}
*/
collectSharedStrings: function () {
var data = this.data;
var maxX = 0;
var strings = {};
for(var row = 0, l = data.length; row < l; row++) {
var dataRow = data[row];
var cellCount = dataRow.length;
maxX = cellCount > maxX ? cellCount : maxX;
for(var c = 0; c < cellCount; c++) {
var cellValue = dataRow[c];
var metadata = cellValue && cellValue.metadata || {};
if (cellValue && typeof cellValue === 'object') {
cellValue = cellValue.value;
}
if(!metadata.type) {
if(typeof cellValue === 'number') {
metadata.type = 'number';
}
}
if(metadata.type === "text" || !metadata.type) {
if(typeof strings[cellValue] === 'undefined') {
strings[cellValue] = true;
}
}
}
}
return _.keys(strings);
},
toXML: function () {
var data = this.data;
var columns = this.columns || [];
var doc = util.createXmlDoc(util.schemas.spreadsheetml, 'worksheet');
var worksheet = doc.documentElement;
var i, l, row;
worksheet.setAttribute('xmlns:r', util.schemas.relationships);
worksheet.setAttribute('xmlns:mc', util.schemas.markupCompat);
var maxX = 0;
var sheetData = util.createElement(doc, 'sheetData');
var cellCache = this._buildCache(doc);
for(row = 0, l = data.length; row < l; row++) {
var dataRow = data[row];
var cellCount = dataRow.length;
maxX = cellCount > maxX ? cellCount : maxX;
var rowNode = doc.createElement('row');
for(var c = 0; c < cellCount; c++) {
columns[c] = columns[c] || {};
var cellValue = dataRow[c];
var cell, metadata = cellValue && cellValue.metadata || {};
if (cellValue && typeof cellValue === 'object') {
cellValue = cellValue.value;
}
if(!metadata.type) {
if(typeof cellValue === 'number') {
metadata.type = 'number';
}
}
switch(metadata.type) {
case "number":
cell = cellCache.number.cloneNode(true);
cell.firstChild.firstChild.nodeValue = cellValue;
break;
case "date":
cell = cellCache.date.cloneNode(true);
cell.firstChild.firstChild.nodeValue = 25569.0 + ((cellValue - this._timezoneOffset) / (60 * 60 * 24 * 1000));
break;
case "formula":
cell = cellCache.formula.cloneNode(true);
cell.firstChild.firstChild.nodeValue = cellValue;
break;
case "text":
/*falls through*/
default:
var id;
if(typeof this.sharedStrings.strings[cellValue] !== 'undefined') {
id = this.sharedStrings.strings[cellValue];
} else {
id = this.sharedStrings.addString(cellValue);
}
cell = cellCache.string.cloneNode(true);
cell.firstChild.firstChild.nodeValue = id;
break;
}
if(metadata.style) {
cell.setAttribute('s', metadata.style);
} else if (this._rowInstructions[row] && this._rowInstructions[row].style !== undefined) {
cell.setAttribute('s', this._rowInstructions[row].style);
}
cell.setAttribute('r', util.positionToLetterRef(c + 1, row + 1));
rowNode.appendChild(cell);
}
rowNode.setAttribute('r', row + 1);
if (this._rowInstructions[row]) {
var rowInst = this._rowInstructions[row];
if (rowInst.height !== undefined) {
rowNode.setAttribute('customHeight', '1');
rowNode.setAttribute('ht', rowInst.height);
}
if (rowInst.style !== undefined) {
rowNode.setAttribute('customFormat', '1');
rowNode.setAttribute('s', rowInst.style);
}
}
sheetData.appendChild(rowNode);
}
if(maxX !== 0) {
worksheet.appendChild(util.createElement(doc, 'dimension', [
['ref', util.positionToLetterRef(1, 1) + ':' + util.positionToLetterRef(maxX, data.length)]
]));
} else {
worksheet.appendChild(util.createElement(doc, 'dimension', [
['ref', util.positionToLetterRef(1, 1)]
]));
}
worksheet.appendChild(this.sheetView.exportXML(doc));
if(this.columns.length) {
worksheet.appendChild(this.exportColumns(doc));
}
worksheet.appendChild(sheetData);
// The spec doesn't say anything about this, but Excel 2013 requires sheetProtection immediately after sheetData
if (this.sheetProtection) {
worksheet.appendChild(this.sheetProtection.exportXML(doc));
}
/**
* Doing this a bit differently, as hyperlinks could be as populous as rows. Looping twice would be bad.
*/
if(this.hyperlinks.length > 0) {
var hyperlinksEl = doc.createElement('hyperlinks');
var hyperlinks = this.hyperlinks;
for(var i = 0, l = hyperlinks.length; i < l; i++) {
var hyperlinkEl = doc.createElement('hyperlink'),
hyperlink = hyperlinks[i];
hyperlinkEl.setAttribute('ref', hyperlink.cell);
hyperlink.id = util.uniqueId('hyperlink');
this.relations.addRelation({
id: hyperlink.id,
target: hyperlink.location,
targetMode: hyperlink.targetMode || 'External'
}, 'hyperlink');
hyperlinkEl.setAttribute('r:id', this.relations.getRelationshipId(hyperlink));
hyperlinksEl.appendChild(hyperlinkEl);
}
worksheet.appendChild(hyperlinksEl);
}
// 'mergeCells' should be written before 'headerFoot' and 'drawing' due to issue
// with Microsoft Excel (2007, 2013)
if (this.mergedCells.length > 0) {
var mergeCells = doc.createElement('mergeCells');
for (i = 0, l = this.mergedCells.length; i < l; i++) {
var mergeCell = doc.createElement('mergeCell');
mergeCell.setAttribute('ref', this.mergedCells[i][0] + ':' + this.mergedCells[i][1]);
mergeCells.appendChild(mergeCell);
}
worksheet.appendChild(mergeCells);
}
this.exportPageSettings(doc, worksheet);
if(this._headers.length > 0 || this._footers.length > 0) {
var headerFooter = doc.createElement('headerFooter');
if(this._headers.length > 0) {
headerFooter.appendChild(this.exportHeader(doc));
}
if(this._footers.length > 0) {
headerFooter.appendChild(this.exportFooter(doc));
}
worksheet.appendChild(headerFooter);
}
// the 'drawing' element should be written last, after 'headerFooter', 'mergeCells', etc. due
// to issue with Microsoft Excel (2007, 2013)
for(i = 0, l = this._drawings.length; i < l; i++) {
var drawing = doc.createElement('drawing');
drawing.setAttribute('r:id', this.relations.getRelationshipId(this._drawings[i]));
worksheet.appendChild(drawing);
}
if(this._tables.length > 0) {
var tables = doc.createElement('tableParts');
tables.setAttribute('count', this._tables.length);
for(i = 0, l = this._tables.length; i < l; i++) {
var table = doc.createElement('tablePart');
table.setAttribute('r:id', this.relations.getRelationshipId(this._tables[i]));
tables.appendChild(table);
}
worksheet.appendChild(tables);
}
return doc;
},
/**
*
* @param {XML Doc} doc
* @returns {XML Node}
*/
exportColumns: function (doc) {
var cols = util.createElement(doc, 'cols');
for(var i = 0, l = this.columns.length; i < l; i++) {
var cd = this.columns[i];
var col = util.createElement(doc, 'col', [
['min', cd.min || i + 1],
['max', cd.max || i + 1]
]);
if (cd.hidden) {
col.setAttribute('hidden', 1);
}
if(cd.bestFit) {
col.setAttribute('bestFit', 1);
}
if(cd.customWidth || cd.width) {
col.setAttribute('customWidth', 1);
}
if(cd.width) {
col.setAttribute('width', cd.width);
} else {
col.setAttribute('width', 9.140625);
}
cols.appendChild(col);
}
return cols;
},
/**
* Sets the page settings on a worksheet node.
*
* @param {XML Doc} doc
* @param {XML Node} worksheet
* @returns {undefined}
*/
exportPageSettings: function (doc, worksheet) {
if(this._margin) {
var defaultVal = 0.7;
var left = this._margin.left?this._margin.left:defaultVal;;
var right = this._margin.right?this._margin.right:defaultVal;;
var top = this._margin.top?this._margin.top:defaultVal;
var bottom = this._margin.bottom?this._margin.bottom:defaultVal;
defaultVal = 0.3;
var header = this._margin.header?this._margin.header:defaultVal;;
var footer = this._margin.footer?this._margin.footer:defaultVal;;
worksheet.appendChild(util.createElement(doc, 'pageMargins', [
['top', top]
, ['bottom', bottom]
, ['left', left]
, ['right', right]
, ['header', header]
, ['footer', footer]
]));
}
if(this._orientation) {
worksheet.appendChild(util.createElement(doc, 'pageSetup', [
['orientation', this._orientation]
]));
}
},
/**
* http://www.schemacentral.com/sc/ooxml/t-ssml_ST_Orientation.html
*
* Can be one of 'portrait' or 'landscape'.
*
* @param {String} orientation
* @returns {undefined}
*/
setPageOrientation: function (orientation) {
this._orientation = orientation;
},
/**
* Set page details in inches.
* use this structure:
* {
* top: 0.7
* , bottom: 0.7
* , left: 0.7
* , right: 0.7
* , header: 0.3
* , footer: 0.3
* }
*
* @returns {undefined}
*/
setPageMargin: function (input) {
this._margin = input;
},
/**
* http://www.schemacentral.com/sc/ooxml/t-ssml_ST_Orientation.html
*
* Can be one of 'portrait' or 'landscape'.
*
* @param {String} orientation
* @returns {undefined}
*/
setPageOrientation: function (orientation) {
this._orientation = orientation;
},
/**
* Expects an array of column definitions. Each column definition needs to have a width assigned to it.
*
* @param {Array} columns
*/
setColumns: function (columns) {
this.columns = columns;
},
/**
* Expects an array of data to be translated into cells.
*
* @param {Array} data Two dimensional array - [ [A1, A2], [B1, B2] ]
* @see <a href='/cookbook/addingDataToAWorksheet.html'>Adding data to a worksheet</a>
*/
setData: function (data) {
this.data = data;
},
/**
* Merge cells in given range
*
* @param cell1 - A1, A2...
* @param cell2 - A2, A3...
*/
mergeCells: function(cell1, cell2) {
this.mergedCells.push([cell1, cell2]);
},
/**
* Added froze pane
* @param column - column number: 0, 1, 2 ...
* @param row - row number: 0, 1, 2 ...
* @param cell - 'A1'
* @deprecated
*/
freezePane: function(column, row, cell) {
this.sheetView.freezePane(column, row, cell);
},
/**
* Expects an array containing an object full of column format definitions.
* http://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.column.aspx
* bestFit
* collapsed
* customWidth
* hidden
* max
* min
* outlineLevel
* phonetic
* style
* width
* @param {Array} columnFormats
*/
setColumnFormats: function (columnFormats) {
this.columnFormats = columnFormats;
}
});
module.exports = Worksheet;