Skip to content

Commit

Permalink
Merge pull request #9484 from ckeditor/i/9477
Browse files Browse the repository at this point in the history
Fix (widget): Pasting plain text while the widget fake caret is active should not remove the widget. Closes #9477.
  • Loading branch information
oleq authored Apr 16, 2021
2 parents 9fd389a + 0b0f296 commit 9978a9a
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 0 deletions.
30 changes: 30 additions & 0 deletions packages/ckeditor5-widget/src/widgettypearound/widgettypearound.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export default class WidgetTypeAround extends Plugin {
this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows();
this._enableDeleteIntegration();
this._enableInsertContentIntegration();
this._enableDeleteContentIntegration();
}

/**
Expand Down Expand Up @@ -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.
Expand Down
166 changes: 166 additions & 0 deletions packages/ckeditor5-widget/tests/widgettypearound/widgettypearound.js
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,26 @@ describe( 'WidgetTypeAround', () => {
expect( getModelData( model ) ).to.equal( '<paragraph>bar[]</paragraph>' );
} );

it( 'should handle pasted content (with formatting)', () => {
setModelData( editor.model, '[<blockWidget></blockWidget>]' );

model.change( writer => {
writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
} );

viewDocument.fire( 'clipboardInput', {
dataTransfer: {
getData() {
return 'foo<b>bar</b>';
}
}
} );

expect( getModelData( model ) ).to.equal(
'<paragraph>foo<$text bold="true">bar[]</$text></paragraph><blockWidget></blockWidget>'
);
} );

function createParagraph( text ) {
return model.change( writer => {
const paragraph = writer.createElement( 'paragraph' );
Expand All @@ -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, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>baz</paragraph>' );

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( '<paragraph>foo[]</paragraph><paragraph></paragraph><paragraph>baz</paragraph>' );
expect( batchSet.size ).to.be.equal( 1 );
} );

it( 'should not alter deleteContent when the "fake caret" is not active', () => {
setModelData( editor.model, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>baz</paragraph>' );

const batchSet = setupBatchWatch();

expect( modelSelection.getAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE ) ).to.be.undefined;

model.deleteContent( modelSelection );

expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph><paragraph>[]</paragraph><paragraph>baz</paragraph>' );
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, '[<blockWidget></blockWidget>]' );

const batchSet = setupBatchWatch();

model.change( writer => {
writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
} );

model.deleteContent( modelSelection );

expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
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, '[<blockWidget></blockWidget>]' );

const batchSet = setupBatchWatch();

model.change( writer => {
writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' );
} );

model.deleteContent( modelSelection );

expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]' );
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, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );

const batchSet = setupBatchWatch();

model.change( writer => {
writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
} );

model.deleteContent( modelSelection );

expect( getModelData( model ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
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, '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );

const batchSet = setupBatchWatch();

model.change( writer => {
writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'after' );
} );

model.deleteContent( modelSelection );

expect( getModelData( model ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>foo</paragraph>' );
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, '[<blockWidget></blockWidget>]' );

editor.plugins.get( WidgetTypeAround ).isEnabled = false;

model.change( writer => {
writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
} );

model.deleteContent( modelSelection );

expect( getModelData( model ) ).to.equal( '<paragraph>[]</paragraph>' );
} );

it( 'should not remove widget while pasting a plain text', () => {
setModelData( editor.model, '[<blockWidget></blockWidget>]' );

model.change( writer => {
writer.setSelectionAttribute( TYPE_AROUND_SELECTION_ATTRIBUTE, 'before' );
} );

viewDocument.fire( 'clipboardInput', {
dataTransfer: {
getData() {
return 'bar';
}
}
} );

expect( getModelData( model ) ).to.equal( '<paragraph>bar[]</paragraph><blockWidget></blockWidget>' );
} );

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',
Expand Down

0 comments on commit 9978a9a

Please sign in to comment.