exceljs
Version:
Excel Workbook Manager - Read and Write xlsx and csv Files.
259 lines (236 loc) • 8.02 kB
JavaScript
const _ = require('../../../utils/under-dash');
const utils = require('../../../utils/utils');
const colCache = require('../../../utils/col-cache');
const BaseXform = require('../base-xform');
const Range = require('../../../doc/range');
function assign(definedName, attributes, name, defaultValue) {
const value = attributes[name];
if (value !== undefined) {
definedName[name] = value;
} else if (defaultValue !== undefined) {
definedName[name] = defaultValue;
}
}
function parseBool(value) {
switch (value) {
case '1':
case 'true':
return true;
default:
return false;
}
}
function assignBool(definedName, attributes, name, defaultValue) {
const value = attributes[name];
if (value !== undefined) {
definedName[name] = parseBool(value);
} else if (defaultValue !== undefined) {
definedName[name] = defaultValue;
}
}
function optimiseDataValidations(model) {
// Squeeze alike data validations together into rectangular ranges
// to reduce file size and speed up Excel load time
const dvList = _.map(model, (dataValidation, address) => ({
address,
dataValidation,
marked: false,
})).sort((a, b) => _.strcmp(a.address, b.address));
const dvMap = _.keyBy(dvList, 'address');
const matchCol = (addr, height, col) => {
for (let i = 0; i < height; i++) {
const otherAddress = colCache.encodeAddress(addr.row + i, col);
if (!model[otherAddress] || !_.isEqual(model[addr.address], model[otherAddress])) {
return false;
}
}
return true;
};
return dvList
.map(dv => {
if (!dv.marked) {
const addr = colCache.decodeAddress(dv.address);
// iterate downwards - finding matching cells
let height = 1;
let otherAddress = colCache.encodeAddress(addr.row + height, addr.col);
while (model[otherAddress] && _.isEqual(dv.dataValidation, model[otherAddress])) {
height++;
otherAddress = colCache.encodeAddress(addr.row + height, addr.col);
}
// iterate rightwards...
let width = 1;
while (matchCol(addr, height, addr.col + width)) {
width++;
}
// mark all included addresses
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
otherAddress = colCache.encodeAddress(addr.row + i, addr.col + j);
dvMap[otherAddress].marked = true;
}
}
if (height > 1 || width > 1) {
const bottom = addr.row + (height - 1);
const right = addr.col + (width - 1);
return {
...dv.dataValidation,
sqref: `${dv.address}:${colCache.encodeAddress(bottom, right)}`,
};
}
return {
...dv.dataValidation,
sqref: dv.address,
};
}
return null;
})
.filter(Boolean);
}
class DataValidationsXform extends BaseXform {
get tag() {
return 'dataValidations';
}
render(xmlStream, model) {
const optimizedModel = optimiseDataValidations(model);
if (optimizedModel.length) {
xmlStream.openNode('dataValidations', {count: optimizedModel.length});
optimizedModel.forEach(value => {
xmlStream.openNode('dataValidation');
if (value.type !== 'any') {
xmlStream.addAttribute('type', value.type);
if (value.operator && value.type !== 'list' && value.operator !== 'between') {
xmlStream.addAttribute('operator', value.operator);
}
if (value.allowBlank) {
xmlStream.addAttribute('allowBlank', '1');
}
}
if (value.showInputMessage) {
xmlStream.addAttribute('showInputMessage', '1');
}
if (value.promptTitle) {
xmlStream.addAttribute('promptTitle', value.promptTitle);
}
if (value.prompt) {
xmlStream.addAttribute('prompt', value.prompt);
}
if (value.showErrorMessage) {
xmlStream.addAttribute('showErrorMessage', '1');
}
if (value.errorStyle) {
xmlStream.addAttribute('errorStyle', value.errorStyle);
}
if (value.errorTitle) {
xmlStream.addAttribute('errorTitle', value.errorTitle);
}
if (value.error) {
xmlStream.addAttribute('error', value.error);
}
xmlStream.addAttribute('sqref', value.sqref);
(value.formulae || []).forEach((formula, index) => {
xmlStream.openNode(`formula${index + 1}`);
if (value.type === 'date') {
xmlStream.writeText(utils.dateToExcel(new Date(formula)));
} else {
xmlStream.writeText(formula);
}
xmlStream.closeNode();
});
xmlStream.closeNode();
});
xmlStream.closeNode();
}
}
parseOpen(node) {
switch (node.name) {
case 'dataValidations':
this.model = {};
return true;
case 'dataValidation': {
this._address = node.attributes.sqref;
const dataValidation = {type: node.attributes.type || 'any', formulae: []};
if (node.attributes.type) {
assignBool(dataValidation, node.attributes, 'allowBlank');
}
assignBool(dataValidation, node.attributes, 'showInputMessage');
assignBool(dataValidation, node.attributes, 'showErrorMessage');
switch (dataValidation.type) {
case 'any':
case 'list':
case 'custom':
break;
default:
assign(dataValidation, node.attributes, 'operator', 'between');
break;
}
assign(dataValidation, node.attributes, 'promptTitle');
assign(dataValidation, node.attributes, 'prompt');
assign(dataValidation, node.attributes, 'errorStyle');
assign(dataValidation, node.attributes, 'errorTitle');
assign(dataValidation, node.attributes, 'error');
this._dataValidation = dataValidation;
return true;
}
case 'formula1':
case 'formula2':
this._formula = [];
return true;
default:
return false;
}
}
parseText(text) {
if (this._formula) {
this._formula.push(text);
}
}
parseClose(name) {
switch (name) {
case 'dataValidations':
return false;
case 'dataValidation': {
if (!this._dataValidation.formulae || !this._dataValidation.formulae.length) {
delete this._dataValidation.formulae;
delete this._dataValidation.operator;
}
// The four known cases: 1. E4:L9 N4:U9 2.E4 L9 3. N4:U9 4. E4
const list = this._address.split(/\s+/g) || [];
list.forEach(addr => {
if (addr.includes(':')) {
const range = new Range(addr);
range.forEachAddress(address => {
this.model[address] = this._dataValidation;
});
} else {
this.model[addr] = this._dataValidation;
}
});
return true;
}
case 'formula1':
case 'formula2': {
let formula = this._formula.join('');
switch (this._dataValidation.type) {
case 'whole':
case 'textLength':
formula = parseInt(formula, 10);
break;
case 'decimal':
formula = parseFloat(formula);
break;
case 'date':
formula = utils.excelToDate(parseFloat(formula));
break;
default:
break;
}
this._dataValidation.formulae.push(formula);
this._formula = undefined;
return true;
}
default:
return true;
}
}
}
module.exports = DataValidationsXform;