From 95d6400fd90d87c9070ce56f81cd63f1eedb11d6 Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Mon, 3 Aug 2026 16:24:43 -0300 Subject: [PATCH 1/5] drag and drop --- .../lib/dragAndDrop/DragAndDropPlugin.ts | 8 +++ .../test/dragAndDrop/DragAndDropPluginTest.ts | 58 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts index e09254bcc13e..cf76263a9c03 100644 --- a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts @@ -61,6 +61,14 @@ export class DragAndDropPlugin implements EditorPlugin { } }, }, + blur: { + beforeDispatch: _ev => { + if (this.internalDrag) { + this.editor?.takeSnapshot(); + this.internalDrag = false; + } + }, + }, }); } diff --git a/packages/roosterjs-content-model-plugins/test/dragAndDrop/DragAndDropPluginTest.ts b/packages/roosterjs-content-model-plugins/test/dragAndDrop/DragAndDropPluginTest.ts index 972640282804..e1d25b1cedfa 100644 --- a/packages/roosterjs-content-model-plugins/test/dragAndDrop/DragAndDropPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/dragAndDrop/DragAndDropPluginTest.ts @@ -11,6 +11,7 @@ describe('DragAndDropPlugin', () => { let isExperimentalFeatureEnabledSpy: jasmine.Spy; let eventMap: Record; let getDOMSelectionSpy: jasmine.Spy; + let takeSnapshotSpy: jasmine.Spy; beforeEach(() => { disposerSpy = jasmine.createSpy('disposer'); @@ -22,11 +23,13 @@ describe('DragAndDropPlugin', () => { .createSpy('isExperimentalFeatureEnabled') .and.returnValue(true); getDOMSelectionSpy = jasmine.createSpy('getDOMSelection'); + takeSnapshotSpy = jasmine.createSpy('takeSnapshot'); editor = ({ attachDomEvent: attachDomEventSpy, isExperimentalFeatureEnabled: isExperimentalFeatureEnabledSpy, getDOMSelection: getDOMSelectionSpy, + takeSnapshot: takeSnapshotSpy, } as any) as IEditor; }); @@ -46,6 +49,7 @@ describe('DragAndDropPlugin', () => { expect(attachDomEventSpy).toHaveBeenCalled(); expect(eventMap.dragstart).toBeDefined(); + expect(eventMap.blur).toBeDefined(); }); it('should initialize with custom forbidden elements', () => { @@ -94,6 +98,60 @@ describe('DragAndDropPlugin', () => { }); }); + describe('blur event', () => { + it('should take a snapshot and reset the internal drag flag when blurred during an internal drag', () => { + spyOn(handleDroppedInternalContentFile, 'handleDroppedInternalContent'); + plugin = new DragAndDropPlugin(); + plugin.initialize(editor); + + const target = document.createElement('div'); + eventMap.dragstart.beforeDispatch({ target } as any); + + eventMap.blur.beforeDispatch({} as any); + + expect(takeSnapshotSpy).toHaveBeenCalledTimes(1); + + // Internal drag flag should be reset, so a subsequent drop is treated as external + const handleDroppedExternalContentSpy = spyOn( + handleDroppedContentFile, + 'handleDroppedExternalContent' + ); + + plugin.onPluginEvent({ + eventType: 'beforeDrop', + rawEvent: { + dataTransfer: { + getData: () => '
test
', + }, + } as any, + }); + + expect(handleDroppedExternalContentSpy).toHaveBeenCalled(); + }); + + it('should not take a snapshot when blurred without an internal drag', () => { + plugin = new DragAndDropPlugin(); + plugin.initialize(editor); + + eventMap.blur.beforeDispatch({} as any); + + expect(takeSnapshotSpy).not.toHaveBeenCalled(); + }); + + it('should only take a snapshot once for consecutive blur events during a single internal drag', () => { + plugin = new DragAndDropPlugin(); + plugin.initialize(editor); + + const target = document.createElement('div'); + eventMap.dragstart.beforeDispatch({ target } as any); + + eventMap.blur.beforeDispatch({} as any); + eventMap.blur.beforeDispatch({} as any); + + expect(takeSnapshotSpy).toHaveBeenCalledTimes(1); + }); + }); + describe('onPluginEvent - beforeDrop', () => { let handleDroppedExternalContentSpy: jasmine.Spy; From 1c8187604155ab9c3583df8b9d3e66a2e9428259 Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Tue, 4 Aug 2026 15:24:35 -0300 Subject: [PATCH 2/5] dispose --- .../lib/dragAndDrop/DragAndDropPlugin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts index cf76263a9c03..8e41d2ee947d 100644 --- a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts @@ -84,6 +84,7 @@ export class DragAndDropPlugin implements EditorPlugin { this.disposer = null; } this.forbiddenElements = []; + this.internalDrag = false; } /** From 4131a7b378ef656161de12d574a2dc5f03eda2ca Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Fri, 7 Aug 2026 15:05:05 -0300 Subject: [PATCH 3/5] merge master --- .../utils/handleDroppedInternalContent.ts | 6 +- .../utils/handleDroppedInternalContentTest.ts | 111 ++++++++++++++++++ yarn.lock | 16 +-- 3 files changed, 123 insertions(+), 10 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/utils/handleDroppedInternalContent.ts b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/utils/handleDroppedInternalContent.ts index 8e7b64f86044..6fe0f8ad86af 100644 --- a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/utils/handleDroppedInternalContent.ts +++ b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/utils/handleDroppedInternalContent.ts @@ -29,8 +29,10 @@ export function handleDroppedInternalContent(editor: IEditor, event: DragEvent): const cloneModel = cloneModelForPaste(model); trimModelForSelection(cloneModel, selection); - if (deleteSelection(model, [], context).deleteResult == 'range') { - normalizeContentModel(model); + if (!event.ctrlKey && !event.metaKey) { + if (deleteSelection(model, [], context).deleteResult == 'range') { + normalizeContentModel(model); + } } const startMarker = createSelectionMarker(insertPoint.marker.format); diff --git a/packages/roosterjs-content-model-plugins/test/dragAndDrop/utils/handleDroppedInternalContentTest.ts b/packages/roosterjs-content-model-plugins/test/dragAndDrop/utils/handleDroppedInternalContentTest.ts index 0c181e3f96b7..d8d14850d477 100644 --- a/packages/roosterjs-content-model-plugins/test/dragAndDrop/utils/handleDroppedInternalContentTest.ts +++ b/packages/roosterjs-content-model-plugins/test/dragAndDrop/utils/handleDroppedInternalContentTest.ts @@ -1,4 +1,5 @@ import * as cloneModelFile from 'roosterjs-content-model-dom/lib/modelApi/editing/cloneModel'; +import * as deleteSelectionFile from 'roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection'; import * as formatInsertPointWithContentModelFile from 'roosterjs-content-model-api/lib/publicApi/utils/formatInsertPointWithContentModel'; import * as getNodePositionFromEventFile from 'roosterjs-content-model-dom/lib/domUtils/event/getNodePositionFromEvent'; import * as trimModelForSelectionFile from 'roosterjs-content-model-dom/lib/domUtils/selection/trimModelForSelection'; @@ -896,3 +897,113 @@ describe('handleDroppedInternalContent - model verification', () => { expect(allText).toContain('cell content'); }); }); + +describe('handleDroppedInternalContent - ctrl key', () => { + let editor: IEditor; + let doc: Document; + let getNodePositionFromEventSpy: jasmine.Spy; + let getDOMHelperSpy: jasmine.Spy; + let getDOMSelectionSpy: jasmine.Spy; + let deleteSelectionSpy: jasmine.Spy; + let selection: DOMSelection; + let capturedCallback: + | (( + model: ContentModelDocument, + context: FormatContentModelContext, + insertPoint?: InsertPoint + ) => void) + | null; + + beforeEach(() => { + doc = document; + capturedCallback = null; + selection = { type: 'range' } as any; + + getNodePositionFromEventSpy = spyOn( + getNodePositionFromEventFile, + 'getNodePositionFromEvent' + ); + + spyOn( + formatInsertPointWithContentModelFile, + 'formatInsertPointWithContentModel' + ).and.callFake((_editor: any, _insertPoint: any, callback: any) => { + capturedCallback = callback; + }); + + // Stub the clone/trim helpers so the merge does not depend on real content, and + // spy on the delete helpers to verify whether the original selection is removed. + spyOn(cloneModelFile, 'cloneModelForPaste').and.returnValue(createContentModelDocument()); + spyOn(trimModelForSelectionFile, 'trimModelForSelection'); + deleteSelectionSpy = spyOn(deleteSelectionFile, 'deleteSelection').and.returnValue({ + deleteResult: 'range', + } as any); + + getDOMHelperSpy = jasmine.createSpy('getDOMHelper').and.returnValue({}); + getDOMSelectionSpy = jasmine.createSpy('getDOMSelection').and.returnValue(selection); + + editor = ({ + getDocument: () => doc, + getDOMHelper: getDOMHelperSpy, + getDOMSelection: getDOMSelectionSpy, + } as any) as IEditor; + }); + + function createInsertPointModel(): { + model: ContentModelDocument; + insertPoint: InsertPoint; + } { + const model = createContentModelDocument(); + const paragraph = createParagraph(); + const marker = createSelectionMarker(); + + paragraph.segments.push(marker); + model.blocks.push(paragraph); + + const insertPoint: InsertPoint = { + marker, + paragraph, + path: [model], + }; + + return { model, insertPoint }; + } + + function runWithCtrlKey(ctrlKey: boolean): ContentModelDocument { + const textNode = document.createTextNode('existing'); + getNodePositionFromEventSpy.and.returnValue({ + node: textNode, + offset: 0, + }); + + const event = { + x: 0, + y: 0, + ctrlKey, + preventDefault: jasmine.createSpy('preventDefault'), + stopPropagation: jasmine.createSpy('stopPropagation'), + } as any; + + handleDroppedInternalContent(editor, event); + + expect(capturedCallback).not.toBeNull(); + + const { model, insertPoint } = createInsertPointModel(); + + capturedCallback!(model, {} as FormatContentModelContext, insertPoint); + + return model; + } + + it('should not delete the original selection when ctrl key is pressed', () => { + runWithCtrlKey(true); + + expect(deleteSelectionSpy).not.toHaveBeenCalled(); + }); + + it('should delete the original selection when ctrl key is not pressed', () => { + const model = runWithCtrlKey(false); + + expect(deleteSelectionSpy).toHaveBeenCalledWith(model, [], jasmine.anything()); + }); +}); diff --git a/yarn.lock b/yarn.lock index f6ec0d0f1974..329f3aa85707 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2004,7 +2004,7 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3 dependencies: ms "2.1.2" -debug@^4.3.1: +debug@^4.3.1, debug@~4.4.1: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -2863,9 +2863,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.1.4" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.4.tgz#3b3daf9ce68f41f956df0b505132c0cfce9ec7af" - integrity sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw== + version "3.1.5" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.5.tgz#610f37419a030270430cecd68d74e3d4d96725d0" + integrity sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw== fastq@^1.6.0: version "1.15.0" @@ -6193,12 +6193,12 @@ socket.io-adapter@~2.5.2: ws "~8.17.1" socket.io-parser@~4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" - integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + version "4.2.7" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.7.tgz#679e51fe24d1c81df90fc5f7efe4a5f432fe99c0" + integrity sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg== dependencies: "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" + debug "~4.4.1" socket.io@^4.2.0: version "4.7.5" From 8c46efdd88a6efbd0043b34e73c4b5d35db682bc Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Fri, 7 Aug 2026 20:50:32 -0300 Subject: [PATCH 4/5] drga out of th editor --- .../lib/constants/ChangeSource.ts | 5 ++ .../lib/dragAndDrop/DragAndDropPlugin.ts | 54 +++++++++++-- .../test/dragAndDrop/DragAndDropPluginTest.ts | 75 +++++++++++-------- 3 files changed, 96 insertions(+), 38 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/constants/ChangeSource.ts b/packages/roosterjs-content-model-dom/lib/constants/ChangeSource.ts index cbaa372038f1..e2004df5624b 100644 --- a/packages/roosterjs-content-model-dom/lib/constants/ChangeSource.ts +++ b/packages/roosterjs-content-model-dom/lib/constants/ChangeSource.ts @@ -70,4 +70,9 @@ export const ChangeSource = { * Content changed by replace */ Replace: 'Replace', + + /** + * Content changed by dragging content out the editor + */ + DragOutOfEditor: 'DragOutOfEditor', }; diff --git a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts index 8e41d2ee947d..7623084f808e 100644 --- a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts @@ -1,6 +1,16 @@ import { handleDroppedExternalContent } from './utils/handleDroppedExternalContent'; -import type { EditorPlugin, IEditor, PluginEvent } from 'roosterjs-content-model-types'; +import type { + EditorPlugin, + IEditor, + PluginEvent, + ReadonlyContentModelSegment, +} from 'roosterjs-content-model-types'; import { handleDroppedInternalContent } from './utils/handleDroppedInternalContent'; +import { + ChangeSource, + getNodePositionFromEvent, + getSelectedSegments, +} from 'roosterjs-content-model-dom'; /** * Options for DragAndDrop plugin @@ -26,6 +36,7 @@ export class DragAndDropPlugin implements EditorPlugin { private forbiddenElements: string[] = []; private internalDrag: boolean = false; private disposer: (() => void) | null = null; + private lastSelectSegments: ReadonlyContentModelSegment[] = []; /** * Construct a new instance of DragAndDropPlugin @@ -59,13 +70,17 @@ export class DragAndDropPlugin implements EditorPlugin { ) { this.adjustDraggingCursor(this.editor, ev as DragEvent); } + editor.formatContentModel(model => { + this.lastSelectSegments = getSelectedSegments(model, false); + return false; + }); }, }, - blur: { - beforeDispatch: _ev => { + dragend: { + beforeDispatch: ev => { if (this.internalDrag) { - this.editor?.takeSnapshot(); - this.internalDrag = false; + const dropEvent = ev as DragEvent; + this.triggerDragOutOfTheEditor(editor, dropEvent); } }, }, @@ -85,6 +100,7 @@ export class DragAndDropPlugin implements EditorPlugin { } this.forbiddenElements = []; this.internalDrag = false; + this.lastSelectSegments = []; } /** @@ -101,6 +117,7 @@ export class DragAndDropPlugin implements EditorPlugin { this.editor.isExperimentalFeatureEnabled('HandleDropInternalContent') ) { handleDroppedInternalContent(this.editor, dropEvent); + this.lastSelectSegments = []; } else if (!this.internalDrag) { const html = dropEvent.dataTransfer?.getData('text/html'); if (html) { @@ -129,4 +146,31 @@ export class DragAndDropPlugin implements EditorPlugin { } } } + + private triggerDragOutOfTheEditor(editor: IEditor, dropEvent: DragEvent) { + const dropPosition = editor.isExperimentalFeatureEnabled('HandleDropInternalContent') + ? undefined + : getNodePositionFromEvent( + editor.getDocument(), + editor.getDOMHelper(), + dropEvent.clientX, + dropEvent.clientY + ); + let modelChanged: boolean = false; + editor.formatContentModel(model => { + const selectedSegments = getSelectedSegments(model, false); + modelChanged = modelChanged = + this.lastSelectSegments.length !== selectedSegments.length || + this.lastSelectSegments.some( + (segment, index) => segment !== selectedSegments[index] + ); + return false; + }); + if (!dropPosition && modelChanged) { + editor.triggerEvent('contentChanged', { + source: ChangeSource.DragOutOfEditor, + }); + editor.takeSnapshot(); + } + } } diff --git a/packages/roosterjs-content-model-plugins/test/dragAndDrop/DragAndDropPluginTest.ts b/packages/roosterjs-content-model-plugins/test/dragAndDrop/DragAndDropPluginTest.ts index e1d25b1cedfa..e297c3fafc8b 100644 --- a/packages/roosterjs-content-model-plugins/test/dragAndDrop/DragAndDropPluginTest.ts +++ b/packages/roosterjs-content-model-plugins/test/dragAndDrop/DragAndDropPluginTest.ts @@ -1,7 +1,8 @@ import * as handleDroppedContentFile from '../../lib/dragAndDrop/utils/handleDroppedExternalContent'; import * as handleDroppedInternalContentFile from '../../lib/dragAndDrop/utils/handleDroppedInternalContent'; import { DragAndDropPlugin } from '../../lib/dragAndDrop/DragAndDropPlugin'; -import { IEditor } from 'roosterjs-content-model-types'; +import { ContentModelDocument, IEditor } from 'roosterjs-content-model-types'; +import { ChangeSource } from 'roosterjs-content-model-dom'; describe('DragAndDropPlugin', () => { let plugin: DragAndDropPlugin; @@ -12,6 +13,8 @@ describe('DragAndDropPlugin', () => { let eventMap: Record; let getDOMSelectionSpy: jasmine.Spy; let takeSnapshotSpy: jasmine.Spy; + let triggerEventSpy: jasmine.Spy; + let contentModel: ContentModelDocument; beforeEach(() => { disposerSpy = jasmine.createSpy('disposer'); @@ -24,12 +27,20 @@ describe('DragAndDropPlugin', () => { .and.returnValue(true); getDOMSelectionSpy = jasmine.createSpy('getDOMSelection'); takeSnapshotSpy = jasmine.createSpy('takeSnapshot'); + triggerEventSpy = jasmine.createSpy('triggerEvent'); + contentModel = { + blockGroupType: 'Document', + blocks: [], + }; editor = ({ attachDomEvent: attachDomEventSpy, isExperimentalFeatureEnabled: isExperimentalFeatureEnabledSpy, getDOMSelection: getDOMSelectionSpy, takeSnapshot: takeSnapshotSpy, + triggerEvent: triggerEventSpy, + formatContentModel: (callback: (model: ContentModelDocument) => void) => + callback(contentModel), } as any) as IEditor; }); @@ -49,7 +60,7 @@ describe('DragAndDropPlugin', () => { expect(attachDomEventSpy).toHaveBeenCalled(); expect(eventMap.dragstart).toBeDefined(); - expect(eventMap.blur).toBeDefined(); + expect(eventMap.dragend).toBeDefined(); }); it('should initialize with custom forbidden elements', () => { @@ -98,57 +109,55 @@ describe('DragAndDropPlugin', () => { }); }); - describe('blur event', () => { - it('should take a snapshot and reset the internal drag flag when blurred during an internal drag', () => { + describe('dragend event', () => { + it('should trigger contentChanged and take a snapshot when the model changed', () => { spyOn(handleDroppedInternalContentFile, 'handleDroppedInternalContent'); plugin = new DragAndDropPlugin(); plugin.initialize(editor); - const target = document.createElement('div'); - eventMap.dragstart.beforeDispatch({ target } as any); - - eventMap.blur.beforeDispatch({} as any); - - expect(takeSnapshotSpy).toHaveBeenCalledTimes(1); - - // Internal drag flag should be reset, so a subsequent drop is treated as external - const handleDroppedExternalContentSpy = spyOn( - handleDroppedContentFile, - 'handleDroppedExternalContent' - ); - - plugin.onPluginEvent({ - eventType: 'beforeDrop', - rawEvent: { - dataTransfer: { - getData: () => '
test
', + contentModel.blocks.push({ + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'test', + format: {}, + isSelected: true, }, - } as any, + ], + format: {}, }); + eventMap.dragstart.beforeDispatch({} as DragEvent); + contentModel.blocks = []; + + eventMap.dragend.beforeDispatch({} as DragEvent); - expect(handleDroppedExternalContentSpy).toHaveBeenCalled(); + expect(triggerEventSpy).toHaveBeenCalledWith('contentChanged', { + source: ChangeSource.DragOutOfEditor, + }); + expect(takeSnapshotSpy).toHaveBeenCalledTimes(1); }); - it('should not take a snapshot when blurred without an internal drag', () => { + it('should not take a snapshot when the model did not change', () => { plugin = new DragAndDropPlugin(); plugin.initialize(editor); - eventMap.blur.beforeDispatch({} as any); + const target = document.createElement('div'); + eventMap.dragstart.beforeDispatch({ target } as any); + eventMap.dragend.beforeDispatch({} as any); + expect(triggerEventSpy).not.toHaveBeenCalled(); expect(takeSnapshotSpy).not.toHaveBeenCalled(); }); - it('should only take a snapshot once for consecutive blur events during a single internal drag', () => { + it('should not take a snapshot when drag ends without an internal drag', () => { plugin = new DragAndDropPlugin(); plugin.initialize(editor); - const target = document.createElement('div'); - eventMap.dragstart.beforeDispatch({ target } as any); - - eventMap.blur.beforeDispatch({} as any); - eventMap.blur.beforeDispatch({} as any); + eventMap.dragend.beforeDispatch({} as any); - expect(takeSnapshotSpy).toHaveBeenCalledTimes(1); + expect(triggerEventSpy).not.toHaveBeenCalled(); + expect(takeSnapshotSpy).not.toHaveBeenCalled(); }); }); From de513c848fb5f991973d73aa36a1b23f3f9b17b6 Mon Sep 17 00:00:00 2001 From: Julia Roldi Date: Tue, 11 Aug 2026 17:11:41 -0300 Subject: [PATCH 5/5] nit --- .../lib/dragAndDrop/DragAndDropPlugin.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts index 7623084f808e..466e800f8db3 100644 --- a/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts +++ b/packages/roosterjs-content-model-plugins/lib/dragAndDrop/DragAndDropPlugin.ts @@ -71,7 +71,10 @@ export class DragAndDropPlugin implements EditorPlugin { this.adjustDraggingCursor(this.editor, ev as DragEvent); } editor.formatContentModel(model => { - this.lastSelectSegments = getSelectedSegments(model, false); + this.lastSelectSegments = getSelectedSegments( + model, + false /* includingFormatHolder */ + ); return false; }); }, @@ -158,8 +161,8 @@ export class DragAndDropPlugin implements EditorPlugin { ); let modelChanged: boolean = false; editor.formatContentModel(model => { - const selectedSegments = getSelectedSegments(model, false); - modelChanged = modelChanged = + const selectedSegments = getSelectedSegments(model, false /* includingFormatHolder */); + modelChanged = this.lastSelectSegments.length !== selectedSegments.length || this.lastSelectSegments.some( (segment, index) => segment !== selectedSegments[index]