From 3958290362501d36533796ac41923e5415edb87d Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Thu, 15 Apr 2021 13:29:36 +0200 Subject: [PATCH 1/2] Widget type around should disable model.deleteContent while the fake caret is active. --- .../src/widgettypearound/widgettypearound.js | 29 +++ .../widgettypearound/widgettypearound.js | 166 ++++++++++++++++++ 2 files changed, 195 insertions(+) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 5664651733e..7d20d8e6bed 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,34 @@ export default class WidgetTypeAround extends Plugin { } ); }, { priority: 'high' } ); } + + /** + * Attaches the {@link module:engine/model/model~Model#event:deleteContent} event listener to block it in cases + * of the fake caret is activated. + * + * This is required for the cases that trigger {@link module:engine/model/model~Model#deleteContent `model.deleteContent()`} + * before calling {@link module:engine/model/model~Model#insertContent `model.insertContent()`} like 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', From 0b0f2964617f535354af58a0f68c5438b3058df9 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 15 Apr 2021 13:54:26 +0200 Subject: [PATCH 2/2] Docs. --- .../src/widgettypearound/widgettypearound.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js index 7d20d8e6bed..98e15a99915 100644 --- a/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js +++ b/packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js @@ -740,11 +740,12 @@ export default class WidgetTypeAround extends Plugin { } /** - * Attaches the {@link module:engine/model/model~Model#event:deleteContent} event listener to block it in cases - * of the fake caret is activated. + * 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 the cases that trigger {@link module:engine/model/model~Model#deleteContent `model.deleteContent()`} - * before calling {@link module:engine/model/model~Model#insertContent `model.insertContent()`} like plain text pasting. + * 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 */