diff --git a/packages/roosterjs-content-model-core/lib/coreApi/setContentModel/setContentModel.ts b/packages/roosterjs-content-model-core/lib/coreApi/setContentModel/setContentModel.ts
index 1c5909d5bf3..dac753d1d96 100644
--- a/packages/roosterjs-content-model-core/lib/coreApi/setContentModel/setContentModel.ts
+++ b/packages/roosterjs-content-model-core/lib/coreApi/setContentModel/setContentModel.ts
@@ -6,6 +6,9 @@ import {
} from 'roosterjs-content-model-dom';
import type { SetContentModel } from 'roosterjs-content-model-types';
+const SelectionClassName = '__persistedSelection';
+const SelectionSelector = '.' + SelectionClassName;
+
/**
* @internal
* Set content with content model
@@ -15,6 +18,16 @@ import type { SetContentModel } from 'roosterjs-content-model-types';
*/
export const setContentModel: SetContentModel = (core, model, option, onNodeCreated) => {
const editorContext = core.api.createEditorContext(core, true /*saveIndex*/);
+
+ if (option?.shouldMaintainSelection) {
+ editorContext.selectionClassName = SelectionClassName;
+ core.api.setEditorStyle(core, SelectionClassName, 'background-color: #ddd!important', [
+ SelectionSelector,
+ ]);
+ } else {
+ core.api.setEditorStyle(core, SelectionClassName, null /*rule*/);
+ }
+
const modelToDomContext = option
? createModelToDomContext(
editorContext,
diff --git a/packages/roosterjs-content-model-core/test/coreApi/setContentModel/setContentModelTest.ts b/packages/roosterjs-content-model-core/test/coreApi/setContentModel/setContentModelTest.ts
index 234717bcdb7..895b5aef77b 100644
--- a/packages/roosterjs-content-model-core/test/coreApi/setContentModel/setContentModelTest.ts
+++ b/packages/roosterjs-content-model-core/test/coreApi/setContentModel/setContentModelTest.ts
@@ -5,7 +5,7 @@ import { setContentModel } from '../../../lib/coreApi/setContentModel/setContent
const mockedDoc = 'DOCUMENT' as any;
const mockedModel = 'MODEL' as any;
-const mockedEditorContext = 'EDITORCONTEXT' as any;
+const mockedEditorContext = { context: 'EDITORCONTEXT' } as any;
const mockedContext = { name: 'CONTEXT' } as any;
const mockedDiv = { ownerDocument: mockedDoc } as any;
const mockedConfig = 'CONFIG' as any;
@@ -18,6 +18,7 @@ describe('setContentModel', () => {
let createModelToDomContextWithConfigSpy: jasmine.Spy;
let setDOMSelectionSpy: jasmine.Spy;
let getDOMSelectionSpy: jasmine.Spy;
+ let setEditorStyleSpy: jasmine.Spy;
beforeEach(() => {
contentModelToDomSpy = spyOn(contentModelToDom, 'contentModelToDom');
@@ -34,6 +35,7 @@ describe('setContentModel', () => {
).and.returnValue(mockedContext);
setDOMSelectionSpy = jasmine.createSpy('setDOMSelection');
getDOMSelectionSpy = jasmine.createSpy('getDOMSelection');
+ setEditorStyleSpy = jasmine.createSpy('setEditorStyle');
core = ({
physicalRoot: mockedDiv,
@@ -42,6 +44,7 @@ describe('setContentModel', () => {
createEditorContext,
setDOMSelection: setDOMSelectionSpy,
getDOMSelection: getDOMSelectionSpy,
+ setEditorStyle: setEditorStyleSpy,
},
lifecycle: {},
cache: {},
@@ -76,6 +79,7 @@ describe('setContentModel', () => {
expect(setDOMSelectionSpy).toHaveBeenCalledWith(core, mockedRange);
expect(core.cache.cachedSelection).toBe(mockedRange);
expect(core.cache.cachedModel).toBe(mockedModel);
+ expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '__persistedSelection', null);
});
it('with default option, no shadow edit', () => {
@@ -98,6 +102,7 @@ describe('setContentModel', () => {
mockedContext
);
expect(setDOMSelectionSpy).toHaveBeenCalledWith(core, mockedRange);
+ expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '__persistedSelection', null);
});
it('with default option, no shadow edit, with additional option', () => {
@@ -125,6 +130,7 @@ describe('setContentModel', () => {
mockedContext
);
expect(setDOMSelectionSpy).toHaveBeenCalledWith(core, mockedRange);
+ expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '__persistedSelection', null);
});
it('no default option, with shadow edit', () => {
@@ -181,6 +187,7 @@ describe('setContentModel', () => {
);
expect(setDOMSelectionSpy).not.toHaveBeenCalled();
expect(core.selection.selection).toBe(mockedRange);
+ expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '__persistedSelection', null);
});
it('restore range selection ', () => {
@@ -215,6 +222,7 @@ describe('setContentModel', () => {
);
expect(setDOMSelectionSpy).not.toHaveBeenCalled();
expect(core.selection.selection).toBe(mockedRange);
+ expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '__persistedSelection', null);
});
it('restore null selection ', () => {
@@ -244,5 +252,6 @@ describe('setContentModel', () => {
);
expect(setDOMSelectionSpy).not.toHaveBeenCalled();
expect(core.selection.selection).toBe(null);
+ expect(setEditorStyleSpy).toHaveBeenCalledWith(core, '__persistedSelection', null);
});
});
diff --git a/packages/roosterjs-content-model-dom/lib/modelToDom/utils/handleSegmentCommon.ts b/packages/roosterjs-content-model-dom/lib/modelToDom/utils/handleSegmentCommon.ts
index ccf24a5c70b..91fc4a0cabd 100644
--- a/packages/roosterjs-content-model-dom/lib/modelToDom/utils/handleSegmentCommon.ts
+++ b/packages/roosterjs-content-model-dom/lib/modelToDom/utils/handleSegmentCommon.ts
@@ -23,5 +23,9 @@ export function handleSegmentCommon(
applyFormat(containerNode, context.formatAppliers.elementBasedSegment, segment.format, context);
+ if (segment.isSelected && context.selectionClassName) {
+ containerNode.className = context.selectionClassName;
+ }
+
context.onNodeCreated?.(segment, segmentNode);
}
diff --git a/packages/roosterjs-content-model-dom/test/modelToDom/utils/handleSegmentCommonTest.ts b/packages/roosterjs-content-model-dom/test/modelToDom/utils/handleSegmentCommonTest.ts
index 8a3b3c923a4..f75f78e25d1 100644
--- a/packages/roosterjs-content-model-dom/test/modelToDom/utils/handleSegmentCommonTest.ts
+++ b/packages/roosterjs-content-model-dom/test/modelToDom/utils/handleSegmentCommonTest.ts
@@ -1,5 +1,6 @@
import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext';
import { createText } from '../../../lib/modelApi/creators/createText';
+import { expectHtml } from '../../testUtils';
import { handleSegmentCommon } from '../../../lib/modelToDom/utils/handleSegmentCommon';
describe('handleSegmentCommon', () => {
@@ -60,4 +61,42 @@ describe('handleSegmentCommon', () => {
expect(segmentNodes.length).toBe(1);
expect(segmentNodes[0]).toBe(parent);
});
+
+ it('selected text', () => {
+ const txt = document.createTextNode('test');
+ const container = document.createElement('span');
+ const segment = createText('test', {
+ textColor: 'red',
+ fontSize: '10pt',
+ lineHeight: '2',
+ fontWeight: 'bold',
+ });
+ const onNodeCreated = jasmine.createSpy('onNodeCreated');
+ const context = createModelToDomContext();
+
+ segment.isSelected = true;
+ context.onNodeCreated = onNodeCreated;
+ context.selectionClassName = 'test';
+
+ segment.link = {
+ dataset: {},
+ format: {
+ href: 'href',
+ },
+ };
+ container.appendChild(txt);
+ const segmentNodes: Node[] = [];
+
+ handleSegmentCommon(document, txt, container, segment, context, segmentNodes);
+
+ expect(context.regularSelection.current.segment).toBe(txt);
+ expectHtml(container.outerHTML, [
+ 'test',
+ 'test',
+ ]);
+ expect(onNodeCreated).toHaveBeenCalledWith(segment, txt);
+ expect(segmentNodes.length).toBe(2);
+ expect(segmentNodes[0]).toBe(txt);
+ expect(segmentNodes[1]).toBe(txt.parentNode!);
+ });
});
diff --git a/packages/roosterjs-content-model-types/lib/context/EditorContext.ts b/packages/roosterjs-content-model-types/lib/context/EditorContext.ts
index 937a72788f6..edeca4073d7 100644
--- a/packages/roosterjs-content-model-types/lib/context/EditorContext.ts
+++ b/packages/roosterjs-content-model-types/lib/context/EditorContext.ts
@@ -62,4 +62,9 @@ export interface EditorContext {
* Enabled experimental features
*/
experimentalFeatures?: ReadonlyArray;
+
+ /**
+ * Optional parameter that indicate the customized classes to be applied on selection block.
+ */
+ selectionClassName?: string;
}
diff --git a/packages/roosterjs-content-model-types/lib/context/ModelToDomOption.ts b/packages/roosterjs-content-model-types/lib/context/ModelToDomOption.ts
index 3ca91ef31db..cf71f3d3341 100644
--- a/packages/roosterjs-content-model-types/lib/context/ModelToDomOption.ts
+++ b/packages/roosterjs-content-model-types/lib/context/ModelToDomOption.ts
@@ -33,4 +33,9 @@ export interface ModelToDomOption {
* When set to true, selection from content model will not be applied
*/
ignoreSelection?: boolean;
+
+ /**
+ * When set to true, selection will be maintained on text even if cursor has moved away from editor.
+ */
+ shouldMaintainSelection?: boolean;
}