瀏覽代碼

Move selection related utilities to utils/selection.js.

Maciej Gołaszewski 5 年之前
父節點
當前提交
6376ba835f

+ 1 - 1
packages/ckeditor5-table/src/commands/insertcolumncommand.js

@@ -9,7 +9,7 @@
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import { findAncestor } from './utils';
-import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils/common';
+import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 
 /**
  * The insert column command.

+ 1 - 1
packages/ckeditor5-table/src/commands/insertrowcommand.js

@@ -9,7 +9,7 @@
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import { findAncestor } from './utils';
-import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/common';
+import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 
 /**
  * The insert row command.

+ 1 - 1
packages/ckeditor5-table/src/commands/mergecellcommand.js

@@ -10,7 +10,7 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import TableWalker from '../tablewalker';
 import { isHeadingColumnCell, findAncestor } from './utils';
-import { getTableCellsContainingSelection } from '../utils/common';
+import { getTableCellsContainingSelection } from '../utils/selection';
 
 /**
  * The merge cell command.

+ 1 - 1
packages/ckeditor5-table/src/commands/mergecellscommand.js

@@ -10,7 +10,7 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 import TableUtils from '../tableutils';
 import { findAncestor, updateNumericAttribute } from './utils';
-import { isSelectionRectangular, getSelectedTableCells } from '../utils/common';
+import { getSelectedTableCells, isSelectionRectangular } from '../utils/selection';
 
 /**
  * The merge cells command.

+ 1 - 1
packages/ckeditor5-table/src/commands/removecolumncommand.js

@@ -10,8 +10,8 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 import TableWalker from '../tablewalker';
-import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils/common';
 import { findAncestor } from './utils';
+import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 
 /**
  * The remove column command.

+ 1 - 1
packages/ckeditor5-table/src/commands/removerowcommand.js

@@ -10,7 +10,7 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 import { findAncestor } from './utils';
-import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/common';
+import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 
 /**
  * The remove row command.

+ 1 - 1
packages/ckeditor5-table/src/commands/selectcolumncommand.js

@@ -11,7 +11,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
 
 import TableWalker from '../tablewalker';
 import { findAncestor } from './utils';
-import { getSelectionAffectedTableCells } from '../utils/common';
+import { getSelectionAffectedTableCells } from '../utils/selection';
 
 /**
  * The select column command.

+ 1 - 1
packages/ckeditor5-table/src/commands/selectrowcommand.js

@@ -10,7 +10,7 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 import { findAncestor } from './utils';
-import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/common';
+import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 
 /**
  * The select row command.

+ 2 - 1
packages/ckeditor5-table/src/commands/setheadercolumncommand.js

@@ -10,7 +10,8 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 import { findAncestor, isHeadingColumnCell, updateNumericAttribute } from './utils';
-import { getColumnIndexes, getSelectionAffectedTableCells, getHorizontallyOverlappingCells, splitVertically } from '../utils/common';
+import { getHorizontallyOverlappingCells, splitVertically } from '../utils/common';
+import { getColumnIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 
 /**
  * The header column command.

+ 2 - 1
packages/ckeditor5-table/src/commands/setheaderrowcommand.js

@@ -10,7 +10,8 @@
 import Command from '@ckeditor/ckeditor5-core/src/command';
 
 import { findAncestor, updateNumericAttribute } from './utils';
-import { getVerticallyOverlappingCells, getRowIndexes, getSelectionAffectedTableCells, splitHorizontally } from '../utils/common';
+import { getVerticallyOverlappingCells, splitHorizontally } from '../utils/common';
+import { getRowIndexes, getSelectionAffectedTableCells } from '../utils/selection';
 
 /**
  * The header row command.

+ 1 - 1
packages/ckeditor5-table/src/commands/splitcellcommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { getSelectionAffectedTableCells } from '../utils/common';
+import { getSelectionAffectedTableCells } from '../utils/selection';
 
 /**
  * The split cell command.

+ 1 - 1
packages/ckeditor5-table/src/tablecellproperties/commands/tablecellpropertycommand.js

@@ -8,7 +8,7 @@
  */
 
 import Command from '@ckeditor/ckeditor5-core/src/command';
