fix: 修复中介导入成功条数计算错误

问题:
- 导入成功条数显示为负数
- 原因:成功数量计算使用 validRecords.size() - failures.size()
- 但没有使用实际的数据库操作返回值

修复:
- saveBatchWithUpsert 和 saveBatch 方法现在返回 int
- 累加实际的数据库影响行数
- 使用 actualSuccessCount 变量跟踪真实成功数量

影响范围:
- CcdiIntermediaryPersonImportServiceImpl
- CcdiIntermediaryEntityImportServiceImpl
This commit is contained in:
wkc
2026-02-08 17:18:18 +08:00
parent bb0d68c41d
commit 5ec5913759
2058 changed files with 234134 additions and 269 deletions

View File

@@ -0,0 +1,91 @@
'use strict';
const colCache = require('../utils/col-cache');
class Anchor {
constructor(worksheet, address, offset = 0) {
this.worksheet = worksheet;
if (!address) {
this.nativeCol = 0;
this.nativeColOff = 0;
this.nativeRow = 0;
this.nativeRowOff = 0;
} else if (typeof address === 'string') {
const decoded = colCache.decodeAddress(address);
this.nativeCol = decoded.col + offset;
this.nativeColOff = 0;
this.nativeRow = decoded.row + offset;
this.nativeRowOff = 0;
} else if (address.nativeCol !== undefined) {
this.nativeCol = address.nativeCol || 0;
this.nativeColOff = address.nativeColOff || 0;
this.nativeRow = address.nativeRow || 0;
this.nativeRowOff = address.nativeRowOff || 0;
} else if (address.col !== undefined) {
this.col = address.col + offset;
this.row = address.row + offset;
} else {
this.nativeCol = 0;
this.nativeColOff = 0;
this.nativeRow = 0;
this.nativeRowOff = 0;
}
}
static asInstance(model) {
return model instanceof Anchor || model == null ? model : new Anchor(model);
}
get col() {
return this.nativeCol + (Math.min(this.colWidth - 1, this.nativeColOff) / this.colWidth);
}
set col(v) {
this.nativeCol = Math.floor(v);
this.nativeColOff = Math.floor((v - this.nativeCol) * this.colWidth);
}
get row() {
return this.nativeRow + (Math.min(this.rowHeight - 1, this.nativeRowOff) / this.rowHeight);
}
set row(v) {
this.nativeRow = Math.floor(v);
this.nativeRowOff = Math.floor((v - this.nativeRow) * this.rowHeight);
}
get colWidth() {
return this.worksheet &&
this.worksheet.getColumn(this.nativeCol + 1) &&
this.worksheet.getColumn(this.nativeCol + 1).isCustomWidth
? Math.floor(this.worksheet.getColumn(this.nativeCol + 1).width * 10000)
: 640000;
}
get rowHeight() {
return this.worksheet &&
this.worksheet.getRow(this.nativeRow + 1) &&
this.worksheet.getRow(this.nativeRow + 1).height
? Math.floor(this.worksheet.getRow(this.nativeRow + 1).height * 10000)
: 180000;
}
get model() {
return {
nativeCol: this.nativeCol,
nativeColOff: this.nativeColOff,
nativeRow: this.nativeRow,
nativeRowOff: this.nativeRowOff,
};
}
set model(value) {
this.nativeCol = value.nativeCol;
this.nativeColOff = value.nativeColOff;
this.nativeRow = value.nativeRow;
this.nativeRowOff = value.nativeRowOff;
}
}
module.exports = Anchor;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,320 @@
'use strict';
const _ = require('../utils/under-dash');
const Enums = require('./enums');
const colCache = require('../utils/col-cache');
const DEFAULT_COLUMN_WIDTH = 9;
// Column defines the column properties for 1 column.
// This includes header rows, widths, key, (style), etc.
// Worksheet will condense the columns as appropriate during serialization
class Column {
constructor(worksheet, number, defn) {
this._worksheet = worksheet;
this._number = number;
if (defn !== false) {
// sometimes defn will follow
this.defn = defn;
}
}
get number() {
return this._number;
}
get worksheet() {
return this._worksheet;
}
get letter() {
return colCache.n2l(this._number);
}
get isCustomWidth() {
return this.width !== undefined && this.width !== DEFAULT_COLUMN_WIDTH;
}
get defn() {
return {
header: this._header,
key: this.key,
width: this.width,
style: this.style,
hidden: this.hidden,
outlineLevel: this.outlineLevel,
};
}
set defn(value) {
if (value) {
this.key = value.key;
this.width = value.width !== undefined ? value.width : DEFAULT_COLUMN_WIDTH;
this.outlineLevel = value.outlineLevel;
if (value.style) {
this.style = value.style;
} else {
this.style = {};
}
// headers must be set after style
this.header = value.header;
this._hidden = !!value.hidden;
} else {
delete this._header;
delete this._key;
delete this.width;
this.style = {};
this.outlineLevel = 0;
}
}
get headers() {
return this._header && this._header instanceof Array ? this._header : [this._header];
}
get header() {
return this._header;
}
set header(value) {
if (value !== undefined) {
this._header = value;
this.headers.forEach((text, index) => {
this._worksheet.getCell(index + 1, this.number).value = text;
});
} else {
this._header = undefined;
}
}
get key() {
return this._key;
}
set key(value) {
const column = this._key && this._worksheet.getColumnKey(this._key);
if (column === this) {
this._worksheet.deleteColumnKey(this._key);
}
this._key = value;
if (value) {
this._worksheet.setColumnKey(this._key, this);
}
}
get hidden() {
return !!this._hidden;
}
set hidden(value) {
this._hidden = value;
}
get outlineLevel() {
return this._outlineLevel || 0;
}
set outlineLevel(value) {
this._outlineLevel = value;
}
get collapsed() {
return !!(
this._outlineLevel && this._outlineLevel >= this._worksheet.properties.outlineLevelCol
);
}
toString() {
return JSON.stringify({
key: this.key,
width: this.width,
headers: this.headers.length ? this.headers : undefined,
});
}
equivalentTo(other) {
return (
this.width === other.width &&
this.hidden === other.hidden &&
this.outlineLevel === other.outlineLevel &&
_.isEqual(this.style, other.style)
);
}
get isDefault() {
if (this.isCustomWidth) {
return false;
}
if (this.hidden) {
return false;
}
if (this.outlineLevel) {
return false;
}
const s = this.style;
if (s && (s.font || s.numFmt || s.alignment || s.border || s.fill || s.protection)) {
return false;
}
return true;
}
get headerCount() {
return this.headers.length;
}
eachCell(options, iteratee) {
const colNumber = this.number;
if (!iteratee) {
iteratee = options;
options = null;
}
this._worksheet.eachRow(options, (row, rowNumber) => {
iteratee(row.getCell(colNumber), rowNumber);
});
}
get values() {
const v = [];
this.eachCell((cell, rowNumber) => {
if (cell && cell.type !== Enums.ValueType.Null) {
v[rowNumber] = cell.value;
}
});
return v;
}
set values(v) {
if (!v) {
return;
}
const colNumber = this.number;
let offset = 0;
if (v.hasOwnProperty('0')) {
// assume contiguous array, start at row 1
offset = 1;
}
v.forEach((value, index) => {
this._worksheet.getCell(index + offset, colNumber).value = value;
});
}
// =========================================================================
// styles
_applyStyle(name, value) {
this.style[name] = value;
this.eachCell(cell => {
cell[name] = value;
});
return value;
}
get numFmt() {
return this.style.numFmt;
}
set numFmt(value) {
this._applyStyle('numFmt', value);
}
get font() {
return this.style.font;
}
set font(value) {
this._applyStyle('font', value);
}
get alignment() {
return this.style.alignment;
}
set alignment(value) {
this._applyStyle('alignment', value);
}
get protection() {
return this.style.protection;
}
set protection(value) {
this._applyStyle('protection', value);
}
get border() {
return this.style.border;
}
set border(value) {
this._applyStyle('border', value);
}
get fill() {
return this.style.fill;
}
set fill(value) {
this._applyStyle('fill', value);
}
// =============================================================================
// static functions
static toModel(columns) {
// Convert array of Column into compressed list cols
const cols = [];
let col = null;
if (columns) {
columns.forEach((column, index) => {
if (column.isDefault) {
if (col) {
col = null;
}
} else if (!col || !column.equivalentTo(col)) {
col = {
min: index + 1,
max: index + 1,
width: column.width !== undefined ? column.width : DEFAULT_COLUMN_WIDTH,
style: column.style,
isCustomWidth: column.isCustomWidth,
hidden: column.hidden,
outlineLevel: column.outlineLevel,
collapsed: column.collapsed,
};
cols.push(col);
} else {
col.max = index + 1;
}
});
}
return cols.length ? cols : undefined;
}
static fromModel(worksheet, cols) {
cols = cols || [];
const columns = [];
let count = 1;
let index = 0;
/**
* sort cols by min
* If it is not sorted, the subsequent column configuration will be overwritten
* */
cols = cols.sort(function(pre, next) {
return pre.min - next.min;
});
while (index < cols.length) {
const col = cols[index++];
while (count < col.min) {
columns.push(new Column(worksheet, count++));
}
while (count <= col.max) {
columns.push(new Column(worksheet, count++, col));
}
}
return columns.length ? columns : null;
}
}
module.exports = Column;

View File

@@ -0,0 +1,19 @@
class DataValidations {
constructor(model) {
this.model = model || {};
}
add(address, validation) {
return (this.model[address] = validation);
}
find(address) {
return this.model[address];
}
remove(address) {
this.model[address] = undefined;
}
}
module.exports = DataValidations;

View File

