autoRowSize.js 19 KB

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