copyPaste.js 11 KB

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