ghostTable.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. import { addClass, outerHeight, outerWidth } from './../helpers/dom/element';
  2. import { arrayEach } from './../helpers/array';
  3. /**
  4. * @class GhostTable
  5. * @util
  6. */
  7. class GhostTable {
  8. constructor(hotInstance) {
  9. /**
  10. * Handsontable instance.
  11. *
  12. * @type {Core}
  13. */
  14. this.hot = hotInstance;
  15. /**
  16. * Container element where every table will be injected.
  17. *
  18. * @type {HTMLElement|null}
  19. */
  20. this.container = null;
  21. /**
  22. * Flag which determine is table was injected to DOM.
  23. *
  24. * @type {Boolean}
  25. */
  26. this.injected = false;
  27. /**
  28. * Added rows collection.
  29. *
  30. * @type {Array}
  31. */
  32. this.rows = [];
  33. /**
  34. * Added columns collection.
  35. *
  36. * @type {Array}
  37. */
  38. this.columns = [];
  39. /**
  40. * Samples prepared for calculations.
  41. *
  42. * @type {Map}
  43. * @default {null}
  44. */
  45. this.samples = null;
  46. /**
  47. * Ghost table settings.
  48. *
  49. * @type {Object}
  50. * @default {Object}
  51. */
  52. this.settings = {
  53. useHeaders: true
  54. };
  55. }
  56. /**
  57. * Add row.
  58. *
  59. * @param {Number} row Row index.
  60. * @param {Map} samples Samples Map object.
  61. */
  62. addRow(row, samples) {
  63. if (this.columns.length) {
  64. throw new Error('Doesn\'t support multi-dimensional table');
  65. }
  66. if (!this.rows.length) {
  67. this.container = this.createContainer(this.hot.rootElement.className);
  68. }
  69. const rowObject = { row };
  70. this.rows.push(rowObject);
  71. this.samples = samples;
  72. this.table = this.createTable(this.hot.table.className);
  73. this.table.colGroup.appendChild(this.createColGroupsCol());
  74. this.table.tr.appendChild(this.createRow(row));
  75. this.container.container.appendChild(this.table.fragment);
  76. rowObject.table = this.table.table;
  77. }
  78. /**
  79. * Add a row consisting of the column headers.
  80. */
  81. addColumnHeadersRow(samples) {
  82. const colHeader = this.hot.getColHeader(0);
  83. if (colHeader !== null && colHeader !== void 0) {
  84. const rowObject = { row: -1 };
  85. this.rows.push(rowObject);
  86. this.container = this.createContainer(this.hot.rootElement.className);
  87. this.samples = samples;
  88. this.table = this.createTable(this.hot.table.className);
  89. this.table.colGroup.appendChild(this.createColGroupsCol());
  90. this.table.tHead.appendChild(this.createColumnHeadersRow());
  91. this.container.container.appendChild(this.table.fragment);
  92. rowObject.table = this.table.table;
  93. }
  94. }
  95. /**
  96. * Add column.
  97. *
  98. * @param {Number} column Column index.
  99. * @param {Map} samples Samples Map object.
  100. */
  101. addColumn(column, samples) {
  102. if (this.rows.length) {
  103. throw new Error('Doesn\'t support multi-dimensional table');
  104. }
  105. if (!this.columns.length) {
  106. this.container = this.createContainer(this.hot.rootElement.className);
  107. }
  108. const columnObject = { col: column };
  109. this.columns.push(columnObject);
  110. this.samples = samples;
  111. this.table = this.createTable(this.hot.table.className);
  112. if (this.getSetting('useHeaders') && this.hot.getColHeader(column) !== null) {
  113. this.hot.view.appendColHeader(column, this.table.th);
  114. }
  115. this.table.tBody.appendChild(this.createCol(column));
  116. this.container.container.appendChild(this.table.fragment);
  117. columnObject.table = this.table.table;
  118. }
  119. /**
  120. * Get calculated heights.
  121. *
  122. * @param {Function} callback Callback which will be fired for each calculated row.
  123. */
  124. getHeights(callback) {
  125. if (!this.injected) {
  126. this.injectTable();
  127. }
  128. arrayEach(this.rows, (row) => {
  129. // -1 <- reduce border-top from table
  130. callback(row.row, outerHeight(row.table) - 1);
  131. });
  132. }
  133. /**
  134. * Get calculated widths.
  135. *
  136. * @param {Function} callback Callback which will be fired for each calculated column.
  137. */
  138. getWidths(callback) {
  139. if (!this.injected) {
  140. this.injectTable();
  141. }
  142. arrayEach(this.columns, (column) => {
  143. callback(column.col, outerWidth(column.table));
  144. });
  145. }
  146. /**
  147. * Set the Ghost Table settings to the provided object.
  148. *
  149. * @param {Object} settings New Ghost Table Settings
  150. */
  151. setSettings(settings) {
  152. this.settings = settings;
  153. }
  154. /**
  155. * Set a single setting of the Ghost Table.
  156. *
  157. * @param {String} name Setting name.
  158. * @param {*} value Setting value.
  159. */
  160. setSetting(name, value) {
  161. if (!this.settings) {
  162. this.settings = {};
  163. }
  164. this.settings[name] = value;
  165. }
  166. /**
  167. * Get the Ghost Table settings.
  168. *
  169. * @returns {Object|null}
  170. */
  171. getSettings() {
  172. return this.settings;
  173. }
  174. /**
  175. * Get a single Ghost Table setting.
  176. *
  177. * @param {String} name
  178. * @returns {Boolean|null}
  179. */
  180. getSetting(name) {
  181. if (this.settings) {
  182. return this.settings[name];
  183. }
  184. return null;
  185. }
  186. /**
  187. * Create colgroup col elements.
  188. *
  189. * @returns {DocumentFragment}
  190. */
  191. createColGroupsCol() {
  192. const d = document;
  193. const fragment = d.createDocumentFragment();
  194. if (this.hot.hasRowHeaders()) {
  195. fragment.appendChild(this.createColElement(-1));
  196. }
  197. this.samples.forEach((sample) => {
  198. arrayEach(sample.strings, (string) => {
  199. fragment.appendChild(this.createColElement(string.col));
  200. });
  201. });
  202. return fragment;
  203. }
  204. /**
  205. * Create table row element.
  206. *
  207. * @param {Number} row Row index.
  208. * @returns {DocumentFragment} Returns created table row elements.
  209. */
  210. createRow(row) {
  211. const d = document;
  212. const fragment = d.createDocumentFragment();
  213. const th = d.createElement('th');
  214. if (this.hot.hasRowHeaders()) {
  215. this.hot.view.appendRowHeader(row, th);
  216. fragment.appendChild(th);
  217. }
  218. this.samples.forEach((sample) => {
  219. arrayEach(sample.strings, (string) => {
  220. const column = string.col;
  221. const cellProperties = this.hot.getCellMeta(row, column);
  222. cellProperties.col = column;
  223. cellProperties.row = row;
  224. const renderer = this.hot.getCellRenderer(cellProperties);
  225. const td = d.createElement('td');
  226. renderer(this.hot, td, row, column, this.hot.colToProp(column), string.value, cellProperties);
  227. fragment.appendChild(td);
  228. });
  229. });
  230. return fragment;
  231. }
  232. createColumnHeadersRow() {
  233. const d = document;
  234. const fragment = d.createDocumentFragment();
  235. if (this.hot.hasRowHeaders()) {
  236. const th = d.createElement('th');
  237. this.hot.view.appendColHeader(-1, th);
  238. fragment.appendChild(th);
  239. }
  240. this.samples.forEach((sample) => {
  241. arrayEach(sample.strings, (string) => {
  242. const column = string.col;
  243. const th = d.createElement('th');
  244. this.hot.view.appendColHeader(column, th);
  245. fragment.appendChild(th);
  246. });
  247. });
  248. return fragment;
  249. }
  250. /**
  251. * Create table column elements.
  252. *
  253. * @param {Number} column Column index.
  254. * @returns {DocumentFragment} Returns created column table column elements.
  255. */
  256. createCol(column) {
  257. const d = document;
  258. const fragment = d.createDocumentFragment();
  259. this.samples.forEach((sample) => {
  260. arrayEach(sample.strings, (string) => {
  261. const row = string.row;
  262. const cellProperties = this.hot.getCellMeta(row, column);
  263. cellProperties.col = column;
  264. cellProperties.row = row;
  265. const renderer = this.hot.getCellRenderer(cellProperties);
  266. const td = d.createElement('td');
  267. const tr = d.createElement('tr');
  268. // Indicate that this element is created and supported by GhostTable. It can be useful to
  269. // exclude rendering performance costly logic or exclude logic which doesn't work within a hidden table.
  270. td.setAttribute('ghost-table', 1);
  271. renderer(this.hot, td, row, column, this.hot.colToProp(column), string.value, cellProperties);
  272. tr.appendChild(td);
  273. fragment.appendChild(tr);
  274. });
  275. });
  276. return fragment;
  277. }
  278. /**
  279. * Remove table from document and reset internal state.
  280. */
  281. clean() {
  282. this.rows.length = 0;
  283. this.rows[-1] = void 0;
  284. this.columns.length = 0;
  285. if (this.samples) {
  286. this.samples.clear();
  287. }
  288. this.samples = null;
  289. this.removeTable();
  290. }
  291. /**
  292. * Inject generated table into document.
  293. *
  294. * @param {HTMLElement} [parent=null]
  295. */
  296. injectTable(parent = null) {
  297. if (!this.injected) {
  298. (parent || this.hot.rootElement).appendChild(this.container.fragment);
  299. this.injected = true;
  300. }
  301. }
  302. /**
  303. * Remove table from document.
  304. */
  305. removeTable() {
  306. if (this.injected && this.container.container.parentNode) {
  307. this.container.container.parentNode.removeChild(this.container.container);
  308. this.container = null;
  309. this.injected = false;
  310. }
  311. }
  312. /**
  313. * Create col element.
  314. *
  315. * @param {Number} column Column index.
  316. * @returns {HTMLElement}
  317. */
  318. createColElement(column) {
  319. const d = document;
  320. const col = d.createElement('col');
  321. col.style.width = `${this.hot.view.wt.wtTable.getStretchedColumnWidth(column)}px`;
  322. return col;
  323. }
  324. /**
  325. * Create table element.
  326. *
  327. * @param {String} className
  328. * @returns {Object}
  329. */
  330. createTable(className = '') {
  331. const d = document;
  332. const fragment = d.createDocumentFragment();
  333. const table = d.createElement('table');
  334. const tHead = d.createElement('thead');
  335. const tBody = d.createElement('tbody');
  336. const colGroup = d.createElement('colgroup');
  337. const tr = d.createElement('tr');
  338. const th = d.createElement('th');
  339. if (this.isVertical()) {
  340. table.appendChild(colGroup);
  341. }
  342. if (this.isHorizontal()) {
  343. tr.appendChild(th);
  344. tHead.appendChild(tr);
  345. table.style.tableLayout = 'auto';
  346. table.style.width = 'auto';
  347. }
  348. table.appendChild(tHead);
  349. if (this.isVertical()) {
  350. tBody.appendChild(tr);
  351. }
  352. table.appendChild(tBody);
  353. addClass(table, className);
  354. fragment.appendChild(table);
  355. return { fragment, table, tHead, tBody, colGroup, tr, th };
  356. }
  357. /**
  358. * Create container for tables.
  359. *
  360. * @param {String} className
  361. * @returns {Object}
  362. */
  363. createContainer(className = '') {
  364. const d = document;
  365. const fragment = d.createDocumentFragment();
  366. const container = d.createElement('div');
  367. const containerClassName = `htGhostTable htAutoSize ${className.trim()}`;
  368. addClass(container, containerClassName);
  369. fragment.appendChild(container);
  370. return { fragment, container };
  371. }
  372. /**
  373. * Checks if table is raised vertically (checking rows).
  374. *
  375. * @returns {Boolean}
  376. */
  377. isVertical() {
  378. return !!(this.rows.length && !this.columns.length);
  379. }
  380. /**
  381. * Checks if table is raised horizontally (checking columns).
  382. *
  383. * @returns {Boolean}
  384. */
  385. isHorizontal() {
  386. return !!(this.columns.length && !this.rows.length);
  387. }
  388. }
  389. export default GhostTable;