@@ -0,0 +1,234 @@
{
"TODO:": "This file is for analysis only. Not ready for release",
"name": "Office Theme",
"themeElements": {
"colorScheme": {
"name": "Office",
"dk1": { "type": "sys", "val": "windowText", "lastClr": "000000" },
"lt1": { "type": "sys", "val": "window", "lastClr": "FFFFFF" },
"dk2": { "type": "srgb", "val": "44546A" },
"lt2": { "type": "srgb", "val": "E7E6E6" },
"accent1": { "type": "srgb", "val": "5B9BD5" },
"accent2": { "type": "srgb", "val": "ED7D31" },
"accent3": { "type": "srgb", "val": "A5A5A5" },
"accent4": { "type": "srgb", "val": "FFC000" },
"accent5": { "type": "srgb", "val": "4472C4" },
"accent6": { "type": "srgb", "val": "70AD47" },
"hlink": { "type": "srgb", "val": "0563C1" },
"folHlink": { "type": "srgb", "val": "954F72" }
},
"fontScheme": {
"name": "Office",
"majorFont": {
"latin": { "typeface": "Calibri Light", "panose": "020F0302020204030204" },
"ea": { "typeface": "" },
"cs": { "typeface": "" },
"fonts": [
{ "script": "Jpan", "typeface": "游ゴシック Light" },
{ "script": "Hang", "typeface": "맑은 고딕" },
{ "script": "Hans", "typeface": "等线 Light" },
{ "script": "Hant", "typeface": "新細明體" },
{ "script": "Arab", "typeface": "Times New Roman" },
{ "script": "Hebr", "typeface": "Times New Roman" },
{ "script": "Thai", "typeface": "Tahoma" },
{ "script": "Ethi", "typeface": "Nyala" },
{ "script": "Beng", "typeface": "Vrinda" },
{ "script": "Gujr", "typeface": "Shruti" },
{ "script": "Khmr", "typeface": "MoolBoran" },
{ "script": "Knda", "typeface": "Tunga" },
{ "script": "Guru", "typeface": "Raavi" },
{ "script": "Cans", "typeface": "Euphemia" },
{ "script": "Cher", "typeface": "Plantagenet Cherokee" },
{ "script": "Yiii", "typeface": "Microsoft Yi Baiti" },
{ "script": "Tibt", "typeface": "Microsoft Himalaya" },
{ "script": "Thaa", "typeface": "MV Boli" },
{ "script": "Deva", "typeface": "Mangal" },
{ "script": "Telu", "typeface": "Gautami" },
{ "script": "Taml", "typeface": "Latha" },
{ "script": "Syrc", "typeface": "Estrangelo Edessa" },
{ "script": "Orya", "typeface": "Kalinga" },
{ "script": "Mlym", "typeface": "Kartika" },
{ "script": "Laoo", "typeface": "DokChampa" },
{ "script": "Sinh", "typeface": "Iskoola Pota" },
{ "script": "Mong", "typeface": "Mongolian Baiti" },
{ "script": "Viet", "typeface": "Times New Roman" },
{ "script": "Uigh", "typeface": "Microsoft Uighur" },
{ "script": "Geor", "typeface": "Sylfaen" }
]
},
"minorFont": {
"latin": { "typeface": "Calibri Light", "panose": "020F0302020204030204" },
"ea": { "typeface": "" },
"cs": { "typeface": "" },
"fonts": [
{ "script":"Jpan", "typeface": "游ゴシック" },
{ "script":"Hang", "typeface": "맑은 고딕" },
{ "script":"Hans", "typeface": "等线" },
{ "script":"Hant", "typeface": "新細明體" },
{ "script":"Arab", "typeface": "Arial" },
{ "script":"Hebr", "typeface": "Arial" },
{ "script":"Thai", "typeface": "Tahoma" },
{ "script":"Ethi", "typeface": "Nyala" },
{ "script":"Beng", "typeface": "Vrinda" },
{ "script":"Gujr", "typeface": "Shruti" },
{ "script":"Khmr", "typeface": "DaunPenh" },
{ "script":"Knda", "typeface": "Tunga" },
{ "script":"Guru", "typeface": "Raavi" },
{ "script":"Cans", "typeface": "Euphemia" },
{ "script":"Cher", "typeface": "Plantagenet Cherokee" },
{ "script":"Yiii", "typeface": "Microsoft Yi Baiti" },
{ "script":"Tibt", "typeface": "Microsoft Himalaya" },
{ "script":"Thaa", "typeface": "MV Boli" },
{ "script":"Deva", "typeface": "Mangal" },
{ "script":"Telu", "typeface": "Gautami" },
{ "script":"Taml", "typeface": "Latha" },
{ "script":"Syrc", "typeface": "Estrangelo Edessa" },
{ "script":"Orya", "typeface": "Kalinga" },
{ "script":"Mlym", "typeface": "Kartika" },
{ "script":"Laoo", "typeface": "DokChampa" },
{ "script":"Sinh", "typeface": "Iskoola Pota" },
{ "script":"Mong", "typeface": "Mongolian Baiti" },
{ "script":"Viet", "typeface": "Arial" },
{ "script":"Uigh", "typeface": "Microsoft Uighur" },
{ "script":"Geor", "typeface": "Sylfaen" }
]
}
},
"fmtScheme": {
"name": "Office",
"fillStyleLst": [
{ "type": "solidFill", "val": "phClr" },
{
"type": "gradFill",
"rotWithShape": true,
"gsLst": [
{ "pos":0, "schemeClr": { "val": "phClr", "lumMod": 110000, "satMod": 105000, "tint": 67000 } },
{ "pos":50000, "schemeClr": { "val": "phClr", "lumMod": 105000, "satMod": 103000, "tint": 73000 } },
{ "pos":100000, "schemeClr": { "val": "phClr", "lumMod": 105000, "satMod": 109000, "tint": 81000 } }
],
"lin": { "ang": 5400000, "scaled": false }
},
{
"type": "gradFill",
"rotWithShape": true,
"gsLst": [
{ "pos":0, "schemeClr": { "val": "phClr", "satMod": 103000, "lumMod": 102000, "tint": 94000 } },
{ "pos":50000, "schemeClr": { "val": "phClr", "satMod": 110000, "lumMod": 100000, "shade": 100000 } },
{ "pos":100000, "schemeClr": { "val": "phClr", "lumMod": 99000, "satMod": 120000, "tint": 78000 } }
],
"lin": { "ang": 5400000, "scaled": false }
}
],
"lnStyleLst": [
{
"w": 6350, "cap": "flat", "cmpd": "sng", "algn": "ctr",
"solidFill": { "schemeClr": { "val": "phClr" } },
"prstDash": { "val": "solid" },
"miter": { "lim": 800000 }
},
{
"w": 12700, "cap": "flat", "cmpd": "sng", "algn": "ctr",
"solidFill": { "schemeClr": { "val": "phClr" } },
"prstDash": { "val": "solid" },
"miter": { "lim": 800000 }
},
{
"w": 19050, "cap": "flat", "cmpd": "sng", "algn": "ctr",
"solidFill": { "schemeClr": { "val": "phClr" } },
"prstDash": { "val": "solid" },
"miter": { "lim": 800000 }
}
],
"effectStyleLst": [
{ "effectLst": [] },
{ "effectLst": [] },
{
"effectLst": [
{
"type": "outerShdw",
"blurRad": 57150, "dist": 19050, "dir": 5400000, "algn": "ctr", "rotWithShape": false,
"srgbClr": {
"val": "000000",
"alpha": {"val": 63000 }
}
}
]
}
],
"bgFillStyleLst": [
{ "type": "solidFill", "schemeClr": { "val": "phClr" } },
{ "type": "solidFill", "schemeClr": { "val": "phClr", "tint": 95000, "satMod": 170000 } },
{
"type": "gradFill",
"rotWithShape": true,
"gsLst": [
{ "pos":0, "schemeClr": { "val": "phClr", "tint": 93000, "satMod": 150000, "shade": 90000, "lumMod": 102000 } },
{ "pos":50000, "schemeClr": { "val": "phClr", "tint": 98000, "satMod": 130000, "shade": 73000, "lumMod": 103000 } },
{ "pos":100000, "schemeClr": { "val": "phClr", "shade": 63000, "satMod": 120000 } }
],
"lin": { "ang": 5400000, "scaled": false }
}
]
}
},
"objectDefaults": {
"spDef": {
"spPr": {},
"bodyPr": {},
"lstStyle": {},
"style": {
"lnRef": {
"idx": 1,
"schemeClr": "accent1"
},
"effectRef": {
"idx": 2,
"schemeClr": "accent1"
},
"fillRef": {
"idx": 3,
"schemeClr": "accent1"
},
"fontRef": {
"idx": "minor",
"schemeClr": "lt1"
}
}
},
"lnDef": {
"spPr": {},
"bodyPr": {},
"lstStyle": {},
"style": {
"lnRef": {
"idx": 2,
"schemeClr": "accent1"
},
"effectRef": {
"idx": 0,
"schemeClr": "accent1"
},
"fillRef": {
"idx": 1,
"schemeClr": "accent1"
},
"fontRef": {
"idx": "minor",
"schemeClr": "tx1"
}
}
}
},
"extraClrSchemeLst": [],
"extLst": [
{
"uri": "{05A4C25C-085E-4340-85A3-A5531E510DB2}",
"themeFamily": {
"name": "Office Theme",
"id": "{62F939B6-93AF-4DB8-9C6B-D6C7DFDC589F}",
"vid": "{4A3C46E8-61CC-4603-A589-7422A47A8E4A}",
"thm15": "http://schemas.microsoft.com/office/thememl/2012/main"
}
}
]
}

View File

