Bladeren bron

Custom table navigation extracted to the separate plugin. Navigation fixes.

Kuba Niegowski 5 jaren geleden
bovenliggende
commit
119ae7b498

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

@@ -13,6 +13,7 @@ import TableEditing from './tableediting';
 import TableUI from './tableui';
 import TableSelection from './tableselection';
 import TableClipboard from './tableclipboard';
+import TableNavigation from './tablenavigation';
 import Widget from '@ckeditor/ckeditor5-widget/src/widget';
 
 import '../theme/table.css';
@@ -36,7 +37,7 @@ export default class Table extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ TableEditing, TableUI, TableSelection, TableClipboard, Widget ];
+		return [ TableEditing, TableUI, TableSelection, TableClipboard, TableNavigation, Widget ];
 	}
 
 	/**

+ 0 - 320
packages/ckeditor5-table/src/tableediting.js

@@ -31,11 +31,7 @@ import SetHeaderColumnCommand from './commands/setheadercolumncommand';
 import MergeCellsCommand from './commands/mergecellscommand';
 import SelectRowCommand from './commands/selectrowcommand';
 import SelectColumnCommand from './commands/selectcolumncommand';
-import { getSelectedTableCells, getTableCellsContainingSelection } from './utils';
 import TableUtils from '../src/tableutils';
-import { findAncestor } from './commands/utils';
-import TableWalker from './tablewalker';
-import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
 
 import injectTableLayoutPostFixer from './converters/table-layout-post-fixer';
 import injectTableCellParagraphPostFixer from './converters/table-cell-paragraph-post-fixer';
@@ -153,16 +149,6 @@ export default class TableEditing extends Plugin {
 		injectTableLayoutPostFixer( model );
 		injectTableCellRefreshPostFixer( model );
 		injectTableCellParagraphPostFixer( model );
-
-		// Handle Tab key navigation.
-		this.editor.keystrokes.set( 'Tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { priority: 'low' } );
-		this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } );
-		this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } );
-
-		this.editor.keystrokes.set( 'ArrowLeft', this._getArrowHandler( 'left' ), { priority: 'low' } );
-		this.editor.keystrokes.set( 'ArrowRight', this._getArrowHandler( 'right' ), { priority: 'low' } );
-		this.editor.keystrokes.set( 'ArrowUp', this._getArrowHandler( 'up' ), { priority: 'low' } );
-		this.editor.keystrokes.set( 'ArrowDown', this._getArrowHandler( 'down' ), { priority: 'low' } );
 	}
 
 	/**
@@ -171,310 +157,4 @@ export default class TableEditing extends Plugin {
 	static get requires() {
 		return [ TableUtils ];
 	}
-
-	/**
-	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed
-	 * when the table widget is selected.
-	 *
-	 * @private
-	 * @param {module:utils/eventinfo~EventInfo} eventInfo
-	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
-	 */
-	_handleTabOnSelectedTable( domEventData, cancel ) {
-		const editor = this.editor;
-		const selection = editor.model.document.selection;
-
-		if ( !selection.isCollapsed && selection.rangeCount === 1 && selection.getFirstRange().isFlat ) {
-			const selectedElement = selection.getSelectedElement();
-
-			if ( !selectedElement || !selectedElement.is( 'table' ) ) {
-				return;
-			}
-
-			cancel();
-
-			editor.model.change( writer => {
-				writer.setSelection( writer.createRangeIn( selectedElement.getChild( 0 ).getChild( 0 ) ) );
-			} );
-		}
-	}
-
-	/**
-	 * Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed
-	 * inside table cell.
-	 *
-	 * @private
-	 * @param {Boolean} isForward Whether this handler will move the selection to the next or the previous cell.
-	 */
-	_getTabHandler( isForward ) {
-		const editor = this.editor;
-
-		return ( domEventData, cancel ) => {
-			const selection = editor.model.document.selection;
-			const tableCell = getTableCellsContainingSelection( selection )[ 0 ];
-
-			if ( !tableCell ) {
-				return;
-			}
-
-			cancel();
-
-			const tableRow = tableCell.parent;
-			const table = tableRow.parent;
-
-			const currentRowIndex = table.getChildIndex( tableRow );
-			const currentCellIndex = tableRow.getChildIndex( tableCell );
-
-			const isFirstCellInRow = currentCellIndex === 0;
-
-			if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) {
-				// It's the first cell of the table - don't do anything (stay in the current position).
-				return;
-			}
-
-			const isLastCellInRow = currentCellIndex === tableRow.childCount - 1;
-			const isLastRow = currentRowIndex === table.childCount - 1;
-
-			if ( isForward && isLastRow && isLastCellInRow ) {
-				editor.execute( 'insertTableRowBelow' );
-
-				// Check if the command actually added a row. If `insertTableRowBelow` execution didn't add a row (because it was disabled
-				// or it got overwritten) do not change the selection.
-				if ( currentRowIndex === table.childCount - 1 ) {
-					return;
-				}
-			}
-
-			let cellToFocus;
-
-			// Move to first cell in next row.
-			if ( isForward && isLastCellInRow ) {
-				const nextRow = table.getChild( currentRowIndex + 1 );
-
-				cellToFocus = nextRow.getChild( 0 );
-			}
-			// Move to last cell in a previous row.
-			else if ( !isForward && isFirstCellInRow ) {
-				const previousRow = table.getChild( currentRowIndex - 1 );
-
-				cellToFocus = previousRow.getChild( previousRow.childCount - 1 );
-			}
-			// Move to next/previous cell.
-			else {
-				cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) );
-			}
-
-			editor.model.change( writer => {
-				writer.setSelection( writer.createRangeIn( cellToFocus ) );
-			} );
-		};
-	}
-
-	/**
-	 * Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the arrow keys executed
-	 * inside table cell.
-	 *
-	 * @private
-	 * @param {String} direction The direction of navigation relative to the cell in which the caret is located.
-	 * Possible values: `"left"`, `"right"`, `"up"` and `"down"`.
-	 */
-	_getArrowHandler( direction ) {
-		return ( data, cancel ) => {
-			const model = this.editor.model;
-			const selection = model.document.selection;
-			const editing = this.editor.editing;
-			const domConverter = editing.view.domConverter;
-
-			// At first let's check if there are some cells that are fully selected (from the outside).
-			const selectedCells = getSelectedTableCells( selection );
-
-			if ( selectedCells.length ) {
-				const tableCell = [ 'left', 'up' ].includes( direction ) ? selectedCells[ 0 ] : selectedCells.pop();
-
-				this._navigateFromCellInDirection( tableCell, direction );
-				cancel();
-				return;
-			}
-
-			// So we fall back to selection inside the table cell.
-			const tableCell = findAncestor( 'tableCell', selection.focus );
-
-			if ( !tableCell ) {
-				return;
-			}
-
-			const cellRange = model.createRangeIn( tableCell );
-
-			// First let's handle simplest cases - left and right is allowed only on first/last positions of a table cell.
-			if ( direction == 'left' ) {
-				if ( selection.isCollapsed && selection.focus.isTouching( cellRange.start ) ) {
-					this._navigateFromCellInDirection( tableCell, direction );
-					cancel();
-				}
-				return;
-			}
-
-			if ( direction == 'right' ) {
-				if ( selection.isCollapsed && selection.focus.isTouching( cellRange.end ) ) {
-					this._navigateFromCellInDirection( tableCell, direction );
-					cancel();
-				}
-				return;
-			}
-
-			// For up and down we could first check if we are on first/last position of a table cell.
-			if ( direction == 'up' && selection.focus.isTouching( cellRange.start ) ) {
-				this._navigateFromCellInDirection( tableCell, direction );
-				cancel();
-				return;
-			}
-
-			// This also solves problem with collapsed selection after text node.
-			if ( direction == 'down' && selection.focus.isTouching( cellRange.end ) ) {
-				this._navigateFromCellInDirection( tableCell, direction );
-				cancel();
-				return;
-			}
-
-			// Ok, so easiest cases didn't solved the problem, let's try finding Rects
-			// of the selection and the first/last row in the table cell.
-			const selectionPosition = direction == 'up' ? selection.getFirstPosition() : selection.getLastPosition();
-			const selectionFocusRange = editing.mapper.toViewRange( model.createRange( selectionPosition ) );
-			const focusRangeRect = Rect.getDomRangeRects( domConverter.viewRangeToDom( selectionFocusRange ) ).pop();
-
-			if ( direction == 'up' ) {
-				const firstLineRect = getRangeLimitLineRect( this.editor, cellRange, true );
-
-				if ( firstLineRect.contains( focusRangeRect ) ) {
-					this._navigateFromCellInDirection( tableCell, direction );
-					cancel();
-				}
-
-				return;
-			}
-
-			if ( direction == 'down' ) {
-				const lastLineRect = getRangeLimitLineRect( this.editor, cellRange, false );
-
-				if ( lastLineRect.contains( focusRangeRect ) ) {
-					this._navigateFromCellInDirection( tableCell, direction );
-					cancel();
-				}
-			}
-		};
-	}
-
-	/**
-	 * Moves the selection from given `tableCell` in the specified `direction`.
-	 *
-	 * @private
-	 * @param {module:engine/model/element~Element} tableCell The table cell to start the selection navigation.
-	 * @param {String} direction Direction in which selection should move.
-	 */
-	_navigateFromCellInDirection( tableCell, direction ) {
-		const model = this.editor.model;
-
-		const table = findAncestor( 'table', tableCell );
-		const tableMap = [ ...new TableWalker( table, { includeSpanned: true } ) ];
-		const { row: lastRow, column: lastColumn } = tableMap[ tableMap.length - 1 ];
-
-		const currentCellInfo = tableMap.find( ( { cell } ) => cell == tableCell );
-		let { row, column } = currentCellInfo;
-
-		switch ( direction ) {
-			case 'left':
-				column--;
-				break;
-
-			case 'up':
-				row--;
-				break;
-
-			case 'right':
-				column += currentCellInfo.colspan || 1;
-				break;
-
-			case 'down':
-				row += currentCellInfo.rowspan || 1;
-				break;
-		}
-
-		const isOutsideVertically = row < 0 || row > lastRow;
-		const isBeforeFirstCell = column < 0 && row <= 0;
-		const isAfterLastCell = column > lastColumn && row >= lastRow;
-
-		// Note that if the last table cell is row-spanned then isAfterLastCell will never be true but we don't know
-		// if user was navigating on the last row or not, so let's allow him to stay in the table.
-
-		if ( isOutsideVertically || isBeforeFirstCell || isAfterLastCell ) {
-			model.change( writer => {
-				writer.setSelection( writer.createRangeOn( table ) );
-			} );
-
-			return;
-		}
-
-		if ( column < 0 ) {
-			column = lastColumn;
-			row--;
-		} else if ( column > lastColumn ) {
-			column = 0;
-			row++;
-		}
-
-		const cellToFocus = tableMap.find( cellInfo => cellInfo.row == row && cellInfo.column == column ).cell;
-		const rangeToFocus = model.createRangeIn( cellToFocus );
-		const positionToFocus = [ 'left', 'up' ].includes( direction ) ? rangeToFocus.end : rangeToFocus.start;
-
-		model.change( writer => {
-			writer.setSelection( positionToFocus );
-		} );
-	}
-}
-
-// Returns `Rect` of first or last line of the `range`.
-//
-// @private
-// @param {module:core/editor/editor~Editor} editor The editor instance.
-// @param {module:engine/model/range~Range} range Range of model elements.
-// @param {Boolean} firstLine Whether should find `Rect` of first or last line.
-// @returns {module:utils/dom/rect~Rect}
-function getRangeLimitLineRect( editor, range, firstLine ) {
-	const editing = editor.editing;
-	const domConverter = editing.view.domConverter;
-
-	const viewCellRange = editing.mapper.toViewRange( range );
-	const cellRangeRects = Rect.getDomRangeRects( domConverter.viewRangeToDom( viewCellRange ) );
-
-	const lineRect = {
-		left: Number.POSITIVE_INFINITY,
-		top: Number.POSITIVE_INFINITY,
-		right: Number.NEGATIVE_INFINITY,
-		bottom: Number.NEGATIVE_INFINITY
-	};
-
-	for ( let i = 0; i < cellRangeRects.length; i++ ) {
-		const idx = firstLine ? i : cellRangeRects.length - i - 1;
-		const rect = cellRangeRects[ idx ];
-		const nextRect = idx + 1 < cellRangeRects.length ? cellRangeRects[ idx + 1 ] : null;
-
-		// First let's skip container Rects.
-		if ( !nextRect || !rect.contains( nextRect ) ) {
-			// Let's check if this rect is in new line.
-			if ( firstLine && rect.left < lineRect.right || !firstLine && rect.right > lineRect.left ) {
-				break;
-			}
-
-			lineRect.left = Math.min( lineRect.left, rect.left );
-			lineRect.top = Math.min( lineRect.top, rect.top );
-			lineRect.right = Math.max( lineRect.right, rect.right );
-			lineRect.bottom = Math.max( lineRect.bottom, rect.bottom );
-		}
-	}
-
-	return new Rect( {
-		...lineRect,
-		width: lineRect.right - lineRect.left,
-		height: lineRect.bottom - lineRect.top
-	} );
 }

