Ver código fonte

Merge pull request #1450 from ckeditor/t/1436

Fix: Improved selection post-fixing mechanism for selections which cross limit element boundaries. Closes #1436.

Feature: The `schema.getLimitElement()` method now accepts also `Range` and `Position` as a parameter.
Piotrek Koszuliński 7 anos atrás
pai
commit
a38208ed2b

+ 24 - 12
packages/ckeditor5-engine/src/model/schema.js

@@ -582,26 +582,38 @@ export default class Schema {
 		}, { priority: 'high' } );
 		}, { priority: 'high' } );
 	}
 	}
 
 
+	/* eslint-disable max-len */
 	/**
 	/**
 	 * Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
 	 * Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
 	 * selection or the root otherwise.
 	 * selection or the root otherwise.
 	 *
 	 *
-	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|module:engine/model/range~Range|module:engine/model/position~Position} selectionOrRangeOrPosition
 	 * Selection which returns the common ancestor.
 	 * Selection which returns the common ancestor.
 	 * @returns {module:engine/model/element~Element}
 	 * @returns {module:engine/model/element~Element}
 	 */
 	 */
-	getLimitElement( selection ) {
-		// Find the common ancestor for all selection's ranges.
-		let element = Array.from( selection.getRanges() )
-			.reduce( ( element, range ) => {
-				const rangeCommonAncestor = range.getCommonAncestor();
-
-				if ( !element ) {
-					return rangeCommonAncestor;
-				}
+	/* eslint-enable max-len */
+	getLimitElement( selectionOrRangeOrPosition ) {
+		let element;
+
+		if ( selectionOrRangeOrPosition instanceof Position ) {
+			element = selectionOrRangeOrPosition.parent;
+		} else {
+			const ranges = selectionOrRangeOrPosition instanceof Range ?
+				[ selectionOrRangeOrPosition ] :
+				Array.from( selectionOrRangeOrPosition.getRanges() );
 
 
-				return element.getCommonAncestor( rangeCommonAncestor, { includeSelf: true } );
-			}, null );
+			// Find the common ancestor for all selection's ranges.
+			element = ranges
+				.reduce( ( element, range ) => {
+					const rangeCommonAncestor = range.getCommonAncestor();
+
+					if ( !element ) {
+						return rangeCommonAncestor;
+					}
+
+					return element.getCommonAncestor( rangeCommonAncestor, { includeSelf: true } );
+				}, null );
+		}
 
 
 		while ( !this.isLimit( element ) ) {
 		while ( !this.isLimit( element ) ) {
 			if ( element.parent ) {
 			if ( element.parent ) {

+ 61 - 54
packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js

@@ -94,9 +94,18 @@ function selectionPostFixer( writer, model ) {
 	if ( wasFixed ) {
 	if ( wasFixed ) {
 		// The above algorithm might create ranges that intersects each other when selection contains more then one range.
 		// The above algorithm might create ranges that intersects each other when selection contains more then one range.
 		// This is case happens mostly on Firefox which creates multiple ranges for selected table.
 		// This is case happens mostly on Firefox which creates multiple ranges for selected table.
-		const combinedRanges = combineOverlapingRanges( ranges );
+		let fixedRanges = ranges;
 
 
-		writer.setSelection( combinedRanges, { backward: selection.isBackward } );
+		// Fixing selection with many ranges usually breaks the selection in Firefox. As only Firefox supports multiple selection ranges
+		// we simply create one continuous range from fixed selection ranges (even if they are not adjacent).
+		if ( ranges.length > 1 ) {
+			const selectionStart = ranges[ 0 ].start;
+			const selectionEnd = ranges[ ranges.length - 1 ].end;
+
+			fixedRanges = [ new Range( selectionStart, selectionEnd ) ];
+		}
+
+		writer.setSelection( fixedRanges, { backward: selection.isBackward } );
 	}
 	}
 }
 }
 
 
@@ -110,7 +119,7 @@ function tryFixingRange( range, schema ) {
 		return tryFixingCollapsedRange( range, schema );
 		return tryFixingCollapsedRange( range, schema );
 	}
 	}
 
 
-	return tryFixingNonCollpasedRage( range, schema );
+	return tryFixingNonCollapsedRage( range, schema );
 }
 }
 
 
 // Tries to fix collapsed ranges.
 // Tries to fix collapsed ranges.
@@ -146,27 +155,57 @@ function tryFixingCollapsedRange( range, schema ) {
 	return new Range( fixedPosition );
 	return new Range( fixedPosition );
 }
 }
 
 
-// Tries to fix a expanded range that overlaps limit nodes.
+// Tries to fix an expanded range.
 //
 //
 // @param {module:engine/model/range~Range} range Expanded range to fix.
 // @param {module:engine/model/range~Range} range Expanded range to fix.
 // @param {module:engine/model/schema~Schema} schema
 // @param {module:engine/model/schema~Schema} schema
 // @returns {module:engine/model/range~Range|null} Returns fixed range or null if range is valid.
 // @returns {module:engine/model/range~Range|null} Returns fixed range or null if range is valid.
-function tryFixingNonCollpasedRage( range, schema ) {
-	// No need to check flat ranges as they will not cross node boundary.
-	if ( range.isFlat ) {
-		return null;
-	}
-
+function tryFixingNonCollapsedRage( range, schema ) {
 	const start = range.start;
 	const start = range.start;
 	const end = range.end;
 	const end = range.end;
 
 
-	const updatedStart = expandSelectionOnIsLimitNode( start, schema, 'start' );
-	const updatedEnd = expandSelectionOnIsLimitNode( end, schema, 'end' );
+	const isTextAllowedOnStart = schema.checkChild( start, '$text' );
+	const isTextAllowedOnEnd = schema.checkChild( end, '$text' );
+
+	const startLimitElement = schema.getLimitElement( start );
+	const endLimitElement = schema.getLimitElement( end );
+
+	// Ranges which both end are inside the same limit element (or root) might needs only minor fix.
+	if ( startLimitElement === endLimitElement ) {
+		// Range is valid when both position allows to place a text:
+		// - <block>f[oobarba]z</block>
+		// This would be "fixed" by a next check but as it will be the same it's better to return null so the selection stays the same.
+		if ( isTextAllowedOnStart && isTextAllowedOnEnd ) {
+			return null;
+		}
+
+		// Range that is on non-limit element (or is partially) must be fixed so it is placed inside the block around $text:
+		// - [<block>foo</block>]    ->    <block>[foo]</block>
+		// - [<block>foo]</block>    ->    <block>[foo]</block>
+		// - <block>f[oo</block>]    ->    <block>f[oo]</block>
+		if ( checkSelectionOnNonLimitElements( start, end, schema ) ) {
+			const fixedStart = schema.getNearestSelectionRange( start, 'forward' );
+			const fixedEnd = schema.getNearestSelectionRange( end, 'backward' );
+
+			return new Range( fixedStart ? fixedStart.start : start, fixedEnd ? fixedEnd.start : end );
+		}
+	}
+
+	const isStartInLimit = startLimitElement && !startLimitElement.is( 'rootElement' );
+	const isEndInLimit = endLimitElement && !endLimitElement.is( 'rootElement' );
+
+	// At this point we eliminated valid positions on text nodes so if one of range positions is placed inside a limit element
+	// then the range crossed limit element boundaries and needs to be fixed.
+	if ( isStartInLimit || isEndInLimit ) {
+		// Although we've already found limit element on start/end positions we must find the outer-most limit element.
+		// as limit elements might be nested directly inside (ie table > tableRow > tableCell).
+		const fixedStart = isStartInLimit ? expandSelectionOnIsLimitNode( Position.createAt( startLimitElement ), schema, 'start' ) : start;
+		const fixedEnd = isEndInLimit ? expandSelectionOnIsLimitNode( Position.createAt( endLimitElement ), schema, 'end' ) : end;
 
 
-	if ( !start.isEqual( updatedStart ) || !end.isEqual( updatedEnd ) ) {
-		return new Range( updatedStart, updatedEnd );
+		return new Range( fixedStart, fixedEnd );
 	}
 	}
 
 
+	// Range was not fixed at this point so it is valid - ie it was placed around limit element already.
 	return null;
 	return null;
 }
 }
 
 
@@ -186,50 +225,18 @@ function expandSelectionOnIsLimitNode( position, schema, expandToDirection ) {
 		parent = parent.parent;
 		parent = parent.parent;
 	}
 	}
 
 
