@ckeditor/ckeditor5-table
Version:
Table feature for CKEditor 5.
95 lines (94 loc) • 4.22 kB
JavaScript
/**
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
*/
import { Command } from 'ckeditor5/src/core.js';
import TableUtils from '../tableutils.js';
import { updateNumericAttribute } from '../utils/common.js';
import { removeEmptyRowsColumns } from '../utils/structure.js';
/**
* The merge cells command.
*
* The command is registered by {@link module:table/tableediting~TableEditing} as the `'mergeTableCells'` editor command.
*
* For example, to merge selected table cells:
*
* ```ts
* editor.execute( 'mergeTableCells' );
* ```
*/
export default class MergeCellsCommand extends Command {
/**
* @inheritDoc
*/
refresh() {
const tableUtils = this.editor.plugins.get(TableUtils);
const selectedTableCells = tableUtils.getSelectedTableCells(this.editor.model.document.selection);
this.isEnabled = tableUtils.isSelectionRectangular(selectedTableCells);
}
/**
* Executes the command.
*
* @fires execute
*/
execute() {
const model = this.editor.model;
const tableUtils = this.editor.plugins.get(TableUtils);
model.change(writer => {
const selectedTableCells = tableUtils.getSelectedTableCells(model.document.selection);
// All cells will be merged into the first one.
const firstTableCell = selectedTableCells.shift();
// Update target cell dimensions.
const { mergeWidth, mergeHeight } = getMergeDimensions(firstTableCell, selectedTableCells, tableUtils);
updateNumericAttribute('colspan', mergeWidth, firstTableCell, writer);
updateNumericAttribute('rowspan', mergeHeight, firstTableCell, writer);
for (const tableCell of selectedTableCells) {
mergeTableCells(tableCell, firstTableCell, writer);
}
const table = firstTableCell.findAncestor('table');
// Remove rows and columns that become empty (have no anchored cells).
removeEmptyRowsColumns(table, tableUtils);
writer.setSelection(firstTableCell, 'in');
});
}
}
/**
* Merges two table cells. It will ensure that after merging cells with empty paragraphs the resulting table cell will only have one
* paragraph. If one of the merged table cells is empty, the merged table cell will have contents of the non-empty table cell.
* If both are empty, the merged table cell will have only one empty paragraph.
*/
function mergeTableCells(cellBeingMerged, targetCell, writer) {
if (!isEmpty(cellBeingMerged)) {
if (isEmpty(targetCell)) {
writer.remove(writer.createRangeIn(targetCell));
}
writer.move(writer.createRangeIn(cellBeingMerged), writer.createPositionAt(targetCell, 'end'));
}
// Remove merged table cell.
writer.remove(cellBeingMerged);
}
/**
* Checks if the passed table cell contains an empty paragraph.
*/
function isEmpty(tableCell) {
const firstTableChild = tableCell.getChild(0);
return tableCell.childCount == 1 && firstTableChild.is('element', 'paragraph') && firstTableChild.isEmpty;
}
function getMergeDimensions(firstTableCell, selectedTableCells, tableUtils) {
let maxWidthOffset = 0;
let maxHeightOffset = 0;
for (const tableCell of selectedTableCells) {
const { row, column } = tableUtils.getCellLocation(tableCell);
maxWidthOffset = getMaxOffset(tableCell, column, maxWidthOffset, 'colspan');
maxHeightOffset = getMaxOffset(tableCell, row, maxHeightOffset, 'rowspan');
}
// Update table cell span attribute and merge set selection on a merged contents.
const { row: firstCellRow, column: firstCellColumn } = tableUtils.getCellLocation(firstTableCell);
const mergeWidth = maxWidthOffset - firstCellColumn;
const mergeHeight = maxHeightOffset - firstCellRow;
return { mergeWidth, mergeHeight };
}
function getMaxOffset(tableCell, start, currentMaxOffset, which) {
const dimensionValue = parseInt(tableCell.getAttribute(which) || '1');
return Math.max(currentMaxOffset, start + dimensionValue);
}