Procházet zdrojové kódy

Added handling of navigation next/on object elements and additional tests.

Kuba Niegowski před 5 roky
rodič
revize
bde665eb25

+ 49 - 9
packages/ckeditor5-table/src/tablenavigation.js

@@ -213,6 +213,19 @@ export default class TableNavigation extends Plugin {
 			return true;
 		}
 
+		// If this is an object selected and it's not at the start or the end of cell content
+		// then let's allow widget handler to take care of it.
+		const objectElement = selection.getSelectedElement();
+
+		if ( objectElement && model.schema.isObject( objectElement ) ) {
+			return;
+		}
+
+		// If next to the selection there is an object then this is not the cell boundary (widget handler should handle this).
+		if ( this._getObjectElementNextToSelection( selection, isForward ) ) {
+			return;
+		}
+
 		// 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 );
 
@@ -269,6 +282,31 @@ export default class TableNavigation extends Plugin {
 		return focus.isEqual( probe.focus );
 	}
 
+	/**
+	 * Checks if {@link module:engine/model/element~Element element} placed next to the current
+	 * {@link module:engine/model/selection~Selection model selection} exists and is marked in
+	 * {@link module:engine/model/schema~Schema schema} as `object`.
+	 *
+	 * @private
+	 * @param {module:engine/model/selection~Selection} modelSelection The selection.
+	 * @param {Boolean} forward Direction of checking.
+	 * @returns {module:engine/model/element~Element|null}
+	 */
+	_getObjectElementNextToSelection( modelSelection, forward ) {
+		const model = this.editor.model;
+		const schema = model.schema;
+
+		const probe = model.createSelection( modelSelection );
+		model.modifySelection( probe, { direction: forward ? 'forward' : 'backward' } );
+		const objectElement = forward ? probe.focus.nodeBefore : probe.focus.nodeAfter;
+
+		if ( !!objectElement && schema.isObject( objectElement ) ) {
+			return objectElement;
+		}
+
+		return null;
+	}
+
 	/**
 	 * 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).
@@ -307,7 +345,7 @@ export default class TableNavigation extends Plugin {
 	 *
 	 * @param {module:engine/model/range~Range} boundaries The range to find position in.
 	 * @param {'forward'|'backward'} direction Search direction.
-	 * @returns {module:engine/model/position~Position|null} Nearest selection range or `null` if one cannot be found.
+	 * @returns {module:engine/model/position~Position} Nearest selection range.
 	 */
 	_getNearestVisibleTextPosition( boundaries, direction ) {
 		const schema = this.editor.model.schema;
@@ -326,8 +364,6 @@ export default class TableNavigation extends Plugin {
 				}
 			}
 		}
