7397d44e6cd4f76fc9068c92fb2efb3e1e24b51c62a4b3b570543b018e073990b848cf2a3f35c58975d3fccd49035345fe9f4b13d9c0f4fee79a504d21cc0f 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import copyPaste from './../../../lib/copyPaste/copyPaste';
  2. import SheetClip from './../../../lib/SheetClip/SheetClip';
  3. import Hooks from './../../pluginHooks';
  4. import {KEY_CODES, isCtrlKey} from './../../helpers/unicode';
  5. import {arrayEach} from './../../helpers/array';
  6. import {rangeEach} from './../../helpers/number';
  7. import {stopImmediatePropagation, isImmediatePropagationStopped} from './../../helpers/dom/event';
  8. import {getSelectionText} from './../../helpers/dom/element';
  9. import {CellCoords, CellRange} from './../../3rdparty/walkontable/src';
  10. Hooks.getSingleton().register('afterCopyLimit');
  11. Hooks.getSingleton().register('modifyCopyableRange');
  12. Hooks.getSingleton().register('beforeCut');
  13. Hooks.getSingleton().register('afterCut');
  14. Hooks.getSingleton().register('beforePaste');
  15. Hooks.getSingleton().register('afterPaste');
  16. Hooks.getSingleton().register('beforeCopy');
  17. Hooks.getSingleton().register('afterCopy');
  18. /**
  19. * @description
  20. * This plugin enables the copy/paste functionality in Handsontable.
  21. *
  22. * @example
  23. * ```js
  24. * ...
  25. * copyPaste: true,
  26. * ...
  27. * ```
  28. * @class CopyPaste
  29. * @plugin CopyPaste
  30. */
  31. function CopyPastePlugin(instance) {
  32. var _this = this;
  33. this.copyPasteInstance = copyPaste();
  34. this.copyPasteInstance.onCut(onCut);
  35. this.copyPasteInstance.triggerCopy = callCopyAction;
  36. this.copyPasteInstance.onPaste(onPaste);
  37. this.onPaste = onPaste; // for paste testing purposes
  38. this.copyableRanges = [];
  39. instance.addHook('beforeKeyDown', onBeforeKeyDown);
  40. function onCut() {
  41. instance.isListening();
  42. }
  43. function callCutAction() {
  44. let rangedData = _this.getRangedData(_this.copyableRanges);
  45. if (instance.getSettings().fragmentSelection && (SheetClip.stringify(rangedData) != getSelectionText())) {
  46. return;
  47. }
  48. let allowCuttingOut = !!instance.runHooks('beforeCut', rangedData, _this.copyableRanges);
  49. if (allowCuttingOut) {
  50. instance.copyPaste.copyPasteInstance.copyable(SheetClip.stringify(rangedData));
  51. instance.selection.empty();
  52. instance.runHooks('afterCut', rangedData, _this.copyableRanges);
  53. } else {
  54. instance.copyPaste.copyPasteInstance.copyable('');
  55. }
  56. }
  57. function callCopyAction() {
  58. if (!instance.isListening()) {
  59. return;
  60. }
  61. let rangedData = _this.getRangedData(_this.copyableRanges);
  62. if (instance.getSettings().fragmentSelection && (SheetClip.stringify(rangedData) != getSelectionText())) {
  63. return;
  64. }
  65. let allowCopying = !!instance.runHooks('beforeCopy', rangedData, _this.copyableRanges);
  66. if (allowCopying) {
  67. instance.copyPaste.copyPasteInstance.copyable(SheetClip.stringify(rangedData));
  68. instance.runHooks('afterCopy', rangedData, _this.copyableRanges);
  69. } else {
  70. instance.copyPaste.copyPasteInstance.copyable('');
  71. }
  72. }
  73. function onPaste(str) {
  74. var
  75. input,
  76. inputArray,
  77. selected,
  78. coordsFrom,
  79. coordsTo,
  80. cellRange,
  81. topLeftCorner,
  82. bottomRightCorner,
  83. areaStart,
  84. areaEnd;
  85. if (!instance.isListening() || !instance.selection.isSelected()) {
  86. return;
  87. }
  88. input = str;
  89. inputArray = SheetClip.parse(input);
  90. selected = instance.getSelected();
  91. coordsFrom = new CellCoords(selected[0], selected[1]);
  92. coordsTo = new CellCoords(selected[2], selected[3]);
  93. cellRange = new CellRange(coordsFrom, coordsFrom, coordsTo);
  94. topLeftCorner = cellRange.getTopLeftCorner();
  95. bottomRightCorner = cellRange.getBottomRightCorner();
  96. areaStart = topLeftCorner;
  97. areaEnd = new CellCoords(
  98. Math.max(bottomRightCorner.row, inputArray.length - 1 + topLeftCorner.row),
  99. Math.max(bottomRightCorner.col, inputArray[0].length - 1 + topLeftCorner.col));
  100. let isSelRowAreaCoverInputValue = coordsTo.row - coordsFrom.row >= inputArray.length - 1;
  101. let isSelColAreaCoverInputValue = coordsTo.col - coordsFrom.col >= inputArray[0].length - 1;
  102. instance.addHookOnce('afterChange', (changes, source) => {
  103. let changesLength = changes ? changes.length : 0;
  104. if (changesLength) {
  105. let offset = {row: 0, col: 0};
  106. let highestColumnIndex = -1;
  107. arrayEach(changes, (change, index) => {
  108. let nextChange = changesLength > index + 1 ? changes[index + 1] : null;
  109. if (nextChange) {
  110. if (!isSelRowAreaCoverInputValue) {
  111. offset.row += Math.max(nextChange[0] - change[0] - 1, 0);
  112. }
  113. if (!isSelColAreaCoverInputValue && change[1] > highestColumnIndex) {
  114. highestColumnIndex = change[1];
  115. offset.col += Math.max(nextChange[1] - change[1] - 1, 0);
  116. }
  117. }
  118. });
  119. instance.selectCell(areaStart.row, areaStart.col, areaEnd.row + offset.row, areaEnd.col + offset.col);
  120. }
  121. });
  122. let allowPasting = !!instance.runHooks('beforePaste', inputArray, _this.copyableRanges);
  123. if (allowPasting) {
  124. instance.populateFromArray(areaStart.row, areaStart.col, inputArray, areaEnd.row, areaEnd.col, 'CopyPaste.paste', instance.getSettings().pasteMode);
  125. instance.runHooks('afterPaste', inputArray, _this.copyableRanges);
  126. }
  127. }
  128. function onBeforeKeyDown(event) {
  129. if (!instance.getSelected()) {
  130. return;
  131. }
  132. if (instance.getActiveEditor() && instance.getActiveEditor().isOpened()) {
  133. return;
  134. }
  135. if (isImmediatePropagationStopped(event)) {
  136. return;
  137. }
  138. if (isCtrlKey(event.keyCode)) {
  139. // When fragmentSelection is enabled and some text is selected then don't blur selection calling 'setCopyableText'
  140. if (instance.getSettings().fragmentSelection && getSelectionText()) {
  141. return;
  142. }
  143. // when CTRL is pressed, prepare selectable text in textarea
  144. _this.setCopyableText();
  145. stopImmediatePropagation(event);
  146. return;
  147. }
  148. // catch CTRL but not right ALT (which in some systems triggers ALT+CTRL)
  149. let ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey;
  150. if (ctrlDown) {
  151. if (event.keyCode == KEY_CODES.A) {
  152. instance._registerTimeout(setTimeout(_this.setCopyableText.bind(_this), 0));
  153. }
  154. if (event.keyCode == KEY_CODES.X) {
  155. callCutAction();
  156. }
  157. if (event.keyCode == KEY_CODES.C) {
  158. callCopyAction();
  159. }
  160. }
  161. }
  162. /**
  163. * Destroy plugin instance.
  164. *
  165. * @function destroy
  166. * @memberof CopyPaste#
  167. */
  168. this.destroy = function() {
  169. if (this.copyPasteInstance) {
  170. this.copyPasteInstance.removeCallback(onCut);
  171. this.copyPasteInstance.removeCallback(onPaste);
  172. this.copyPasteInstance.destroy();
  173. this.copyPasteInstance = null;
  174. }
  175. instance.removeHook('beforeKeyDown', onBeforeKeyDown);
  176. };
  177. instance.addHook('afterDestroy', this.destroy.bind(this));
  178. /**
  179. * @function triggerPaste
  180. * @memberof CopyPaste#
  181. */
  182. this.triggerPaste = this.copyPasteInstance.triggerPaste.bind(this.copyPasteInstance);
  183. /**
  184. * @function triggerCut
  185. * @memberof CopyPaste#
  186. */
  187. this.triggerCut = this.copyPasteInstance.triggerCut.bind(this.copyPasteInstance);
  188. /**
  189. * Prepares copyable text in the invisible textarea.
  190. *
  191. * @function setCopyable
  192. * @memberof CopyPaste#
  193. */
  194. this.setCopyableText = function() {
  195. var settings = instance.getSettings();
  196. var copyRowsLimit = settings.copyRowsLimit;
  197. var copyColsLimit = settings.copyColsLimit;
  198. var selRange = instance.getSelectedRange();
  199. var topLeft = selRange.getTopLeftCorner();
  200. var bottomRight = selRange.getBottomRightCorner();
  201. var startRow = topLeft.row;
  202. var startCol = topLeft.col;
  203. var endRow = bottomRight.row;
  204. var endCol = bottomRight.col;
  205. var finalEndRow = Math.min(endRow, startRow + copyRowsLimit - 1);
  206. var finalEndCol = Math.min(endCol, startCol + copyColsLimit - 1);
  207. this.copyableRanges.length = 0;
  208. this.copyableRanges.push({
  209. startRow,
  210. startCol,
  211. endRow: finalEndRow,
  212. endCol: finalEndCol
  213. });
  214. this.copyableRanges = instance.runHooks('modifyCopyableRange', this.copyableRanges);
  215. let copyableData = this.getRangedCopyableData(this.copyableRanges);
  216. instance.copyPaste.copyPasteInstance.copyable(copyableData);
  217. if (endRow !== finalEndRow || endCol !== finalEndCol) {
  218. instance.runHooks('afterCopyLimit', endRow - startRow + 1, endCol - startCol + 1, copyRowsLimit, copyColsLimit);
  219. }
  220. };
  221. /**
  222. * Create copyable text releated to range objects.
  223. *
  224. * @since 0.19.0
  225. * @param {Array} ranges Array of Objects with properties `startRow`, `endRow`, `startCol` and `endCol`.
  226. * @returns {String} Returns string which will be copied into clipboard.
  227. */
  228. this.getRangedCopyableData = function(ranges) {
  229. let dataSet = [];
  230. let copyableRows = [];
  231. let copyableColumns = [];
  232. // Count all copyable rows and columns
  233. arrayEach(ranges, (range) => {
  234. rangeEach(range.startRow, range.endRow, (row) => {
  235. if (copyableRows.indexOf(row) === -1) {
  236. copyableRows.push(row);
  237. }
  238. });
  239. rangeEach(range.startCol, range.endCol, (column) => {
  240. if (copyableColumns.indexOf(column) === -1) {
  241. copyableColumns.push(column);
  242. }
  243. });
  244. });
  245. // Concat all rows and columns data defined in ranges into one copyable string
  246. arrayEach(copyableRows, (row) => {
  247. let rowSet = [];
  248. arrayEach(copyableColumns, (column) => {
  249. rowSet.push(instance.getCopyableData(row, column));
  250. });
  251. dataSet.push(rowSet);
  252. });
  253. return SheetClip.stringify(dataSet);
  254. };
  255. /**
  256. * Create copyable text releated to range objects.
  257. *
  258. * @since 0.31.1
  259. * @param {Array} ranges Array of Objects with properties `startRow`, `startCol`, `endRow` and `endCol`.
  260. * @returns {Array} Returns array of arrays which will be copied into clipboard.
  261. */
  262. this.getRangedData = function(ranges) {
  263. let dataSet = [];
  264. let copyableRows = [];
  265. let copyableColumns = [];
  266. // Count all copyable rows and columns
  267. arrayEach(ranges, (range) => {
  268. rangeEach(range.startRow, range.endRow, (row) => {
  269. if (copyableRows.indexOf(row) === -1) {
  270. copyableRows.push(row);
  271. }
  272. });
  273. rangeEach(range.startCol, range.endCol, (column) => {
  274. if (copyableColumns.indexOf(column) === -1) {
  275. copyableColumns.push(column);
  276. }
  277. });
  278. });
  279. // Concat all rows and columns data defined in ranges into one copyable string
  280. arrayEach(copyableRows, (row) => {
  281. let rowSet = [];
  282. arrayEach(copyableColumns, (column) => {
  283. rowSet.push(instance.getCopyableData(row, column));
  284. });
  285. dataSet.push(rowSet);
  286. });
  287. return dataSet;
  288. };
  289. }
  290. /**
  291. * Init plugin.
  292. *
  293. * @function init
  294. * @memberof CopyPaste#
  295. */
  296. function init() {
  297. var instance = this,
  298. pluginEnabled = instance.getSettings().copyPaste !== false;
  299. if (pluginEnabled && !instance.copyPaste) {
  300. /**
  301. * Instance of CopyPaste Plugin {@link Handsontable.CopyPaste}
  302. *
  303. * @alias copyPaste
  304. * @memberof! Handsontable.Core#
  305. * @type {CopyPaste}
  306. */
  307. instance.copyPaste = new CopyPastePlugin(instance);
  308. } else if (!pluginEnabled && instance.copyPaste) {
  309. instance.copyPaste.destroy();
  310. instance.copyPaste = null;
  311. }
  312. }
  313. Hooks.getSingleton().add('afterInit', init);
  314. Hooks.getSingleton().add('afterUpdateSettings', init);
  315. export default CopyPastePlugin;