-	if ( node === parent ) {
-		// If there is not is limit block the return original position.
-		return position;
-	}
-
 	// Depending on direction of expanding selection return position before or after found node.
 	// Depending on direction of expanding selection return position before or after found node.
 	return expandToDirection === 'start' ? Position.createBefore( node ) : Position.createAfter( node );
 	return expandToDirection === 'start' ? Position.createBefore( node ) : Position.createAfter( node );
 }
 }
 
 
-// Returns minimal set of continuous ranges.
+// Checks whether both range ends are placed around non-limit elements.
 //
 //
-// @param {Array.<module:engine/model/range~Range>} ranges
-// @returns {Array.<module:engine/model/range~Range>}
-function combineOverlapingRanges( ranges ) {
-	const combinedRanges = [];
-
-	// Seed the state.
-	let previousRange = ranges[ 0 ];
-	combinedRanges.push( previousRange );
-
-	// Go through each ranges and check if it can be merged with previous one.
-	for ( const range of ranges ) {
-		// Do not push same ranges (ie might be created in a table).
-		if ( range.isEqual( previousRange ) ) {
-			continue;
-		}
-
-		// Merge intersecting range into previous one.
-		if ( range.isIntersecting( previousRange ) ) {
-			const newStart = previousRange.start.isBefore( range.start ) ? previousRange.start : range.start;
-			const newEnd = range.end.isAfter( previousRange.end ) ? range.end : previousRange.end;
-			const combinedRange = new Range( newStart, newEnd );
-
-			// Replace previous range with the combined one.
-			combinedRanges.splice( combinedRanges.indexOf( previousRange ), 1, combinedRange );
-
-			previousRange = combinedRange;
-
-			continue;
-		}
-
-		previousRange = range;
-		combinedRanges.push( range );
-	}
+// @param {module:engine/model/position~Position} start
+// @param {module:engine/model/position~Position} end
+// @param {module:engine/model/schema~Schema} schema
+function checkSelectionOnNonLimitElements( start, end, schema ) {
+	const startIsOnBlock = ( start.nodeAfter && !schema.isLimit( start.nodeAfter ) ) || schema.checkChild( start, '$text' );
+	const endIsOnBlock = ( end.nodeBefore && !schema.isLimit( end.nodeBefore ) ) || schema.checkChild( end, '$text' );
 
 
-	return combinedRanges;
+	return startIsOnBlock && endIsOnBlock;
 }
 }

+ 11 - 11
packages/ckeditor5-engine/tests/conversion/downcast-selection-converters.js

