1207 lines
45 KiB
JavaScript
1207 lines
45 KiB
JavaScript
import "core-js/modules/es.array.push.js";
|
|
import { BasePlugin } from "../base/index.mjs";
|
|
import Hooks from "../../pluginHooks.mjs";
|
|
import MergedCellsCollection from "./cellsCollection.mjs";
|
|
import MergedCellCoords from "./cellCoords.mjs";
|
|
import AutofillCalculations from "./calculations/autofill.mjs";
|
|
import SelectionCalculations from "./calculations/selection.mjs";
|
|
import toggleMergeItem from "./contextMenuItem/toggleMerge.mjs";
|
|
import { arrayEach } from "../../helpers/array.mjs";
|
|
import { isObject, clone } from "../../helpers/object.mjs";
|
|
import { warn } from "../../helpers/console.mjs";
|
|
import { rangeEach } from "../../helpers/number.mjs";
|
|
import { applySpanProperties } from "./utils.mjs";
|
|
import { getStyle } from "../../helpers/dom/element.mjs";
|
|
import { isChrome } from "../../helpers/browser.mjs";
|
|
Hooks.getSingleton().register('beforeMergeCells');
|
|
Hooks.getSingleton().register('afterMergeCells');
|
|
Hooks.getSingleton().register('beforeUnmergeCells');
|
|
Hooks.getSingleton().register('afterUnmergeCells');
|
|
export const PLUGIN_KEY = 'mergeCells';
|
|
export const PLUGIN_PRIORITY = 150;
|
|
const privatePool = new WeakMap();
|
|
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}
|
|
* ]}
|
|
* />
|
|
* ```
|
|
* :::
|
|
*/
|
|
export class MergeCells extends BasePlugin {
|
|
static get PLUGIN_KEY() {
|
|
return PLUGIN_KEY;
|
|
}
|
|
static get PLUGIN_PRIORITY() {
|
|
return PLUGIN_PRIORITY;
|
|
}
|
|
constructor(hotInstance) {
|
|
super(hotInstance);
|
|
privatePool.set(this, {
|
|
lastDesiredCoords: null
|
|
});
|
|
|
|
/**
|
|
* A container for all the merged cells.
|
|
*
|
|
* @private
|
|
* @type {MergedCellsCollection}
|
|
*/
|
|
this.mergedCellsCollection = null;
|
|
/**
|
|
* Instance of the class responsible for all the autofill-related calculations.
|
|
*
|
|
* @private
|
|
* @type {AutofillCalculations}
|
|
*/
|
|
this.autofillCalculations = null;
|
|
/**
|
|
* Instance of the class responsible for the selection-related calculations.
|
|
*
|
|
* @private
|
|
* @type {SelectionCalculations}
|
|
*/
|
|
this.selectionCalculations = null;
|
|
}
|
|
|
|
/**
|
|
* 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 MergedCellsCollection(this);
|
|
this.autofillCalculations = new AutofillCalculations(this);
|
|
this.selectionCalculations = new SelectionCalculations(this);
|
|
this.addHook('afterInit', function () {
|
|
return _this.onAfterInit(...arguments);
|
|
});
|
|
this.addHook('modifyTransformStart', function () {
|
|
return _this.onModifyTransformStart(...arguments);
|
|
});
|
|
this.addHook('afterModifyTransformStart', function () {
|
|
return _this.onAfterModifyTransformStart(...arguments);
|
|
});
|
|
this.addHook('modifyTransformEnd', function () {
|
|
return _this.onModifyTransformEnd(...arguments);
|
|
});
|
|
this.addHook('modifyGetCellCoords', function () {
|
|
return _this.onModifyGetCellCoords(...arguments);
|
|
});
|
|
this.addHook('beforeSetRangeStart', function () {
|
|
return _this.onBeforeSetRangeStart(...arguments);
|
|
});
|
|
this.addHook('beforeSetRangeStartOnly', function () {
|
|
return _this.onBeforeSetRangeStart(...arguments);
|
|
});
|
|
this.addHook('beforeSetRangeEnd', function () {
|
|
return _this.onBeforeSetRangeEnd(...arguments);
|
|
});
|
|
this.addHook('afterIsMultipleSelection', function () {
|
|
return _this.onAfterIsMultipleSelection(...arguments);
|
|
});
|
|
this.addHook('afterRenderer', function () {
|
|
return _this.onAfterRenderer(...arguments);
|
|
});
|
|
this.addHook('afterContextMenuDefaultOptions', function () {
|
|
return _this.addMergeActionsToContextMenu(...arguments);
|
|
});
|
|
this.addHook('afterGetCellMeta', function () {
|
|
return _this.onAfterGetCellMeta(...arguments);
|
|
});
|
|
this.addHook('afterViewportRowCalculatorOverride', function () {
|
|
return _this.onAfterViewportRowCalculatorOverride(...arguments);
|
|
});
|
|
this.addHook('afterViewportColumnCalculatorOverride', function () {
|
|
return _this.onAfterViewportColumnCalculatorOverride(...arguments);
|
|
});
|
|
this.addHook('modifyAutofillRange', function () {
|
|
return _this.onModifyAutofillRange(...arguments);
|
|
});
|
|
this.addHook('afterCreateCol', function () {
|
|
return _this.onAfterCreateCol(...arguments);
|
|
});
|
|
this.addHook('afterRemoveCol', function () {
|
|
return _this.onAfterRemoveCol(...arguments);
|
|
});
|
|
this.addHook('afterCreateRow', function () {
|
|
return _this.onAfterCreateRow(...arguments);
|
|
});
|
|
this.addHook('afterRemoveRow', function () {
|
|
return _this.onAfterRemoveRow(...arguments);
|
|
});
|
|
this.addHook('afterChange', function () {
|
|
return _this.onAfterChange(...arguments);
|
|
});
|
|
this.addHook('beforeDrawBorders', function () {
|
|
return _this.onBeforeDrawAreaBorders(...arguments);
|
|
});
|
|
this.addHook('afterDrawSelection', function () {
|
|
return _this.onAfterDrawSelection(...arguments);
|
|
});
|
|
this.addHook('beforeRemoveCellClassNames', function () {
|
|
return _this.onBeforeRemoveCellClassNames(...arguments);
|
|
});
|
|
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() {
|
|
const settings = this.hot.getSettings()[PLUGIN_KEY];
|
|
this.disablePlugin();
|
|
this.enablePlugin();
|
|
this.generateFromSettings(settings);
|
|
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 (!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 = 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 => {
|
|
rowElement.style.background = getStyle(rowElement, 'backgroundColor').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) {
|
|
let valid = true;
|
|
if (!setting) {
|
|
return false;
|
|
}
|
|
if (MergedCellCoords.containsNegativeValues(setting)) {
|
|
warn(MergedCellCoords.NEGATIVE_VALUES_WARNING(setting));
|
|
valid = false;
|
|
} else if (MergedCellCoords.isOutOfBounds(setting, this.hot.countRows(), this.hot.countCols())) {
|
|
warn(MergedCellCoords.IS_OUT_OF_BOUNDS_WARNING(setting));
|
|
valid = false;
|
|
} else if (MergedCellCoords.isSingleCell(setting)) {
|
|
warn(MergedCellCoords.IS_SINGLE_CELL(setting));
|
|
valid = false;
|
|
} else if (MergedCellCoords.containsZeroSpan(setting)) {
|
|
warn(MergedCellCoords.ZERO_SPAN_WARNING(setting));
|
|
valid = false;
|
|
}
|
|
return valid;
|
|
}
|
|
|
|
/**
|
|
* Generates the merged cells from the settings provided to the plugin.
|
|
*
|
|
* @private
|
|
* @param {Array|boolean} settings The settings provided to the plugin.
|
|
*/
|
|
generateFromSettings(settings) {
|
|
if (Array.isArray(settings)) {
|
|
const populatedNulls = [];
|
|
arrayEach(settings, setting => {
|
|
if (!this.validateSetting(setting)) {
|
|
return;
|
|
}
|
|
const highlight = this.hot._createCellCoords(setting.row, setting.col);
|
|
const rangeEnd = this.hot._createCellCoords(setting.row + setting.rowspan - 1, setting.col + setting.colspan - 1);
|
|
const mergeRange = this.hot._createCellRange(highlight, highlight, rangeEnd);
|
|
|
|
// Merging without data population.
|
|
this.mergeRange(mergeRange, true, true);
|
|
rangeEach(setting.row, setting.row + setting.rowspan - 1, rowIndex => {
|
|
rangeEach(setting.col, setting.col + setting.colspan - 1, columnIndex => {
|
|
// Not resetting a cell representing a merge area's value.
|
|
if ((rowIndex === setting.row && columnIndex === setting.col) === false) {
|
|
populatedNulls.push([rowIndex, columnIndex, null]);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// There are no merged cells. Thus, no data population is needed.
|
|
if (populatedNulls.length === 0) {
|
|
return;
|
|
}
|
|
this.hot.setDataAtCell(populatedNulls);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Merge or unmerge, based on last selected range.
|
|
*
|
|
* @private
|
|
*/
|
|
toggleMergeOnSelection() {
|
|
const currentRange = this.hot.getSelectedRangeLast();
|
|
if (!currentRange) {
|
|
return;
|
|
}
|
|
currentRange.setDirection(this.hot.isRtl() ? 'NE-SW' : 'NW-SE');
|
|
const {
|
|
from,
|
|
to
|
|
} = currentRange;
|
|
this.toggleMerge(currentRange);
|
|
this.hot.selectCell(from.row, from.col, to.row, to.col, false);
|
|
}
|
|
|
|
/**
|
|
* 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.getSelectedRangeLast();
|
|
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.getSelectedRangeLast();
|
|
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);
|
|
rangeEach(0, mergeParent.rowspan - 1, i => {
|
|
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);
|
|
if (mergedCellAdded) {
|
|
if (preventPopulation) {
|
|
populationInfo = [mergeParent.row, mergeParent.col, clearedData];
|
|
} else {
|
|
this.hot.populateFromArray(mergeParent.row, mergeParent.col, clearedData, void 0, void 0, 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) {
|
|
return;
|
|
}
|
|
this.hot.runHooks('beforeUnmergeCells', cellRange, auto);
|
|
arrayEach(mergedCells, currentCollection => {
|
|
this.mergedCellsCollection.remove(currentCollection.row, currentCollection.col);
|
|
rangeEach(0, currentCollection.rowspan - 1, i => {
|
|
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 mergedCell = this.mergedCellsCollection.get(cellRange.from.row, cellRange.from.col);
|
|
const mergedCellCoversWholeRange = mergedCell.row === cellRange.from.row && mergedCell.col === cellRange.from.col && mergedCell.row + mergedCell.rowspan - 1 === cellRange.to.row && mergedCell.col + mergedCell.colspan - 1 === cellRange.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));
|
|
}
|
|
|
|
/**
|
|
* `afterInit` hook callback.
|
|
*
|
|
* @private
|
|
*/
|
|
onAfterInit() {
|
|
this.generateFromSettings(this.hot.getSettings()[PLUGIN_KEY]);
|
|
this.hot.render();
|
|
}
|
|
|
|
/**
|
|
* 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: () => {
|
|
this.toggleMerge(this.hot.getSelectedRangeLast());
|
|
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.
|
|
*
|
|
* @private
|
|
* @param {boolean} isMultiple Determines whether the current selection contains multiple cells.
|
|
* @returns {boolean}
|
|
*/
|
|
onAfterIsMultipleSelection(isMultiple) {
|
|
if (isMultiple) {
|
|
const mergedCells = this.mergedCellsCollection.mergedCells;
|
|
const selectionRange = this.hot.getSelectedRangeLast();
|
|
for (let group = 0; group < mergedCells.length; group += 1) {
|
|
if (selectionRange.from.row === mergedCells[group].row && selectionRange.from.col === mergedCells[group].col && selectionRange.to.row === mergedCells[group].row + mergedCells[group].rowspan - 1 && selectionRange.to.col === mergedCells[group].col + mergedCells[group].colspan - 1) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return isMultiple;
|
|
}
|
|
|
|
/**
|
|
* `modifyTransformStart` hook callback.
|
|
*
|
|
* @private
|
|
* @param {object} delta The transformation delta.
|
|
*/
|
|
onModifyTransformStart(delta) {
|
|
const priv = privatePool.get(this);
|
|
const currentlySelectedRange = this.hot.getSelectedRangeLast();
|
|
let newDelta = {
|
|
row: delta.row,
|
|
col: delta.col
|
|
};
|
|
let nextPosition = null;
|
|
const currentPosition = this.hot._createCellCoords(currentlySelectedRange.highlight.row, currentlySelectedRange.highlight.col);
|
|
const mergedParent = this.mergedCellsCollection.get(currentPosition.row, currentPosition.col);
|
|
if (!priv.lastDesiredCoords) {
|
|
priv.lastDesiredCoords = this.hot._createCellCoords(null, null);
|
|
}
|
|
if (mergedParent) {
|
|
// only merge selected
|
|
const mergeTopLeft = this.hot._createCellCoords(mergedParent.row, mergedParent.col);
|
|
const mergeBottomRight = this.hot._createCellCoords(mergedParent.row + mergedParent.rowspan - 1, mergedParent.col + mergedParent.colspan - 1);
|
|
const mergeRange = this.hot._createCellRange(mergeTopLeft, mergeTopLeft, mergeBottomRight);
|
|
if (!mergeRange.includes(priv.lastDesiredCoords)) {
|
|
priv.lastDesiredCoords = this.hot._createCellCoords(null, null); // reset outdated version of lastDesiredCoords
|
|
}
|
|
|
|
newDelta.row = priv.lastDesiredCoords.row ? priv.lastDesiredCoords.row - currentPosition.row : newDelta.row;
|
|
newDelta.col = priv.lastDesiredCoords.col ? priv.lastDesiredCoords.col - currentPosition.col : newDelta.col;
|
|
if (delta.row > 0) {
|
|
// moving down
|
|
newDelta.row = mergedParent.row + mergedParent.rowspan - 1 - currentPosition.row + delta.row;
|
|
} else if (delta.row < 0) {
|
|
// moving up
|
|
newDelta.row = currentPosition.row - mergedParent.row + delta.row;
|
|
}
|
|
if (delta.col > 0) {
|
|
// moving right
|
|
newDelta.col = mergedParent.col + mergedParent.colspan - 1 - currentPosition.col + delta.col;
|
|
} else if (delta.col < 0) {
|
|
// moving left
|
|
newDelta.col = currentPosition.col - mergedParent.col + delta.col;
|
|
}
|
|
}
|
|
nextPosition = this.hot._createCellCoords(currentlySelectedRange.highlight.row + newDelta.row, currentlySelectedRange.highlight.col + newDelta.col);
|
|
const nextPositionMergedCell = this.mergedCellsCollection.get(nextPosition.row, nextPosition.col);
|
|
if (nextPositionMergedCell) {
|
|
// skipping the invisible cells in the merge range
|
|
const firstRenderableCoords = this.mergedCellsCollection.getFirstRenderableCoords(nextPositionMergedCell.row, nextPositionMergedCell.col);
|
|
priv.lastDesiredCoords = nextPosition;
|
|
newDelta = {
|
|
row: firstRenderableCoords.row - currentPosition.row,
|
|
col: firstRenderableCoords.col - currentPosition.col
|
|
};
|
|
}
|
|
if (newDelta.row !== 0) {
|
|
delta.row = newDelta.row;
|
|
}
|
|
if (newDelta.col !== 0) {
|
|
delta.col = newDelta.col;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `modifyTransformEnd` hook callback. Needed to handle "jumping over" merged merged cells, while selecting.
|
|
*
|
|
* @private
|
|
* @param {object} delta The transformation delta.
|
|
*/
|
|
onModifyTransformEnd(delta) {
|
|
const currentSelectionRange = this.hot.getSelectedRangeLast();
|
|
const newDelta = clone(delta);
|
|
const newSelectionRange = this.selectionCalculations.getUpdatedSelectionRange(currentSelectionRange, delta);
|
|
let tempDelta = clone(newDelta);
|
|
const mergedCellsWithinRange = this.mergedCellsCollection.getWithinRange(newSelectionRange, true);
|
|
do {
|
|
tempDelta = clone(newDelta);
|
|
this.selectionCalculations.getUpdatedSelectionRange(currentSelectionRange, newDelta);
|
|
arrayEach(mergedCellsWithinRange, mergedCell => {
|
|
this.selectionCalculations.snapDelta(newDelta, currentSelectionRange, mergedCell);
|
|
});
|
|
} while (newDelta.row !== tempDelta.row || newDelta.col !== tempDelta.col);
|
|
delta.row = newDelta.row;
|
|
delta.col = newDelta.col;
|
|
}
|
|
|
|
/**
|
|
* `modifyGetCellCoords` hook callback. Swaps the `getCell` coords with the merged parent coords.
|
|
*
|
|
* @private
|
|
* @param {number} row Row index.
|
|
* @param {number} column Visual column index.
|
|
* @returns {Array|undefined} Visual coordinates of the merge.
|
|
*/
|
|
onModifyGetCellCoords(row, column) {
|
|
if (row < 0 || column < 0) {
|
|
return;
|
|
}
|
|
const mergeParent = this.mergedCellsCollection.get(row, column);
|
|
if (!mergeParent) {
|
|
return;
|
|
}
|
|
const {
|
|
row: mergeRow,
|
|
col: mergeColumn,
|
|
colspan,
|
|
rowspan
|
|
} = mergeParent;
|
|
return [
|
|
// Most top-left merged cell coords.
|
|
mergeRow, mergeColumn,
|
|
// Most bottom-right merged cell coords.
|
|
mergeRow + rowspan - 1, mergeColumn + colspan - 1];
|
|
}
|
|
|
|
/**
|
|
* `afterContextMenuDefaultOptions` hook callback.
|
|
*
|
|
* @private
|
|
* @param {object} defaultOptions The default context menu options.
|
|
*/
|
|
addMergeActionsToContextMenu(defaultOptions) {
|
|
defaultOptions.items.push({
|
|
name: '---------'
|
|
}, toggleMergeItem(this));
|
|
}
|
|
|
|
/**
|
|
* `afterRenderer` hook callback.
|
|
*
|
|
* @private
|
|
* @param {HTMLElement} TD The cell to be modified.
|
|
* @param {number} row Row index.
|
|
* @param {number} col Visual column index.
|
|
*/
|
|
onAfterRenderer(TD, row, col) {
|
|
const mergedCell = this.mergedCellsCollection.get(row, col);
|
|
// We shouldn't override data in the collection.
|
|
const mergedCellCopy = isObject(mergedCell) ? clone(mergedCell) : void 0;
|
|
if (isObject(mergedCellCopy)) {
|
|
const {
|
|
rowIndexMapper: rowMapper,
|
|
columnIndexMapper: columnMapper
|
|
} = this.hot;
|
|
const {
|
|
row: mergeRow,
|
|
col: mergeColumn,
|
|
colspan,
|
|
rowspan
|
|
} = mergedCellCopy;
|
|
const [lastMergedRowIndex, lastMergedColumnIndex] = this.translateMergedCellToRenderable(mergeRow, rowspan, mergeColumn, colspan);
|
|
const renderedRowIndex = rowMapper.getRenderableFromVisualIndex(row);
|
|
const renderedColumnIndex = columnMapper.getRenderableFromVisualIndex(col);
|
|
const maxRowSpan = lastMergedRowIndex - renderedRowIndex + 1; // Number of rendered columns.
|
|
const maxColSpan = lastMergedColumnIndex - renderedColumnIndex + 1; // Number of rendered columns.
|
|
|
|
// We just try to determine some values basing on the actual number of rendered indexes (some columns may be hidden).
|
|
mergedCellCopy.row = rowMapper.getNearestNotHiddenIndex(mergedCellCopy.row, 1);
|
|
// We just try to determine some values basing on the actual number of rendered indexes (some columns may be hidden).
|
|
mergedCellCopy.col = columnMapper.getNearestNotHiddenIndex(mergedCellCopy.col, 1);
|
|
// The `rowSpan` property for a `TD` element should be at most equal to number of rendered rows in the merge area.
|
|
mergedCellCopy.rowspan = Math.min(mergedCellCopy.rowspan, maxRowSpan);
|
|
// The `colSpan` property for a `TD` element should be at most equal to number of rendered columns in the merge area.
|
|
mergedCellCopy.colspan = Math.min(mergedCellCopy.colspan, maxColSpan);
|
|
}
|
|
applySpanProperties(TD, mergedCellCopy, row, col);
|
|
}
|
|
|
|
/**
|
|
* `beforeSetRangeStart` and `beforeSetRangeStartOnly` hook callback.
|
|
* A selection within merge area should be rewritten to the start of merge area.
|
|
*
|
|
* @private
|
|
* @param {object} coords Cell coords.
|
|
*/
|
|
onBeforeSetRangeStart(coords) {
|
|
// TODO: It is a workaround, but probably this hook may be needed. Every selection on the merge area
|
|
// could set start point of the selection to the start of the merge area. However, logic inside `expandByRange` need
|
|
// an initial start point. Click on the merge cell when there are some hidden indexes break the logic in some cases.
|
|
// Please take a look at #7010 for more information. I'm not sure if selection directions are calculated properly
|
|
// and what was idea for flipping direction inside `expandByRange` method.
|
|
if (this.mergedCellsCollection.isFirstRenderableMergedCell(coords.row, coords.col)) {
|
|
const mergeParent = this.mergedCellsCollection.get(coords.row, coords.col);
|
|
[coords.row, coords.col] = [mergeParent.row, mergeParent.col];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `beforeSetRangeEnd` hook callback.
|
|
* While selecting cells with keyboard or mouse, make sure that rectangular area is expanded to the extent of the
|
|
* merged cell.
|
|
*
|
|
* Note: Please keep in mind that callback may modify both start and end range coordinates by the reference.
|
|
*
|
|
* @private
|
|
* @param {object} coords Cell coords.
|
|
*/
|
|
onBeforeSetRangeEnd(coords) {
|
|
const selRange = this.hot.getSelectedRangeLast();
|
|
selRange.highlight = this.hot._createCellCoords(selRange.highlight.row, selRange.highlight.col); // clone in case we will modify its reference
|
|
selRange.to = coords;
|
|
let rangeExpanded = false;
|
|
if (this.hot.selection.isSelectedByColumnHeader() || this.hot.selection.isSelectedByRowHeader()) {
|
|
return;
|
|
}
|
|
do {
|
|
rangeExpanded = false;
|
|
for (let i = 0; i < this.mergedCellsCollection.mergedCells.length; i += 1) {
|
|
const cellInfo = this.mergedCellsCollection.mergedCells[i];
|
|
const mergedCellRange = cellInfo.getRange();
|
|
if (selRange.expandByRange(mergedCellRange)) {
|
|
coords.row = selRange.to.row;
|
|
coords.col = selRange.to.col;
|
|
rangeExpanded = true;
|
|
}
|
|
}
|
|
} while (rangeExpanded);
|
|
}
|
|
|
|
/**
|
|
* The `afterGetCellMeta` hook callback.
|
|
*
|
|
* @private
|
|
* @param {number} row Row index.
|
|
* @param {number} col Column index.
|
|
* @param {object} cellProperties The cell properties object.
|
|
*/
|
|
onAfterGetCellMeta(row, col, cellProperties) {
|
|
const mergeParent = this.mergedCellsCollection.get(row, col);
|
|
if (mergeParent) {
|
|
if (mergeParent.row !== row || mergeParent.col !== col) {
|
|
cellProperties.copyable = false;
|
|
} else {
|
|
cellProperties.rowspan = mergeParent.rowspan;
|
|
cellProperties.colspan = mergeParent.colspan;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `afterViewportRowCalculatorOverride` hook callback.
|
|
*
|
|
* @private
|
|
* @param {object} calc The row calculator object.
|
|
*/
|
|
onAfterViewportRowCalculatorOverride(calc) {
|
|
const nrOfColumns = this.hot.countCols();
|
|
this.modifyViewportRowStart(calc, nrOfColumns);
|
|
this.modifyViewportRowEnd(calc, nrOfColumns);
|
|
}
|
|
|
|
/**
|
|
* 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 (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 (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.
|
|
*
|
|
* @private
|
|
* @param {object} calc The column calculator object.
|
|
*/
|
|
onAfterViewportColumnCalculatorOverride(calc) {
|
|
const nrOfRows = this.hot.countRows();
|
|
this.modifyViewportColumnStart(calc, nrOfRows);
|
|
this.modifyViewportColumnEnd(calc, nrOfRows);
|
|
}
|
|
|
|
/**
|
|
* 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 (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 (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.
|
|
*
|
|
* @private
|
|
* @param {Array} drag The drag area coordinates.
|
|
* @param {Array} select The selection information.
|
|
* @returns {Array} The new drag area.
|
|
*/
|
|
onModifyAutofillRange(drag, select) {
|
|
this.autofillCalculations.correctSelectionAreaSize(select);
|
|
const dragDirection = this.autofillCalculations.getDirection(select, drag);
|
|
let dragArea = drag;
|
|
if (this.autofillCalculations.dragAreaOverlapsCollections(select, dragArea, dragDirection)) {
|
|
dragArea = select;
|
|
return dragArea;
|
|
}
|
|
const mergedCellsWithinSelectionArea = this.mergedCellsCollection.getWithinRange({
|
|
from: {
|
|
row: select[0],
|
|
col: select[1]
|
|
},
|
|
to: {
|
|
row: select[2],
|
|
col: select[3]
|
|
}
|
|
});
|
|
if (!mergedCellsWithinSelectionArea) {
|
|
return dragArea;
|
|
}
|
|
dragArea = this.autofillCalculations.snapDragArea(select, dragArea, dragDirection, mergedCellsWithinSelectionArea);
|
|
return dragArea;
|
|
}
|
|
|
|
/**
|
|
* `afterCreateCol` hook callback.
|
|
*
|
|
* @private
|
|
* @param {number} column Column index.
|
|
* @param {number} count Number of created columns.
|
|
*/
|
|
onAfterCreateCol(column, count) {
|
|
this.mergedCellsCollection.shiftCollections('right', column, count);
|
|
}
|
|
|
|
/**
|
|
* `afterRemoveCol` hook callback.
|
|
*
|
|
* @private
|
|
* @param {number} column Column index.
|
|
* @param {number} count Number of removed columns.
|
|
*/
|
|
onAfterRemoveCol(column, count) {
|
|
this.mergedCellsCollection.shiftCollections('left', column, count);
|
|
}
|
|
|
|
/**
|
|
* `afterCreateRow` hook callback.
|
|
*
|
|
* @private
|
|
* @param {number} row Row index.
|
|
* @param {number} count Number of created rows.
|
|
* @param {string} source Source of change.
|
|
*/
|
|
onAfterCreateRow(row, count, source) {
|
|
if (source === 'auto') {
|
|
return;
|
|
}
|
|
this.mergedCellsCollection.shiftCollections('down', row, count);
|
|
}
|
|
|
|
/**
|
|
* `afterRemoveRow` hook callback.
|
|
*
|
|
* @private
|
|
* @param {number} row Row index.
|
|
* @param {number} count Number of removed rows.
|
|
*/
|
|
onAfterRemoveRow(row, count) {
|
|
this.mergedCellsCollection.shiftCollections('up', row, count);
|
|
}
|
|
|
|
/**
|
|
* `afterChange` hook callback. Used to propagate merged cells after using Autofill.
|
|
*
|
|
* @private
|
|
* @param {Array} changes The changes array.
|
|
* @param {string} source Determines the source of the change.
|
|
*/
|
|
onAfterChange(changes, source) {
|
|
if (source !== 'Autofill.fill') {
|
|
return;
|
|
}
|
|
this.autofillCalculations.recreateAfterDataPopulation(changes);
|
|
}
|
|
|
|
/**
|
|
* `beforeDrawAreaBorders` hook callback.
|
|
*
|
|
* @private
|
|
* @param {Array} corners Visual coordinates of the area corners.
|
|
* @param {string} className Class name for the area.
|
|
*/
|
|
onBeforeDrawAreaBorders(corners, className) {
|
|
if (className && className === 'area') {
|
|
const selectedRange = this.hot.getSelectedRangeLast();
|
|
const mergedCellsWithinRange = this.mergedCellsCollection.getWithinRange(selectedRange);
|
|
arrayEach(mergedCellsWithinRange, mergedCell => {
|
|
if (selectedRange.getBottomEndCorner().row === mergedCell.getLastRow() && selectedRange.getBottomEndCorner().col === mergedCell.getLastColumn()) {
|
|
corners[2] = mergedCell.row;
|
|
corners[3] = mergedCell.col;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `afterModifyTransformStart` hook callback. Fixes a problem with navigating through merged cells at the edges of
|
|
* the table with the ENTER/SHIFT+ENTER/TAB/SHIFT+TAB keys.
|
|
*
|
|
* @private
|
|
* @param {CellCoords} coords Coordinates of the to-be-selected cell.
|
|
* @param {number} rowTransformDir Row transformation direction (negative value = up, 0 = none, positive value =
|
|
* down).
|
|
* @param {number} colTransformDir Column transformation direction (negative value = up, 0 = none, positive value =
|
|
* down).
|
|
*/
|
|
onAfterModifyTransformStart(coords, rowTransformDir, colTransformDir) {
|
|
if (!this.enabled) {
|
|
return;
|
|
}
|
|
const mergedCellAtCoords = this.mergedCellsCollection.get(coords.row, coords.col);
|
|
if (!mergedCellAtCoords) {
|
|
return;
|
|
}
|
|
const goingDown = rowTransformDir > 0;
|
|
const goingUp = rowTransformDir < 0;
|
|
const goingLeft = colTransformDir < 0;
|
|
const goingRight = colTransformDir > 0;
|
|
const mergedCellOnBottomEdge = mergedCellAtCoords.row + mergedCellAtCoords.rowspan - 1 === this.hot.countRows() - 1;
|
|
const mergedCellOnTopEdge = mergedCellAtCoords.row === 0;
|
|
const mergedCellOnRightEdge = mergedCellAtCoords.col + mergedCellAtCoords.colspan - 1 === this.hot.countCols() - 1;
|
|
const mergedCellOnLeftEdge = mergedCellAtCoords.col === 0;
|
|
if (goingDown && mergedCellOnBottomEdge || goingUp && mergedCellOnTopEdge || goingRight && mergedCellOnRightEdge || goingLeft && mergedCellOnLeftEdge) {
|
|
coords.row = mergedCellAtCoords.row;
|
|
coords.col = mergedCellAtCoords.col;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `afterDrawSelection` hook callback. Used to add the additional class name for the entirely-selected merged cells.
|
|
*
|
|
* @private
|
|
* @param {number} currentRow Visual row index of the currently processed cell.
|
|
* @param {number} currentColumn Visual column index of the currently cell.
|
|
* @param {Array} cornersOfSelection Array of the current selection in a form of `[startRow, startColumn, endRow,
|
|
* endColumn]`.
|
|
* @param {number|undefined} layerLevel Number indicating which layer of selection is currently processed.
|
|
* @returns {string|undefined} A `String`, which will act as an additional `className` to be added to the currently
|
|
* processed cell.
|
|
*/
|
|
onAfterDrawSelection(currentRow, currentColumn, cornersOfSelection, layerLevel) {
|
|
// Nothing's selected (hook might be triggered by the custom borders)
|
|
if (!cornersOfSelection) {
|
|
return;
|
|
}
|
|
return this.selectionCalculations.getSelectedMergedCellClassName(currentRow, currentColumn, cornersOfSelection, layerLevel);
|
|
}
|
|
|
|
/**
|
|
* `beforeRemoveCellClassNames` hook callback. Used to remove additional class name from all cells in the table.
|
|
*
|
|
* @private
|
|
* @returns {string[]} An `Array` of `String`s. Each of these strings will act like class names to be removed from
|
|
* all the cells in the table.
|
|
*/
|
|
onBeforeRemoveCellClassNames() {
|
|
return this.selectionCalculations.getSelectedMergedCellClassNameToRemove();
|
|
}
|
|
} |