@elbstack/xlsx-populate
Version:
Excel XLSX parser/generator written in JavaScript with Node.js and browser support, jQuery/d3-style method chaining, and a focus on keeping existing workbook features and styles in tact.
586 lines (482 loc) • 18.7 kB
JavaScript
"use strict";
/* eslint camelcase:off */
const ArgHandler = require("./ArgHandler");
const _ = require("lodash");
const xmlq = require("./xmlq");
const colorIndexes = require("./colorIndexes");
/**
* A style.
* @ignore
*/
class Style {
/**
* Creates a new instance of _Style.
* @constructor
* @param {StyleSheet} styleSheet - The styleSheet.
* @param {number} id - The style ID.
* @param {{}} xfNode - The xf node.
* @param {{}} fontNode - The font node.
* @param {{}} fillNode - The fill node.
* @param {{}} borderNode - The border node.
*/
constructor(styleSheet, id, xfNode, fontNode, fillNode, borderNode) {
this._styleSheet = styleSheet;
this._id = id;
this._xfNode = xfNode;
this._fontNode = fontNode;
this._fillNode = fillNode;
this._borderNode = borderNode;
}
/**
* Gets the style ID.
* @returns {number} The ID.
*/
id() {
return this._id;
}
/**
* Gets or sets a style.
* @param {string} name - The style name.
* @param {*} [value] - The value to set.
* @returns {*|Style} The value if getting or the style if setting.
*/
style() {
return new ArgHandler("_Style.style")
.case('string', name => {
const getterName = `_get_${name}`;
if (!this[getterName]) throw new Error(`_Style.style: '${name}' is not a valid style`);
return this[getterName]();
})
.case(['string', '*'], (name, value) => {
const setterName = `_set_${name}`;
if (!this[setterName]) throw new Error(`_Style.style: '${name}' is not a valid style`);
this[setterName](value);
return this;
})
.handle(arguments);
}
_getColor(node, name) {
const child = xmlq.findChild(node, name);
if (!child || !child.attributes) return;
const color = {};
if (child.attributes.hasOwnProperty('rgb')) color.rgb = child.attributes.rgb;
else if (child.attributes.hasOwnProperty('theme')) color.theme = child.attributes.theme;
else if (child.attributes.hasOwnProperty('indexed')) color.rgb = colorIndexes[child.attributes.indexed];
if (child.attributes.hasOwnProperty('tint')) color.tint = child.attributes.tint;
if (_.isEmpty(color)) return;
return color;
}
_setColor(node, name, color) {
if (typeof color === "string") color = { rgb: color };
else if (typeof color === "number") color = { theme: color };
xmlq.setChildAttributes(node, name, {
rgb: color && color.rgb && color.rgb.toUpperCase(),
indexed: null,
theme: color && color.theme,
tint: color && color.tint
});
xmlq.removeChildIfEmpty(node, 'color');
}
_get_bold() {
return xmlq.hasChild(this._fontNode, 'b');
}
_set_bold(bold) {
if (bold) xmlq.appendChildIfNotFound(this._fontNode, "b");
else xmlq.removeChild(this._fontNode, 'b');
}
_get_italic() {
return xmlq.hasChild(this._fontNode, 'i');
}
_set_italic(italic) {
if (italic) xmlq.appendChildIfNotFound(this._fontNode, "i");
else xmlq.removeChild(this._fontNode, 'i');
}
_get_underline() {
const uNode = xmlq.findChild(this._fontNode, 'u');
return uNode ? uNode.attributes.val || true : false;
}
_set_underline(underline) {
if (underline) {
const uNode = xmlq.appendChildIfNotFound(this._fontNode, "u");
const val = typeof underline === 'string' ? underline : null;
xmlq.setAttributes(uNode, { val });
} else {
xmlq.removeChild(this._fontNode, 'u');
}
}
_get_strikethrough() {
return xmlq.hasChild(this._fontNode, 'strike');
}
_set_strikethrough(strikethrough) {
if (strikethrough) xmlq.appendChildIfNotFound(this._fontNode, "strike");
else xmlq.removeChild(this._fontNode, 'strike');
}
_getFontVerticalAlignment() {
return xmlq.getChildAttribute(this._fontNode, 'vertAlign', "val");
}
_setFontVerticalAlignment(alignment) {
xmlq.setChildAttributes(this._fontNode, 'vertAlign', { val: alignment });
xmlq.removeChildIfEmpty(this._fontNode, 'vertAlign');
}
_get_subscript() {
return this._getFontVerticalAlignment() === "subscript";
}
_set_subscript(subscript) {
this._setFontVerticalAlignment(subscript ? "subscript" : null);
}
_get_superscript() {
return this._getFontVerticalAlignment() === "superscript";
}
_set_superscript(superscript) {
this._setFontVerticalAlignment(superscript ? "superscript" : null);
}
_get_fontSize() {
return xmlq.getChildAttribute(this._fontNode, 'sz', "val");
}
_set_fontSize(size) {
xmlq.setChildAttributes(this._fontNode, 'sz', { val: size });
xmlq.removeChildIfEmpty(this._fontNode, 'sz');
}
_get_fontFamily() {
return xmlq.getChildAttribute(this._fontNode, 'name', "val");
}
_set_fontFamily(family) {
xmlq.setChildAttributes(this._fontNode, 'name', { val: family });
xmlq.removeChildIfEmpty(this._fontNode, 'name');
}
_get_fontColor() {
return this._getColor(this._fontNode, "color");
}
_set_fontColor(color) {
this._setColor(this._fontNode, "color", color);
}
_get_horizontalAlignment() {
return xmlq.getChildAttribute(this._xfNode, 'alignment', "horizontal");
}
_set_horizontalAlignment(alignment) {
xmlq.setChildAttributes(this._xfNode, 'alignment', { horizontal: alignment });
xmlq.removeChildIfEmpty(this._xfNode, 'alignment');
}
_get_justifyLastLine() {
return xmlq.getChildAttribute(this._xfNode, 'alignment', "justifyLastLine") === 1;
}
_set_justifyLastLine(justifyLastLine) {
xmlq.setChildAttributes(this._xfNode, 'alignment', { justifyLastLine: justifyLastLine ? 1 : null });
xmlq.removeChildIfEmpty(this._xfNode, 'alignment');
}
_get_indent() {
return xmlq.getChildAttribute(this._xfNode, 'alignment', "indent");
}
_set_indent(indent) {
xmlq.setChildAttributes(this._xfNode, 'alignment', { indent });
xmlq.removeChildIfEmpty(this._xfNode, 'alignment');
}
_get_verticalAlignment() {
return xmlq.getChildAttribute(this._xfNode, 'alignment', "vertical");
}
_set_verticalAlignment(alignment) {
xmlq.setChildAttributes(this._xfNode, 'alignment', { vertical: alignment });
xmlq.removeChildIfEmpty(this._xfNode, 'alignment');
}
_get_wrapText() {
return xmlq.getChildAttribute(this._xfNode, 'alignment', "wrapText") === 1;
}
_set_wrapText(wrapText) {
xmlq.setChildAttributes(this._xfNode, 'alignment', { wrapText: wrapText ? 1 : null });
xmlq.removeChildIfEmpty(this._xfNode, 'alignment');
}
_get_shrinkToFit() {
return xmlq.getChildAttribute(this._xfNode, 'alignment', "shrinkToFit") === 1;
}
_set_shrinkToFit(shrinkToFit) {
xmlq.setChildAttributes(this._xfNode, 'alignment', { shrinkToFit: shrinkToFit ? 1 : null });
xmlq.removeChildIfEmpty(this._xfNode, 'alignment');
}
_get_textDirection() {
const readingOrder = xmlq.getChildAttribute(this._xfNode, 'alignment', "readingOrder");
if (readingOrder === 1) return "left-to-right";
if (readingOrder === 2) return "right-to-left";
return readingOrder;
}
_set_textDirection(textDirection) {
let readingOrder;
if (textDirection === "left-to-right") readingOrder = 1;
else if (textDirection === "right-to-left") readingOrder = 2;
xmlq.setChildAttributes(this._xfNode, 'alignment', { readingOrder });
xmlq.removeChildIfEmpty(this._xfNode, 'alignment');
}
_getTextRotation() {
return xmlq.getChildAttribute(this._xfNode, 'alignment', "textRotation");
}
_setTextRotation(textRotation) {
xmlq.setChildAttributes(this._xfNode, 'alignment', { textRotation });
xmlq.removeChildIfEmpty(this._xfNode, 'alignment');
}
_get_textRotation() {
let textRotation = this._getTextRotation();
// Negative angles in Excel correspond to values > 90 in OOXML.
if (textRotation > 90) textRotation = 90 - textRotation;
return textRotation;
}
_set_textRotation(textRotation) {
// Negative angles in Excel correspond to values > 90 in OOXML.
if (textRotation < 0) textRotation = 90 - textRotation;
this._setTextRotation(textRotation);
}
_get_angleTextCounterclockwise() {
return this._getTextRotation() === 45;
}
_set_angleTextCounterclockwise(value) {
this._setTextRotation(value ? 45 : null);
}
_get_angleTextClockwise() {
return this._getTextRotation() === 135;
}
_set_angleTextClockwise(value) {
this._setTextRotation(value ? 135 : null);
}
_get_rotateTextUp() {
return this._getTextRotation() === 90;
}
_set_rotateTextUp(value) {
this._setTextRotation(value ? 90 : null);
}
_get_rotateTextDown() {
return this._getTextRotation() === 180;
}
_set_rotateTextDown(value) {
this._setTextRotation(value ? 180 : null);
}
_get_verticalText() {
return this._getTextRotation() === 255;
}
_set_verticalText(value) {
this._setTextRotation(value ? 255 : null);
}
_get_fill() {
const patternFillNode = xmlq.findChild(this._fillNode, 'patternFill');// jq.get(this._fillNode, "patternFill[0]");
const gradientFillNode = xmlq.findChild(this._fillNode, 'gradientFill');// jq.get(this._fillNode, "gradientFill[0]");
const patternType = patternFillNode && patternFillNode.attributes.patternType;// jq.get(patternFillNode, "$.patternType");
if (patternType === "solid") {
return {
type: "solid",
color: this._getColor(patternFillNode, "fgColor")
};
}
if (patternType) {
return {
type: "pattern",
pattern: patternType,
foreground: this._getColor(patternFillNode, "fgColor"),
background: this._getColor(patternFillNode, "bgColor")
};
}
if (gradientFillNode) {
const gradientType = gradientFillNode.attributes.type || "linear";
const fill = {
type: "gradient",
gradientType,
stops: _.map(gradientFillNode.children, stop => ({
position: stop.attributes.position,
color: this._getColor(stop, "color")
}))
};
if (gradientType === "linear") {
fill.angle = gradientFillNode.attributes.degree;
} else {
fill.left = gradientFillNode.attributes.left;
fill.right = gradientFillNode.attributes.right;
fill.top = gradientFillNode.attributes.top;
fill.bottom = gradientFillNode.attributes.bottom;
}
return fill;
}
}
_set_fill(fill) {
this._fillNode.children = [];
// No fill
if (_.isNil(fill)) return;
// Pattern fill
if (fill.type === "pattern") {
const patternFill = {
name: 'patternFill',
attributes: { patternType: fill.pattern },
children: []
};
this._fillNode.children.push(patternFill);
this._setColor(patternFill, "fgColor", fill.foreground);
this._setColor(patternFill, "bgColor", fill.background);
return;
}
// Gradient fill
if (fill.type === "gradient") {
const gradientFill = { name: 'gradientFill', attributes: {}, children: [] };
this._fillNode.children.push(gradientFill);
xmlq.setAttributes(gradientFill, {
type: fill.gradientType === "path" ? "path" : undefined,
left: fill.left,
right: fill.right,
top: fill.top,
bottom: fill.bottom,
degree: fill.angle
});
_.forEach(fill.stops, (fillStop, i) => {
const stop = {
name: 'stop',
attributes: { position: fillStop.position },
children: []
};
gradientFill.children.push(stop);
this._setColor(stop, 'color', fillStop.color);
});
return;
}
// Solid fill (really a pattern fill with a solid pattern type).
if (!_.isObject(fill)) fill = { type: "solid", color: fill };
else if (fill.hasOwnProperty('rgb') || fill.hasOwnProperty("theme")) fill = { color: fill };
const patternFill = {
name: 'patternFill',
attributes: { patternType: 'solid' }
};
this._fillNode.children.push(patternFill);
this._setColor(patternFill, "fgColor", fill.color);
}
_getBorder() {
const result = {};
["left", "right", "top", "bottom", "diagonal"].forEach(side => {
const sideNode = xmlq.findChild(this._borderNode, side);
const sideResult = {};
const style = xmlq.getChildAttribute(this._borderNode, side, 'style');
if (style) sideResult.style = style;
const color = this._getColor(sideNode, 'color');
if (color) sideResult.color = color;
if (side === "diagonal") {
const up = this._borderNode.attributes.diagonalUp;
const down = this._borderNode.attributes.diagonalDown;
let direction;
if (up && down) direction = "both";
else if (up) direction = "up";
else if (down) direction = "down";
if (direction) sideResult.direction = direction;
}
if (!_.isEmpty(sideResult)) result[side] = sideResult;
});
return result;
}
_setBorder(settings) {
_.forOwn(settings, (setting, side) => {
if (typeof setting === "boolean") {
setting = { style: setting ? "thin" : null };
} else if (typeof setting === "string") {
setting = { style: setting };
} else if (setting === null || setting === undefined) {
setting = { style: null, color: null, direction: null };
}
if (setting.hasOwnProperty("style")) {
xmlq.setChildAttributes(this._borderNode, side, { style: setting.style });
}
if (setting.hasOwnProperty("color")) {
const sideNode = xmlq.findChild(this._borderNode, side);
this._setColor(sideNode, "color", setting.color);
}
if (side === "diagonal") {
xmlq.setAttributes(this._borderNode, {
diagonalUp: setting.direction === "up" || setting.direction === "both" ? 1 : null,
diagonalDown: setting.direction === "down" || setting.direction === "both" ? 1 : null
});
}
});
}
_get_border() {
return this._getBorder();
}
_set_border(settings) {
if (_.isObject(settings) && !settings.hasOwnProperty("style") && !settings.hasOwnProperty("color")) {
settings = _.defaults(settings, {
left: null,
right: null,
top: null,
bottom: null,
diagonal: null
});
this._setBorder(settings);
} else {
this._setBorder({
left: settings,
right: settings,
top: settings,
bottom: settings
});
}
}
_get_borderColor() {
return _.mapValues(this._getBorder(), value => value.color);
}
_set_borderColor(color) {
if (_.isObject(color)) {
this._setBorder(_.mapValues(color, color => ({ color })));
} else {
this._setBorder({
left: { color },
right: { color },
top: { color },
bottom: { color },
diagonal: { color }
});
}
}
_get_borderStyle() {
return _.mapValues(this._getBorder(), value => value.style);
}
_set_borderStyle(style) {
if (_.isObject(style)) {
this._setBorder(_.mapValues(style, style => ({ style })));
} else {
this._setBorder({
left: { style },
right: { style },
top: { style },
bottom: { style }
});
}
}
_get_diagonalBorderDirection() {
const border = this._getBorder().diagonal;
return border && border.direction;
}
_set_diagonalBorderDirection(direction) {
this._setBorder({ diagonal: { direction } });
}
_get_numberFormat() {
const numFmtId = this._xfNode.attributes.numFmtId || 0;
return this._styleSheet.getNumberFormatCode(numFmtId);
}
_set_numberFormat(formatCode) {
this._xfNode.attributes.numFmtId = this._styleSheet.getNumberFormatId(formatCode);
}
}
["left", "right", "top", "bottom", "diagonal"].forEach(side => {
Style.prototype[`_get_${side}Border`] = function () {
return this._getBorder()[side];
};
Style.prototype[`_set_${side}Border`] = function (settings) {
this._setBorder({ [side]: settings });
};
Style.prototype[`_get_${side}BorderColor`] = function () {
const border = this._getBorder()[side];
return border && border.color;
};
Style.prototype[`_set_${side}BorderColor`] = function (color) {
this._setBorder({ [side]: { color } });
};
Style.prototype[`_get_${side}BorderStyle`] = function () {
const border = this._getBorder()[side];
return border && border.style;
};
Style.prototype[`_set_${side}BorderStyle`] = function (style) {
this._setBorder({ [side]: { style } });
};
});
// IE doesn't support function names so explicitly set it.
if (!Style.name) Style.name = "Style";
module.exports = Style;