table.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. /* eslint-disable max-classes-per-file */
  2. const colCache = require('../utils/col-cache');
  3. class Column {
  4. // wrapper around column model, allowing access and manipulation
  5. constructor(table, column, index) {
  6. this.table = table;
  7. this.column = column;
  8. this.index = index;
  9. }
  10. _set(name, value) {
  11. this.table.cacheState();
  12. this.column[name] = value;
  13. }
  14. /* eslint-disable lines-between-class-members */
  15. get name() {
  16. return this.column.name;
  17. }
  18. set name(value) {
  19. this._set('name', value);
  20. }
  21. get filterButton() {
  22. return this.column.filterButton;
  23. }
  24. set filterButton(value) {
  25. this.column.filterButton = value;
  26. }
  27. get style() {
  28. return this.column.style;
  29. }
  30. set style(value) {
  31. this.column.style = value;
  32. }
  33. get totalsRowLabel() {
  34. return this.column.totalsRowLabel;
  35. }
  36. set totalsRowLabel(value) {
  37. this._set('totalsRowLabel', value);
  38. }
  39. get totalsRowFunction() {
  40. return this.column.totalsRowFunction;
  41. }
  42. set totalsRowFunction(value) {
  43. this._set('totalsRowFunction', value);
  44. }
  45. get totalsRowResult() {
  46. return this.column.totalsRowResult;
  47. }
  48. set totalsRowResult(value) {
  49. this._set('totalsRowResult', value);
  50. }
  51. get totalsRowFormula() {
  52. return this.column.totalsRowFormula;
  53. }
  54. set totalsRowFormula(value) {
  55. this._set('totalsRowFormula', value);
  56. }
  57. /* eslint-enable lines-between-class-members */
  58. }
  59. class Table {
  60. constructor(worksheet, table) {
  61. this.worksheet = worksheet;
  62. if (table) {
  63. this.table = table;
  64. // check things are ok first
  65. this.validate();
  66. this.store();
  67. }
  68. }
  69. getFormula(column) {
  70. // get the correct formula to apply to the totals row
  71. switch (column.totalsRowFunction) {
  72. case 'none':
  73. return null;
  74. case 'average':
  75. return `SUBTOTAL(101,${this.table.name}[${column.name}])`;
  76. case 'countNums':
  77. return `SUBTOTAL(102,${this.table.name}[${column.name}])`;
  78. case 'count':
  79. return `SUBTOTAL(103,${this.table.name}[${column.name}])`;
  80. case 'max':
  81. return `SUBTOTAL(104,${this.table.name}[${column.name}])`;
  82. case 'min':
  83. return `SUBTOTAL(105,${this.table.name}[${column.name}])`;
  84. case 'stdDev':
  85. return `SUBTOTAL(106,${this.table.name}[${column.name}])`;
  86. case 'var':
  87. return `SUBTOTAL(107,${this.table.name}[${column.name}])`;
  88. case 'sum':
  89. return `SUBTOTAL(109,${this.table.name}[${column.name}])`;
  90. case 'custom':
  91. return column.totalsRowFormula;
  92. default:
  93. throw new Error(`Invalid Totals Row Function: ${column.totalsRowFunction}`);
  94. }
  95. }
  96. get width() {
  97. // width of the table
  98. return this.table.columns.length;
  99. }
  100. get height() {
  101. // height of the table data
  102. return this.table.rows.length;
  103. }
  104. get filterHeight() {
  105. // height of the table data plus optional header row
  106. return this.height + (this.table.headerRow ? 1 : 0);
  107. }
  108. get tableHeight() {
  109. // full height of the table on the sheet
  110. return this.filterHeight + (this.table.totalsRow ? 1 : 0);
  111. }
  112. validate() {
  113. const {table} = this;
  114. // set defaults and check is valid
  115. const assign = (o, name, dflt) => {
  116. if (o[name] === undefined) {
  117. o[name] = dflt;
  118. }
  119. };
  120. assign(table, 'headerRow', true);
  121. assign(table, 'totalsRow', false);
  122. assign(table, 'style', {});
  123. assign(table.style, 'theme', 'TableStyleMedium2');
  124. assign(table.style, 'showFirstColumn', false);
  125. assign(table.style, 'showLastColumn', false);
  126. assign(table.style, 'showRowStripes', false);
  127. assign(table.style, 'showColumnStripes', false);
  128. const assert = (test, message) => {
  129. if (!test) {
  130. throw new Error(message);
  131. }
  132. };
  133. assert(table.ref, 'Table must have ref');
  134. assert(table.columns, 'Table must have column definitions');
  135. assert(table.rows, 'Table must have row definitions');
  136. table.tl = colCache.decodeAddress(table.ref);
  137. const {row, col} = table.tl;
  138. assert(row > 0, 'Table must be on valid row');
  139. assert(col > 0, 'Table must be on valid col');
  140. const {width, filterHeight, tableHeight} = this;
  141. // autoFilterRef is a range that includes optional headers only
  142. table.autoFilterRef = colCache.encode(row, col, row + filterHeight - 1, col + width - 1);
  143. // tableRef is a range that includes optional headers and totals
  144. table.tableRef = colCache.encode(row, col, row + tableHeight - 1, col + width - 1);
  145. table.columns.forEach((column, i) => {
  146. assert(column.name, `Column ${i} must have a name`);
  147. if (i === 0) {
  148. assign(column, 'totalsRowLabel', 'Total');
  149. } else {
  150. assign(column, 'totalsRowFunction', 'none');
  151. column.totalsRowFormula = this.getFormula(column);
  152. }
  153. });
  154. }
  155. store() {
  156. // where the table needs to store table data, headers, footers in
  157. // the sheet...
  158. const assignStyle = (cell, style) => {
  159. if (style) {
  160. Object.keys(style).forEach(key => {
  161. cell[key] = style[key];
  162. });
  163. }
  164. };
  165. const {worksheet, table} = this;
  166. const {row, col} = table.tl;
  167. let count = 0;
  168. if (table.headerRow) {
  169. const r = worksheet.getRow(row + count++);
  170. table.columns.forEach((column, j) => {
  171. const {style, name} = column;
  172. const cell = r.getCell(col + j);
  173. cell.value = name;
  174. assignStyle(cell, style);
  175. });
  176. }
  177. table.rows.forEach(data => {
  178. const r = worksheet.getRow(row + count++);
  179. data.forEach((value, j) => {
  180. const cell = r.getCell(col + j);
  181. cell.value = value;
  182. assignStyle(cell, table.columns[j].style);
  183. });
  184. });
  185. if (table.totalsRow) {
  186. const r = worksheet.getRow(row + count++);
  187. table.columns.forEach((column, j) => {
  188. const cell = r.getCell(col + j);
  189. if (j === 0) {
  190. cell.value = column.totalsRowLabel;
  191. } else {
  192. const formula = this.getFormula(column);
  193. if (formula) {
  194. cell.value = {
  195. formula: column.totalsRowFormula,
  196. result: column.totalsRowResult,
  197. };
  198. } else {
  199. cell.value = null;
  200. }
  201. }
  202. assignStyle(cell, column.style);
  203. });
  204. }
  205. }
  206. load(worksheet) {
  207. // where the table will read necessary features from a loaded sheet
  208. const {table} = this;
  209. const {row, col} = table.tl;
  210. let count = 0;
  211. if (table.headerRow) {
  212. const r = worksheet.getRow(row + count++);
  213. table.columns.forEach((column, j) => {
  214. const cell = r.getCell(col + j);
  215. cell.value = column.name;
  216. });
  217. }
  218. table.rows.forEach(data => {
  219. const r = worksheet.getRow(row + count++);
  220. data.forEach((value, j) => {
  221. const cell = r.getCell(col + j);
  222. cell.value = value;
  223. });
  224. });
  225. if (table.totalsRow) {
  226. const r = worksheet.getRow(row + count++);
  227. table.columns.forEach((column, j) => {
  228. const cell = r.getCell(col + j);
  229. if (j === 0) {
  230. cell.value = column.totalsRowLabel;
  231. } else {
  232. const formula = this.getFormula(column);
  233. if (formula) {
  234. cell.value = {
  235. formula: column.totalsRowFormula,
  236. result: column.totalsRowResult,
  237. };
  238. }
  239. }
  240. });
  241. }
  242. }
  243. get model() {
  244. return this.table;
  245. }
  246. set model(value) {
  247. this.table = value;
  248. }
  249. // ================================================================
  250. // TODO: Mutating methods
  251. cacheState() {
  252. if (!this._cache) {
  253. this._cache = {
  254. ref: this.ref,
  255. width: this.width,
  256. tableHeight: this.tableHeight,
  257. };
  258. }
  259. }
  260. commit() {
  261. // changes may have been made that might have on-sheet effects
  262. if (!this._cache) {
  263. return;
  264. }
  265. // check things are ok first
  266. this.validate();
  267. const ref = colCache.decodeAddress(this._cache.ref);
  268. if (this.ref !== this._cache.ref) {
  269. // wipe out whole table footprint at previous location
  270. for (let i = 0; i < this._cache.tableHeight; i++) {
  271. const row = this.worksheet.getRow(ref.row + i);
  272. for (let j = 0; j < this._cache.width; j++) {
  273. const cell = row.getCell(ref.col + j);
  274. cell.value = null;
  275. }
  276. }
  277. } else {
  278. // clear out below table if it has shrunk
  279. for (let i = this.tableHeight; i < this._cache.tableHeight; i++) {
  280. const row = this.worksheet.getRow(ref.row + i);
  281. for (let j = 0; j < this._cache.width; j++) {
  282. const cell = row.getCell(ref.col + j);
  283. cell.value = null;
  284. }
  285. }
  286. // clear out to right of table if it has lost columns
  287. for (let i = 0; i < this.tableHeight; i++) {
  288. const row = this.worksheet.getRow(ref.row + i);
  289. for (let j = this.width; j < this._cache.width; j++) {
  290. const cell = row.getCell(ref.col + j);
  291. cell.value = null;
  292. }
  293. }
  294. }
  295. this.store();
  296. }
  297. addRow(values, rowNumber) {
  298. // Add a row of data, either insert at rowNumber or append
  299. this.cacheState();
  300. if (rowNumber === undefined) {
  301. this.table.rows.push(values);
  302. } else {
  303. this.table.rows.splice(rowNumber, 0, values);
  304. }
  305. }
  306. removeRows(rowIndex, count = 1) {
  307. // Remove a rows of data
  308. this.cacheState();
  309. this.table.rows.splice(rowIndex, count);
  310. }
  311. getColumn(colIndex) {
  312. const column = this.table.columns[colIndex];
  313. return new Column(this, column, colIndex);
  314. }
  315. addColumn(column, values, colIndex) {
  316. // Add a new column, including column defn and values
  317. // Inserts at colNumber or adds to the right
  318. this.cacheState();
  319. if (colIndex === undefined) {
  320. this.table.columns.push(column);
  321. this.table.rows.forEach((row, i) => {
  322. row.push(values[i]);
  323. });
  324. } else {
  325. this.table.columns.splice(colIndex, 0, column);
  326. this.table.rows.forEach((row, i) => {
  327. row.splice(colIndex, 0, values[i]);
  328. });
  329. }
  330. }
  331. removeColumns(colIndex, count = 1) {
  332. // Remove a column with data
  333. this.cacheState();
  334. this.table.columns.splice(colIndex, count);
  335. this.table.rows.forEach(row => {
  336. row.splice(colIndex, count);
  337. });
  338. }
  339. _assign(target, prop, value) {
  340. this.cacheState();
  341. target[prop] = value;
  342. }
  343. /* eslint-disable lines-between-class-members */
  344. get ref() {
  345. return this.table.ref;
  346. }
  347. set ref(value) {
  348. this._assign(this.table, 'ref', value);
  349. }
  350. get name() {
  351. return this.table.name;
  352. }
  353. set name(value) {
  354. this.table.name = value;
  355. }
  356. get displayName() {
  357. return this.table.displyName || this.table.name;
  358. }
  359. set displayNamename(value) {
  360. this.table.displayName = value;
  361. }
  362. get headerRow() {
  363. return this.table.headerRow;
  364. }
  365. set headerRow(value) {
  366. this._assign(this.table, 'headerRow', value);
  367. }
  368. get totalsRow() {
  369. return this.table.totalsRow;
  370. }
  371. set totalsRow(value) {
  372. this._assign(this.table, 'totalsRow', value);
  373. }
  374. get theme() {
  375. return this.table.style.name;
  376. }
  377. set theme(value) {
  378. this.table.style.name = value;
  379. }
  380. get showFirstColumn() {
  381. return this.table.style.showFirstColumn;
  382. }
  383. set showFirstColumn(value) {
  384. this.table.style.showFirstColumn = value;
  385. }
  386. get showLastColumn() {
  387. return this.table.style.showLastColumn;
  388. }
  389. set showLastColumn(value) {
  390. this.table.style.showLastColumn = value;
  391. }
  392. get showRowStripes() {
  393. return this.table.style.showRowStripes;
  394. }
  395. set showRowStripes(value) {
  396. this.table.style.showRowStripes = value;
  397. }
  398. get showColumnStripes() {
  399. return this.table.style.showColumnStripes;
  400. }
  401. set showColumnStripes(value) {
  402. this.table.style.showColumnStripes = value;
  403. }
  404. /* eslint-enable lines-between-class-members */
  405. }
  406. module.exports = Table;