diff --git a/src/inputcommand.js b/src/inputcommand.js index 6fe85a5..9f47346 100644 --- a/src/inputcommand.js +++ b/src/inputcommand.js @@ -83,25 +83,22 @@ export default class InputCommand extends Command { const doc = model.document; const text = options.text || ''; const textInsertions = text.length; - const range = options.range || doc.selection.getFirstRange(); + const selection = options.range ? model.createSelection( options.range ) : doc.selection; const resultRange = options.resultRange; model.enqueueChange( this._buffer.batch, writer => { - const isCollapsedRange = range.isCollapsed; - this._buffer.lock(); - model.deleteContent( model.createSelection( range ) ); + model.deleteContent( selection ); if ( text ) { - model.insertContent( writer.createText( text, doc.selection.getAttributes() ), range.start ); + model.insertContent( writer.createText( text, doc.selection.getAttributes() ), selection ); } if ( resultRange ) { writer.setSelection( resultRange ); - } else if ( isCollapsedRange ) { - // If range was collapsed just shift the selection by the number of inserted characters. - writer.setSelection( range.start.getShiftedBy( textInsertions ) ); + } else if ( !selection.is( 'documentSelection' ) ) { + writer.setSelection( selection ); } this._buffer.unlock(); diff --git a/tests/inputcommand.js b/tests/inputcommand.js index 626f751..2931a34 100644 --- a/tests/inputcommand.js +++ b/tests/inputcommand.js @@ -29,8 +29,8 @@ describe( 'InputCommand', () => { buffer = inputCommand.buffer; buffer.size = 0; - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - model.schema.register( 'h1', { inheritAllFrom: '$block' } ); + model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); + model.schema.register( 'heading1', { inheritAllFrom: '$block' } ); } ); } ); @@ -63,22 +63,22 @@ describe( 'InputCommand', () => { describe( 'execute()', () => { it( 'uses enqueueChange', () => { - setData( model, '

foo[]bar

' ); + setData( model, 'foo[]bar' ); model.enqueueChange( () => { editor.execute( 'input', { text: 'x' } ); // We expect that command is executed in enqueue changes block. Since we are already in // an enqueued block, the command execution will be postponed. Hence, no changes. - expect( getData( model ) ).to.be.equal( '

foo[]bar

' ); + expect( getData( model ) ).to.be.equal( 'foo[]bar' ); } ); // After all enqueued changes are done, the command execution is reflected. - expect( getData( model ) ).to.be.equal( '

foox[]bar

' ); + expect( getData( model ) ).to.be.equal( 'foox[]bar' ); } ); it( 'should lock and unlock buffer', () => { - setData( model, '

foo[]bar

' ); + setData( model, 'foo[]bar' ); const spyLock = testUtils.sinon.spy( buffer, 'lock' ); const spyUnlock = testUtils.sinon.spy( buffer, 'unlock' ); @@ -92,102 +92,102 @@ describe( 'InputCommand', () => { } ); it( 'inserts text for collapsed range', () => { - setData( model, '

foo[]

' ); + setData( model, 'foo[]' ); editor.execute( 'input', { text: 'bar', range: doc.selection.getFirstRange() } ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

foobar[]

' ); + expect( getData( model ) ).to.be.equal( 'foobar[]' ); expect( buffer.size ).to.be.equal( 3 ); } ); it( 'replaces text for range within single element on the beginning', () => { - setData( model, '

[fooba]r

' ); + setData( model, '[fooba]r' ); editor.execute( 'input', { text: 'rab', range: doc.selection.getFirstRange() } ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

rab[]r

' ); + expect( getData( model ) ).to.be.equal( 'rab[]r' ); expect( buffer.size ).to.be.equal( 3 ); } ); it( 'replaces text for range within single element in the middle', () => { - setData( model, '

fo[oba]r

' ); + setData( model, 'fo[oba]r' ); editor.execute( 'input', { text: 'bazz', range: doc.selection.getFirstRange() } ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

fobazz[]r

' ); + expect( getData( model ) ).to.be.equal( 'fobazz[]r' ); expect( buffer.size ).to.be.equal( 4 ); } ); it( 'replaces text for range within single element on the end', () => { - setData( model, '

fooba[r]

' ); + setData( model, 'fooba[r]' ); editor.execute( 'input', { text: 'zzz', range: doc.selection.getFirstRange() } ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

foobazzz[]

' ); + expect( getData( model ) ).to.be.equal( 'foobazzz[]' ); expect( buffer.size ).to.be.equal( 3 ); } ); it( 'replaces text for range within multiple elements', () => { - setData( model, '

F[OO

b]ar

' ); + setData( model, 'F[OOb]ar' ); editor.execute( 'input', { text: 'unny c', range: doc.selection.getFirstRange() } ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

Funny c[]ar

' ); + expect( getData( model ) ).to.be.equal( 'Funny c[]ar' ); expect( buffer.size ).to.be.equal( 6 ); } ); it( 'uses current selection when range is not given', () => { - setData( model, '

foob[ar]

' ); + setData( model, 'foob[ar]' ); editor.execute( 'input', { text: 'az' } ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

foobaz[]

' ); + expect( getData( model ) ).to.be.equal( 'foobaz[]' ); expect( buffer.size ).to.be.equal( 2 ); } ); it( 'only removes content when empty text given', () => { - setData( model, '

[fo]obar

' ); + setData( model, '[fo]obar' ); editor.execute( 'input', { text: '', range: doc.selection.getFirstRange() } ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

[]obar

' ); + expect( getData( model ) ).to.be.equal( '[]obar' ); expect( buffer.size ).to.be.equal( 0 ); } ); it( 'should set selection according to passed resultRange (collapsed)', () => { - setData( model, '

[foo]bar

' ); + setData( model, '[foo]bar' ); editor.execute( 'input', { text: 'new', resultRange: editor.model.createRange( editor.model.createPositionFromPath( doc.getRoot(), [ 0, 5 ] ) ) } ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

newba[]r

' ); + expect( getData( model ) ).to.be.equal( 'newba[]r' ); expect( buffer.size ).to.be.equal( 3 ); } ); it( 'should set selection according to passed resultRange (non-collapsed)', () => { - setData( model, '

[foo]bar

' ); + setData( model, '[foo]bar' ); editor.execute( 'input', { text: 'new', @@ -197,30 +197,30 @@ describe( 'InputCommand', () => { ) } ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

new[bar]

' ); + expect( getData( model ) ).to.be.equal( 'new[bar]' ); expect( buffer.size ).to.be.equal( 3 ); } ); it( 'only removes content when no text given (with default non-collapsed range)', () => { - setData( model, '

[fo]obar

' ); + setData( model, '[fo]obar' ); editor.execute( 'input' ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

[]obar

' ); + expect( getData( model ) ).to.be.equal( '[]obar' ); expect( buffer.size ).to.be.equal( 0 ); } ); it( 'does not change selection and content when no text given (with default collapsed range)', () => { - setData( model, '

fo[]obar

' ); + setData( model, 'fo[]obar' ); editor.execute( 'input' ); - expect( getData( model, { selection: true } ) ).to.be.equal( '

fo[]obar

' ); + expect( getData( model ) ).to.be.equal( 'fo[]obar' ); expect( buffer.size ).to.be.equal( 0 ); } ); it( 'does not create insert delta when no text given', () => { - setData( model, '

foo[]bar

' ); + setData( model, 'foo[]bar' ); const version = doc.version; @@ -228,9 +228,62 @@ describe( 'InputCommand', () => { expect( doc.version ).to.equal( version ); } ); + + it( 'handles multi-range selection', () => { + model.schema.register( 'object', { + allowWhere: '$block', + allowContentOf: '$block', + isObject: true + } ); + + setData( + model, + 'x' + + '[y]' + + 'y' + + '[y]' + + 'z' + ); + + // deleteContent() does not support multi-range selections yet, so we need to mock it here. + // See https://github.com/ckeditor/ckeditor5/issues/6328. + model.on( 'deleteContent', ( evt, args ) => { + const [ selection ] = args; + + if ( selection.rangeCount != 2 ) { + return; + } + + evt.stop(); + + model.change( writer => { + let rangeSelection; + + for ( const range of selection.getRanges() ) { + rangeSelection = writer.createSelection( range ); + + model.deleteContent( rangeSelection ); + } + + writer.setSelection( rangeSelection ); + } ); + }, { priority: 'high' } ); + + editor.execute( 'input', { + text: 'foo' + } ); + + expect( getData( model ) ).to.be.equal( + 'x' + + '' + + 'y' + + 'foo[]' + + 'z' + ); + } ); } ); - describe( 'destroy', () => { + describe( 'destroy()', () => { it( 'should destroy change buffer', () => { const command = editor.commands.get( 'input' ); const destroy = command._buffer.destroy = testUtils.sinon.spy();