autoRowSize.js 19 KB

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