src/sheet.js
import { Row } from './row';
import { Col } from './col';
import { handleStyle, handleNumFmtId, Border } from './style';
import { num2col } from './lib';
import { makeXworksheet, XsheetData, Xpane, Xcols, Xcol, Xrow, Xdimension, Xc, Xf, XmergeCells, XmergeCell } from './xmlWorksheet';
/**
* Sheet of the xlsx file.
* ```js
* import { File } from 'better-xlsx';
* const file = new File();
* const sheet = file.addSheet('Sheet-1');
* const row = sheet.addRow();
* const cell = row.addCell();
* ```
*/
export class Sheet {
rows = [];
cols = [];
maxRow = 0;
maxCol = 0;
hidden = false;
sheetViews = [];
sheetFormat = {
defaultColWidth: 0,
defaultRowHeight: 0,
outlineLevelCol: 0,
outlineLevelRow: 0
};
constructor ({ name, file, selected }) {
this.name = name;
this.file = file;
this.selected = selected;
}
/**
* Create a Row and add it into the Sheet.
* @return {Row}
*/
addRow () {
const row = new Row({ sheet: this });
this.rows.push(row);
if (this.rows.length > this.maxRow) {
this.maxRow = this.rows.length;
}
return row;
}
maybeAddCol (cellCount) {
if (cellCount > this.maxCol) {
const col = new Col({
min: cellCount,
max: cellCount,
hidden: false,
collapsed: false
});
this.cols.push(col);
this.maxCol = cellCount;
}
}
/**
* Get Col of the sheet with index and create cols when `index > maxCol`.
* @param {Number} idx Index of the Col [from 0].
* @return {Col}
*/
col (idx) {
this.maybeAddCol(idx + 1);
return this.cols[idx];
}
/**
* Get Row of the sheet with index and create rows when `index > maxRow`.
* @param {Number} idx Index of the Row [from 0].
* @return {Row}
*/
row (idx) {
for (let len = this.rows.length; len <= idx; len++) {
this.addRow();
}
return this.rows[idx];
}
/**
* Get Cell of the sheet with `(row, col)` and create cell when out of range.
* @param {Number} row
* @param {Number} col
* @return {Cell}
*/
cell (row, col) {
for (let len = this.rows.length; len <= row; len++) {
this.addRow();
}
const r = this.rows[row];
for (let len = r.cells.length; len <= col; len++) {
r.addCell();
}
return r.cells[col];
}
/**
* Set columns width from `startcol` to `endcol`.
* @param {Number} startcol
* @param {Number} endcol
* @param {Number} width
*/
setColWidth (startcol, endcol, width) {
if (startcol > endcol) {
throw new Error(`Could not set width for range ${startcol}-${endcol}: startcol must be less than endcol.`);
}
const col = new Col({
min: startcol + 1,
max: endcol + 1,
hidden: false,
collapsed: false,
width: width
});
this.cols.push(col);
if (endcol + 1 > this.maxCol) {
this.maxCol = endcol + 1;
}
}
handleMerged () {
const merged = [];
for (let r = 0; r < this.rows.length; r++) {
const row = this.rows[r];
for (let c = 0; c < row.cells.length; c++) {
const cell = row.cells[c];
if (cell.hMerge > 0 || cell.vMerge > 0) {
merged.push({ r, c, cell });
}
}
}
for (const { r, c, cell } of merged) {
const { border } = cell.style;
cell.style.border = new Border({});
for (let rownum = 0; rownum <= cell.vMerge; rownum++) {
for (let colnum = 0; colnum <= cell.hMerge; colnum++) {
const tmpcell = this.cell(r + rownum, c + colnum);
const arr = [];
if (rownum === 0) {
arr.push('top');
}
if (rownum === cell.vMerge) {
arr.push('bottom');
}
if (colnum === 0) {
arr.push('left');
}
if (colnum === cell.hMerge) {
arr.push('right');
}
if (arr.length) {
tmpcell.style.applyBorder = true;
arr.forEach(k => {
const ck = `${k}Color`;
tmpcell.style.border[k] = border[k];
tmpcell.style.border[ck] = border[ck];
});
}
}
}
}
}
makeXSheet (refTable, styles) {
const sheet = makeXworksheet();
const xSheet = new XsheetData();
let maxRow = 0;
let maxCell = 0;
let maxLevelCol;
let maxLevelRow;
this.handleMerged();
for (let i = 0; i < this.sheetViews.length; i++) {
const view = this.sheetViews[i];
if (view && view.pane) {
sheet.sheetViews.children[i].children.push(new Xpane({
xSplit: view.pane.xSplit,
ySplit: view.pane.ySplit,
topLeftCell: view.pane.topLeftCell,
activePane: view.pane.activePane,
state: view.pane.state
}));
}
}
if (this.selected) {
sheet.sheetViews.children[0].tabSelected = true;
}
if (this.sheetFormat.defaultRowHeight !== 0) {
sheet.sheetFormatPr.defaultRowHeight = this.sheetFormat.defaultRowHeight;
}
if (this.sheetFormat.defaultColWidth !== 0) {
sheet.sheetFormatPr.defaultColWidth = this.sheetFormat.defaultColWidth;
}
const fIdList = [];
sheet.cols = new Xcols();
for (let c = 0; c < this.cols.length; c++) {
const col = this.cols[c];
col.min = col.min || 1;
col.max = col.max || 1;
const xNumFmt = styles.newNumFmt(col.numFmt);
const fId = handleStyle(col.style, xNumFmt.numFmtId, styles);
fIdList.push(fId);
let customWidth = 0;
if (col.width === 0) {
col.width = 9.5;
} else {
customWidth = 1;
}
sheet.cols.children.push(new Xcol({
min: col.min,
max: col.max,
hidden: col.hidden,
width: col.width,
customWidth: customWidth,
collapsed: col.collapsed,
outlineLevel: col.outlineLevel,
style: fId
}));
if (col.outlineLevel > maxLevelCol) {
maxLevelCol = col.outlineLevel;
}
}
for (let r = 0; r < this.rows.length; r++) {
const row = this.rows[r];
if (r > maxRow) maxRow = r;
const xRow = new Xrow({ r: r + 1 });
if (row.isCustom) {
xRow.customHeight = true;
xRow.ht = row.height;
}
xRow.outlineLevel = row.outlineLevel;
if (row.outlineLevel > maxLevelRow) {
maxLevelRow = row.outlineLevel;
}
for (let c = 0; c < row.cells.length; c++) {
let fId = fIdList[c];
const cell = row.cells[c];
const xNumFmt = styles.newNumFmt(cell.numFmt);
const style = cell.style;
if (style !== null) {
fId = handleStyle(style, xNumFmt.numFmtId, styles);
} else if (cell.numFmt && this.cols[c].numFmt !== cell.numFmt) {
fId = handleNumFmtId(xNumFmt.NumFmtId, styles);
}
if (c > maxCell) maxCell = c;
const xC = new Xc({ r: `${num2col(c)}${r + 1}` });
switch (cell.cellType) {
case 'TypeString':
if (cell.value) {
xC.v = refTable.addString(cell.value);
}
xC.t = 's';
xC.s = fId;
break;
case 'TypeBool':
xC.v = cell.value;
xC.t = 'b';
xC.s = fId;
break;
case 'TypeNumeric':
xC.v = cell.value;
xC.s = fId;
break;
case 'TypeDate':
xC.v = cell.value;
xC.s = fId;
break;
case 'TypeFormula':
xC.v = cell.value;
xC.f = new Xf({}, [cell.formula]);
xC.s = fId;
break;
case 'TypeError':
xC.v = cell.value;
xC.f = new Xf({}, [cell.formula]);
xC.t = 'e';
xC.s = fId;
break;
case 'TypeGeneral':
xC.v = cell.value;
xC.s = fId;
break;
}
xRow.children.push(xC);
if (cell.hMerge > 0 || cell.vMerge > 0) {
// r == rownum, c == colnum
const start = `${num2col(c)}${r + 1}`;
const endcol = c + cell.hMerge;
const endrow = r + cell.vMerge + 1;
const end = `${num2col(endcol)}${endrow}`;
const mc = new XmergeCell({ ref: start + ':' + end });
if (sheet.mergeCells === null) {
sheet.mergeCells = new XmergeCells();
}
sheet.mergeCells.children.push(mc);
}
}
xSheet.children.push(xRow);
}
// Update sheet format with the freshly determined max levels
this.sheetFormat.outlineLevelCol = maxLevelCol;
this.sheetFormat.outlineLevelRow = maxLevelRow;
// .. and then also apply this to the xml worksheet
sheet.sheetFormatPr.outlineLevelCol = this.sheetFormat.outlineLevelCol;
sheet.sheetFormatPr.outlineLevelRow = this.sheetFormat.outlineLevelRow;
if (sheet.mergeCells !== null) {
sheet.mergeCells.count = sheet.mergeCells.children.length;
}
sheet.sheetData = xSheet;
const dimension = new Xdimension({
ref: `A1:${num2col(maxCell)}${maxRow + 1}`
});
if (dimension.ref === 'A1:A1') {
dimension.ref = 'A1';
}
sheet.dimension = dimension;
if (this.afterMake) {
this.afterMake(sheet);
}
return sheet;
}
}