@@ -0,0 +1,196 @@
'use strict';
const _ = require('../utils/under-dash');
const colCache = require('../utils/col-cache');
const CellMatrix = require('../utils/cell-matrix');
const Range = require('./range');
const rangeRegexp = /[$](\w+)[$](\d+)(:[$](\w+)[$](\d+))?/;
class DefinedNames {
constructor() {
this.matrixMap = {};
}
getMatrix(name) {
const matrix = this.matrixMap[name] || (this.matrixMap[name] = new CellMatrix());
return matrix;
}
// add a name to a cell. locStr in the form SheetName!$col$row or SheetName!$c1$r1:$c2:$r2
add(locStr, name) {
const location = colCache.decodeEx(locStr);
this.addEx(location, name);
}
addEx(location, name) {
const matrix = this.getMatrix(name);
if (location.top) {
for (let col = location.left; col <= location.right; col++) {
for (let row = location.top; row <= location.bottom; row++) {
const address = {
sheetName: location.sheetName,
address: colCache.n2l(col) + row,
row,
col,
};
matrix.addCellEx(address);
}
}
} else {
matrix.addCellEx(location);
}
}
remove(locStr, name) {
const location = colCache.decodeEx(locStr);
this.removeEx(location, name);
}
removeEx(location, name) {
const matrix = this.getMatrix(name);
matrix.removeCellEx(location);
}
removeAllNames(location) {
_.each(this.matrixMap, matrix => {
matrix.removeCellEx(location);
});
}
forEach(callback) {
_.each(this.matrixMap, (matrix, name) => {
matrix.forEach(cell => {
callback(name, cell);
});
});
}
// get all the names of a cell
getNames(addressStr) {
return this.getNamesEx(colCache.decodeEx(addressStr));
}
getNamesEx(address) {
return _.map(this.matrixMap, (matrix, name) => matrix.findCellEx(address) && name).filter(
Boolean
);
}
_explore(matrix, cell) {
cell.mark = false;
const {sheetName} = cell;
const range = new Range(cell.row, cell.col, cell.row, cell.col, sheetName);
let x;
let y;
// grow vertical - only one col to worry about
function vGrow(yy, edge) {
const c = matrix.findCellAt(sheetName, yy, cell.col);
if (!c || !c.mark) {
return false;
}
range[edge] = yy;
c.mark = false;
return true;
}
for (y = cell.row - 1; vGrow(y, 'top'); y--);
for (y = cell.row + 1; vGrow(y, 'bottom'); y++);
// grow horizontal - ensure all rows can grow
function hGrow(xx, edge) {
const cells = [];
for (y = range.top; y <= range.bottom; y++) {
const c = matrix.findCellAt(sheetName, y, xx);
if (c && c.mark) {
cells.push(c);
} else {
return false;
}
}
range[edge] = xx;
for (let i = 0; i < cells.length; i++) {
cells[i].mark = false;
}
return true;
}
for (x = cell.col - 1; hGrow(x, 'left'); x--);
for (x = cell.col + 1; hGrow(x, 'right'); x++);
return range;
}
getRanges(name, matrix) {
matrix = matrix || this.matrixMap[name];
if (!matrix) {
return {name, ranges: []};
}
// mark and sweep!
matrix.forEach(cell => {
cell.mark = true;
});
const ranges = matrix
.map(cell => cell.mark && this._explore(matrix, cell))
.filter(Boolean)
.map(range => range.$shortRange);
return {
name,
ranges,
};
}
normaliseMatrix(matrix, sheetName) {
// some of the cells might have shifted on specified sheet
// need to reassign rows, cols
matrix.forEachInSheet(sheetName, (cell, row, col) => {
if (cell) {
if (cell.row !== row || cell.col !== col) {
cell.row = row;
cell.col = col;
cell.address = colCache.n2l(col) + row;
}
}
});
}
spliceRows(sheetName, start, numDelete, numInsert) {
_.each(this.matrixMap, matrix => {
matrix.spliceRows(sheetName, start, numDelete, numInsert);
this.normaliseMatrix(matrix, sheetName);
});
}
spliceColumns(sheetName, start, numDelete, numInsert) {
_.each(this.matrixMap, matrix => {
matrix.spliceColumns(sheetName, start, numDelete, numInsert);
this.normaliseMatrix(matrix, sheetName);
});
}
get model() {
// To get names per cell - just iterate over all names finding cells if they exist
return _.map(this.matrixMap, (matrix, name) => this.getRanges(name, matrix)).filter(
definedName => definedName.ranges.length
);
}
set model(value) {
// value is [ { name, ranges }, ... ]
const matrixMap = (this.matrixMap = {});
value.forEach(definedName => {
const matrix = (matrixMap[definedName.name] = new CellMatrix());
definedName.ranges.forEach(rangeStr => {
if (rangeRegexp.test(rangeStr.split('!').pop() || '')) {
matrix.addCell(rangeStr);
}
});
});
}
}
module.exports = DefinedNames;

View File

@@ -0,0 +1,48 @@
'use strict';
module.exports = {
ValueType: {
Null: 0,
Merge: 1,
Number: 2,
String: 3,
Date: 4,
Hyperlink: 5,
Formula: 6,
SharedString: 7,
RichText: 8,
Boolean: 9,
Error: 10,
},
FormulaType: {
None: 0,
Master: 1,
Shared: 2,
},
RelationshipType: {
None: 0,
OfficeDocument: 1,
Worksheet: 2,
CalcChain: 3,
SharedStrings: 4,
Styles: 5,
Theme: 6,
Hyperlink: 7,
},
DocumentType: {
Xlsx: 1,
},
ReadingOrder: {
LeftToRight: 1,
RightToLeft: 2,
},
ErrorValue: {
NotApplicable: '#N/A',
Ref: '#REF!',
Name: '#NAME?',
DivZero: '#DIV/0!',
Null: '#NULL!',
Value: '#VALUE!',
Num: '#NUM!',
},
};

View File

@@ -0,0 +1,59 @@
const colCache = require('../utils/col-cache');
const Anchor = require('./anchor');
class Image {
constructor(worksheet, model) {
this.worksheet = worksheet;
this.model = model;
}
get model() {
switch (this.type) {
case 'background':
return {
type: this.type,
imageId: this.imageId,
};
case 'image':
return {
type: this.type,
imageId: this.imageId,
hyperlinks: this.range.hyperlinks,
range: {
tl: this.range.tl.model,
br: this.range.br && this.range.br.model,
ext: this.range.ext,
editAs: this.range.editAs,
},
};
default:
throw new Error('Invalid Image Type');
}
}
set model({type, imageId, range, hyperlinks}) {
this.type = type;
this.imageId = imageId;
if (type === 'image') {
if (typeof range === 'string') {
const decoded = colCache.decode(range);
this.range = {
tl: new Anchor(this.worksheet, {col: decoded.left, row: decoded.top}, -1),
br: new Anchor(this.worksheet, {col: decoded.right, row: decoded.bottom}, 0),
editAs: 'oneCell',
};
} else {
this.range = {
tl: new Anchor(this.worksheet, range.tl, 0),
br: range.br && new Anchor(this.worksheet, range.br, 0),
ext: range.ext,
editAs: range.editAs,
hyperlinks: hyperlinks || range.hyperlinks,
};
}
}
}
}
module.exports = Image;

View File

@@ -0,0 +1,18 @@
'use strict';
const XLSX = require('../xlsx/xlsx');
class ModelContainer {
constructor(model) {
this.model = model;
}
get xlsx() {
if (!this._xlsx) {
this._xlsx = new XLSX(this);
}
return this._xlsx;
}
}
module.exports = ModelContainer;

View File

@@ -0,0 +1,65 @@
const _ = require('../utils/under-dash');
class Note {
constructor(note) {
this.note = note;
}
get model() {
let value = null;
switch (typeof this.note) {
case 'string':
value = {
type: 'note',
note: {
texts: [
{
text: this.note,
},
],
},
};
break;
default:
value = {
type: 'note',
note: this.note,
};
break;
}
// Suitable for all cell comments
return _.deepMerge({}, Note.DEFAULT_CONFIGS, value);
}
set model(value) {
const {note} = value;
const {texts} = note;
if (texts.length === 1 && Object.keys(texts[0]).length === 1) {
this.note = texts[0].text;
} else {
this.note = note;
}
}
static fromModel(model) {
const note = new Note();
note.model = model;
return note;
}
}
Note.DEFAULT_CONFIGS = {
note: {
margins: {
insetmode: 'auto',
inset: [0.13, 0.13, 0.25, 0.25],
},
protection: {
locked: 'True',
lockText: 'True',
},
editAs: 'absolute',
},
};
module.exports = Note;

View File

