ソースを参照

Using `modifySelection` to find navigable selection positions.

Kuba Niegowski 5 年 前
コミット
9901707a70
1 ファイル変更63 行追加45 行削除
  1. 63 45
      packages/ckeditor5-table/src/tablenavigation.js

+ 63 - 45
packages/ckeditor5-table/src/tablenavigation.js

@@ -184,21 +184,21 @@ export default class TableNavigation extends Plugin {
 		const selection = model.document.selection;
 		const isForward = [ 'right', 'down' ].includes( direction );
 
-		// At first let's check if there are some cells that are fully selected (from the outside).
+		// In case one or more table cells are selected (from outside),
+		// move the selection to a cell adjacent to the selected table fragment.
 		const selectedCells = getSelectedTableCells( selection );
 
 		if ( selectedCells.length ) {
-			const tableCell = isForward ? selectedCells.pop() : selectedCells[ 0 ];
+			const tableCell = isForward ? selectedCells[ selectedCells.length - 1 ] : selectedCells[ 0 ];
 
 			this._navigateFromCellInDirection( tableCell, direction );
 
 			return true;
 		}
 
-		// So we fall back to selection inside the table cell.
+		// Abort if we're not in a table.
 		const tableCell = findAncestor( 'tableCell', selection.focus );
 
-		// But the selection is outside the table.
 		if ( !tableCell ) {
 			return;
 		}
@@ -206,17 +206,13 @@ export default class TableNavigation extends Plugin {
 		const cellRange = model.createRangeIn( tableCell );
 
 		// Let's check if the selection is at the beginning/end of the cell.
-		if ( isSelectionAtCellEdge( cellRange, selection, direction ) ) {
+		if ( this._isSelectionAtCellEdge( cellRange, selection, isForward ) ) {
 			this._navigateFromCellInDirection( tableCell, direction );
 
 			return true;
 		}
 
-		// 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, or if an object element is selected at beginning/end of range.
-		//
-		// We try to find a nearest text position that is not before/after the selection. If there is no
-		// such range, there is some object element at the beginning/end of range.
+		// If there isn't any $text position between cell edge and selection then we shall move the selection to next cell.
 		const textRange = this._findTextRangeFromSelection( cellRange, selection, isForward );
 
 		if ( !textRange ) {
@@ -225,14 +221,16 @@ export default class TableNavigation extends Plugin {
 			return true;
 		}
 
-		// There wasn't any object element so let's check if range is a single line. If it's a single line
-		// then move selection to the beginning/end of a cell content.
+		// If the navigation is horizontal then we have no more custom cases.
+		if ( [ 'left', 'right' ].includes( direction ) ) {
+			return;
+		}
+
+		// If the range is a single line then move the selection to the beginning/end of a cell content.
 		//
-		// We can't move the selection directly to other cell because of dual position at the end/beginning
+		// We can't move the selection directly to the another cell because of dual position at the end/beginning
 		// of wrapped line (it's at the same time at the end of one line and at the start of the next line).
-		const isVertical = [ 'up', 'down' ].includes( direction );
-
-		if ( isVertical && this._isSingleLineRange( textRange, isForward ) ) {
+		if ( this._isSingleLineRange( textRange, isForward ) ) {
 			model.change( writer => {
 				writer.setSelection( isForward ? cellRange.end : cellRange.start );
 			} );
@@ -242,8 +240,37 @@ export default class TableNavigation extends Plugin {
 	}
 
 	/**
-	 * Returns a range from beginning/end of `range` up to `selection` or `null` if resulting range can't contain
-	 * text element (according to schema).
+	 * Returns `true` if `selection` is at `cellRange` edge according to navigation `direction`.
+	 *
+	 * @private
+	 * @param {module:engine/model/range~Range} cellRange The bounding cell range.
+	 * @param {module:engine/model/selection~Selection} selection The current selection.
+	 * @param {Boolean} isForward The expected navigation direction.
+	 * @returns {Boolean}
+	 */
+	_isSelectionAtCellEdge( cellRange, selection, isForward ) {
+		const model = this.editor.model;
+		const schema = this.editor.model.schema;
+
+		const focus = isForward ? selection.getLastPosition() : selection.getFirstPosition();
+
+		// If the current limit element is not table cell we are for sure not at the cell edge.
+		// Also `modifySelection` will not let us out of it.
+		if ( !schema.getLimitElement( focus ).is( 'tableCell' ) ) {
+			return false;
+		}
+
+		const probe = model.createSelection( focus );
+
+		model.modifySelection( probe, { direction: isForward ? 'forward' : 'backward' } );
+
+		// If there was no change in the focus position, then it's not possible to move the selection there.
+		return focus.isEqual( probe.focus );
+	}
+
+	/**
+	 * Returns a range from beginning/end of range up to selection closest position.
+	 * Returns `null` if resulting range can't contain text element (according to schema).
 	 *
 	 * @private
 	 * @param {module:engine/model/range~Range} range Current table cell content range.
@@ -260,11 +287,7 @@ export default class TableNavigation extends Plugin {
 				return null;
 			}
 
-			// Wrapped lines contain exactly the same position at the end of current line
-			// and at the beginning of next line. That position's client rect is at the end
-			// of current line. In case of caret at first position of the last line that 'dual'
-			// position would be detected as it's not the last line.
-			return new ModelRange( position.isAtEnd ? position : position.getShiftedBy( 1 ), lastRangePosition );
+			return new ModelRange( position, lastRangePosition );
 		} else {
 			const position = selection.getFirstPosition();
 			const firstRangePosition = this._getNearestTextPosition( range, 'forward' );
@@ -301,7 +324,7 @@ export default class TableNavigation extends Plugin {
 	}
 
 	/**
-	 * Checks if `modelRange` is a single line.
+	 * Checks if the `modelRange` renders to a single line in the DOM.
 	 *
 	 * @private
 	 * @param {module:engine/model/range~Range} modelRange Current table cell content range.
@@ -312,11 +335,19 @@ export default class TableNavigation extends Plugin {
 		const editing = this.editor.editing;
 		const domConverter = editing.view.domConverter;
 
+		if ( isForward && !modelRange.start.isAtEnd ) {
+			// Wrapped lines contain exactly the same position at the end of current line
+			// and at the beginning of next line. That position's client rect is at the end
+			// of current line. In case of caret at first position of the last line that 'dual'
+			// position would be detected as it's not the last line.
+			modelRange = new ModelRange( modelRange.start.getShiftedBy( 1 ), modelRange.end );
+		}
+
 		const viewRange = editing.mapper.toViewRange( modelRange );
 		const domRange = domConverter.viewRangeToDom( viewRange );
 		const rects = Rect.getDomRangeRects( domRange );
 
-		let boundaryVerticalPosition = undefined;
+		let boundaryVerticalPosition;
 
 		for ( let i = 0; i < rects.length; i++ ) {
 			const idx = isForward ? rects.length - i - 1 : i;
@@ -354,7 +385,7 @@ export default class TableNavigation extends Plugin {
 	}
 
 	/**
-	 * Moves the selection from given `tableCell` in the specified `direction`.
+	 * Moves the selection from the given table cell in the specified direction.
 	 *
 	 * @private
 	 * @param {module:engine/model/element~Element} tableCell The table cell to start the selection navigation.
@@ -411,18 +442,19 @@ export default class TableNavigation extends Plugin {
 			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;
+		const cellToSelect = tableMap.find( cellInfo => cellInfo.row == row && cellInfo.column == column ).cell;
+		const isForward = [ 'right', 'down' ].includes( direction );
+		const positionToSelect = model.createPositionAt( cellToSelect, isForward ? 0 : 'end' );
 
 		model.change( writer => {
-			writer.setSelection( positionToFocus );
+			writer.setSelection( positionToSelect );
 		} );
 	}
 }
 
 // Returns 'true' if provided key code represents one of the arrow keys.
 //
+// @private
 // @param {Number} keyCode
 // @returns {Boolean}
 function isArrowKeyCode( keyCode ) {
@@ -434,6 +466,7 @@ function isArrowKeyCode( keyCode ) {
 
 // Returns direction name from `keyCode`.
 //
+// @private
 // @param {Number} keyCode
 // @param {Boolean} isLtrContent The content language direction.
 // @returns {'left'|'up'|'right'|'down'} Arrow direction.
@@ -445,18 +478,3 @@ function getDirectionFromKeyCode( keyCode, isLtrContent ) {
 		case keyCodes.arrowdown: return 'down';
 	}
 }
-
-// Returns `true` if `selection` is at `cellRange` edge according to navigation `direction`.
-//
-// @param {module:engine/model/range~Range} cellRange The bounding cell range.
-// @param {module:engine/model/selection~Selection} selection The current selection.
-// @param {'left'|'up'|'right'|'down'} direction The expected navigation direction.
-// @returns {Boolean}
-function isSelectionAtCellEdge( 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 );
-	}
-}