| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124 |
- /* eslint-disable max-classes-per-file */
- const colCache = require('../utils/col-cache');
- const _ = require('../utils/under-dash');
- const Enums = require('./enums');
- const {slideFormula} = require('../utils/shared-formula');
- const Note = require('./note');
- // Cell requirements
- // Operate inside a worksheet
- // Store and retrieve a value with a range of types: text, number, date, hyperlink, reference, formula, etc.
- // Manage/use and manipulate cell format either as local to cell or inherited from column or row.
- class Cell {
- constructor(row, column, address) {
- if (!row || !column) {
- throw new Error('A Cell needs a Row');
- }
- this._row = row;
- this._column = column;
- colCache.validateAddress(address);
- this._address = address;
- // TODO: lazy evaluation of this._value
- this._value = Value.create(Cell.Types.Null, this);
- this.style = this._mergeStyle(row.style, column.style, {});
- this._mergeCount = 0;
- }
- get worksheet() {
- return this._row.worksheet;
- }
- get workbook() {
- return this._row.worksheet.workbook;
- }
- // help GC by removing cyclic (and other) references
- destroy() {
- delete this.style;
- delete this._value;
- delete this._row;
- delete this._column;
- delete this._address;
- }
- // =========================================================================
- // Styles stuff
- get numFmt() {
- return this.style.numFmt;
- }
- set numFmt(value) {
- this.style.numFmt = value;
- }
- get font() {
- return this.style.font;
- }
- set font(value) {
- this.style.font = value;
- }
- get alignment() {
- return this.style.alignment;
- }
- set alignment(value) {
- this.style.alignment = value;
- }
- get border() {
- return this.style.border;
- }
- set border(value) {
- this.style.border = value;
- }
- get fill() {
- return this.style.fill;
- }
- set fill(value) {
- this.style.fill = value;
- }
- get protection() {
- return this.style.protection;
- }
- set protection(value) {
- this.style.protection = value;
- }
- _mergeStyle(rowStyle, colStyle, style) {
- const numFmt = (rowStyle && rowStyle.numFmt) || (colStyle && colStyle.numFmt);
- if (numFmt) style.numFmt = numFmt;
- const font = (rowStyle && rowStyle.font) || (colStyle && colStyle.font);
- if (font) style.font = font;
- const alignment = (rowStyle && rowStyle.alignment) || (colStyle && colStyle.alignment);
- if (alignment) style.alignment = alignment;
- const border = (rowStyle && rowStyle.border) || (colStyle && colStyle.border);
- if (border) style.border = border;
- const fill = (rowStyle && rowStyle.fill) || (colStyle && colStyle.fill);
- if (fill) style.fill = fill;
- const protection = (rowStyle && rowStyle.protection) || (colStyle && colStyle.protection);
- if (protection) style.protection = protection;
- return style;
- }
- // =========================================================================
- // return the address for this cell
- get address() {
- return this._address;
- }
- get row() {
- return this._row.number;
- }
- get col() {
- return this._column.number;
- }
- get $col$row() {
- return `$${this._column.letter}$${this.row}`;
- }
- // =========================================================================
- // Value stuff
- get type() {
- return this._value.type;
- }
- get effectiveType() {
- return this._value.effectiveType;
- }
- toCsvString() {
- return this._value.toCsvString();
- }
- // =========================================================================
- // Merge stuff
- addMergeRef() {
- this._mergeCount++;
- }
- releaseMergeRef() {
- this._mergeCount--;
- }
- get isMerged() {
- return this._mergeCount > 0 || this.type === Cell.Types.Merge;
- }
- merge(master, ignoreStyle) {
- this._value.release();
- this._value = Value.create(Cell.Types.Merge, this, master);
- if (!ignoreStyle) {
- this.style = master.style;
- }
- }
- unmerge() {
- if (this.type === Cell.Types.Merge) {
- this._value.release();
- this._value = Value.create(Cell.Types.Null, this);
- this.style = this._mergeStyle(this._row.style, this._column.style, {});
- }
- }
- isMergedTo(master) {
- if (this._value.type !== Cell.Types.Merge) return false;
- return this._value.isMergedTo(master);
- }
- get master() {
- if (this.type === Cell.Types.Merge) {
- return this._value.master;
- }
- return this; // an unmerged cell is its own master
- }
- get isHyperlink() {
- return this._value.type === Cell.Types.Hyperlink;
- }
- get hyperlink() {
- return this._value.hyperlink;
- }
- // return the value
- get value() {
- return this._value.value;
- }
- // set the value - can be number, string or raw
- set value(v) {
- // special case - merge cells set their master's value
- if (this.type === Cell.Types.Merge) {
- this._value.master.value = v;
- return;
- }
- this._value.release();
- // assign value
- this._value = Value.create(Value.getType(v), this, v);
- }
- get note() {
- return this._comment && this._comment.note;
- }
- set note(note) {
- this._comment = new Note(note);
- }
- get text() {
- return this._value.toString();
- }
- get html() {
- return _.escapeHtml(this.text);
- }
- toString() {
- return this.text;
- }
- _upgradeToHyperlink(hyperlink) {
- // if this cell is a string, turn it into a Hyperlink
- if (this.type === Cell.Types.String) {
- this._value = Value.create(Cell.Types.Hyperlink, this, {
- text: this._value.value,
- hyperlink,
- });
- }
- }
- // =========================================================================
- // Formula stuff
- get formula() {
- return this._value.formula;
- }
- get result() {
- return this._value.result;
- }
- get formulaType() {
- return this._value.formulaType;
- }
- // =========================================================================
- // Name stuff
- get fullAddress() {
- const {worksheet} = this._row;
- return {
- sheetName: worksheet.name,
- address: this.address,
- row: this.row,
- col: this.col,
- };
- }
- get name() {
- return this.names[0];
- }
- set name(value) {
- this.names = [value];
- }
- get names() {
- return this.workbook.definedNames.getNamesEx(this.fullAddress);
- }
- set names(value) {
- const {definedNames} = this.workbook;
- definedNames.removeAllNames(this.fullAddress);
- value.forEach(name => {
- definedNames.addEx(this.fullAddress, name);
- });
- }
- addName(name) {
- this.workbook.definedNames.addEx(this.fullAddress, name);
- }
- removeName(name) {
- this.workbook.definedNames.removeEx(this.fullAddress, name);
- }
- removeAllNames() {
- this.workbook.definedNames.removeAllNames(this.fullAddress);
- }
- // =========================================================================
- // Data Validation stuff
- get _dataValidations() {
- return this.worksheet.dataValidations;
- }
- get dataValidation() {
- return this._dataValidations.find(this.address);
- }
- set dataValidation(value) {
- this._dataValidations.add(this.address, value);
- }
- // =========================================================================
- // Model stuff
- get model() {
- const {model} = this._value;
- model.style = this.style;
- if (this._comment) {
- model.comment = this._comment.model;
- }
- return model;
- }
- set model(value) {
- this._value.release();
- this._value = Value.create(value.type, this);
- this._value.model = value;
- if (value.comment) {
- switch (value.comment.type) {
- case 'note':
- this._comment = Note.fromModel(value.comment);
- break;
- }
- }
- if (value.style) {
- this.style = value.style;
- } else {
- this.style = {};
- }
- }
- }
- Cell.Types = Enums.ValueType;
- // =============================================================================
- // Internal Value Types
- class NullValue {
- constructor(cell) {
- this.model = {
- address: cell.address,
- type: Cell.Types.Null,
- };
- }
- get value() {
- return null;
- }
- set value(value) {
- // nothing to do
- }
- get type() {
- return Cell.Types.Null;
- }
- get effectiveType() {
- return Cell.Types.Null;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return '';
- }
- release() {}
- toString() {
- return '';
- }
- }
- class NumberValue {
- constructor(cell, value) {
- this.model = {
- address: cell.address,
- type: Cell.Types.Number,
- value,
- };
- }
- get value() {
- return this.model.value;
- }
- set value(value) {
- this.model.value = value;
- }
- get type() {
- return Cell.Types.Number;
- }
- get effectiveType() {
- return Cell.Types.Number;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return this.model.value.toString();
- }
- release() {}
- toString() {
- return this.model.value.toString();
- }
- }
- class StringValue {
- constructor(cell, value) {
- this.model = {
- address: cell.address,
- type: Cell.Types.String,
- value,
- };
- }
- get value() {
- return this.model.value;
- }
- set value(value) {
- this.model.value = value;
- }
- get type() {
- return Cell.Types.String;
- }
- get effectiveType() {
- return Cell.Types.String;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return `"${this.model.value.replace(/"/g, '""')}"`;
- }
- release() {}
- toString() {
- return this.model.value;
- }
- }
- class RichTextValue {
- constructor(cell, value) {
- this.model = {
- address: cell.address,
- type: Cell.Types.String,
- value,
- };
- }
- get value() {
- return this.model.value;
- }
- set value(value) {
- this.model.value = value;
- }
- toString() {
- return this.model.value.richText.map(t => t.text).join('');
- }
- get type() {
- return Cell.Types.RichText;
- }
- get effectiveType() {
- return Cell.Types.RichText;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return `"${this.text.replace(/"/g, '""')}"`;
- }
- release() {}
- }
- class DateValue {
- constructor(cell, value) {
- this.model = {
- address: cell.address,
- type: Cell.Types.Date,
- value,
- };
- }
- get value() {
- return this.model.value;
- }
- set value(value) {
- this.model.value = value;
- }
- get type() {
- return Cell.Types.Date;
- }
- get effectiveType() {
- return Cell.Types.Date;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return this.model.value.toISOString();
- }
- release() {}
- toString() {
- return this.model.value.toString();
- }
- }
- class HyperlinkValue {
- constructor(cell, value) {
- this.model = {
- address: cell.address,
- type: Cell.Types.Hyperlink,
- text: value ? value.text : undefined,
- hyperlink: value ? value.hyperlink : undefined,
- };
- if (value && value.tooltip) {
- this.model.tooltip = value.tooltip;
- }
- }
- get value() {
- const v = {
- text: this.model.text,
- hyperlink: this.model.hyperlink,
- };
- if (this.model.tooltip) {
- v.tooltip = this.model.tooltip;
- }
- return v;
- }
- set value(value) {
- this.model = {
- text: value.text,
- hyperlink: value.hyperlink,
- };
- if (value.tooltip) {
- this.model.tooltip = value.tooltip;
- }
- }
- get text() {
- return this.model.text;
- }
- set text(value) {
- this.model.text = value;
- }
- /*
- get tooltip() {
- return this.model.tooltip;
- }
- set tooltip(value) {
- this.model.tooltip = value;
- } */
- get hyperlink() {
- return this.model.hyperlink;
- }
- set hyperlink(value) {
- this.model.hyperlink = value;
- }
- get type() {
- return Cell.Types.Hyperlink;
- }
- get effectiveType() {
- return Cell.Types.Hyperlink;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return this.model.hyperlink;
- }
- release() {}
- toString() {
- return this.model.text;
- }
- }
- class MergeValue {
- constructor(cell, master) {
- this.model = {
- address: cell.address,
- type: Cell.Types.Merge,
- master: master ? master.address : undefined,
- };
- this._master = master;
- if (master) {
- master.addMergeRef();
- }
- }
- get value() {
- return this._master.value;
- }
- set value(value) {
- if (value instanceof Cell) {
- if (this._master) {
- this._master.releaseMergeRef();
- }
- value.addMergeRef();
- this._master = value;
- } else {
- this._master.value = value;
- }
- }
- isMergedTo(master) {
- return master === this._master;
- }
- get master() {
- return this._master;
- }
- get type() {
- return Cell.Types.Merge;
- }
- get effectiveType() {
- return this._master.effectiveType;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return '';
- }
- release() {
- this._master.releaseMergeRef();
- }
- toString() {
- return this.value.toString();
- }
- }
- class FormulaValue {
- constructor(cell, value) {
- this.cell = cell;
- this.model = {
- address: cell.address,
- type: Cell.Types.Formula,
- shareType: value ? value.shareType : undefined,
- ref: value ? value.ref : undefined,
- formula: value ? value.formula : undefined,
- sharedFormula: value ? value.sharedFormula : undefined,
- result: value ? value.result : undefined,
- };
- }
- _copyModel(model) {
- const copy = {};
- const cp = name => {
- const value = model[name];
- if (value) {
- copy[name] = value;
- }
- };
- cp('formula');
- cp('result');
- cp('ref');
- cp('shareType');
- cp('sharedFormula');
- return copy;
- }
- get value() {
- return this._copyModel(this.model);
- }
- set value(value) {
- this.model = this._copyModel(value);
- }
- validate(value) {
- switch (Value.getType(value)) {
- case Cell.Types.Null:
- case Cell.Types.String:
- case Cell.Types.Number:
- case Cell.Types.Date:
- break;
- case Cell.Types.Hyperlink:
- case Cell.Types.Formula:
- default:
- throw new Error('Cannot process that type of result value');
- }
- }
- get dependencies() {
- // find all the ranges and cells mentioned in the formula
- const ranges = this.formula.match(/([a-zA-Z0-9]+!)?[A-Z]{1,3}\d{1,4}:[A-Z]{1,3}\d{1,4}/g);
- const cells = this.formula
- .replace(/([a-zA-Z0-9]+!)?[A-Z]{1,3}\d{1,4}:[A-Z]{1,3}\d{1,4}/g, '')
- .match(/([a-zA-Z0-9]+!)?[A-Z]{1,3}\d{1,4}/g);
- return {
- ranges,
- cells,
- };
- }
- get formula() {
- return this.model.formula || this._getTranslatedFormula();
- }
- set formula(value) {
- this.model.formula = value;
- }
- get formulaType() {
- if (this.model.formula) {
- return Enums.FormulaType.Master;
- }
- if (this.model.sharedFormula) {
- return Enums.FormulaType.Shared;
- }
- return Enums.FormulaType.None;
- }
- get result() {
- return this.model.result;
- }
- set result(value) {
- this.model.result = value;
- }
- get type() {
- return Cell.Types.Formula;
- }
- get effectiveType() {
- const v = this.model.result;
- if (v === null || v === undefined) {
- return Enums.ValueType.Null;
- }
- if (v instanceof String || typeof v === 'string') {
- return Enums.ValueType.String;
- }
- if (typeof v === 'number') {
- return Enums.ValueType.Number;
- }
- if (v instanceof Date) {
- return Enums.ValueType.Date;
- }
- if (v.text && v.hyperlink) {
- return Enums.ValueType.Hyperlink;
- }
- if (v.formula) {
- return Enums.ValueType.Formula;
- }
- return Enums.ValueType.Null;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- _getTranslatedFormula() {
- if (!this._translatedFormula && this.model.sharedFormula) {
- const {worksheet} = this.cell;
- const master = worksheet.findCell(this.model.sharedFormula);
- this._translatedFormula =
- master && slideFormula(master.formula, master.address, this.model.address);
- }
- return this._translatedFormula;
- }
- toCsvString() {
- return `${this.model.result || ''}`;
- }
- release() {}
- toString() {
- return this.model.result ? this.model.result.toString() : '';
- }
- }
- class SharedStringValue {
- constructor(cell, value) {
- this.model = {
- address: cell.address,
- type: Cell.Types.SharedString,
- value,
- };
- }
- get value() {
- return this.model.value;
- }
- set value(value) {
- this.model.value = value;
- }
- get type() {
- return Cell.Types.SharedString;
- }
- get effectiveType() {
- return Cell.Types.SharedString;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return this.model.value.toString();
- }
- release() {}
- toString() {
- return this.model.value.toString();
- }
- }
- class BooleanValue {
- constructor(cell, value) {
- this.model = {
- address: cell.address,
- type: Cell.Types.Boolean,
- value,
- };
- }
- get value() {
- return this.model.value;
- }
- set value(value) {
- this.model.value = value;
- }
- get type() {
- return Cell.Types.Boolean;
- }
- get effectiveType() {
- return Cell.Types.Boolean;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return this.model.value ? 1 : 0;
- }
- release() {}
- toString() {
- return this.model.value.toString();
- }
- }
- class ErrorValue {
- constructor(cell, value) {
- this.model = {
- address: cell.address,
- type: Cell.Types.Error,
- value,
- };
- }
- get value() {
- return this.model.value;
- }
- set value(value) {
- this.model.value = value;
- }
- get type() {
- return Cell.Types.Error;
- }
- get effectiveType() {
- return Cell.Types.Error;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return this.toString();
- }
- release() {}
- toString() {
- return this.model.value.error.toString();
- }
- }
- class JSONValue {
- constructor(cell, value) {
- this.model = {
- address: cell.address,
- type: Cell.Types.String,
- value: JSON.stringify(value),
- rawValue: value,
- };
- }
- get value() {
- return this.model.rawValue;
- }
- set value(value) {
- this.model.rawValue = value;
- this.model.value = JSON.stringify(value);
- }
- get type() {
- return Cell.Types.String;
- }
- get effectiveType() {
- return Cell.Types.String;
- }
- get address() {
- return this.model.address;
- }
- set address(value) {
- this.model.address = value;
- }
- toCsvString() {
- return this.model.value;
- }
- release() {}
- toString() {
- return this.model.value;
- }
- }
- // Value is a place to hold common static Value type functions
- const Value = {
- getType(value) {
- if (value === null || value === undefined) {
- return Cell.Types.Null;
- }
- if (value instanceof String || typeof value === 'string') {
- return Cell.Types.String;
- }
- if (typeof value === 'number') {
- return Cell.Types.Number;
- }
- if (typeof value === 'boolean') {
- return Cell.Types.Boolean;
- }
- if (value instanceof Date) {
- return Cell.Types.Date;
- }
- if (value.text && value.hyperlink) {
- return Cell.Types.Hyperlink;
- }
- if (value.formula || value.sharedFormula) {
- return Cell.Types.Formula;
- }
- if (value.richText) {
- return Cell.Types.RichText;
- }
- if (value.sharedString) {
- return Cell.Types.SharedString;
- }
- if (value.error) {
- return Cell.Types.Error;
- }
- return Cell.Types.JSON;
- },
- // map valueType to constructor
- types: [
- {t: Cell.Types.Null, f: NullValue},
- {t: Cell.Types.Number, f: NumberValue},
- {t: Cell.Types.String, f: StringValue},
- {t: Cell.Types.Date, f: DateValue},
- {t: Cell.Types.Hyperlink, f: HyperlinkValue},
- {t: Cell.Types.Formula, f: FormulaValue},
- {t: Cell.Types.Merge, f: MergeValue},
- {t: Cell.Types.JSON, f: JSONValue},
- {t: Cell.Types.SharedString, f: SharedStringValue},
- {t: Cell.Types.RichText, f: RichTextValue},
- {t: Cell.Types.Boolean, f: BooleanValue},
- {t: Cell.Types.Error, f: ErrorValue},
- ].reduce((p, t) => {
- p[t.t] = t.f;
- return p;
- }, []),
- create(type, cell, value) {
- const T = this.types[type];
- if (!T) {
- throw new Error(`Could not create Value of type ${type}`);
- }
- return new T(cell, value);
- },
- };
- module.exports = Cell;
|