From 451ad21f3f985016bfe05c61b9e6cef8382b94dc Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Tue, 7 Jan 2025 12:45:33 +0100 Subject: [PATCH 1/6] Add `sourceEditorId` property to editor clipboard events. --- .../src/clipboardpipeline.ts | 14 +++ .../tests/clipboardpipeline.js | 97 ++++++++++++++++++- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index 4c9a57f6a9c..2b2b0587724 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -236,10 +236,12 @@ export default class ClipboardPipeline extends Plugin { } const eventInfo = new EventInfo( this, 'inputTransformation' ); + const sourceEditorId = dataTransfer.getData( 'application/ckeditor5-editor-id' ) || null; this.fire( eventInfo, { content, dataTransfer, + sourceEditorId, targetRanges: data.targetRanges, method: data.method as 'paste' | 'drop' } ); @@ -278,6 +280,7 @@ export default class ClipboardPipeline extends Plugin { this.fire( 'contentInsertion', { content: modelFragment, method: data.method, + sourceEditorId: data.sourceEditorId, dataTransfer: data.dataTransfer, targetRanges: data.targetRanges } ); @@ -331,6 +334,7 @@ export default class ClipboardPipeline extends Plugin { if ( !data.content.isEmpty ) { data.dataTransfer.setData( 'text/html', this.editor.data.htmlProcessor.toData( data.content ) ); data.dataTransfer.setData( 'text/plain', viewToPlainText( editor.data.htmlProcessor.domConverter, data.content ) ); + data.dataTransfer.setData( 'application/ckeditor5-editor-id', this.editor.id ); } if ( data.method == 'cut' ) { @@ -389,6 +393,11 @@ export interface ClipboardInputTransformationData { * Whether the event was triggered by a paste or a drop operation. */ method: 'paste' | 'drop'; + + /** + * ID of the editor instance from which the content was copied. + */ + sourceEditorId: string | null; } /** @@ -433,6 +442,11 @@ export interface ClipboardContentInsertionData { */ method: 'paste' | 'drop'; + /** + * ID of the editor instance from which the content was copied. + */ + sourceEditorId: string | null; + /** * The data transfer instance. */ diff --git a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js index 56b720dd382..c3bd70b8bb2 100644 --- a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js +++ b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js @@ -462,10 +462,105 @@ describe( 'ClipboardPipeline feature', () => { expect( spy.callCount ).to.equal( 1 ); } ); + describe( 'source editor ID in events', () => { + it( 'should be null when pasting content from outside the editor', () => { + const dataTransferMock = createDataTransfer( { 'text/html': '

external content

' } ); + const inputTransformationSpy = sinon.spy(); + + clipboardPlugin.on( 'inputTransformation', ( evt, data ) => { + inputTransformationSpy( data.sourceEditorId ); + } ); + + viewDocument.fire( 'paste', { + dataTransfer: dataTransferMock, + preventDefault: () => {}, + stopPropagation: () => {} + } ); + + sinon.assert.calledWith( inputTransformationSpy, null ); + } ); + + it( 'should contain editor ID when pasting content copied from the same editor (in dataTransfer)', () => { + const spy = sinon.spy(); + + setModelData( editor.model, 'f[oo]bar' ); + + // Copy selected content. + const dataTransferMock = createDataTransfer(); + + viewDocument.fire( 'copy', { + dataTransfer: dataTransferMock, + preventDefault: () => {} + } ); + + clipboardPlugin.on( 'inputTransformation', ( evt, data ) => { + spy( data.dataTransfer.getData( 'application/ckeditor5-editor-id' ) ); + } ); + + // Paste the copied content. + viewDocument.fire( 'paste', { + dataTransfer: dataTransferMock, + preventDefault: () => {}, + stopPropagation: () => {} + } ); + + sinon.assert.calledWith( spy, editor.id ); + } ); + + it( 'should contain editor ID when pasting content copied from the same editor', () => { + const spy = sinon.spy(); + + setModelData( editor.model, 'f[oo]bar' ); + + // Copy selected content. + const dataTransferMock = createDataTransfer(); + + viewDocument.fire( 'copy', { + dataTransfer: dataTransferMock, + preventDefault: () => {} + } ); + + clipboardPlugin.on( 'inputTransformation', ( evt, data ) => { + spy( data.sourceEditorId ); + } ); + + // Paste the copied content. + viewDocument.fire( 'paste', { + dataTransfer: dataTransferMock, + preventDefault: () => {}, + stopPropagation: () => {} + } ); + + sinon.assert.calledWith( spy, editor.id ); + } ); + + it( 'should be propagated to contentInsertion event', () => { + const dataTransferMock = createDataTransfer( { 'text/html': '

external content

' } ); + const contentInsertionSpy = sinon.spy(); + + clipboardPlugin.on( 'contentInsertion', ( evt, data ) => { + contentInsertionSpy( data.sourceEditorId ); + } ); + + viewDocument.fire( 'paste', { + dataTransfer: dataTransferMock, + preventDefault: () => {}, + stopPropagation: () => {} + } ); + + sinon.assert.calledWith( contentInsertionSpy, null ); + } ); + } ); + function createDataTransfer( data ) { + const state = Object.create( data || {} ); + return { getData( type ) { - return data[ type ]; + return state[ type ]; + }, + setData( type, newData ) { + state[ type ] = newData; } }; } From 5b473e120aa0ee3825c81e922a21d60cda9a8550 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Tue, 7 Jan 2025 13:02:24 +0100 Subject: [PATCH 2/6] Fix codeblock tests. --- packages/ckeditor5-code-block/tests/codeblockediting.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-code-block/tests/codeblockediting.js b/packages/ckeditor5-code-block/tests/codeblockediting.js index 6bfb6ad0d6d..e210ec4d8fc 100644 --- a/packages/ckeditor5-code-block/tests/codeblockediting.js +++ b/packages/ckeditor5-code-block/tests/codeblockediting.js @@ -1682,7 +1682,7 @@ describe( 'CodeBlockEditing', () => { '[]o' + '' ); - sinon.assert.calledOnce( dataTransferMock.getData ); + sinon.assert.calledTwice( dataTransferMock.getData ); // Make sure that ClipboardPipeline was not interrupted. sinon.assert.calledOnce( contentInsertionSpy ); @@ -1723,7 +1723,7 @@ describe( 'CodeBlockEditing', () => { 'bar' ); - sinon.assert.calledOnce( dataTransferMock.getData ); + sinon.assert.calledTwice( dataTransferMock.getData ); // Make sure that ClipboardPipeline was not interrupted. sinon.assert.calledOnce( contentInsertionSpy ); From efb2b6ee2ca2603ac20150c6b362dc31f103c991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Fri, 17 Jan 2025 10:02:28 +0100 Subject: [PATCH 3/6] Update packages/ckeditor5-clipboard/src/clipboardpipeline.ts --- packages/ckeditor5-clipboard/src/clipboardpipeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index 2b2b0587724..7d731ede003 100644 --- a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts +++ b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts @@ -443,7 +443,7 @@ export interface ClipboardContentInsertionData { method: 'paste' | 'drop'; /** - * ID of the editor instance from which the content was copied. + * The ID of the editor instance from which the content was copied. */ sourceEditorId: string | null; From 14f952064bc09683db7f63c449eb12d3da12e780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Fri, 17 Jan 2025 10:02:37 +0100 Subject: [PATCH 4/6] Update packages/ckeditor5-clipboard/tests/clipboardpipeline.js --- packages/ckeditor5-clipboard/tests/clipboardpipeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js index c3bd70b8bb2..a6a85ce0f0e 100644 --- a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js +++ b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js @@ -480,7 +480,7 @@ describe( 'ClipboardPipeline feature', () => { sinon.assert.calledWith( inputTransformationSpy, null ); } ); - it( 'should contain editor ID when pasting content copied from the same editor (in dataTransfer)', () => { + it( 'should contain an editor ID when pasting content copied from the same editor (in dataTransfer)', () => { const spy = sinon.spy(); setModelData( editor.model, 'f[oo]bar' ); From 6bc33005e2415e9224daed4f3464eec0f7bf4a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Fri, 17 Jan 2025 10:02:44 +0100 Subject: [PATCH 5/6] Update packages/ckeditor5-clipboard/tests/clipboardpipeline.js --- packages/ckeditor5-clipboard/tests/clipboardpipeline.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js index a6a85ce0f0e..70a6d54056f 100644 --- a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js +++ b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js @@ -507,7 +507,7 @@ describe( 'ClipboardPipeline feature', () => { sinon.assert.calledWith( spy, editor.id ); } ); - it( 'should contain editor ID when pasting content copied from the same editor', () => { + it( 'should contain an editor ID when pasting content copied from the same editor', () => { const spy = sinon.spy(); setModelData( editor.model, 'f[oo]bar' ); From b998be06ef8f821783f2863752d8b29a5dc5e089 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Wed, 22 Jan 2025 15:41:02 +0100 Subject: [PATCH 6/6] Add missing test. --- .../tests/clipboardpipeline.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js index 70a6d54056f..198dc70fd5e 100644 --- a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js +++ b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js @@ -534,7 +534,7 @@ describe( 'ClipboardPipeline feature', () => { sinon.assert.calledWith( spy, editor.id ); } ); - it( 'should be propagated to contentInsertion event', () => { + it( 'should be propagated to contentInsertion event (when it\'s external content)', () => { const dataTransferMock = createDataTransfer( { 'text/html': '

external content

' } ); const contentInsertionSpy = sinon.spy(); @@ -550,6 +550,27 @@ describe( 'ClipboardPipeline feature', () => { sinon.assert.calledWith( contentInsertionSpy, null ); } ); + + it( 'should be propagated to contentInsertion event (when it\'s internal content)', () => { + const dataTransferMock = createDataTransfer( { + 'text/html': '

internal content

', + 'application/ckeditor5-editor-id': editor.id + } ); + + const contentInsertionSpy = sinon.spy(); + + clipboardPlugin.on( 'contentInsertion', ( evt, data ) => { + contentInsertionSpy( data.sourceEditorId ); + } ); + + viewDocument.fire( 'paste', { + dataTransfer: dataTransferMock, + preventDefault: () => {}, + stopPropagation: () => {} + } ); + + sinon.assert.calledWith( contentInsertionSpy, editor.id ); + } ); } ); function createDataTransfer( data ) {