Skip to content

Commit

Permalink
Skip Trigger plugin event on plain text paste (#2011)
Browse files Browse the repository at this point in the history
* init

* remove unneeded change

* Update return type
  • Loading branch information
BryanValverdeU authored Aug 8, 2023
1 parent d6a390c commit 795c00d
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 */,
Expand All @@ -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] : []),
],
},
Expand All @@ -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',
Expand Down Expand Up @@ -120,7 +124,7 @@ function createBeforePasteEventData(
htmlAfter: '',
htmlAttributes: {},
domToModelOption: {},
pasteType: pasteType,
pasteType,
};
}

Expand All @@ -135,7 +139,7 @@ function triggerPluginEventAndCreatePasteFragment(
pasteAsText: boolean,
pasteAsImage: boolean,
eventData: ContentModelBeforePasteEventData
): { pluginEvent: ContentModelBeforePasteEvent; fragment: DocumentFragment } {
): ContentModelBeforePasteEventData {
const event = {
eventType: PluginEventType.BeforePaste,
...eventData,
Expand Down Expand Up @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,18 +36,16 @@ describe('Paste ', () => {

let div: HTMLDivElement;

const clipboardData: ClipboardData = {
types: ['image/png', 'text/html'],
text: '',
image: <File>null!,
rawHtml: '<html>\r\n<body>teststring<img src="" />teststring</body>\r\n</html>',
customValues: {},
imageDataUri: <string>null!,
};

beforeEach(() => {
spyOn(domToContentModel, 'domToContentModel').and.callThrough();

clipboardData = {
types: ['image/png', 'text/html'],
text: '',
image: <File>null!,
rawHtml: '<html>\r\n<body>teststring<img src="" />teststring</body>\r\n</html>',
customValues: {},
imageDataUri: <string>null!,
};
div = document.createElement('div');
document.body.appendChild(div);
mockedModel = ({} as any) as ContentModelDocument;
Expand Down Expand Up @@ -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();
Expand All @@ -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: <File>null!,
rawHtml: '<html>\r\n<body>teststring<img src="" />teststring</body>\r\n</html>',
customValues: {},
imageDataUri: <string>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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,27 @@ describe('createPasteFragment', () => {
expect(html.trim()).toBe('teststring<img src="">teststring');
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: '<html>\r\n<body>teststring<img src="" />teststring</body>\r\n</html>',
customValues: {},
imageDataUri: null,
};
createPasteFragment(core, clipboardData, null, true /* plainText */, false, false);

expect(triggerEvent).not.toHaveBeenCalled();
});
});

function getHTML(fragment: DocumentFragment) {
Expand Down

0 comments on commit 795c00d

Please sign in to comment.