@@ -0,0 +1,257 @@
const colCache = require('../utils/col-cache');
// used by worksheet to calculate sheet dimensions
class Range {
constructor() {
this.decode(arguments);
}
setTLBR(t, l, b, r, s) {
if (arguments.length < 4) {
// setTLBR(tl, br, s)
const tl = colCache.decodeAddress(t);
const br = colCache.decodeAddress(l);
this.model = {
top: Math.min(tl.row, br.row),
left: Math.min(tl.col, br.col),
bottom: Math.max(tl.row, br.row),
right: Math.max(tl.col, br.col),
sheetName: b,
};
this.setTLBR(tl.row, tl.col, br.row, br.col, s);
} else {
// setTLBR(t, l, b, r, s)
this.model = {
top: Math.min(t, b),
left: Math.min(l, r),
bottom: Math.max(t, b),
right: Math.max(l, r),
sheetName: s,
};
}
}
decode(argv) {
switch (argv.length) {
case 5: // [t,l,b,r,s]
this.setTLBR(argv[0], argv[1], argv[2], argv[3], argv[4]);
break;
case 4: // [t,l,b,r]
this.setTLBR(argv[0], argv[1], argv[2], argv[3]);
break;
case 3: // [tl,br,s]
this.setTLBR(argv[0], argv[1], argv[2]);
break;
case 2: // [tl,br]
this.setTLBR(argv[0], argv[1]);
break;
case 1: {
const value = argv[0];
if (value instanceof Range) {
// copy constructor
this.model = {
top: value.model.top,
left: value.model.left,
bottom: value.model.bottom,
right: value.model.right,
sheetName: value.sheetName,
};
} else if (value instanceof Array) {
// an arguments array
this.decode(value);
} else if (value.top && value.left && value.bottom && value.right) {
// a model
this.model = {
top: value.top,
left: value.left,
bottom: value.bottom,
right: value.right,
sheetName: value.sheetName,
};
} else {
// [sheetName!]tl:br
const tlbr = colCache.decodeEx(value);
if (tlbr.top) {
this.model = {
top: tlbr.top,
left: tlbr.left,
bottom: tlbr.bottom,
right: tlbr.right,
sheetName: tlbr.sheetName,
};
} else {
this.model = {
top: tlbr.row,
left: tlbr.col,
bottom: tlbr.row,
right: tlbr.col,
sheetName: tlbr.sheetName,
};
}
}
break;
}
case 0:
this.model = {
top: 0,
left: 0,
bottom: 0,
right: 0,
};
break;
default:
throw new Error(`Invalid number of arguments to _getDimensions() - ${argv.length}`);
}
}
get top() {
return this.model.top || 1;
}
set top(value) {
this.model.top = value;
}
get left() {
return this.model.left || 1;
}
set left(value) {
this.model.left = value;
}
get bottom() {
return this.model.bottom || 1;
}
set bottom(value) {
this.model.bottom = value;
}
get right() {
return this.model.right || 1;
}
set right(value) {
this.model.right = value;
}
get sheetName() {
return this.model.sheetName;
}
set sheetName(value) {
this.model.sheetName = value;
}
get _serialisedSheetName() {
const {sheetName} = this.model;
if (sheetName) {
if (/^[a-zA-Z0-9]*$/.test(sheetName)) {
return `${sheetName}!`;
}
return `'${sheetName}'!`;
}
return '';
}
expand(top, left, bottom, right) {
if (!this.model.top || top < this.top) this.top = top;
if (!this.model.left || left < this.left) this.left = left;
if (!this.model.bottom || bottom > this.bottom) this.bottom = bottom;
if (!this.model.right || right > this.right) this.right = right;
}
expandRow(row) {
if (row) {
const {dimensions, number} = row;
if (dimensions) {
this.expand(number, dimensions.min, number, dimensions.max);
}
}
}
expandToAddress(addressStr) {
const address = colCache.decodeEx(addressStr);
this.expand(address.row, address.col, address.row, address.col);
}
get tl() {
return colCache.n2l(this.left) + this.top;
}
get $t$l() {
return `$${colCache.n2l(this.left)}$${this.top}`;
}
get br() {
return colCache.n2l(this.right) + this.bottom;
}
get $b$r() {
return `$${colCache.n2l(this.right)}$${this.bottom}`;
}
get range() {
return `${this._serialisedSheetName + this.tl}:${this.br}`;
}
get $range() {
return `${this._serialisedSheetName + this.$t$l}:${this.$b$r}`;
}
get shortRange() {
return this.count > 1 ? this.range : this._serialisedSheetName + this.tl;
}
get $shortRange() {
return this.count > 1 ? this.$range : this._serialisedSheetName + this.$t$l;
}
get count() {
return (1 + this.bottom - this.top) * (1 + this.right - this.left);
}
toString() {
return this.range;
}
intersects(other) {
if (other.sheetName && this.sheetName && other.sheetName !== this.sheetName) return false;
if (other.bottom < this.top) return false;
if (other.top > this.bottom) return false;
if (other.right < this.left) return false;
if (other.left > this.right) return false;
return true;
}
contains(addressStr) {
const address = colCache.decodeEx(addressStr);
return this.containsEx(address);
}
containsEx(address) {
if (address.sheetName && this.sheetName && address.sheetName !== this.sheetName) return false;
return (
address.row >= this.top &&
address.row <= this.bottom &&
address.col >= this.left &&
address.col <= this.right
);
}
forEachAddress(cb) {
for (let col = this.left; col <= this.right; col++) {
for (let row = this.top; row <= this.bottom; row++) {
cb(colCache.encodeAddress(row, col), row, col);
}
}
}
}
module.exports = Range;

View File

@@ -0,0 +1,415 @@
'use strict';
const _ = require('../utils/under-dash');
const Enums = require('./enums');
const colCache = require('../utils/col-cache');
const Cell = require('./cell');
class Row {
constructor(worksheet, number) {
this._worksheet = worksheet;
this._number = number;
this._cells = [];
this.style = {};
this.outlineLevel = 0;
}
// return the row number
get number() {
return this._number;
}
get worksheet() {
return this._worksheet;
}
// Inform Streaming Writer that this row (and all rows before it) are complete
// and ready to write. Has no effect on Worksheet document
commit() {
this._worksheet._commitRow(this); // eslint-disable-line no-underscore-dangle
}
// helps GC by breaking cyclic references
destroy() {
delete this._worksheet;
delete this._cells;
delete this.style;
}
findCell(colNumber) {
return this._cells[colNumber - 1];
}
// given {address, row, col}, find or create new cell
getCellEx(address) {
let cell = this._cells[address.col - 1];
if (!cell) {
const column = this._worksheet.getColumn(address.col);
cell = new Cell(this, column, address.address);
this._cells[address.col - 1] = cell;
}
return cell;
}
// get cell by key, letter or column number
getCell(col) {
if (typeof col === 'string') {
// is it a key?
const column = this._worksheet.getColumnKey(col);
if (column) {
col = column.number;
} else {
col = colCache.l2n(col);
}
}
return (
this._cells[col - 1] ||
this.getCellEx({
address: colCache.encodeAddress(this._number, col),
row: this._number,
col,
})
);
}
// remove cell(s) and shift all higher cells down by count
splice(start, count, ...inserts) {
const nKeep = start + count;
const nExpand = inserts.length - count;
const nEnd = this._cells.length;
let i;
let cSrc;
let cDst;
if (nExpand < 0) {
// remove cells
for (i = start + inserts.length; i <= nEnd; i++) {
cDst = this._cells[i - 1];
cSrc = this._cells[i - nExpand - 1];
if (cSrc) {
cDst = this.getCell(i);
cDst.value = cSrc.value;
cDst.style = cSrc.style;
// eslint-disable-next-line no-underscore-dangle
cDst._comment = cSrc._comment;
} else if (cDst) {
cDst.value = null;
cDst.style = {};
// eslint-disable-next-line no-underscore-dangle
cDst._comment = undefined;
}
}
} else if (nExpand > 0) {
// insert new cells
for (i = nEnd; i >= nKeep; i--) {
cSrc = this._cells[i - 1];
if (cSrc) {
cDst = this.getCell(i + nExpand);
cDst.value = cSrc.value;
cDst.style = cSrc.style;
// eslint-disable-next-line no-underscore-dangle
cDst._comment = cSrc._comment;
} else {
this._cells[i + nExpand - 1] = undefined;
}
}
}
// now add the new values
for (i = 0; i < inserts.length; i++) {
cDst = this.getCell(start + i);
cDst.value = inserts[i];
cDst.style = {};
// eslint-disable-next-line no-underscore-dangle
cDst._comment = undefined;
}
}
// Iterate over all non-null cells in this row
eachCell(options, iteratee) {
if (!iteratee) {
iteratee = options;
options = null;
}
if (options && options.includeEmpty) {
const n = this._cells.length;
for (let i = 1; i <= n; i++) {
iteratee(this.getCell(i), i);
}
} else {
this._cells.forEach((cell, index) => {
if (cell && cell.type !== Enums.ValueType.Null) {
iteratee(cell, index + 1);
}
});
}
}
// ===========================================================================
// Page Breaks
addPageBreak(lft, rght) {
const ws = this._worksheet;
const left = Math.max(0, lft - 1) || 0;
const right = Math.max(0, rght - 1) || 16838;
const pb = {
id: this._number,
max: right,
man: 1,
};
if (left) pb.min = left;
ws.rowBreaks.push(pb);
}
// return a sparse array of cell values
get values() {
const values = [];
this._cells.forEach(cell => {
if (cell && cell.type !== Enums.ValueType.Null) {
values[cell.col] = cell.value;
}
});
return values;
}
// set the values by contiguous or sparse array, or by key'd object literal
set values(value) {
// this operation is not additive - any prior cells are removed
this._cells = [];
if (!value) {
// empty row
} else if (value instanceof Array) {
let offset = 0;
if (value.hasOwnProperty('0')) {
// contiguous array - start at column 1
offset = 1;
}
value.forEach((item, index) => {
if (item !== undefined) {
this.getCellEx({
address: colCache.encodeAddress(this._number, index + offset),
row: this._number,
col: index + offset,
}).value = item;
}
});
} else {
// assume object with column keys
this._worksheet.eachColumnKey((column, key) => {
if (value[key] !== undefined) {
this.getCellEx({
address: colCache.encodeAddress(this._number, column.number),
row: this._number,
col: column.number,
}).value = value[key];
}
});
}
}
// returns true if the row includes at least one cell with a value
get hasValues() {
return _.some(this._cells, cell => cell && cell.type !== Enums.ValueType.Null);
}
get cellCount() {
return this._cells.length;
}
get actualCellCount() {
let count = 0;
this.eachCell(() => {
count++;
});
return count;
}
// get the min and max column number for the non-null cells in this row or null
get dimensions() {
let min = 0;
let max = 0;
this._cells.forEach(cell => {
if (cell && cell.type !== Enums.ValueType.Null) {
if (!min || min > cell.col) {
min = cell.col;
}
if (max < cell.col) {
max = cell.col;
}
}
});
return min > 0
? {
min,
max,
}
: null;
}
// =========================================================================
// styles
_applyStyle(name, value) {
this.style[name] = value;
this._cells.forEach(cell => {
if (cell) {
cell[name] = value;
}
});
return value;
}
get numFmt() {
return this.style.numFmt;
}
set numFmt(value) {
this._applyStyle('numFmt', value);
}
get font() {
return this.style.font;
}
set font(value) {
this._applyStyle('font', value);
}
get alignment() {
return this.style.alignment;
}
set alignment(value) {
this._applyStyle('alignment', value);
}
get protection() {
return this.style.protection;
}
set protection(value) {
this._applyStyle('protection', value);
}
get border() {
return this.style.border;
}
set border(value) {
this._applyStyle('border', value);
}
get fill() {
return this.style.fill;
}
set fill(value) {
this._applyStyle('fill', value);
}
get hidden() {
return !!this._hidden;
}
set hidden(value) {
this._hidden = value;
}
get outlineLevel() {
return this._outlineLevel || 0;
}
set outlineLevel(value) {
this._outlineLevel = value;
}
get collapsed() {
return !!(
this._outlineLevel && this._outlineLevel >= this._worksheet.properties.outlineLevelRow
);
}
// =========================================================================
get model() {
const cells = [];
let min = 0;
let max = 0;
this._cells.forEach(cell => {
if (cell) {
const cellModel = cell.model;
if (cellModel) {
if (!min || min > cell.col) {
min = cell.col;
}
if (max < cell.col) {
max = cell.col;
}
cells.push(cellModel);
}
}
});
return this.height || cells.length
? {
cells,
number: this.number,
min,
max,
height: this.height,
style: this.style,
hidden: this.hidden,
outlineLevel: this.outlineLevel,
collapsed: this.collapsed,
}
: null;
}
set model(value) {
if (value.number !== this._number) {
throw new Error('Invalid row number in model');
}
this._cells = [];
let previousAddress;
value.cells.forEach(cellModel => {
switch (cellModel.type) {
case Cell.Types.Merge:
// special case - don't add this types
break;
default: {
let address;
if (cellModel.address) {
address = colCache.decodeAddress(cellModel.address);
} else if (previousAddress) {
// This is a <c> element without an r attribute
// Assume that it's the cell for the next column
const {row} = previousAddress;
const col = previousAddress.col + 1;
address = {
row,
col,
address: colCache.encodeAddress(row, col),
$col$row: `$${colCache.n2l(col)}$${row}`,
};
}
previousAddress = address;
const cell = this.getCellEx(address);
cell.model = cellModel;
break;
}
}
});
if (value.height) {
this.height = value.height;
} else {
delete this.height;
}
this.hidden = value.hidden;
this.outlineLevel = value.outlineLevel || 0;
this.style = (value.style && JSON.parse(JSON.stringify(value.style))) || {};
}
}
module.exports = Row;

