@quantlab/handsontable
Version:
Spreadsheet-like data grid editor that provides copy/paste functionality compatible with Excel/Google Docs
646 lines (513 loc) • 20.4 kB
JavaScript
'use strict';
exports.__esModule = true;
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
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; }; }();
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); } };
var _base = require('./../_base.js');
var _base2 = _interopRequireDefault(_base);
var _pluginHooks = require('./../../pluginHooks');
var _pluginHooks2 = _interopRequireDefault(_pluginHooks);
var _SheetClip = require('./../../../lib/SheetClip/SheetClip');
var _SheetClip2 = _interopRequireDefault(_SheetClip);
var _src = require('./../../3rdparty/walkontable/src');
var _unicode = require('./../../helpers/unicode');
var _element = require('./../../helpers/dom/element');
var _array = require('./../../helpers/array');
var _number = require('./../../helpers/number');
var _event = require('./../../helpers/dom/event');
var _plugins = require('./../../plugins');
var _textarea = require('./textarea');
var _textarea2 = _interopRequireDefault(_textarea);
var _copy = require('./contextMenuItem/copy');
var _copy2 = _interopRequireDefault(_copy);
var _cut = require('./contextMenuItem/cut');
var _cut2 = _interopRequireDefault(_cut);
var _eventManager = require('./../../eventManager');
var _eventManager2 = _interopRequireDefault(_eventManager);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
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; }
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; }
_pluginHooks2.default.getSingleton().register('afterCopyLimit');
_pluginHooks2.default.getSingleton().register('modifyCopyableRange');
_pluginHooks2.default.getSingleton().register('beforeCut');
_pluginHooks2.default.getSingleton().register('afterCut');
_pluginHooks2.default.getSingleton().register('beforePaste');
_pluginHooks2.default.getSingleton().register('afterPaste');
_pluginHooks2.default.getSingleton().register('beforeCopy');
_pluginHooks2.default.getSingleton().register('afterCopy');
var ROWS_LIMIT = 1000;
var COLUMNS_LIMIT = 1000;
var privatePool = new WeakMap();
/**
* @description
* This plugin enables the copy/paste functionality in the Handsontable.
*
* @example
* ```js
* ...
* copyPaste: true,
* ...
* ```
* @class CopyPaste
* @plugin CopyPaste
*/
var CopyPaste = function (_BasePlugin) {
_inherits(CopyPaste, _BasePlugin);
function CopyPaste(hotInstance) {
_classCallCheck(this, CopyPaste);
/**
* Event manager
*
* @type {EventManager}
*/
var _this = _possibleConstructorReturn(this, (CopyPaste.__proto__ || Object.getPrototypeOf(CopyPaste)).call(this, hotInstance));
_this.eventManager = new _eventManager2.default(_this);
/**
* Maximum number of columns than can be copied to clipboard using <kbd>CTRL</kbd> + <kbd>C</kbd>.
*
* @private
* @type {Number}
* @default 1000
*/
_this.columnsLimit = COLUMNS_LIMIT;
/**
* Ranges of the cells coordinates, which should be used to copy/cut/paste actions.
*
* @private
* @type {Array}
*/
_this.copyableRanges = [];
/**
* Defines paste (<kbd>CTRL</kbd> + <kbd>V</kbd>) behavior.
* * Default value `"overwrite"` will paste clipboard value over current selection.
* * When set to `"shift_down"`, clipboard data will be pasted in place of current selection, while all selected cells are moved down.
* * When set to `"shift_right"`, clipboard data will be pasted in place of current selection, while all selected cells are moved right.
*
* @private
* @type {String}
* @default 'overwrite'
*/
_this.pasteMode = 'overwrite';
/**
* Maximum number of rows than can be copied to clipboard using <kbd>CTRL</kbd> + <kbd>C</kbd>.
*
* @private
* @type {Number}
* @default 1000
*/
_this.rowsLimit = ROWS_LIMIT;
/**
* The `textarea` element which is necessary to process copying, cutting off and pasting.
*
* @private
* @type {HTMLElement}
* @default undefined
*/
_this.textarea = void 0;
privatePool.set(_this, {
isTriggeredByPaste: false
});
return _this;
}
/**
* Check if plugin is enabled.
*
* @returns {Boolean}
*/
_createClass(CopyPaste, [{
key: 'isEnabled',
value: function isEnabled() {
return !!this.hot.getSettings().copyPaste;
}
/**
* Enable the plugin.
*/
}, {
key: 'enablePlugin',
value: function enablePlugin() {
var _this2 = this;
if (this.enabled) {
return;
}
var settings = this.hot.getSettings();
this.textarea = _textarea2.default.getSingleton();
if (_typeof(settings.copyPaste) === 'object') {
this.pasteMode = settings.copyPaste.pasteMode || this.pasteMode;
this.rowsLimit = settings.copyPaste.rowsLimit || this.rowsLimit;
this.columnsLimit = settings.copyPaste.columnsLimit || this.columnsLimit;
}
this.addHook('afterContextMenuDefaultOptions', function (options) {
return _this2.onAfterContextMenuDefaultOptions(options);
});
this.addHook('beforeKeyDown', function (event) {
return _this2.onBeforeKeyDown(event);
});
// this.addHook('beforeOnCellMouseDown', () => this.onBeforeOnCellMouseDown());
this.registerEvents();
_get(CopyPaste.prototype.__proto__ || Object.getPrototypeOf(CopyPaste.prototype), 'enablePlugin', this).call(this);
}
/**
* Updates the plugin to use the latest options you have specified.
*/
}, {
key: 'updatePlugin',
value: function updatePlugin() {
this.disablePlugin();
this.enablePlugin();
_get(CopyPaste.prototype.__proto__ || Object.getPrototypeOf(CopyPaste.prototype), 'updatePlugin', this).call(this);
}
/**
* Disable plugin for this Handsontable instance.
*/
}, {
key: 'disablePlugin',
value: function disablePlugin() {
if (this.textarea) {
this.textarea.destroy();
}
_get(CopyPaste.prototype.__proto__ || Object.getPrototypeOf(CopyPaste.prototype), 'disablePlugin', this).call(this);
}
/**
* Prepares copyable text in the invisible textarea.
*
* @function setCopyable
* @memberof CopyPaste#
*/
}, {
key: 'setCopyableText',
value: function setCopyableText() {
var selRange = this.hot.getSelectedRange();
var topLeft = selRange.getTopLeftCorner();
var bottomRight = selRange.getBottomRightCorner();
var startRow = topLeft.row;
var startCol = topLeft.col;
var endRow = bottomRight.row;
var endCol = bottomRight.col;
var finalEndRow = Math.min(endRow, startRow + this.rowsLimit - 1);
var finalEndCol = Math.min(endCol, startCol + this.columnsLimit - 1);
this.copyableRanges.length = 0;
this.copyableRanges.push({
startRow: startRow,
startCol: startCol,
endRow: finalEndRow,
endCol: finalEndCol
});
this.copyableRanges = this.hot.runHooks('modifyCopyableRange', this.copyableRanges);
var copyableData = this.getRangedCopyableData(this.copyableRanges);
this.textarea.setValue(copyableData);
if (endRow !== finalEndRow || endCol !== finalEndCol) {
this.hot.runHooks('afterCopyLimit', endRow - startRow + 1, endCol - startCol + 1, this.rowsLimit, this.columnsLimit);
}
}
/**
* Create copyable text releated to range objects.
*
* @since 0.19.0
* @param {Array} ranges Array of Objects with properties `startRow`, `endRow`, `startCol` and `endCol`.
* @returns {String} Returns string which will be copied into clipboard.
*/
}, {
key: 'getRangedCopyableData',
value: function getRangedCopyableData(ranges) {
var _this3 = this;
var dataSet = [];
var copyableRows = [];
var copyableColumns = [];
// Count all copyable rows and columns
(0, _array.arrayEach)(ranges, function (range) {
(0, _number.rangeEach)(range.startRow, range.endRow, function (row) {
if (copyableRows.indexOf(row) === -1) {
copyableRows.push(row);
}
});
(0, _number.rangeEach)(range.startCol, range.endCol, function (column) {
if (copyableColumns.indexOf(column) === -1) {
copyableColumns.push(column);
}
});
});
// Concat all rows and columns data defined in ranges into one copyable string
(0, _array.arrayEach)(copyableRows, function (row) {
var rowSet = [];
(0, _array.arrayEach)(copyableColumns, function (column) {
rowSet.push(_this3.hot.getCopyableData(row, column));
});
dataSet.push(rowSet);
});
return _SheetClip2.default.stringify(dataSet);
}
/**
* Create copyable text releated to range objects.
*
* @since 0.31.1
* @param {Array} ranges Array of Objects with properties `startRow`, `startCol`, `endRow` and `endCol`.
* @returns {Array} Returns array of arrays which will be copied into clipboard.
*/
}, {
key: 'getRangedData',
value: function getRangedData(ranges) {
var _this4 = this;
var dataSet = [];
var copyableRows = [];
var copyableColumns = [];
// Count all copyable rows and columns
(0, _array.arrayEach)(ranges, function (range) {
(0, _number.rangeEach)(range.startRow, range.endRow, function (row) {
if (copyableRows.indexOf(row) === -1) {
copyableRows.push(row);
}
});
(0, _number.rangeEach)(range.startCol, range.endCol, function (column) {
if (copyableColumns.indexOf(column) === -1) {
copyableColumns.push(column);
}
});
});
// Concat all rows and columns data defined in ranges into one copyable string
(0, _array.arrayEach)(copyableRows, function (row) {
var rowSet = [];
(0, _array.arrayEach)(copyableColumns, function (column) {
rowSet.push(_this4.hot.getCopyableData(row, column));
});
dataSet.push(rowSet);
});
return dataSet;
}
/**
* Copy action.
*
* @param {Boolean} isTriggeredByClick Flag to determine that copy action was executed by the mouse click.
*/
}, {
key: 'copy',
value: function copy(isTriggeredByClick) {
var rangedData = this.getRangedData(this.copyableRanges);
var allowCopying = !!this.hot.runHooks('beforeCopy', rangedData, this.copyableRanges);
if (allowCopying) {
this.textarea.setValue(_SheetClip2.default.stringify(rangedData));
this.textarea.select();
if (isTriggeredByClick) {
document.execCommand('copy');
}
this.hot.runHooks('afterCopy', rangedData, this.copyableRanges);
} else {
this.textarea.setValue('');
}
}
/**
* Cut action.
*
* @param {Boolean} isTriggeredByClick Flag to determine that cut action was executed by the mouse click.
*/
}, {
key: 'cut',
value: function cut(isTriggeredByClick) {
var rangedData = this.getRangedData(this.copyableRanges);
var allowCuttingOut = !!this.hot.runHooks('beforeCut', rangedData, this.copyableRanges);
if (allowCuttingOut) {
this.textarea.setValue(_SheetClip2.default.stringify(rangedData));
this.hot.selection.empty();
this.textarea.select();
if (isTriggeredByClick) {
document.execCommand('cut');
}
this.hot.runHooks('afterCut', rangedData, this.copyableRanges);
} else {
this.textarea.setValue('');
}
}
/**
* Simulated paste action.
*
* @param {String} [value=''] New value, which should be `pasted`.
*/
}, {
key: 'paste',
value: function paste() {
var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
this.textarea.setValue(value);
this.onPaste();
this.onInput();
}
/**
* Register event listeners.
*
* @private
*/
}, {
key: 'registerEvents',
value: function registerEvents() {
var _this5 = this;
this.eventManager.addEventListener(this.textarea.element, 'paste', function (event) {
return _this5.onPaste(event);
});
this.eventManager.addEventListener(this.textarea.element, 'input', function (event) {
return _this5.onInput(event);
});
}
/**
* Trigger to make possible observe `onInput` in textarea.
*
* @private
*/
}, {
key: 'triggerPaste',
value: function triggerPaste() {
this.textarea.select();
this.onPaste();
}
/**
* `paste` event callback on textarea element.
*
* @private
*/
}, {
key: 'onPaste',
value: function onPaste() {
var priv = privatePool.get(this);
priv.isTriggeredByPaste = true;
}
/**
* `input` event callback is called after `paste` event callback.
*
* @private
*/
}, {
key: 'onInput',
value: function onInput() {
var _this6 = this;
var priv = privatePool.get(this);
if (!this.hot.isListening() || !priv.isTriggeredByPaste) {
return;
}
priv.isTriggeredByPaste = false;
var input = void 0,
inputArray = void 0,
selected = void 0,
coordsFrom = void 0,
coordsTo = void 0,
cellRange = void 0,
topLeftCorner = void 0,
bottomRightCorner = void 0,
areaStart = void 0,
areaEnd = void 0;
input = this.textarea.getValue();
inputArray = _SheetClip2.default.parse(input);
var allowPasting = !!this.hot.runHooks('beforePaste', inputArray, this.copyableRanges);
if (!allowPasting) {
return;
}
selected = this.hot.getSelected();
coordsFrom = new _src.CellCoords(selected[0], selected[1]);
coordsTo = new _src.CellCoords(selected[2], selected[3]);
cellRange = new _src.CellRange(coordsFrom, coordsFrom, coordsTo);
topLeftCorner = cellRange.getTopLeftCorner();
bottomRightCorner = cellRange.getBottomRightCorner();
areaStart = topLeftCorner;
areaEnd = new _src.CellCoords(Math.max(bottomRightCorner.row, inputArray.length - 1 + topLeftCorner.row), Math.max(bottomRightCorner.col, inputArray[0].length - 1 + topLeftCorner.col));
var isSelRowAreaCoverInputValue = coordsTo.row - coordsFrom.row >= inputArray.length - 1;
var isSelColAreaCoverInputValue = coordsTo.col - coordsFrom.col >= inputArray[0].length - 1;
this.hot.addHookOnce('afterChange', function (changes, source) {
var changesLength = changes ? changes.length : 0;
if (changesLength) {
var offset = { row: 0, col: 0 };
var highestColumnIndex = -1;
(0, _array.arrayEach)(changes, function (change, index) {
var nextChange = changesLength > index + 1 ? changes[index + 1] : null;
if (nextChange) {
if (!isSelRowAreaCoverInputValue) {
offset.row += Math.max(nextChange[0] - change[0] - 1, 0);
}
if (!isSelColAreaCoverInputValue && change[1] > highestColumnIndex) {
highestColumnIndex = change[1];
offset.col += Math.max(nextChange[1] - change[1] - 1, 0);
}
}
});
_this6.hot.selectCell(areaStart.row, areaStart.col, areaEnd.row + offset.row, areaEnd.col + offset.col);
}
});
this.hot.populateFromArray(areaStart.row, areaStart.col, inputArray, areaEnd.row, areaEnd.col, 'CopyPaste.paste', this.pasteMode);
this.hot.runHooks('afterPaste', inputArray, this.copyableRanges);
}
/**
* Add copy, cut and paste options to the Context Menu.
*
* @private
* @param {Object} options Contains default added options of the Context Menu.
*/
}, {
key: 'onAfterContextMenuDefaultOptions',
value: function onAfterContextMenuDefaultOptions(options) {
options.items.push({
name: '---------'
}, (0, _copy2.default)(this), (0, _cut2.default)(this));
}
/**
* beforeKeyDown callback.
*
* @private
* @param {Event} event
*/
}, {
key: 'onBeforeKeyDown',
value: function onBeforeKeyDown(event) {
var _this7 = this;
if (!this.hot.getSelected()) {
return;
}
if (this.hot.getActiveEditor() && this.hot.getActiveEditor().isOpened()) {
return;
}
if ((0, _event.isImmediatePropagationStopped)(event)) {
return;
}
if (!this.textarea.isActive() && (0, _element.getSelectionText)()) {
return;
}
if ((0, _unicode.isCtrlKey)(event.keyCode)) {
// When fragmentSelection is enabled and some text is selected then don't blur selection calling 'setCopyableText'
if (this.hot.getSettings().fragmentSelection && (0, _element.getSelectionText)()) {
return;
}
// when CTRL is pressed, prepare selectable text in textarea
this.setCopyableText();
(0, _event.stopImmediatePropagation)(event);
return;
}
// catch CTRL but not right ALT (which in some systems triggers ALT+CTRL)
var ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey;
if (ctrlDown) {
if (event.keyCode == _unicode.KEY_CODES.A) {
setTimeout(function () {
_this7.setCopyableText();
}, 0);
}
if (event.keyCode == _unicode.KEY_CODES.X) {
this.cut();
}
if (event.keyCode == _unicode.KEY_CODES.C) {
this.copy();
}
if (event.keyCode == _unicode.KEY_CODES.V) {
this.triggerPaste();
}
}
}
/**
* Destroy plugin instance.
*/
}, {
key: 'destroy',
value: function destroy() {
if (this.textarea) {
this.textarea.destroy();
}
_get(CopyPaste.prototype.__proto__ || Object.getPrototypeOf(CopyPaste.prototype), 'destroy', this).call(this);
}
}]);
return CopyPaste;
}(_base2.default);
(0, _plugins.registerPlugin)('CopyPaste', CopyPaste);
exports.default = CopyPaste;