UNPKG

@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.

358 lines (318 loc) 10.7 kB
"use strict"; const _ = require("lodash"); const Cell = require("./Cell"); const regexify = require("./regexify"); const ArgHandler = require("./ArgHandler"); const addressConverter = require('./addressConverter'); /** * A row. */ class Row { // /** // * Creates a new instance of Row. // * @param {Sheet} sheet - The parent sheet. // * @param {{}} node - The row node. // */ constructor(sheet, node) { this._sheet = sheet; this._init(node); } /* PUBLIC */ /** * Get the address of the row. * @param {{}} [opts] - Options * @param {boolean} [opts.includeSheetName] - Include the sheet name in the address. * @param {boolean} [opts.anchored] - Anchor the address. * @returns {string} The address */ address(opts) { return addressConverter.toAddress({ type: 'row', rowNumber: this.rowNumber(), sheetName: opts && opts.includeSheetName && this.sheet().name(), rowAnchored: opts && opts.anchored }); } /** * Get a cell in the row. * @param {string|number} columnNameOrNumber - The name or number of the column. * @returns {Cell} The cell. */ cell(columnNameOrNumber) { let columnNumber = columnNameOrNumber; if (typeof columnNameOrNumber === 'string') { columnNumber = addressConverter.columnNameToNumber(columnNameOrNumber); } // Return an existing cell. if (this._cells[columnNumber]) return this._cells[columnNumber]; // No cell exists for this. // Check if there is an existing row/column style for the new cell. let styleId; const rowStyleId = this._node.attributes.s; const columnStyleId = this.sheet().existingColumnStyleId(columnNumber); // Row style takes priority. If a cell has both row and column styles it should have created a cell entry with a cell-specific style. if (!_.isNil(rowStyleId)) styleId = rowStyleId; else if (!_.isNil(columnStyleId)) styleId = columnStyleId; // Create the new cell. const cell = new Cell(this, columnNumber, styleId); this._cells[columnNumber] = cell; return cell; } /** * Gets the row height. * @returns {undefined|number} The height (or undefined). *//** * Sets the row height. * @param {number} height - The height of the row. * @returns {Row} The row. */ height() { return new ArgHandler('Row.height') .case(() => { return this._node.attributes.customHeight ? this._node.attributes.ht : undefined; }) .case('number', height => { this._node.attributes.ht = height; this._node.attributes.customHeight = 1; return this; }) .case('nil', () => { delete this._node.attributes.ht; delete this._node.attributes.customHeight; return this; }) .handle(arguments); } /** * Gets a value indicating whether the row is hidden. * @returns {boolean} A flag indicating whether the row is hidden. *//** * Sets whether the row is hidden. * @param {boolean} hidden - A flag indicating whether to hide the row. * @returns {Row} The row. */ hidden() { return new ArgHandler("Row.hidden") .case(() => { return this._node.attributes.hidden === 1; }) .case('boolean', hidden => { if (hidden) this._node.attributes.hidden = 1; else delete this._node.attributes.hidden; return this; }) .handle(arguments); } /** * Gets the row number. * @returns {number} The row number. */ rowNumber() { return this._node.attributes.r; } /** * Gets the parent sheet of the row. * @returns {Sheet} The parent sheet. */ sheet() { return this._sheet; } /** * Gets an individual style. * @param {string} name - The name of the style. * @returns {*} The style. *//** * Gets multiple styles. * @param {Array.<string>} names - The names of the style. * @returns {object.<string, *>} Object whose keys are the style names and values are the styles. *//** * Sets an individual style. * @param {string} name - The name of the style. * @param {*} value - The value to set. * @returns {Cell} The cell. *//** * Sets multiple styles. * @param {object.<string, *>} styles - Object whose keys are the style names and values are the styles to set. * @returns {Cell} The cell. *//** * Sets to a specific style * @param {Style} style - Style object given from stylesheet.createStyle * @returns {Cell} The cell. */ style() { return new ArgHandler("Row.style") .case('string', name => { // Get single value this._createStyleIfNeeded(); return this._style.style(name); }) .case('array', names => { // Get list of values const values = {}; names.forEach(name => { values[name] = this.style(name); }); return values; }) .case(['string', '*'], (name, value) => { this._createCellStylesIfNeeded(); // Style each existing cell within this row. (Cells don't inherit ow/column styles.) _.forEach(this._cells, cell => { if (cell) cell.style(name, value); }); // Set the style on the row. this._createStyleIfNeeded(); this._style.style(name, value); return this; }) .case('object', nameValues => { // Object of key value pairs to set for (const name in nameValues) { if (!nameValues.hasOwnProperty(name)) continue; const value = nameValues[name]; this.style(name, value); } return this; }) .case('Style', style => { this._createCellStylesIfNeeded(); // Style each existing cell within this row. (Cells don't inherit ow/column styles.) _.forEach(this._cells, cell => { if (cell) cell.style(style); }); this._style = style; this._node.attributes.s = style.id(); this._node.attributes.customFormat = 1; return this; }) .handle(arguments); } /** * Get the parent workbook. * @returns {Workbook} The parent workbook. */ workbook() { return this.sheet().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._cells.forEach(cell => { if (!cell) return; if (cell.sharesFormula(sharedFormulaId)) cell.clear(); }); } /** * Find a pattern in the row and optionally replace it. * @param {string|RegExp} pattern - The search pattern. * @param {string} [replacement] - The replacement text. * @returns {Array.<Cell>} The matched cells. * @ignore */ find(pattern, replacement) { pattern = regexify(pattern); const matches = []; this._cells.forEach(cell => { if (!cell) return; if (cell.find(pattern, replacement)) matches.push(cell); }); return matches; } /** * Check if the row has a cell at the given column number. * @param {number} columnNumber - The column number. * @returns {boolean} True if a cell exists, false otherwise. * @ignore */ hasCell(columnNumber) { return !!this._cells[columnNumber]; } /** * Check if the column has a style defined. * @returns {boolean} True if a style exists, false otherwise. * @ignore */ hasStyle() { return !_.isNil(this._node.attributes.s); } /** * Returns the nax used column number. * @returns {number} The max used column number. * @ignore */ minUsedColumnNumber() { return _.findIndex(this._cells); } /** * Returns the nax used column number. * @returns {number} The max used column number. * @ignore */ maxUsedColumnNumber() { return this._cells.length - 1; } /** * Convert the row to an object. * @returns {{}} The object form. * @ignore */ toXml() { return this._node; } /* PRIVATE */ /** * If a column node is already defined that intersects with this row and that column has a style set, we * need to make sure that a cell node exists at the intersection so we can style it appropriately. * Fetching the cell will force a new cell node to be created with a style matching the column. * @returns {undefined} * @private */ _createCellStylesIfNeeded() { this.sheet().forEachExistingColumnNumber(columnNumber => { if (!_.isNil(this.sheet().existingColumnStyleId(columnNumber))) this.cell(columnNumber); }); } /** * Create a style for this row if it doesn't already exist. * @returns {undefined} * @private */ _createStyleIfNeeded() { if (!this._style) { const styleId = this._node.attributes.s; this._style = this.workbook().styleSheet().createStyle(styleId); this._node.attributes.s = this._style.id(); this._node.attributes.customFormat = 1; } } /** * Initialize the row node. * @param {{}} node - The row node. * @returns {undefined} * @private */ _init(node) { this._node = node; this._cells = []; this._node.children.forEach(cellNode => { const cell = new Cell(this, cellNode); this._cells[cell.columnNumber()] = cell; }); this._node.children = this._cells; } } module.exports = Row; /* <row r="6" spans="1:9" x14ac:dyDescent="0.25"> <c r="A6" s="1" t="s"> <v>2</v> </c> <c r="B6" s="1"/> <c r="C6" s="1"/> </row> */