View File

@@ -0,0 +1,465 @@
/* eslint-disable max-classes-per-file */
const colCache = require('../utils/col-cache');
class Column {
// wrapper around column model, allowing access and manipulation
constructor(table, column, index) {
this.table = table;
this.column = column;
this.index = index;
}
_set(name, value) {
this.table.cacheState();
this.column[name] = value;
}
/* eslint-disable lines-between-class-members */
get name() {
return this.column.name;
}
set name(value) {
this._set('name', value);
}
get filterButton() {
return this.column.filterButton;
}
set filterButton(value) {
this.column.filterButton = value;
}
get style() {
return this.column.style;
}
set style(value) {
this.column.style = value;
}
get totalsRowLabel() {
return this.column.totalsRowLabel;
}
set totalsRowLabel(value) {
this._set('totalsRowLabel', value);
}
get totalsRowFunction() {
return this.column.totalsRowFunction;
}
set totalsRowFunction(value) {
this._set('totalsRowFunction', value);
}
get totalsRowResult() {
return this.column.totalsRowResult;
}
set totalsRowResult(value) {
this._set('totalsRowResult', value);
}
get totalsRowFormula() {
return this.column.totalsRowFormula;
}
set totalsRowFormula(value) {
this._set('totalsRowFormula', value);
}
/* eslint-enable lines-between-class-members */
}
class Table {
constructor(worksheet, table) {
this.worksheet = worksheet;
if (table) {
this.table = table;
// check things are ok first
this.validate();
this.store();
}
}
getFormula(column) {
// get the correct formula to apply to the totals row
switch (column.totalsRowFunction) {
case 'none':
return null;
case 'average':
return `SUBTOTAL(101,${this.table.name}[${column.name}])`;
case 'countNums':
return `SUBTOTAL(102,${this.table.name}[${column.name}])`;
case 'count':
return `SUBTOTAL(103,${this.table.name}[${column.name}])`;
case 'max':
return `SUBTOTAL(104,${this.table.name}[${column.name}])`;
case 'min':
return `SUBTOTAL(105,${this.table.name}[${column.name}])`;
case 'stdDev':
return `SUBTOTAL(106,${this.table.name}[${column.name}])`;
case 'var':
return `SUBTOTAL(107,${this.table.name}[${column.name}])`;
case 'sum':
return `SUBTOTAL(109,${this.table.name}[${column.name}])`;
case 'custom':
return column.totalsRowFormula;
default:
throw new Error(`Invalid Totals Row Function: ${column.totalsRowFunction}`);
}
}
get width() {
// width of the table
return this.table.columns.length;
}
get height() {
// height of the table data
return this.table.rows.length;
}
get filterHeight() {
// height of the table data plus optional header row
return this.height + (this.table.headerRow ? 1 : 0);
}
get tableHeight() {
// full height of the table on the sheet
return this.filterHeight + (this.table.totalsRow ? 1 : 0);
}
validate() {
const {table} = this;
// set defaults and check is valid
const assign = (o, name, dflt) => {
if (o[name] === undefined) {
o[name] = dflt;
}
};
assign(table, 'headerRow', true);
assign(table, 'totalsRow', false);
assign(table, 'style', {});
assign(table.style, 'theme', 'TableStyleMedium2');
assign(table.style, 'showFirstColumn', false);
assign(table.style, 'showLastColumn', false);
assign(table.style, 'showRowStripes', false);
assign(table.style, 'showColumnStripes', false);
const assert = (test, message) => {
if (!test) {
throw new Error(message);
}
};
assert(table.ref, 'Table must have ref');
assert(table.columns, 'Table must have column definitions');
assert(table.rows, 'Table must have row definitions');
table.tl = colCache.decodeAddress(table.ref);
const {row, col} = table.tl;
assert(row > 0, 'Table must be on valid row');
assert(col > 0, 'Table must be on valid col');
const {width, filterHeight, tableHeight} = this;
// autoFilterRef is a range that includes optional headers only
table.autoFilterRef = colCache.encode(row, col, row + filterHeight - 1, col + width - 1);
// tableRef is a range that includes optional headers and totals
table.tableRef = colCache.encode(row, col, row + tableHeight - 1, col + width - 1);
table.columns.forEach((column, i) => {
assert(column.name, `Column ${i} must have a name`);
if (i === 0) {
assign(column, 'totalsRowLabel', 'Total');
} else {
assign(column, 'totalsRowFunction', 'none');
column.totalsRowFormula = this.getFormula(column);
}
});
}
store() {
// where the table needs to store table data, headers, footers in
// the sheet...
const assignStyle = (cell, style) => {
if (style) {
Object.keys(style).forEach(key => {
cell[key] = style[key];
});
}
};
const {worksheet, table} = this;
const {row, col} = table.tl;
let count = 0;
if (table.headerRow) {
const r = worksheet.getRow(row + count++);
table.columns.forEach((column, j) => {
const {style, name} = column;
const cell = r.getCell(col + j);
cell.value = name;
assignStyle(cell, style);
});
}
table.rows.forEach(data => {
const r = worksheet.getRow(row + count++);
data.forEach((value, j) => {
const cell = r.getCell(col + j);
cell.value = value;
assignStyle(cell, table.columns[j].style);
});
});
if (table.totalsRow) {
const r = worksheet.getRow(row + count++);
table.columns.forEach((column, j) => {
const cell = r.getCell(col + j);
if (j === 0) {
cell.value = column.totalsRowLabel;
} else {
const formula = this.getFormula(column);
if (formula) {
cell.value = {
formula: column.totalsRowFormula,
result: column.totalsRowResult,
};
} else {
cell.value = null;
}
}
assignStyle(cell, column.style);
});
}
}
load(worksheet) {
// where the table will read necessary features from a loaded sheet
const {table} = this;
const {row, col} = table.tl;
let count = 0;
if (table.headerRow) {
const r = worksheet.getRow(row + count++);
table.columns.forEach((column, j) => {
const cell = r.getCell(col + j);
cell.value = column.name;
});
}
table.rows.forEach(data => {
const r = worksheet.getRow(row + count++);
data.forEach((value, j) => {
const cell = r.getCell(col + j);
cell.value = value;
});
});
if (table.totalsRow) {
const r = worksheet.getRow(row + count++);
table.columns.forEach((column, j) => {
const cell = r.getCell(col + j);
if (j === 0) {
cell.value = column.totalsRowLabel;
} else {
const formula = this.getFormula(column);
if (formula) {
cell.value = {
formula: column.totalsRowFormula,
result: column.totalsRowResult,
};
}
}
});
}
}
get model() {
return this.table;
}
set model(value) {
this.table = value;
}
// ================================================================
// TODO: Mutating methods
cacheState() {
if (!this._cache) {
this._cache = {
ref: this.ref,
width: this.width,
tableHeight: this.tableHeight,
};
}
}
commit() {
// changes may have been made that might have on-sheet effects
if (!this._cache) {
return;
}
// check things are ok first
this.validate();
const ref = colCache.decodeAddress(this._cache.ref);
if (this.ref !== this._cache.ref) {
// wipe out whole table footprint at previous location
for (let i = 0; i < this._cache.tableHeight; i++) {
const row = this.worksheet.getRow(ref.row + i);
for (let j = 0; j < this._cache.width; j++) {
const cell = row.getCell(ref.col + j);
cell.value = null;
}
}
} else {
// clear out below table if it has shrunk
for (let i = this.tableHeight; i < this._cache.tableHeight; i++) {
const row = this.worksheet.getRow(ref.row + i);
for (let j = 0; j < this._cache.width; j++) {
const cell = row.getCell(ref.col + j);
cell.value = null;
}
}
// clear out to right of table if it has lost columns
for (let i = 0; i < this.tableHeight; i++) {
const row = this.worksheet.getRow(ref.row + i);
for (let j = this.width; j < this._cache.width; j++) {
const cell = row.getCell(ref.col + j);
cell.value = null;
}
}
}
this.store();
}
addRow(values, rowNumber) {
// Add a row of data, either insert at rowNumber or append
this.cacheState();
if (rowNumber === undefined) {
this.table.rows.push(values);
} else {
this.table.rows.splice(rowNumber, 0, values);
}
}
removeRows(rowIndex, count = 1) {
// Remove a rows of data
this.cacheState();
this.table.rows.splice(rowIndex, count);
}
getColumn(colIndex) {
const column = this.table.columns[colIndex];
return new Column(this, column, colIndex);
}
addColumn(column, values, colIndex) {
// Add a new column, including column defn and values
// Inserts at colNumber or adds to the right
this.cacheState();
if (colIndex === undefined) {
this.table.columns.push(column);
this.table.rows.forEach((row, i) => {
row.push(values[i]);
});
} else {
this.table.columns.splice(colIndex, 0, column);
this.table.rows.forEach((row, i) => {
row.splice(colIndex, 0, values[i]);
});
}
}
removeColumns(colIndex, count = 1) {
// Remove a column with data
this.cacheState();
this.table.columns.splice(colIndex, count);
this.table.rows.forEach(row => {
row.splice(colIndex, count);
});
}
_assign(target, prop, value) {
this.cacheState();
target[prop] = value;
}
/* eslint-disable lines-between-class-members */
get ref() {
return this.table.ref;
}
set ref(value) {
this._assign(this.table, 'ref', value);
}
get name() {
return this.table.name;
}
set name(value) {
this.table.name = value;
}
get displayName() {
return this.table.displyName || this.table.name;
}
set displayNamename(value) {
this.table.displayName = value;
}
get headerRow() {
return this.table.headerRow;
}
set headerRow(value) {
this._assign(this.table, 'headerRow', value);
}
get totalsRow() {
return this.table.totalsRow;
}
set totalsRow(value) {
this._assign(this.table, 'totalsRow', value);
}
get theme() {
return this.table.style.name;
}
set theme(value) {
this.table.style.name = value;
}
get showFirstColumn() {
return this.table.style.showFirstColumn;
}
set showFirstColumn(value) {
this.table.style.showFirstColumn = value;
}
get showLastColumn() {
return this.table.style.showLastColumn;
}
set showLastColumn(value) {
this.table.style.showLastColumn = value;
}
get showRowStripes() {
return this.table.style.showRowStripes;
}
set showRowStripes(value) {
this.table.style.showRowStripes = value;
}
get showColumnStripes() {
return this.table.style.showColumnStripes;
}
set showColumnStripes(value) {
this.table.style.showColumnStripes = value;
}
/* eslint-enable lines-between-class-members */
}
module.exports = Table;

