UNPKG

handsontable

Version:

Handsontable is a JavaScript Data Grid available for React, Angular and Vue.

852 lines (830 loc) • 36.1 kB
import "core-js/modules/es.error.cause.js"; import "core-js/modules/es.array.push.js"; import "core-js/modules/es.json.stringify.js"; import "core-js/modules/esnext.iterator.constructor.js"; import "core-js/modules/esnext.iterator.filter.js"; import "core-js/modules/esnext.iterator.map.js"; function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } import { BasePlugin } from "../base/index.mjs"; import { Hooks } from "../../core/hooks/index.mjs"; import { stringify, parse } from "../../3rdparty/SheetClip/index.mjs"; import { arrayEach } from "../../helpers/array.mjs"; import { sanitize, isJSON } from "../../helpers/string.mjs"; import { isObject } from "../../helpers/object.mjs"; import { removeContentEditableFromElementAndDeselect, runWithSelectedContendEditableElement, makeElementContentEditableAndSelectItsContent, isHTMLElement, isInternalElement } from "../../helpers/dom/element.mjs"; import { isSafari } from "../../helpers/browser.mjs"; import copyItem from "./contextMenuItem/copy.mjs"; import copyColumnHeadersOnlyItem from "./contextMenuItem/copyColumnHeadersOnly.mjs"; import copyWithColumnGroupHeadersItem from "./contextMenuItem/copyWithColumnGroupHeaders.mjs"; import copyWithColumnHeadersItem from "./contextMenuItem/copyWithColumnHeaders.mjs"; import cutItem from "./contextMenuItem/cut.mjs"; import PasteEvent from "./pasteEvent.mjs"; import { CopyableRangesFactory, normalizeRanges } from "./copyableRanges.mjs"; import { _dataToHTML, htmlToGridSettings } from "../../utils/parseTable.mjs"; Hooks.getSingleton().register('afterCopyLimit'); Hooks.getSingleton().register('modifyCopyableRange'); Hooks.getSingleton().register('beforeCut'); Hooks.getSingleton().register('afterCut'); Hooks.getSingleton().register('beforePaste'); Hooks.getSingleton().register('afterPaste'); Hooks.getSingleton().register('beforeCopy'); Hooks.getSingleton().register('afterCopy'); export const PLUGIN_KEY = 'copyPaste'; export const PLUGIN_PRIORITY = 80; const SETTING_KEYS = ['fragmentSelection']; const SOURCE_DATA_HTML_MIME_TYPE = 'application/ht-source-data-json-html'; const META_HEAD = ['<meta name="generator" content="Handsontable"/>', '<style type="text/css">td{white-space:normal}br{mso-data-placement:same-cell}</style>'].join(''); /* eslint-disable jsdoc/require-description-complete-sentence */ /** * @description * Copy, cut, and paste data by using the `CopyPaste` plugin. * * Control the `CopyPaste` plugin programmatically through its [API methods](#methods). * * The user can access the copy-paste features through: * - The [context menu](@/guides/cell-features/clipboard/clipboard.md#context-menu). * - The [keyboard shortcuts](@/guides/cell-features/clipboard/clipboard.md#related-keyboard-shortcuts). * - The browser's menu bar. * * Read more: * - [Guides: Clipboard](@/guides/cell-features/clipboard/clipboard.md) * - [Configuration options: `copyPaste`](@/api/options.md#copypaste) * * @example * ```js * // enable the plugin with the default configuration * copyPaste: true, * * // or, enable the plugin with a custom configuration * copyPaste: { * columnsLimit: 25, * rowsLimit: 50, * pasteMode: 'shift_down', * copyColumnHeaders: true, * copyColumnGroupHeaders: true, * copyColumnHeadersOnly: true, * uiContainer: document.body, * }, * ``` * @class CopyPaste * @plugin CopyPaste */ var _enableCopyColumnHeaders = /*#__PURE__*/new WeakMap(); var _enableCopyColumnGroupHeaders = /*#__PURE__*/new WeakMap(); var _enableCopyColumnHeadersOnly = /*#__PURE__*/new WeakMap(); var _copyMode = /*#__PURE__*/new WeakMap(); var _isTriggeredByCopy = /*#__PURE__*/new WeakMap(); var _isTriggeredByCut = /*#__PURE__*/new WeakMap(); var _copyableRangesFactory = /*#__PURE__*/new WeakMap(); var _preventViewportScrollOnPaste = /*#__PURE__*/new WeakMap(); var _CopyPaste_brand = /*#__PURE__*/new WeakSet(); export class CopyPaste extends BasePlugin { constructor() { super(...arguments); /** * Ensure that the `copy`/`cut` events get triggered properly in Safari. * * @param {string} eventName Name of the event to get triggered. */ _classPrivateMethodInitSpec(this, _CopyPaste_brand); /** * The maximum number of columns than can be copied to the clipboard. * * @type {number} * @default Infinity */ _defineProperty(this, "columnsLimit", Infinity); /** * The maximum number of rows than can be copied to the clipboard. * * @type {number} * @default Infinity */ _defineProperty(this, "rowsLimit", Infinity); /** * When pasting: * - `'overwrite'` - overwrite the currently-selected cells * - `'shift_down'` - move currently-selected cells down * - `'shift_right'` - move currently-selected cells to the right * * @type {string} * @default 'overwrite' */ _defineProperty(this, "pasteMode", 'overwrite'); /** * The UI container for the secondary focusable element. * * @type {HTMLElement} */ _defineProperty(this, "uiContainer", this.hot.rootDocument.body); /** * Shows the "Copy with headers" item in the context menu and extends the context menu with the * `'copy_with_column_headers'` option that can be used for creating custom menus arrangements. * * @type {boolean} * @default false */ _classPrivateFieldInitSpec(this, _enableCopyColumnHeaders, false); /** * Shows the "Copy with group headers" item in the context menu and extends the context menu with the * `'copy_with_column_group headers'` option that can be used for creating custom menus arrangements. * * @type {boolean} * @default false */ _classPrivateFieldInitSpec(this, _enableCopyColumnGroupHeaders, false); /** * Shows the "Copy headers only" item in the context menu and extends the context menu with the * `'copy_column_headers_only'` option that can be used for creating custom menus arrangements. * * @type {boolean} * @default false */ _classPrivateFieldInitSpec(this, _enableCopyColumnHeadersOnly, false); /** * Defines the data range to copy. Possible values: * * `'cells-only'` Copy selected cells only; * * `'column-headers-only'` Copy column headers only; * * `'with-column-group-headers'` Copy cells with all column headers; * * `'with-column-headers'` Copy cells with column headers; * * @type {'cells-only' | 'column-headers-only' | 'with-column-group-headers' | 'with-column-headers'} */ _classPrivateFieldInitSpec(this, _copyMode, 'cells-only'); /** * Flag that is used to prevent copying when the native shortcut was not pressed. * * @type {boolean} */ _classPrivateFieldInitSpec(this, _isTriggeredByCopy, false); /** * Flag that is used to prevent cutting when the native shortcut was not pressed. * * @type {boolean} */ _classPrivateFieldInitSpec(this, _isTriggeredByCut, false); /** * Class that helps generate copyable ranges based on the current selection for different copy mode * types. * * @type {CopyableRangesFactory} */ _classPrivateFieldInitSpec(this, _copyableRangesFactory, new CopyableRangesFactory({ countRows: () => this.hot.countRows(), countColumns: () => this.hot.countCols(), rowsLimit: () => this.rowsLimit, columnsLimit: () => this.columnsLimit, countColumnHeaders: () => this.hot.view.getColumnHeadersCount() })); /** * Flag that indicates if the viewport scroll should be prevented after pasting the data. * * @type {boolean} */ _classPrivateFieldInitSpec(this, _preventViewportScrollOnPaste, false); /** * Ranges of the cells coordinates, which should be used to copy/cut/paste actions. * * @private * @type {Array<{startRow: number, startCol: number, endRow: number, endCol: number}>} */ _defineProperty(this, "copyableRanges", []); } static get PLUGIN_KEY() { return PLUGIN_KEY; } static get SETTING_KEYS() { return [PLUGIN_KEY, ...SETTING_KEYS]; } static get PLUGIN_PRIORITY() { return PLUGIN_PRIORITY; } static get DEFAULT_SETTINGS() { return { pasteMode: 'overwrite', rowsLimit: Infinity, columnsLimit: Infinity, copyColumnHeaders: false, copyColumnGroupHeaders: false, copyColumnHeadersOnly: false }; } /** * Checks if the [`CopyPaste`](#copypaste) plugin is enabled. * * This method gets called by Handsontable's [`beforeInit`](@/api/hooks.md#beforeinit) hook. * If it returns `true`, the [`enablePlugin()`](#enableplugin) method gets called. * * @returns {boolean} */ isEnabled() { return !!this.hot.getSettings()[PLUGIN_KEY]; } /** * Enables the [`CopyPaste`](#copypaste) plugin for your Handsontable instance. */ enablePlugin() { var _this$getSetting, _this$getSetting2, _this = this; if (this.enabled) { return; } this.pasteMode = (_this$getSetting = this.getSetting('pasteMode')) !== null && _this$getSetting !== void 0 ? _this$getSetting : this.pasteMode; this.rowsLimit = isNaN(this.getSetting('rowsLimit')) ? this.rowsLimit : this.getSetting('rowsLimit'); this.columnsLimit = isNaN(this.getSetting('columnsLimit')) ? this.columnsLimit : this.getSetting('columnsLimit'); _classPrivateFieldSet(_enableCopyColumnHeaders, this, this.getSetting('copyColumnHeaders')); _classPrivateFieldSet(_enableCopyColumnGroupHeaders, this, this.getSetting('copyColumnGroupHeaders')); _classPrivateFieldSet(_enableCopyColumnHeadersOnly, this, this.getSetting('copyColumnHeadersOnly')); this.uiContainer = (_this$getSetting2 = this.getSetting('uiContainer')) !== null && _this$getSetting2 !== void 0 ? _this$getSetting2 : this.uiContainer; this.addHook('afterContextMenuDefaultOptions', options => _assertClassBrand(_CopyPaste_brand, this, _onAfterContextMenuDefaultOptions).call(this, options)); this.addHook('afterSelection', function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _assertClassBrand(_CopyPaste_brand, _this, _onAfterSelection).call(_this, ...args); }); this.addHook('afterSelectionEnd', () => _assertClassBrand(_CopyPaste_brand, this, _onAfterSelectionEnd).call(this)); // Events are attached to the document, not the root table element - as it should, // for Chrome 133 and lower to copy/paste/cut work properly (#dev-2277). this.eventManager.addEventListener(this.hot.rootDocument, 'copy', function () { return _this.onCopy(...arguments); }); this.eventManager.addEventListener(this.hot.rootDocument, 'cut', function () { return _this.onCut(...arguments); }); this.eventManager.addEventListener(this.hot.rootDocument, 'paste', function () { return _this.onPaste(...arguments); }); // Without this workaround Safari (tested on Safari@16.5.2) does allow copying/cutting from the browser menu. if (isSafari()) { this.eventManager.addEventListener(this.hot.rootDocument.body, 'mouseenter', function () { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return _assertClassBrand(_CopyPaste_brand, _this, _onSafariMouseEnter).call(_this, ...args); }); this.eventManager.addEventListener(this.hot.rootDocument.body, 'mouseleave', function () { for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } return _assertClassBrand(_CopyPaste_brand, _this, _onSafariMouseLeave).call(_this, ...args); }); this.addHook('afterSelection', () => _assertClassBrand(_CopyPaste_brand, this, _onSafariAfterSelection).call(this)); } super.enablePlugin(); } /** * Updates the state of the [`CopyPaste`](#copypaste) plugin. * * Gets called when [`updateSettings()`](@/api/core.md#updatesettings) * is invoked with any of the following configuration options: * - [`copyPaste`](@/api/options.md#copypaste) * - [`fragmentSelection`](@/api/options.md#fragmentselection) */ updatePlugin() { this.disablePlugin(); this.enablePlugin(); super.updatePlugin(); } /** * Disables the [`CopyPaste`](#copypaste) plugin for your Handsontable instance. */ disablePlugin() { super.disablePlugin(); } /** * Copies the contents of the selected cells (and/or their related column headers) to the system clipboard. * * Takes an optional parameter (`copyMode`) that defines the scope of copying: * * | `copyMode` value | Description | * | ----------------------------- | --------------------------------------------------------------- | * | `'cells-only'` (default) | Copy the selected cells | * | `'with-column-headers'` | - Copy the selected cells<br>- Copy the nearest column headers | * | `'with-column-group-headers'` | - Copy the selected cells<br>- Copy all related columns headers | * | `'column-headers-only'` | Copy the nearest column headers (without copying cells) | * * @param {string} [copyMode='cells-only'] Copy mode. */ copy() { let copyMode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'cells-only'; _classPrivateFieldSet(_copyMode, this, copyMode); _classPrivateFieldSet(_isTriggeredByCopy, this, true); _assertClassBrand(_CopyPaste_brand, this, _ensureClipboardEventsGetTriggered).call(this, 'copy'); } /** * Copies the contents of the selected cells. */ copyCellsOnly() { this.copy('cells-only'); } /** * Copies the contents of column headers that are nearest to the selected cells. */ copyColumnHeadersOnly() { this.copy('column-headers-only'); } /** * Copies the contents of the selected cells and all their related column headers. */ copyWithAllColumnHeaders() { this.copy('with-column-group-headers'); } /** * Copies the contents of the selected cells and their nearest column headers. */ copyWithColumnHeaders() { this.copy('with-column-headers'); } /** * Cuts the contents of the selected cells to the system clipboard. */ cut() { _classPrivateFieldSet(_isTriggeredByCut, this, true); _assertClassBrand(_CopyPaste_brand, this, _ensureClipboardEventsGetTriggered).call(this, 'cut'); } /** * Converts the contents of multiple ranges (`ranges`) into a single string. * * @param {Array<{startRow: number, startCol: number, endRow: number, endCol: number}>} ranges Array of objects with properties `startRow`, `endRow`, `startCol` and `endCol`. * @returns {string} A string that will be copied to the clipboard. */ getRangedCopyableData(ranges) { return stringify(this.getRangedData(ranges)); } /** * Converts the contents of multiple ranges (`ranges`) into an array of arrays. * * @param {Array<{startRow: number, startCol: number, endRow: number, endCol: number}>} ranges Array of objects with properties `startRow`, `startCol`, `endRow` and `endCol`. * @param {boolean} [useSourceData=false] Whether to use the source data instead of the data. This will stringify objects as JSON. * @returns {Array[]} An array of arrays that will be copied to the clipboard. */ getRangedData(ranges) { let useSourceData = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; const data = []; const { rows, columns } = normalizeRanges(ranges); // concatenate all rows and columns data defined in ranges into one copyable string arrayEach(rows, row => { const rowSet = []; arrayEach(columns, column => { if (row < 0) { // `row` as the second argument acts here as the `headerLevel` argument rowSet.push(this.hot.getColHeader(column, row)); } else { let copyableCellData = useSourceData ? this.hot.getCopyableSourceData(row, column) : this.hot.getCopyableData(row, column); if (useSourceData && isObject(copyableCellData)) { copyableCellData = JSON.stringify(copyableCellData); } rowSet.push(copyableCellData); } }); data.push(rowSet); }); return data; } /** * Simulates the paste action. * * For security reasons, modern browsers don't allow reading from the system clipboard. * * @param {string} pastableText The value to paste, as a raw string. * @param {string} [pastableHtml=''] The value to paste, as HTML. */ paste() { let pastableText = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; let pastableHtml = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : pastableText; if (!pastableText && !pastableHtml) { return; } const pasteData = new PasteEvent(); if (pastableText) { pasteData.clipboardData.setData('text/plain', pastableText); } if (pastableHtml) { pasteData.clipboardData.setData('text/html', pastableHtml); } this.onPaste(pasteData); } /** * Prepares copyable text from the cells selection in the invisible textarea. */ setCopyableText() { const selectionRange = this.hot.getSelectedRangeActive(); if (!selectionRange) { return; } if (selectionRange.isSingleHeader()) { this.copyableRanges = []; return; } _classPrivateFieldGet(_copyableRangesFactory, this).setSelectedRange(selectionRange); const groupedRanges = new Map([['headers', null], ['cells', null]]); if (_classPrivateFieldGet(_copyMode, this) === 'column-headers-only') { groupedRanges.set('headers', _classPrivateFieldGet(_copyableRangesFactory, this).getMostBottomColumnHeadersRange()); } else { if (_classPrivateFieldGet(_copyMode, this) === 'with-column-headers') { groupedRanges.set('headers', _classPrivateFieldGet(_copyableRangesFactory, this).getMostBottomColumnHeadersRange()); } else if (_classPrivateFieldGet(_copyMode, this) === 'with-column-group-headers') { groupedRanges.set('headers', _classPrivateFieldGet(_copyableRangesFactory, this).getAllColumnHeadersRange()); } groupedRanges.set('cells', _classPrivateFieldGet(_copyableRangesFactory, this).getCellsRange()); } this.copyableRanges = Array.from(groupedRanges.values()).filter(range => range !== null).map(_ref => { let { startRow, startCol, endRow, endCol } = _ref; return { startRow, startCol, endRow, endCol }; }); this.copyableRanges = this.hot.runHooks('modifyCopyableRange', this.copyableRanges); const cellsRange = groupedRanges.get('cells'); if (cellsRange !== null && cellsRange.isRangeTrimmed) { const { startRow, startCol, endRow, endCol } = cellsRange; this.hot.runHooks('afterCopyLimit', endRow - startRow + 1, endCol - startCol + 1, this.rowsLimit, this.columnsLimit); } } /** * Verifies if editor exists and is open. * * @private * @returns {boolean} */ isEditorOpened() { var _this$hot$getActiveEd; return (_this$hot$getActiveEd = this.hot.getActiveEditor()) === null || _this$hot$getActiveEd === void 0 ? void 0 : _this$hot$getActiveEd.isOpened(); } /** * Prepares new values to populate them into datasource. * * @private * @param {Array} inputArray An array of the data to populate. * @param {Array} sourceInputArray An array of the source data to populate. * @param {Array} [selection] The selection which indicates from what position the data will be populated. * @returns {Array} Range coordinates after populate data. */ populateValues(inputArray, sourceInputArray) { let selection = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.hot.getSelectedRangeActive(); if (!inputArray.length) { return [null, null, null, null]; } const populatedRowsLength = inputArray.length; const populatedColumnsLength = inputArray[0].length; const newRows = []; const { row: startRow, col: startColumn } = selection.getTopStartCorner(); const { row: endRowFromSelection, col: endColumnFromSelection } = selection.getBottomEndCorner(); let visualRowForPopulatedData = startRow; let visualColumnForPopulatedData = startColumn; let lastVisualRow = startRow; let lastVisualColumn = startColumn; // We try to populate just all copied data or repeat copied data within a selection. Please keep in mind that we // don't know whether populated data is bigger than selection on start as there are some cells for which values // should be not inserted (it's known right after getting cell meta). while (newRows.length < populatedRowsLength || visualRowForPopulatedData <= endRowFromSelection) { const { skipRowOnPaste, visualRow } = this.hot.getCellMeta(visualRowForPopulatedData, startColumn); visualRowForPopulatedData = visualRow + 1; if (skipRowOnPaste === true) { /* eslint-disable no-continue */ continue; } lastVisualRow = visualRow; visualColumnForPopulatedData = startColumn; const newRow = []; const insertedRow = newRows.length % populatedRowsLength; while (newRow.length < populatedColumnsLength || visualColumnForPopulatedData <= endColumnFromSelection) { var _sourceInputArray$ins; const { skipColumnOnPaste, visualCol } = this.hot.getCellMeta(visualRow, visualColumnForPopulatedData); const sourceDataAtTarget = this.hot.getSourceDataAtCell(visualRow, visualColumnForPopulatedData); const insertedColumn = newRow.length % populatedColumnsLength; visualColumnForPopulatedData = visualCol + 1; if (skipColumnOnPaste === true) { /* eslint-disable no-continue */ continue; } lastVisualColumn = visualCol; const sourceCellValue = sourceInputArray === null || sourceInputArray === void 0 || (_sourceInputArray$ins = sourceInputArray[insertedRow]) === null || _sourceInputArray$ins === void 0 ? void 0 : _sourceInputArray$ins[insertedColumn]; let cellValue = inputArray[insertedRow][insertedColumn]; if (sourceInputArray && isJSON(sourceCellValue)) { const parsedCellValue = JSON.parse(sourceCellValue); if (isObject(sourceDataAtTarget) || sourceDataAtTarget === null) { cellValue = parsedCellValue; } } newRow.push(cellValue); } newRows.push(newRow); } _classPrivateFieldSet(_preventViewportScrollOnPaste, this, true); this.hot.populateFromArray(startRow, startColumn, newRows, undefined, undefined, 'CopyPaste.paste', this.pasteMode); return [startRow, startColumn, lastVisualRow, lastVisualColumn]; } /** * Add the `contenteditable` attribute to the highlighted cell and select its content. */ /** * `copy` event callback on textarea element. * * @param {Event} event ClipboardEvent. * @private */ onCopy(event) { const eventTarget = event.composedPath()[0]; const focusedElement = this.hot.getFocusManager().getRefocusElement(); const isHotInput = eventTarget === null || eventTarget === void 0 ? void 0 : eventTarget.hasAttribute('data-hot-input'); if (!this.hot.isListening() && !_classPrivateFieldGet(_isTriggeredByCopy, this) || this.isEditorOpened() || isHTMLElement(eventTarget) && (isHotInput && eventTarget !== focusedElement || !isHotInput && eventTarget !== this.hot.rootDocument.body && !isInternalElement(eventTarget, this.hot.rootElement))) { return; } event.preventDefault(); this.setCopyableText(); _classPrivateFieldSet(_isTriggeredByCopy, this, false); const rangedData = this.getRangedData(this.copyableRanges); const rangedSourceData = this.getRangedData(this.copyableRanges, true); const copiedHeadersCount = _assertClassBrand(_CopyPaste_brand, this, _countCopiedHeaders).call(this, this.copyableRanges); const allowCopying = !!this.hot.runHooks('beforeCopy', rangedData, this.copyableRanges, copiedHeadersCount); if (allowCopying) { _assertClassBrand(_CopyPaste_brand, this, _setClipboardData).call(this, event, rangedData, rangedSourceData); this.hot.runHooks('afterCopy', rangedData, this.copyableRanges, copiedHeadersCount); } _classPrivateFieldSet(_copyMode, this, 'cells-only'); } /** * `cut` event callback on textarea element. * * @param {Event} event ClipboardEvent. * @private */ onCut(event) { const eventTarget = event.composedPath()[0]; const focusedElement = this.hot.getFocusManager().getRefocusElement(); const isHotInput = eventTarget === null || eventTarget === void 0 ? void 0 : eventTarget.hasAttribute('data-hot-input'); if (!this.hot.isListening() && !_classPrivateFieldGet(_isTriggeredByCut, this) || this.isEditorOpened() || isHTMLElement(eventTarget) && (isHotInput && eventTarget !== focusedElement || !isHotInput && eventTarget !== this.hot.rootDocument.body && !isInternalElement(eventTarget, this.hot.rootElement))) { return; } event.preventDefault(); this.setCopyableText(); _classPrivateFieldSet(_isTriggeredByCut, this, false); const rangedData = this.getRangedData(this.copyableRanges); const rangedSourceData = this.getRangedData(this.copyableRanges, true); const allowCuttingOut = !!this.hot.runHooks('beforeCut', rangedData, this.copyableRanges); if (allowCuttingOut) { _assertClassBrand(_CopyPaste_brand, this, _setClipboardData).call(this, event, rangedData, rangedSourceData); this.hot.emptySelectedCells('CopyPaste.cut'); this.hot.runHooks('afterCut', rangedData, this.copyableRanges); } } /** * `paste` event callback on textarea element. * * @param {Event} event ClipboardEvent or pseudo ClipboardEvent, if paste was called manually. * @private */ onPaste(event) { const eventTarget = event.composedPath()[0]; const focusedElement = this.hot.getFocusManager().getRefocusElement(); const isHotInput = eventTarget === null || eventTarget === void 0 ? void 0 : eventTarget.hasAttribute('data-hot-input'); if (!this.hot.isListening() || this.isEditorOpened() || !this.hot.getSelected() || isHTMLElement(eventTarget) && (isHotInput && eventTarget !== focusedElement || !isHotInput && eventTarget !== this.hot.rootDocument.body && !isInternalElement(eventTarget, this.hot.rootElement))) { return; } event.preventDefault(); let pastedData; let pastedSourceData; if (event && typeof event.clipboardData !== 'undefined') { const sourceDataHTML = event.clipboardData.getData(SOURCE_DATA_HTML_MIME_TYPE); if (sourceDataHTML) { const parsedSourceConfig = htmlToGridSettings(sourceDataHTML, this.hot.rootDocument); pastedSourceData = parsedSourceConfig.data; } const textHTML = sanitize(event.clipboardData.getData('text/html'), { ADD_TAGS: ['meta'], ADD_ATTR: ['content'], FORCE_BODY: true }); if (textHTML && /(<table)|(<TABLE)/g.test(textHTML)) { const parsedConfig = htmlToGridSettings(textHTML, this.hot.rootDocument); pastedData = parsedConfig.data; } else { pastedData = event.clipboardData.getData('text/plain'); } } else if (typeof ClipboardEvent === 'undefined' && typeof this.hot.rootWindow.clipboardData !== 'undefined') { pastedData = this.hot.rootWindow.clipboardData.getData('Text'); } if (typeof pastedData === 'string') { pastedData = parse(pastedData); } if (pastedData === void 0 || pastedData && pastedData.length === 0) { return; } if (this.hot.runHooks('beforePaste', pastedData, this.copyableRanges) === false) { return; } const [startRow, startColumn, endRow, endColumn] = this.populateValues(pastedData, pastedSourceData); if (startRow !== null && startColumn !== null) { this.hot.selectCell(startRow, startColumn, Math.min(this.hot.countRows() - 1, endRow), Math.min(this.hot.countCols() - 1, endColumn)); } this.hot.runHooks('afterPaste', pastedData, this.copyableRanges); } /** * Sets the clipboard data. * * @param {ClipboardEvent} event The Clipboard event. * @param {Array} rangedData Ranged data to set to the clipboard. * @param {Array} rangedSourceData Ranged source data to set to the clipboard. */ /** * Destroys the `CopyPaste` plugin instance. */ destroy() { super.destroy(); } } function _ensureClipboardEventsGetTriggered(eventName) { // Without this workaround Safari (tested on Safari@16.5.2) does not trigger the 'copy' event. if (isSafari()) { const activeSelectedRange = this.hot.getSelectedRangeActive(); if (activeSelectedRange) { const { row: highlightRow, col: highlightColumn } = activeSelectedRange.highlight; const currentlySelectedCell = this.hot.getCell(highlightRow, highlightColumn, true); if (currentlySelectedCell) { runWithSelectedContendEditableElement(currentlySelectedCell, () => { this.hot.rootDocument.execCommand(eventName); }); } } } else { this.hot.rootDocument.execCommand(eventName); } } /** * Counts how many column headers will be copied based on the passed range. * * @private * @param {Array<{startRow: number, startCol: number, endRow: number, endCol: number}>} ranges Array of objects with properties `startRow`, `startCol`, `endRow` and `endCol`. * @returns {{ columnHeadersCount: number }} Returns an object with keys that holds * information with the number of copied headers. */ function _countCopiedHeaders(ranges) { const { rows } = normalizeRanges(ranges); let columnHeadersCount = 0; for (let row = 0; row < rows.length; row++) { if (rows[row] >= 0) { break; } columnHeadersCount += 1; } return { columnHeadersCount }; } function _addContentEditableToHighlightedCell() { if (this.hot.isListening()) { const activeSelectedRange = this.hot.getSelectedRangeActive(); if (activeSelectedRange) { const { row: highlightRow, col: highlightColumn } = activeSelectedRange.highlight; const currentlySelectedCell = this.hot.getCell(highlightRow, highlightColumn, true); if (currentlySelectedCell) { makeElementContentEditableAndSelectItsContent(currentlySelectedCell); } } } } /** * Remove the `contenteditable` attribute from the highlighted cell and deselect its content. */ function _removeContentEditableFromHighlightedCell() { // If the instance is not listening, the workaround is not needed. if (this.hot.isListening()) { const activeSelectedRange = this.hot.getSelectedRangeActive(); if (activeSelectedRange) { const { row: highlightRow, col: highlightColumn } = activeSelectedRange.highlight; const currentlySelectedCell = this.hot.getCell(highlightRow, highlightColumn, true); if (currentlySelectedCell !== null && currentlySelectedCell !== void 0 && currentlySelectedCell.hasAttribute('contenteditable')) { removeContentEditableFromElementAndDeselect(currentlySelectedCell); } } } } function _setClipboardData(event, rangedData, rangedSourceData) { const textPlain = stringify(rangedData); if (event && event.clipboardData) { const textHTML = _dataToHTML(rangedData); const textSourceDataHTML = _dataToHTML(rangedSourceData); event.clipboardData.setData('text/plain', textPlain); event.clipboardData.setData('text/html', [META_HEAD, textHTML].join('')); event.clipboardData.setData(SOURCE_DATA_HTML_MIME_TYPE, [META_HEAD, textSourceDataHTML].join('')); } else if (typeof ClipboardEvent === 'undefined') { this.hot.rootWindow.clipboardData.setData('Text', textPlain); } } /** * Add copy and cut options to the Context Menu. * * @param {object} options Contains default added options of the Context Menu. */ function _onAfterContextMenuDefaultOptions(options) { options.items.push({ name: '---------' }, copyItem(this)); if (_classPrivateFieldGet(_enableCopyColumnHeaders, this)) { options.items.push(copyWithColumnHeadersItem(this)); } if (_classPrivateFieldGet(_enableCopyColumnGroupHeaders, this)) { options.items.push(copyWithColumnGroupHeadersItem(this)); } if (_classPrivateFieldGet(_enableCopyColumnHeadersOnly, this)) { options.items.push(copyColumnHeadersOnlyItem(this)); } options.items.push(cutItem(this)); } /** * Disables the viewport scroll after pasting the data. * * @param {number} fromRow Selection start row visual index. * @param {number} fromColumn Selection start column visual index. * @param {number} toRow Selection end row visual index. * @param {number} toColumn Selection end column visual index. * @param {object} preventScrolling Object with `value` property. If `true`, the viewport scroll will be prevented. */ function _onAfterSelection(fromRow, fromColumn, toRow, toColumn, preventScrolling) { if (_classPrivateFieldGet(_preventViewportScrollOnPaste, this)) { preventScrolling.value = true; } _classPrivateFieldSet(_preventViewportScrollOnPaste, this, false); } /** * Force focus on focusableElement after end of the selection. */ function _onAfterSelectionEnd() { if (this.isEditorOpened()) { return; } if (this.hot.getSettings().fragmentSelection) { return; } this.setCopyableText(); } /** * `document.body` `mouseenter` callback used to work around a Safari's problem with copying/cutting from the * browser's menu. */ function _onSafariMouseEnter() { _assertClassBrand(_CopyPaste_brand, this, _removeContentEditableFromHighlightedCell).call(this); } /** * `document.body` `mouseleave` callback used to work around a Safari's problem with copying/cutting from the * browser's menu. */ function _onSafariMouseLeave() { _assertClassBrand(_CopyPaste_brand, this, _addContentEditableToHighlightedCell).call(this); } /** * `afterSelection` hook callback triggered only on Safari. */ function _onSafariAfterSelection() { _assertClassBrand(_CopyPaste_brand, this, _removeContentEditableFromHighlightedCell).call(this); }