diff --git a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts index fb0e61e966a..708826b597b 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -36,7 +36,11 @@ export function cloneModelForPaste(model: ReadonlyContentModelDocument) { /** * @internal */ -export function mergePasteContent(editor: IEditor, eventResult: BeforePasteEvent) { +export function mergePasteContent( + editor: IEditor, + eventResult: BeforePasteEvent, + isFirstPaste: boolean +) { const { fragment, domToModelOption, @@ -48,7 +52,7 @@ export function mergePasteContent(editor: IEditor, eventResult: BeforePasteEvent editor.formatContentModel( (model, context) => { - if (clipboardData.modelBeforePaste) { + if (!isFirstPaste && clipboardData.modelBeforePaste) { const clonedModel = cloneModelForPaste(clipboardData.modelBeforePaste); model.blocks = clonedModel.blocks; } diff --git a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts index 1028bc20765..d7b2d815c2d 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/paste.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/paste.ts @@ -22,7 +22,11 @@ export function paste( pasteTypeOrGetter: PasteTypeOrGetter = 'normal' ) { editor.focus(); + let isFirstPaste = false; + if (!clipboardData.modelBeforePaste) { + isFirstPaste = true; + editor.formatContentModel(model => { clipboardData.modelBeforePaste = cloneModelForPaste(model); @@ -64,7 +68,7 @@ export function paste( convertInlineCss(eventResult.fragment, htmlFromClipboard.globalCssRules); // 6. Merge pasted content into main Content Model - mergePasteContent(editor, eventResult); + mergePasteContent(editor, eventResult, isFirstPaste); } function createDOMFromHtml( diff --git a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts index 71efb5798c0..b8a4779b761 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts @@ -1,3 +1,4 @@ +import * as cloneModel from 'roosterjs-content-model-dom/lib/modelApi/editing/cloneModel'; import * as createDomToModelContextForSanitizing from '../../../lib/command/createModelFromHtml/createDomToModelContextForSanitizing'; import * as domToContentModel from 'roosterjs-content-model-dom/lib/domToModel/domToContentModel'; import * as getSegmentTextFormatFile from 'roosterjs-content-model-dom/lib/modelApi/editing/getSegmentTextFormat'; @@ -22,6 +23,7 @@ import { FormatContentModelOptions, InsertPoint, IEditor, + ClipboardData, } from 'roosterjs-content-model-types'; describe('mergePasteContent', () => { @@ -30,11 +32,12 @@ describe('mergePasteContent', () => { let formatContentModel: jasmine.Spy; let sourceModel: ContentModelDocument; let editor: IEditor; - const mockedClipboard = 'CLIPBOARD' as any; + let mockedClipboard: ClipboardData; beforeEach(() => { formatResult = undefined; context = undefined; + mockedClipboard = 'CLIPBOARD' as any; formatContentModel = jasmine .createSpy('formatContentModel') @@ -167,7 +170,7 @@ describe('mergePasteContent', () => { clipboardData: mockedClipboard, } as any; - mergePasteContent(editor, eventResult); + mergePasteContent(editor, eventResult, true); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -274,7 +277,7 @@ describe('mergePasteContent', () => { clipboardData: mockedClipboard, } as any; - mergePasteContent(editor, eventResult); + mergePasteContent(editor, eventResult, true); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -296,7 +299,7 @@ describe('mergePasteContent', () => { clipboardData: mockedClipboard, } as any; - mergePasteContent(editor, eventResult); + mergePasteContent(editor, eventResult, true); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -377,11 +380,15 @@ describe('mergePasteContent', () => { }, }); - mergePasteContent(editor, { - fragment: mockedFragment, - domToModelOption: mockedDefaultDomToModelOptions, - clipboardData: mockedClipboard, - } as any); + mergePasteContent( + editor, + { + fragment: mockedFragment, + domToModelOption: mockedDefaultDomToModelOptions, + clipboardData: mockedClipboard, + } as any, + true + ); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -439,7 +446,7 @@ describe('mergePasteContent', () => { containsBlockElements: true, } as any; - mergePasteContent(editor, eventResult); + mergePasteContent(editor, eventResult, true); expect(formatContentModel).toHaveBeenCalledTimes(1); expect(formatResult).toBeTrue(); @@ -483,13 +490,17 @@ describe('mergePasteContent', () => { para.segments.push(marker); addBlock(sourceModel, para); - mergePasteContent(editor, { - fragment, - containsBlockElements: true, - domToModelOption: {}, - pasteType: 'normal', - clipboardData: mockedClipboard, - }); + mergePasteContent( + editor, + { + fragment, + containsBlockElements: true, + domToModelOption: {}, + pasteType: 'normal', + clipboardData: mockedClipboard, + }, + true + ); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, @@ -876,12 +887,16 @@ describe('mergePasteContent', () => { para.segments.push(marker); sourceModel.blocks.push(para); - mergePasteContent(editor, { - fragment, - domToModelOption: {}, - pasteType: 'mergeFormat', - clipboardData: mockedClipboard, - } as any); + mergePasteContent( + editor, + { + fragment, + domToModelOption: {}, + pasteType: 'mergeFormat', + clipboardData: mockedClipboard, + } as any, + true + ); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, @@ -1245,12 +1260,16 @@ describe('mergePasteContent', () => { para.segments.push(marker); sourceModel.blocks.push(para); - mergePasteContent(editor, { - fragment, - domToModelOption: {}, - pasteType: 'asPlainText', - clipboardData: mockedClipboard, - } as any); + mergePasteContent( + editor, + { + fragment, + domToModelOption: {}, + pasteType: 'asPlainText', + clipboardData: mockedClipboard, + } as any, + true + ); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, @@ -1472,13 +1491,17 @@ describe('mergePasteContent', () => { para.segments.push(createText('Text in source'), marker); addBlock(sourceModel, para); - mergePasteContent(editor, { - fragment, - containsBlockElements: false, - domToModelOption: {}, - pasteType: 'normal', - clipboardData: mockedClipboard, - }); + mergePasteContent( + editor, + { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'normal', + clipboardData: mockedClipboard, + }, + true + ); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, @@ -1611,13 +1634,17 @@ describe('mergePasteContent', () => { para.segments.push(createText('Text in source'), marker); addBlock(sourceModel, para); - mergePasteContent(editor, { - fragment, - containsBlockElements: false, - domToModelOption: {}, - pasteType: 'mergeFormat', - clipboardData: mockedClipboard, - }); + mergePasteContent( + editor, + { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'mergeFormat', + clipboardData: mockedClipboard, + }, + true + ); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, @@ -1747,13 +1774,17 @@ describe('mergePasteContent', () => { para.segments.push(createText('Text in source'), marker); addBlock(sourceModel, para); - mergePasteContent(editor, { - fragment, - containsBlockElements: false, - domToModelOption: {}, - pasteType: 'asPlainText', - clipboardData: mockedClipboard, - }); + mergePasteContent( + editor, + { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'asPlainText', + clipboardData: mockedClipboard, + }, + true + ); expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, @@ -1842,4 +1873,169 @@ describe('mergePasteContent', () => { format: { fontFamily: 'Aptos', fontSize: '10pt', textColor: 'blue' }, }); }); + + it('do not clone model for first paste, and keep cache', () => { + const fragment = createPasteFragment( + document, + { text: 'text' } as any, + 'asPlainText', + document.body + ); + const div = document.createElement('div'); + const cloneModelSpy = spyOn(cloneModel, 'cloneModel').and.callThrough(); + + sourceModel = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Br', format: {} }], + format: {}, + cachedElement: div, + }, + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + }, + ], + format: {}, + }; + + const modelBeforePaste: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + }, + ], + }; + mockedClipboard = { + modelBeforePaste, + } as any; + + mergePasteContent( + editor, + { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'asPlainText', + clipboardData: mockedClipboard, + }, + true + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Br', format: {} }], + format: {}, + cachedElement: div, + }, + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'text', format: {} }, + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + ], + format: {}, + }, + ], + format: {}, + }); + expect(cloneModelSpy).not.toHaveBeenCalled(); + }); + + it('clone model for second paste, and clear cache', () => { + const fragment = createPasteFragment( + document, + { text: 'text' } as any, + 'asPlainText', + document.body + ); + const div = document.createElement('div'); + const cloneModelSpy = spyOn(cloneModel, 'cloneModel').and.callThrough(); + + sourceModel = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [{ segmentType: 'Br', format: {} }], + format: {}, + cachedElement: div, + }, + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + }, + ], + format: {}, + }; + + const modelBeforePaste: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + { segmentType: 'Br', format: {} }, + ], + format: {}, + }, + ], + }; + mockedClipboard = { + modelBeforePaste, + } as any; + + mergePasteContent( + editor, + { + fragment, + containsBlockElements: false, + domToModelOption: {}, + pasteType: 'asPlainText', + clipboardData: mockedClipboard, + }, + false + ); + + expect(sourceModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { segmentType: 'Text', text: 'text', format: {} }, + { segmentType: 'SelectionMarker', isSelected: true, format: {} }, + ], + format: {}, + cachedElement: undefined, + isImplicit: undefined, + }, + ], + format: {}, + }); + expect(cloneModelSpy).toHaveBeenCalledTimes(1); + expect(cloneModelSpy).toHaveBeenCalledWith(modelBeforePaste, { + includeCachedElement: jasmine.anything(), + } as any); + }); }); diff --git a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts index a6aab524bb7..2230d485c73 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/pasteTest.ts @@ -6,6 +6,7 @@ import * as generatePasteOptionFromPluginsFile from '../../../lib/command/paste/ import * as getPasteSourceF from 'roosterjs-content-model-plugins/lib/paste/pasteSourceValidations/getPasteSource'; import * as getSelectedSegmentsF from 'roosterjs-content-model-dom/lib/modelApi/selection/collectSelections'; import * as mergeModelFile from 'roosterjs-content-model-dom/lib/modelApi/editing/mergeModel'; +import * as mergePasteContentFile from '../../../lib/command/paste/mergePasteContent'; import * as PPT from 'roosterjs-content-model-plugins/lib/paste/PowerPoint/processPastedContentFromPowerPoint'; import * as setProcessorF from 'roosterjs-content-model-plugins/lib/paste/utils/setProcessor'; import * as WacComponents from 'roosterjs-content-model-plugins/lib/paste/WacComponents/processPastedContentWacComponents'; @@ -312,6 +313,7 @@ describe('paste with content model & paste plugin', () => { describe('Paste with clipboardData', () => { let editor: IEditor = undefined!; const ID = 'EDITOR_ID'; + let mergePasteContentSpy: jasmine.Spy; beforeEach(() => { editor = initEditor(ID); @@ -325,6 +327,7 @@ describe('Paste with clipboardData', () => { htmlFirstLevelChildTags: ['P', 'P'], html: '', }); + mergePasteContentSpy = spyOn(mergePasteContentFile, 'mergePasteContent').and.callThrough(); }); afterEach(() => { @@ -390,6 +393,33 @@ describe('Paste with clipboardData', () => { ], format: {}, }); + expect(mergePasteContentSpy.calls.argsFor(0)[2]).toBeTrue(); + }); + + it('Second paste', () => { + clipboardData.rawHtml = ''; + clipboardData.modelBeforePaste = { + blockGroupType: 'Document', + blocks: [], + }; + + paste(editor, clipboardData); + + const model = editor.getContentModelCopy('connected'); + + expectEqual(model, { + blockGroupType: 'Document', + blocks: [ + { + isImplicit: true, + segments: [{ isSelected: true, segmentType: 'SelectionMarker', format: {} }], + blockType: 'Paragraph', + format: {}, + }, + ], + format: {}, + }); + expect(mergePasteContentSpy.calls.argsFor(0)[2]).toBeFalse(); }); it('Remove unsupported url of link from clipboardContent', () => { @@ -437,6 +467,7 @@ describe('Paste with clipboardData', () => { ], format: {}, }); + expect(mergePasteContentSpy.calls.argsFor(0)[2]).toBeTrue(); }); it('Keep supported url of link from clipboardContent', () => { @@ -496,5 +527,6 @@ describe('Paste with clipboardData', () => { ], format: {}, }); + expect(mergePasteContentSpy.calls.argsFor(0)[2]).toBeTrue(); }); });