+ 417 - 0
packages/ckeditor5-table/src/tablenavigation.js

@@ -0,0 +1,417 @@
+/**
+ * @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/tablenavigation
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import { getSelectedTableCells, getTableCellsContainingSelection } from './utils';
+import { findAncestor } from './commands/utils';
+import TableWalker from './tablewalker';
+import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
+
+/**
+ * This plugin enables a keyboard navigation for tables.
+ * It is loaded automatically by the {@link module:table/table~Table} plugin.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class TableNavigation extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TableNavigation';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		// Handle Tab key navigation.
+		this.editor.keystrokes.set( 'Tab', ( ...args ) => this._handleTabOnSelectedTable( ...args ), { priority: 'low' } );
+		this.editor.keystrokes.set( 'Tab', this._getTabHandler( true ), { priority: 'low' } );
+		this.editor.keystrokes.set( 'Shift+Tab', this._getTabHandler( false ), { priority: 'low' } );
+
+		// Handle arrows navigation.
+		this.editor.keystrokes.set( 'ArrowLeft', this._getArrowHandler( 'left' ), { priority: 'low' } );
+		this.editor.keystrokes.set( 'ArrowRight', this._getArrowHandler( 'right' ), { priority: 'low' } );
+		this.editor.keystrokes.set( 'ArrowUp', this._getArrowHandler( 'up' ), { priority: 'low' } );
+		this.editor.keystrokes.set( 'ArrowDown', this._getArrowHandler( 'down' ), { priority: 'low' } );
+	}
+
+	/**
+	 * Handles {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed
+	 * when the table widget is selected.
+	 *
+	 * @private
+	 * @param {module:utils/eventinfo~EventInfo} eventInfo
+	 * @param {module:engine/view/observer/domeventdata~DomEventData} domEventData
+	 */
+	_handleTabOnSelectedTable( domEventData, cancel ) {
+		const editor = this.editor;
+		const selection = editor.model.document.selection;
+
+		if ( !selection.isCollapsed && selection.rangeCount === 1 && selection.getFirstRange().isFlat ) {
+			const selectedElement = selection.getSelectedElement();
+
+			if ( !selectedElement || !selectedElement.is( 'table' ) ) {
+				return;
+			}
+
+			cancel();
+
+			editor.model.change( writer => {
+				writer.setSelection( writer.createRangeIn( selectedElement.getChild( 0 ).getChild( 0 ) ) );
+			} );
+		}
+	}
+
+	/**
+	 * Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the <kbd>Tab</kbd> key executed
+	 * inside table cell.
+	 *
+	 * @private
+	 * @param {Boolean} isForward Whether this handler will move the selection to the next or the previous cell.
+	 */
+	_getTabHandler( isForward ) {
+		const editor = this.editor;
+
+		return ( domEventData, cancel ) => {
+			const selection = editor.model.document.selection;
+			const tableCell = getTableCellsContainingSelection( selection )[ 0 ];
+
+			if ( !tableCell ) {
+				return;
+			}
+
+			cancel();
+
+			const tableRow = tableCell.parent;
+			const table = tableRow.parent;
+
+			const currentRowIndex = table.getChildIndex( tableRow );
+			const currentCellIndex = tableRow.getChildIndex( tableCell );
+
+			const isFirstCellInRow = currentCellIndex === 0;
+
+			if ( !isForward && isFirstCellInRow && currentRowIndex === 0 ) {
+				// It's the first cell of the table - don't do anything (stay in the current position).
+				return;
+			}
+
+			const isLastCellInRow = currentCellIndex === tableRow.childCount - 1;
+			const isLastRow = currentRowIndex === table.childCount - 1;
+
+			if ( isForward && isLastRow && isLastCellInRow ) {
+				editor.execute( 'insertTableRowBelow' );
+
+				// Check if the command actually added a row. If `insertTableRowBelow` execution didn't add a row (because it was disabled
+				// or it got overwritten) do not change the selection.
+				if ( currentRowIndex === table.childCount - 1 ) {
+					return;
+				}
+			}
+
+			let cellToFocus;
+
+			// Move to first cell in next row.
+			if ( isForward && isLastCellInRow ) {
+				const nextRow = table.getChild( currentRowIndex + 1 );
+
+				cellToFocus = nextRow.getChild( 0 );
+			}
+			// Move to last cell in a previous row.
+			else if ( !isForward && isFirstCellInRow ) {
+				const previousRow = table.getChild( currentRowIndex - 1 );
+
+				cellToFocus = previousRow.getChild( previousRow.childCount - 1 );
+			}
+			// Move to next/previous cell.
+			else {
+				cellToFocus = tableRow.getChild( currentCellIndex + ( isForward ? 1 : -1 ) );
+			}
+
+			editor.model.change( writer => {
+				writer.setSelection( writer.createRangeIn( cellToFocus ) );
+			} );
+		};
+	}
+
+	/**
+	 * Returns a handler for {@link module:engine/view/document~Document#event:keydown keydown} events for the arrow keys executed
+	 * inside table cell.
+	 *
+	 * @private
+	 * @param {String} direction The direction of navigation relative to the cell in which the caret is located.
+	 * Possible values: `"left"`, `"right"`, `"up"` and `"down"`.
+	 */
+	_getArrowHandler( direction ) {
+		return ( data, cancel ) => {
+			const model = this.editor.model;
+			const selection = model.document.selection;
+
+			// At first let's check if there are some cells that are fully selected (from the outside).
+			const selectedCells = getSelectedTableCells( selection );
+
+			if ( selectedCells.length ) {
+				const tableCell = [ 'left', 'up' ].includes( direction ) ? selectedCells[ 0 ] : selectedCells.pop();
+
+				this._navigateFromCellInDirection( tableCell, direction );
+
+				return cancel();
+			}
+
+			// So we fall back to selection inside the table cell.
+			const tableCell = findAncestor( 'tableCell', selection.focus );
+
+			// But the selection is outside the table.
+			if ( !tableCell ) {
+				return;
+			}
+
+			const cellRange = model.createRangeIn( tableCell );
+
+			// Let's check if the selection is at the beginning/end of the cell.
+			if ( isSelectionAtEdge( cellRange, selection, direction ) ) {
+				this._navigateFromCellInDirection( tableCell, direction );
+
+				return cancel();
+			}
+
+			// Ok, so easiest cases didn't solved the problem, let's try to find out if we are in the first/last
+			// line of the cell content, and if so we will move the caret to beginning/end.
+			if ( [ 'up', 'down' ].includes( direction ) && this._handleEdgeLineNavigation( direction, cellRange ) ) {
+				cancel();
+			}
+		};
+	}
+
+	/**
+	 * Detects if a keyboard navigation is on the first/last row of the table cell and moves selection to the beginning/end.
+	 *
+	 * @private
+	 * @param {String} direction The direction of navigation relative to the cell in which the caret is located.
+	 * Possible values: `"left"`, `"right"`, `"up"` and `"down"`.
+	 * @param {module:engine/model/range~Range} cellRange Current table cell content range.
+	 * @returns {Boolean} Whether navigation was handled.
+	 */
+	_handleEdgeLineNavigation( direction, cellRange ) {
+		const model = this.editor.model;
+		const selection = model.document.selection;
+		const editing = this.editor.editing;
+
+		const selectionPosition = direction == 'up' ? selection.getFirstPosition() : selection.getLastPosition();
+
+		const focusRangeBefore = createRangeFromPositionAndLength( model, selectionPosition, -1 );
+		const focusRangeAfter = createRangeFromPositionAndLength( model, selectionPosition, 1 );
+
+		const focusRangeRectBefore = focusRangeBefore ? mapModelRangeToViewRect( editing, focusRangeBefore ) : null;
+		const focusRangeRectAfter = focusRangeAfter ? mapModelRangeToViewRect( editing, focusRangeAfter ) : null;
+
+		if ( direction == 'up' ) {
+			const modelRange = model.createRange( cellRange.start, selectionPosition );
+			const firstLineRect = getRangeLimitLineRect( this.editor, modelRange, true );
+
+			if (
+				focusRangeRectBefore && firstLineRect.getIntersectionArea( focusRangeRectBefore ) ||
+				focusRangeRectAfter && firstLineRect.getIntersectionArea( focusRangeRectAfter )
+			) {
+				model.change( writer => {
+					writer.setSelection( cellRange.start );
+				} );
+
+				return true;
+			}
+		} else if ( direction == 'down' ) {
+			const modelRange = model.createRange( selectionPosition, cellRange.end );
+			const lastLineRect = getRangeLimitLineRect( this.editor, modelRange, false );
+
+			if (
+				focusRangeRectBefore && lastLineRect.getIntersectionArea( focusRangeRectBefore ) ||
+				focusRangeRectAfter && lastLineRect.getIntersectionArea( focusRangeRectAfter )
+			) {
+				model.change( writer => {
+					writer.setSelection( cellRange.end );
+				} );
+
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Moves the selection from given `tableCell` in the specified `direction`.
+	 *
+	 * @private
+	 * @param {module:engine/model/element~Element} tableCell The table cell to start the selection navigation.
+	 * @param {String} direction Direction in which selection should move.
+	 */
+	_navigateFromCellInDirection( tableCell, direction ) {
+		const model = this.editor.model;
+
+		const table = findAncestor( 'table', tableCell );
+		const tableMap = [ ...new TableWalker( table, { includeSpanned: true } ) ];
+		const { row: lastRow, column: lastColumn } = tableMap[ tableMap.length - 1 ];
+
+		const currentCellInfo = tableMap.find( ( { cell } ) => cell == tableCell );
+		let { row, column } = currentCellInfo;
+
+		switch ( direction ) {
+			case 'left':
+				column--;
+				break;
+
+			case 'up':
+				row--;
+				break;
+
+			case 'right':
+				column += currentCellInfo.colspan || 1;
+				break;
+
+			case 'down':
+				row += currentCellInfo.rowspan || 1;
+				break;
+		}
+
+		const isOutsideVertically = row < 0 || row > lastRow;
+		const isBeforeFirstCell = column < 0 && row <= 0;
+		const isAfterLastCell = column > lastColumn && row >= lastRow;
+
+		// Note that if the last table cell is row-spanned then isAfterLastCell will never be true but we don't know
+		// if user was navigating on the last row or not, so let's allow him to stay in the table.
+
+		if ( isOutsideVertically || isBeforeFirstCell || isAfterLastCell ) {
+			model.change( writer => {
+				writer.setSelection( writer.createRangeOn( table ) );
+			} );
+
+			return;
+		}
+
+		if ( column < 0 ) {
+			column = lastColumn;
+			row--;
+		} else if ( column > lastColumn ) {
+			column = 0;
+			row++;
+		}
+
+		const cellToFocus = tableMap.find( cellInfo => cellInfo.row == row && cellInfo.column == column ).cell;
+		const rangeToFocus = model.createRangeIn( cellToFocus );
+		const positionToFocus = [ 'left', 'up' ].includes( direction ) ? rangeToFocus.end : rangeToFocus.start;
+
+		model.change( writer => {
+			writer.setSelection( positionToFocus );
+		} );
+	}
+}
+
+function isSelectionAtEdge( cellRange, selection, direction ) {
+	switch ( direction ) {
+		case 'left': return selection.isCollapsed && selection.focus.isTouching( cellRange.start );
+		case 'right': return selection.isCollapsed && selection.focus.isTouching( cellRange.end );
+		case 'up': return selection.focus.isTouching( cellRange.start );
+		case 'down': return selection.focus.isTouching( cellRange.end );
+	}
+
+	return false;
+}
+
+// Returns `Rect` of first or last line of the `range`.
+//
+// Note: This is not handling RTL languages.
+//
+// @private
+// @param {module:core/editor/editor~Editor} editor The editor instance.
+// @param {module:engine/model/range~Range} range Range of model elements.
+// @param {Boolean} findFirstLine Whether should find `Rect` of first or last line.
+// @returns {module:utils/dom/rect~Rect}
+function getRangeLimitLineRect( editor, range, findFirstLine ) {
+	const editing = editor.editing;
+	const domConverter = editing.view.domConverter;
+
+	const viewCellRange = editing.mapper.toViewRange( range );
+	const domRange = domConverter.viewRangeToDom( viewCellRange );
+	const cellRangeRects = Rect.getDomRangeRects( domRange );
+
+	const lineRect = {
+		left: Number.POSITIVE_INFINITY,
+		top: Number.POSITIVE_INFINITY,
+		right: Number.NEGATIVE_INFINITY,
+		bottom: Number.NEGATIVE_INFINITY
+	};
+
+	for ( let i = 0; i < cellRangeRects.length; i++ ) {
+		const idx = findFirstLine ? i : cellRangeRects.length - i - 1;
+		const rect = cellRangeRects[ idx ];
+
+		const nextRect = cellRangeRects.find( ( rect, i ) => i > idx && rect.width > 0 );
+
+		// First let's skip container Rects.
+		if ( nextRect && rect.contains( nextRect ) ) {
+			continue;
+		}
+
+		// Let's check if this rect is in new line.
+		if ( findFirstLine ) {
+			if (
+				Math.round( rect.left ) < Math.round( lineRect.right ) ||
+				Math.round( rect.left ) == Math.round( lineRect.right ) &&
+				Math.round( rect.top ) >= Math.round( lineRect.bottom )
+			) {
+				break;
+			}
+		} else {
+			if (
+				Math.round( lineRect.left ) < Math.round( rect.right ) ||
+				Math.round( lineRect.left ) == Math.round( rect.right ) &&
+				Math.round( lineRect.top ) >= Math.round( rect.bottom )
+			) {
+				break;
+			}
+		}
+
+		lineRect.left = Math.min( lineRect.left, rect.left );
+		lineRect.top = Math.min( lineRect.top, rect.top );
+		lineRect.right = Math.max( lineRect.right, rect.right );
+		lineRect.bottom = Math.max( lineRect.bottom, rect.bottom );
+	}
+
+	return new Rect( {
+		...lineRect,
+		width: lineRect.right - lineRect.left,
+		height: lineRect.bottom - lineRect.top
+	} );
+}
+
+function createRangeFromPositionAndLength( model, position, length ) {
+	const newOffset = position.offset + length;
+
+	if ( newOffset < 0 || newOffset > position.parent.maxOffset ) {
+		return null;
+	}
+
+	const otherPosition = Object.assign( position.clone(), { offset: newOffset } );
+
+	if ( position.isBefore( otherPosition ) ) {
+		return model.createRange( position, otherPosition );
+	}
+
+	return model.createRange( otherPosition, position );
+}
+
+function mapModelRangeToViewRect( editing, modelRange ) {
+	const domConverter = editing.view.domConverter;
+
+	const viewRange = editing.mapper.toViewRange( modelRange );
+	const domRange = domConverter.viewRangeToDom( viewRange );
+
+	return Rect.getDomRangeRects( domRange ).pop();
+}