Skip to content

Commit

Permalink
Add sourceEditorId property to clipboard input events. (#17707)
Browse files Browse the repository at this point in the history
Feature (clipboard): Add ability to detect paste events originating from editor. Closes #15935
  • Loading branch information
Mati365 authored Jan 22, 2025
1 parent bf21d48 commit 0eeccbe
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 3 deletions.
14 changes: 14 additions & 0 deletions packages/ckeditor5-clipboard/src/clipboardpipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClipboardInputTransformationEvent>( eventInfo, {
content,
dataTransfer,
sourceEditorId,
targetRanges: data.targetRanges,
method: data.method as 'paste' | 'drop'
} );
Expand Down Expand Up @@ -278,6 +280,7 @@ export default class ClipboardPipeline extends Plugin {
this.fire<ClipboardContentInsertionEvent>( 'contentInsertion', {
content: modelFragment,
method: data.method,
sourceEditorId: data.sourceEditorId,
dataTransfer: data.dataTransfer,
targetRanges: data.targetRanges
} );
Expand Down Expand Up @@ -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' ) {
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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.
*/
Expand Down
118 changes: 117 additions & 1 deletion packages/ckeditor5-clipboard/tests/clipboardpipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': '<p>external content</p>' } );
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, '<paragraph>f[oo]bar</paragraph>' );

// 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, '<paragraph>f[oo]bar</paragraph>' );

// 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': '<p>external content</p>' } );
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': '<p>internal content</p>',
'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;
}
};
}
Expand Down
4 changes: 2 additions & 2 deletions packages/ckeditor5-code-block/tests/codeblockediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -1682,7 +1682,7 @@ describe( 'CodeBlockEditing', () => {
'[]o' +
'</codeBlock>' );

sinon.assert.calledOnce( dataTransferMock.getData );
sinon.assert.calledTwice( dataTransferMock.getData );

// Make sure that ClipboardPipeline was not interrupted.
sinon.assert.calledOnce( contentInsertionSpy );
Expand Down Expand Up @@ -1723,7 +1723,7 @@ describe( 'CodeBlockEditing', () => {
'<paragraph>bar</paragraph>'
);

sinon.assert.calledOnce( dataTransferMock.getData );
sinon.assert.calledTwice( dataTransferMock.getData );

// Make sure that ClipboardPipeline was not interrupted.
sinon.assert.calledOnce( contentInsertionSpy );
Expand Down

0 comments on commit 0eeccbe

Please sign in to comment.