Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #227 from ckeditor/i/6116
Browse files Browse the repository at this point in the history
Other: Introduced support for multi-range selections. See ckeditor/ckeditor5#6116.
  • Loading branch information
Reinmar authored Mar 9, 2020
2 parents cb75e45 + 53ee255 commit 64daf31
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 38 deletions.
13 changes: 5 additions & 8 deletions src/inputcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
113 changes: 83 additions & 30 deletions tests/inputcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' } );
} );
} );

Expand Down Expand Up @@ -63,22 +63,22 @@ describe( 'InputCommand', () => {

describe( 'execute()', () => {
it( 'uses enqueueChange', () => {
setData( model, '<p>foo[]bar</p>' );
setData( model, '<paragraph>foo[]bar</paragraph>' );

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( '<p>foo[]bar</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>foo[]bar</paragraph>' );
} );

// After all enqueued changes are done, the command execution is reflected.
expect( getData( model ) ).to.be.equal( '<p>foox[]bar</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>foox[]bar</paragraph>' );
} );

it( 'should lock and unlock buffer', () => {
setData( model, '<p>foo[]bar</p>' );
setData( model, '<paragraph>foo[]bar</paragraph>' );

const spyLock = testUtils.sinon.spy( buffer, 'lock' );
const spyUnlock = testUtils.sinon.spy( buffer, 'unlock' );
Expand All @@ -92,102 +92,102 @@ describe( 'InputCommand', () => {
} );

it( 'inserts text for collapsed range', () => {
setData( model, '<p>foo[]</p>' );
setData( model, '<paragraph>foo[]</paragraph>' );

editor.execute( 'input', {
text: 'bar',
range: doc.selection.getFirstRange()
} );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>foobar[]</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>foobar[]</paragraph>' );
expect( buffer.size ).to.be.equal( 3 );
} );

it( 'replaces text for range within single element on the beginning', () => {
setData( model, '<p>[fooba]r</p>' );
setData( model, '<paragraph>[fooba]r</paragraph>' );

editor.execute( 'input', {
text: 'rab',
range: doc.selection.getFirstRange()
} );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>rab[]r</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>rab[]r</paragraph>' );
expect( buffer.size ).to.be.equal( 3 );
} );

it( 'replaces text for range within single element in the middle', () => {
setData( model, '<p>fo[oba]r</p>' );
setData( model, '<paragraph>fo[oba]r</paragraph>' );

editor.execute( 'input', {
text: 'bazz',
range: doc.selection.getFirstRange()
} );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>fobazz[]r</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>fobazz[]r</paragraph>' );
expect( buffer.size ).to.be.equal( 4 );
} );

it( 'replaces text for range within single element on the end', () => {
setData( model, '<p>fooba[r]</p>' );
setData( model, '<paragraph>fooba[r]</paragraph>' );

editor.execute( 'input', {
text: 'zzz',
range: doc.selection.getFirstRange()
} );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>foobazzz[]</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>foobazzz[]</paragraph>' );
expect( buffer.size ).to.be.equal( 3 );
} );

it( 'replaces text for range within multiple elements', () => {
setData( model, '<h1>F[OO</h1><p>b]ar</p>' );
setData( model, '<heading1>F[OO</heading1><paragraph>b]ar</paragraph>' );

editor.execute( 'input', {
text: 'unny c',
range: doc.selection.getFirstRange()
} );

expect( getData( model, { selection: true } ) ).to.be.equal( '<h1>Funny c[]ar</h1>' );
expect( getData( model ) ).to.be.equal( '<heading1>Funny c[]ar</heading1>' );
expect( buffer.size ).to.be.equal( 6 );
} );

it( 'uses current selection when range is not given', () => {
setData( model, '<p>foob[ar]</p>' );
setData( model, '<paragraph>foob[ar]</paragraph>' );

editor.execute( 'input', {
text: 'az'
} );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>foobaz[]</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>foobaz[]</paragraph>' );
expect( buffer.size ).to.be.equal( 2 );
} );

it( 'only removes content when empty text given', () => {
setData( model, '<p>[fo]obar</p>' );
setData( model, '<paragraph>[fo]obar</paragraph>' );

editor.execute( 'input', {
text: '',
range: doc.selection.getFirstRange()
} );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>[]obar</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>[]obar</paragraph>' );
expect( buffer.size ).to.be.equal( 0 );
} );

it( 'should set selection according to passed resultRange (collapsed)', () => {
setData( model, '<p>[foo]bar</p>' );
setData( model, '<paragraph>[foo]bar</paragraph>' );

editor.execute( 'input', {
text: 'new',
resultRange: editor.model.createRange( editor.model.createPositionFromPath( doc.getRoot(), [ 0, 5 ] ) )
} );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>newba[]r</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>newba[]r</paragraph>' );
expect( buffer.size ).to.be.equal( 3 );
} );

it( 'should set selection according to passed resultRange (non-collapsed)', () => {
setData( model, '<p>[foo]bar</p>' );
setData( model, '<paragraph>[foo]bar</paragraph>' );

editor.execute( 'input', {
text: 'new',
Expand All @@ -197,40 +197,93 @@ describe( 'InputCommand', () => {
)
} );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>new[bar]</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>new[bar]</paragraph>' );
expect( buffer.size ).to.be.equal( 3 );
} );

it( 'only removes content when no text given (with default non-collapsed range)', () => {
setData( model, '<p>[fo]obar</p>' );
setData( model, '<paragraph>[fo]obar</paragraph>' );

editor.execute( 'input' );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>[]obar</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>[]obar</paragraph>' );
expect( buffer.size ).to.be.equal( 0 );
} );

it( 'does not change selection and content when no text given (with default collapsed range)', () => {
setData( model, '<p>fo[]obar</p>' );
setData( model, '<paragraph>fo[]obar</paragraph>' );

editor.execute( 'input' );

expect( getData( model, { selection: true } ) ).to.be.equal( '<p>fo[]obar</p>' );
expect( getData( model ) ).to.be.equal( '<paragraph>fo[]obar</paragraph>' );
expect( buffer.size ).to.be.equal( 0 );
} );

it( 'does not create insert delta when no text given', () => {
setData( model, '<p>foo[]bar</p>' );
setData( model, '<paragraph>foo[]bar</paragraph>' );

const version = doc.version;

editor.execute( 'input' );

expect( doc.version ).to.equal( version );
} );

it( 'handles multi-range selection', () => {
model.schema.register( 'object', {
allowWhere: '$block',
allowContentOf: '$block',
isObject: true
} );

setData(
model,
'<paragraph>x</paragraph>' +
'[<object>y</object>]' +
'<paragraph>y</paragraph>' +
'[<object>y</object>]' +
'<paragraph>z</paragraph>'
);

// 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(
'<paragraph>x</paragraph>' +
'<paragraph></paragraph>' +
'<paragraph>y</paragraph>' +
'<paragraph>foo[]</paragraph>' +
'<paragraph>z</paragraph>'
);
} );
} );

describe( 'destroy', () => {
describe( 'destroy()', () => {
it( 'should destroy change buffer', () => {
const command = editor.commands.get( 'input' );
const destroy = command._buffer.destroy = testUtils.sinon.spy();
Expand Down

0 comments on commit 64daf31

Please sign in to comment.