From 0eeccbe89c6e307e921d429aaccab5a98ca406d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Bagi=C5=84ski?= Date: Wed, 22 Jan 2025 18:28:37 +0100 Subject: [PATCH] Add `sourceEditorId` property to clipboard input events. (#17707) Feature (clipboard): Add ability to detect paste events originating from editor. Closes #15935 --- .../src/clipboardpipeline.ts | 14 +++ .../tests/clipboardpipeline.js | 118 +++++++++++++++++- .../tests/codeblockediting.js | 4 +- 3 files changed, 133 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-clipboard/src/clipboardpipeline.ts b/packages/ckeditor5-clipboard/src/clipboardpipeline.ts index 4c9a57f6a9c..7d731ede003 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'; + /** + * The 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..198dc70fd5e 100644 --- a/packages/ckeditor5-clipboard/tests/clipboardpipeline.js +++ b/packages/ckeditor5-clipboard/tests/clipboardpipeline.js @@ -462,10 +462,126 @@ 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 an 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 an 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 (when it\'s external content)', () => { + 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 ); + } ); + + 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 ) { + const state = Object.create( data || {} ); + return { getData( type ) { - return data[ type ]; + return state[ type ]; + }, + setData( type, newData ) { + state[ type ] = newData; } }; } 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 );