From 78b498a72fd4cab886a849ce663200000847c42e Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Thu, 29 Feb 2024 09:59:19 -0800 Subject: [PATCH] Improve Entity State related API (#2444) * Improve Entity State related API * fix build and test * add test --------- Co-authored-by: Bryan Valverde U --- .../lib/publicApi/entity/insertEntity.ts | 36 +++++++- .../test/publicApi/entity/insertEntityTest.ts | 85 +++++++++++++++++++ .../lib/corePlugin/EntityPlugin.ts | 27 +++--- .../lib/editor/Editor.ts | 10 ++- .../lib/utils/restoreSnapshotHTML.ts | 32 +++---- .../test/corePlugin/EntityPluginTest.ts | 6 +- .../test/editor/EditorTest.ts | 34 +++++++- .../lib/domUtils/entityUtils.ts | 23 ++++- .../entity/entityFormatHandler.ts | 13 +-- .../roosterjs-content-model-dom/lib/index.ts | 2 +- .../test/domUtils/entityUtilTest.ts | 70 +++++++-------- .../lib/editor/IEditor.ts | 4 +- .../lib/parameter/InsertEntityOptions.ts | 5 ++ 13 files changed, 252 insertions(+), 95 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts b/packages-content-model/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts index 4fa758d60ee..c6615a0089c 100644 --- a/packages-content-model/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts +++ b/packages-content-model/roosterjs-content-model-api/lib/publicApi/entity/insertEntity.ts @@ -1,12 +1,17 @@ import { ChangeSource } from 'roosterjs-content-model-core'; -import { createEntity, normalizeContentModel } from 'roosterjs-content-model-dom'; import { insertEntityModel } from '../../modelApi/entity/insertEntityModel'; +import { + createEntity, + normalizeContentModel, + parseEntityFormat, +} from 'roosterjs-content-model-dom'; import type { ContentModelEntity, DOMSelection, InsertEntityPosition, InsertEntityOptions, IEditor, + EntityState, } from 'roosterjs-content-model-types'; const BlockEntityTag = 'div'; @@ -57,7 +62,8 @@ export default function insertEntity( position?: InsertEntityPosition | DOMSelection, options?: InsertEntityOptions ): ContentModelEntity | null { - const { contentNode, focusAfterEntity, wrapperDisplay, skipUndoSnapshot } = options || {}; + const { contentNode, focusAfterEntity, wrapperDisplay, skipUndoSnapshot, initialEntityState } = + options || {}; const document = editor.getDocument(); const wrapper = document.createElement(isBlock ? BlockEntityTag : InlineEntityTag); const display = wrapperDisplay ?? (isBlock ? undefined : 'inline-block'); @@ -75,6 +81,10 @@ export default function insertEntity( const entityModel = createEntity(wrapper, true /* isReadonly */, undefined /*format*/, type); + if (!skipUndoSnapshot) { + editor.takeSnapshot(); + } + editor.formatContentModel( (model, context) => { insertEntityModel( @@ -88,7 +98,7 @@ export default function insertEntity( normalizeContentModel(model); - context.skipUndoSnapshot = skipUndoSnapshot; + context.skipUndoSnapshot = true; context.newEntities.push(entityModel); return true; @@ -106,5 +116,25 @@ export default function insertEntity( } ); + if (!skipUndoSnapshot) { + let entityState: EntityState | undefined; + + if (initialEntityState) { + const format = parseEntityFormat(wrapper); + const { id, entityType } = format; + + entityState = + id && entityType + ? { + id: id, + type: entityType, + state: initialEntityState, + } + : undefined; + } + + editor.takeSnapshot(entityState); + } + return entityModel; } diff --git a/packages-content-model/roosterjs-content-model-api/test/publicApi/entity/insertEntityTest.ts b/packages-content-model/roosterjs-content-model-api/test/publicApi/entity/insertEntityTest.ts index f8a8f3619e2..475163b52e5 100644 --- a/packages-content-model/roosterjs-content-model-api/test/publicApi/entity/insertEntityTest.ts +++ b/packages-content-model/roosterjs-content-model-api/test/publicApi/entity/insertEntityTest.ts @@ -24,6 +24,7 @@ describe('insertEntity', () => { let insertEntityModelSpy: jasmine.Spy; let isDarkModeSpy: jasmine.Spy; let normalizeContentModelSpy: jasmine.Spy; + let takeSnapshotSpy: jasmine.Spy; const type = 'Entity'; const apiName = 'insertEntity'; @@ -39,6 +40,7 @@ describe('insertEntity', () => { appendChildSpy = jasmine.createSpy('appendChildSpy'); insertEntityModelSpy = spyOn(insertEntityModel, 'insertEntityModel'); isDarkModeSpy = jasmine.createSpy('isDarkMode'); + takeSnapshotSpy = jasmine.createSpy('takeSnapshot'); wrapper = { style: { @@ -68,6 +70,7 @@ describe('insertEntity', () => { getDocument: getDocumentSpy, isDarkMode: isDarkModeSpy, formatContentModel: formatWithContentModelSpy, + takeSnapshot: takeSnapshotSpy, } as any; spyOn(entityUtils, 'addDelimiters').and.returnValue([]); @@ -76,6 +79,9 @@ describe('insertEntity', () => { it('insert inline entity to top', () => { const entity = insertEntity(editor, type, false, 'begin'); + expect(takeSnapshotSpy).toHaveBeenCalledTimes(2); + expect(takeSnapshotSpy).toHaveBeenCalledWith(); + expect(takeSnapshotSpy).toHaveBeenCalledWith(undefined); expect(createElementSpy).toHaveBeenCalledWith('span'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'inline-block'); expect(appendChildSpy).not.toHaveBeenCalled(); @@ -120,6 +126,9 @@ describe('insertEntity', () => { it('block inline entity to root', () => { const entity = insertEntity(editor, type, true, 'root'); + expect(takeSnapshotSpy).toHaveBeenCalledTimes(2); + expect(takeSnapshotSpy).toHaveBeenCalledWith(); + expect(takeSnapshotSpy).toHaveBeenCalledWith(undefined); expect(createElementSpy).toHaveBeenCalledWith('div'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'inline-block'); expect(setPropertySpy).toHaveBeenCalledWith('width', '100%'); @@ -172,6 +181,7 @@ describe('insertEntity', () => { wrapperDisplay: 'none', }); + expect(takeSnapshotSpy).toHaveBeenCalledTimes(0); expect(createElementSpy).toHaveBeenCalledWith('div'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'none'); expect(setPropertySpy).not.toHaveBeenCalledWith('display', 'inline-block'); @@ -221,6 +231,81 @@ describe('insertEntity', () => { const entity = insertEntity(editor, type, false, 'begin'); + expect(takeSnapshotSpy).toHaveBeenCalledTimes(2); + expect(takeSnapshotSpy).toHaveBeenCalledWith(); + expect(takeSnapshotSpy).toHaveBeenCalledWith(undefined); + expect(createElementSpy).toHaveBeenCalledWith('span'); + expect(setPropertySpy).toHaveBeenCalledWith('display', 'inline-block'); + expect(appendChildSpy).not.toHaveBeenCalled(); + expect(formatWithContentModelSpy.calls.argsFor(0)[1].apiName).toBe(apiName); + expect(formatWithContentModelSpy.calls.argsFor(0)[1].changeSource).toBe( + ChangeSource.InsertEntity + ); + expect(insertEntityModelSpy).toHaveBeenCalledWith( + model, + { + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, + wrapper: wrapper, + }, + 'begin', + false, + undefined, + context + ); + expect(triggerContentChangedEventSpy).not.toHaveBeenCalled(); + expect(normalizeContentModelSpy).toHaveBeenCalled(); + + expect(context.newEntities).toEqual([ + { + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { + id: undefined, + entityType: 'Entity', + isReadonly: true, + }, + wrapper, + }, + ]); + + expect(entity).toEqual({ + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, + wrapper: wrapper, + }); + }); + + it('Insert with initial state', () => { + spyOn(entityUtils, 'parseEntityFormat').and.returnValue({ + id: 'A', + entityType: 'B', + }); + + const entity = insertEntity(editor, type, false, 'begin', { + initialEntityState: 'test', + }); + + expect(takeSnapshotSpy).toHaveBeenCalledTimes(2); + expect(takeSnapshotSpy).toHaveBeenCalledWith(); + expect(takeSnapshotSpy).toHaveBeenCalledWith({ + id: 'A', + type: 'B', + state: 'test', + }); expect(createElementSpy).toHaveBeenCalledWith('span'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'inline-block'); expect(appendChildSpy).not.toHaveBeenCalled(); diff --git a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts index ec0cf75cb08..460ef490da6 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/corePlugin/EntityPlugin.ts @@ -11,12 +11,11 @@ import { getAllEntityWrappers, getObjectKeys, isEntityElement, - parseEntityClassName, + parseEntityFormat, } from 'roosterjs-content-model-dom'; import type { ChangedEntity, ContentChangedEvent, - ContentModelEntityFormat, EntityOperation, EntityPluginState, IEditor, @@ -209,16 +208,19 @@ class EntityPlugin implements PluginWithState { result.splice(index, 1); } else { // Entity is not in editor, which means it is deleted, use a temporary entity here to represent this entity - const tempEntity = createEntity(entry.element); - let isEntity = false; - - entry.element.classList.forEach(name => { - isEntity = parseEntityClassName(name, tempEntity.entityFormat) || isEntity; - }); + const format = parseEntityFormat(entry.element); + + if (!format.isFakeEntity) { + const entity = createEntity( + entry.element, + format.isReadonly, + {}, + format.entityType, + format.id + ); - if (isEntity) { result.push({ - entity: tempEntity, + entity: entity, operation: 'overwrite', }); } @@ -244,10 +246,7 @@ class EntityPlugin implements PluginWithState { rawEvent?: Event, state?: string ) { - const format: ContentModelEntityFormat = {}; - wrapper.classList.forEach(name => { - parseEntityClassName(name, format); - }); + const format = parseEntityFormat(wrapper); return format.id && format.entityType && !format.isFakeEntity ? editor.triggerEvent('entityOperation', { diff --git a/packages-content-model/roosterjs-content-model-core/lib/editor/Editor.ts b/packages-content-model/roosterjs-content-model-core/lib/editor/Editor.ts index 03485506e05..5477ab1dcdf 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/editor/Editor.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/editor/Editor.ts @@ -27,6 +27,7 @@ import type { EditorOptions, TrustedHTMLHandler, Rect, + EntityState, } from 'roosterjs-content-model-types'; /** @@ -174,11 +175,16 @@ export class Editor implements IEditor { /** * Add a single undo snapshot to undo stack + * @param entityState @optional State for entity if we want to add entity state for this snapshot */ - takeSnapshot(): Snapshot | null { + takeSnapshot(entityState?: EntityState): Snapshot | null { const core = this.getCore(); - return core.api.addUndoSnapshot(core, false /*canUndoByBackspace*/); + return core.api.addUndoSnapshot( + core, + false /*canUndoByBackspace*/, + entityState ? [entityState] : undefined + ); } /** diff --git a/packages-content-model/roosterjs-content-model-core/lib/utils/restoreSnapshotHTML.ts b/packages-content-model/roosterjs-content-model-core/lib/utils/restoreSnapshotHTML.ts index 1e8a67da11c..ad4a2e1a5f7 100644 --- a/packages-content-model/roosterjs-content-model-core/lib/utils/restoreSnapshotHTML.ts +++ b/packages-content-model/roosterjs-content-model-core/lib/utils/restoreSnapshotHTML.ts @@ -2,15 +2,10 @@ import { getAllEntityWrappers, isEntityElement, isNodeOfType, - parseEntityClassName, + parseEntityFormat, reuseCachedElement, } from 'roosterjs-content-model-dom'; -import type { - Snapshot, - EditorCore, - KnownEntityItem, - ContentModelEntityFormat, -} from 'roosterjs-content-model-types'; +import type { Snapshot, EditorCore, KnownEntityItem } from 'roosterjs-content-model-types'; const BlockEntityContainer = '_E_EBlockEntityContainer'; @@ -79,11 +74,7 @@ function tryGetEntityElement( if (isNodeOfType(node, 'ELEMENT_NODE')) { if (isEntityElement(node)) { - const format: ContentModelEntityFormat = {}; - - node.classList.forEach(name => { - parseEntityClassName(name, format); - }); + const format = parseEntityFormat(node); result = (format.id && entityMap[format.id]?.element) || null; } else if (isBlockEntityContainer(node)) { @@ -93,6 +84,7 @@ function tryGetEntityElement( return result; } + function isBlockEntityContainer(node: HTMLElement) { return node.classList.contains(BlockEntityContainer); } @@ -101,14 +93,16 @@ function tryGetEntityFromContainer( element: HTMLElement, entityMap: Record ): HTMLElement | null { - const format: ContentModelEntityFormat = {}; - element.childNodes.forEach(node => { + for (let node = element.firstChild; node; node = node.nextSibling) { if (isEntityElement(node) && isNodeOfType(node, 'ELEMENT_NODE')) { - node.classList.forEach(name => parseEntityClassName(name, format)); - } - }); + const format = parseEntityFormat(node); + const parent = format.id ? entityMap[format.id]?.element.parentElement : null; - const parent = format.id ? entityMap[format.id]?.element.parentElement : null; + return isNodeOfType(parent, 'ELEMENT_NODE') && isBlockEntityContainer(parent) + ? parent + : null; + } + } - return isNodeOfType(parent, 'ELEMENT_NODE') && isBlockEntityContainer(parent) ? parent : null; + return null; } diff --git a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts index 010938c9869..0f9bf77b654 100644 --- a/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/corePlugin/EntityPluginTest.ts @@ -599,7 +599,7 @@ describe('EntityPlugin', () => { it('Click on entity', () => { const mockedNode = { parentNode: null as any, - classList: ['_ENtity', '_EType_A', '_EId_A'], + classList: ['_Entity', '_EType_A', '_EId_A'], } as any; const mockedEvent = { target: mockedNode, @@ -631,7 +631,7 @@ describe('EntityPlugin', () => { it('Click on child of entity', () => { const mockedNode1 = { parentNode: null as any, - classList: ['_ENtity', '_EType_A', '_EId_A'], + classList: ['_Entity', '_EType_A', '_EId_A'], } as any; const mockedNode2 = { @@ -667,7 +667,7 @@ describe('EntityPlugin', () => { it('Not clicking', () => { const mockedNode = { parentNode: null as any, - classList: ['_ENtity', '_EType_A', '_EId_A'], + classList: ['_Entity', '_EType_A', '_EId_A'], } as any; const mockedEvent = { target: mockedNode, diff --git a/packages-content-model/roosterjs-content-model-core/test/editor/EditorTest.ts b/packages-content-model/roosterjs-content-model-core/test/editor/EditorTest.ts index 0effbb59304..591c3df519b 100644 --- a/packages-content-model/roosterjs-content-model-core/test/editor/EditorTest.ts +++ b/packages-content-model/roosterjs-content-model-core/test/editor/EditorTest.ts @@ -431,7 +431,7 @@ describe('Editor', () => { const snapshot = editor.takeSnapshot(); - expect(addUndoSnapshotSpy).toHaveBeenCalledWith(mockedCore, false); + expect(addUndoSnapshotSpy).toHaveBeenCalledWith(mockedCore, false, undefined); expect(snapshot).toBe(mockedSnapshot); editor.dispose(); @@ -465,6 +465,38 @@ describe('Editor', () => { expect(() => editor.takeSnapshot()).toThrow(); }); + it('takeSnapshot', () => { + const div = document.createElement('div'); + const mockedSnapshot = 'SNAPSHOT' as any; + const resetSpy = jasmine.createSpy('reset'); + const addUndoSnapshotSpy = jasmine + .createSpy('addUndoSnapshot') + .and.returnValue(mockedSnapshot); + const mockedCore = { + plugins: [], + darkColorHandler: { + updateKnownColor: updateKnownColorSpy, + reset: resetSpy, + }, + api: { addUndoSnapshot: addUndoSnapshotSpy, setContentModel: setContentModelSpy }, + } as any; + + createEditorCoreSpy.and.returnValue(mockedCore); + + const editor = new Editor(div); + const snapshot = editor.takeSnapshot(); + + expect(snapshot).toEqual(mockedSnapshot); + expect(addUndoSnapshotSpy).toHaveBeenCalledTimes(1); + expect(addUndoSnapshotSpy).toHaveBeenCalledWith(mockedCore, false, undefined); + + const mockedState = 'STATE' as any; + + editor.takeSnapshot(mockedState); + expect(addUndoSnapshotSpy).toHaveBeenCalledTimes(2); + expect(addUndoSnapshotSpy).toHaveBeenCalledWith(mockedCore, false, [mockedState]); + }); + it('restoreSnapshot', () => { const div = document.createElement('div'); const mockedSnapshot = 'SNAPSHOT' as any; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domUtils/entityUtils.ts b/packages-content-model/roosterjs-content-model-dom/lib/domUtils/entityUtils.ts index a95979139dd..2579f769c37 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domUtils/entityUtils.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domUtils/entityUtils.ts @@ -32,12 +32,33 @@ export function getAllEntityWrappers(root: HTMLElement): HTMLElement[] { return toArray(root.querySelectorAll('.' + ENTITY_INFO_NAME)) as HTMLElement[]; } +/** + * Parse entity format from entity wrapper element + * @param wrapper The wrapper element to parse entity format from + * @returns Entity format + */ +export function parseEntityFormat(wrapper: HTMLElement): ContentModelEntityFormat { + let isEntity = false; + const format: ContentModelEntityFormat = {}; + + wrapper.classList.forEach(name => { + isEntity = parseEntityClassName(name, format) || isEntity; + }); + + if (!isEntity) { + format.isFakeEntity = true; + format.isReadonly = !wrapper.isContentEditable; + } + + return format; +} + /** * Parse entity class names from entity wrapper element * @param className Class names of entity * @param format The output entity format object */ -export function parseEntityClassName( +function parseEntityClassName( className: string, format: ContentModelEntityFormat ): boolean | undefined { diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/entity/entityFormatHandler.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/entity/entityFormatHandler.ts index 2c360c3f243..d1192fda95b 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/entity/entityFormatHandler.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/entity/entityFormatHandler.ts @@ -1,4 +1,4 @@ -import { generateEntityClassNames, parseEntityClassName } from '../../domUtils/entityUtils'; +import { generateEntityClassNames, parseEntityFormat } from '../../domUtils/entityUtils'; import type { EntityInfoFormat, IdFormat } from 'roosterjs-content-model-types'; import type { FormatHandler } from '../FormatHandler'; @@ -7,16 +7,7 @@ import type { FormatHandler } from '../FormatHandler'; */ export const entityFormatHandler: FormatHandler = { parse: (format, element) => { - let isEntity = false; - - element.classList.forEach(name => { - isEntity = parseEntityClassName(name, format) || isEntity; - }); - - if (!isEntity) { - format.isFakeEntity = true; - format.isReadonly = !element.isContentEditable; - } + Object.assign(format, parseEntityFormat(element)); }, apply: (format, element) => { diff --git a/packages-content-model/roosterjs-content-model-dom/lib/index.ts b/packages-content-model/roosterjs-content-model-dom/lib/index.ts index 0ffe238f6e4..b8f909736da 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/index.ts @@ -24,7 +24,7 @@ export { wrap } from './domUtils/wrap'; export { isEntityElement, getAllEntityWrappers, - parseEntityClassName, + parseEntityFormat, generateEntityClassNames, addDelimiters, isEntityDelimiter, diff --git a/packages-content-model/roosterjs-content-model-dom/test/domUtils/entityUtilTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domUtils/entityUtilTest.ts index 74d896e4da0..686a73ed507 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domUtils/entityUtilTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domUtils/entityUtilTest.ts @@ -5,7 +5,7 @@ import { getAllEntityWrappers, isEntityDelimiter, isEntityElement, - parseEntityClassName, + parseEntityFormat, } from '../../lib/domUtils/entityUtils'; export function setEntityElementClasses( @@ -43,69 +43,61 @@ describe('isEntityElement', () => { }); }); -describe('parseEntityClassName', () => { +describe('parseEntityFormat', () => { it('No entity class', () => { - const format: ContentModelEntityFormat = {}; + const div = document.createElement('div'); + + div.className = 'test'; - const result = parseEntityClassName('test', format); + const format = parseEntityFormat(div); - expect(result).toBeFalsy(); - expect(format).toEqual({}); + expect(format).toEqual({ + isFakeEntity: true, + isReadonly: true, + }); }); it('Entity class', () => { - const format: ContentModelEntityFormat = {}; - - const result = parseEntityClassName('_Entity', format); - - expect(result).toBeTrue(); - expect(format).toEqual({}); - }); + const div = document.createElement('div'); - it('EntityId class', () => { - const format: ContentModelEntityFormat = {}; + div.className = '_Entity _EId_A _EType_B _EReadonly_1'; - const result = parseEntityClassName('_EId_A', format); + const format = parseEntityFormat(div); - expect(result).toBeFalsy(); expect(format).toEqual({ id: 'A', + entityType: 'B', + isReadonly: true, }); }); - it('EntityType class', () => { - const format: ContentModelEntityFormat = {}; + it('Fake entity', () => { + const div = document.createElement('div'); - const result = parseEntityClassName('_EType_B', format); + div.contentEditable = 'true'; - expect(result).toBeFalsy(); - expect(format).toEqual({ - entityType: 'B', - }); - }); - - it('Entity readonly class', () => { - const format: ContentModelEntityFormat = {}; + div.className = '_EId_A _EType_B _EReadonly_1'; - const result = parseEntityClassName('_EReadonly_1', format); + const format = parseEntityFormat(div); - expect(result).toBeFalsy(); expect(format).toEqual({ - isReadonly: true, + isFakeEntity: true, + isReadonly: false, + id: 'A', + entityType: 'B', }); }); - it('Parse class on existing format', () => { - const format: ContentModelEntityFormat = { - id: 'A', - }; + it('Fake entity, readonly', () => { + const div = document.createElement('div'); + + div.contentEditable = 'false'; - const result = parseEntityClassName('_EType_B', format); + const format = parseEntityFormat(div); - expect(result).toBeFalsy(); expect(format).toEqual({ - id: 'A', - entityType: 'B', + isFakeEntity: true, + isReadonly: true, }); }); }); diff --git a/packages-content-model/roosterjs-content-model-types/lib/editor/IEditor.ts b/packages-content-model/roosterjs-content-model-types/lib/editor/IEditor.ts index 88b194a9a35..301a2367ace 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/editor/IEditor.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/editor/IEditor.ts @@ -17,6 +17,7 @@ import type { import type { DarkColorHandler } from '../context/DarkColorHandler'; import type { TrustedHTMLHandler } from '../parameter/TrustedHTMLHandler'; import type { Rect } from '../parameter/Rect'; +import type { EntityState } from '../parameter/FormatContentModelContext'; /** * An interface of Editor, built on top of Content Model @@ -125,8 +126,9 @@ export interface IEditor { /** * Add a single undo snapshot to undo stack + * @param entityState @optional State for entity if we want to add entity state for this snapshot */ - takeSnapshot(): Snapshot | null; + takeSnapshot(entityState?: EntityState): Snapshot | null; /** * Restore an undo snapshot into editor diff --git a/packages-content-model/roosterjs-content-model-types/lib/parameter/InsertEntityOptions.ts b/packages-content-model/roosterjs-content-model-types/lib/parameter/InsertEntityOptions.ts index e936fd88255..8844a0c1e9c 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/parameter/InsertEntityOptions.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/parameter/InsertEntityOptions.ts @@ -21,4 +21,9 @@ export interface InsertEntityOptions { * Whether skip adding an undo snapshot around */ skipUndoSnapshot?: boolean; + + /** + * Initial entity state, this is used when restore an undo snapshot to right after entity is inserted, this state will be used for set initial state of entity + */ + initialEntityState?: string; }