You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					466 lines
				
				12 KiB
			
		
		
			
		
	
	
					466 lines
				
				12 KiB
			| 
											2 years ago
										 | /* 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; |