diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts index d8e6a3b6770..0d4b921dda7 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/paste.ts @@ -53,7 +53,11 @@ export default function paste( getPasteType(pasteAsText, applyCurrentFormat, pasteAsImage) ); - const { pluginEvent, fragment } = triggerPluginEventAndCreatePasteFragment( + const { + domToModelOption, + fragment, + customizedMerge, + } = triggerPluginEventAndCreatePasteFragment( editor, clipboardData, null /* position */, @@ -63,15 +67,15 @@ export default function paste( ); const pasteModel = domToContentModel(fragment, { - ...pluginEvent.domToModelOption, + ...domToModelOption, additionalFormatParsers: { - ...pluginEvent.domToModelOption.additionalFormatParsers, + ...domToModelOption.additionalFormatParsers, block: [ - ...(pluginEvent.domToModelOption.additionalFormatParsers?.block || []), + ...(domToModelOption.additionalFormatParsers?.block || []), ...(applyCurrentFormat ? [blockElementParser] : []), ], listLevel: [ - ...(pluginEvent.domToModelOption.additionalFormatParsers?.listLevel || []), + ...(domToModelOption.additionalFormatParsers?.listLevel || []), ...(applyCurrentFormat ? [blockElementParser] : []), ], }, @@ -82,8 +86,8 @@ export default function paste( editor, 'Paste', model => { - if (pluginEvent.customizedMerge) { - pluginEvent.customizedMerge(model, pasteModel); + if (customizedMerge) { + customizedMerge(model, pasteModel); } else { mergeModel(model, pasteModel, getOnDeleteEntityCallback(editor), { mergeFormat: applyCurrentFormat ? 'keepSourceEmphasisFormat' : 'none', @@ -120,7 +124,7 @@ function createBeforePasteEventData( htmlAfter: '', htmlAttributes: {}, domToModelOption: {}, - pasteType: pasteType, + pasteType, }; } @@ -135,7 +139,7 @@ function triggerPluginEventAndCreatePasteFragment( pasteAsText: boolean, pasteAsImage: boolean, eventData: ContentModelBeforePasteEventData -): { pluginEvent: ContentModelBeforePasteEvent; fragment: DocumentFragment } { +): ContentModelBeforePasteEventData { const event = { eventType: PluginEventType.BeforePaste, ...eventData, @@ -163,17 +167,20 @@ function triggerPluginEventAndCreatePasteFragment( handleTextPaste(text, position, fragment); } - // Step 4: Trigger BeforePasteEvent so that plugins can do proper change before paste - const pluginEvent = editor.triggerPluginEvent( - PluginEventType.BeforePaste, - eventData, - true /* broadcast */ - ) as ContentModelBeforePasteEvent; + let pluginEvent: ContentModelBeforePasteEvent | undefined = undefined; + // Step 4: Trigger BeforePasteEvent so that plugins can do proper change before paste, when the type of paste is different than Plain Text + if (event.pasteType !== PasteType.AsPlainText) { + pluginEvent = editor.triggerPluginEvent( + PluginEventType.BeforePaste, + eventData, + true /* broadcast */ + ) as ContentModelBeforePasteEvent; + } // Step 5. Sanitize the fragment before paste to make sure the content is safe sanitizePasteContent(event, position); - return { fragment, pluginEvent }; + return pluginEvent || eventData; } /** diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts index 885815ae29f..99ace99056c 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts @@ -1,10 +1,21 @@ +import * as addParserF from '../../../lib/editor/plugins/PastePlugin/utils/addParser'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; +import * as ExcelF from '../../../lib/editor/plugins/PastePlugin/Excel/processPastedContentFromExcel'; +import * as getPasteSourceF from 'roosterjs-editor-dom/lib/pasteSourceValidations/getPasteSource'; import * as mergeModelFile from '../../../lib/modelApi/common/mergeModel'; -import paste from '../../../lib/publicApi/utils/paste'; -import { ClipboardData, PasteType } from 'roosterjs-editor-types'; +import * as pasteF from '../../../lib/publicApi/utils/paste'; +import * as PPT from '../../../lib/editor/plugins/PastePlugin/PowerPoint/processPastedContentFromPowerPoint'; +import * as setProcessorF from '../../../lib/editor/plugins/PastePlugin/utils/setProcessor'; +import * as WacComponents from '../../../lib/editor/plugins/PastePlugin/WacComponents/processPastedContentWacComponents'; +import * as WordDesktopFile from '../../../lib/editor/plugins/PastePlugin/WordDesktop/processPastedContentFromWordDesktop'; +import ContentModelEditor from '../../../lib/editor/ContentModelEditor'; +import ContentModelPastePlugin from '../../../lib/editor/plugins/PastePlugin/ContentModelPastePlugin'; +import { ClipboardData, KnownPasteSourceType, PasteType } from 'roosterjs-editor-types'; import { ContentModelDocument } from 'roosterjs-content-model-types'; import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +let clipboardData: ClipboardData; + describe('Paste ', () => { let editor: IContentModelEditor; let addUndoSnapshot: jasmine.Spy; @@ -25,18 +36,16 @@ describe('Paste ', () => { let div: HTMLDivElement; - const clipboardData: ClipboardData = { - types: ['image/png', 'text/html'], - text: '', - image: null!, - rawHtml: '\r\nteststringteststring\r\n', - customValues: {}, - imageDataUri: null!, - }; - beforeEach(() => { spyOn(domToContentModel, 'domToContentModel').and.callThrough(); - + clipboardData = { + types: ['image/png', 'text/html'], + text: '', + image: null!, + rawHtml: '\r\nteststringteststring\r\n', + customValues: {}, + imageDataUri: null!, + }; div = document.createElement('div'); document.body.appendChild(div); mockedModel = ({} as any) as ContentModelDocument; @@ -98,7 +107,7 @@ describe('Paste ', () => { }); it('Execute', () => { - paste(editor, clipboardData, false, false, false); + pasteF.default(editor, clipboardData, false, false, false); expect(setContentModel).toHaveBeenCalled(); expect(focus).toHaveBeenCalled(); @@ -111,4 +120,160 @@ describe('Paste ', () => { expect(mockedModel).toEqual(mockedMergeModel); expect(clipboardData).toEqual(undoSnapshotResult); }); + + it('Execute | As plain text', () => { + pasteF.default(editor, clipboardData, true /* asPlainText */, false, false); + + expect(setContentModel).toHaveBeenCalled(); + expect(focus).toHaveBeenCalled(); + expect(addUndoSnapshot).toHaveBeenCalled(); + expect(getFocusedPosition).not.toHaveBeenCalled(); + expect(getContent).toHaveBeenCalled(); + expect(triggerPluginEvent).not.toHaveBeenCalled(); + expect(getDocument).toHaveBeenCalled(); + expect(getTrustedHTMLHandler).toHaveBeenCalled(); + expect(mockedModel).toEqual(mockedMergeModel); + expect(clipboardData).toEqual(undoSnapshotResult); + }); +}); + +describe('paste with content model & paste plugin', () => { + let editor: ContentModelEditor | undefined; + let div: HTMLDivElement | undefined; + + beforeEach(() => { + div = document.createElement('div'); + document.body.appendChild(div); + editor = new ContentModelEditor(div, { + plugins: [new ContentModelPastePlugin()], + }); + spyOn(addParserF, 'default').and.callThrough(); + spyOn(setProcessorF, 'setProcessor').and.callThrough(); + clipboardData = { + types: ['image/png', 'text/html'], + text: '', + image: null!, + rawHtml: '\r\nteststringteststring\r\n', + customValues: {}, + imageDataUri: null!, + }; + }); + + afterEach(() => { + editor?.dispose(); + editor = undefined; + div?.remove(); + div = undefined; + }); + + it('Word Desktop', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.WordDesktop); + spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); + + pasteF.default(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(1); + expect(addParserF.default).toHaveBeenCalledTimes(4); + expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(1); + }); + + it('Word Online', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.WacComponents); + spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); + + pasteF.default(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(4); + expect(addParserF.default).toHaveBeenCalledTimes(5); + expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(1); + }); + + it('Excel Online', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.ExcelOnline); + spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); + + pasteF.default(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.default).toHaveBeenCalledTimes(2); + expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(1); + }); + + it('Excel Desktop', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.ExcelDesktop); + spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); + + pasteF.default(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.default).toHaveBeenCalledTimes(2); + expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(1); + }); + + it('PowerPoint', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.PowerPointDesktop); + spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); + + pasteF.default(editor!, clipboardData); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.default).toHaveBeenCalledTimes(1); + expect(PPT.processPastedContentFromPowerPoint).toHaveBeenCalledTimes(1); + }); + + // Plain Text + it('Word Desktop | Plain Text', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.WordDesktop); + spyOn(WordDesktopFile, 'processPastedContentFromWordDesktop').and.callThrough(); + + pasteF.default(editor!, clipboardData, true /* pasteAsText */); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.default).toHaveBeenCalledTimes(0); + expect(WordDesktopFile.processPastedContentFromWordDesktop).toHaveBeenCalledTimes(0); + }); + + it('Word Online | Plain Text', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.WacComponents); + spyOn(WacComponents, 'processPastedContentWacComponents').and.callThrough(); + + pasteF.default(editor!, clipboardData, true /* pasteAsText */); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.default).toHaveBeenCalledTimes(0); + expect(WacComponents.processPastedContentWacComponents).toHaveBeenCalledTimes(0); + }); + + it('Excel Online | Plain Text', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.ExcelOnline); + spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); + + pasteF.default(editor!, clipboardData, true /* pasteAsText */); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.default).toHaveBeenCalledTimes(0); + expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(0); + }); + + it('Excel Desktop | Plain Text', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.ExcelDesktop); + spyOn(ExcelF, 'processPastedContentFromExcel').and.callThrough(); + + pasteF.default(editor!, clipboardData, true /* pasteAsText */); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.default).toHaveBeenCalledTimes(0); + expect(ExcelF.processPastedContentFromExcel).toHaveBeenCalledTimes(0); + }); + + it('PowerPoint | Plain Text', () => { + spyOn(getPasteSourceF, 'default').and.returnValue(KnownPasteSourceType.PowerPointDesktop); + spyOn(PPT, 'processPastedContentFromPowerPoint').and.callThrough(); + + pasteF.default(editor!, clipboardData, true /* pasteAsText */); + + expect(setProcessorF.setProcessor).toHaveBeenCalledTimes(0); + expect(addParserF.default).toHaveBeenCalledTimes(0); + expect(PPT.processPastedContentFromPowerPoint).toHaveBeenCalledTimes(0); + }); }); diff --git a/packages/roosterjs-editor-core/lib/coreApi/createPasteFragment.ts b/packages/roosterjs-editor-core/lib/coreApi/createPasteFragment.ts index e687aa251ef..7a1b98e8bc3 100644 --- a/packages/roosterjs-editor-core/lib/coreApi/createPasteFragment.ts +++ b/packages/roosterjs-editor-core/lib/coreApi/createPasteFragment.ts @@ -125,8 +125,10 @@ function createFragmentFromClipboardData( handleTextPaste(text, position, fragment); } - // Step 4: Trigger BeforePasteEvent so that plugins can do proper change before paste - core.api.triggerEvent(core, event, true /*broadcast*/); + // Step 4: Trigger BeforePasteEvent so that plugins can do proper change before paste, when the type of paste is different than Plain Text + if (event.pasteType !== PasteType.AsPlainText) { + core.api.triggerEvent(core, event, true /*broadcast*/); + } // Step 5. Sanitize the fragment before paste to make sure the content is safe sanitizePasteContent(event, position); diff --git a/packages/roosterjs-editor-core/test/coreApi/createPasteFragmentTest.ts b/packages/roosterjs-editor-core/test/coreApi/createPasteFragmentTest.ts index 8cf5f6fb80c..60df016dbb3 100644 --- a/packages/roosterjs-editor-core/test/coreApi/createPasteFragmentTest.ts +++ b/packages/roosterjs-editor-core/test/coreApi/createPasteFragmentTest.ts @@ -480,6 +480,27 @@ describe('createPasteFragment', () => { expect(html.trim()).toBe('teststringteststring'); expect(clipboardData.htmlFirstLevelChildTags).toEqual(['', 'IMG', '']); }); + + it('Skip triggerEvent on Plain text paste', () => { + const triggerEvent = jasmine.createSpy(); + const core = createEditorCore(div, { + coreApiOverride: { + triggerEvent, + }, + }); + + const clipboardData: ClipboardData = { + types: ['image/png', 'text/html'], + text: '', + image: null, + rawHtml: '\r\nteststringteststring\r\n', + customValues: {}, + imageDataUri: null, + }; + createPasteFragment(core, clipboardData, null, true /* plainText */, false, false); + + expect(triggerEvent).not.toHaveBeenCalled(); + }); }); function getHTML(fragment: DocumentFragment) {