f3549ae315449b5d3448b0124d92c100c85e8541450f8958ff3e44828414719642d746615ab5c0d84dc91dc9c56d5d0d9a6f9421c537832fe76e799d3a902c 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. import BasePlugin from './../_base';
  2. import {arrayEach, arrayFilter} from './../../helpers/array';
  3. import {cancelAnimationFrame, requestAnimationFrame} from './../../helpers/feature';
  4. import {isVisible} from './../../helpers/dom/element';
  5. import GhostTable from './../../utils/ghostTable';
  6. import {isObject, objectEach, hasOwnProperty} from './../../helpers/object';
  7. import {valueAccordingPercent, rangeEach} from './../../helpers/number';
  8. import {registerPlugin} from './../../plugins';
  9. import SamplesGenerator from './../../utils/samplesGenerator';
  10. import {isPercentValue} from './../../helpers/string';
  11. /**
  12. * @plugin AutoRowSize
  13. *
  14. * @description
  15. * This plugin allows to set row heights based on their highest cells.
  16. *
  17. * By default, the plugin is declared as `undefined`, which makes it disabled (same as if it was declared as `false`).
  18. * Enabling this plugin may decrease the overall table performance, as it needs to calculate the heights of all cells to
  19. * resize the rows accordingly.
  20. * If you experience problems with the performance, try turning this feature off and declaring the row heights manually.
  21. *
  22. * Row height calculations are divided into sync and async part. Each of this parts has their own advantages and
  23. * disadvantages. Synchronous calculations are faster but they block the browser UI, while the slower asynchronous operations don't
  24. * block the browser UI.
  25. *
  26. * To configure the sync/async distribution, you can pass an absolute value (number of columns) or a percentage value to a config object:
  27. * ```js
  28. * ...
  29. * // as a number (300 columns in sync, rest async)
  30. * autoRowSize: {syncLimit: 300},
  31. * ...
  32. *
  33. * ...
  34. * // as a string (percent)
  35. * autoRowSize: {syncLimit: '40%'},
  36. * ...
  37. * ```
  38. *
  39. * You can also use the `allowSampleDuplicates` option to allow sampling duplicate values when calculating the row height. Note, that this might have
  40. * a negative impact on performance.
  41. *
  42. * To configure this plugin see {@link Options#autoRowSize}.
  43. *
  44. * @example
  45. *
  46. * ```js
  47. * ...
  48. * var hot = new Handsontable(document.getElementById('example'), {
  49. * date: getData(),
  50. * autoRowSize: true
  51. * });
  52. * // Access to plugin instance:
  53. * var plugin = hot.getPlugin('autoRowSize');
  54. *
  55. * plugin.getRowHeight(4);
  56. *
  57. * if (plugin.isEnabled()) {
  58. * // code...
  59. * }
  60. * ...
  61. * ```
  62. */
  63. class AutoRowSize extends BasePlugin {
  64. static get CALCULATION_STEP() {
  65. return 50;
  66. }
  67. static get SYNC_CALCULATION_LIMIT() {
  68. return 500;
  69. }
  70. constructor(hotInstance) {
  71. super(hotInstance);
  72. /**
  73. * Cached rows heights.
  74. *
  75. * @type {Array}
  76. */
  77. this.heights = [];
  78. /**
  79. * Instance of {@link GhostTable} for rows and columns size calculations.
  80. *
  81. * @type {GhostTable}
  82. */
  83. this.ghostTable = new GhostTable(this.hot);
  84. /**
  85. * Instance of {@link SamplesGenerator} for generating samples necessary for rows height calculations.
  86. *
  87. * @type {SamplesGenerator}
  88. */
  89. this.samplesGenerator = new SamplesGenerator((row, col) => {
  90. if (row >= 0) {
  91. return this.hot.getDataAtCell(row, col);
  92. } else if (row === -1) {
  93. return this.hot.getColHeader(col);
  94. }
  95. return null;
  96. });
  97. /**
  98. * `true` if only the first calculation was performed.
  99. *
  100. * @type {Boolean}
  101. */
  102. this.firstCalculation = true;
  103. /**
  104. * `true` if the size calculation is in progress.
  105. *
  106. * @type {Boolean}
  107. */
  108. this.inProgress = false;
  109. // moved to constructor to allow auto-sizing the rows when the plugin is disabled
  110. this.addHook('beforeRowResize', (row, size, isDblClick) => this.onBeforeRowResize(row, size, isDblClick));
  111. }
  112. /**
  113. * Check if the plugin is enabled in the Handsontable settings.
  114. *
  115. * @returns {Boolean}
  116. */
  117. isEnabled() {
  118. return this.hot.getSettings().autoRowSize === true || isObject(this.hot.getSettings().autoRowSize);
  119. }
  120. /**
  121. * Enable plugin for this Handsontable instance.
  122. */
  123. enablePlugin() {
  124. if (this.enabled) {
  125. return;
  126. }
  127. this.setSamplingOptions();
  128. this.addHook('afterLoadData', () => this.onAfterLoadData());
  129. this.addHook('beforeChange', (changes) => this.onBeforeChange(changes));
  130. this.addHook('beforeColumnMove', () => this.recalculateAllRowsHeight());
  131. this.addHook('beforeColumnResize', () => this.recalculateAllRowsHeight());
  132. this.addHook('beforeColumnSort', () => this.clearCache());
  133. this.addHook('beforeRender', (force) => this.onBeforeRender(force));
  134. this.addHook('beforeRowMove', (rowStart, rowEnd) => this.onBeforeRowMove(rowStart, rowEnd));
  135. this.addHook('modifyRowHeight', (height, row) => this.getRowHeight(row, height));
  136. this.addHook('modifyColumnHeaderHeight', () => this.getColumnHeaderHeight());
  137. super.enablePlugin();
  138. }
  139. /**
  140. * Disable plugin for this Handsontable instance.
  141. */
  142. disablePlugin() {
  143. super.disablePlugin();
  144. }
  145. /**
  146. * Calculate a given rows height.
  147. *
  148. * @param {Number|Object} rowRange Row range object.
  149. * @param {Number|Object} colRange Column range object.
  150. * @param {Boolean} [force=false] If `true` force calculate height even when value was cached earlier.
  151. */
  152. calculateRowsHeight(rowRange = {from: 0, to: this.hot.countRows() - 1}, colRange = {from: 0, to: this.hot.countCols() - 1}, force = false) {
  153. if (typeof rowRange === 'number') {
  154. rowRange = {from: rowRange, to: rowRange};
  155. }
  156. if (typeof colRange === 'number') {
  157. colRange = {from: colRange, to: colRange};
  158. }
  159. if (this.hot.getColHeader(0) !== null) {
  160. const samples = this.samplesGenerator.generateRowSamples(-1, colRange);
  161. this.ghostTable.addColumnHeadersRow(samples.get(-1));
  162. }
  163. rangeEach(rowRange.from, rowRange.to, (row) => {
  164. // For rows we must calculate row height even when user had set height value manually.
  165. // We can shrink column but cannot shrink rows!
  166. if (force || this.heights[row] === void 0) {
  167. const samples = this.samplesGenerator.generateRowSamples(row, colRange);
  168. samples.forEach((sample, row) => {
  169. this.ghostTable.addRow(row, sample);
  170. });
  171. }
  172. });
  173. if (this.ghostTable.rows.length) {
  174. this.ghostTable.getHeights((row, height) => {
  175. this.heights[row] = height;
  176. });
  177. this.ghostTable.clean();
  178. }
  179. }
  180. /**
  181. * Calculate the height of all the rows.
  182. *
  183. * @param {Object|Number} colRange Column range object.
  184. */
  185. calculateAllRowsHeight(colRange = {from: 0, to: this.hot.countCols() - 1}) {
  186. let current = 0;
  187. let length = this.hot.countRows() - 1;
  188. let timer = null;
  189. this.inProgress = true;
  190. let loop = () => {
  191. // When hot was destroyed after calculating finished cancel frame
  192. if (!this.hot) {
  193. cancelAnimationFrame(timer);
  194. this.inProgress = false;
  195. return;
  196. }
  197. this.calculateRowsHeight({from: current, to: Math.min(current + AutoRowSize.CALCULATION_STEP, length)}, colRange);
  198. current = current + AutoRowSize.CALCULATION_STEP + 1;
  199. if (current < length) {
  200. timer = requestAnimationFrame(loop);
  201. } else {
  202. cancelAnimationFrame(timer);
  203. this.inProgress = false;
  204. // @TODO Should call once per render cycle, currently fired separately in different plugins
  205. this.hot.view.wt.wtOverlays.adjustElementsSize(true);
  206. // tmp
  207. if (this.hot.view.wt.wtOverlays.leftOverlay.needFullRender) {
  208. this.hot.view.wt.wtOverlays.leftOverlay.clone.draw();
  209. }
  210. }
  211. };
  212. // sync
  213. if (this.firstCalculation && this.getSyncCalculationLimit()) {
  214. this.calculateRowsHeight({from: 0, to: this.getSyncCalculationLimit()}, colRange);
  215. this.firstCalculation = false;
  216. current = this.getSyncCalculationLimit() + 1;
  217. }
  218. // async
  219. if (current < length) {
  220. loop();
  221. } else {
  222. this.inProgress = false;
  223. this.hot.view.wt.wtOverlays.adjustElementsSize(false);
  224. }
  225. }
  226. /**
  227. * Set the sampling options.
  228. *
  229. * @private
  230. */
  231. setSamplingOptions() {
  232. let setting = this.hot.getSettings().autoRowSize;
  233. let samplingRatio = setting && hasOwnProperty(setting, 'samplingRatio') ? this.hot.getSettings().autoRowSize.samplingRatio : void 0;
  234. let allowSampleDuplicates = setting && hasOwnProperty(setting, 'allowSampleDuplicates') ? this.hot.getSettings().autoRowSize.allowSampleDuplicates : void 0;
  235. if (samplingRatio && !isNaN(samplingRatio)) {
  236. this.samplesGenerator.setSampleCount(parseInt(samplingRatio, 10));
  237. }
  238. if (allowSampleDuplicates) {
  239. this.samplesGenerator.setAllowDuplicates(allowSampleDuplicates);
  240. }
  241. }
  242. /**
  243. * Recalculate all rows height (overwrite cache values).
  244. */
  245. recalculateAllRowsHeight() {
  246. if (isVisible(this.hot.view.wt.wtTable.TABLE)) {
  247. this.clearCache();
  248. this.calculateAllRowsHeight();
  249. }
  250. }
  251. /**
  252. * Get value which tells how much rows will be calculated synchronously. Rest rows will be calculated asynchronously.
  253. *
  254. * @returns {Number}
  255. */
  256. getSyncCalculationLimit() {
  257. /* eslint-disable no-bitwise */
  258. let limit = AutoRowSize.SYNC_CALCULATION_LIMIT;
  259. let rowsLimit = this.hot.countRows() - 1;
  260. if (isObject(this.hot.getSettings().autoRowSize)) {
  261. limit = this.hot.getSettings().autoRowSize.syncLimit;
  262. if (isPercentValue(limit)) {
  263. limit = valueAccordingPercent(rowsLimit, limit);
  264. } else {
  265. // Force to Number
  266. limit >>= 0;
  267. }
  268. }
  269. return Math.min(limit, rowsLimit);
  270. }
  271. /**
  272. * Get the calculated row height.
  273. *
  274. * @param {Number} row Row index.
  275. * @param {Number} [defaultHeight] Default row height. It will be pick up if no calculated height found.
  276. * @returns {Number}
  277. */
  278. getRowHeight(row, defaultHeight = void 0) {
  279. let height = defaultHeight;
  280. if (this.heights[row] !== void 0 && this.heights[row] > (defaultHeight || 0)) {
  281. height = this.heights[row];
  282. }
  283. return height;
  284. }
  285. /**
  286. * Get the calculated column header height.
  287. *
  288. * @returns {Number|undefined}
  289. */
  290. getColumnHeaderHeight() {
  291. return this.heights[-1];
  292. }
  293. /**
  294. * Get the first visible row.
  295. *
  296. * @returns {Number} Returns row index or -1 if table is not rendered.
  297. */
  298. getFirstVisibleRow() {
  299. const wot = this.hot.view.wt;
  300. if (wot.wtViewport.rowsVisibleCalculator) {
  301. return wot.wtTable.getFirstVisibleRow();
  302. }
  303. if (wot.wtViewport.rowsRenderCalculator) {
  304. return wot.wtTable.getFirstRenderedRow();
  305. }
  306. return -1;
  307. }
  308. /**
  309. * Get the last visible row.
  310. *
  311. * @returns {Number} Returns row index or -1 if table is not rendered.
  312. */
  313. getLastVisibleRow() {
  314. const wot = this.hot.view.wt;
  315. if (wot.wtViewport.rowsVisibleCalculator) {
  316. return wot.wtTable.getLastVisibleRow();
  317. }
  318. if (wot.wtViewport.rowsRenderCalculator) {
  319. return wot.wtTable.getLastRenderedRow();
  320. }
  321. return -1;
  322. }
  323. /**
  324. * Clear cached heights.
  325. */
  326. clearCache() {
  327. this.heights.length = 0;
  328. this.heights[-1] = void 0;
  329. }
  330. /**
  331. * Clear cache by range.
  332. *
  333. * @param {Object|Number} range Row range object.
  334. */
  335. clearCacheByRange(range) {
  336. if (typeof range === 'number') {
  337. range = {from: range, to: range};
  338. }
  339. rangeEach(Math.min(range.from, range.to), Math.max(range.from, range.to), (row) => {
  340. this.heights[row] = void 0;
  341. });
  342. }
  343. /**
  344. * @returns {Boolean}
  345. */
  346. isNeedRecalculate() {
  347. return !!arrayFilter(this.heights, (item) => (item === void 0)).length;
  348. }
  349. /**
  350. * On before render listener.
  351. *
  352. * @private
  353. */
  354. onBeforeRender() {
  355. let force = this.hot.renderCall;
  356. this.calculateRowsHeight({from: this.getFirstVisibleRow(), to: this.getLastVisibleRow()}, void 0, force);
  357. let fixedRowsBottom = this.hot.getSettings().fixedRowsBottom;
  358. // Calculate rows height synchronously for bottom overlay
  359. if (fixedRowsBottom) {
  360. let totalRows = this.hot.countRows() - 1;
  361. this.calculateRowsHeight({from: totalRows - fixedRowsBottom, to: totalRows});
  362. }
  363. if (this.isNeedRecalculate() && !this.inProgress) {
  364. this.calculateAllRowsHeight();
  365. }
  366. }
  367. /**
  368. * On before row move listener.
  369. *
  370. * @private
  371. * @param {Number} from Row index where was grabbed.
  372. * @param {Number} to Destination row index.
  373. */
  374. onBeforeRowMove(from, to) {
  375. this.clearCacheByRange({from, to});
  376. this.calculateAllRowsHeight();
  377. }
  378. /**
  379. * On before row resize listener.
  380. *
  381. * @private
  382. * @param {Number} row
  383. * @param {Number} size
  384. * @param {Boolean} isDblClick
  385. * @returns {Number}
  386. */
  387. onBeforeRowResize(row, size, isDblClick) {
  388. if (isDblClick) {
  389. this.calculateRowsHeight(row, void 0, true);
  390. size = this.getRowHeight(row);
  391. }
  392. return size;
  393. }
  394. /**
  395. * On after load data listener.
  396. *
  397. * @private
  398. */
  399. onAfterLoadData() {
  400. if (this.hot.view) {
  401. this.recalculateAllRowsHeight();
  402. } else {
  403. // first load - initialization
  404. setTimeout(() => {
  405. if (this.hot) {
  406. this.recalculateAllRowsHeight();
  407. }
  408. }, 0);
  409. }
  410. }
  411. /**
  412. * On before change listener.
  413. *
  414. * @private
  415. * @param {Array} changes
  416. */
  417. onBeforeChange(changes) {
  418. let range = null;
  419. if (changes.length === 1) {
  420. range = changes[0][0];
  421. } else if (changes.length > 1) {
  422. range = {
  423. from: changes[0][0],
  424. to: changes[changes.length - 1][0],
  425. };
  426. }
  427. if (range !== null) {
  428. this.clearCacheByRange(range);
  429. }
  430. }
  431. /**
  432. * Destroy plugin instance.
  433. */
  434. destroy() {
  435. this.ghostTable.clean();
  436. super.destroy();
  437. }
  438. }
  439. registerPlugin('autoRowSize', AutoRowSize);
  440. export default AutoRowSize;