Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,9 @@ export const ChangeSource = {
* Content changed by replace
*/
Replace: 'Replace',

/**
* Content changed by dragging content out the editor
*/
DragOutOfEditor: 'DragOutOfEditor',
};
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -59,6 +70,21 @@ export class DragAndDropPlugin implements EditorPlugin {
) {
this.adjustDraggingCursor(this.editor, ev as DragEvent);
}
editor.formatContentModel(model => {
this.lastSelectSegments = getSelectedSegments(
model,
false /* includingFormatHolder */
);
return false;
});
},
},
dragend: {
beforeDispatch: ev => {
if (this.internalDrag) {
const dropEvent = ev as DragEvent;
this.triggerDragOutOfTheEditor(editor, dropEvent);
}
Comment on lines +84 to +87
},
},
});
Expand All @@ -76,6 +102,8 @@ export class DragAndDropPlugin implements EditorPlugin {
this.disposer = null;
}
this.forbiddenElements = [];
this.internalDrag = false;
this.lastSelectSegments = [];
}

/**
Expand All @@ -92,6 +120,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) {
Expand Down Expand Up @@ -120,4 +149,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 /* includingFormatHolder */);
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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,6 +12,9 @@ describe('DragAndDropPlugin', () => {
let isExperimentalFeatureEnabledSpy: jasmine.Spy;
let eventMap: Record<string, any>;
let getDOMSelectionSpy: jasmine.Spy;
let takeSnapshotSpy: jasmine.Spy;
let triggerEventSpy: jasmine.Spy;
let contentModel: ContentModelDocument;

beforeEach(() => {
disposerSpy = jasmine.createSpy('disposer');
Expand All @@ -22,11 +26,21 @@ describe('DragAndDropPlugin', () => {
.createSpy('isExperimentalFeatureEnabled')
.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;
});

Expand All @@ -46,6 +60,7 @@ describe('DragAndDropPlugin', () => {

expect(attachDomEventSpy).toHaveBeenCalled();
expect(eventMap.dragstart).toBeDefined();
expect(eventMap.dragend).toBeDefined();
});

it('should initialize with custom forbidden elements', () => {
Expand Down Expand Up @@ -94,6 +109,58 @@ describe('DragAndDropPlugin', () => {
});
});

describe('dragend event', () => {
it('should trigger contentChanged and take a snapshot when the model changed', () => {
spyOn(handleDroppedInternalContentFile, 'handleDroppedInternalContent');
plugin = new DragAndDropPlugin();
plugin.initialize(editor);

contentModel.blocks.push({
blockType: 'Paragraph',
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
isSelected: true,
},
],
format: {},
});
eventMap.dragstart.beforeDispatch({} as DragEvent);
contentModel.blocks = [];

eventMap.dragend.beforeDispatch({} as DragEvent);

expect(triggerEventSpy).toHaveBeenCalledWith('contentChanged', {
source: ChangeSource.DragOutOfEditor,
});
expect(takeSnapshotSpy).toHaveBeenCalledTimes(1);
});

it('should not take a snapshot when the model did not change', () => {
plugin = new DragAndDropPlugin();
plugin.initialize(editor);

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 not take a snapshot when drag ends without an internal drag', () => {
plugin = new DragAndDropPlugin();
plugin.initialize(editor);

eventMap.dragend.beforeDispatch({} as any);

expect(triggerEventSpy).not.toHaveBeenCalled();
expect(takeSnapshotSpy).not.toHaveBeenCalled();
});
});

describe('onPluginEvent - beforeDrop', () => {
let handleDroppedExternalContentSpy: jasmine.Spy;

Expand Down
Loading