@@ -494,9 +494,9 @@ describe( 'downcast-selection-converters', () => {
 
 
 	describe( 'table cell selection converter', () => {
 	describe( 'table cell selection converter', () => {
 		beforeEach( () => {
 		beforeEach( () => {
-			model.schema.register( 'table' );
-			model.schema.register( 'tr' );
-			model.schema.register( 'td' );
+			model.schema.register( 'table', { isLimit: true } );
+			model.schema.register( 'tr', { isLimit: true } );
+			model.schema.register( 'td', { isLimit: true } );
 
 
 			model.schema.extend( 'table', { allowIn: '$root' } );
 			model.schema.extend( 'table', { allowIn: '$root' } );
 			model.schema.extend( 'tr', { allowIn: 'table' } );
 			model.schema.extend( 'tr', { allowIn: 'table' } );
@@ -519,16 +519,16 @@ describe( 'downcast-selection-converters', () => {
 				}
 				}
 
 
 				for ( const range of selection.getRanges() ) {
 				for ( const range of selection.getRanges() ) {
-					const node = range.start.nodeAfter;
+					const node = range.start.parent;
 
 
-					if ( node == range.end.nodeBefore && node instanceof ModelElement && node.name == 'td' ) {
+					if ( node instanceof ModelElement && node.name == 'td' ) {
 						conversionApi.consumable.consume( selection, 'selection' );
 						conversionApi.consumable.consume( selection, 'selection' );
 
 
 						const viewNode = conversionApi.mapper.toViewElement( node );
 						const viewNode = conversionApi.mapper.toViewElement( node );
 						conversionApi.writer.addClass( 'selected', viewNode );
 						conversionApi.writer.addClass( 'selected', viewNode );
 					}
 					}
 				}
 				}
-			} );
+			}, { priority: 'high' } );
 		} );
 		} );
 
 
 		it( 'should not be used to convert selection that is not on table cell', () => {
 		it( 'should not be used to convert selection that is not on table cell', () => {
@@ -541,8 +541,8 @@ describe( 'downcast-selection-converters', () => {
 
 
 		it( 'should add a class to the selected table cell', () => {
 		it( 'should add a class to the selected table cell', () => {
 			test(
 			test(
-				// table tr#0 |td#0, table tr#0 td#0|
-				[ [ 0, 0, 0 ], [ 0, 0, 1 ] ],
+				// table tr#0 td#0 [foo, table tr#0 td#0 bar]
+				[ [ 0, 0, 0, 0 ], [ 0, 0, 0, 3 ] ],
 				'<table><tr><td>foo</td></tr><tr><td>bar</td></tr></table>',
 				'<table><tr><td>foo</td></tr><tr><td>bar</td></tr></table>',
 				'<table><tr><td class="selected">foo</td></tr><tr><td>bar</td></tr></table>'
 				'<table><tr><td class="selected">foo</td></tr><tr><td>bar</td></tr></table>'
 			);
 			);
@@ -550,10 +550,10 @@ describe( 'downcast-selection-converters', () => {
 
 
 		it( 'should not be used if selection contains more than just a table cell', () => {
 		it( 'should not be used if selection contains more than just a table cell', () => {
 			test(
 			test(
-				// table tr td#1, table tr#2
-				[ [ 0, 0, 0, 1 ], [ 0, 0, 2 ] ],
+				// table tr td#1 f{oo bar, table tr#2 bar]
+				[ [ 0, 0, 0, 1 ], [ 0, 0, 1, 3 ] ],
 				'<table><tr><td>foo</td><td>bar</td></tr></table>',
 				'<table><tr><td>foo</td><td>bar</td></tr></table>',
-				'<table><tr><td>f{oo</td><td>bar</td>]</tr></table>'
+				'[<table><tr><td>foo</td><td>bar</td></tr></table>]'
 			);
 			);
 		} );
 		} );
 	} );
 	} );

+ 44 - 6
packages/ckeditor5-engine/tests/model/schema.js

@@ -17,7 +17,7 @@ import Position from '../../src/model/position';
 import Range from '../../src/model/range';
 import Range from '../../src/model/range';
 import Selection from '../../src/model/selection';
 import Selection from '../../src/model/selection';
 
 
-import { setData, getData, stringify } from '../../src/dev-utils/model';
+import { getData, setData, stringify, parse } from '../../src/dev-utils/model';
 
 
 import AttributeDelta from '../../src/model/delta/attributedelta';
 import AttributeDelta from '../../src/model/delta/attributedelta';
 
 
@@ -983,6 +983,38 @@ describe( 'Schema', () => {
 
 
 			expect( schema.getLimitElement( doc.selection ) ).to.equal( root );
 			expect( schema.getLimitElement( doc.selection ) ).to.equal( root );
 		} );
 		} );
+
+		it( 'accepts range as an argument', () => {
+			schema.extend( 'article', { isLimit: true } );
+			schema.extend( 'section', { isLimit: true } );
+
+			const data = '<div><section><article><paragraph>foobar</paragraph></article></section></div>';
+			const parsedModel = parse( data, model.schema, { context: [ root.name ] } );
+
+			model.change( writer => {
+				writer.insert( parsedModel, root );
+			} );
+
+			const article = root.getNodeByPath( [ 0, 0, 0 ] );
+
+			expect( schema.getLimitElement( new Range( new Position( root, [ 0, 0, 0, 0, 2 ] ) ) ) ).to.equal( article );
+		} );
+
+		it( 'accepts position as an argument', () => {
+			schema.extend( 'article', { isLimit: true } );
+			schema.extend( 'section', { isLimit: true } );
+
+			const data = '<div><section><article><paragraph>foobar</paragraph></article></section></div>';
+			const parsedModel = parse( data, model.schema, { context: [ root.name ] } );
+
+			model.change( writer => {
+				writer.insert( parsedModel, root );
+			} );
+
+			const article = root.getNodeByPath( [ 0, 0, 0 ] );
+
+			expect( schema.getLimitElement( new Position( root, [ 0, 0, 0, 0, 2 ] ) ) ).to.equal( article );
+		} );
 	} );
 	} );
 
 
 	describe( 'checkAttributeInSelection()', () => {
 	describe( 'checkAttributeInSelection()', () => {
@@ -1101,7 +1133,13 @@ describe( 'Schema', () => {
 				}
 				}
 			} );
 			} );
 
 
-			setData( model, '<p>foo<img />bar</p>' );
+			// Parse data string to model.
+			const parsedModel = parse( '<p>foo<img />bar</p>', model.schema, { context: [ root.name ] } );
+
+			// Set parsed model data to prevent selection post-fixer from running.
+			model.change( writer => {
+				writer.insert( parsedModel, root );
+			} );
 
 
 			ranges = [ Range.createOn( root.getChild( 0 ) ) ];
 			ranges = [ Range.createOn( root.getChild( 0 ) ) ];
 		} );
 		} );