-import { getSelectionAffectedTableCells } from '../../utils/common';
+import { getSelectionAffectedTableCells } from '../../utils/selection';
 
 /**
  * The table cell attribute command.

+ 1 - 4
packages/ckeditor5-table/src/tableclipboard.js

@@ -12,18 +12,15 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import TableSelection from './tableselection';
 import TableWalker from './tablewalker';
 import {
-	getColumnIndexes,
 	getVerticallyOverlappingCells,
-	getRowIndexes,
-	getSelectionAffectedTableCells,
 	getHorizontallyOverlappingCells,
-	isSelectionRectangular,
 	splitHorizontally,
 	splitVertically
 } from './utils/common';
 import { findAncestor } from './commands/utils';
 import { cropTableToDimensions, trimTableCellIfNeeded } from './tableselection/croptable';
 import TableUtils from './tableutils';
+import { getColumnIndexes, getRowIndexes, getSelectionAffectedTableCells, isSelectionRectangular } from './utils/selection';
 
 /**
  * This plugin adds support for copying/cutting/pasting fragments of tables.

+ 1 - 1
packages/ckeditor5-table/src/tablenavigation.js

@@ -10,12 +10,12 @@
 import TableSelection from './tableselection';
 import TableWalker from './tablewalker';
 import { findAncestor } from './commands/utils';
-import { getSelectedTableCells, getTableCellsContainingSelection } from './utils/common';
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 import priorities from '@ckeditor/ckeditor5-utils/src/priorities';
 import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import { getSelectedTableCells, getTableCellsContainingSelection } from './utils/selection';
 
 /**
  * This plugin enables keyboard navigation for tables.

+ 1 - 1
packages/ckeditor5-table/src/tableselection.js

@@ -13,11 +13,11 @@ import first from '@ckeditor/ckeditor5-utils/src/first';
 import TableWalker from './tablewalker';
 import TableUtils from './tableutils';
 import MouseEventsObserver from './tableselection/mouseeventsobserver';
-import { getColumnIndexes, getRowIndexes, getSelectedTableCells, getTableCellsContainingSelection } from './utils/common';
 import { findAncestor } from './commands/utils';
 import { cropTableToDimensions } from './tableselection/croptable';
 
 import '../theme/tableselection.css';
+import { getColumnIndexes, getRowIndexes, getSelectedTableCells, getTableCellsContainingSelection } from './utils/selection';
 
 /**
  * This plugin enables the advanced table cells, rows and columns selection.

+ 1 - 262
packages/ckeditor5-table/src/utils/common.js

@@ -4,7 +4,7 @@
  */
 
 /**
- * @module table/utils
+ * @module table/utils/common
  */
 
 import { isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
@@ -68,184 +68,6 @@ export function getTableWidgetAncestor( selection ) {
 }
 
 /**
- * Returns all model table cells that are fully selected (from the outside)
- * within the provided model selection's ranges.
- *
- * To obtain the cells selected from the inside, use
- * {@link module:table/utils~getTableCellsContainingSelection}.
- *
- * @param {module:engine/model/selection~Selection} selection
- * @returns {Array.<module:engine/model/element~Element>}
- */
-export function getSelectedTableCells( selection ) {
-	const cells = [];
-
-	for ( const range of sortRanges( selection.getRanges() ) ) {
-		const element = range.getContainedElement();
-
-		if ( element && element.is( 'tableCell' ) ) {
-			cells.push( element );
-		}
-	}
-
-	return cells;
-}
-
-/**
- * Returns all model table cells that the provided model selection's ranges
- * {@link module:engine/model/range~Range#start} inside.
- *
- * To obtain the cells selected from the outside, use
- * {@link module:table/utils~getSelectedTableCells}.
- *
- * @param {module:engine/model/selection~Selection} selection
- * @returns {Array.<module:engine/model/element~Element>}
- */
-export function getTableCellsContainingSelection( selection ) {
-	const cells = [];
-
-	for ( const range of selection.getRanges() ) {
-		const cellWithSelection = findAncestor( 'tableCell', range.start );
-
-		if ( cellWithSelection ) {
-			cells.push( cellWithSelection );
-		}
-	}
-
-	return cells;
-}
-
-/**
- * Returns all model table cells that are either completely selected
- * by selection ranges or host selection range
- * {@link module:engine/model/range~Range#start start positions} inside them.
- *
- * Combines {@link module:table/utils~getTableCellsContainingSelection} and
- * {@link module:table/utils~getSelectedTableCells}.
- *
- * @param {module:engine/model/selection~Selection} selection
- * @returns {Array.<module:engine/model/element~Element>}
- */
-export function getSelectionAffectedTableCells( selection ) {
-	const selectedCells = getSelectedTableCells( selection );
-
-	if ( selectedCells.length ) {
-		return selectedCells;
-	}
-
-	return getTableCellsContainingSelection( selection );
-}
-
-/**
- * Returns an object with the `first` and `last` row index contained in the given `tableCells`.
- *
- *		const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
- *
- *		const { first, last } = getRowIndexes( selectedTableCells );
- *
- *		console.log( `Selected rows: ${ first } to ${ last }` );
- *
- * @param {Array.<module:engine/model/element~Element>} tableCells
- * @returns {Object} Returns an object with the `first` and `last` table row indexes.
- */
-export function getRowIndexes( tableCells ) {
-	const indexes = tableCells.map( cell => cell.parent.index );
-
-	return getFirstLastIndexesObject( indexes );
-}
-
-/**
- * Returns an object with the `first` and `last` column index contained in the given `tableCells`.
- *
- *		const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
- *
- *		const { first, last } = getColumnIndexes( selectedTableCells );
- *
- *		console.log( `Selected columns: ${ first } to ${ last }` );
- *
- * @param {Array.<module:engine/model/element~Element>} tableCells
- * @returns {Object} Returns an object with the `first` and `last` table column indexes.
- */
-export function getColumnIndexes( tableCells ) {
-	const table = findAncestor( 'table', tableCells[ 0 ] );
-	const tableMap = [ ...new TableWalker( table ) ];
-
-	const indexes = tableMap
-		.filter( entry => tableCells.includes( entry.cell ) )
-		.map( entry => entry.column );
-
-	return getFirstLastIndexesObject( indexes );
-}
-
-/**
- * Checks if the selection contains cells that do not exceed rectangular selection.
- *
- * In a table below:
- *
- *   ┌───┬───┬───┬───┐
- *   │ a │ b │ c │ d │
- *   ├───┴───┼───┤   │
- *   │ e     │ f │   │
- *   │       ├───┼───┤
- *   │       │ g │ h │
- *   └───────┴───┴───┘
- *
- * Valid selections are these which create a solid rectangle (without gaps), such as:
- *   - a, b (two horizontal cells)
- *   - c, f (two vertical cells)
- *   - a, b, e (cell "e" spans over four cells)
- *   - c, d, f (cell d spans over a cell in the row below)
- *
- * While an invalid selection would be:
- *   - a, c (the unselected cell "b" creates a gap)
- *   - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap)
- *
- * @param {Array.<module:engine/model/element~Element>} selectedTableCells
- * @param {module:table/tableutils~TableUtils} tableUtils
- * @returns {Boolean}
- */
-export function isSelectionRectangular( selectedTableCells, tableUtils ) {
-	if ( selectedTableCells.length < 2 || !areCellInTheSameTableSection( selectedTableCells ) ) {
-		return false;
-	}
-
-	// A valid selection is a fully occupied rectangle composed of table cells.
-	// Below we will calculate the area of a selected table cells and the area of valid selection.
-	// The area of a valid selection is defined by top-left and bottom-right cells.
-	const rows = new Set();
-	const columns = new Set();
-
-	let areaOfSelectedCells = 0;
-
-	for ( const tableCell of selectedTableCells ) {
-		const { row, column } = tableUtils.getCellLocation( tableCell );
-		const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
-		const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
-
-		// Record row & column indexes of current cell.
-		rows.add( row );
-		columns.add( column );
-
-		// For cells that spans over multiple rows add also the last row that this cell spans over.
-		if ( rowspan > 1 ) {
-			rows.add( row + rowspan - 1 );
-		}
-
-		// For cells that spans over multiple columns add also the last column that this cell spans over.
-		if ( colspan > 1 ) {
-			columns.add( column + colspan - 1 );
-		}
-
-		areaOfSelectedCells += ( rowspan * colspan );
-	}
-
-	// We can only merge table cells that are in adjacent rows...
-	const areaOfValidSelection = getBiggestRectangleArea( rows, columns );
-
-	return areaOfValidSelection == areaOfSelectedCells;
-}
-
-/**
  * Returns slot info of cells that starts above and overlaps a given row.
  *
  * In a table below, passing `overlapRow = 3`
@@ -409,86 +231,3 @@ export function splitVertically( tableCell, columnIndex, splitColumn, writer ) {
 	// Update the colspan attribute after updating table.
 	updateNumericAttribute( 'colspan', newColspan, tableCell, writer );
 }
-
-// Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
-function getFirstLastIndexesObject( indexes ) {
-	const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB );
-
-	const first = allIndexesSorted[ 0 ];
-	const last = allIndexesSorted[ allIndexesSorted.length - 1 ];
-
-	return { first, last };
-}
-
-function sortRanges( rangesIterator ) {
-	return Array.from( rangesIterator ).sort( compareRangeOrder );
-}
-
-function compareRangeOrder( rangeA, rangeB ) {
-	// Since table cell ranges are disjoint, it's enough to check their start positions.
-	const posA = rangeA.start;
-	const posB = rangeB.start;
-
-	// Checking for equal position (returning 0) is not needed because this would be either:
-	// a. Intersecting range (not allowed by model)
-	// b. Collapsed range on the same position (allowed by model but should not happen).
-	return posA.isBefore( posB ) ? -1 : 1;
-}
-
-// Calculates the area of a maximum rectangle that can span over the provided row & column indexes.
-//
-// @param {Array.<Number>} rows
-// @param {Array.<Number>} columns
-// @returns {Number}
-function getBiggestRectangleArea( rows, columns ) {
-	const rowsIndexes = Array.from( rows.values() );
-	const columnIndexes = Array.from( columns.values() );
-
-	const lastRow = Math.max( ...rowsIndexes );
-	const firstRow = Math.min( ...rowsIndexes );
-	const lastColumn = Math.max( ...columnIndexes );
-	const firstColumn = Math.min( ...columnIndexes );
-
-	return ( lastRow - firstRow + 1 ) * ( lastColumn - firstColumn + 1 );
-}
-
-// Checks if the selection does not mix a header (column or row) with other cells.
-//
-// For instance, in the table below valid selections consist of cells with the same letter only.
-// So, a-a (same heading row and column) or d-d (body cells) are valid while c-d or a-b are not.
-//
-//    header columns
-//     ↓   ↓
-//   ┌───┬───┬───┬───┐
-//   │ a │ a │ b │ b │  ← header row
-//   ├───┼───┼───┼───┤
-//   │ c │ c │ d │ d │
-//   ├───┼───┼───┼───┤
-//   │ c │ c │ d │ d │
-//   └───┴───┴───┴───┘
-//
-function areCellInTheSameTableSection( tableCells ) {
-	const table = findAncestor( 'table', tableCells[ 0 ] );
-
-	const rowIndexes = getRowIndexes( tableCells );
-	const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
-
-	// Calculating row indexes is a bit cheaper so if this check fails we can't merge.
-	if ( !areIndexesInSameSection( rowIndexes, headingRows ) ) {
-		return false;
-	}
-
-	const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
-	const columnIndexes = getColumnIndexes( tableCells );
-
-	// Similarly cells must be in same column section.
-	return areIndexesInSameSection( columnIndexes, headingColumns );
-}
-
-// Unified check if table rows/columns indexes are in the same heading/body section.
-function areIndexesInSameSection( { first, last }, headingSectionSize ) {
-	const firstCellIsInHeading = first < headingSectionSize;
-	const lastCellIsInHeading = last < headingSectionSize;
-
-	return firstCellIsInHeading === lastCellIsInHeading;
-}

+ 271 - 0
packages/ckeditor5-table/src/utils/selection.js

@@ -0,0 +1,271 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/utils/selection
+ */
+
+import { findAncestor } from '../commands/utils';
+import TableWalker from '../tablewalker';
+
+/**
+ * Returns all model table cells that are fully selected (from the outside)
+ * within the provided model selection's ranges.
+ *
+ * To obtain the cells selected from the inside, use
+ * {@link module:table/utils~getTableCellsContainingSelection}.
+ *
+ * @param {module:engine/model/selection~Selection} selection
+ * @returns {Array.<module:engine/model/element~Element>}
+ */
+export function getSelectedTableCells( selection ) {
+	const cells = [];
+
+	for ( const range of sortRanges( selection.getRanges() ) ) {
+		const element = range.getContainedElement();
+
+		if ( element && element.is( 'tableCell' ) ) {
+			cells.push( element );
+		}
+	}
+
+	return cells;
+}
+
+/**
+ * Returns all model table cells that the provided model selection's ranges
+ * {@link module:engine/model/range~Range#start} inside.
+ *
+ * To obtain the cells selected from the outside, use
+ * {@link module:table/utils~getSelectedTableCells}.
+ *
+ * @param {module:engine/model/selection~Selection} selection
+ * @returns {Array.<module:engine/model/element~Element>}
+ */
+export function getTableCellsContainingSelection( selection ) {
+	const cells = [];
+
+	for ( const range of selection.getRanges() ) {
+		const cellWithSelection = findAncestor( 'tableCell', range.start );
+
+		if ( cellWithSelection ) {
+			cells.push( cellWithSelection );
+		}
+	}
+
+	return cells;
+}
+
+/**
+ * Returns all model table cells that are either completely selected
+ * by selection ranges or host selection range
+ * {@link module:engine/model/range~Range#start start positions} inside them.
+ *
+ * Combines {@link module:table/utils~getTableCellsContainingSelection} and
+ * {@link module:table/utils~getSelectedTableCells}.
+ *
+ * @param {module:engine/model/selection~Selection} selection
+ * @returns {Array.<module:engine/model/element~Element>}
+ */
+export function getSelectionAffectedTableCells( selection ) {
+	const selectedCells = getSelectedTableCells( selection );
+
+	if ( selectedCells.length ) {
+		return selectedCells;
+	}
+
+	return getTableCellsContainingSelection( selection );
+}
+
+/**
+ * Returns an object with the `first` and `last` row index contained in the given `tableCells`.
+ *
+ *		const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
+ *
+ *		const { first, last } = getRowIndexes( selectedTableCells );
+ *
+ *		console.log( `Selected rows: ${ first } to ${ last }` );
+ *
+ * @param {Array.<module:engine/model/element~Element>} tableCells
+ * @returns {Object} Returns an object with the `first` and `last` table row indexes.
+ */
+export function getRowIndexes( tableCells ) {
+	const indexes = tableCells.map( cell => cell.parent.index );
+
+	return getFirstLastIndexesObject( indexes );
+}
+
+/**
+ * Returns an object with the `first` and `last` column index contained in the given `tableCells`.
+ *
+ *		const selectedTableCells = getSelectedTableCells( editor.model.document.selection );
+ *
+ *		const { first, last } = getColumnIndexes( selectedTableCells );
+ *
+ *		console.log( `Selected columns: ${ first } to ${ last }` );
+ *
+ * @param {Array.<module:engine/model/element~Element>} tableCells
+ * @returns {Object} Returns an object with the `first` and `last` table column indexes.
+ */
+export function getColumnIndexes( tableCells ) {
+	const table = findAncestor( 'table', tableCells[ 0 ] );
+	const tableMap = [ ...new TableWalker( table ) ];
+
+	const indexes = tableMap
+		.filter( entry => tableCells.includes( entry.cell ) )
+		.map( entry => entry.column );
+
+	return getFirstLastIndexesObject( indexes );
+}
+
+/**
+ * Checks if the selection contains cells that do not exceed rectangular selection.
+ *
+ * In a table below:
+ *
+ *   ┌───┬───┬───┬───┐
+ *   │ a │ b │ c │ d │
+ *   ├───┴───┼───┤   │
+ *   │ e     │ f │   │
+ *   │       ├───┼───┤
+ *   │       │ g │ h │
+ *   └───────┴───┴───┘
+ *
+ * Valid selections are these which create a solid rectangle (without gaps), such as:
+ *   - a, b (two horizontal cells)
+ *   - c, f (two vertical cells)
+ *   - a, b, e (cell "e" spans over four cells)
+ *   - c, d, f (cell d spans over a cell in the row below)
+ *
+ * While an invalid selection would be:
+ *   - a, c (the unselected cell "b" creates a gap)
+ *   - f, g, h (cell "d" spans over a cell from the row of "f" cell - thus creates a gap)
+ *
+ * @param {Array.<module:engine/model/element~Element>} selectedTableCells
+ * @param {module:table/tableutils~TableUtils} tableUtils
+ * @returns {Boolean}
+ */
+export function isSelectionRectangular( selectedTableCells, tableUtils ) {
+	if ( selectedTableCells.length < 2 || !areCellInTheSameTableSection( selectedTableCells ) ) {
+		return false;
+	}
+
+	// A valid selection is a fully occupied rectangle composed of table cells.
+	// Below we will calculate the area of a selected table cells and the area of valid selection.
+	// The area of a valid selection is defined by top-left and bottom-right cells.
+	const rows = new Set();
+	const columns = new Set();
+
+	let areaOfSelectedCells = 0;
+
+	for ( const tableCell of selectedTableCells ) {
+		const { row, column } = tableUtils.getCellLocation( tableCell );
+		const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
+		const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
+
+		// Record row & column indexes of current cell.
+		rows.add( row );
+		columns.add( column );
+
+		// For cells that spans over multiple rows add also the last row that this cell spans over.
+		if ( rowspan > 1 ) {
+			rows.add( row + rowspan - 1 );
+		}
+
+		// For cells that spans over multiple columns add also the last column that this cell spans over.
+		if ( colspan > 1 ) {
+			columns.add( column + colspan - 1 );
+		}
+
+		areaOfSelectedCells += ( rowspan * colspan );
+	}
+
+	// We can only merge table cells that are in adjacent rows...
+	const areaOfValidSelection = getBiggestRectangleArea( rows, columns );
+
+	return areaOfValidSelection == areaOfSelectedCells;
+}
+
+// Helper method to get an object with `first` and `last` indexes from an unsorted array of indexes.
+function getFirstLastIndexesObject( indexes ) {
+	const allIndexesSorted = indexes.sort( ( indexA, indexB ) => indexA - indexB );
+
+	const first = allIndexesSorted[ 0 ];
+	const last = allIndexesSorted[ allIndexesSorted.length - 1 ];
+
+	return { first, last };
+}
+
+function sortRanges( rangesIterator ) {
+	return Array.from( rangesIterator ).sort( compareRangeOrder );
+}
+
+function compareRangeOrder( rangeA, rangeB ) {
+	// Since table cell ranges are disjoint, it's enough to check their start positions.
+	const posA = rangeA.start;
+	const posB = rangeB.start;
+
+	// Checking for equal position (returning 0) is not needed because this would be either:
+	// a. Intersecting range (not allowed by model)
+	// b. Collapsed range on the same position (allowed by model but should not happen).
+	return posA.isBefore( posB ) ? -1 : 1;
+}
+
+// Calculates the area of a maximum rectangle that can span over the provided row & column indexes.
+//
+// @param {Array.<Number>} rows
+// @param {Array.<Number>} columns
+// @returns {Number}
+function getBiggestRectangleArea( rows, columns ) {
+	const rowsIndexes = Array.from( rows.values() );
+	const columnIndexes = Array.from( columns.values() );
+
+	const lastRow = Math.max( ...rowsIndexes );
+	const firstRow = Math.min( ...rowsIndexes );
+	const lastColumn = Math.max( ...columnIndexes );
+	const firstColumn = Math.min( ...columnIndexes );
+
+	return ( lastRow - firstRow + 1 ) * ( lastColumn - firstColumn + 1 );
+}
+
+// Checks if the selection does not mix a header (column or row) with other cells.
+//
+// For instance, in the table below valid selections consist of cells with the same letter only.
+// So, a-a (same heading row and column) or d-d (body cells) are valid while c-d or a-b are not.
+//
+//    header columns
+//     ↓   ↓
+//   ┌───┬───┬───┬───┐
+//   │ a │ a │ b │ b │  ← header row
+//   ├───┼───┼───┼───┤
+//   │ c │ c │ d │ d │
+//   ├───┼───┼───┼───┤
+//   │ c │ c │ d │ d │
+//   └───┴───┴───┴───┘
+function areCellInTheSameTableSection( tableCells ) {
+	const table = findAncestor( 'table', tableCells[ 0 ] );
+
+	const rowIndexes = getRowIndexes( tableCells );
+	const headingRows = parseInt( table.getAttribute( 'headingRows' ) || 0 );
+
+	// Calculating row indexes is a bit cheaper so if this check fails we can't merge.
+	if ( !areIndexesInSameSection( rowIndexes, headingRows ) ) {
+		return false;
+	}
+
+	const headingColumns = parseInt( table.getAttribute( 'headingColumns' ) || 0 );
+	const columnIndexes = getColumnIndexes( tableCells );
+
+	// Similarly cells must be in same column section.
+	return areIndexesInSameSection( columnIndexes, headingColumns );
+}
+
+// Unified check if table rows/columns indexes are in the same heading/body section.
+function areIndexesInSameSection( { first, last }, headingSectionSize ) {
+	const firstCellIsInHeading = first < headingSectionSize;
+	const lastCellIsInHeading = last < headingSectionSize;
+
+	return firstCellIsInHeading === lastCellIsInHeading;
+}

+ 1 - 1
packages/ckeditor5-table/tests/manual/tablemocking.js

@@ -14,8 +14,8 @@ import { diffString } from 'json-diff';
 import { debounce } from 'lodash-es';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import TableWalker from '../../src/tablewalker';
-import { getSelectionAffectedTableCells } from '../../src/utils/common';
 import { findAncestor } from '../../src/commands/utils';
+import { getSelectionAffectedTableCells } from '../../src/utils/selection';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {

+ 1 - 1
packages/ckeditor5-table/tests/tablenavigation.js

@@ -8,7 +8,6 @@ import Table from '../src/table';
 import TableEditing from '../src/tableediting';
 import TableSelection from '../src/tableselection';
 import { modelTable } from './_utils/utils';
-import { getTableCellsContainingSelection } from '../src/utils/common';
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
@@ -26,6 +25,7 @@ import { getData as getModelData, setData as setModelData } from '@ckeditor/cked
 import { assertEqualMarkup } from '@ckeditor/ckeditor5-utils/tests/_utils/utils';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import env from '@ckeditor/ckeditor5-utils/src/env';
+import { getTableCellsContainingSelection } from '../src/utils/selection';
 
 describe( 'TableNavigation', () => {
 	let editor, model, modelRoot, tableSelection, tableNavigation, selection;

+ 1 - 1
packages/ckeditor5-table/tests/ui/utils.js

@@ -7,7 +7,6 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
 import Table from '../../src/table';
 import TableCellProperties from '../../src/tablecellproperties';
 import { findAncestor } from '../../src/commands/utils';
-import { getTableCellsContainingSelection } from '../../src/utils/common';
 import global from '@ckeditor/ckeditor5-utils/src/dom/global';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import View from '@ckeditor/ckeditor5-ui/src/view';
@@ -31,6 +30,7 @@ import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { centeredBalloonPositionForLongWidgets } from '@ckeditor/ckeditor5-widget/src/utils';
 import { modelTable } from '../_utils/utils';
+import { getTableCellsContainingSelection } from '../../src/utils/selection';
 
 describe( 'UI Utils', () => {
 	let editor, editingView, balloon, editorElement;

+ 2 - 3
packages/ckeditor5-table/tests/utils.js

@@ -11,10 +11,9 @@ import TableEditing from '../src/tableediting';
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { modelTable } from './_utils/utils';
 import {
-	getSelectedTableCells,
-	getTableCellsContainingSelection,
-	getSelectionAffectedTableCells, getVerticallyOverlappingCells, getHorizontallyOverlappingCells
+	getVerticallyOverlappingCells, getHorizontallyOverlappingCells
 } from '../src/utils/common';
+import { getSelectedTableCells, getSelectionAffectedTableCells, getTableCellsContainingSelection } from '../src/utils/selection';
 
 describe( 'table utils', () => {
 	let editor, model, tableSelection, modelRoot;