manualColumnResize.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. import BasePlugin from './../_base';
  2. import { addClass, hasClass, removeClass, outerHeight } from './../../helpers/dom/element';
  3. import EventManager from './../../eventManager';
  4. import { pageX } from './../../helpers/dom/event';
  5. import { arrayEach } from './../../helpers/array';
  6. import { rangeEach } from './../../helpers/number';
  7. import { registerPlugin } from './../../plugins';
  8. // Developer note! Whenever you make a change in this file, make an analogous change in manualRowResize.js
  9. /**
  10. * @description
  11. * This plugin allows to change columns width. To make columns width persistent the {@link Options#persistentState}
  12. * plugin should be enabled.
  13. *
  14. * The plugin creates additional components to make resizing possibly using user interface:
  15. * - handle - the draggable element that sets the desired width of the column.
  16. * - guide - the helper guide that shows the desired width as a vertical guide.
  17. *
  18. * @plugin ManualColumnResize
  19. */
  20. class ManualColumnResize extends BasePlugin {
  21. constructor(hotInstance) {
  22. super(hotInstance);
  23. this.currentTH = null;
  24. this.currentCol = null;
  25. this.selectedCols = [];
  26. this.currentWidth = null;
  27. this.newSize = null;
  28. this.startY = null;
  29. this.startWidth = null;
  30. this.startOffset = null;
  31. this.handle = document.createElement('DIV');
  32. this.guide = document.createElement('DIV');
  33. this.eventManager = new EventManager(this);
  34. this.pressed = null;
  35. this.dblclick = 0;
  36. this.autoresizeTimeout = null;
  37. this.manualColumnWidths = [];
  38. addClass(this.handle, 'manualColumnResizer');
  39. addClass(this.guide, 'manualColumnResizerGuide');
  40. }
  41. /**
  42. * Checks if the plugin is enabled in the handsontable settings. This method is executed in {@link Hooks#beforeInit}
  43. * hook and if it returns `true` than the {@link ManualColumnResize#enablePlugin} method is called.
  44. *
  45. * @returns {Boolean}
  46. */
  47. isEnabled() {
  48. return this.hot.getSettings().manualColumnResize;
  49. }
  50. /**
  51. * Enables the plugin functionality for this Handsontable instance.
  52. */
  53. enablePlugin() {
  54. if (this.enabled) {
  55. return;
  56. }
  57. this.manualColumnWidths = [];
  58. const initialColumnWidth = this.hot.getSettings().manualColumnResize;
  59. const loadedManualColumnWidths = this.loadManualColumnWidths();
  60. this.addHook('modifyColWidth', (width, col) => this.onModifyColWidth(width, col));
  61. this.addHook('beforeStretchingColumnWidth', (stretchedWidth, column) => this.onBeforeStretchingColumnWidth(stretchedWidth, column));
  62. this.addHook('beforeColumnResize', (currentColumn, newSize, isDoubleClick) => this.onBeforeColumnResize(currentColumn, newSize, isDoubleClick));
  63. if (typeof loadedManualColumnWidths !== 'undefined') {
  64. this.manualColumnWidths = loadedManualColumnWidths;
  65. } else if (Array.isArray(initialColumnWidth)) {
  66. this.manualColumnWidths = initialColumnWidth;
  67. } else {
  68. this.manualColumnWidths = [];
  69. }
  70. // Handsontable.hooks.register('beforeColumnResize');
  71. // Handsontable.hooks.register('afterColumnResize');
  72. this.bindEvents();
  73. super.enablePlugin();
  74. }
  75. /**
  76. * Updates the plugin state. This method is executed when {@link Core#updateSettings} is invoked.
  77. */
  78. updatePlugin() {
  79. const initialColumnWidth = this.hot.getSettings().manualColumnResize;
  80. if (Array.isArray(initialColumnWidth)) {
  81. this.manualColumnWidths = initialColumnWidth;
  82. } else if (!initialColumnWidth) {
  83. this.manualColumnWidths = [];
  84. }
  85. }
  86. /**
  87. * Disables the plugin functionality for this Handsontable instance.
  88. */
  89. disablePlugin() {
  90. super.disablePlugin();
  91. }
  92. /**
  93. * Saves the current sizes using the persistentState plugin (the {@link Options#persistentState} option has to be enabled).
  94. */
  95. saveManualColumnWidths() {
  96. this.hot.runHooks('persistentStateSave', 'manualColumnWidths', this.manualColumnWidths);
  97. }
  98. /**
  99. * Loads the previously saved sizes using the persistentState plugin (the {@link Options#persistentState} option has to be enabled).
  100. *
  101. * @returns {Array}
  102. *
  103. * @fires Hooks#persistentStateLoad
  104. * @fires Hooks#manualColumnWidths
  105. */
  106. loadManualColumnWidths() {
  107. const storedState = {};
  108. this.hot.runHooks('persistentStateLoad', 'manualColumnWidths', storedState);
  109. return storedState.value;
  110. }
  111. /**
  112. * Set the resize handle position.
  113. *
  114. * @private
  115. * @param {HTMLCellElement} TH TH HTML element.
  116. */
  117. setupHandlePosition(TH) {
  118. if (!TH.parentNode) {
  119. return false;
  120. }
  121. this.currentTH = TH;
  122. // getCoords returns CellCoords
  123. // const col = this.hot.view.wt.wtTable.getCoords(TH).col;
  124. const text = TH.innerText.replace('\n', '<br>');
  125. let col = this.hot.getColHeader().indexOf(text);
  126. // 如果这里调大说明存在列头一模一样的情况,这时就直接用 html element里的顺序来
  127. if (this.hot.getSettings().viewportColumnRenderingOffset >= 20) {
  128. col = TH.cellIndex;
  129. if (!TH.parentNode.childNodes[0].innerText.replace(/(\s| )/gi, '')) {
  130. col -= 1;
  131. }
  132. }
  133. const headerHeight = outerHeight(this.currentTH);
  134. if (col >= 0) { // if not col header
  135. const box = this.currentTH.getBoundingClientRect();
  136. this.currentCol = col;
  137. this.selectedCols = [];
  138. if (this.hot.selection.isSelected() && this.hot.selection.isSelectedByColumnHeader()) {
  139. const { from, to } = this.hot.getSelectedRangeLast();
  140. let start = from.col;
  141. let end = to.col;
  142. if (start >= end) {
  143. start = to.col;
  144. end = from.col;
  145. }
  146. if (this.currentCol >= start && this.currentCol <= end) {
  147. rangeEach(start, end, i => this.selectedCols.push(i));
  148. } else {
  149. this.selectedCols.push(this.currentCol);
  150. }
  151. } else {
  152. this.selectedCols.push(this.currentCol);
  153. }
  154. this.startOffset = box.left - 6;
  155. this.startWidth = parseInt(box.width, 10);
  156. this.handle.style.top = `${box.top}px`;
  157. this.handle.style.left = `${this.startOffset + this.startWidth}px`;
  158. this.handle.style.height = `${headerHeight}px`;
  159. this.hot.rootElement.appendChild(this.handle);
  160. }
  161. }
  162. /**
  163. * Refresh the resize handle position.
  164. *
  165. * @private
  166. */
  167. refreshHandlePosition() {
  168. this.handle.style.left = `${this.startOffset + this.currentWidth}px`;
  169. }
  170. /**
  171. * Sets the resize guide position.
  172. *
  173. * @private
  174. */
  175. setupGuidePosition() {
  176. const handleHeight = parseInt(outerHeight(this.handle), 10);
  177. const handleBottomPosition = parseInt(this.handle.style.top, 10) + handleHeight;
  178. const maximumVisibleElementHeight = parseInt(this.hot.view.maximumVisibleElementHeight(0), 10);
  179. addClass(this.handle, 'active');
  180. addClass(this.guide, 'active');
  181. this.guide.style.top = `${handleBottomPosition}px`;
  182. this.guide.style.left = this.handle.style.left;
  183. this.guide.style.height = `${maximumVisibleElementHeight - handleHeight}px`;
  184. this.hot.rootElement.appendChild(this.guide);
  185. }
  186. /**
  187. * Refresh the resize guide position.
  188. *
  189. * @private
  190. */
  191. refreshGuidePosition() {
  192. this.guide.style.left = this.handle.style.left;
  193. }
  194. /**
  195. * Hides both the resize handle and resize guide.
  196. *
  197. * @private
  198. */
  199. hideHandleAndGuide() {
  200. removeClass(this.handle, 'active');
  201. removeClass(this.guide, 'active');
  202. }
  203. /**
  204. * Checks if provided element is considered a column header.
  205. *
  206. * @private
  207. * @param {HTMLElement} element HTML element.
  208. * @returns {Boolean}
  209. */
  210. checkIfColumnHeader(element) {
  211. if (element !== this.hot.rootElement) {
  212. const parent = element.parentNode;
  213. if (parent.tagName === 'THEAD') {
  214. return true;
  215. }
  216. return this.checkIfColumnHeader(parent);
  217. }
  218. return false;
  219. }
  220. /**
  221. * Gets the TH element from the provided element.
  222. *
  223. * @private
  224. * @param {HTMLElement} element HTML element.
  225. * @returns {HTMLElement}
  226. */
  227. getTHFromTargetElement(element) {
  228. if (element.tagName !== 'TABLE') {
  229. if (element.tagName === 'TH') {
  230. return element;
  231. }
  232. return this.getTHFromTargetElement(element.parentNode);
  233. }
  234. return null;
  235. }
  236. /**
  237. * 'mouseover' event callback - set the handle position.
  238. *
  239. * @private
  240. * @param {MouseEvent} event
  241. */
  242. onMouseOver(event) {
  243. if (this.checkIfColumnHeader(event.target)) {
  244. const th = this.getTHFromTargetElement(event.target);
  245. if (!th) {
  246. return;
  247. }
  248. const colspan = th.getAttribute('colspan');
  249. if (th && (colspan === null || colspan === 1)) {
  250. if (!this.pressed) {
  251. this.setupHandlePosition(th);
  252. }
  253. }
  254. }
  255. }
  256. /**
  257. * Auto-size row after doubleclick - callback.
  258. *
  259. * @private
  260. *
  261. * @fires Hooks#beforeColumnResize
  262. * @fires Hooks#afterColumnResize
  263. */
  264. afterMouseDownTimeout() {
  265. const render = () => {
  266. this.hot.forceFullRender = true;
  267. this.hot.view.render(); // updates all
  268. this.hot.view.wt.wtOverlays.adjustElementsSize(true);
  269. };
  270. const resize = (selectedCol, forceRender) => {
  271. const hookNewSize = this.hot.runHooks('beforeColumnResize', selectedCol, this.newSize, true);
  272. if (hookNewSize !== void 0) {
  273. this.newSize = hookNewSize;
  274. }
  275. if (this.hot.getSettings().stretchH === 'all') {
  276. this.clearManualSize(selectedCol);
  277. } else {
  278. this.setManualSize(selectedCol, this.newSize); // double click sets by auto row size plugin
  279. }
  280. if (forceRender) {
  281. render();
  282. }
  283. this.saveManualColumnWidths();
  284. this.hot.runHooks('afterColumnResize', selectedCol, this.newSize, true);
  285. };
  286. if (this.dblclick >= 2) {
  287. const selectedColsLength = this.selectedCols.length;
  288. if (selectedColsLength > 1) {
  289. arrayEach(this.selectedCols, (selectedCol) => {
  290. resize(selectedCol);
  291. });
  292. render();
  293. } else {
  294. arrayEach(this.selectedCols, (selectedCol) => {
  295. resize(selectedCol, true);
  296. });
  297. }
  298. }
  299. this.dblclick = 0;
  300. this.autoresizeTimeout = null;
  301. }
  302. /**
  303. * 'mousedown' event callback.
  304. *
  305. * @private
  306. * @param {MouseEvent} event
  307. */
  308. onMouseDown(event) {
  309. if (hasClass(event.target, 'manualColumnResizer')) {
  310. this.setupGuidePosition();
  311. this.pressed = this.hot;
  312. if (this.autoresizeTimeout === null) {
  313. this.autoresizeTimeout = setTimeout(() => this.afterMouseDownTimeout(), 500);
  314. this.hot._registerTimeout(this.autoresizeTimeout);
  315. }
  316. this.dblclick += 1;
  317. this.startX = pageX(event);
  318. this.newSize = this.startWidth;
  319. }
  320. }
  321. /**
  322. * 'mousemove' event callback - refresh the handle and guide positions, cache the new column width.
  323. *
  324. * @private
  325. * @param {MouseEvent} event
  326. */
  327. onMouseMove(event) {
  328. if (this.pressed) {
  329. this.currentWidth = this.startWidth + (pageX(event) - this.startX);
  330. arrayEach(this.selectedCols, (selectedCol) => {
  331. this.newSize = this.setManualSize(selectedCol, this.currentWidth);
  332. });
  333. this.refreshHandlePosition();
  334. this.refreshGuidePosition();
  335. }
  336. }
  337. /**
  338. * 'mouseup' event callback - apply the column resizing.
  339. *
  340. * @private
  341. *
  342. * @fires Hooks#beforeColumnResize
  343. * @fires Hooks#afterColumnResize
  344. */
  345. onMouseUp() {
  346. const render = () => {
  347. this.hot.forceFullRender = true;
  348. this.hot.view.render(); // updates all
  349. this.hot.view.wt.wtOverlays.adjustElementsSize(true);
  350. };
  351. const resize = (selectedCol, forceRender) => {
  352. this.hot.runHooks('beforeColumnResize', selectedCol, this.newSize, false);
  353. if (forceRender) {
  354. render();
  355. }
  356. this.saveManualColumnWidths();
  357. this.hot.runHooks('afterColumnResize', selectedCol, this.newSize);
  358. };
  359. if (this.pressed) {
  360. this.hideHandleAndGuide();
  361. this.pressed = false;
  362. if (this.newSize !== this.startWidth) {
  363. const selectedColsLength = this.selectedCols.length;
  364. if (selectedColsLength > 1) {
  365. arrayEach(this.selectedCols, (selectedCol) => {
  366. resize(selectedCol);
  367. });
  368. render();
  369. } else {
  370. arrayEach(this.selectedCols, (selectedCol) => {
  371. resize(selectedCol, true);
  372. });
  373. }
  374. }
  375. this.setupHandlePosition(this.currentTH);
  376. }
  377. }
  378. /**
  379. * Binds the mouse events.
  380. *
  381. * @private
  382. */
  383. bindEvents() {
  384. this.eventManager.addEventListener(this.hot.rootElement, 'mouseover', e => this.onMouseOver(e));
  385. this.eventManager.addEventListener(this.hot.rootElement, 'mousedown', e => this.onMouseDown(e));
  386. this.eventManager.addEventListener(window, 'mousemove', e => this.onMouseMove(e));
  387. this.eventManager.addEventListener(window, 'mouseup', () => this.onMouseUp());
  388. }
  389. /**
  390. * Sets the new width for specified column index.
  391. *
  392. * @param {Number} column Visual column index.
  393. * @param {Number} width Column width (no less than 20px).
  394. * @returns {Number} Returns new width.
  395. */
  396. setManualSize(column, width) {
  397. const newWidth = Math.max(width, 20);
  398. /**
  399. * We need to run col through modifyCol hook, in case the order of displayed columns is different than the order
  400. * in data source. For instance, this order can be modified by manualColumnMove plugin.
  401. */
  402. const physicalColumn = this.hot.runHooks('modifyCol', column);
  403. this.manualColumnWidths[physicalColumn] = newWidth;
  404. return newWidth;
  405. }
  406. /**
  407. * Clears the cache for the specified column index.
  408. *
  409. * @param {Number} column Visual column index.
  410. */
  411. clearManualSize(column) {
  412. const physicalColumn = this.hot.runHooks('modifyCol', column);
  413. this.manualColumnWidths[physicalColumn] = void 0;
  414. }
  415. /**
  416. * Modifies the provided column width, based on the plugin settings
  417. *
  418. * @private
  419. * @param {Number} width Column width.
  420. * @param {Number} column Visual column index.
  421. * @returns {Number}
  422. */
  423. onModifyColWidth(width, column) {
  424. let newWidth = width;
  425. if (this.enabled) {
  426. const physicalColumn = this.hot.runHooks('modifyCol', column);
  427. const columnWidth = this.manualColumnWidths[physicalColumn];
  428. if (this.hot.getSettings().manualColumnResize && columnWidth) {
  429. newWidth = columnWidth;
  430. }
  431. }
  432. return newWidth;
  433. }
  434. /**
  435. * Modifies the provided column stretched width. This hook decides if specified column should be stretched or not.
  436. *
  437. * @private
  438. * @param {Number} stretchedWidth Stretched width.
  439. * @param {Number} column Physical column index.
  440. * @returns {Number}
  441. */
  442. onBeforeStretchingColumnWidth(stretchedWidth, column) {
  443. let width = this.manualColumnWidths[column];
  444. if (width === void 0) {
  445. width = stretchedWidth;
  446. }
  447. return width;
  448. }
  449. /**
  450. * `beforeColumnResize` hook callback.
  451. *
  452. * @private
  453. */
  454. onBeforeColumnResize() {
  455. // clear the header height cache information
  456. this.hot.view.wt.wtViewport.hasOversizedColumnHeadersMarked = {};
  457. }
  458. }
  459. registerPlugin('manualColumnResize', ManualColumnResize);
  460. export default ManualColumnResize;