From 25b380b2fe03506798ec58961ac9565a77eefc26 Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Wed, 15 Jul 2026 15:52:05 -0300 Subject: [PATCH 1/4] do not pasted image --- .../lib/command/paste/mergePasteContent.ts | 25 +++- .../command/paste/mergePasteContentTest.ts | 111 +++++++++++++++++- .../command/paste/retrieveHtmlInfoTest.ts | 2 +- 3 files changed, 134 insertions(+), 4 deletions(-) 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..3c3f96648a11 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -7,6 +7,8 @@ import { getSelectedSegments, mergeModel, cloneModelForPaste, + isNodeOfType, + isElementOfType, } from 'roosterjs-content-model-dom'; import type { BeforePasteEvent, @@ -37,6 +39,8 @@ export function mergePasteContent( containsBlockElements, } = eventResult; + const shouldScrollCaretIntoView = !isImageFragment(fragment); + editor.formatContentModel( (model, context) => { if (!isFirstPaste && clipboardData.modelBeforePaste) { @@ -80,7 +84,7 @@ export function mergePasteContent( { changeSource: ChangeSource.Paste, getChangeData: () => clipboardData, - scrollCaretIntoView: true, + scrollCaretIntoView: shouldScrollCaretIntoView, apiName: 'paste', } ); @@ -137,3 +141,22 @@ function getLastSegmentFormat(pasteModel: ContentModelDocument): ContentModelSeg return {}; } + +function isImageFragment(pasteFragment: DocumentFragment): boolean { + const childNodes = pasteFragment.childNodes; + if (childNodes.length === 1 && isNodeOfType(childNodes[0], 'ELEMENT_NODE')) { + if (isElementOfType(childNodes[0], 'img')) { + return true; + } + if (isElementOfType(childNodes[0], 'span') || isElementOfType(childNodes[0], 'div')) { + const child = childNodes[0]; + return ( + child.childNodes.length === 1 && + isNodeOfType(child.childNodes[0], 'ELEMENT_NODE') && + isElementOfType(child.childNodes[0], 'img') + ); + } + } + + return false; +} 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..c98c9fe0971c 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!(); @@ -379,7 +387,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 +460,7 @@ describe('mergePasteContent', () => { domToModelOption: { additionalAllowedTags: [] }, clipboardData: mockedClipboard, containsBlockElements: true, + fragment: document.createDocumentFragment(), } as any; mergePasteContent(editor, eventResult, true); @@ -2043,4 +2052,102 @@ 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 scroll caret into view when fragment is a single element other than img/span/div', () => { + const anchor = document.createElement('a'); + anchor.appendChild(document.createElement('img')); + + runTest(createFragment(anchor), true); + }); + }); }); diff --git a/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts b/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts index c66545d8b1ef..8edf39544daa 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/retrieveHtmlInfoTest.ts @@ -217,7 +217,7 @@ describe('retrieveHtmlInfo', () => { { selectors: ['test'], text: - 'border-width: medium; border-style: none; border-color: currentcolor; border-image: initial;', + 'border-width: medium; border-style: none; border-color: currentcolor; border-image: none;', }, ], metadata: {}, From a14ed1f961a21ed403bf2175fd404236b9e3b791 Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Wed, 15 Jul 2026 16:16:54 -0300 Subject: [PATCH 2/4] fix test --- .../test/command/paste/mergePasteContentTest.ts | 3 +++ 1 file changed, 3 insertions(+) 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 c98c9fe0971c..4d83621319a2 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts @@ -183,6 +183,7 @@ describe('mergePasteContent', () => { pasteType: 'normal', domToModelOption: { additionalAllowedTags: [] }, clipboardData: mockedClipboard, + fragment: document.createDocumentFragment(), } as any; mergePasteContent(editor, eventResult, true); @@ -290,6 +291,7 @@ describe('mergePasteContent', () => { domToModelOption: { additionalAllowedTags: [] }, customizedMerge, clipboardData: mockedClipboard, + fragment: document.createDocumentFragment(), } as any; mergePasteContent(editor, eventResult, true); @@ -312,6 +314,7 @@ describe('mergePasteContent', () => { pasteType: 'mergeFormat', domToModelOption: { additionalAllowedTags: [] }, clipboardData: mockedClipboard, + fragment: document.createDocumentFragment(), } as any; mergePasteContent(editor, eventResult, true); From 3abbfe420a7ea051c90e7dcbc48413c0fb923053 Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Thu, 16 Jul 2026 10:50:16 -0300 Subject: [PATCH 3/4] Rerun --- .../test/command/paste/mergePasteContentTest.ts | 2 -- 1 file changed, 2 deletions(-) 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 4d83621319a2..f0b5a19dac4e 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts @@ -2142,14 +2142,12 @@ describe('mergePasteContent', () => { 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 scroll caret into view when fragment is a single element other than img/span/div', () => { const anchor = document.createElement('a'); anchor.appendChild(document.createElement('img')); - runTest(createFragment(anchor), true); }); }); From 418a08337b9a37d57b33caf640ec8bb23e3776f6 Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Mon, 20 Jul 2026 18:10:11 -0300 Subject: [PATCH 4/4] fix comments --- .../lib/command/paste/mergePasteContent.ts | 28 ++++++------------- .../command/paste/mergePasteContentTest.ts | 4 +-- 2 files changed, 10 insertions(+), 22 deletions(-) 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 3c3f96648a11..32fa0ddf6607 100644 --- a/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts +++ b/packages/roosterjs-content-model-core/lib/command/paste/mergePasteContent.ts @@ -7,8 +7,6 @@ import { getSelectedSegments, mergeModel, cloneModelForPaste, - isNodeOfType, - isElementOfType, } from 'roosterjs-content-model-dom'; import type { BeforePasteEvent, @@ -39,7 +37,7 @@ export function mergePasteContent( containsBlockElements, } = eventResult; - const shouldScrollCaretIntoView = !isImageFragment(fragment); + const shouldScrollCaretIntoView = !isImageOnlyFragment(fragment); editor.formatContentModel( (model, context) => { @@ -142,21 +140,11 @@ function getLastSegmentFormat(pasteModel: ContentModelDocument): ContentModelSeg return {}; } -function isImageFragment(pasteFragment: DocumentFragment): boolean { - const childNodes = pasteFragment.childNodes; - if (childNodes.length === 1 && isNodeOfType(childNodes[0], 'ELEMENT_NODE')) { - if (isElementOfType(childNodes[0], 'img')) { - return true; - } - if (isElementOfType(childNodes[0], 'span') || isElementOfType(childNodes[0], 'div')) { - const child = childNodes[0]; - return ( - child.childNodes.length === 1 && - isNodeOfType(child.childNodes[0], 'ELEMENT_NODE') && - isElementOfType(child.childNodes[0], 'img') - ); - } - } - - return false; +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 f0b5a19dac4e..f50ecbb4ee0b 100644 --- a/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts +++ b/packages/roosterjs-content-model-core/test/command/paste/mergePasteContentTest.ts @@ -2145,10 +2145,10 @@ describe('mergePasteContent', () => { runTest(createFragment(span), true); }); - it('should scroll caret into view when fragment is a single element other than img/span/div', () => { + 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), true); + runTest(createFragment(anchor), false); }); }); });