|
|
@@ -10,6 +10,12 @@ import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
|
|
|
* @module widget/verticalnavigationhandler
|
|
|
*/
|
|
|
|
|
|
+/**
|
|
|
+ * Returns 'keydown' handler for up/down arrow keys that modifies the caret movement if it's in a text line next to an object.
|
|
|
+ *
|
|
|
+ * @param {module:engine/controller/editingcontroller~EditingController} editing The editing controller.
|
|
|
+ * @returns {Function}
|
|
|
+ */
|
|
|
export default function verticalNavigationHandler( editing ) {
|
|
|
const model = editing.model;
|
|
|
|
|
|
@@ -23,13 +29,18 @@ export default function verticalNavigationHandler( editing ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const range = findTextRangeFromSelection( model, editing.mapper, selection, arrowDownPressed );
|
|
|
+ // Find a range between selection and closest limit element.
|
|
|
+ const range = findTextRangeFromSelection( editing, selection, arrowDownPressed );
|
|
|
|
|
|
if ( !range || range.start.isTouching( range.end ) ) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if ( isSingleLineRange( model, editing, range, arrowDownPressed ) ) {
|
|
|
+ // If the range is a single line (there is no word wrapping) then move the selection to the position closest to the limit element.
|
|
|
+ //
|
|
|
+ // We can't move the selection directly to the isObject element (eg. table 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).
|
|
|
+ if ( isSingleLineRange( editing, range, arrowDownPressed ) ) {
|
|
|
model.change( writer => {
|
|
|
const newPosition = arrowDownPressed ? range.end : range.start;
|
|
|
|
|
|
@@ -50,15 +61,25 @@ export default function verticalNavigationHandler( editing ) {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
-function findTextRangeFromSelection( model, mapper, selection, isForward ) {
|
|
|
- const schema = model.schema;
|
|
|
+// Finds the range between selection and closest limit element (in the direction of navigation).
|
|
|
+// The position next to limit element is adjusted to the closest allowed `$text` position.
|
|
|
+//
|
|
|
+// Returns `null` if, according to the schema, the resulting range cannot contain a `$text` element.
|
|
|
+//
|
|
|
+// @param {module:engine/controller/editingcontroller~EditingController} editing The editing controller.
|
|
|
+// @param {module:engine/model/selection~Selection} selection The current selection.
|
|
|
+// @param {Boolean} isForward The expected navigation direction.
|
|
|
+// @returns {module:engine/model/range~Range|null}
|
|
|
+//
|
|
|
+function findTextRangeFromSelection( editing, selection, isForward ) {
|
|
|
+ const model = editing.model;
|
|
|
|
|
|
if ( isForward ) {
|
|
|
const startPosition = selection.isCollapsed ? selection.focus : selection.getLastPosition();
|
|
|
const endPosition = getNearestNonInlineLimit( model, startPosition, 'forward' );
|
|
|
|
|
|
const range = model.createRange( startPosition, endPosition );
|
|
|
- const lastRangePosition = getNearestVisibleTextPosition( schema, mapper, range, 'backward' );
|
|
|
+ const lastRangePosition = getNearestVisibleTextPosition( editing, range, 'backward' );
|
|
|
|
|
|
if ( lastRangePosition && startPosition.isBefore( lastRangePosition ) ) {
|
|
|
return model.createRange( startPosition, lastRangePosition );
|
|
|
@@ -70,7 +91,7 @@ function findTextRangeFromSelection( model, mapper, selection, isForward ) {
|
|
|
const startPosition = getNearestNonInlineLimit( model, endPosition, 'backward' );
|
|
|
|
|
|
const range = model.createRange( startPosition, endPosition );
|
|
|
- const firstRangePosition = getNearestVisibleTextPosition( schema, mapper, range, 'forward' );
|
|
|
+ const firstRangePosition = getNearestVisibleTextPosition( editing, range, 'forward' );
|
|
|
|
|
|
if ( firstRangePosition && endPosition.isAfter( firstRangePosition ) ) {
|
|
|
return model.createRange( firstRangePosition, endPosition );
|
|
|
@@ -80,6 +101,13 @@ function findTextRangeFromSelection( model, mapper, selection, isForward ) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// Finds the limit element position that is closest to startPosition.
|
|
|
+//
|
|
|
+// @param {module:engine/model/model~Model} model
|
|
|
+// @param {<module:engine/model/position~Position>} startPosition
|
|
|
+// @param {'forward'|'backward'} direction Search direction.
|
|
|
+// @returns {<module:engine/model/position~Position>}
|
|
|
+//
|
|
|
function getNearestNonInlineLimit( model, startPosition, direction ) {
|
|
|
const schema = model.schema;
|
|
|
const range = model.createRangeIn( startPosition.root );
|
|
|
@@ -93,7 +121,18 @@ function getNearestNonInlineLimit( model, startPosition, direction ) {
|
|
|
return direction == 'forward' ? range.end : range.start;
|
|
|
}
|
|
|
|
|
|
-function getNearestVisibleTextPosition( schema, mapper, range, direction ) {
|
|
|
+// Basing on the provided range, finds the first or last (depending on `direction`) position inside the range
|
|
|
+// that can contain `$text` (according to schema) and is visible in the view.
|
|
|
+//
|
|
|
+// @param {module:engine/controller/editingcontroller~EditingController} editing The editing controller.
|
|
|
+// @param {module:engine/model/range~Range} range The range to find the position in.
|
|
|
+// @param {'forward'|'backward'} direction Search direction.
|
|
|
+// @returns {module:engine/model/position~Position} The nearest selection range.
|
|
|
+//
|
|
|
+function getNearestVisibleTextPosition( editing, range, direction ) {
|
|
|
+ const schema = editing.model.schema;
|
|
|
+ const mapper = editing.mapper;
|
|
|
+
|
|
|
const position = direction == 'backward' ? range.end : range.start;
|
|
|
|
|
|
if ( schema.checkChild( position, '$text' ) ) {
|
|
|
@@ -111,7 +150,16 @@ function getNearestVisibleTextPosition( schema, mapper, range, direction ) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function isSingleLineRange( model, editing, modelRange, isForward ) {
|
|
|
+// Checks if the DOM range corresponding to the provided model range renders as a single line by analyzing DOMRects
|
|
|
+// (verifying if they visually wrap content to the next line).
|
|
|
+//
|
|
|
+// @param {module:engine/controller/editingcontroller~EditingController} editing The editing controller.
|
|
|
+// @param {module:engine/model/range~Range} modelRange The current table cell content range.
|
|
|
+// @param {Boolean} isForward The expected navigation direction.
|
|
|
+// @returns {Boolean}
|
|
|
+//
|
|
|
+function isSingleLineRange( editing, modelRange, isForward ) {
|
|
|
+ const model = editing.model;
|
|
|
const domConverter = editing.view.domConverter;
|
|
|
|
|
|
// Wrapped lines contain exactly the same position at the end of current line
|