80b2eb06a6e3eb32fe45f9e8bec6fc49302e615182cb4412d5a35e917b90598e6b90ece1337fb3cab014438126e419d15262b492b0352a37c6c1ccbe39f981 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import BasePlugin from './../_base';
  2. import Hooks from './../../pluginHooks';
  3. import {offset, outerHeight, outerWidth} from './../../helpers/dom/element';
  4. import EventManager from './../../eventManager';
  5. import {registerPlugin} from './../../plugins';
  6. import {CellCoords} from './../../3rdparty/walkontable/src';
  7. import {getDeltas, getDragDirectionAndRange, DIRECTIONS, getMappedFillHandleSetting} from './utils';
  8. Hooks.getSingleton().register('modifyAutofillRange');
  9. Hooks.getSingleton().register('beforeAutofill');
  10. const INSERT_ROW_ALTER_ACTION_NAME = 'insert_row';
  11. const INTERVAL_FOR_ADDING_ROW = 200;
  12. /**
  13. * This plugin provides "drag-down" and "copy-down" functionalities, both operated
  14. * using the small square in the right bottom of the cell selection.
  15. *
  16. * "Drag-down" expands the value of the selected cells to the neighbouring
  17. * cells when you drag the small square in the corner.
  18. *
  19. * "Copy-down" copies the value of the selection to all empty cells
  20. * below when you double click the small square.
  21. *
  22. * @class Autofill
  23. * @plugin Autofill
  24. */
  25. class Autofill extends BasePlugin {
  26. constructor(hotInstance) {
  27. super(hotInstance);
  28. /**
  29. * Event manager
  30. *
  31. * @type {EventManager}
  32. */
  33. this.eventManager = new EventManager(this);
  34. /**
  35. * Specifies if adding new row started.
  36. *
  37. * @type {Boolean}
  38. */
  39. this.addingStarted = false;
  40. /**
  41. * Specifies if there was mouse down on the cell corner.
  42. *
  43. * @type {Boolean}
  44. */
  45. this.mouseDownOnCellCorner = false;
  46. /**
  47. * Specifies if mouse was dragged outside Handsontable.
  48. *
  49. * @type {Boolean}
  50. */
  51. this.mouseDragOutside = false;
  52. /**
  53. * Specifies how many cell levels were dragged using the handle.
  54. *
  55. * @type {Boolean}
  56. */
  57. this.handleDraggedCells = 0;
  58. /**
  59. * Specifies allowed directions of drag.
  60. *
  61. * @type {Array}
  62. */
  63. this.directions = [];
  64. /**
  65. * Specifies if can insert new rows if needed.
  66. *
  67. * @type {Boolean}
  68. */
  69. this.autoInsertRow = false;
  70. }
  71. /**
  72. * Check if the plugin is enabled in the Handsontable settings.
  73. *
  74. * @returns {Boolean}
  75. */
  76. isEnabled() {
  77. return this.hot.getSettings().fillHandle;
  78. }
  79. /**
  80. * Enable plugin for this Handsontable instance.
  81. */
  82. enablePlugin() {
  83. if (this.enabled) {
  84. return;
  85. }
  86. this.mapSettings();
  87. this.registerEvents();
  88. this.addHook('afterOnCellCornerMouseDown', (event) => this.onAfterCellCornerMouseDown(event));
  89. this.addHook('afterOnCellCornerDblClick', (event) => this.onCellCornerDblClick(event));
  90. this.addHook('beforeOnCellMouseOver', (event, coords, TD) => this.onBeforeCellMouseOver(coords));
  91. super.enablePlugin();
  92. }
  93. /**
  94. * Update plugin for this Handsontable instance.
  95. */
  96. updatePlugin() {
  97. this.disablePlugin();
  98. this.enablePlugin();
  99. super.updatePlugin();
  100. }
  101. /**
  102. * Disable plugin for this Handsontable instance.
  103. */
  104. disablePlugin() {
  105. this.clearMappedSettings();
  106. super.disablePlugin();
  107. }
  108. /**
  109. * Get selection data
  110. *
  111. * @private
  112. * @returns {Array} Array with the data.
  113. */
  114. getSelectionData() {
  115. const selRange = {
  116. from: this.hot.getSelectedRange().from,
  117. to: this.hot.getSelectedRange().to,
  118. };
  119. return this.hot.getData(selRange.from.row, selRange.from.col, selRange.to.row, selRange.to.col);
  120. }
  121. /**
  122. * Try to apply fill values to the area in fill border, omitting the selection border.
  123. *
  124. * @private
  125. * @returns {Boolean} reports if fill was applied.
  126. */
  127. fillIn() {
  128. if (this.hot.view.wt.selections.fill.isEmpty()) {
  129. return false;
  130. }
  131. const cornersOfSelectionAndDragAreas = this.hot.view.wt.selections.fill.getCorners();
  132. this.resetSelectionOfDraggedArea();
  133. const cornersOfSelectedCells = this.getCornersOfSelectedCells();
  134. const {directionOfDrag, startOfDragCoords, endOfDragCoords} = getDragDirectionAndRange(cornersOfSelectedCells, cornersOfSelectionAndDragAreas);
  135. this.hot.runHooks('modifyAutofillRange', cornersOfSelectedCells, cornersOfSelectionAndDragAreas);
  136. if (startOfDragCoords && startOfDragCoords.row > -1 && startOfDragCoords.col > -1) {
  137. const selectionData = this.getSelectionData();
  138. const deltas = getDeltas(startOfDragCoords, endOfDragCoords, selectionData, directionOfDrag);
  139. this.hot.runHooks('beforeAutofill', startOfDragCoords, endOfDragCoords, selectionData);
  140. this.hot.populateFromArray(
  141. startOfDragCoords.row,
  142. startOfDragCoords.col,
  143. selectionData,
  144. endOfDragCoords.row,
  145. endOfDragCoords.col,
  146. `${this.pluginName}.fill`,
  147. null,
  148. directionOfDrag,
  149. deltas
  150. );
  151. this.setSelection(cornersOfSelectionAndDragAreas);
  152. } else {
  153. // reset to avoid some range bug
  154. this.hot.selection.refreshBorders();
  155. }
  156. return true;
  157. }
  158. /**
  159. * Reduce the selection area if the handle was dragged outside of the table or on headers.
  160. *
  161. * @private
  162. * @param {CellCoords} coords indexes of selection corners.
  163. * @returns {CellCoords}
  164. */
  165. reduceSelectionAreaIfNeeded(coords) {
  166. if (coords.row < 0) {
  167. coords.row = 0;
  168. }
  169. if (coords.col < 0) {
  170. coords.col = 0;
  171. }
  172. return coords;
  173. }
  174. /**
  175. * Get the coordinates of the drag & drop borders.
  176. *
  177. * @private
  178. * @param {CellCoords} coordsOfSelection `CellCoords` coord object.
  179. * @returns {Array}
  180. */
  181. getCoordsOfDragAndDropBorders(coordsOfSelection) {
  182. const topLeftCorner = this.hot.getSelectedRange().getTopLeftCorner();
  183. const bottomRightCorner = this.hot.getSelectedRange().getBottomRightCorner();
  184. let coords;
  185. if (this.directions.includes(DIRECTIONS.vertical) &&
  186. (bottomRightCorner.row < coordsOfSelection.row || topLeftCorner.row > coordsOfSelection.row)) {
  187. coords = new CellCoords(coordsOfSelection.row, bottomRightCorner.col);
  188. } else if (this.directions.includes(DIRECTIONS.horizontal)) {
  189. coords = new CellCoords(bottomRightCorner.row, coordsOfSelection.col);
  190. } else {
  191. // wrong direction
  192. return;
  193. }
  194. return this.reduceSelectionAreaIfNeeded(coords);
  195. }
  196. /**
  197. * Show the fill border.
  198. *
  199. * @private
  200. * @param {CellCoords} coordsOfSelection `CellCoords` coord object.
  201. */
  202. showBorder(coordsOfSelection) {
  203. const coordsOfDragAndDropBorders = this.getCoordsOfDragAndDropBorders(coordsOfSelection);
  204. if (coordsOfDragAndDropBorders) {
  205. this.redrawBorders(coordsOfDragAndDropBorders);
  206. }
  207. }
  208. /**
  209. * Add new row
  210. *
  211. * @private
  212. */
  213. addRow() {
  214. this.hot._registerTimeout(setTimeout(() => {
  215. this.hot.alter(INSERT_ROW_ALTER_ACTION_NAME, void 0, 1, `${this.pluginName}.fill`);
  216. this.addingStarted = false;
  217. }, INTERVAL_FOR_ADDING_ROW));
  218. }
  219. /**
  220. * Add new rows if they are needed to continue auto-filling values.
  221. *
  222. * @private
  223. */
  224. addNewRowIfNeeded() {
  225. if (this.hot.view.wt.selections.fill.cellRange && this.addingStarted === false && this.autoInsertRow) {
  226. const cornersOfSelectedCells = this.hot.getSelected();
  227. const cornersOfSelectedDragArea = this.hot.view.wt.selections.fill.getCorners();
  228. const nrOfTableRows = this.hot.countRows();
  229. if (cornersOfSelectedCells[2] < nrOfTableRows - 1 && cornersOfSelectedDragArea[2] === nrOfTableRows - 1) {
  230. this.addingStarted = true;
  231. this.addRow();
  232. }
  233. }
  234. }
  235. /**
  236. * Get corners of selected cells.
  237. *
  238. * @private
  239. * @returns {Array}
  240. */
  241. getCornersOfSelectedCells() {
  242. if (this.hot.selection.isMultiple()) {
  243. return this.hot.view.wt.selections.area.getCorners();
  244. }
  245. return this.hot.view.wt.selections.current.getCorners();
  246. }
  247. /**
  248. * Get index of last adjacent filled in row
  249. *
  250. * @private
  251. * @param {Array} cornersOfSelectedCells indexes of selection corners.
  252. * @returns {Number} gives number greater than or equal to zero when selection adjacent can be applied.
  253. * or -1 when selection adjacent can't be applied
  254. */
  255. getIndexOfLastAdjacentFilledInRow(cornersOfSelectedCells) {
  256. const data = this.hot.getData();
  257. const nrOfTableRows = this.hot.countRows();
  258. let lastFilledInRowIndex;
  259. for (let rowIndex = cornersOfSelectedCells[2] + 1; rowIndex < nrOfTableRows; rowIndex++) {
  260. for (let columnIndex = cornersOfSelectedCells[1]; columnIndex <= cornersOfSelectedCells[3]; columnIndex++) {
  261. const dataInCell = data[rowIndex][columnIndex];
  262. if (dataInCell) {
  263. return -1;
  264. }
  265. }
  266. const dataInNextLeftCell = data[rowIndex][cornersOfSelectedCells[1] - 1];
  267. const dataInNextRightCell = data[rowIndex][cornersOfSelectedCells[3] + 1];
  268. if (!!dataInNextLeftCell || !!dataInNextRightCell) {
  269. lastFilledInRowIndex = rowIndex;
  270. }
  271. }
  272. return lastFilledInRowIndex;
  273. }
  274. /**
  275. * Add a selection from the start area to the specific row index.
  276. *
  277. * @private
  278. * @param {Array} selectStartArea selection area from which we start to create more comprehensive selection.
  279. * @param {Number} rowIndex
  280. */
  281. addSelectionFromStartAreaToSpecificRowIndex(selectStartArea, rowIndex) {
  282. this.hot.view.wt.selections.fill.clear();
  283. this.hot.view.wt.selections.fill.add(new CellCoords(
  284. selectStartArea[0],
  285. selectStartArea[1])
  286. );
  287. this.hot.view.wt.selections.fill.add(new CellCoords(
  288. rowIndex,
  289. selectStartArea[3])
  290. );
  291. }
  292. /**
  293. * Set selection based on passed corners.
  294. *
  295. * @private
  296. * @param {Array} cornersOfArea
  297. */
  298. setSelection(cornersOfArea) {
  299. this.hot.selection.setRangeStart(new CellCoords(
  300. cornersOfArea[0],
  301. cornersOfArea[1])
  302. );
  303. this.hot.selection.setRangeEnd(new CellCoords(
  304. cornersOfArea[2],
  305. cornersOfArea[3])
  306. );
  307. }
  308. /**
  309. * Try to select cells down to the last row in the left column and then returns if selection was applied.
  310. *
  311. * @private
  312. * @returns {Boolean}
  313. */
  314. selectAdjacent() {
  315. const cornersOfSelectedCells = this.getCornersOfSelectedCells();
  316. const lastFilledInRowIndex = this.getIndexOfLastAdjacentFilledInRow(cornersOfSelectedCells);
  317. if (lastFilledInRowIndex === -1) {
  318. return false;
  319. }
  320. this.addSelectionFromStartAreaToSpecificRowIndex(cornersOfSelectedCells, lastFilledInRowIndex);
  321. return true;
  322. }
  323. /**
  324. * Reset selection of dragged area.
  325. *
  326. * @private
  327. */
  328. resetSelectionOfDraggedArea() {
  329. this.handleDraggedCells = 0;
  330. this.hot.view.wt.selections.fill.clear();
  331. }
  332. /**
  333. * Redraw borders.
  334. *
  335. * @private
  336. * @param {CellCoords} coords `CellCoords` coord object.
  337. */
  338. redrawBorders(coords) {
  339. this.hot.view.wt.selections.fill.clear();
  340. this.hot.view.wt.selections.fill.add(this.hot.getSelectedRange().from);
  341. this.hot.view.wt.selections.fill.add(this.hot.getSelectedRange().to);
  342. this.hot.view.wt.selections.fill.add(coords);
  343. this.hot.view.render();
  344. }
  345. /**
  346. * Get if mouse was dragged outside.
  347. *
  348. * @private
  349. * @param {MouseEvent} event `mousemove` event properties.
  350. * @returns {Boolean}
  351. */
  352. getIfMouseWasDraggedOutside(event) {
  353. const tableBottom = offset(this.hot.table).top - (window.pageYOffset ||
  354. document.documentElement.scrollTop) + outerHeight(this.hot.table);
  355. const tableRight = offset(this.hot.table).left - (window.pageXOffset ||
  356. document.documentElement.scrollLeft) + outerWidth(this.hot.table);
  357. return event.clientY > tableBottom && event.clientX <= tableRight;
  358. }
  359. /**
  360. * Bind the events used by the plugin.
  361. *
  362. * @private
  363. */
  364. registerEvents() {
  365. this.eventManager.addEventListener(document.documentElement, 'mouseup', () => this.onMouseUp());
  366. this.eventManager.addEventListener(document.documentElement, 'mousemove', (event) => this.onMouseMove(event));
  367. }
  368. /**
  369. * On cell corner double click callback.
  370. *
  371. * @private
  372. */
  373. onCellCornerDblClick() {
  374. const selectionApplied = this.selectAdjacent();
  375. if (selectionApplied) {
  376. this.fillIn();
  377. }
  378. }
  379. /**
  380. * On after cell corner mouse down listener.
  381. *
  382. * @private
  383. */
  384. onAfterCellCornerMouseDown() {
  385. this.handleDraggedCells = 1;
  386. this.mouseDownOnCellCorner = true;
  387. }
  388. /**
  389. * On before cell mouse over listener.
  390. *
  391. * @private
  392. * @param {CellCoords} coords `CellCoords` coord object.
  393. */
  394. onBeforeCellMouseOver(coords) {
  395. if (this.mouseDownOnCellCorner && !this.hot.view.isMouseDown() && this.handleDraggedCells) {
  396. this.handleDraggedCells++;
  397. this.showBorder(coords);
  398. this.addNewRowIfNeeded();
  399. }
  400. }
  401. /**
  402. * On mouse up listener.
  403. *
  404. * @private
  405. */
  406. onMouseUp() {
  407. if (this.handleDraggedCells) {
  408. if (this.handleDraggedCells > 1) {
  409. this.fillIn();
  410. }
  411. this.handleDraggedCells = 0;
  412. this.mouseDownOnCellCorner = false;
  413. }
  414. }
  415. /**
  416. * On mouse move listener.
  417. *
  418. * @private
  419. * @param {MouseEvent} event `mousemove` event properties.
  420. */
  421. onMouseMove(event) {
  422. const mouseWasDraggedOutside = this.getIfMouseWasDraggedOutside(event);
  423. if (this.addingStarted === false && this.handleDraggedCells > 0 && mouseWasDraggedOutside) {
  424. this.mouseDragOutside = true;
  425. this.addingStarted = true;
  426. } else {
  427. this.mouseDragOutside = false;
  428. }
  429. if (this.mouseDragOutside && this.autoInsertRow) {
  430. this.addRow();
  431. }
  432. }
  433. /**
  434. * Clear mapped settings.
  435. *
  436. * @private
  437. */
  438. clearMappedSettings() {
  439. this.directions.length = 0;
  440. this.autoInsertRow = false;
  441. }
  442. /**
  443. * Map settings.
  444. *
  445. * @private
  446. */
  447. mapSettings() {
  448. const mappedSettings = getMappedFillHandleSetting(this.hot.getSettings().fillHandle);
  449. this.directions = mappedSettings.directions;
  450. this.autoInsertRow = mappedSettings.autoInsertRow;
  451. }
  452. /**
  453. * Destroy plugin instance.
  454. */
  455. destroy() {
  456. super.destroy();
  457. }
  458. }
  459. registerPlugin('autofill', Autofill);
  460. export default Autofill;