@@ -1123,12 +1161,12 @@ describe( 'Schema', () => {
 			schema.extend( 'img', { allowAttributes: 'bold' } );
 			schema.extend( 'img', { allowAttributes: 'bold' } );
 			schema.extend( '$text', { allowIn: 'img' } );
 			schema.extend( '$text', { allowIn: 'img' } );
 
 
-			setData( model, '[<p>foo<img>xxx</img>bar</p>]' );
+			setData( model, '<p>[foo<img>xxx</img>bar]</p>' );
 
 
 			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
 			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
 			const sel = new Selection( validRanges );
 			const sel = new Selection( validRanges );
 
 
-			expect( stringify( root, sel ) ).to.equal( '[<p>foo<img>]xxx[</img>bar</p>]' );
+			expect( stringify( root, sel ) ).to.equal( '<p>[foo<img>]xxx[</img>bar]</p>' );
 		} );
 		} );
 
 
 		it( 'should return three ranges when attribute is not allowed on one element but is allowed on its child', () => {
 		it( 'should return three ranges when attribute is not allowed on one element but is allowed on its child', () => {
@@ -1141,12 +1179,12 @@ describe( 'Schema', () => {
 				}
 				}
 			} );
 			} );
 
 
-			setData( model, '[<p>foo<img>xxx</img>bar</p>]' );
+			setData( model, '<p>[foo<img>xxx</img>bar]</p>' );
 
 
 			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
 			const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
 			const sel = new Selection( validRanges );
 			const sel = new Selection( validRanges );
 
 
