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 ] ], '
foo
bar
', '
foo
bar
' ); @@ -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 ] ], '
foobar
', - ']
f{oobar
' + '[
foobar
]' ); } ); } ); 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 = '
foobar
'; + 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 = '
foobar
'; + 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, '

foobar

' ); + // Parse data string to model. + const parsedModel = parse( '

foobar

', 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, '[

fooxxxbar

]' ); + setData( model, '

[fooxxxbar]

' ); 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, '[

fooxxxbar

]' ); + setData( model, '

[fooxxxbar]

' ); 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]yyyz', + '[x]yyyz', { 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', + 'xyyyz', { 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( 'xz' ); } ); it( 'creates paragraph when text is not allowed (two blocks selected)', () => { setData( model, - 'x[yyyyyy]z', + 'xyyyyyyz', { rootName: 'bodyRoot' } ); - deleteContent( model, doc.selection ); + // [yyyyyy] + 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( 'xz' ); } ); 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' + - '' + - 'aaabbb' + - '
' + - '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( - '[' + - 'aaabbb' + - '
]' + - '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[]' + - '
' + - 'aaabbb' + - '
' + - 'bar' - ); + expect( getModelData( model ) ).to.equal( 'foo[]' ); } ); - describe( 'not collapsed selection', () => { + describe( 'non-collapsed selection - table scenarios', () => { + beforeEach( () => { + setModelData( model, + '[]foo' + + '' + + 'aaabbb' + + '
' + + '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[aab]bb model.change( writer => { writer.setSelection( ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 1 ).getChild( 0 ).getChild( 0 ), 1, @@ -163,7 +184,8 @@ describe( 'Selection post-fixer', () => { '
' + 'aaabbb' + '
' + - '[]' + + '[]' + + '
' + 'xxxyyy' + '
' + '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' + + '' + + '[aaabbb]' + + '
' + + 'baz' + ); + + expect( getModelData( model ) ).to.equal( + 'foo' + + '[' + + 'aaabbb' + + '
]' + + 'baz' + ); + } ); + + it( 'should fix #7 (element selection of non-objects)', () => { + setModelData( model, + 'foo' + + '' + + '[12' + + '34]' + + '56' + + '
' + + 'baz' + ); + + expect( getModelData( model ) ).to.equal( + 'foo' + + '[' + + '12' + + '34' + + '56' + + '
]' + + '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', () => { '' + 'aaabbb' + '
' + - '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[oox]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[ooxxx]... + 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 ); + + // foox[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[ob]ar' + ); + + expect( getModelData( model ) ).to.equal( + 'fo[ob]ar' ); } ); } ); describe( 'collapsed selection', () => { + beforeEach( () => { + setModelData( model, + '[]foo' + + '' + + 'aaabbb' + + '
' + + '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]' + '
' + 'aaabbb' + '
' + 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; } );