UNPKG

handsontable

Version:

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

1,160 lines (1,119 loc) • 64.8 kB
"use strict"; exports.__esModule = true; require("core-js/modules/es.error.cause.js"); require("core-js/modules/es.array.push.js"); require("core-js/modules/es.set.difference.v2.js"); require("core-js/modules/es.set.intersection.v2.js"); require("core-js/modules/es.set.is-disjoint-from.v2.js"); require("core-js/modules/es.set.is-subset-of.v2.js"); require("core-js/modules/es.set.is-superset-of.v2.js"); require("core-js/modules/es.set.symmetric-difference.v2.js"); require("core-js/modules/es.set.union.v2.js"); require("core-js/modules/esnext.iterator.constructor.js"); require("core-js/modules/esnext.iterator.filter.js"); require("core-js/modules/esnext.iterator.for-each.js"); require("core-js/modules/esnext.iterator.map.js"); require("core-js/modules/esnext.iterator.reduce.js"); var _base = require("../base"); var _hooks = require("../../core/hooks"); var _cellsCollection = _interopRequireDefault(require("./cellsCollection")); var _cellCoords = _interopRequireDefault(require("./cellCoords")); var _autofill = _interopRequireDefault(require("./calculations/autofill")); var _selection = _interopRequireDefault(require("./calculations/selection")); var _toggleMerge = _interopRequireDefault(require("./contextMenuItem/toggleMerge")); var _array = require("../../helpers/array"); var _object = require("../../helpers/object"); var _console = require("../../helpers/console"); var _number = require("../../helpers/number"); var _element = require("../../helpers/dom/element"); var _browser = require("../../helpers/browser"); var _focusOrder2 = require("./focusOrder"); var _renderer = require("./renderer"); var _utils = require("./utils"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } 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 _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } 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"); } _hooks.Hooks.getSingleton().register('beforeMergeCells'); _hooks.Hooks.getSingleton().register('afterMergeCells'); _hooks.Hooks.getSingleton().register('beforeUnmergeCells'); _hooks.Hooks.getSingleton().register('afterUnmergeCells'); const PLUGIN_KEY = exports.PLUGIN_KEY = 'mergeCells'; const PLUGIN_PRIORITY = exports.PLUGIN_PRIORITY = 150; const SHORTCUTS_GROUP = PLUGIN_KEY; /* eslint-disable jsdoc/require-description-complete-sentence */ /** * @plugin MergeCells * @class MergeCells * * @description * Plugin, which allows merging cells in the table (using the initial configuration, API or context menu). * * @example * * ::: only-for javascript * ```js * const hot = new Handsontable(document.getElementById('example'), { * data: getData(), * mergeCells: [ * {row: 0, col: 3, rowspan: 3, colspan: 3}, * {row: 2, col: 6, rowspan: 2, colspan: 2}, * {row: 4, col: 8, rowspan: 3, colspan: 3} * ], * ``` * ::: * * ::: only-for react * ```jsx * <HotTable * data={getData()} * // enable plugin * mergeCells={[ * {row: 0, col: 3, rowspan: 3, colspan: 3}, * {row: 2, col: 6, rowspan: 2, colspan: 2}, * {row: 4, col: 8, rowspan: 3, colspan: 3} * ]} * /> * ``` * ::: * * ::: only-for angular * ```ts * settings = { * data: getData(), * // Enable plugin * mergeCells: [ * { row: 0, col: 3, rowspan: 3, colspan: 3 }, * { row: 2, col: 6, rowspan: 2, colspan: 2 }, * { row: 4, col: 8, rowspan: 3, colspan: 3 }, * ], * }; * ``` * * ```html * <hot-table [settings]="settings"></hot-table> * ``` * ::: */ var _lastSelectedFocus = /*#__PURE__*/new WeakMap(); var _lastFocusDelta = /*#__PURE__*/new WeakMap(); var _focusOrder = /*#__PURE__*/new WeakMap(); var _cellRenderer = /*#__PURE__*/new WeakMap(); var _MergeCells_brand = /*#__PURE__*/new WeakSet(); class MergeCells extends _base.BasePlugin { constructor() { super(...arguments); /** * `afterInit` hook callback. */ _classPrivateMethodInitSpec(this, _MergeCells_brand); /** * A container for all the merged cells. * * @private * @type {MergedCellsCollection} */ _defineProperty(this, "mergedCellsCollection", null); /** * Instance of the class responsible for all the autofill-related calculations. * * @private * @type {AutofillCalculations} */ _defineProperty(this, "autofillCalculations", null); /** * Instance of the class responsible for the selection-related calculations. * * @private * @type {SelectionCalculations} */ _defineProperty(this, "selectionCalculations", null); /** * The holder for the last selected focus coordinates. This allows keeping the correct coordinates in cases after the * focus is moved out of the merged cell. * * @type {CellCoords} */ _classPrivateFieldInitSpec(this, _lastSelectedFocus, null); /** * The last used transformation delta. * * @type {{ row: number, col: number }} */ _classPrivateFieldInitSpec(this, _lastFocusDelta, { row: 0, col: 0 }); /** * The module responsible for providing the correct focus order (vertical and horizontal) within a selection that * contains merged cells. * * @type {FocusOrder} */ _classPrivateFieldInitSpec(this, _focusOrder, new _focusOrder2.FocusOrder({ mergedCellsGetter: (row, column) => this.mergedCellsCollection.get(row, column), rowIndexMapper: this.hot.rowIndexMapper, columnIndexMapper: this.hot.columnIndexMapper })); /** * The cell renderer responsible for rendering the merged cells. * * @type {{before: Function, after: Function}} */ _classPrivateFieldInitSpec(this, _cellRenderer, (0, _renderer.createMergeCellRenderer)(this)); } static get PLUGIN_KEY() { return PLUGIN_KEY; } static get PLUGIN_PRIORITY() { return PLUGIN_PRIORITY; } static get DEFAULT_SETTINGS() { return { [_base.defaultMainSettingSymbol]: 'cells', virtualized: false, cells: [] }; } /** * Checks if the plugin is enabled in the handsontable settings. This method is executed in {@link Hooks#beforeInit} * hook and if it returns `true` then the {@link MergeCells#enablePlugin} method is called. * * @returns {boolean} */ isEnabled() { return !!this.hot.getSettings()[PLUGIN_KEY]; } /** * Enables the plugin functionality for this Handsontable instance. */ enablePlugin() { var _this = this; if (this.enabled) { return; } this.mergedCellsCollection = new _cellsCollection.default(this); this.autofillCalculations = new _autofill.default(this); this.selectionCalculations = new _selection.default(this); this.addHook('afterInit', function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterInit).call(_this, ...args); }); this.addHook('modifyTransformFocus', function () { for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } return _assertClassBrand(_MergeCells_brand, _this, _onModifyTransformFocus).call(_this, ...args); }); this.addHook('modifyTransformStart', function () { for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { args[_key3] = arguments[_key3]; } return _assertClassBrand(_MergeCells_brand, _this, _onModifyTransformStart).call(_this, ...args); }); this.addHook('modifyTransformEnd', function () { for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { args[_key4] = arguments[_key4]; } return _assertClassBrand(_MergeCells_brand, _this, _onModifyTransformEnd).call(_this, ...args); }); this.addHook('beforeSelectionHighlightSet', function () { for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { args[_key5] = arguments[_key5]; } return _assertClassBrand(_MergeCells_brand, _this, _onBeforeSelectionHighlightSet).call(_this, ...args); }); this.addHook('beforeSetRangeStart', function () { for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) { args[_key6] = arguments[_key6]; } return _assertClassBrand(_MergeCells_brand, _this, _onBeforeSetRangeStart).call(_this, ...args); }); this.addHook('beforeSetRangeStartOnly', function () { for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) { args[_key7] = arguments[_key7]; } return _assertClassBrand(_MergeCells_brand, _this, _onBeforeSetRangeStart).call(_this, ...args); }); this.addHook('beforeSelectionFocusSet', function () { for (var _len8 = arguments.length, args = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) { args[_key8] = arguments[_key8]; } return _assertClassBrand(_MergeCells_brand, _this, _onBeforeSelectionFocusSet).call(_this, ...args); }); this.addHook('afterSelectionFocusSet', function () { for (var _len9 = arguments.length, args = new Array(_len9), _key9 = 0; _key9 < _len9; _key9++) { args[_key9] = arguments[_key9]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterSelectionFocusSet).call(_this, ...args); }); this.addHook('afterSelectionEnd', function () { for (var _len0 = arguments.length, args = new Array(_len0), _key0 = 0; _key0 < _len0; _key0++) { args[_key0] = arguments[_key0]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterSelectionEnd).call(_this, ...args); }); this.addHook('modifyGetCellCoords', function () { for (var _len1 = arguments.length, args = new Array(_len1), _key1 = 0; _key1 < _len1; _key1++) { args[_key1] = arguments[_key1]; } return _assertClassBrand(_MergeCells_brand, _this, _onModifyGetCellCoords).call(_this, ...args); }); this.addHook('modifyGetCoordsElement', function () { for (var _len10 = arguments.length, args = new Array(_len10), _key10 = 0; _key10 < _len10; _key10++) { args[_key10] = arguments[_key10]; } return _assertClassBrand(_MergeCells_brand, _this, _onModifyGetCellCoords).call(_this, ...args); }); this.addHook('afterIsMultipleSelection', function () { for (var _len11 = arguments.length, args = new Array(_len11), _key11 = 0; _key11 < _len11; _key11++) { args[_key11] = arguments[_key11]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterIsMultipleSelection).call(_this, ...args); }); this.addHook('afterRenderer', function () { return _classPrivateFieldGet(_cellRenderer, _this).after(...arguments); }); this.addHook('afterContextMenuDefaultOptions', function () { for (var _len12 = arguments.length, args = new Array(_len12), _key12 = 0; _key12 < _len12; _key12++) { args[_key12] = arguments[_key12]; } return _assertClassBrand(_MergeCells_brand, _this, _addMergeActionsToContextMenu).call(_this, ...args); }); this.addHook('afterGetCellMeta', function () { for (var _len13 = arguments.length, args = new Array(_len13), _key13 = 0; _key13 < _len13; _key13++) { args[_key13] = arguments[_key13]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterGetCellMeta).call(_this, ...args); }); this.addHook('afterViewportRowCalculatorOverride', function () { for (var _len14 = arguments.length, args = new Array(_len14), _key14 = 0; _key14 < _len14; _key14++) { args[_key14] = arguments[_key14]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterViewportRowCalculatorOverride).call(_this, ...args); }); this.addHook('afterViewportColumnCalculatorOverride', function () { for (var _len15 = arguments.length, args = new Array(_len15), _key15 = 0; _key15 < _len15; _key15++) { args[_key15] = arguments[_key15]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterViewportColumnCalculatorOverride).call(_this, ...args); }); this.addHook('modifyAutofillRange', function () { for (var _len16 = arguments.length, args = new Array(_len16), _key16 = 0; _key16 < _len16; _key16++) { args[_key16] = arguments[_key16]; } return _assertClassBrand(_MergeCells_brand, _this, _onModifyAutofillRange).call(_this, ...args); }); this.addHook('afterCreateCol', function () { for (var _len17 = arguments.length, args = new Array(_len17), _key17 = 0; _key17 < _len17; _key17++) { args[_key17] = arguments[_key17]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterCreateCol).call(_this, ...args); }); this.addHook('afterRemoveCol', function () { for (var _len18 = arguments.length, args = new Array(_len18), _key18 = 0; _key18 < _len18; _key18++) { args[_key18] = arguments[_key18]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterRemoveCol).call(_this, ...args); }); this.addHook('afterCreateRow', function () { for (var _len19 = arguments.length, args = new Array(_len19), _key19 = 0; _key19 < _len19; _key19++) { args[_key19] = arguments[_key19]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterCreateRow).call(_this, ...args); }); this.addHook('afterRemoveRow', function () { for (var _len20 = arguments.length, args = new Array(_len20), _key20 = 0; _key20 < _len20; _key20++) { args[_key20] = arguments[_key20]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterRemoveRow).call(_this, ...args); }); this.addHook('afterChange', function () { for (var _len21 = arguments.length, args = new Array(_len21), _key21 = 0; _key21 < _len21; _key21++) { args[_key21] = arguments[_key21]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterChange).call(_this, ...args); }); this.addHook('beforeDrawBorders', function () { for (var _len22 = arguments.length, args = new Array(_len22), _key22 = 0; _key22 < _len22; _key22++) { args[_key22] = arguments[_key22]; } return _assertClassBrand(_MergeCells_brand, _this, _onBeforeDrawAreaBorders).call(_this, ...args); }); this.addHook('afterDrawSelection', function () { for (var _len23 = arguments.length, args = new Array(_len23), _key23 = 0; _key23 < _len23; _key23++) { args[_key23] = arguments[_key23]; } return _assertClassBrand(_MergeCells_brand, _this, _onAfterDrawSelection).call(_this, ...args); }); this.addHook('beforeRemoveCellClassNames', function () { for (var _len24 = arguments.length, args = new Array(_len24), _key24 = 0; _key24 < _len24; _key24++) { args[_key24] = arguments[_key24]; } return _assertClassBrand(_MergeCells_brand, _this, _onBeforeRemoveCellClassNames).call(_this, ...args); }); this.addHook('beforeBeginEditing', function () { for (var _len25 = arguments.length, args = new Array(_len25), _key25 = 0; _key25 < _len25; _key25++) { args[_key25] = arguments[_key25]; } return _assertClassBrand(_MergeCells_brand, _this, _onBeforeBeginEditing).call(_this, ...args); }); this.addHook('modifyRowHeightByOverlayName', function () { for (var _len26 = arguments.length, args = new Array(_len26), _key26 = 0; _key26 < _len26; _key26++) { args[_key26] = arguments[_key26]; } return _assertClassBrand(_MergeCells_brand, _this, _onModifyRowHeightByOverlayName).call(_this, ...args); }); this.addHook('beforeUndoStackChange', (action, source) => { if (source === 'MergeCells') { return false; } }); this.registerShortcuts(); super.enablePlugin(); } /** * Disables the plugin functionality for this Handsontable instance. */ disablePlugin() { this.clearCollections(); this.unregisterShortcuts(); this.hot.render(); super.disablePlugin(); } /** * Updates the plugin's state. * * This method is executed when [`updateSettings()`](@/api/core.md#updatesettings) is invoked with any of the * following configuration options: * - [`mergeCells`](@/api/options.md#mergecells) */ updatePlugin() { this.disablePlugin(); this.enablePlugin(); this.generateFromSettings(); super.updatePlugin(); } /** * If the browser is recognized as Chrome, force an additional repaint to prevent showing the effects of a Chrome bug. * * Issue described in https://github.com/handsontable/dev-handsontable/issues/521. * * @private */ ifChromeForceRepaint() { if (!(0, _browser.isChrome)()) { return; } const rowsToRefresh = []; let rowIndexesToRefresh = []; this.mergedCellsCollection.mergedCells.forEach(mergedCell => { const { row, rowspan } = mergedCell; for (let r = row + 1; r < row + rowspan; r++) { rowIndexesToRefresh.push(r); } }); // Remove duplicates rowIndexesToRefresh = [...new Set(rowIndexesToRefresh)]; rowIndexesToRefresh.forEach(rowIndex => { const renderableRowIndex = this.hot.rowIndexMapper.getRenderableFromVisualIndex(rowIndex); this.hot.view._wt.wtOverlays.getOverlays(true).map(overlay => (overlay === null || overlay === void 0 ? void 0 : overlay.name) === 'master' ? overlay : overlay.clone.wtTable).forEach(wtTableRef => { const rowToRefresh = wtTableRef.getRow(renderableRowIndex); if (rowToRefresh) { // Modify the TR's `background` property to later modify it asynchronously. // The background color is getting modified only with the alpha, so the change should not be visible (and is // covered by the TDs' background color). rowToRefresh.style.background = (0, _element.getStyle)(rowToRefresh, 'backgroundColor').replace(')', ', 0.99)'); rowsToRefresh.push(rowToRefresh); } }); }); // Asynchronously revert the TRs' `background` property to force a fresh repaint. this.hot._registerTimeout(() => { rowsToRefresh.forEach(rowElement => { var _getStyle; rowElement.style.background = (_getStyle = (0, _element.getStyle)(rowElement, 'backgroundColor')) === null || _getStyle === void 0 ? void 0 : _getStyle.replace(', 0.99)', ')'); }); }, 1); } /** * Validates a single setting object, represented by a single merged cell information object. * * @private * @param {object} setting An object with `row`, `col`, `rowspan` and `colspan` properties. * @returns {boolean} */ validateSetting(setting) { if (!setting) { return false; } if (_cellCoords.default.containsNegativeValues(setting)) { (0, _console.warn)(_cellCoords.default.NEGATIVE_VALUES_WARNING(setting)); return false; } if (_cellCoords.default.isOutOfBounds(setting, this.hot.countRows(), this.hot.countCols())) { (0, _console.warn)(_cellCoords.default.IS_OUT_OF_BOUNDS_WARNING(setting)); return false; } if (_cellCoords.default.isSingleCell(setting)) { (0, _console.warn)(_cellCoords.default.IS_SINGLE_CELL(setting)); return false; } if (_cellCoords.default.containsZeroSpan(setting)) { (0, _console.warn)(_cellCoords.default.ZERO_SPAN_WARNING(setting)); return false; } return true; } /** * Generates the merged cells from the settings provided to the plugin. * * @private */ generateFromSettings() { const validSettings = this.getSetting('cells').filter(mergeCellInfo => this.validateSetting(mergeCellInfo)); const nonOverlappingSettings = this.mergedCellsCollection.filterOverlappingMergeCells(validSettings); const populatedNulls = []; nonOverlappingSettings.forEach(mergeCellInfo => { const { row, col, rowspan, colspan } = mergeCellInfo; const from = this.hot._createCellCoords(row, col); const to = this.hot._createCellCoords(row + rowspan - 1, col + colspan - 1); const mergeRange = this.hot._createCellRange(from, from, to); // Merging without data population. this.mergeRange(mergeRange, true, true); for (let r = row; r < row + rowspan; r++) { for (let c = col; c < col + colspan; c++) { // Not resetting a cell representing a merge area's value. if (r !== row || c !== col) { populatedNulls.push([r, c, null]); } } } }); // There are no merged cells. Thus, no data population is needed. if (populatedNulls.length === 0) { return; } // TODO: Change the `source` argument to a more meaningful value, e.g. `${this.pluginName}.clearCells`. this.hot.setDataAtCell(populatedNulls, undefined, undefined, this.pluginName); } /** * Clears the merged cells from the merged cell container. */ clearCollections() { this.mergedCellsCollection.clear(); } /** * Returns `true` if a range is mergeable. * * @private * @param {object} newMergedCellInfo Merged cell information object to test. * @param {boolean} [auto=false] `true` if triggered at initialization. * @returns {boolean} */ canMergeRange(newMergedCellInfo) { let auto = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; return auto ? true : this.validateSetting(newMergedCellInfo); } /** * Merges the selection provided as a cell range. * * @param {CellRange} [cellRange] Selection cell range. */ mergeSelection() { let cellRange = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.hot.getSelectedRangeActive(); if (!cellRange) { return; } cellRange.setDirection(this.hot.isRtl() ? 'NE-SW' : 'NW-SE'); const { from, to } = cellRange; this.unmergeRange(cellRange, true); this.mergeRange(cellRange); this.hot.selectCell(from.row, from.col, to.row, to.col, false); } /** * Unmerges the selection provided as a cell range. * * @param {CellRange} [cellRange] Selection cell range. */ unmergeSelection() { let cellRange = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.hot.getSelectedRangeActive(); if (!cellRange) { return; } const { from, to } = cellRange; this.unmergeRange(cellRange, true); this.hot.selectCell(from.row, from.col, to.row, to.col, false); } /** * Merges cells in the provided cell range. * * @private * @param {CellRange} cellRange Cell range to merge. * @param {boolean} [auto=false] `true` if is called automatically, e.g. At initialization. * @param {boolean} [preventPopulation=false] `true`, if the method should not run `populateFromArray` at the end, * but rather return its arguments. * @returns {Array|boolean} Returns an array of [row, column, dataUnderCollection] if preventPopulation is set to * true. If the the merging process went successful, it returns `true`, otherwise - `false`. * @fires Hooks#beforeMergeCells * @fires Hooks#afterMergeCells */ mergeRange(cellRange) { let auto = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; let preventPopulation = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; const topStart = cellRange.getTopStartCorner(); const bottomEnd = cellRange.getBottomEndCorner(); const mergeParent = { row: topStart.row, col: topStart.col, rowspan: bottomEnd.row - topStart.row + 1, colspan: bottomEnd.col - topStart.col + 1 }; const clearedData = []; let populationInfo = null; if (!this.canMergeRange(mergeParent, auto)) { return false; } this.hot.runHooks('beforeMergeCells', cellRange, auto); (0, _number.rangeEach)(0, mergeParent.rowspan - 1, i => { (0, _number.rangeEach)(0, mergeParent.colspan - 1, j => { let clearedValue = null; if (!clearedData[i]) { clearedData[i] = []; } if (i === 0 && j === 0) { clearedValue = this.hot.getSourceDataAtCell(this.hot.toPhysicalRow(mergeParent.row), this.hot.toPhysicalColumn(mergeParent.col)); } else { this.hot.setCellMeta(mergeParent.row + i, mergeParent.col + j, 'hidden', true); } clearedData[i][j] = clearedValue; }); }); this.hot.setCellMeta(mergeParent.row, mergeParent.col, 'spanned', true); const mergedCellAdded = this.mergedCellsCollection.add(mergeParent, auto); if (mergedCellAdded) { if (preventPopulation) { populationInfo = [mergeParent.row, mergeParent.col, clearedData]; } else { // TODO: Change the `source` argument to a more meaningful value, e.g. `${this.pluginName}.clearCells`. this.hot.populateFromArray(mergeParent.row, mergeParent.col, clearedData, undefined, undefined, this.pluginName); } if (!auto) { this.ifChromeForceRepaint(); } this.hot.runHooks('afterMergeCells', cellRange, mergeParent, auto); return populationInfo; } return true; } /** * Unmerges the selection provided as a cell range. If no cell range is provided, it uses the current selection. * * @private * @param {CellRange} cellRange Selection cell range. * @param {boolean} [auto=false] `true` if called automatically by the plugin. * * @fires Hooks#beforeUnmergeCells * @fires Hooks#afterUnmergeCells */ unmergeRange(cellRange) { let auto = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; const mergedCells = this.mergedCellsCollection.getWithinRange(cellRange); if (mergedCells.length === 0) { return; } this.hot.runHooks('beforeUnmergeCells', cellRange, auto); (0, _array.arrayEach)(mergedCells, currentCollection => { this.mergedCellsCollection.remove(currentCollection.row, currentCollection.col); (0, _number.rangeEach)(0, currentCollection.rowspan - 1, i => { (0, _number.rangeEach)(0, currentCollection.colspan - 1, j => { this.hot.removeCellMeta(currentCollection.row + i, currentCollection.col + j, 'hidden'); this.hot.removeCellMeta(currentCollection.row + i, currentCollection.col + j, 'copyable'); }); }); this.hot.removeCellMeta(currentCollection.row, currentCollection.col, 'spanned'); }); this.hot.runHooks('afterUnmergeCells', cellRange, auto); this.hot.render(); } /** * Merges or unmerges, based on the cell range provided as `cellRange`. * * @private * @param {CellRange} cellRange The cell range to merge or unmerged. */ toggleMerge(cellRange) { const { from, to } = cellRange.clone().normalize(); const mergedCell = this.mergedCellsCollection.get(from.row, from.col); const mergedCellCoversWholeRange = mergedCell.row === from.row && mergedCell.col === from.col && mergedCell.row + mergedCell.rowspan - 1 === to.row && mergedCell.col + mergedCell.colspan - 1 === to.col; if (mergedCellCoversWholeRange) { this.unmergeRange(cellRange); } else { this.mergeSelection(cellRange); } } /** * Merges the specified range. * * @param {number} startRow Start row of the merged cell. * @param {number} startColumn Start column of the merged cell. * @param {number} endRow End row of the merged cell. * @param {number} endColumn End column of the merged cell. * @fires Hooks#beforeMergeCells * @fires Hooks#afterMergeCells */ merge(startRow, startColumn, endRow, endColumn) { const start = this.hot._createCellCoords(startRow, startColumn); const end = this.hot._createCellCoords(endRow, endColumn); this.mergeRange(this.hot._createCellRange(start, start, end)); } /** * Unmerges the merged cell in the provided range. * * @param {number} startRow Start row of the merged cell. * @param {number} startColumn Start column of the merged cell. * @param {number} endRow End row of the merged cell. * @param {number} endColumn End column of the merged cell. * @fires Hooks#beforeUnmergeCells * @fires Hooks#afterUnmergeCells */ unmerge(startRow, startColumn, endRow, endColumn) { const start = this.hot._createCellCoords(startRow, startColumn); const end = this.hot._createCellCoords(endRow, endColumn); this.unmergeRange(this.hot._createCellRange(start, start, end)); } /** * Register shortcuts responsible for toggling a merge. * * @private */ registerShortcuts() { const shortcutManager = this.hot.getShortcutManager(); const gridContext = shortcutManager.getContext('grid'); gridContext.addShortcut({ keys: [['Control', 'm']], callback: () => { const range = this.hot.getSelectedRangeActive(); if (range && !range.isSingleHeader()) { this.toggleMerge(range); this.hot.render(); } }, runOnlyIf: event => !event.altKey, // right ALT in some systems triggers ALT+CTRL group: SHORTCUTS_GROUP }); } /** * Unregister shortcuts responsible for toggling a merge. * * @private */ unregisterShortcuts() { const shortcutManager = this.hot.getShortcutManager(); const gridContext = shortcutManager.getContext('grid'); gridContext.removeShortcutsByGroup(SHORTCUTS_GROUP); } /** * Modifies the information on whether the current selection contains multiple cells. The `afterIsMultipleSelection` * hook callback. * * @param {boolean} isMultiple Determines whether the current selection contains multiple cells. * @returns {boolean} */ /** * Modify viewport start when needed. We extend viewport when merged cells aren't fully visible. * * @private * @param {object} calc The row calculator object. * @param {number} nrOfColumns Number of visual columns. */ modifyViewportRowStart(calc, nrOfColumns) { const rowMapper = this.hot.rowIndexMapper; const visualStartRow = rowMapper.getVisualFromRenderableIndex(calc.startRow); for (let visualColumnIndex = 0; visualColumnIndex < nrOfColumns; visualColumnIndex += 1) { const mergeParentForViewportStart = this.mergedCellsCollection.get(visualStartRow, visualColumnIndex); if ((0, _object.isObject)(mergeParentForViewportStart)) { const renderableIndexAtMergeStart = rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(mergeParentForViewportStart.row, 1)); // Merge start is out of the viewport (i.e. when we scrolled to the bottom and we can see just part of a merge). if (renderableIndexAtMergeStart < calc.startRow) { // We extend viewport when some rows have been merged. calc.startRow = renderableIndexAtMergeStart; // We are looking for next merges inside already extended viewport (starting again from row equal to 0). this.modifyViewportRowStart(calc, nrOfColumns); // recursively search upwards return; // Finish the current loop. Everything will be checked from the beginning by above recursion. } } } } /** * Modify viewport end when needed. We extend viewport when merged cells aren't fully visible. * * @private * @param {object} calc The row calculator object. * @param {number} nrOfColumns Number of visual columns. */ modifyViewportRowEnd(calc, nrOfColumns) { const rowMapper = this.hot.rowIndexMapper; const visualEndRow = rowMapper.getVisualFromRenderableIndex(calc.endRow); for (let visualColumnIndex = 0; visualColumnIndex < nrOfColumns; visualColumnIndex += 1) { const mergeParentForViewportEnd = this.mergedCellsCollection.get(visualEndRow, visualColumnIndex); if ((0, _object.isObject)(mergeParentForViewportEnd)) { const mergeEnd = mergeParentForViewportEnd.row + mergeParentForViewportEnd.rowspan - 1; const renderableIndexAtMergeEnd = rowMapper.getRenderableFromVisualIndex(rowMapper.getNearestNotHiddenIndex(mergeEnd, -1)); // Merge end is out of the viewport. if (renderableIndexAtMergeEnd > calc.endRow) { // We extend the viewport when some rows have been merged. calc.endRow = renderableIndexAtMergeEnd; // We are looking for next merges inside already extended viewport (starting again from row equal to 0). this.modifyViewportRowEnd(calc, nrOfColumns); // recursively search upwards return; // Finish the current loop. Everything will be checked from the beginning by above recursion. } } } } /** * `afterViewportColumnCalculatorOverride` hook callback. * * @param {object} calc The column calculator object. */ /** * Modify viewport start when needed. We extend viewport when merged cells aren't fully visible. * * @private * @param {object} calc The column calculator object. * @param {number} nrOfRows Number of visual rows. */ modifyViewportColumnStart(calc, nrOfRows) { const columnMapper = this.hot.columnIndexMapper; const visualStartCol = columnMapper.getVisualFromRenderableIndex(calc.startColumn); for (let visualRowIndex = 0; visualRowIndex < nrOfRows; visualRowIndex += 1) { const mergeParentForViewportStart = this.mergedCellsCollection.get(visualRowIndex, visualStartCol); if ((0, _object.isObject)(mergeParentForViewportStart)) { const renderableIndexAtMergeStart = columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(mergeParentForViewportStart.col, 1)); // Merge start is out of the viewport (i.e. when we scrolled to the right and we can see just part of a merge). if (renderableIndexAtMergeStart < calc.startColumn) { // We extend viewport when some columns have been merged. calc.startColumn = renderableIndexAtMergeStart; // We are looking for next merges inside already extended viewport (starting again from column equal to 0). this.modifyViewportColumnStart(calc, nrOfRows); // recursively search upwards return; // Finish the current loop. Everything will be checked from the beginning by above recursion. } } } } /** * Modify viewport end when needed. We extend viewport when merged cells aren't fully visible. * * @private * @param {object} calc The column calculator object. * @param {number} nrOfRows Number of visual rows. */ modifyViewportColumnEnd(calc, nrOfRows) { const columnMapper = this.hot.columnIndexMapper; const visualEndCol = columnMapper.getVisualFromRenderableIndex(calc.endColumn); for (let visualRowIndex = 0; visualRowIndex < nrOfRows; visualRowIndex += 1) { const mergeParentForViewportEnd = this.mergedCellsCollection.get(visualRowIndex, visualEndCol); if ((0, _object.isObject)(mergeParentForViewportEnd)) { const mergeEnd = mergeParentForViewportEnd.col + mergeParentForViewportEnd.colspan - 1; const renderableIndexAtMergeEnd = columnMapper.getRenderableFromVisualIndex(columnMapper.getNearestNotHiddenIndex(mergeEnd, -1)); // Merge end is out of the viewport. if (renderableIndexAtMergeEnd > calc.endColumn) { // We extend the viewport when some columns have been merged. calc.endColumn = renderableIndexAtMergeEnd; // We are looking for next merges inside already extended viewport (starting again from column equal to 0). this.modifyViewportColumnEnd(calc, nrOfRows); // recursively search upwards return; // Finish the current loop. Everything will be checked from the beginning by above recursion. } } } } /** * Translates merged cell coordinates to renderable indexes. * * @private * @param {number} parentRow Visual row index. * @param {number} rowspan Rowspan which describes shift which will be applied to parent row * to calculate renderable index which points to the most bottom * index position. Pass rowspan as `0` to calculate the most top * index position. * @param {number} parentColumn Visual column index. * @param {number} colspan Colspan which describes shift which will be applied to parent column * to calculate renderable index which points to the most right * index position. Pass colspan as `0` to calculate the most left * index position. * @returns {number[]} */ translateMergedCellToRenderable(parentRow, rowspan, parentColumn, colspan) { const { rowIndexMapper: rowMapper, columnIndexMapper: columnMapper } = this.hot; let firstNonHiddenRow; let firstNonHiddenColumn; if (rowspan === 0) { firstNonHiddenRow = rowMapper.getNearestNotHiddenIndex(parentRow, 1); } else { firstNonHiddenRow = rowMapper.getNearestNotHiddenIndex(parentRow + rowspan - 1, -1); } if (colspan === 0) { firstNonHiddenColumn = columnMapper.getNearestNotHiddenIndex(parentColumn, 1); } else { firstNonHiddenColumn = columnMapper.getNearestNotHiddenIndex(parentColumn + colspan - 1, -1); } const renderableRow = parentRow >= 0 ? rowMapper.getRenderableFromVisualIndex(firstNonHiddenRow) : parentRow; const renderableColumn = parentColumn >= 0 ? columnMapper.getRenderableFromVisualIndex(firstNonHiddenColumn) : parentColumn; return [renderableRow, renderableColumn]; } /** * The `modifyAutofillRange` hook callback. * * @param {Array} fullArea The drag + base area coordinates. * @param {Array} baseArea The selection information. * @returns {Array} The new drag area. */ } exports.MergeCells = MergeCells; function _onAfterInit() { this.generateFromSettings(); this.hot.render(); } function _onAfterIsMultipleSelection(isMultiple) { if (isMultiple) { const mergedCells = this.mergedCellsCollection.mergedCells; const selectionRange = this.hot.getSelectedRangeActive(); const topStartCoords = selectionRange.getTopStartCorner(); const bottomEndCoords = selectionRange.getBottomEndCorner(); for (let group = 0; group < mergedCells.length; group += 1) { if (topStartCoords.row === mergedCells[group].row && topStartCoords.col === mergedCells[group].col && bottomEndCoords.row === mergedCells[group].row + mergedCells[group].rowspan - 1 && bottomEndCoords.col === mergedCells[group].col + mergedCells[group].colspan - 1) { return false; } } } return isMultiple; } /** * `modifyTransformFocus` hook callback. * * @param {object} delta The transformation delta. */ function _onModifyTransformFocus(delta) { _classPrivateFieldGet(_lastFocusDelta, this).row = delta.row; _classPrivateFieldGet(_lastFocusDelta, this).col = delta.col; } /** * `modifyTransformStart` hook callback. * * @param {object} delta The transformation delta. */ function _onModifyTransformStart(delta) { const selectedRange = this.hot.getSelectedRangeActive(); const { highlight } = selectedRange; const { columnIndexMapper, rowIndexMapper } = this.hot; if (_classPrivateFieldGet(_lastSelectedFocus, this)) { if (rowIndexMapper.getRenderableFromVisualIndex(_classPrivateFieldGet(_lastSelectedFocus, this).row) !== null) { highlight.row = _classPrivateFieldGet(_lastSelectedFocus, this).row; } if (columnIndexMapper.getRenderableFromVisualIndex(_classPrivateFieldGet(_lastSelectedFocus, this).col) !== null) { highlight.col = _classPrivateFieldGet(_lastSelectedFocus, this).col; } _classPrivateFieldSet(_lastSelectedFocus, this, null); } const mergedParent = this.mergedCellsCollection.get(highlight.row, highlight.col); if (!mergedParent) { return; } const visualColumnIndexStart = mergedParent.col; const visualColumnIndexEnd = mergedParent.col + mergedParent.colspan - 1; if (delta.col < 0) { const nextColumn = highlight.col >= visualColumnIndexStart && highlight.col <= visualColumnIndexEnd ? visualColumnIndexStart - 1 : visualColumnIndexEnd; const notHiddenColumnIndex = columnIndexMapper.getNearestNotHiddenIndex(nextColumn, -1); if (notHiddenColumnIndex === null) { // There are no visible columns anymore, so move the selection out of the table edge. This will // be processed by the selection Transformer class as a move selection to the previous row (if autoWrapRow is enabled). delta.col = -this.hot.view.countRenderableColumnsInRange(0, highlight.col); } else { delta.col = -Math.max(this.hot.view.countRenderableColumnsInRange(notHiddenColumnIndex, highlight.col) - 1, 1); } } else if (delta.col > 0) { const nextColumn = highlight.col >= visualColumnIndexStart && highlight.col <= visualColumnIndexEnd ? visualColumnIndexEnd + 1 : visualColumnIndexStart; const notHiddenColumnIndex = columnIndexMapper.getNearestNotHiddenIndex(nextColumn, 1); if (notHiddenColumnIndex === null) { // There are no visible columns anymore, so move the selection out of the table edge. This will // be processed by the selection Transformer class as a move selection to the next row (if autoWrapRow is enabled). delta.col = this.hot.view.countRenderableColumnsInRange(highlight.col, this.hot.countCols()); } else { delta.col = Math.max(this.hot.view.countRenderableColumnsInRange(highlight.col, notHiddenColumnIndex) - 1, 1); } } const visualRowIndexStart = mergedParent.row; const visualRowIndexEnd = mergedParent.row + mergedParent.rowspan - 1; if (delta.row < 0) { const nextRow = highlight.row >= visualRowIndexStart && highlight.row <= visualRowIndexEnd ? visualRowIndexStart - 1 : visualRowIndexEnd; const notHiddenRowIndex = rowIndexMapper.getNearestNotHiddenIndex(nextRow, -1); if (notHiddenRowIndex === null) { // There are no visible rows anymore, so move the selection out of the table edge. This will // be processed by the selection Transformer class as a move selection to the previous column (if autoWrapCol is enabled). delta.row = -this.hot.view.countRenderableRowsInRange(0, highlight.row); } else { delta.row = -Math.max(this.hot.view.countRenderableRowsInRange(notHiddenRowIndex, highlight.row) - 1, 1); } } else if (delta.row > 0) { const nextRow = highlight.row >= visualRowIndexStart && highlight.row <= visualRowIndexEnd ? visualRowIndexEnd + 1 : visualRowIndexStart; const notHiddenRowIndex = rowIndexMapper.getNearestNotHiddenIndex(nextRow, 1); if (notHiddenRowIndex === null) { // There are no visible rows anymore, so move the selection out of the table edge. This will // be processed by the selection Transformer class as a move selection to the next column (if autoWrapCol is enabled). delta.row = this.hot.view.countRenderableRowsInRange(highlight.row, this.hot.countRows()); } else { delta.row = Math.max(this.hot.view.countRenderableRowsInRange(highlight.row, notHiddenRowIndex) - 1, 1); } } } /** * The hook allows to modify the delta transformation object necessary for correct selection end transformations. * The logic here handles "jumping over" merged merged cells, while selecting. * * @param {{ row: number, col: number }} delta The transformation delta. */ function _onModifyTransformEnd(delta) { const selectedRange = this.hot.getSelectedRangeActive(); const cloneRange = selectedRange.clone(); const { to } = selectedRange; const { columnIndexMapper, rowIndexMapper } = this.hot; const expandCloneRange = (row, col) => { cloneRange.expand(this.hot._createCellCoords(row, col)); for (let i = 0; i < this.mergedCellsCollection.mergedCells.length; i += 1) { cloneRange.expandByRange(this.mergedCellsCollection.mergedCells[i].getRange()); } }; if (delta.col < 0) { let nextColumn = this.mergedCellsCollection.getStartMostColumnIndex(selectedRange, to.col) + delta.col; expandCloneRange(to.row, nextColumn); if (selectedRange.getHorizontalDirection() === 'E-W' && cloneRange.getHorizontalDirection() === 'E-W') { nextColumn = cloneRange.getTopStartCorner().col; } const notHiddenColumnIndex = columnIndexMapper.getNearestNotHiddenIndex(nextColumn, 1); if (notHiddenColumnIndex !== null) { delta.col = -Math.max(this.hot.view.countRenderableColumnsInRange(notHiddenColumnIndex, to.col) - 1, 1); } } else if (delta.col > 0) { let nextColumn = this.mergedCellsCollection.getEndMostColumnIndex(selectedRange, to.col) + delta.col; expandCloneRange(to.row, nextColumn); if (selectedRange.getHorizontalDirection() === 'W-E' && cloneRange.getHorizontalDirection() === 'W-E') { nextColumn = cloneRange.getBottomEndCorner().col; } const notHiddenColumnIndex = columnIndexMapper.getNearestNotHiddenIndex(nextColumn, -1); if (notHiddenColumnIndex !== null) { delta.col = Math.max(this.hot.view.countRenderableColumnsInRange(to.col, notHiddenColumnIndex) - 1, 1); } } if (delta.row < 0) { let nextRow = this.mergedCellsCollection.getTopMostRowIndex(selectedRange, to.row) + delta.row; expandCloneRange(nextRow, to.col); if (selectedRange.getVerticalDirection() === 'S-N' && cloneRange.getVerticalDirection() === 'S-N') { nextRow = cloneRange.getTopStartCorner().row; } const notHiddenRowIndex = rowIndexMapper.getNearestNotHiddenIndex(nextRow, 1); if (notHiddenRowIndex !== null) { delta.row = -Math.max(this.hot.view.countRenderableRowsInRange(notHiddenRowIndex, to.row) - 1, 1); } } else if (delta.row > 0) { let nextRow = this.mergedCellsCollection.getBottomMostRowIndex(selectedRange, to.row) + delta.row; expandCloneRange(nextRow, to.col); if (selectedRange.getVerticalDirection() === 'N-S' && cloneRange.getVerticalDirection() === 'N-S') { nextRow = cloneRange.getBottomStartCorner().row; } const notHiddenRowIndex = rowIndexMapper.getNearestNotHiddenIndex(nextRow, -1); if (notHiddenRowIndex !== null) { delta.row = Math.max(this.hot.view.countRenderableRowsInRange(to.row, notHiddenRowIndex) - 1, 1); } } } /** * The hook corrects the range (before drawing it) after the selection was made on the merged cells. * It expands the range to cover the entire area of the selected merged cells. */ function _onBeforeSelectionHighlightSet() { const selectedRange = this.hot.getSelectedRangeLast(); const { highlight } = selectedRange; if (this.hot.selection.isSelectedByColumnHeader() || this.hot.selection.isSelectedByRowHeader()) { _classPrivateFieldSet(_lastSelectedFocus, this, highlight.clone()); return; } for (let i = 0; i < this.mergedCellsCollection.mergedCells.length; i += 1) { selectedRange.expandByRange(this.mergedCellsCollection.mergedCells[i].getRange(), false); } // TODO: This is a workaround for an issue with the selection not being extended properly. // In some cases when the merge cells are defined in random order the selection is not // extended in that way that it covers all overlapped merge cells. for (let i = 0; i < this.mergedCellsCollection.mergedCells.length; i += 1) { selectedRange.expandByRange(this.mergedCellsCollection.mergedCells[i].getRange(), false); } const mergedParent = this.mergedCellsCollection.get(highlight.row, highlight.col); _classPrivateFieldSet(_lastSelectedFocus, this, highlight.clone()); if (mergedParent) { highlight.assign(mergedParent); } } /** * The `modifyGetCellCoords` hook callback allows forwarding all `getCell` calls that point in-between the merged cells * to the root element of the cell. * * @param {number} row Row index. * @param {number} column Visual column index. * @param {boolean} topmost Indicates if the requested element belongs to the topmost layer (any overlay) or not. * @param {string} [source] String that identifies how this coords change will be processed. * @returns {Array|undefined} Visual coordinates of the merge. */ function _onModifyGetCellCoords(row, column, topmost, source) { if (row < 0 || column < 0) { return; } const mergeParent = this.mergedCellsCollection.get(row, column); if (!mergeParent) { return; } const { row: mergeRow, col: mergeColumn, colspan, rowspan } = mergeParent;