View File

@@ -0,0 +1,221 @@
'use strict';
const Worksheet = require('./worksheet');
const DefinedNames = require('./defined-names');
const XLSX = require('../xlsx/xlsx');
const CSV = require('../csv/csv');
// Workbook requirements
// Load and Save from file and stream
// Access/Add/Delete individual worksheets
// Manage String table, Hyperlink table, etc.
// Manage scaffolding for contained objects to write to/read from
class Workbook {
constructor() {
this.category = '';
this.company = '';
this.created = new Date();
this.description = '';
this.keywords = '';
this.manager = '';
this.modified = this.created;
this.properties = {};
this.calcProperties = {};
this._worksheets = [];
this.subject = '';
this.title = '';
this.views = [];
this.media = [];
this._definedNames = new DefinedNames();
}
get xlsx() {
if (!this._xlsx) this._xlsx = new XLSX(this);
return this._xlsx;
}
get csv() {
if (!this._csv) this._csv = new CSV(this);
return this._csv;
}
get nextId() {
// find the next unique spot to add worksheet
for (let i = 1; i < this._worksheets.length; i++) {
if (!this._worksheets[i]) {
return i;
}
}
return this._worksheets.length || 1;
}
addWorksheet(name, options) {
const id = this.nextId;
// if options is a color, call it tabColor (and signal deprecated message)
if (options) {
if (typeof options === 'string') {
// eslint-disable-next-line no-console
console.trace(
'tabColor argument is now deprecated. Please use workbook.addWorksheet(name, {properties: { tabColor: { argb: "rbg value" } }'
);
options = {
properties: {
tabColor: {argb: options},
},
};
} else if (options.argb || options.theme || options.indexed) {
// eslint-disable-next-line no-console
console.trace(
'tabColor argument is now deprecated. Please use workbook.addWorksheet(name, {properties: { tabColor: { ... } }'
);
options = {
properties: {
tabColor: options,
},
};
}
}
const lastOrderNo = this._worksheets.reduce((acc, ws) => ((ws && ws.orderNo) > acc ? ws.orderNo : acc), 0);
const worksheetOptions = Object.assign({}, options, {
id,
name,
orderNo: lastOrderNo + 1,
workbook: this,
});
const worksheet = new Worksheet(worksheetOptions);
this._worksheets[id] = worksheet;
return worksheet;
}
removeWorksheetEx(worksheet) {
delete this._worksheets[worksheet.id];
}
removeWorksheet(id) {
const worksheet = this.getWorksheet(id);
if (worksheet) {
worksheet.destroy();
}
}
getWorksheet(id) {
if (id === undefined) {
return this._worksheets.find(Boolean);
}
if (typeof id === 'number') {
return this._worksheets[id];
}
if (typeof id === 'string') {
return this._worksheets.find(worksheet => worksheet && worksheet.name === id);
}
return undefined;
}
get worksheets() {
// return a clone of _worksheets
return this._worksheets
.slice(1)
.sort((a, b) => a.orderNo - b.orderNo)
.filter(Boolean);
}
eachSheet(iteratee) {
this.worksheets.forEach(sheet => {
iteratee(sheet, sheet.id);
});
}
get definedNames() {
return this._definedNames;
}
clearThemes() {
// Note: themes are not an exposed feature, meddle at your peril!
this._themes = undefined;
}
addImage(image) {
// TODO: validation?
const id = this.media.length;
this.media.push(Object.assign({}, image, {type: 'image'}));
return id;
}
getImage(id) {
return this.media[id];
}
get model() {
return {
creator: this.creator || 'Unknown',
lastModifiedBy: this.lastModifiedBy || 'Unknown',
lastPrinted: this.lastPrinted,
created: this.created,
modified: this.modified,
properties: this.properties,
worksheets: this.worksheets.map(worksheet => worksheet.model),
sheets: this.worksheets.map(ws => ws.model).filter(Boolean),
definedNames: this._definedNames.model,
views: this.views,
company: this.company,
manager: this.manager,
title: this.title,
subject: this.subject,
keywords: this.keywords,
category: this.category,
description: this.description,
language: this.language,
revision: this.revision,
contentStatus: this.contentStatus,
themes: this._themes,
media: this.media,
calcProperties: this.calcProperties,
};
}
set model(value) {
this.creator = value.creator;
this.lastModifiedBy = value.lastModifiedBy;
this.lastPrinted = value.lastPrinted;
this.created = value.created;
this.modified = value.modified;
this.company = value.company;
this.manager = value.manager;
this.title = value.title;
this.subject = value.subject;
this.keywords = value.keywords;
this.category = value.category;
this.description = value.description;
this.language = value.language;
this.revision = value.revision;
this.contentStatus = value.contentStatus;
this.properties = value.properties;
this.calcProperties = value.calcProperties;
this._worksheets = [];
value.worksheets.forEach(worksheetModel => {
const {id, name, state} = worksheetModel;
const orderNo = value.sheets && value.sheets.findIndex(ws => ws.id === id);
const worksheet = (this._worksheets[id] = new Worksheet({
id,
name,
orderNo,
state,
workbook: this,
}));
worksheet.model = worksheetModel;
});
this._definedNames.model = value.definedNames;
this.views = value.views;
this._themes = value.themes;
this.media = value.media || [];
}
}
module.exports = Workbook;

View File

