diff --git a/packages/ckeditor5-clipboard/src/clipboard.js b/packages/ckeditor5-clipboard/src/clipboard.js
index fd72d74f9db..8e1bd943fe3 100644
--- a/packages/ckeditor5-clipboard/src/clipboard.js
+++ b/packages/ckeditor5-clipboard/src/clipboard.js
@@ -115,15 +115,25 @@ export default class Clipboard extends Plugin {
return;
}
- // Plain text can be determined based on event flag (#7799) or auto detection (#1006). If detected
- // preserve selection attributes on pasted items.
- if ( data.asPlainText || isPlainTextFragment( modelFragment ) ) {
- // Consider only formatting attributes.
- const textAttributes = new Map( Array.from( modelDocument.selection.getAttributes() ).filter(
- keyValuePair => editor.model.schema.getAttributeProperties( keyValuePair[ 0 ] ).isFormatting
- ) );
-
- model.change( writer => {
+ model.change( writer => {
+ const selection = model.document.selection;
+
+ // Plain text can be determined based on event flag (#7799) or auto detection (#1006). If detected
+ // preserve selection attributes on pasted items.
+ if ( data.asPlainText || isPlainTextFragment( modelFragment, model.schema ) ) {
+ // Formatting attributes should be preserved.
+ const textAttributes = Array.from( selection.getAttributes() )
+ .filter( ( [ key ] ) => model.schema.getAttributeProperties( key ).isFormatting );
+
+ if ( !selection.isCollapsed ) {
+ model.deleteContent( selection, { doNotAutoparagraph: true } );
+ }
+
+ // But also preserve other attributes if they survived the content deletion (because they were not fully selected).
+ // For example linkHref is not a formatting attribute but it should be preserved if pasted text was in the middle
+ // of a link.
+ textAttributes.push( ...selection.getAttributes() );
+
const range = writer.createRangeIn( modelFragment );
for ( const item of range.getItems() ) {
@@ -131,10 +141,10 @@ export default class Clipboard extends Plugin {
writer.setAttributes( textAttributes, item );
}
}
- } );
- }
+ }
- model.insertContent( modelFragment );
+ model.insertContent( modelFragment );
+ } );
evt.stop();
}
@@ -235,13 +245,18 @@ export default class Clipboard extends Plugin {
// Returns true if specified `documentFragment` represents a plain text.
//
// @param {module:engine/view/documentfragment~DocumentFragment} documentFragment
+// @param {module:engine/model/schema~Schema} schema
// @returns {Boolean}
-function isPlainTextFragment( documentFragment ) {
+function isPlainTextFragment( documentFragment, schema ) {
if ( documentFragment.childCount > 1 ) {
return false;
}
const child = documentFragment.getChild( 0 );
+ if ( schema.isObject( child ) ) {
+ return false;
+ }
+
return [ ...child.getAttributeKeys() ].length == 0;
}
diff --git a/packages/ckeditor5-clipboard/tests/clipboard.js b/packages/ckeditor5-clipboard/tests/clipboard.js
index a68ea5e52fa..0a7e4731774 100644
--- a/packages/ckeditor5-clipboard/tests/clipboard.js
+++ b/packages/ckeditor5-clipboard/tests/clipboard.js
@@ -231,6 +231,7 @@ describe( 'Clipboard feature', () => {
viewDocument.fire( 'paste', {
dataTransfer: dataTransferMock,
+ stopPropagation() {},
preventDefault() {}
} );
@@ -526,23 +527,61 @@ describe( 'Clipboard feature', () => {
expect( getModelData( model ) ).to.equal( '<$text bold="true">Bolded []text.$text>' );
} );
- it( 'ignores non-formatting text attributes', () => {
- setModelData( model, '<$text test="true">Bolded []text.$text>' );
+ it( 'should preserve non formatting attribute if it wasn\'t fully selected', () => {
+ setModelData( model, '<$text test="true">Linked [text].$text>' );
- const dataTransferMock = createDataTransfer( {
- 'text/html': 'foo',
- 'text/plain': 'foo'
+ viewDocument.fire( 'clipboardInput', {
+ dataTransfer: createDataTransfer( {
+ 'text/html': 'foo',
+ 'text/plain': 'foo'
+ } ),
+ stopPropagation() {},
+ preventDefault() {}
} );
+ expect( getModelData( model ) ).to.equal( '<$text test="true">Linked foo[].$text>' );
+ } );
+
+ it( 'should not preserve non formatting attribute if it was fully selected', () => {
+ setModelData( model, '<$text test="true">[Linked text.]$text>' );
+
viewDocument.fire( 'clipboardInput', {
- dataTransfer: dataTransferMock,
- asPlainText: false,
+ dataTransfer: createDataTransfer( {
+ 'text/html': 'foo',
+ 'text/plain': 'foo'
+ } ),
+ stopPropagation() {},
+ preventDefault() {}
+ } );
+
+ expect( getModelData( model ) ).to.equal( 'foo[]' );
+ } );
+
+ it( 'should not treat a pasted object as a plain text', () => {
+ model.schema.register( 'obj', {
+ allowWhere: '$block',
+ isObject: true,
+ isBlock: true
+ } );
+
+ editor.conversion.elementToElement( { model: 'obj', view: 'obj' } );
+
+ setModelData( model, '<$text bold="true">Bolded [text].$text>' );
+
+ viewDocument.fire( 'clipboardInput', {
+ dataTransfer: createDataTransfer( {
+ 'text/html': '',
+ 'text/plain': 'foo'
+ } ),
stopPropagation() {},
preventDefault() {}
} );
expect( getModelData( model ) ).to.equal(
- '<$text test="true">Bolded $text>foo[]<$text test="true">text.$text>' );
+ '<$text bold="true">Bolded $text>' +
+ '[]' +
+ '<$text bold="true">.$text>'
+ );
} );
} );
diff --git a/packages/ckeditor5-link/tests/linkediting.js b/packages/ckeditor5-link/tests/linkediting.js
index 7d0dc37c65f..ee13d361934 100644
--- a/packages/ckeditor5-link/tests/linkediting.js
+++ b/packages/ckeditor5-link/tests/linkediting.js
@@ -51,6 +51,9 @@ describe( 'LinkEditing', () => {
} );
editor.model.schema.extend( '$text', { allowAttributes: 'bold' } );
+ editor.model.schema.setAttributeProperties( 'bold', {
+ isFormatting: true
+ } );
editor.conversion.attributeToElement( {
model: 'bold',
@@ -143,7 +146,7 @@ describe( 'LinkEditing', () => {
// https://github.com/ckeditor/ckeditor5/issues/6053
describe( 'selection attribute management on paste', () => {
- it( 'should remove link atttributes when pasting a link', () => {
+ it( 'should remove link attributes when pasting a link', () => {
setModelData( model, 'foo[]' );
model.change( writer => {
@@ -155,7 +158,7 @@ describe( 'LinkEditing', () => {
expect( [ ...model.document.selection.getAttributeKeys() ] ).to.be.empty;
} );
- it( 'should remove all atttributes starting with "link" (e.g. decorator attributes) when pasting a link', () => {
+ it( 'should remove all attributes starting with "link" (e.g. decorator attributes) when pasting a link', () => {
setModelData( model, 'foo[]' );
model.change( writer => {
@@ -171,7 +174,7 @@ describe( 'LinkEditing', () => {
expect( [ ...model.document.selection.getAttributeKeys() ] ).to.be.empty;
} );
- it( 'should not remove link atttributes when pasting a non-link content', () => {
+ it( 'should not remove link attributes when pasting a non-link content', () => {
setModelData( model, '<$text linkHref="ckeditor.com">foo[]$text>' );
model.change( writer => {
@@ -188,7 +191,7 @@ describe( 'LinkEditing', () => {
expect( model.document.selection ).to.have.attribute( 'bold' );
} );
- it( 'should not remove link atttributes when pasting in the middle of a link with the same URL', () => {
+ it( 'should not remove link attributes when pasting in the middle of a link with the same URL', () => {
setModelData( model, '<$text linkHref="ckeditor.com">fo[]o$text>' );
model.change( writer => {
@@ -199,7 +202,7 @@ describe( 'LinkEditing', () => {
expect( model.document.selection ).to.have.attribute( 'linkHref' );
} );
- it( 'should not remove link atttributes from the selection when pasting before a link when the gravity is overridden', () => {
+ it( 'should not remove link attributes from the selection when pasting before a link when the gravity is overridden', () => {
setModelData( model, 'foo[]<$text linkHref="ckeditor.com">bar$text>' );
view.document.fire( 'keydown', {
@@ -226,7 +229,7 @@ describe( 'LinkEditing', () => {
expect( model.document.selection ).to.have.attribute( 'linkHref' );
} );
- it( 'should not remove link atttributes when pasting a link into another link (different URLs, no merge)', () => {
+ it( 'should not remove link attributes when pasting a link into another link (different URLs, no merge)', () => {
setModelData( model, '<$text linkHref="ckeditor.com">f[]oo$text>' );
model.change( writer => {
@@ -244,7 +247,7 @@ describe( 'LinkEditing', () => {
expect( model.document.selection ).to.have.attribute( 'linkHref' );
} );
- it( 'should not remove link atttributes when pasting before another link (different URLs, no merge)', () => {
+ it( 'should not remove link attributes when pasting before another link (different URLs, no merge)', () => {
setModelData( model, '[]<$text linkHref="ckeditor.com">foo$text>' );
expect( model.document.selection ).to.have.property( 'isGravityOverridden', false );
@@ -263,6 +266,43 @@ describe( 'LinkEditing', () => {
expect( model.document.selection ).to.have.attribute( 'linkHref' );
expect( model.document.selection ).to.have.attribute( 'linkHref', 'http://INSERTED' );
} );
+
+ // https://github.com/ckeditor/ckeditor5/issues/8158
+ it( 'should expand link text on pasting plain text', () => {
+ setModelData( model, '<$text linkHref="ckeditor.com">f[]oo$text>' );
+
+ view.document.fire( 'paste', {
+ dataTransfer: createDataTransfer( {
+ 'text/html': '
bar
',
+ 'text/plain': 'bar'
+ } ),
+ preventDefault: sinon.spy(),
+ stopPropagation: sinon.spy()
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ '' +
+ '<$text linkHref="ckeditor.com">fbar[]oo$text>' +
+ ''
+ );
+ } );
+
+ it( 'doesn\'t affect attributes other than link', () => {
+ setModelData( model, '<$text bold="true">[foo]$text>' );
+
+ view.document.fire( 'paste', {
+ dataTransfer: createDataTransfer( {
+ 'text/html': 'bar
',
+ 'text/plain': 'bar'
+ } ),
+ preventDefault: sinon.spy(),
+ stopPropagation: sinon.spy()
+ } );
+
+ expect( getModelData( model ) ).to.equal(
+ '<$text bold="true">bar[]$text>'
+ );
+ } );
} );
describe( 'command', () => {
@@ -1397,15 +1437,6 @@ describe( 'LinkEditing', () => {
'This is Abcde[]from <$text linkHref="bar">Bar$text>.'
);
} );
-
- function createDataTransfer( data ) {
- return {
- getData( type ) {
- return data[ type ];
- },
- setData() {}
- };
- }
} );
// https://github.com/ckeditor/ckeditor5/issues/7521
@@ -1586,4 +1617,13 @@ describe( 'LinkEditing', () => {
expect( model.document.selection.hasAttribute( 'linkHref' ), 'removing space after the link' ).to.equal( true );
} );
} );
+
+ function createDataTransfer( data ) {
+ return {
+ getData( type ) {
+ return data[ type ];
+ },
+ setData() {}
+ };
+ }
} );