Browse Source

Merge pull request #227 from ckeditor/i/6116

Other: Introduced support for multi-range selections. See ckeditor/ckeditor5#6116.
Piotrek Koszuliński 5 years ago
parent
commit
51f9d4c462

+ 5 - 8
packages/ckeditor5-typing/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();

+ 83 - 30
packages/ckeditor5-typing/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, '<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' );
@@ -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',
@@ -197,30 +197,30 @@ 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;
 
@@ -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,
+				'<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();