diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 5664651733e..98e15a99915 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -121,6 +121,7 @@ export default class WidgetTypeAround extends Plugin { this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows(); this._enableDeleteIntegration(); this._enableInsertContentIntegration(); + this._enableDeleteContentIntegration(); } /** @@ -737,6 +738,35 @@ export default class WidgetTypeAround extends Plugin { } ); }, { priority: 'high' } ); } + + /** + * Attaches the {@link module:engine/model/model~Model#event:deleteContent} event listener to block the event when the fake + * caret is active. + * + * This is required for cases that trigger {@link module:engine/model/model~Model#deleteContent `model.deleteContent()`} + * before calling {@link module:engine/model/model~Model#insertContent `model.insertContent()`} like, for instance, + * plain text pasting. + * + * @private + */ + _enableDeleteContentIntegration() { + const editor = this.editor; + const model = this.editor.model; + const documentSelection = model.document.selection; + + this._listenToIfEnabled( editor.model, 'deleteContent', ( evt, [ selection ] ) => { + if ( selection && !selection.is( 'documentSelection' ) ) { + return; + } + + const typeAroundFakeCaretPosition = getTypeAroundFakeCaretPosition( documentSelection ); + + // Disable removing the selection content while pasting plain text. + if ( typeAroundFakeCaretPosition ) { + evt.stop(); + } + }, { priority: 'high' } ); + } } // Injects the type around UI into a view widget instance. diff --git a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js index 0eecb10d8dc..c441932c9fa 100644 --- a/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js @@ -1567,6 +1567,26 @@ describe( 'WidgetTypeAround', () => { expect( getModelData( model ) ).to.equal( 'bar[]' ); } ); + it( 'should handle pasted content (with formatting)', () => { + setModelData( editor.model, '[]' ); + + model.change( writer => { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + } ); + + viewDocument.fire( 'clipboardInput', { + dataTransfer: { + getData() { + return 'foobar'; + } + } + } ); + + expect( getModelData( model ) ).to.equal( + 'foo<$text bold="true">bar[]' + ); + } ); + function createParagraph( text ) { return model.change( writer => { const paragraph = writer.createElement( 'paragraph' ); @@ -1590,6 +1610,152 @@ describe( 'WidgetTypeAround', () => { } } ); + describe( 'Model#deleteContent() integration', () => { + let model, modelSelection; + + beforeEach( () => { + model = editor.model; + modelSelection = model.document.selection; + } ); + + it( 'should not alter deleteContent for the selection other than the document selection', () => { + setModelData( editor.model, 'foo[]baz' ); + + const batchSet = setupBatchWatch(); + const selection = model.createSelection( modelSelection ); + + model.change( writer => { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + model.deleteContent( selection ); + } ); + + expect( getModelData( model ) ).to.equal( 'foo[]baz' ); + expect( batchSet.size ).to.be.equal( 1 ); + } ); + + it( 'should not alter deleteContent when the "fake caret" is not active', () => { + setModelData( editor.model, 'foo[]baz' ); + + const batchSet = setupBatchWatch(); + + expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined; + + model.deleteContent( modelSelection ); + + expect( getModelData( model ) ).to.equal( 'foo[]baz' ); + expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined; + expect( batchSet.size ).to.be.equal( 1 ); + } ); + + it( 'should disable deleteContent before a widget when it\'s the first element of the root', () => { + setModelData( editor.model, '[]' ); + + const batchSet = setupBatchWatch(); + + model.change( writer => { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + } ); + + model.deleteContent( modelSelection ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' ); + expect( batchSet.size ).to.be.equal( 0 ); + } ); + + it( 'should disable insertContent after a widget when it\'s the last element of the root', () => { + setModelData( editor.model, '[]' ); + + const batchSet = setupBatchWatch(); + + model.change( writer => { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' ); + } ); + + model.deleteContent( modelSelection ); + + expect( getModelData( model ) ).to.equal( '[]' ); + expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' ); + expect( batchSet.size ).to.be.equal( 0 ); + } ); + + it( 'should disable insertContent before a widget when it\'s not the first element of the root', () => { + setModelData( editor.model, 'foo[]' ); + + const batchSet = setupBatchWatch(); + + model.change( writer => { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + } ); + + model.deleteContent( modelSelection ); + + expect( getModelData( model ) ).to.equal( 'foo[]' ); + expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'before' ); + expect( batchSet.size ).to.be.equal( 0 ); + } ); + + it( 'should disable insertContent after a widget when it\'s not the last element of the root', () => { + setModelData( editor.model, '[]foo' ); + + const batchSet = setupBatchWatch(); + + model.change( writer => { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' ); + } ); + + model.deleteContent( modelSelection ); + + expect( getModelData( model ) ).to.equal( '[]foo' ); + expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.equal( 'after' ); + expect( batchSet.size ).to.be.equal( 0 ); + } ); + + it( 'should not block when the plugin is disabled', () => { + setModelData( editor.model, '[]' ); + + editor.plugins.get( WidgetTypeAround ).isEnabled = false; + + model.change( writer => { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + } ); + + model.deleteContent( modelSelection ); + + expect( getModelData( model ) ).to.equal( '[]' ); + } ); + + it( 'should not remove widget while pasting a plain text', () => { + setModelData( editor.model, '[]' ); + + model.change( writer => { + writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' ); + } ); + + viewDocument.fire( 'clipboardInput', { + dataTransfer: { + getData() { + return 'bar'; + } + } + } ); + + expect( getModelData( model ) ).to.equal( 'bar[]' ); + } ); + + function setupBatchWatch() { + const createdBatches = new Set(); + + model.on( 'applyOperation', ( evt, [ operation ] ) => { + if ( operation.isDocumentOperation ) { + createdBatches.add( operation.batch ); + } + } ); + + return createdBatches; + } + } ); + function blockWidgetPlugin( editor ) { editor.model.schema.register( 'blockWidget', { inheritAllFrom: '$block',