excel-builder-vanilla
Version:
An easy way of building Excel files with javascript
706 lines (655 loc) • 21.2 kB
text/typescript
import type { ExcelFontStyle, ExcelStyleInstruction } from '../interfaces.js';
import { isObject, isString } from '../utilities/isTypeOf.js';
import { pick } from '../utilities/pick.js';
import { uniqueId } from '../utilities/uniqueId.js';
import { Util } from './Util.js';
import type { XMLDOM } from './XMLDOM.js';
/**
* @module Excel/StyleSheet
*/
export class StyleSheet {
id = uniqueId('StyleSheet');
cellStyles = [
{
name: 'Normal',
xfId: '0',
builtinId: '0',
},
];
defaultTableStyle = false;
differentialStyles: any[] = [{}];
masterCellFormats: any[] = [
{
numFmtId: 0,
fontId: 0,
fillId: 0,
borderId: 0,
xfid: 0,
},
];
masterCellStyles: any[] = [
{
numFmtId: 0,
fontId: 0,
fillId: 0,
borderId: 0,
},
];
fonts: ExcelFontStyle[] = [{}];
numberFormatters: any[] = [];
fills: any[] = [
{},
{
type: 'pattern',
patternType: 'gray125',
fgColor: 'FF333333',
bgColor: 'FF333333',
},
];
borders: any[] = [
{
top: {},
left: {},
right: {},
bottom: {},
diagonal: {},
},
];
tableStyles: any[] = [];
createSimpleFormatter(type: string) {
const sid = this.masterCellFormats.length;
const style: { [id: string]: number } = {
id: sid,
};
switch (type) {
case 'date':
style.numFmtId = 14;
break;
}
this.masterCellFormats.push(style);
return style;
}
createFill(fillInstructions: any) {
const id = this.fills.length;
const fill = fillInstructions;
fill.id = id;
this.fills.push(fill);
return fill;
}
createNumberFormatter(formatInstructions: any) {
const id = this.numberFormatters.length + 100;
const format = {
id: id,
formatCode: formatInstructions,
};
this.numberFormatters.push(format);
return format;
}
/**
* alignment: {
* horizontal: http://www.schemacentral.com/sc/ooxml/t-ssml_ST_HorizontalAlignment.html
* vertical: http://www.schemacentral.com/sc/ooxml/t-ssml_ST_VerticalAlignment.html
* @param {Object} styleInstructions
*/
createFormat(styleInstructions: ExcelStyleInstruction) {
const sid = this.masterCellFormats.length;
const style: any = {
id: sid,
};
if (styleInstructions.protection) {
style.protection = styleInstructions.protection;
}
if (styleInstructions.font && isObject(styleInstructions.font)) {
style.fontId = this.createFontStyle(styleInstructions.font).id;
} else if (styleInstructions.font) {
if (Number.isNaN(Number.parseInt(styleInstructions.font, 10))) {
throw new Error('Passing a non-numeric font id is not supported');
}
style.fontId = styleInstructions.font;
}
if (styleInstructions.format && isString(styleInstructions.format)) {
style.numFmtId = this.createNumberFormatter(styleInstructions.format).id;
} else if (styleInstructions.format) {
if (Number.isNaN(Number.parseInt(styleInstructions.format, 10))) {
throw new Error('Invalid number formatter id');
}
style.numFmtId = styleInstructions.format;
}
if (styleInstructions.border && isObject(styleInstructions.border)) {
style.borderId = this.createBorderFormatter(styleInstructions.border).id;
} else if (styleInstructions.border) {
if (Number.isNaN(Number.parseInt(styleInstructions.border, 10))) {
throw new Error('Passing a non-numeric border id is not supported');
}
style.borderId = styleInstructions.border;
}
if (styleInstructions.fill && isObject(styleInstructions.fill)) {
style.fillId = this.createFill(styleInstructions.fill).id;
} else if (styleInstructions.fill) {
if (Number.isNaN(Number.parseInt(styleInstructions.fill, 10))) {
throw new Error('Passing a non-numeric fill id is not supported');
}
style.fillId = styleInstructions.fill;
}
if (styleInstructions.alignment && isObject(styleInstructions.alignment)) {
style.alignment = pick(styleInstructions.alignment, [
'horizontal',
'justifyLastLine',
'readingOrder',
'relativeIndent',
'shrinkToFit',
'textRotation',
'vertical',
'wrapText',
]);
}
this.masterCellFormats.push(style);
return style;
}
createDifferentialStyle(styleInstructions: ExcelStyleInstruction) {
const id = this.differentialStyles.length;
const style: ExcelStyleInstruction = {
id,
};
if (styleInstructions.font && isObject(styleInstructions.font)) {
style.font = styleInstructions.font;
}
if (styleInstructions.border && isObject(styleInstructions.border)) {
style.border = Object.assign(
{
top: {},
left: {},
right: {},
bottom: {},
diagonal: {},
},
styleInstructions.border,
);
}
if (styleInstructions.fill && isObject(styleInstructions.fill)) {
style.fill = styleInstructions.fill;
}
if (styleInstructions.alignment && isObject(styleInstructions.alignment)) {
style.alignment = styleInstructions.alignment;
}
if (styleInstructions.format && isString(styleInstructions.format)) {
style.numFmt = styleInstructions.format;
}
this.differentialStyles[id] = style;
return style;
}
/**
* Should be an object containing keys that match with one of the keys from this list:
* http://www.schemacentral.com/sc/ooxml/t-ssml_ST_TableStyleType.html
*
* The value should be a reference to a differential format (dxf)
* @param {Object} instructions
*/
createTableStyle(instructions: any) {
this.tableStyles.push(instructions);
}
/**
* All params optional
* Expects: {
* top: {},
* left: {},
* right: {},
* bottom: {},
* diagonal: {},
* outline: boolean,
* diagonalUp: boolean,
* diagonalDown: boolean
* }
* Each border should follow:
* {
* style: styleString, http://www.schemacentral.com/sc/ooxml/t-ssml_ST_BorderStyle.html
* color: ARBG color (requires the A, so for example FF006666)
* }
* @param {Object} border
*/
createBorderFormatter(border: any) {
border = { top: {}, left: {}, right: {}, bottom: {}, diagonal: {}, id: this.borders.length, ...border };
this.borders.push(border);
return border;
}
/**
* Supported font styles:
* bold
* italic
* underline (single, double, singleAccounting, doubleAccounting)
* size
* color
* fontName
* strike (strikethrough)
* outline (does this actually do anything?)
* shadow (does this actually do anything?)
* superscript
* subscript
*
* Color is a future goal - at the moment it's looking a bit complicated
* @param {Object} instructions
*/
createFontStyle(instructions: ExcelFontStyle) {
const fontId = this.fonts.length;
const fontStyle: any = {
id: fontId,
};
if (instructions.bold) {
fontStyle.bold = true;
}
if (instructions.italic) {
fontStyle.italic = true;
}
if (instructions.superscript) {
fontStyle.vertAlign = 'superscript';
}
if (instructions.subscript) {
fontStyle.vertAlign = 'subscript';
}
if (instructions.underline) {
if (
typeof instructions.underline === 'string' &&
['double', 'singleAccounting', 'doubleAccounting'].includes(instructions.underline)
) {
fontStyle.underline = instructions.underline;
} else {
fontStyle.underline = true;
}
}
if (instructions.strike) {
fontStyle.strike = true;
}
if (instructions.outline) {
fontStyle.outline = true;
}
if (instructions.shadow) {
fontStyle.shadow = true;
}
if (instructions.size) {
fontStyle.size = instructions.size;
}
if (instructions.color) {
fontStyle.color = instructions.color;
}
if (instructions.fontName) {
fontStyle.fontName = instructions.fontName;
}
this.fonts.push(fontStyle);
return fontStyle;
}
exportBorders(doc: XMLDOM) {
const borders = doc.createElement('borders');
borders.setAttribute('count', this.borders.length);
for (let i = 0, l = this.borders.length; i < l; i++) {
borders.appendChild(this.exportBorder(doc, this.borders[i]));
}
return borders;
}
exportBorder(doc: XMLDOM, data: any) {
const border = doc.createElement('border');
const borderGenerator = (name: string) => {
const b = doc.createElement(name);
if (data[name].style) {
b.setAttribute('style', data[name].style);
}
if (data[name].color) {
b.appendChild(this.exportColor(doc, data[name].color));
}
return b;
};
border.appendChild(borderGenerator('left'));
border.appendChild(borderGenerator('right'));
border.appendChild(borderGenerator('top'));
border.appendChild(borderGenerator('bottom'));
border.appendChild(borderGenerator('diagonal'));
return border;
}
exportColor(doc: XMLDOM, color: any) {
const colorEl = doc.createElement('color');
if (isString(color)) {
colorEl.setAttribute('rgb', color);
return colorEl;
}
if (color.tint !== undefined) {
colorEl.setAttribute('tint', color.tint);
}
if (color.auto !== undefined) {
colorEl.setAttribute('auto', String(!!color.auto));
}
if (color.theme !== undefined) {
colorEl.setAttribute('theme', color.theme);
}
return colorEl;
}
exportMasterCellFormats(doc: XMLDOM) {
const cellFormats = Util.createElement(doc, 'cellXfs', [['count', this.masterCellFormats.length]]);
for (let i = 0, l = this.masterCellFormats.length; i < l; i++) {
const mformat = this.masterCellFormats[i];
cellFormats.appendChild(this.exportCellFormatElement(doc, mformat));
}
return cellFormats;
}
exportMasterCellStyles(doc: XMLDOM) {
const records = Util.createElement(doc, 'cellStyleXfs', [['count', this.masterCellStyles.length]]);
for (let i = 0, l = this.masterCellStyles.length; i < l; i++) {
const mstyle = this.masterCellStyles[i];
records.appendChild(this.exportCellFormatElement(doc, mstyle));
}
return records;
}
exportCellFormatElement(doc: XMLDOM, styleInstructions: ExcelStyleInstruction) {
const xf = doc.createElement('xf');
const allowed = [
'applyAlignment',
'applyBorder',
'applyFill',
'applyFont',
'applyNumberFormat',
'applyProtection',
'borderId',
'fillId',
'fontId',
'numFmtId',
'pivotButton',
'quotePrefix',
'xfId',
];
const attributes: any = Object.keys(styleInstructions).filter(key => allowed.indexOf(key) !== -1);
if (styleInstructions.alignment) {
const alignmentData = styleInstructions.alignment;
xf.appendChild(this.exportAlignment(doc, alignmentData));
}
if (styleInstructions.protection) {
xf.appendChild(this.exportProtection(doc, styleInstructions.protection));
xf.setAttribute('applyProtection', '1');
}
let a = attributes.length;
while (a--) {
xf.setAttribute(attributes[a], styleInstructions[attributes[a] as keyof ExcelStyleInstruction]);
}
if (styleInstructions.fillId) {
xf.setAttribute('applyFill', '1');
}
if (styleInstructions.fontId) {
xf.setAttribute('applyFont', '1');
}
if (styleInstructions.borderId) {
xf.setAttribute('applyBorder', '1');
}
if (styleInstructions.alignment) {
xf.setAttribute('applyAlignment', '1');
}
if (styleInstructions.numFmtId) {
xf.setAttribute('applyNumberFormat', '1');
}
if (styleInstructions.numFmtId !== undefined && styleInstructions.xfId === undefined) {
xf.setAttribute('xfId', '0');
}
return xf;
}
exportAlignment(doc: XMLDOM, alignmentData: any) {
const alignment = doc.createElement('alignment');
const someKeys = Object.keys(alignmentData);
for (let i = 0, l = someKeys.length; i < l; i++) {
alignment.setAttribute(someKeys[i], alignmentData[someKeys[i]]);
}
return alignment;
}
exportFonts(doc: XMLDOM) {
const fonts = doc.createElement('fonts');
fonts.setAttribute('count', String(this.fonts.length));
for (let i = 0, l = this.fonts.length; i < l; i++) {
const fd = this.fonts[i];
fonts.appendChild(this.exportFont(doc, fd));
}
return fonts;
}
exportFont(doc: XMLDOM, fd: any) {
const font = doc.createElement('font');
if (fd.size) {
const size = doc.createElement('sz');
size.setAttribute('val', fd.size);
font.appendChild(size);
}
if (fd.fontName) {
const fontName = doc.createElement('name');
fontName.setAttribute('val', fd.fontName);
font.appendChild(fontName);
}
if (fd.bold) {
font.appendChild(doc.createElement('b'));
}
if (fd.italic) {
font.appendChild(doc.createElement('i'));
}
if (fd.vertAlign) {
const vertAlign = doc.createElement('vertAlign');
vertAlign.setAttribute('val', fd.vertAlign);
font.appendChild(vertAlign);
}
if (fd.underline) {
const u = doc.createElement('u');
if (fd.underline !== true) {
u.setAttribute('val', fd.underline);
}
font.appendChild(u);
}
if (fd.strike) {
font.appendChild(doc.createElement('strike'));
}
if (fd.shadow) {
font.appendChild(doc.createElement('shadow'));
}
if (fd.outline) {
font.appendChild(doc.createElement('outline'));
}
if (fd.color) {
font.appendChild(this.exportColor(doc, fd.color));
}
return font;
}
exportFills(doc: XMLDOM) {
const fills = doc.createElement('fills');
fills.setAttribute('count', String(this.fills.length));
for (let i = 0, l = this.fills.length; i < l; i++) {
const fd = this.fills[i];
fills.appendChild(this.exportFill(doc, fd));
}
return fills;
}
exportFill(doc: XMLDOM, fd: any) {
let fillDef: any;
const fill = doc.createElement('fill');
if (fd.type === 'pattern') {
fillDef = this.exportPatternFill(doc, fd);
fill.appendChild(fillDef);
} else if (fd.type === 'gradient') {
fillDef = this.exportGradientFill(doc, fd);
fill.appendChild(fillDef);
}
return fill;
}
exportGradientFill(doc: XMLDOM, data: any) {
const fillDef = doc.createElement('gradientFill');
if (data.degree) {
fillDef.setAttribute('degree', data.degree);
} else if (data.left) {
fillDef.setAttribute('left', data.left);
fillDef.setAttribute('right', data.right);
fillDef.setAttribute('top', data.top);
fillDef.setAttribute('bottom', data.bottom);
}
const start = doc.createElement('stop');
start.setAttribute('position', data.start.pureAt || 0);
const startColor = doc.createElement('color');
if (typeof data.start === 'string' || data.start.color) {
startColor.setAttribute('rgb', data.start.color || data.start);
} else if (data.start.theme) {
startColor.setAttribute('theme', data.start.theme);
}
const end = doc.createElement('stop');
const endColor = doc.createElement('color');
end.setAttribute('position', data.end.pureAt || 1);
if (typeof data.start === 'string' || data.end.color) {
endColor.setAttribute('rgb', data.end.color || data.end);
} else if (data.end.theme) {
endColor.setAttribute('theme', data.end.theme);
}
start.appendChild(startColor);
end.appendChild(endColor);
fillDef.appendChild(start);
fillDef.appendChild(end);
return fillDef;
}
/**
* Pattern types: http://www.schemacentral.com/sc/ooxml/t-ssml_ST_PatternType.html
* @param {XMLDoc} doc
* @param {Object} data
*/
exportPatternFill(doc: XMLDOM, data: any) {
const fillDef = Util.createElement(doc, 'patternFill', [['patternType', data.patternType]]);
if (!data.bgColor) {
data.bgColor = 'FFFFFFFF';
}
if (!data.fgColor) {
data.fgColor = 'FFFFFFFF';
}
const bgColor = doc.createElement('bgColor');
if (isString(data.bgColor)) {
bgColor.setAttribute('rgb', data.bgColor);
} else {
if (data.bgColor.theme) {
bgColor.setAttribute('theme', data.bgColor.theme);
} else {
bgColor.setAttribute('rgb', data.bgColor.rbg);
}
}
const fgColor = doc.createElement('fgColor');
if (isString(data.fgColor)) {
fgColor.setAttribute('rgb', data.fgColor);
} else {
if (data.fgColor.theme) {
fgColor.setAttribute('theme', data.fgColor.theme);
} else {
fgColor.setAttribute('rgb', data.fgColor.rbg);
}
}
fillDef.appendChild(fgColor);
fillDef.appendChild(bgColor);
return fillDef;
}
exportNumberFormatters(doc: XMLDOM) {
const formatters = doc.createElement('numFmts');
formatters.setAttribute('count', String(this.numberFormatters.length));
for (let i = 0, l = this.numberFormatters.length; i < l; i++) {
const fd = this.numberFormatters[i];
formatters.appendChild(this.exportNumberFormatter(doc, fd));
}
return formatters;
}
exportNumberFormatter(doc: XMLDOM, fd: any) {
const numFmt = doc.createElement('numFmt');
numFmt.setAttribute('numFmtId', fd.id);
numFmt.setAttribute('formatCode', fd.formatCode);
return numFmt;
}
exportCellStyles(doc: XMLDOM) {
const cellStyles = doc.createElement('cellStyles');
cellStyles.setAttribute('count', String(this.cellStyles.length));
for (let i = 0, l = this.cellStyles.length; i < l; i++) {
const style: any = this.cellStyles[i];
delete style.id; // Remove internal id
const record = Util.createElement(doc, 'cellStyle');
cellStyles.appendChild(record);
const attributes = Object.keys(style);
let a = attributes.length;
while (a--) {
record.setAttribute(attributes[a], style[attributes[a]]);
}
}
return cellStyles;
}
exportDifferentialStyles(doc: XMLDOM) {
const dxfs = doc.createElement('dxfs');
dxfs.setAttribute('count', String(this.differentialStyles.length));
for (let i = 0, l = this.differentialStyles.length; i < l; i++) {
const style = this.differentialStyles[i];
dxfs.appendChild(this.exportDFX(doc, style));
}
return dxfs;
}
exportDFX(doc: XMLDOM, style: any) {
const dxf = doc.createElement('dxf');
if (style.font) {
dxf.appendChild(this.exportFont(doc, style.font));
}
if (style.fill) {
dxf.appendChild(this.exportFill(doc, style.fill));
}
if (style.border) {
dxf.appendChild(this.exportBorder(doc, style.border));
}
if (style.numFmt) {
dxf.appendChild(this.exportNumberFormatter(doc, style.numFmt));
}
if (style.alignment) {
dxf.appendChild(this.exportAlignment(doc, style.alignment));
}
return dxf;
}
exportTableStyles(doc: XMLDOM) {
const tableStyles = doc.createElement('tableStyles');
tableStyles.setAttribute('count', String(this.tableStyles.length));
if (this.defaultTableStyle) {
tableStyles.setAttribute('defaultTableStyle', String(this.defaultTableStyle));
}
for (let i = 0, l = this.tableStyles.length; i < l; i++) {
tableStyles.appendChild(this.exportTableStyle(doc, this.tableStyles[i]));
}
return tableStyles;
}
exportTableStyle(doc: XMLDOM, style: { name: string; wholeTable?: number; headerRow?: number }) {
const tableStyle = doc.createElement('tableStyle');
tableStyle.setAttribute('name', style.name);
tableStyle.setAttribute('pivot', String(0));
let i = 0;
Object.entries(style).forEach(([key, value]) => {
if (key === 'name') {
return;
}
i++;
const styleEl = doc.createElement('tableStyleElement');
styleEl.setAttribute('type', key);
styleEl.setAttribute('dxfId', value);
tableStyle.appendChild(styleEl);
});
tableStyle.setAttribute('count', String(i));
return tableStyle;
}
exportProtection(doc: XMLDOM, protectionData: any) {
const node = doc.createElement('protection');
// eslint-disable-next-line no-restricted-syntax
for (const k in protectionData) {
if (k in protectionData) {
node.setAttribute(k, protectionData[k]);
}
}
return node;
}
toXML() {
const doc = Util.createXmlDoc(Util.schemas.spreadsheetml, 'styleSheet');
const styleSheet = doc.documentElement;
styleSheet.appendChild(this.exportNumberFormatters(doc));
styleSheet.appendChild(this.exportFonts(doc));
styleSheet.appendChild(this.exportFills(doc));
styleSheet.appendChild(this.exportBorders(doc));
styleSheet.appendChild(this.exportMasterCellStyles(doc));
styleSheet.appendChild(this.exportMasterCellFormats(doc));
styleSheet.appendChild(this.exportCellStyles(doc));
styleSheet.appendChild(this.exportDifferentialStyles(doc));
if (this.tableStyles.length) {
styleSheet.appendChild(this.exportTableStyles(doc));
}
return doc;
}
}