@@ -0,0 +1,927 @@
const _ = require('../utils/under-dash');
const colCache = require('../utils/col-cache');
const Range = require('./range');
const Row = require('./row');
const Column = require('./column');
const Enums = require('./enums');
const Image = require('./image');
const Table = require('./table');
const DataValidations = require('./data-validations');
const Encryptor = require('../utils/encryptor');
const {copyStyle} = require('../utils/copy-style');
// Worksheet requirements
// Operate as sheet inside workbook or standalone
// Load and Save from file and stream
// Access/Add/Delete individual cells
// Manage column widths and row heights
class Worksheet {
constructor(options) {
options = options || {};
this._workbook = options.workbook;
// in a workbook, each sheet will have a number
this.id = options.id;
this.orderNo = options.orderNo;
// and a name
this.name = options.name;
// add a state
this.state = options.state || 'visible';
// rows allows access organised by row. Sparse array of arrays indexed by row-1, col
// Note: _rows is zero based. Must subtract 1 to go from cell.row to index
this._rows = [];
// column definitions
this._columns = null;
// column keys (addRow convenience): key ==> this._collumns index
this._keys = {};
// keep record of all merges
this._merges = {};
// record of all row and column pageBreaks
this.rowBreaks = [];
// for tabColor, default row height, outline levels, etc
this.properties = Object.assign(
{},
{
defaultRowHeight: 15,
dyDescent: 55,
outlineLevelCol: 0,
outlineLevelRow: 0,
},
options.properties
);
// for all things printing
this.pageSetup = Object.assign(
{},
{
margins: {left: 0.7, right: 0.7, top: 0.75, bottom: 0.75, header: 0.3, footer: 0.3},
orientation: 'portrait',
horizontalDpi: 4294967295,
verticalDpi: 4294967295,
fitToPage: !!(
options.pageSetup &&
(options.pageSetup.fitToWidth || options.pageSetup.fitToHeight) &&
!options.pageSetup.scale
),
pageOrder: 'downThenOver',
blackAndWhite: false,
draft: false,
cellComments: 'None',
errors: 'displayed',
scale: 100,
fitToWidth: 1,
fitToHeight: 1,
paperSize: undefined,
showRowColHeaders: false,
showGridLines: false,
firstPageNumber: undefined,
horizontalCentered: false,
verticalCentered: false,
rowBreaks: null,
colBreaks: null,
},
options.pageSetup
);
this.headerFooter = Object.assign(
{},
{
differentFirst: false,
differentOddEven: false,
oddHeader: null,
oddFooter: null,
evenHeader: null,
evenFooter: null,
firstHeader: null,
firstFooter: null,
},
options.headerFooter
);
this.dataValidations = new DataValidations();
// for freezepanes, split, zoom, gridlines, etc
this.views = options.views || [];
this.autoFilter = options.autoFilter || null;
// for images, etc
this._media = [];
// worksheet protection
this.sheetProtection = null;
// for tables
this.tables = {};
this.conditionalFormattings = [];
}
get name() {
return this._name;
}
set name(name) {
if (name === undefined) {
name = `sheet${this.id}`;
}
if (this._name === name) return;
if (typeof name !== 'string') {
throw new Error('The name has to be a string.');
}
if (name === '') {
throw new Error('The name can\'t be empty.');
}
if (name === 'History') {
throw new Error('The name "History" is protected. Please use a different name.');
}
// Illegal character in worksheet name: asterisk (*), question mark (?),
// colon (:), forward slash (/ \), or bracket ([])
if (/[*?:/\\[\]]/.test(name)) {
throw new Error(`Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ]`);
}
if (/(^')|('$)/.test(name)) {
throw new Error(`The first or last character of worksheet name cannot be a single quotation mark: ${name}`);
}
if (name && name.length > 31) {
// eslint-disable-next-line no-console
console.warn(`Worksheet name ${name} exceeds 31 chars. This will be truncated`);
name = name.substring(0, 31);
}
if (this._workbook._worksheets.find(ws => ws && ws.name.toLowerCase() === name.toLowerCase())) {
throw new Error(`Worksheet name already exists: ${name}`);
}
this._name = name;
}
get workbook() {
return this._workbook;
}
// when you're done with this worksheet, call this to remove from workbook
destroy() {
this._workbook.removeWorksheetEx(this);
}
// Get the bounding range of the cells in this worksheet
get dimensions() {
const dimensions = new Range();
this._rows.forEach(row => {
if (row) {
const rowDims = row.dimensions;
if (rowDims) {
dimensions.expand(row.number, rowDims.min, row.number, rowDims.max);
}
}
});
return dimensions;
}
// =========================================================================
// Columns
// get the current columns array.
get columns() {
return this._columns;
}
// set the columns from an array of column definitions.
// Note: any headers defined will overwrite existing values.
set columns(value) {
// calculate max header row count
this._headerRowCount = value.reduce((pv, cv) => {
const headerCount = (cv.header && 1) || (cv.headers && cv.headers.length) || 0;
return Math.max(pv, headerCount);
}, 0);
// construct Column objects
let count = 1;
const columns = (this._columns = []);
value.forEach(defn => {
const column = new Column(this, count++, false);
columns.push(column);
column.defn = defn;
});
}
getColumnKey(key) {
return this._keys[key];
}
setColumnKey(key, value) {
this._keys[key] = value;
}
deleteColumnKey(key) {
delete this._keys[key];
}
eachColumnKey(f) {
_.each(this._keys, f);
}
// get a single column by col number. If it doesn't exist, create it and any gaps before it
getColumn(c) {
if (typeof c === 'string') {
// if it matches a key'd column, return that
const col = this._keys[c];
if (col) return col;
// otherwise, assume letter
c = colCache.l2n(c);
}
if (!this._columns) {
this._columns = [];
}
if (c > this._columns.length) {
let n = this._columns.length + 1;
while (n <= c) {
this._columns.push(new Column(this, n++));
}
}
return this._columns[c - 1];
}
spliceColumns(start, count, ...inserts) {
const rows = this._rows;
const nRows = rows.length;
if (inserts.length > 0) {
// must iterate over all rows whether they exist yet or not
for (let i = 0; i < nRows; i++) {
const rowArguments = [start, count];
// eslint-disable-next-line no-loop-func
inserts.forEach(insert => {
rowArguments.push(insert[i] || null);
});
const row = this.getRow(i + 1);
// eslint-disable-next-line prefer-spread
row.splice.apply(row, rowArguments);
}
} else {
// nothing to insert, so just splice all rows
this._rows.forEach(r => {
if (r) {
r.splice(start, count);
}
});
}
// splice column definitions
const nExpand = inserts.length - count;
const nKeep = start + count;
const nEnd = this._columns.length;
if (nExpand < 0) {
for (let i = start + inserts.length; i <= nEnd; i++) {
this.getColumn(i).defn = this.getColumn(i - nExpand).defn;
}
} else if (nExpand > 0) {
for (let i = nEnd; i >= nKeep; i--) {
this.getColumn(i + nExpand).defn = this.getColumn(i).defn;
}
}
for (let i = start; i < start + inserts.length; i++) {
this.getColumn(i).defn = null;
}
// account for defined names
this.workbook.definedNames.spliceColumns(this.name, start, count, inserts.length);
}
get lastColumn() {
return this.getColumn(this.columnCount);
}
get columnCount() {
let maxCount = 0;
this.eachRow(row => {
maxCount = Math.max(maxCount, row.cellCount);
});
return maxCount;
}
get actualColumnCount() {
// performance nightmare - for each row, counts all the columns used
const counts = [];
let count = 0;
this.eachRow(row => {
row.eachCell(({col}) => {
if (!counts[col]) {
counts[col] = true;
count++;
}
});
});
return count;
}
// =========================================================================
// Rows
_commitRow() {
// nop - allows streaming reader to fill a document
}
get _lastRowNumber() {
// need to cope with results of splice
const rows = this._rows;
let n = rows.length;
while (n > 0 && rows[n - 1] === undefined) {
n--;
}
return n;
}
get _nextRow() {
return this._lastRowNumber + 1;
}
get lastRow() {
if (this._rows.length) {
return this._rows[this._rows.length - 1];
}
return undefined;
}
// find a row (if exists) by row number
findRow(r) {
return this._rows[r - 1];
}
// find multiple rows (if exists) by row number
findRows(start, length) {
return this._rows.slice(start - 1, start - 1 + length);
}
get rowCount() {
return this._lastRowNumber;
}
get actualRowCount() {
// counts actual rows that have actual data
let count = 0;
this.eachRow(() => {
count++;
});
return count;
}
// get a row by row number.
getRow(r) {
let row = this._rows[r - 1];
if (!row) {
row = this._rows[r - 1] = new Row(this, r);
}
return row;
}
// get multiple rows by row number.
getRows(start, length) {
if (length < 1) return undefined;
const rows = [];
for (let i = start; i < start + length; i++) {
rows.push(this.getRow(i));
}
return rows;
}
addRow(value, style = 'n') {
const rowNo = this._nextRow;
const row = this.getRow(rowNo);
row.values = value;
this._setStyleOption(rowNo, style[0] === 'i' ? style : 'n');
return row;
}
addRows(value, style = 'n') {
const rows = [];
value.forEach(row => {
rows.push(this.addRow(row, style));
});
return rows;
}
insertRow(pos, value, style = 'n') {
this.spliceRows(pos, 0, value);
this._setStyleOption(pos, style);
return this.getRow(pos);
}
insertRows(pos, values, style = 'n') {
this.spliceRows(pos, 0, ...values);
if (style !== 'n') {
// copy over the styles
for (let i = 0; i < values.length; i++) {
if (style[0] === 'o' && this.findRow(values.length + pos + i) !== undefined) {
this._copyStyle(values.length + pos + i, pos + i, style[1] === '+');
} else if (style[0] === 'i' && this.findRow(pos - 1) !== undefined) {
this._copyStyle(pos - 1, pos + i, style[1] === '+');
}
}
}
return this.getRows(pos, values.length);
}
// set row at position to same style as of either pervious row (option 'i') or next row (option 'o')
_setStyleOption(pos, style = 'n') {
if (style[0] === 'o' && this.findRow(pos + 1) !== undefined) {
this._copyStyle(pos + 1, pos, style[1] === '+');
} else if (style[0] === 'i' && this.findRow(pos - 1) !== undefined) {
this._copyStyle(pos - 1, pos, style[1] === '+');
}
}
_copyStyle(src, dest, styleEmpty = false) {
const rSrc = this.getRow(src);
const rDst = this.getRow(dest);
rDst.style = copyStyle(rSrc.style);
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: styleEmpty}, (cell, colNumber) => {
rDst.getCell(colNumber).style = copyStyle(cell.style);
});
rDst.height = rSrc.height;
}
duplicateRow(rowNum, count, insert = false) {
// create count duplicates of rowNum
// either inserting new or overwriting existing rows
const rSrc = this._rows[rowNum - 1];
const inserts = new Array(count).fill(rSrc.values);
this.spliceRows(rowNum + 1, insert ? 0 : count, ...inserts);
// now copy styles...
for (let i = 0; i < count; i++) {
const rDst = this._rows[rowNum + i];
rDst.style = rSrc.style;
rDst.height = rSrc.height;
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => {
rDst.getCell(colNumber).style = cell.style;
});
}
}
spliceRows(start, count, ...inserts) {
// same problem as row.splice, except worse.
const nKeep = start + count;
const nInserts = inserts.length;
const nExpand = nInserts - count;
const nEnd = this._rows.length;
let i;
let rSrc;
if (nExpand < 0) {
// remove rows
if (start === nEnd) {
this._rows[nEnd - 1] = undefined;
}
for (i = nKeep; i <= nEnd; i++) {
rSrc = this._rows[i - 1];
if (rSrc) {
const rDst = this.getRow(i + nExpand);
rDst.values = rSrc.values;
rDst.style = rSrc.style;
rDst.height = rSrc.height;
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => {
rDst.getCell(colNumber).style = cell.style;
});
this._rows[i - 1] = undefined;
} else {
this._rows[i + nExpand - 1] = undefined;
}
}
} else if (nExpand > 0) {
// insert new cells
for (i = nEnd; i >= nKeep; i--) {
rSrc = this._rows[i - 1];
if (rSrc) {
const rDst = this.getRow(i + nExpand);
rDst.values = rSrc.values;
rDst.style = rSrc.style;
rDst.height = rSrc.height;
// eslint-disable-next-line no-loop-func
rSrc.eachCell({includeEmpty: true}, (cell, colNumber) => {
rDst.getCell(colNumber).style = cell.style;
// remerge cells accounting for insert offset
if (cell._value.constructor.name === 'MergeValue') {
const cellToBeMerged = this.getRow(cell._row._number + nInserts).getCell(colNumber);
const prevMaster = cell._value._master;
const newMaster = this.getRow(prevMaster._row._number + nInserts).getCell(prevMaster._column._number);
cellToBeMerged.merge(newMaster);
}
});
} else {
this._rows[i + nExpand - 1] = undefined;
}
}
}
// now copy over the new values
for (i = 0; i < nInserts; i++) {
const rDst = this.getRow(start + i);
rDst.style = {};
rDst.values = inserts[i];
}
// account for defined names
this.workbook.definedNames.spliceRows(this.name, start, count, nInserts);
}
// iterate over every row in the worksheet, including maybe empty rows
eachRow(options, iteratee) {
if (!iteratee) {
iteratee = options;
options = undefined;
}
if (options && options.includeEmpty) {
const n = this._rows.length;
for (let i = 1; i <= n; i++) {
iteratee(this.getRow(i), i);
}
} else {
this._rows.forEach(row => {
if (row && row.hasValues) {
iteratee(row, row.number);
}
});
}
}
// return all rows as sparse array
getSheetValues() {
const rows = [];
this._rows.forEach(row => {
if (row) {
rows[row.number] = row.values;
}
});
return rows;
}
// =========================================================================
// Cells
// returns the cell at [r,c] or address given by r. If not found, return undefined
findCell(r, c) {
const address = colCache.getAddress(r, c);
const row = this._rows[address.row - 1];
return row ? row.findCell(address.col) : undefined;
}
// return the cell at [r,c] or address given by r. If not found, create a new one.
getCell(r, c) {
const address = colCache.getAddress(r, c);
const row = this.getRow(address.row);
return row.getCellEx(address);
}
// =========================================================================
// Merge
// convert the range defined by ['tl:br'], [tl,br] or [t,l,b,r] into a single 'merged' cell
mergeCells(...cells) {
const dimensions = new Range(cells);
this._mergeCellsInternal(dimensions);
}
mergeCellsWithoutStyle(...cells) {
const dimensions = new Range(cells);
this._mergeCellsInternal(dimensions, true);
}
_mergeCellsInternal(dimensions, ignoreStyle) {
// check cells aren't already merged
_.each(this._merges, merge => {
if (merge.intersects(dimensions)) {
throw new Error('Cannot merge already merged cells');
}
});
// apply merge
const master = this.getCell(dimensions.top, dimensions.left);
for (let i = dimensions.top; i <= dimensions.bottom; i++) {
for (let j = dimensions.left; j <= dimensions.right; j++) {
// merge all but the master cell
if (i > dimensions.top || j > dimensions.left) {
this.getCell(i, j).merge(master, ignoreStyle);
}
}
}
// index merge
this._merges[master.address] = dimensions;
}
_unMergeMaster(master) {
// master is always top left of a rectangle
const merge = this._merges[master.address];
if (merge) {
for (let i = merge.top; i <= merge.bottom; i++) {
for (let j = merge.left; j <= merge.right; j++) {
this.getCell(i, j).unmerge();
}
}
delete this._merges[master.address];
}
}
get hasMerges() {
// return true if this._merges has a merge object
return _.some(this._merges, Boolean);
}
// scan the range defined by ['tl:br'], [tl,br] or [t,l,b,r] and if any cell is part of a merge,
// un-merge the group. Note this function can affect multiple merges and merge-blocks are
// atomic - either they're all merged or all un-merged.
unMergeCells(...cells) {
const dimensions = new Range(cells);
// find any cells in that range and unmerge them
for (let i = dimensions.top; i <= dimensions.bottom; i++) {
for (let j = dimensions.left; j <= dimensions.right; j++) {
const cell = this.findCell(i, j);
if (cell) {
if (cell.type === Enums.ValueType.Merge) {
// this cell merges to another master
this._unMergeMaster(cell.master);
} else if (this._merges[cell.address]) {
// this cell is a master
this._unMergeMaster(cell);
}
}
}
}
}
// ===========================================================================
// Shared/Array Formula
fillFormula(range, formula, results, shareType = 'shared') {
// Define formula for top-left cell and share to rest
const decoded = colCache.decode(range);
const {top, left, bottom, right} = decoded;
const width = right - left + 1;
const masterAddress = colCache.encodeAddress(top, left);
const isShared = shareType === 'shared';
// work out result accessor
let getResult;
if (typeof results === 'function') {
getResult = results;
} else if (Array.isArray(results)) {
if (Array.isArray(results[0])) {
getResult = (row, col) => results[row - top][col - left];
} else {
// eslint-disable-next-line no-mixed-operators
getResult = (row, col) => results[(row - top) * width + (col - left)];
}
} else {
getResult = () => undefined;
}
let first = true;
for (let r = top; r <= bottom; r++) {
for (let c = left; c <= right; c++) {
if (first) {
this.getCell(r, c).value = {
shareType,
formula,
ref: range,
result: getResult(r, c),
};
first = false;
} else {
this.getCell(r, c).value = isShared
? {
sharedFormula: masterAddress,
result: getResult(r, c),
}
: getResult(r, c);
}
}
}
}
// =========================================================================
// Images
addImage(imageId, range) {
const model = {
type: 'image',
imageId,
range,
};
this._media.push(new Image(this, model));
}
getImages() {
return this._media.filter(m => m.type === 'image');
}
addBackgroundImage(imageId) {
const model = {
type: 'background',
imageId,
};
this._media.push(new Image(this, model));
}
getBackgroundImageId() {
const image = this._media.find(m => m.type === 'background');
return image && image.imageId;
}
// =========================================================================
// Worksheet Protection
protect(password, options) {
// TODO: make this function truly async
// perhaps marshal to worker thread or something
return new Promise(resolve => {
this.sheetProtection = {
sheet: true,
};
if (options && 'spinCount' in options) {
// force spinCount to be integer >= 0
options.spinCount = Number.isFinite(options.spinCount) ? Math.round(Math.max(0, options.spinCount)) : 100000;
}
if (password) {
this.sheetProtection.algorithmName = 'SHA-512';
this.sheetProtection.saltValue = Encryptor.randomBytes(16).toString('base64');
this.sheetProtection.spinCount = options && 'spinCount' in options ? options.spinCount : 100000; // allow user specified spinCount
this.sheetProtection.hashValue = Encryptor.convertPasswordToHash(
password,
'SHA512',
this.sheetProtection.saltValue,
this.sheetProtection.spinCount
);
}
if (options) {
this.sheetProtection = Object.assign(this.sheetProtection, options);
if (!password && 'spinCount' in options) {
delete this.sheetProtection.spinCount;
}
}
resolve();
});
}
unprotect() {
this.sheetProtection = null;
}
// =========================================================================
// Tables
addTable(model) {
const table = new Table(this, model);
this.tables[model.name] = table;
return table;
}
getTable(name) {
return this.tables[name];
}
removeTable(name) {
delete this.tables[name];
}
getTables() {
return Object.values(this.tables);
}
// ===========================================================================
// Conditional Formatting
addConditionalFormatting(cf) {
this.conditionalFormattings.push(cf);
}
removeConditionalFormatting(filter) {
if (typeof filter === 'number') {
this.conditionalFormattings.splice(filter, 1);
} else if (filter instanceof Function) {
this.conditionalFormattings = this.conditionalFormattings.filter(filter);
} else {
this.conditionalFormattings = [];
}
}
// ===========================================================================
// Deprecated
get tabColor() {
// eslint-disable-next-line no-console
console.trace('worksheet.tabColor property is now deprecated. Please use worksheet.properties.tabColor');
return this.properties.tabColor;
}
set tabColor(value) {
// eslint-disable-next-line no-console
console.trace('worksheet.tabColor property is now deprecated. Please use worksheet.properties.tabColor');
this.properties.tabColor = value;
}
// ===========================================================================
// Model
get model() {
const model = {
id: this.id,
name: this.name,
dataValidations: this.dataValidations.model,
properties: this.properties,
state: this.state,
pageSetup: this.pageSetup,
headerFooter: this.headerFooter,
rowBreaks: this.rowBreaks,
views: this.views,
autoFilter: this.autoFilter,
media: this._media.map(medium => medium.model),
sheetProtection: this.sheetProtection,
tables: Object.values(this.tables).map(table => table.model),
conditionalFormattings: this.conditionalFormattings,
};
// =================================================
// columns
model.cols = Column.toModel(this.columns);
// ==========================================================
// Rows
const rows = (model.rows = []);
const dimensions = (model.dimensions = new Range());
this._rows.forEach(row => {
const rowModel = row && row.model;
if (rowModel) {
dimensions.expand(rowModel.number, rowModel.min, rowModel.number, rowModel.max);
rows.push(rowModel);
}
});
// ==========================================================
// Merges
model.merges = [];
_.each(this._merges, merge => {
model.merges.push(merge.range);
});
return model;
}
_parseRows(model) {
this._rows = [];
model.rows.forEach(rowModel => {
const row = new Row(this, rowModel.number);
this._rows[row.number - 1] = row;
row.model = rowModel;
});
}
_parseMergeCells(model) {
_.each(model.mergeCells, merge => {
// Do not merge styles when importing an Excel file
// since each cell may have different styles intentionally.
this.mergeCellsWithoutStyle(merge);
});
}
set model(value) {
this.name = value.name;
this._columns = Column.fromModel(this, value.cols);
this._parseRows(value);
this._parseMergeCells(value);
this.dataValidations = new DataValidations(value.dataValidations);
this.properties = value.properties;
this.pageSetup = value.pageSetup;
this.headerFooter = value.headerFooter;
this.views = value.views;
this.autoFilter = value.autoFilter;
this._media = value.media.map(medium => new Image(this, medium));
this.sheetProtection = value.sheetProtection;
this.tables = value.tables.reduce((tables, table) => {
const t = new Table();
t.model = table;
tables[table.name] = t;
return tables;
}, {});
this.conditionalFormattings = value.conditionalFormattings;
}
}
module.exports = Worksheet;