-
-		return null;
 	}
 
 	/**
@@ -339,6 +375,7 @@ export default class TableNavigation extends Plugin {
 	 * @returns {Boolean}
 	 */
 	_isSingleLineRange( modelRange, isForward ) {
+		const model = this.editor.model;
 		const editing = this.editor.editing;
 		const domConverter = editing.view.domConverter;
 
@@ -346,13 +383,16 @@ export default class TableNavigation extends Plugin {
 		// 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.
-		if ( isForward && !modelRange.start.isAtEnd ) {
-			const shiftedPosition = modelRange.start.getShiftedBy( 1 );
+		if ( isForward ) {
+			const probe = model.createSelection( modelRange.start );
+
+			model.modifySelection( probe );
 
-			// If shifted position is at the end of the parent element then we can't skip it
-			// because we could detect a next paragraph as single-line.
-			if ( !shiftedPosition.isAtEnd ) {
-				modelRange = new ModelRange( shiftedPosition, modelRange.end );
+			// If the new position is at the end of the container then we can't use this position
+			// because it would provide incorrect result for eg caption of image and selection
+			// just before end of it. Also in this case there is no "dual" position.
+			if ( !probe.focus.isAtEnd && !modelRange.start.isEqual( probe.focus ) ) {
+				modelRange = new ModelRange( probe.focus, modelRange.end );
 			}
 		}
 

+ 180 - 3
packages/ckeditor5-table/tests/tablenavigation.js

@@ -1599,15 +1599,13 @@ describe( 'TableNavigation', () => {
 				} );
 
 				describe( 'two horizontal lines as only cell content (widget without $text positions)', () => {
-					beforeEach( () => {
+					it( 'should navigate to the cell on the left', () => {
 						setModelData( model, modelTable( [
 							[ '00', '01', '02' ],
 							[ '10', '[<horizontalLine></horizontalLine>]<horizontalLine></horizontalLine>', '12' ],
 							[ '20', '21', '22' ]
 						] ) );
-					} );
 
-					it( 'should navigate to the cell on the left', () => {
 						editor.editing.view.document.fire( 'keydown', leftArrowDomEvtDataStub );
 
 						sinon.assert.calledOnce( leftArrowDomEvtDataStub.preventDefault );
@@ -1621,6 +1619,12 @@ describe( 'TableNavigation', () => {
 					} );
 
 					it( 'should navigate to the cell on the right', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', '<horizontalLine></horizontalLine>[<horizontalLine></horizontalLine>]', '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
 						editor.editing.view.document.fire( 'keydown', rightArrowDomEvtDataStub );
 
 						sinon.assert.calledOnce( rightArrowDomEvtDataStub.preventDefault );
@@ -1634,6 +1638,12 @@ describe( 'TableNavigation', () => {
 					} );
 
 					it( 'should navigate to the cell above', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', '[<horizontalLine></horizontalLine>]<horizontalLine></horizontalLine>', '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
 						editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub );
 
 						sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault );
@@ -1647,6 +1657,12 @@ describe( 'TableNavigation', () => {
 					} );
 
 					it( 'should navigate to the cell below', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', '<horizontalLine></horizontalLine>[<horizontalLine></horizontalLine>]', '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
 						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
 
 						sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault );
@@ -1807,6 +1823,167 @@ describe( 'TableNavigation', () => {
 					} );
 				} );
 
+				describe( 'with multiple paragraphs of text', () => {
+					const text = new Array( 100 ).fill( 0 ).map( () => 'word' ).join( ' ' );
+
+					it( 'should not navigate if caret is in the middle line of a text', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<paragraph>${ text }[]${ text }</paragraph><paragraph>foobar</paragraph>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
+						editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub );
+
+						sinon.assert.notCalled( upArrowDomEvtDataStub.preventDefault );
+						sinon.assert.notCalled( upArrowDomEvtDataStub.stopPropagation );
+					} );
+
+					it( 'should move caret to beginning of cell content if caret is in the first line of a text', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<paragraph>word[]${ text }</paragraph><paragraph>foobar</paragraph>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
+						editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub );
+
+						sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault );
+						sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation );
+
+						assertEqualMarkup( getModelData( model ), modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<paragraph>[]word${ text }</paragraph><paragraph>foobar</paragraph>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+					} );
+
+					it( 'should not move caret to end of cell content if caret is not last line of text', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<paragraph>${ text }word []word</paragraph><paragraph>foobar</paragraph>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
+						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
+
+						sinon.assert.notCalled( downArrowDomEvtDataStub.preventDefault );
+						sinon.assert.notCalled( downArrowDomEvtDataStub.stopPropagation );
+					} );
+
+					it( 'should move caret to end of cell content if caret is in the last line of a text', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<paragraph>foobar</paragraph><paragraph>${ text }word []word</paragraph>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
+						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
+
+						sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault );
+						sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation );
+
+						assertEqualMarkup( getModelData( model ), modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<paragraph>foobar</paragraph><paragraph>${ text }word word[]</paragraph>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+					} );
+				} );
+
+				describe( 'with horizontal line widget', () => {
+					const text = new Array( 100 ).fill( 0 ).map( () => 'word' ).join( ' ' );
+
+					it( 'should not navigate if caret is in the middle line of a text', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<horizontalLine></horizontalLine><paragraph>${ text }[]${ text }</paragraph>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
+						editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub );
+
+						sinon.assert.notCalled( upArrowDomEvtDataStub.preventDefault );
+						sinon.assert.notCalled( upArrowDomEvtDataStub.stopPropagation );
+					} );
+
+					it( 'should move caret to beginning of cell content if caret is in the first line of a text', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<horizontalLine></horizontalLine><paragraph>word[] ${ text }</paragraph>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
+						editor.editing.view.document.fire( 'keydown', upArrowDomEvtDataStub );
+
+						sinon.assert.calledOnce( upArrowDomEvtDataStub.preventDefault );
+						sinon.assert.calledOnce( upArrowDomEvtDataStub.stopPropagation );
+
+						assertEqualMarkup( getModelData( model ), modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `[<horizontalLine></horizontalLine>]<paragraph>word ${ text }</paragraph>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+					} );
+
+					it( 'should move caret to end of cell content if caret is in the last line of a text', () => {
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<paragraph>${ text } word []word</paragraph><horizontalLine></horizontalLine>`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
+						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
+
+						sinon.assert.calledOnce( downArrowDomEvtDataStub.preventDefault );
+						sinon.assert.calledOnce( downArrowDomEvtDataStub.stopPropagation );
+
+						assertEqualMarkup( getModelData( model ), modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `<paragraph>${ text } word word</paragraph>[<horizontalLine></horizontalLine>]`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+					} );
+
+					it( 'should not move caret to end of cell content if widget is selected in middle of cell content', () => {
+						const paragraph = `<paragraph>${ text }</paragraph>`;
+						const hr = '<horizontalLine></horizontalLine>';
+
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `${ paragraph }[${ hr }]${ paragraph }${ hr }`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
+						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
+
+						assertEqualMarkup( getModelData( model ), modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `${ paragraph }${ hr }<paragraph>[]${ text }</paragraph>${ hr }`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+					} );
+
+					it( 'should not move caret to end of cell content if widget is next to selection', () => {
+						const paragraph = `<paragraph>${ text }</paragraph>`;
+						const hr = '<horizontalLine></horizontalLine>';
+
+						setModelData( model, modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `${ paragraph }[]${ hr }`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+
+						editor.editing.view.document.fire( 'keydown', downArrowDomEvtDataStub );
+
+						assertEqualMarkup( getModelData( model ), modelTable( [
+							[ '00', '01', '02' ],
+							[ '10', `${ paragraph }[${ hr }]`, '12' ],
+							[ '20', '21', '22' ]
+						] ) );
+					} );
+				} );
+
 				describe( 'contains image widget with caption and selection inside the caption', () => {
 					it( 'should not navigate to the cell on the left', () => {
 						setModelData( model, modelTable( [