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 9b80f37aef2e..32fa0ddf6607 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -37,6 +37,8 @@ export function mergePasteContent( containsBlockElements, } = eventResult; + const shouldScrollCaretIntoView = !isImageOnlyFragment(fragment); + editor.formatContentModel( (model, context) => { if (!isFirstPaste && clipboardData.modelBeforePaste) { @@ -80,7 +82,7 @@ export function mergePasteContent( { changeSource: ChangeSource.Paste, getChangeData: () => clipboardData, - scrollCaretIntoView: true, + scrollCaretIntoView: shouldScrollCaretIntoView, apiName: 'paste', } ); @@ -137,3 +139,12 @@ function getLastSegmentFormat(pasteModel: ContentModelDocument): ContentModelSeg return {}; } + +function isImageOnlyFragment(pasteFragment: DocumentFragment): boolean { + const images = pasteFragment.querySelectorAll('img'); + return ( + images.length === 1 && + pasteFragment.childNodes.length === 1 && + pasteFragment.textContent?.trim() === '' + ); +} 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 d56a2b01e55c..f50ecbb4ee0b 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts @@ -35,11 +35,18 @@ describe('mergePasteContent', () => { let editor: IEditor; let mockedClipboard: ClipboardData; let mockedDOMHelper: DOMHelper; + let formatOptions: FormatContentModelOptions | undefined; beforeEach(() => { formatResult = undefined; context = undefined; - mockedClipboard = 'CLIPBOARD' as any; + formatOptions = undefined; + mockedClipboard = { + fragment: { + childNodes: [], + length: 0, + }, + } as any; formatContentModel = jasmine .createSpy('formatContentModel') @@ -49,6 +56,7 @@ describe('mergePasteContent', () => { deletedEntities: [], newImages: [], }; + formatOptions = options; formatResult = callback(sourceModel, context); const changedData = options.getChangeData!(); @@ -175,6 +183,7 @@ describe('mergePasteContent', () => { pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, clipboardData: mockedClipboard, + fragment: document.createDocumentFragment(), } as any; mergePasteContent(editor, eventResult, true); @@ -282,6 +291,7 @@ describe('mergePasteContent', () => { domToModelOption: { additionalAllowedTags: [] }, customizedMerge, clipboardData: mockedClipboard, + fragment: document.createDocumentFragment(), } as any; mergePasteContent(editor, eventResult, true); @@ -304,6 +314,7 @@ describe('mergePasteContent', () => { pasteType: 'mergeFormat', domToModelOption: { additionalAllowedTags: [] }, clipboardData: mockedClipboard, + fragment: document.createDocumentFragment(), } as any; mergePasteContent(editor, eventResult, true); @@ -379,7 +390,7 @@ describe('mergePasteContent', () => { const mockedDomToModelOptions = 'OPTION1' as any; const mockedDefaultDomToModelOptions = 'OPTIONS3' as any; - const mockedFragment = 'FRAGMENT' as any; + const mockedFragment = document.createDocumentFragment(); (editor as any).getEnvironment = () => ({ domToModelSettings: { @@ -452,6 +463,7 @@ describe('mergePasteContent', () => { domToModelOption: { additionalAllowedTags: [] }, clipboardData: mockedClipboard, containsBlockElements: true, + fragment: document.createDocumentFragment(), } as any; mergePasteContent(editor, eventResult, true); @@ -2043,4 +2055,100 @@ describe('mergePasteContent', () => { }); expect(cloneModelSpy).toHaveBeenCalledTimes(1); }); + + describe('scrollCaretIntoView based on paste fragment', () => { + function runTest(fragment: DocumentFragment, expectedScrollCaretIntoView: boolean) { + spyOn(mergeModelFile, 'mergeModel').and.callThrough(); + sourceModel = createContentModelDocument(); + const para = createParagraph(); + para.segments.push(createSelectionMarker()); + sourceModel.blocks.push(para); + + mergePasteContent( + editor, + { + fragment, + domToModelOption: {}, + pasteType: 'normal', + clipboardData: mockedClipboard, + }, + true + ); + + expect(formatOptions).toBeDefined(); + expect(formatOptions!.scrollCaretIntoView).toBe(expectedScrollCaretIntoView); + } + + function createFragment(...nodes: Node[]): DocumentFragment { + const fragment = document.createDocumentFragment(); + nodes.forEach(node => fragment.appendChild(node)); + return fragment; + } + + it('should not scroll caret into view when fragment is a single image', () => { + const img = document.createElement('img'); + img.src = 'test.png'; + + runTest(createFragment(img), false); + }); + + it('should not scroll caret into view when fragment is a span wrapping an image', () => { + const span = document.createElement('span'); + span.appendChild(document.createElement('img')); + + runTest(createFragment(span), false); + }); + + it('should not scroll caret into view when fragment is a div wrapping an image', () => { + const div = document.createElement('div'); + div.appendChild(document.createElement('img')); + + runTest(createFragment(div), false); + }); + + it('should scroll caret into view when fragment is a single text node', () => { + runTest(createFragment(document.createTextNode('text')), true); + }); + + it('should scroll caret into view when fragment is a paragraph', () => { + const p = document.createElement('p'); + p.textContent = 'text'; + + runTest(createFragment(p), true); + }); + + it('should scroll caret into view when fragment has multiple children', () => { + const img1 = document.createElement('img'); + const img2 = document.createElement('img'); + + runTest(createFragment(img1, img2), true); + }); + + it('should scroll caret into view when span wraps multiple children', () => { + const span = document.createElement('span'); + span.appendChild(document.createElement('img')); + span.appendChild(document.createElement('img')); + + runTest(createFragment(span), true); + }); + + it('should scroll caret into view when span wraps a non-image element', () => { + const span = document.createElement('span'); + span.appendChild(document.createElement('b')); + + runTest(createFragment(span), true); + }); + + it('should scroll caret into view when span wraps a text node', () => { + const span = document.createElement('span'); + span.appendChild(document.createTextNode('text')); + runTest(createFragment(span), true); + }); + + it('should not scroll caret into view when fragment is an anchor wrapping an image', () => { + const anchor = document.createElement('a'); + anchor.appendChild(document.createElement('img')); + runTest(createFragment(anchor), false); + }); + }); });