diff --git a/src/model/schema.js b/src/model/schema.js
index 23e78fdd2..c3779b524 100644
--- a/src/model/schema.js
+++ b/src/model/schema.js
@@ -582,26 +582,38 @@ export default class Schema {
}, { priority: 'high' } );
}
+ /* eslint-disable max-len */
/**
* Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
* 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.
* @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 ) ) {
if ( element.parent ) {
diff --git a/src/model/utils/selection-post-fixer.js b/src/model/utils/selection-post-fixer.js
index eea8bbc23..804c7b01d 100644
--- a/src/model/utils/selection-post-fixer.js
+++ b/src/model/utils/selection-post-fixer.js
@@ -94,9 +94,18 @@ function selectionPostFixer( writer, model ) {
if ( wasFixed ) {
// 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.
- 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 tryFixingNonCollpasedRage( range, schema );
+ return tryFixingNonCollapsedRage( range, schema );
}
// Tries to fix collapsed ranges.
@@ -146,27 +155,57 @@ function tryFixingCollapsedRange( range, schema ) {
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/schema~Schema} schema
// @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 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:
+ // - f[oobarba]z
+ // 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:
+ // - [foo ] -> [foo]
+ // - [foo] -> [foo]
+ // - f[oo ] -> f[oo]
+ 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;
}
@@ -186,50 +225,18 @@ function expandSelectionOnIsLimitNode( position, schema, expandToDirection ) {
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.
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.} ranges
-// @returns {Array.}
-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;
}
diff --git a/tests/conversion/downcast-selection-converters.js b/tests/conversion/downcast-selection-converters.js
index 2adf54a49..2a0def3f1 100644
--- a/tests/conversion/downcast-selection-converters.js
+++ b/tests/conversion/downcast-selection-converters.js
@@ -494,9 +494,9 @@ describe( 'downcast-selection-converters', () => {
describe( 'table cell selection converter', () => {
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( 'tr', { allowIn: 'table' } );
@@ -519,16 +519,16 @@ describe( 'downcast-selection-converters', () => {
}
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' );
const viewNode = conversionApi.mapper.toViewElement( node );
conversionApi.writer.addClass( 'selected', viewNode );
}
}
- } );
+ }, { priority: 'high' } );
} );
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', () => {
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 ] ],
'',
''
);
@@ -550,10 +550,10 @@ describe( 'downcast-selection-converters', () => {
it( 'should not be used if selection contains more than just a table cell', () => {
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 ] ],
'',
- ''
+ '[]'
);
} );
} );
diff --git a/tests/model/schema.js b/tests/model/schema.js
index 2607b0d2b..75c332fa2 100644
--- a/tests/model/schema.js
+++ b/tests/model/schema.js
@@ -17,7 +17,7 @@ import Position from '../../src/model/position';
import Range from '../../src/model/range';
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';
@@ -983,6 +983,38 @@ describe( 'Schema', () => {
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 = '';
+ 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 = '';
+ 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()', () => {
@@ -1101,7 +1133,13 @@ describe( 'Schema', () => {
}
} );
- setData( model, 'foo bar
' );
+ // Parse data string to model.
+ const parsedModel = parse( 'foo bar
', 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 ) ) ];
} );
@@ -1123,12 +1161,12 @@ describe( 'Schema', () => {
schema.extend( 'img', { allowAttributes: 'bold' } );
schema.extend( '$text', { allowIn: 'img' } );
- setData( model, '[foo xxxbar
]' );
+ setData( model, '[foo xxxbar]
' );
const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
const sel = new Selection( validRanges );
- expect( stringify( root, sel ) ).to.equal( '[foo ]xxx[bar
]' );
+ expect( stringify( root, sel ) ).to.equal( '[foo ]xxx[bar]
' );
} );
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, '[foo xxxbar
]' );
+ setData( model, '[foo xxxbar]
' );
const validRanges = schema.getValidRanges( doc.selection.getRanges(), attribute );
const sel = new Selection( validRanges );
- expect( stringify( root, sel ) ).to.equal( '[foo] [xxx][bar
]' );
+ expect( stringify( root, sel ) ).to.equal( '[foo] [xxx][bar]
' );
} );
it( 'should not leak beyond the given ranges', () => {
diff --git a/tests/model/utils/deletecontent.js b/tests/model/utils/deletecontent.js
index 5271641d2..10495b51d 100644
--- a/tests/model/utils/deletecontent.js
+++ b/tests/model/utils/deletecontent.js
@@ -562,7 +562,7 @@ describe( 'DataController utils', () => {
schema.register( 'image', { allowWhere: '$text' } );
schema.register( 'paragraph', { inheritAllFrom: '$block' } );
schema.register( 'heading1', { inheritAllFrom: '$block' } );
- schema.register( 'blockWidget' );
+ schema.register( 'blockWidget', { isLimit: true } );
schema.register( 'restrictedRoot', {
isLimit: true
} );
@@ -608,7 +608,7 @@ describe( 'DataController utils', () => {
it( 'creates a paragraph when text is not allowed (custom selection)', () => {
setData(
model,
- '[x ]yyy z ',
+ '[x] yyy z ',
{ rootName: 'bodyRoot' }
);
@@ -621,7 +621,7 @@ describe( 'DataController utils', () => {
deleteContent( model, selection );
expect( getData( model, { rootName: 'bodyRoot' } ) )
- .to.equal( '[x ]z ' );
+ .to.equal( '[x] z ' );
} );
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)', () => {
setData(
model,
- 'x [yyy ]z ',
+ 'x yyy z ',
{ rootName: 'bodyRoot' }
);
- deleteContent( model, doc.selection );
+ // [yyy ]
+ const range = new Range(
+ new Position( doc.getRoot( 'bodyRoot' ), [ 1 ] ),
+ new Position( doc.getRoot( 'bodyRoot' ), [ 2 ] )
+ );
- expect( getData( model, { rootName: 'bodyRoot' } ) )
- .to.equal( 'x [] z ' );
+ deleteContent( model, new Selection( range ) );
+
+ expect( getData( model, { rootName: 'bodyRoot', withoutSelection: true } ) )
+ .to.equal( 'x z ' );
} );
it( 'creates paragraph when text is not allowed (two blocks selected)', () => {
setData(
model,
- 'x [yyy yyy ]z ',
+ 'x yyy yyy z ',
{ rootName: 'bodyRoot' }
);
- deleteContent( model, doc.selection );
+ // [yyy yyy ]
+ const range = new Range(
+ new Position( doc.getRoot( 'bodyRoot' ), [ 1 ] ),
+ new Position( doc.getRoot( 'bodyRoot' ), [ 3 ] )
+ );
- expect( getData( model, { rootName: 'bodyRoot' } ) )
- .to.equal( 'x [] z ' );
+ deleteContent( model, new Selection( range ) );
+
+ expect( getData( model, { rootName: 'bodyRoot', withoutSelection: true } ) )
+ .to.equal( 'x z ' );
} );
it( 'creates paragraph when text is not allowed (all content selected)', () => {
diff --git a/tests/model/utils/getselectedcontent.js b/tests/model/utils/getselectedcontent.js
index 87ad51974..c1b304305 100644
--- a/tests/model/utils/getselectedcontent.js
+++ b/tests/model/utils/getselectedcontent.js
@@ -129,7 +129,7 @@ describe( 'DataController utils', () => {
schema.register( 'paragraph', { inheritAllFrom: '$block' } );
schema.register( 'heading1', { inheritAllFrom: '$block' } );
- schema.register( 'blockImage' );
+ schema.register( 'blockImage', { isObject: true } );
schema.register( 'caption' );
schema.register( 'image', { allowWhere: '$text' } );
diff --git a/tests/model/utils/selection-post-fixer.js b/tests/model/utils/selection-post-fixer.js
index afaf30cf3..d13f7126b 100644
--- a/tests/model/utils/selection-post-fixer.js
+++ b/tests/model/utils/selection-post-fixer.js
@@ -26,6 +26,7 @@ describe( 'Selection post-fixer', () => {
modelRoot = model.document.createRoot();
model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
model.schema.register( 'table', {
allowWhere: '$block',
isObject: true,
@@ -43,13 +44,26 @@ describe( 'Selection post-fixer', () => {
isLimit: true
} );
- setModelData( model,
- '[]foo ' +
- '' +
- 'bar '
- );
+ 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', () => {
@@ -59,37 +73,41 @@ describe( 'Selection post-fixer', () => {
} );
it( 'should react to structure changes', () => {
+ setModelData( model, '[]foo ' );
+
model.change( writer => {
writer.remove( modelRoot.getChild( 0 ) );
} );
- expect( getModelData( model ) ).to.equal(
- '[]' +
- 'bar '
- );
+ expect( getModelData( model ) ).to.equal( '[ ]' );
} );
it( 'should react to selection changes', () => {
- // foo []...
+ setModelData( model, '[]foo ' );
+
+ // foo []
model.change( writer => {
writer.setSelection(
ModelRange.createFromParentsAndOffsets( modelRoot, 1, modelRoot, 1 )
);
} );
- expect( getModelData( model ) ).to.equal(
- 'foo[] ' +
- '' +
- 'bar '
- );
+ expect( getModelData( model ) ).to.equal( 'foo[] ' );
} );
- describe( 'not collapsed selection', () => {
+ describe( 'non-collapsed selection - table scenarios', () => {
+ beforeEach( () => {
+ setModelData( model,
+ '[]foo ' +
+ '' +
+ 'bar '
+ );
+ } );
+
it( 'should fix #1', () => {
+ // f[oo ]...
model.change( writer => {
writer.setSelection( ModelRange.createFromParentsAndOffsets(
modelRoot.getChild( 0 ), 1,
@@ -107,6 +125,7 @@ describe( 'Selection post-fixer', () => {
} );
it( 'should fix #2', () => {
+ // ...b]ar
model.change( writer => {
writer.setSelection( ModelRange.createFromParentsAndOffsets(
modelRoot.getChild( 1 ).getChild( 0 ), 1,
@@ -124,6 +143,7 @@ describe( 'Selection post-fixer', () => {
} );
it( 'should fix #3', () => {
+ // f[oo ]...
model.change( writer => {
writer.setSelection( ModelRange.createFromParentsAndOffsets(
modelRoot.getChild( 0 ), 1,
@@ -141,6 +161,7 @@ describe( 'Selection post-fixer', () => {
} );
it( 'should fix #4', () => {
+ // foo a[aa b]bb
model.change( writer => {
writer.setSelection( ModelRange.createFromParentsAndOffsets(
modelRoot.getChild( 1 ).getChild( 0 ).getChild( 0 ), 1,
@@ -163,7 +184,8 @@ describe( 'Selection post-fixer', () => {
'' +
- '[]' +
+ '[]' +
+ '' +
'baz '
@@ -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,
+ 'foo ' +
+ '' +
+ 'baz '
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo ' +
+ '[]' +
+ 'baz '
+ );
+ } );
+
+ it( 'should fix #7 (element selection of non-objects)', () => {
+ setModelData( model,
+ 'foo ' +
+ '' +
+ '[1 2 ' +
+ '3 4 ] ' +
+ '5 6 ' +
+ '
' +
+ 'baz '
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo ' +
+ '[' +
+ '1 2 ' +
+ '3 4 ' +
+ '5 6 ' +
+ '
]' +
+ 'baz '
+ );
+ } );
+
+ it( 'should fix #8 (cross-limit selection which starts in a non-limit elements)', () => {
+ model.schema.extend( 'paragraph', { allowIn: 'tableCell' } );
+
+ setModelData( model,
+ 'foo ' +
+ '' +
+ '' +
+ 'f[oo ' +
+ 'b]ar ' +
+ ' ' +
+ '
' +
+ 'baz '
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo ' +
+ '[' +
+ '' +
+ 'foo ' +
+ 'bar ' +
+ ' ' +
+ '
]' +
+ 'baz '
+ );
+ } );
+
it( 'should not fix #1', () => {
setModelData( model,
'foo ' +
@@ -278,12 +369,346 @@ describe( 'Selection post-fixer', () => {
'' +
- 'b]a[r] '
+ 'bar] '
+ );
+ } );
+ } );
+
+ describe( 'non-collapsed selection - image scenarios', () => {
+ beforeEach( () => {
+ setModelData( model,
+ '[]foo ' +
+ '' +
+ 'xxx ' +
+ ' ' +
+ 'bar '
+ );
+ } );
+
+ it( 'should fix #1 (crossing object and limit boundaries)', () => {
+ model.change( writer => {
+ // f[oo x]xx ...
+ writer.setSelection( ModelRange.createFromParentsAndOffsets(
+ modelRoot.getChild( 0 ), 1,
+ modelRoot.getChild( 1 ).getChild( 0 ), 1
+ ) );
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ 'f[oo ' +
+ '' +
+ 'xxx ' +
+ ' ]' +
+ 'bar '
+ );
+ } );
+
+ it( 'should fix #2 (crossing object boundary)', () => {
+ model.change( writer => {
+ // f[oo ]xxx ...
+ writer.setSelection( ModelRange.createFromParentsAndOffsets(
+ modelRoot.getChild( 0 ), 1,
+ modelRoot.getChild( 1 ), 0
+ ) );
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ 'f[oo ' +
+ '' +
+ 'xxx ' +
+ ' ]' +
+ 'bar '
+ );
+ } );
+
+ it( 'should fix #3 (crossing object boundary)', () => {
+ model.change( writer => {
+ // f[oo xxx ] ...
+ writer.setSelection( ModelRange.createFromParentsAndOffsets(
+ modelRoot.getChild( 0 ), 1,
+ modelRoot.getChild( 1 ), 1
+ ) );
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ 'f[oo ' +
+ '' +
+ 'xxx ' +
+ ' ]' +
+ 'bar '
+ );
+ } );
+
+ it( 'should fix #4 (element selection of not an object)', () => {
+ model.change( writer => {
+ // foo [xxx ] ...
+ writer.setSelection( ModelRange.createFromParentsAndOffsets(
+ modelRoot.getChild( 1 ), 0,
+ modelRoot.getChild( 1 ), 1
+ ) );
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo ' +
+ '[' +
+ 'xxx ' +
+ ' ]' +
+ 'bar '
+ );
+ } );
+
+ it( 'should not fix #1 (element selection of an object)', () => {
+ model.change( writer => {
+ // foo [xxx ]...
+ writer.setSelection( ModelRange.createFromParentsAndOffsets(
+ modelRoot, 1,
+ modelRoot, 2
+ ) );
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo ' +
+ '[' +
+ 'xxx ' +
+ ' ]' +
+ 'bar '
+ );
+ } );
+
+ it( 'should not fix #2 (inside a limit)', () => {
+ model.change( writer => {
+ const caption = modelRoot.getChild( 1 ).getChild( 0 );
+
+ // foo [xxx] ...
+ writer.setSelection( ModelRange.createFromParentsAndOffsets(
+ caption, 0,
+ caption, 3
+ ) );
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo ' +
+ '' +
+ '[xxx] ' +
+ ' ' +
+ 'bar '
+ );
+ } );
+
+ it( 'should not fix #3 (inside a limit - partial text selection)', () => {
+ model.change( writer => {
+ const caption = modelRoot.getChild( 1 ).getChild( 0 );
+
+ // foo [xx]x ...
+ writer.setSelection( ModelRange.createFromParentsAndOffsets(
+ caption, 0,
+ caption, 2
+ ) );
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo ' +
+ '' +
+ '[xx]x ' +
+ ' ' +
+ 'bar '
+ );
+ } );
+
+ it( 'should not fix #4 (inside a limit - partial text selection)', () => {
+ model.change( writer => {
+ const caption = modelRoot.getChild( 1 ).getChild( 0 );
+
+ // foo x[xx] ...
+ writer.setSelection( ModelRange.createFromParentsAndOffsets(
+ caption, 1,
+ caption, 3
+ ) );
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ 'foo ' +
+ '' +
+ 'x[xx] ' +
+ ' ' +
+ 'bar '
+ );
+ } );
+
+ it( 'should not fix #5 (selection in root on non limit element that doesn\'t allow text)', () => {
+ setModelData( model,
+ '[ ]'
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ '[ ]'
+ );
+ } );
+ } );
+
+ describe( 'non-collapsed selection - other scenarios', () => {
+ it( 'should fix #1 (element selection of not an object)', () => {
+ setModelData( model,
+ 'aaa ' +
+ '[bbb ]' +
+ 'ccc '
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ 'aaa ' +
+ '[bbb] ' +
+ 'ccc '
+ );
+ } );
+
+ it( 'should fix #2 (elements selection of not an object)', () => {
+ setModelData( model,
+ 'aaa ' +
+ '[bbb ' +
+ 'ccc ]'
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ 'aaa ' +
+ '[bbb ' +
+ 'ccc] '
+ );
+ } );
+
+ it( 'should fix #3 (partial selection of not an object)', () => {
+ setModelData( model,
+ 'aaa ' +
+ '[bbb ' +
+ 'ccc] '
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ 'aaa ' +
+ '[bbb ' +
+ 'ccc] '
+ );
+ } );
+
+ it( 'should fix #4 (partial selection of not an object)', () => {
+ setModelData( model,
+ 'aaa ' +
+ 'b[bb ]' +
+ 'ccc '
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ 'aaa ' +
+ 'b[bb] ' +
+ 'ccc '
+ );
+ } );
+
+ it( 'should fix #5 (partial selection of not an object)', () => {
+ setModelData( model,
+ 'aaa ' +
+ '[bb]b ' +
+ 'ccc '
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ 'aaa ' +
+ '[bb]b ' +
+ 'ccc '
+ );
+ } );
+
+ 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,
+ '[ ]'
+ );
+
+ expect( getModelData( model ) ).to.equal( '[ ]' );
+ } );
+
+ 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,
+ '[] '
+ );
+
+ expect( getModelData( model ) ).to.equal( '[ ]' );
+ } );
+
+ 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,
+ ''
+ );
+
+ expect( getModelData( model ) ).to.equal( '' );
+ } );
+
+ 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,
+ ''
+ );
+
+ expect( getModelData( model ) ).to.equal( '' );
+ } );
+
+ it( 'should not fix #1 (selection on text node)', () => {
+ setModelData( model, 'foob[a]r ', { lastRangeBackward: true } );
+
+ expect( getModelData( model ) ).to.equal( 'foob[a]r ' );
+ } );
+
+ it( 'should not fix #2', () => {
+ setModelData( model,
+ '[ ] '
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ '[ ] '
+ );
+ } );
+
+ it( 'should not fix #3', () => {
+ setModelData( model,
+ 'fo[o b]ar '
+ );
+
+ expect( getModelData( model ) ).to.equal(
+ 'fo[o b]ar '
);
} );
} );
describe( 'collapsed selection', () => {
+ beforeEach( () => {
+ setModelData( model,
+ '[]foo ' +
+ '' +
+ 'bar '
+ );
+ } );
+
it( 'should fix #1', () => {
// []...
model.change( writer => {
@@ -321,7 +746,7 @@ describe( 'Selection post-fixer', () => {
} );
it( 'should fix multiple ranges #1', () => {
- // [] []...
+ // []foo []...
model.change( writer => {
writer.setSelection(
[
@@ -332,7 +757,7 @@ describe( 'Selection post-fixer', () => {
} );
expect( getModelData( model ) ).to.equal(
- '[]foo[] ' +
+ '[foo] ' +
'' +
diff --git a/tests/model/writer.js b/tests/model/writer.js
index bbc535357..2c88776cc 100644
--- a/tests/model/writer.js
+++ b/tests/model/writer.js
@@ -2375,12 +2375,12 @@ describe( 'Writer', () => {
} );
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 } );
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.isBackward ).to.be.true;
} );