-			expect( stringify( root, sel ) ).to.equal( '[<p>foo]<img>[xxx]</img>[bar</p>]' );
+			expect( stringify( root, sel ) ).to.equal( '<p>[foo]<img>[xxx]</img>[bar]</p>' );
 		} );
 		} );
 
 
 		it( 'should not leak beyond the given ranges', () => {
 		it( 'should not leak beyond the given ranges', () => {

+ 23 - 11
packages/ckeditor5-engine/tests/model/utils/deletecontent.js

@@ -562,7 +562,7 @@ describe( 'DataController utils', () => {
 				schema.register( 'image', { allowWhere: '$text' } );
 				schema.register( 'image', { allowWhere: '$text' } );
 				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 				schema.register( 'heading1', { inheritAllFrom: '$block' } );
 				schema.register( 'heading1', { inheritAllFrom: '$block' } );
-				schema.register( 'blockWidget' );
+				schema.register( 'blockWidget', { isLimit: true } );
 				schema.register( 'restrictedRoot', {
 				schema.register( 'restrictedRoot', {
 					isLimit: true
 					isLimit: true
 				} );
 				} );
@@ -608,7 +608,7 @@ describe( 'DataController utils', () => {
 			it( 'creates a paragraph when text is not allowed (custom selection)', () => {
 			it( 'creates a paragraph when text is not allowed (custom selection)', () => {
 				setData(
 				setData(
 					model,
 					model,
-					'[<paragraph>x</paragraph>]<paragraph>yyy</paragraph><paragraph>z</paragraph>',
+					'<paragraph>[x]</paragraph><paragraph>yyy</paragraph><paragraph>z</paragraph>',
 					{ rootName: 'bodyRoot' }
 					{ rootName: 'bodyRoot' }
 				);
 				);
 
 
@@ -621,7 +621,7 @@ describe( 'DataController utils', () => {
 				deleteContent( model, selection );
 				deleteContent( model, selection );
 
 
 				expect( getData( model, { rootName: 'bodyRoot' } ) )
 				expect( getData( model, { rootName: 'bodyRoot' } ) )
-					.to.equal( '[<paragraph>x</paragraph>]<paragraph></paragraph><paragraph>z</paragraph>' );
+					.to.equal( '<paragraph>[x]</paragraph><paragraph></paragraph><paragraph>z</paragraph>' );
 			} );
 			} );
 
 
 			it( 'creates a paragraph when text is not allowed (block widget selected)', () => {
 			it( 'creates a paragraph when text is not allowed (block widget selected)', () => {
@@ -640,27 +640,39 @@ describe( 'DataController utils', () => {
 			it( 'creates paragraph when text is not allowed (heading selected)', () => {
 			it( 'creates paragraph when text is not allowed (heading selected)', () => {
 				setData(
 				setData(
 					model,
 					model,
-					'<paragraph>x</paragraph>[<heading1>yyy</heading1>]<paragraph>z</paragraph>',
+					'<paragraph>x</paragraph><heading1>yyy</heading1><paragraph>z</paragraph>',
 					{ rootName: 'bodyRoot' }
 					{ rootName: 'bodyRoot' }
 				);
 				);
 
 
-				deleteContent( model, doc.selection );
+				// [<heading1>yyy</heading1>]
+				const range = new Range(
+					new Position( doc.getRoot( 'bodyRoot' ), [ 1 ] ),
+					new Position( doc.getRoot( 'bodyRoot' ), [ 2 ] )
+				);
 
 
-				expect( getData( model, { rootName: 'bodyRoot' } ) )
-					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
+				deleteContent( model, new Selection( range ) );
+
+				expect( getData( model, { rootName: 'bodyRoot', withoutSelection: true } ) )
+					.to.equal( '<paragraph>x</paragraph><paragraph></paragraph><paragraph>z</paragraph>' );
 			} );
 			} );
 
 
 			it( 'creates paragraph when text is not allowed (two blocks selected)', () => {
 			it( 'creates paragraph when text is not allowed (two blocks selected)', () => {
 				setData(
 				setData(
 					model,
 					model,
-					'<paragraph>x</paragraph>[<heading1>yyy</heading1><paragraph>yyy</paragraph>]<paragraph>z</paragraph>',
+					'<paragraph>x</paragraph><heading1>yyy</heading1><paragraph>yyy</paragraph><paragraph>z</paragraph>',
 					{ rootName: 'bodyRoot' }
 					{ rootName: 'bodyRoot' }
 				);
 				);
 
 
-				deleteContent( model, doc.selection );
+				// [<heading1>yyy</heading1><paragraph>yyy</paragraph>]
+				const range = new Range(
+					new Position( doc.getRoot( 'bodyRoot' ), [ 1 ] ),
+					new Position( doc.getRoot( 'bodyRoot' ), [ 3 ] )
+				);
 
 
-				expect( getData( model, { rootName: 'bodyRoot' } ) )
-					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
+				deleteContent( model, new Selection( range ) );
+
+				expect( getData( model, { rootName: 'bodyRoot', withoutSelection: true } ) )
+					.to.equal( '<paragraph>x</paragraph><paragraph></paragraph><paragraph>z</paragraph>' );
 			} );
 			} );
 
 
 			it( 'creates paragraph when text is not allowed (all content selected)', () => {
 			it( 'creates paragraph when text is not allowed (all content selected)', () => {

+ 1 - 1
packages/ckeditor5-engine/tests/model/utils/getselectedcontent.js

@@ -129,7 +129,7 @@ describe( 'DataController utils', () => {
 
 
 				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 				schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 				schema.register( 'heading1', { inheritAllFrom: '$block' } );
 				schema.register( 'heading1', { inheritAllFrom: '$block' } );
-				schema.register( 'blockImage' );
+				schema.register( 'blockImage', { isObject: true } );
 				schema.register( 'caption' );
 				schema.register( 'caption' );
 				schema.register( 'image', { allowWhere: '$text' } );
 				schema.register( 'image', { allowWhere: '$text' } );
 
 

+ 451 - 26
packages/ckeditor5-engine/tests/model/utils/selection-post-fixer.js

@@ -26,6 +26,7 @@ describe( 'Selection post-fixer', () => {
 			modelRoot = model.document.createRoot();
 			modelRoot = model.document.createRoot();
 
 
 			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
 			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
 			model.schema.register( 'table', {
 			model.schema.register( 'table', {
 				allowWhere: '$block',
 				allowWhere: '$block',
 				isObject: true,
 				isObject: true,
@@ -43,13 +44,26 @@ describe( 'Selection post-fixer', () => {
 				isLimit: true
 				isLimit: true
 			} );
 			} );
 
 
-			setModelData( model,
-				'<paragraph>[]foo</paragraph>' +
-				'<table>' +
-					'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
-				'</table>' +
-				'<paragraph>bar</paragraph>'
-			);
+			model.schema.register( 'image', {
+				allowIn: '$root',
+				isObject: true
+			} );
+
+			model.schema.register( 'caption', {
+				allowIn: 'image',
+				allowContentOf: '$block',
+				isLimit: true
+			} );
+
+			model.schema.register( 'inlineWidget', {
+				isObject: true,
+				allowIn: [ '$block', '$clipboardHolder' ]
+			} );
+
+			model.schema.register( 'figure', {
+				allowIn: '$root',
+				allowAttributes: [ 'name', 'title' ]
+			} );
 		} );
 		} );
 
 
 		it( 'should not crash if there is no correct position for model selection', () => {
 		it( 'should not crash if there is no correct position for model selection', () => {
@@ -59,37 +73,41 @@ describe( 'Selection post-fixer', () => {
 		} );
 		} );
 
 
 		it( 'should react to structure changes', () => {
 		it( 'should react to structure changes', () => {
+			setModelData( model, '<paragraph>[]foo</paragraph><image></image>' );
+
 			model.change( writer => {
 			model.change( writer => {
 				writer.remove( modelRoot.getChild( 0 ) );
 				writer.remove( modelRoot.getChild( 0 ) );
 			} );
 			} );
 
 
-			expect( getModelData( model ) ).to.equal(
-				'[<table>' +
-					'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
-				'</table>]' +
-				'<paragraph>bar</paragraph>'
-			);
+			expect( getModelData( model ) ).to.equal( '[<image></image>]' );
 		} );
 		} );
 
 
 		it( 'should react to selection changes', () => {
 		it( 'should react to selection changes', () => {
-			// <paragraph>foo</paragraph>[]<table>...
+			setModelData( model, '<paragraph>[]foo</paragraph><image></image>' );
+
+			// <paragraph>foo</paragraph>[]<image></image>
 			model.change( writer => {
 			model.change( writer => {
 				writer.setSelection(
 				writer.setSelection(
 					ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 )
 					ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 )
 				);
 				);
 			} );
 			} );
 
 
-			expect( getModelData( model ) ).to.equal(
-				'<paragraph>foo[]</paragraph>' +
-				'<table>' +
-					'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
-				'</table>' +
-				'<paragraph>bar</paragraph>'
-			);
+			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]</paragraph><image></image>' );
 		} );
 		} );
 
 
-		describe( 'not collapsed selection', () => {
+		describe( 'non-collapsed selection - table scenarios', () => {
+			beforeEach( () => {
+				setModelData( model,
+					'<paragraph>[]foo</paragraph>' +
+					'<table>' +
+						'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
+					'</table>' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
 			it( 'should fix #1', () => {
 			it( 'should fix #1', () => {
+				// <paragraph>f[oo</paragraph><table><tableRow><tableCell></tableCell>]<tableCell>...
 				model.change( writer => {
 				model.change( writer => {
 					writer.setSelection( ModelRange.createFromParentsAndOffsets(
 					writer.setSelection( ModelRange.createFromParentsAndOffsets(
 						modelRoot.getChild( 0 ), 1,
 						modelRoot.getChild( 0 ), 1,
@@ -107,6 +125,7 @@ describe( 'Selection post-fixer', () => {
 			} );
 			} );
 
 
 			it( 'should fix #2', () => {
 			it( 'should fix #2', () => {
+				// ...<table><tableRow><tableCell></tableCell>[<tableCell></tableCell></tableRow></table><paragraph>b]ar</paragraph>
 				model.change( writer => {
 				model.change( writer => {
 					writer.setSelection( ModelRange.createFromParentsAndOffsets(
 					writer.setSelection( ModelRange.createFromParentsAndOffsets(
 						modelRoot.getChild( 1 ).getChild( 0 ), 1,
 						modelRoot.getChild( 1 ).getChild( 0 ), 1,
@@ -124,6 +143,7 @@ describe( 'Selection post-fixer', () => {
 			} );
 			} );
 
 
 			it( 'should fix #3', () => {
 			it( 'should fix #3', () => {
+				// <paragraph>f[oo</paragraph><table>]<tableRow>...
 				model.change( writer => {
 				model.change( writer => {
 					writer.setSelection( ModelRange.createFromParentsAndOffsets(
 					writer.setSelection( ModelRange.createFromParentsAndOffsets(
 						modelRoot.getChild( 0 ), 1,
 						modelRoot.getChild( 0 ), 1,
@@ -141,6 +161,7 @@ describe( 'Selection post-fixer', () => {
 			} );
 			} );
 
 
 			it( 'should fix #4', () => {
 			it( 'should fix #4', () => {
+				// <paragraph>foo</paragraph><table><tableRow><tableCell>a[aa</tableCell><tableCell>b]bb</tableCell>
 				model.change( writer => {
 				model.change( writer => {
 					writer.setSelection( ModelRange.createFromParentsAndOffsets(
 					writer.setSelection( ModelRange.createFromParentsAndOffsets(
 						modelRoot.getChild( 1 ).getChild( 0 ).getChild( 0 ), 1,
 						modelRoot.getChild( 1 ).getChild( 0 ).getChild( 0 ), 1,
@@ -163,7 +184,8 @@ describe( 'Selection post-fixer', () => {
 					'<table>' +
 					'<table>' +
 						'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
 						'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
 					'</table>' +
 					'</table>' +
-					'[]<table>' +
+					'[]' +
+					'<table>' +
 						'<tableRow><tableCell>xxx</tableCell><tableCell>yyy</tableCell></tableRow>' +
 						'<tableRow><tableCell>xxx</tableCell><tableCell>yyy</tableCell></tableRow>' +
 					'</table>' +
 					'</table>' +
 					'<paragraph>baz</paragraph>'
 					'<paragraph>baz</paragraph>'
@@ -181,6 +203,75 @@ describe( 'Selection post-fixer', () => {
 				);
 				);
 			} );
 			} );
 
 
+			// There's a chance that this and the following test will not be up to date with
+			// how the table feature is really implemented once we'll introduce row/cells/columns selection
+			// in which case all these elements will need to be marked as objects.
+			it( 'should fix #6 (element selection of not an object)', () => {
+				setModelData( model,
+					'<paragraph>foo</paragraph>' +
+					'<table>' +
+						'[<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>]' +
+					'</table>' +
+					'<paragraph>baz</paragraph>'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>foo</paragraph>' +
+					'[<table>' +
+						'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
+					'</table>]' +
+					'<paragraph>baz</paragraph>'
+				);
+			} );
+
+			it( 'should fix #7 (element selection of non-objects)', () => {
+				setModelData( model,
+					'<paragraph>foo</paragraph>' +
+					'<table>' +
+						'[<tableRow><tableCell>1</tableCell><tableCell>2</tableCell></tableRow>' +
+						'<tableRow><tableCell>3</tableCell><tableCell>4</tableCell>]</tableRow>' +
+						'<tableRow><tableCell>5</tableCell><tableCell>6</tableCell></tableRow>' +
+					'</table>' +
+					'<paragraph>baz</paragraph>'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>foo</paragraph>' +
+					'[<table>' +
+						'<tableRow><tableCell>1</tableCell><tableCell>2</tableCell></tableRow>' +
+						'<tableRow><tableCell>3</tableCell><tableCell>4</tableCell></tableRow>' +
+						'<tableRow><tableCell>5</tableCell><tableCell>6</tableCell></tableRow>' +
+					'</table>]' +
+					'<paragraph>baz</paragraph>'
+				);
+			} );
+
+			it( 'should fix #8 (cross-limit selection which starts in a non-limit elements)', () => {
+				model.schema.extend( 'paragraph', { allowIn: 'tableCell' } );
+
+				setModelData( model,
+					'<paragraph>foo</paragraph>' +
+					'<table>' +
+						'<tableRow>' +
+							'<tableCell><paragraph>f[oo</paragraph></tableCell>' +
+							'<tableCell><paragraph>b]ar</paragraph></tableCell>' +
+						'</tableRow>' +
+					'</table>' +
+					'<paragraph>baz</paragraph>'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>foo</paragraph>' +
+					'[<table>' +
+						'<tableRow>' +
+							'<tableCell><paragraph>foo</paragraph></tableCell>' +
+							'<tableCell><paragraph>bar</paragraph></tableCell>' +
+						'</tableRow>' +
+					'</table>]' +
+					'<paragraph>baz</paragraph>'
+				);
+			} );
+
 			it( 'should not fix #1', () => {
 			it( 'should not fix #1', () => {
 				setModelData( model,
 				setModelData( model,
 					'<paragraph>foo</paragraph>' +
 					'<paragraph>foo</paragraph>' +
@@ -278,12 +369,346 @@ describe( 'Selection post-fixer', () => {
 					'<table>' +
 					'<table>' +
 						'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
 						'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
 					'</table>' +
 					'</table>' +
-					'<paragraph>b]a[r]</paragraph>'
+					'<paragraph>bar]</paragraph>'
+				);
+			} );
+		} );
+
+		describe( 'non-collapsed selection - image scenarios', () => {
+			beforeEach( () => {
+				setModelData( model,
+					'<paragraph>[]foo</paragraph>' +
+					'<image>' +
+						'<caption>xxx</caption>' +
+					'</image>' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should fix #1 (crossing object and limit boundaries)', () => {
+				model.change( writer => {
+					// <paragraph>f[oo</paragraph><image><caption>x]xx</caption>...
+					writer.setSelection( ModelRange.createFromParentsAndOffsets(
+						modelRoot.getChild( 0 ), 1,
+						modelRoot.getChild( 1 ).getChild( 0 ), 1
+					) );
+				} );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>f[oo</paragraph>' +
+					'<image>' +
+						'<caption>xxx</caption>' +
+					'</image>]' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should fix #2 (crossing object boundary)', () => {
+				model.change( writer => {
+					// <paragraph>f[oo</paragraph><image>]<caption>xxx</caption>...
+					writer.setSelection( ModelRange.createFromParentsAndOffsets(
+						modelRoot.getChild( 0 ), 1,
+						modelRoot.getChild( 1 ), 0
+					) );
+				} );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>f[oo</paragraph>' +
+					'<image>' +
+						'<caption>xxx</caption>' +
+					'</image>]' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should fix #3 (crossing object boundary)', () => {
+				model.change( writer => {
+					// <paragraph>f[oo</paragraph><image><caption>xxx</caption>]</image>...
+					writer.setSelection( ModelRange.createFromParentsAndOffsets(
+						modelRoot.getChild( 0 ), 1,
+						modelRoot.getChild( 1 ), 1
+					) );
+				} );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>f[oo</paragraph>' +
+					'<image>' +
+						'<caption>xxx</caption>' +
+					'</image>]' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should fix #4 (element selection of not an object)', () => {
+				model.change( writer => {
+					// <paragraph>foo</paragraph><image>[<caption>xxx</caption>]</image>...
+					writer.setSelection( ModelRange.createFromParentsAndOffsets(
+						modelRoot.getChild( 1 ), 0,
+						modelRoot.getChild( 1 ), 1
+					) );
+				} );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>foo</paragraph>' +
+					'[<image>' +
+						'<caption>xxx</caption>' +
+					'</image>]' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should not fix #1 (element selection of an object)', () => {
+				model.change( writer => {
+					// <paragraph>foo</paragraph>[<image><caption>xxx</caption></image>]...
+					writer.setSelection( ModelRange.createFromParentsAndOffsets(
+						modelRoot, 1,
+						modelRoot, 2
+					) );
+				} );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>foo</paragraph>' +
+					'[<image>' +
+						'<caption>xxx</caption>' +
+					'</image>]' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should not fix #2 (inside a limit)', () => {
+				model.change( writer => {
+					const caption = modelRoot.getChild( 1 ).getChild( 0 );
+
+					// <paragraph>foo</paragraph><image><caption>[xxx]</caption></image>...
+					writer.setSelection( ModelRange.createFromParentsAndOffsets(
+						caption, 0,
+						caption, 3
+					) );
+				} );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>foo</paragraph>' +
+					'<image>' +
+						'<caption>[xxx]</caption>' +
+					'</image>' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should not fix #3 (inside a limit - partial text selection)', () => {
+				model.change( writer => {
+					const caption = modelRoot.getChild( 1 ).getChild( 0 );
+
+					// <paragraph>foo</paragraph><image><caption>[xx]x</caption></image>...
+					writer.setSelection( ModelRange.createFromParentsAndOffsets(
+						caption, 0,
+						caption, 2
+					) );
+				} );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>foo</paragraph>' +
+					'<image>' +
+						'<caption>[xx]x</caption>' +
+					'</image>' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should not fix #4 (inside a limit - partial text selection)', () => {
+				model.change( writer => {
+					const caption = modelRoot.getChild( 1 ).getChild( 0 );
+
+					// <paragraph>foo</paragraph><image><caption>x[xx]</caption></image>...
+					writer.setSelection( ModelRange.createFromParentsAndOffsets(
+						caption, 1,
+						caption, 3
+					) );
+				} );
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>foo</paragraph>' +
+					'<image>' +
+						'<caption>x[xx]</caption>' +
+					'</image>' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should not fix #5 (selection in root on non limit element that doesn\'t allow text)', () => {
+				setModelData( model,
+					'[<figure></figure>]'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'[<figure></figure>]'
+				);
+			} );
+		} );
+
+		describe( 'non-collapsed selection - other scenarios', () => {
+			it( 'should fix #1 (element selection of not an object)', () => {
+				setModelData( model,
+					'<paragraph>aaa</paragraph>' +
+					'[<paragraph>bbb</paragraph>]' +
+					'<paragraph>ccc</paragraph>'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>aaa</paragraph>' +
+					'<paragraph>[bbb]</paragraph>' +
+					'<paragraph>ccc</paragraph>'
+				);
+			} );
+
+			it( 'should fix #2 (elements selection of not an object)', () => {
+				setModelData( model,
+					'<paragraph>aaa</paragraph>' +
+					'[<paragraph>bbb</paragraph>' +
+					'<paragraph>ccc</paragraph>]'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>aaa</paragraph>' +
+					'<paragraph>[bbb</paragraph>' +
+					'<paragraph>ccc]</paragraph>'
+				);
+			} );
+
+			it( 'should fix #3 (partial selection of not an object)', () => {
+				setModelData( model,
+					'<paragraph>aaa</paragraph>' +
+					'[<paragraph>bbb</paragraph>' +
+					'<paragraph>ccc]</paragraph>'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>aaa</paragraph>' +
+					'<paragraph>[bbb</paragraph>' +
+					'<paragraph>ccc]</paragraph>'
+				);
+			} );
+
+			it( 'should fix #4 (partial selection of not an object)', () => {
+				setModelData( model,
+					'<paragraph>aaa</paragraph>' +
+					'<paragraph>b[bb</paragraph>]' +
+					'<paragraph>ccc</paragraph>'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>aaa</paragraph>' +
+					'<paragraph>b[bb]</paragraph>' +
+					'<paragraph>ccc</paragraph>'
+				);
+			} );
+
+			it( 'should fix #5 (partial selection of not an object)', () => {
+				setModelData( model,
+					'<paragraph>aaa</paragraph>' +
+					'[<paragraph>bb]b</paragraph>' +
+					'<paragraph>ccc</paragraph>'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>aaa</paragraph>' +
+					'<paragraph>[bb]b</paragraph>' +
+					'<paragraph>ccc</paragraph>'
+				);
+			} );
+
+			it( 'should fix #6 (selection must not cross a limit element; starts in a root)', () => {
+				model.schema.register( 'a', { isLimit: true, allowIn: '$root' } );
+				model.schema.register( 'b', { isLimit: true, allowIn: 'a' } );
+				model.schema.register( 'c', { allowIn: 'b' } );
+				model.schema.extend( '$text', { allowIn: 'c' } );
+
+				setModelData( model,
+					'<a><b><c>[</c></b></a>]'
+				);
+
+				expect( getModelData( model ) ).to.equal( '[<a><b><c></c></b></a>]' );
+			} );
+
+			it( 'should fix #7 (selection must not cross a limit element; ends in a root)', () => {
+				model.schema.register( 'a', { isLimit: true, allowIn: '$root' } );
+				model.schema.register( 'b', { isLimit: true, allowIn: 'a' } );
+				model.schema.register( 'c', { allowIn: 'b' } );
+				model.schema.extend( '$text', { allowIn: 'c' } );
+
+				setModelData( model,
+					'[<a><b><c>]</c></b></a>'
+				);
+
+				expect( getModelData( model ) ).to.equal( '[<a><b><c></c></b></a>]' );
+			} );
+
+			it( 'should fix #8 (selection must not cross a limit element; starts in a non-limit)', () => {
+				model.schema.register( 'div', { allowIn: '$root' } );
+				model.schema.register( 'a', { isLimit: true, allowIn: 'div' } );
+				model.schema.register( 'b', { isLimit: true, allowIn: 'a' } );
+				model.schema.register( 'c', { allowIn: 'b' } );
+				model.schema.extend( '$text', { allowIn: 'c' } );
+
+				setModelData( model,
+					'<div>[<a><b><c>]</c></b></a></div>'
+				);
+
+				expect( getModelData( model ) ).to.equal( '<div>[<a><b><c></c></b></a>]</div>' );
+			} );
+
+			it( 'should fix #9 (selection must not cross a limit element; ends in a non-limit)', () => {
+				model.schema.register( 'div', { allowIn: '$root' } );
+				model.schema.register( 'a', { isLimit: true, allowIn: 'div' } );
+				model.schema.register( 'b', { isLimit: true, allowIn: 'a' } );
+				model.schema.register( 'c', { allowIn: 'b' } );
+				model.schema.extend( '$text', { allowIn: 'c' } );
+
+				setModelData( model,
+					'<div><a><b><c>[</c></b></a>]</div>'
+				);
+
+				expect( getModelData( model ) ).to.equal( '<div>[<a><b><c></c></b></a>]</div>' );
+			} );
+
+			it( 'should not fix #1 (selection on text node)', () => {
+				setModelData( model, '<paragraph>foob[a]r</paragraph>', { lastRangeBackward: true } );
+
+				expect( getModelData( model ) ).to.equal( '<paragraph>foob[a]r</paragraph>' );
+			} );
+
+			it( 'should not fix #2', () => {
+				setModelData( model,
+					'<paragraph>[<inlineWidget></inlineWidget>]</paragraph>'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>[<inlineWidget></inlineWidget>]</paragraph>'
+				);
+			} );
+
+			it( 'should not fix #3', () => {
+				setModelData( model,
+					'<paragraph>fo[o<inlineWidget></inlineWidget>b]ar</paragraph>'
+				);
+
+				expect( getModelData( model ) ).to.equal(
+					'<paragraph>fo[o<inlineWidget></inlineWidget>b]ar</paragraph>'
 				);
 				);
 			} );
 			} );
 		} );
 		} );
 
 
 		describe( 'collapsed selection', () => {
 		describe( 'collapsed selection', () => {
+			beforeEach( () => {
+				setModelData( model,
+					'<paragraph>[]foo</paragraph>' +
+					'<table>' +
+						'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
+					'</table>' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
 			it( 'should fix #1', () => {
 			it( 'should fix #1', () => {
 				// <table>[]<tableRow>...
 				// <table>[]<tableRow>...
 				model.change( writer => {
 				model.change( writer => {
@@ -321,7 +746,7 @@ describe( 'Selection post-fixer', () => {
 			} );
 			} );
 
 
 			it( 'should fix multiple ranges #1', () => {
 			it( 'should fix multiple ranges #1', () => {
-				// []<paragraph></paragraph>[]<table>...
+				// []<paragraph>foo</paragraph>[]<table>...
 				model.change( writer => {
 				model.change( writer => {
 					writer.setSelection(
 					writer.setSelection(
 						[
 						[
@@ -332,7 +757,7 @@ describe( 'Selection post-fixer', () => {
 				} );
 				} );
 
 
 				expect( getModelData( model ) ).to.equal(
 				expect( getModelData( model ) ).to.equal(
-					'<paragraph>[]foo[]</paragraph>' +
+					'<paragraph>[foo]</paragraph>' +
 					'<table>' +
 					'<table>' +
 						'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
 						'<tableRow><tableCell>aaa</tableCell><tableCell>bbb</tableCell></tableRow>' +
 					'</table>' +
 					'</table>' +

+ 2 - 2
packages/ckeditor5-engine/tests/model/writer.js

@@ -2375,12 +2375,12 @@ describe( 'Writer', () => {
 		} );
 		} );
 
 
 		it( 'should change document selection ranges', () => {
 		it( 'should change document selection ranges', () => {
-			const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 2 ] ) );
+			const range = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 2, 2 ] ) );
 
 
 			setSelection( range, { backward: true } );
 			setSelection( range, { backward: true } );
 
 
 			expect( model.document.selection._ranges.length ).to.equal( 1 );
 			expect( model.document.selection._ranges.length ).to.equal( 1 );
-			expect( model.document.selection._ranges[ 0 ].start.path ).to.deep.equal( [ 1 ] );
+			expect( model.document.selection._ranges[ 0 ].start.path ).to.deep.equal( [ 1, 0 ] );
 			expect( model.document.selection._ranges[ 0 ].end.path ).to.deep.equal( [ 2, 2 ] );
 			expect( model.document.selection._ranges[ 0 ].end.path ).to.deep.equal( [ 2, 2 ] );
 			expect( model.document.selection.isBackward ).to.be.true;
 			expect( model.document.selection.isBackward ).to.be.true;
 		} );
 		} );