table.js 12 KB

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