@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.
965 lines (875 loc) • 37.7 kB
JavaScript
"use strict";
const _ = require("lodash");
const Cell = require("./Cell");
const Row = require("./Row");
const Column = require("./Column");
const Range = require("./Range");
const Relationships = require("./Relationships");
const xmlq = require("./xmlq");
const regexify = require("./regexify");
const addressConverter = require("./addressConverter");
const ArgHandler = require("./ArgHandler");
const colorIndexes = require("./colorIndexes");
// Order of the nodes as defined by the spec.
const nodeOrder = [
"sheetPr", "dimension", "sheetViews", "sheetFormatPr", "cols", "sheetData",
"sheetCalcPr", "sheetProtection", "autoFilter", "protectedRanges", "scenarios", "autoFilter",
"sortState", "dataConsolidate", "customSheetViews", "mergeCells", "phoneticPr",
"conditionalFormatting", "dataValidations", "hyperlinks", "printOptions",
"pageMargins", "pageSetup", "headerFooter", "rowBreaks", "colBreaks",
"customProperties", "cellWatches", "ignoredErrors", "smartTags", "drawing",
"drawingHF", "picture", "oleObjects", "controls", "webPublishItems", "tableParts",
"extLst"
];
/**
* A worksheet.
*/
class Sheet {
// /**
// * Creates a new instance of Sheet.
// * @param {Workbook} workbook - The parent workbook.
// * @param {{}} idNode - The sheet ID node (from the parent workbook).
// * @param {{}} node - The sheet node.
// * @param {{}} [relationshipsNode] - The optional sheet relationships node.
// */
constructor(workbook, idNode, node, relationshipsNode) {
this._init(workbook, idNode, node, relationshipsNode);
}
/* PUBLIC */
/**
* Gets a value indicating whether the sheet is the active sheet in the workbook.
* @returns {boolean} True if active, false otherwise.
*//**
* Make the sheet the active sheet in the workkbok.
* @param {boolean} active - Must be set to `true`. Deactivating directly is not supported. To deactivate, you should activate a different sheet instead.
* @returns {Sheet} The sheet.
*/
active() {
return new ArgHandler('Sheet.active')
.case(() => {
return this.workbook().activeSheet() === this;
})
.case('boolean', active => {
if (!active) throw new Error("Deactivating sheet directly not supported. Activate a different sheet instead.");
this.workbook().activeSheet(this);
return this;
})
.handle(arguments);
}
/**
* Get the active cell in the sheet.
* @returns {Cell} The active cell.
*//**
* Set the active cell in the workbook.
* @param {string|Cell} cell - The cell or address of cell to activate.
* @returns {Sheet} The sheet.
*//**
* Set the active cell in the workbook by row and column.
* @param {number} rowNumber - The row number of the cell.
* @param {string|number} columnNameOrNumber - The column name or number of the cell.
* @returns {Sheet} The sheet.
*/
activeCell() {
const sheetViewNode = this._getOrCreateSheetViewNode();
let selectionNode = xmlq.findChild(sheetViewNode, "selection");
return new ArgHandler('Sheet.activeCell')
.case(() => {
const cellAddress = selectionNode ? selectionNode.attributes.activeCell : "A1";
return this.cell(cellAddress);
})
.case(['number', '*'], (rowNumber, columnNameOrNumber) => {
const cell = this.cell(rowNumber, columnNameOrNumber);
return this.activeCell(cell);
})
.case('*', cell => {
if (!selectionNode) {
selectionNode = {
name: "selection",
attributes: {},
children: []
};
xmlq.appendChild(sheetViewNode, selectionNode);
}
if (!(cell instanceof Cell)) cell = this.cell(cell);
selectionNode.attributes.activeCell = selectionNode.attributes.sqref = cell.address();
return this;
})
.handle(arguments);
}
/**
* Gets the cell with the given address.
* @param {string} address - The address of the cell.
* @returns {Cell} The cell.
*//**
* Gets the cell with the given row and column numbers.
* @param {number} rowNumber - The row number of the cell.
* @param {string|number} columnNameOrNumber - The column name or number of the cell.
* @returns {Cell} The cell.
*/
cell() {
return new ArgHandler('Sheet.cell')
.case('string', address => {
const ref = addressConverter.fromAddress(address);
if (ref.type !== 'cell') throw new Error('Sheet.cell: Invalid address.');
return this.row(ref.rowNumber).cell(ref.columnNumber);
})
.case(['number', '*'], (rowNumber, columnNameOrNumber) => {
return this.row(rowNumber).cell(columnNameOrNumber);
})
.handle(arguments);
}
/**
* Gets a column in the sheet.
* @param {string|number} columnNameOrNumber - The name or number of the column.
* @returns {Column} The column.
*/
column(columnNameOrNumber) {
const columnNumber = typeof columnNameOrNumber === "string" ? addressConverter.columnNameToNumber(columnNameOrNumber) : columnNameOrNumber;
// If we're already created a column for this column number, return it.
if (this._columns[columnNumber]) return this._columns[columnNumber];
// We need to create a new column, which requires a backing col node. There may already exist a node whose min/max cover our column.
// First, see if there is an existing col node.
const existingColNode = this._colNodes[columnNumber];
let colNode;
if (existingColNode) {
// If the existing node covered earlier columns than the new one, we need to have a col node to cover the min up to our new node.
if (existingColNode.attributes.min < columnNumber) {
// Clone the node and set the max to the column before our new col.
const beforeColNode = _.cloneDeep(existingColNode);
beforeColNode.attributes.max = columnNumber - 1;
// Update the col nodes cache.
for (let i = beforeColNode.attributes.min; i <= beforeColNode.attributes.max; i++) {
this._colNodes[i] = beforeColNode;
}
}
// Make a clone for the new column. Set the min/max to the column number and cache it.
colNode = _.cloneDeep(existingColNode);
colNode.attributes.min = columnNumber;
colNode.attributes.max = columnNumber;
this._colNodes[columnNumber] = colNode;
// If the max of the existing node is greater than the nre one, create a col node for that too.
if (existingColNode.attributes.max > columnNumber) {
const afterColNode = _.cloneDeep(existingColNode);
afterColNode.attributes.min = columnNumber + 1;
for (let i = afterColNode.attributes.min; i <= afterColNode.attributes.max; i++) {
this._colNodes[i] = afterColNode;
}
}
} else {
// The was no existing node so create a new one.
colNode = {
name: 'col',
attributes: {
min: columnNumber,
max: columnNumber
},
children: []
};
this._colNodes[columnNumber] = colNode;
}
// Create the new column and cache it.
const column = new Column(this, colNode);
this._columns[columnNumber] = column;
return column;
}
/**
* Gets a defined name scoped to the sheet.
* @param {string} name - The defined name.
* @returns {undefined|string|Cell|Range|Row|Column} What the defined name refers to or undefined if not found. Will return the string formula if not a Row, Column, Cell, or Range.
*//**
* Set a defined name scoped to the sheet.
* @param {string} name - The defined name.
* @param {string|Cell|Range|Row|Column} refersTo - What the name refers to.
* @returns {Workbook} The workbook.
*/
definedName() {
return new ArgHandler("Workbook.definedName")
.case('string', name => {
return this.workbook().scopedDefinedName(this, name);
})
.case(['string', '*'], (name, refersTo) => {
this.workbook().scopedDefinedName(this, name, refersTo);
return this;
})
.handle(arguments);
}
/**
* Deletes the sheet and returns the parent workbook.
* @returns {Workbook} The workbook.
*/
delete() {
this.workbook().deleteSheet(this);
return this.workbook();
}
/**
* Find the given pattern in the sheet and optionally replace it.
* @param {string|RegExp} pattern - The pattern to look for. Providing a string will result in a case-insensitive substring search. Use a RegExp for more sophisticated searches.
* @param {string|function} [replacement] - The text to replace or a String.replace callback function. If pattern is a string, all occurrences of the pattern in each cell will be replaced.
* @returns {Array.<Cell>} The matching cells.
*/
find(pattern, replacement) {
pattern = regexify(pattern);
let matches = [];
this._rows.forEach(row => {
if (!row) return;
matches = matches.concat(row.find(pattern, replacement));
});
return matches;
}
/**
* Gets a value indicating whether this sheet's grid lines are visible.
* @returns {boolean} True if selected, false if not.
*//**
* Sets whether this sheet's grid lines are visible.
* @param {boolean} selected - True to make visible, false to hide.
* @returns {Sheet} The sheet.
*/
gridLinesVisible() {
const sheetViewNode = this._getOrCreateSheetViewNode();
return new ArgHandler('Sheet.gridLinesVisible')
.case(() => {
return sheetViewNode.attributes.showGridLines === 1 || sheetViewNode.attributes.showGridLines === undefined;
})
.case('boolean', visible => {
sheetViewNode.attributes.showGridLines = visible ? 1 : 0;
return this;
})
.handle(arguments);
}
/**
* Gets a value indicating if the sheet is hidden or not.
* @returns {boolean|string} True if hidden, false if visible, and 'very' if very hidden.
*//**
* Set whether the sheet is hidden or not.
* @param {boolean|string} hidden - True to hide, false to show, and 'very' to make very hidden.
* @returns {Sheet} The sheet.
*/
hidden() {
return new ArgHandler('Sheet.hidden')
.case(() => {
if (this._idNode.attributes.state === 'hidden') return true;
if (this._idNode.attributes.state === 'veryHidden') return "very";
return false;
})
.case('*', hidden => {
if (hidden) {
const visibleSheets = _.filter(this.workbook().sheets(), sheet => !sheet.hidden());
if (visibleSheets.length === 1 && visibleSheets[0] === this) {
throw new Error("This sheet may not be hidden as a workbook must contain at least one visible sheet.");
}
// If activate, activate the first other visible sheet.
if (this.active()) {
const activeIndex = visibleSheets[0] === this ? 1 : 0;
visibleSheets[activeIndex].active(true);
}
}
if (hidden === 'very') this._idNode.attributes.state = 'veryHidden';
else if (hidden) this._idNode.attributes.state = 'hidden';
else delete this._idNode.attributes.state;
return this;
})
.handle(arguments);
}
/**
* Move the sheet.
* @param {number|string|Sheet} [indexOrBeforeSheet] The index to move the sheet to or the sheet (or name of sheet) to move this sheet before. Omit this argument to move to the end of the workbook.
* @returns {Sheet} The sheet.
*/
move(indexOrBeforeSheet) {
this.workbook().moveSheet(this, indexOrBeforeSheet);
return this;
}
/**
* Get the name of the sheet.
* @returns {string} The sheet name.
*//**
* Set the name of the sheet. *Note: this method does not rename references to the sheet so formulas, etc. can be broken. Use with caution!*
* @param {string} name - The name to set to the sheet.
* @returns {Sheet} The sheet.
*/
name() {
return new ArgHandler('Sheet.name')
.case(() => {
return this._idNode.attributes.name.toString();
})
.case('string', name => {
this._idNode.attributes.name = name;
return this;
})
.handle(arguments);
}
/**
* Gets a range from the given range address.
* @param {string} address - The range address (e.g. 'A1:B3').
* @returns {Range} The range.
*//**
* Gets a range from the given cells or cell addresses.
* @param {string|Cell} startCell - The starting cell or cell address (e.g. 'A1').
* @param {string|Cell} endCell - The ending cell or cell address (e.g. 'B3').
* @returns {Range} The range.
*//**
* Gets a range from the given row numbers and column names or numbers.
* @param {number} startRowNumber - The starting cell row number.
* @param {string|number} startColumnNameOrNumber - The starting cell column name or number.
* @param {number} endRowNumber - The ending cell row number.
* @param {string|number} endColumnNameOrNumber - The ending cell column name or number.
* @returns {Range} The range.
*/
range() {
return new ArgHandler('Sheet.range')
.case('string', address => {
const ref = addressConverter.fromAddress(address);
if (ref.type !== 'range') throw new Error('Sheet.range: Invalid address');
return this.range(ref.startRowNumber, ref.startColumnNumber, ref.endRowNumber, ref.endColumnNumber);
})
.case(['*', '*'], (startCell, endCell) => {
if (typeof startCell === "string") startCell = this.cell(startCell);
if (typeof endCell === "string") endCell = this.cell(endCell);
return new Range(startCell, endCell);
})
.case(['number', '*', 'number', '*'], (startRowNumber, startColumnNameOrNumber, endRowNumber, endColumnNameOrNumber) => {
return this.range(this.cell(startRowNumber, startColumnNameOrNumber), this.cell(endRowNumber, endColumnNameOrNumber));
})
.handle(arguments);
}
/**
* Unsets sheet autoFilter.
* @returns {Sheet} This sheet.
*//**
* Sets sheet autoFilter to a Range.
* @param {Range} range - The autoFilter range.
* @returns {Sheet} This sheet.
*/
autoFilter(range) {
this._autoFilter = range;
return this;
}
/**
* Gets the row with the given number.
* @param {number} rowNumber - The row number.
* @returns {Row} The row with the given number.
*/
row(rowNumber) {
if (this._rows[rowNumber]) return this._rows[rowNumber];
const rowNode = {
name: 'row',
attributes: {
r: rowNumber
},
children: []
};
const row = new Row(this, rowNode);
this._rows[rowNumber] = row;
return row;
}
/**
* Get the tab color. (See style [Color](#color).)
* @returns {undefined|Color} The color or undefined if not set.
*//**
* Sets the tab color. (See style [Color](#color).)
* @returns {Color|string|number} color - Color of the tab. If string, will set an RGB color. If number, will set a theme color.
*/
tabColor() {
return new ArgHandler("Sheet.tabColor")
.case(() => {
const tabColorNode = xmlq.findChild(this._sheetPrNode, "tabColor");
if (!tabColorNode) return;
const color = {};
if (tabColorNode.attributes.hasOwnProperty('rgb')) color.rgb = tabColorNode.attributes.rgb;
else if (tabColorNode.attributes.hasOwnProperty('theme')) color.theme = tabColorNode.attributes.theme;
else if (tabColorNode.attributes.hasOwnProperty('indexed')) color.rgb = colorIndexes[tabColorNode.attributes.indexed];
if (tabColorNode.attributes.hasOwnProperty('tint')) color.tint = tabColorNode.attributes.tint;
return color;
})
.case("string", rgb => this.tabColor({ rgb }))
.case("integer", theme => this.tabColor({ theme }))
.case("nil", () => {
xmlq.removeChild(this._sheetPrNode, "tabColor");
return this;
})
.case("object", color => {
const tabColorNode = xmlq.appendChildIfNotFound(this._sheetPrNode, "tabColor");
xmlq.setAttributes(tabColorNode, {
rgb: color.rgb && color.rgb.toUpperCase(),
indexed: null,
theme: color.theme,
tint: color.tint
});
return this;
})
.handle(arguments);
}
/**
* Gets a value indicating whether this sheet is selected.
* @returns {boolean} True if selected, false if not.
*//**
* Sets whether this sheet is selected.
* @param {boolean} selected - True to select, false to deselected.
* @returns {Sheet} The sheet.
*/
tabSelected() {
const sheetViewNode = this._getOrCreateSheetViewNode();
return new ArgHandler('Sheet.tabSelected')
.case(() => {
return sheetViewNode.attributes.tabSelected === 1;
})
.case('boolean', selected => {
if (selected) sheetViewNode.attributes.tabSelected = 1;
else delete sheetViewNode.attributes.tabSelected;
return this;
})
.handle(arguments);
}
/**
* Get the range of cells in the sheet that have contained a value or style at any point. Useful for extracting the entire sheet contents.
* @returns {Range|undefined} The used range or undefined if no cells in the sheet are used.
*/
usedRange() {
const minRowNumber = _.findIndex(this._rows);
const maxRowNumber = this._rows.length - 1;
let minColumnNumber = 0;
let maxColumnNumber = 0;
for (let i = 0; i < this._rows.length; i++) {
const row = this._rows[i];
if (!row) continue;
const minUsedColumnNumber = row.minUsedColumnNumber();
const maxUsedColumnNumber = row.maxUsedColumnNumber();
if (minUsedColumnNumber > 0 && (!minColumnNumber || minUsedColumnNumber < minColumnNumber)) minColumnNumber = minUsedColumnNumber;
if (maxUsedColumnNumber > 0 && (!maxColumnNumber || maxUsedColumnNumber > maxColumnNumber)) maxColumnNumber = maxUsedColumnNumber;
}
// Return undefined if nothing in the sheet is used.
if (minRowNumber <= 0 || minColumnNumber <= 0 || maxRowNumber <= 0 || maxColumnNumber <= 0) return;
return this.range(minRowNumber, minColumnNumber, maxRowNumber, maxColumnNumber);
}
/**
* Gets the parent workbook.
* @returns {Workbook} The parent workbook.
*/
workbook() {
return this._workbook;
}
/* INTERNAL */
/**
* Clear cells that are using a given shared formula ID.
* @param {number} sharedFormulaId - The shared formula ID.
* @returns {undefined}
* @ignore
*/
clearCellsUsingSharedFormula(sharedFormulaId) {
this._rows.forEach(row => {
if (!row) return;
row.clearCellsUsingSharedFormula(sharedFormulaId);
});
}
/**
* Get an existing column style ID.
* @param {number} columnNumber - The column number.
* @returns {undefined|number} The style ID.
* @ignore
*/
existingColumnStyleId(columnNumber) {
// This will work after setting Column.style because Column updates the attributes live.
const colNode = this._colNodes[columnNumber];
return colNode && colNode.attributes.style;
}
/**
* Call a callback for each column number that has a node defined for it.
* @param {Function} callback - The callback.
* @returns {undefined}
* @ignore
*/
forEachExistingColumnNumber(callback) {
_.forEach(this._colNodes, (node, columnNumber) => {
if (!node) return;
callback(columnNumber);
});
}
/**
* Call a callback for each existing row.
* @param {Function} callback - The callback.
* @returns {undefined}
* @ignore
*/
forEachExistingRow(callback) {
_.forEach(this._rows, (row, rowNumber) => {
if (row) callback(row, rowNumber);
});
return this;
}
/**
* Get the hyperlink attached to the cell with the given address.
* @param {string} address - The address of the hyperlinked cell.
* @returns {string|undefined} The hyperlink or undefined if not set.
* @ignore
*//**
* Set the hyperlink attached to the cell with the given address.
* @param {string} address - The address to of the hyperlinked cell.
* @param {boolean} hyperlink - The hyperlink to set or undefined to clear.
* @returns {Sheet} The sheet.
* @ignore
*/
hyperlink() {
return new ArgHandler('Sheet.hyperlink')
.case('string', address => {
const hyperlinkNode = this._hyperlinks[address];
if (!hyperlinkNode) return;
const relationship = this._relationships.findById(hyperlinkNode.attributes['r:id']);
return relationship && relationship.attributes.Target;
})
.case(['string', 'nil'], address => {
delete this._hyperlinks[address];
return this;
})
.case(['string', 'string'], (address, hyperlink) => {
const relationship = this._relationships.add("hyperlink", hyperlink, "External");
this._hyperlinks[address] = {
name: 'hyperlink',
attributes: { ref: address, 'r:id': relationship.attributes.Id },
children: []
};
return this;
})
.handle(arguments);
}
/**
* Increment and return the max shared formula ID.
* @returns {number} The new max shared formula ID.
* @ignore
*/
incrementMaxSharedFormulaId() {
return ++this._maxSharedFormulaId;
}
/**
* Get a value indicating whether the cells in the given address are merged.
* @param {string} address - The address to check.
* @returns {boolean} True if merged, false if not merged.
* @ignore
*//**
* Merge/unmerge cells by adding/removing a mergeCell entry.
* @param {string} address - The address to merge.
* @param {boolean} merged - True to merge, false to unmerge.
* @returns {Sheet} The sheet.
* @ignore
*/
merged() {
return new ArgHandler('Sheet.merge')
.case('string', address => {
return this._mergeCells.hasOwnProperty(address);
})
.case(['string', '*'], (address, merge) => {
if (merge) {
this._mergeCells[address] = {
name: 'mergeCell',
attributes: { ref: address },
children: []
};
} else {
delete this._mergeCells[address];
}
return this;
})
.handle(arguments);
}
/**
* Gets a Object or undefined of the cells in the given address.
* @param {string} address - The address to check.
* @returns {object|boolean} Object or false if not set
* @ignore
*//**
* Removes dataValidation at the given address
* @param {string} address - The address to remove.
* @param {boolean} obj - false to delete.
* @returns {boolean} true if removed.
* @ignore
*//**
* Add dataValidation to cells at the given address if object or string
* @param {string} address - The address to set.
* @param {object|string} obj - Object or String to set
* @returns {Sheet} The sheet.
* @ignore
*/
dataValidation() {
return new ArgHandler('Sheet.dataValidation')
.case('string', address => {
if (this._dataValidations[address]) {
return {
type: this._dataValidations[address].attributes.type,
allowBlank: this._dataValidations[address].attributes.allowBlank,
showInputMessage: this._dataValidations[address].attributes.showInputMessage,
prompt: this._dataValidations[address].attributes.prompt,
promptTitle: this._dataValidations[address].attributes.promptTitle,
showErrorMessage: this._dataValidations[address].attributes.showErrorMessage,
error: this._dataValidations[address].attributes.error,
errorTitle: this._dataValidations[address].attributes.errorTitle,
operator: this._dataValidations[address].attributes.operator,
formula1: this._dataValidations[address].children[0].children[0],
formula2: this._dataValidations[address].children[1] ? this._dataValidations[address].children[1].children[0] : undefined
};
} else {
return false;
}
})
.case(['string', 'boolean'], (address, obj) => {
if (this._dataValidations[address]) {
if (obj === false) return delete this._dataValidations[address];
} else {
return false;
}
})
.case(['string', '*'], (address, obj) => {
if (typeof obj === 'string') {
this._dataValidations[address] = {
name: 'dataValidation',
attributes: {
type: 'list',
allowBlank: false,
showInputMessage: false,
prompt: '',
promptTitle: '',
showErrorMessage: false,
error: '',
errorTitle: '',
operator: '',
sqref: address
},
children: [
{
name: 'formula1',
atrributes: {},
children: [obj]
},
{
name: 'formula2',
atrributes: {},
children: ['']
}
]
};
} else if (typeof obj === 'object') {
this._dataValidations[address] = {
name: 'dataValidation',
attributes: {
type: obj.type ? obj.type : 'list',
allowBlank: obj.allowBlank,
showInputMessage: obj.showInputMessage,
prompt: obj.prompt,
promptTitle: obj.promptTitle,
showErrorMessage: obj.showErrorMessage,
error: obj.error,
errorTitle: obj.errorTitle,
operator: obj.operator,
sqref: address
},
children: [
{
name: 'formula1',
atrributes: {},
children: [
obj.formula1
]
},
{
name: 'formula2',
atrributes: {},
children: [
obj.formula2
]
}
]
};
}
return this;
})
.handle(arguments);
}
/**
* Convert the sheet to a collection of XML objects.
* @returns {{}} The XML forms.
* @ignore
*/
toXmls() {
// Shallow clone the node so we don't have to remove these children later if they don't belong.
const node = _.clone(this._node);
node.children = node.children.slice();
// Add the columns if needed.
this._colsNode.children = _.filter(this._colNodes, (colNode, i) => {
// Columns should only be present if they have attributes other than min/max.
return colNode && i === colNode.attributes.min && Object.keys(colNode.attributes).length > 2;
});
if (this._colsNode.children.length) {
xmlq.insertInOrder(node, this._colsNode, nodeOrder);
}
// Add the hyperlinks if needed.
this._hyperlinksNode.children = _.values(this._hyperlinks);
if (this._hyperlinksNode.children.length) {
xmlq.insertInOrder(node, this._hyperlinksNode, nodeOrder);
}
// Add the merge cells if needed.
this._mergeCellsNode.children = _.values(this._mergeCells);
if (this._mergeCellsNode.children.length) {
xmlq.insertInOrder(node, this._mergeCellsNode, nodeOrder);
}
// Add the DataValidation cells if needed.
this._dataValidationsNode.children = _.values(this._dataValidations);
if (this._dataValidationsNode.children.length) {
xmlq.insertInOrder(node, this._dataValidationsNode, nodeOrder);
}
if (this._autoFilter) {
xmlq.insertInOrder(node, {
name: "autoFilter",
children: [],
attributes: {
ref: this._autoFilter.address()
}
}, nodeOrder);
}
return {
id: this._idNode,
sheet: node,
relationships: this._relationships
};
}
/**
* Update the max shared formula ID to the given value if greater than current.
* @param {number} sharedFormulaId - The new shared formula ID.
* @returns {undefined}
* @ignore
*/
updateMaxSharedFormulaId(sharedFormulaId) {
if (sharedFormulaId > this._maxSharedFormulaId) {
this._maxSharedFormulaId = sharedFormulaId;
}
}
/* PRIVATE */
/**
* Get the sheet view node if it exists or create it if it doesn't.
* @returns {{}} The sheet view node.
* @private
*/
_getOrCreateSheetViewNode() {
let sheetViewsNode = xmlq.findChild(this._node, "sheetViews");
if (!sheetViewsNode) {
sheetViewsNode = {
name: "sheetViews",
attributes: {},
children: [{
name: "sheetView",
attributes: {
workbookViewId: 0
},
children: []
}]
};
xmlq.insertInOrder(this._node, sheetViewsNode, nodeOrder);
}
return xmlq.findChild(sheetViewsNode, "sheetView");
}
/**
* Initializes the sheet.
* @param {Workbook} workbook - The parent workbook.
* @param {{}} idNode - The sheet ID node (from the parent workbook).
* @param {{}} node - The sheet node.
* @param {{}} [relationshipsNode] - The optional sheet relationships node.
* @returns {undefined}
* @private
*/
_init(workbook, idNode, node, relationshipsNode) {
if (!node) {
node = {
name: "worksheet",
attributes: {
xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
'xmlns:r': "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
'xmlns:mc': "http://schemas.openxmlformats.org/markup-compatibility/2006",
'mc:Ignorable': "x14ac",
'xmlns:x14ac': "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"
},
children: [{
name: "sheetData",
attributes: {},
children: []
}]
};
}
this._workbook = workbook;
this._idNode = idNode;
this._node = node;
this._maxSharedFormulaId = -1;
this._mergeCells = {};
this._dataValidations = {};
this._hyperlinks = {};
this._autoFilter = null;
// Create the relationships.
this._relationships = new Relationships(relationshipsNode);
// Delete the optional dimension node
xmlq.removeChild(this._node, "dimension");
// Create the rows.
this._rows = [];
this._sheetDataNode = xmlq.findChild(this._node, "sheetData");
this._sheetDataNode.children.forEach(rowNode => {
const row = new Row(this, rowNode);
this._rows[row.rowNumber()] = row;
});
this._sheetDataNode.children = this._rows;
// Create the columns node.
this._columns = [];
this._colsNode = xmlq.findChild(this._node, "cols");
if (this._colsNode) {
xmlq.removeChild(this._node, this._colsNode);
} else {
this._colsNode = { name: 'cols', attributes: {}, children: [] };
}
// Cache the col nodes.
this._colNodes = [];
_.forEach(this._colsNode.children, colNode => {
const min = colNode.attributes.min;
const max = colNode.attributes.max;
for (let i = min; i <= max; i++) {
this._colNodes[i] = colNode;
}
});
// Create the sheet properties node.
this._sheetPrNode = xmlq.findChild(this._node, "sheetPr");
if (!this._sheetPrNode) {
this._sheetPrNode = { name: 'sheetPr', attributes: {}, children: [] };
xmlq.insertInOrder(this._node, this._sheetPrNode, nodeOrder);
}
// Create the merge cells.
this._mergeCellsNode = xmlq.findChild(this._node, "mergeCells");
if (this._mergeCellsNode) {
xmlq.removeChild(this._node, this._mergeCellsNode);
} else {
this._mergeCellsNode = { name: 'mergeCells', attributes: {}, children: [] };
}
const mergeCellNodes = this._mergeCellsNode.children;
this._mergeCellsNode.children = [];
mergeCellNodes.forEach(mergeCellNode => {
this._mergeCells[mergeCellNode.attributes.ref] = mergeCellNode;
});
// Create the DataValidations.
this._dataValidationsNode = xmlq.findChild(this._node, "dataValidations");
if (this._dataValidationsNode) {
xmlq.removeChild(this._node, this._dataValidationsNode);
} else {
this._dataValidationsNode = { name: 'dataValidations', attributes: {}, children: [] };
}
const dataValidationNodes = this._dataValidationsNode.children;
this._dataValidationsNode.children = [];
dataValidationNodes.forEach(dataValidationNode => {
this._dataValidations[dataValidationNode.attributes.sqref] = dataValidationNode;
});
// Create the hyperlinks.
this._hyperlinksNode = xmlq.findChild(this._node, "hyperlinks");
if (this._hyperlinksNode) {
xmlq.removeChild(this._node, this._hyperlinksNode);
} else {
this._hyperlinksNode = { name: 'hyperlinks', attributes: {}, children: [] };
}
const hyperlinkNodes = this._hyperlinksNode.children;
this._hyperlinksNode.children = [];
hyperlinkNodes.forEach(hyperlinkNode => {
this._hyperlinks[hyperlinkNode.attributes.ref] = hyperlinkNode;
});
}
}
module.exports = Sheet;