diff --git a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts index 480a7ad3a28..835f5084082 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts @@ -47,7 +47,10 @@ export const formatContentModel: FormatContentModel = ( if (changed) { const isNested = core.undo.isNested; - const shouldAddSnapshot = !skipUndoSnapshot && !isNested; + const shouldAddSnapshot = + (!skipUndoSnapshot || skipUndoSnapshot == 'DoNotSkip') && !isNested; + const shouldMarkNewContent = + (skipUndoSnapshot === true || skipUndoSnapshot == 'MarkNewContent') && !isNested; let selection: DOMSelection | undefined; if (shouldAddSnapshot) { @@ -94,7 +97,9 @@ export const formatContentModel: FormatContentModel = ( if (shouldAddSnapshot) { core.api.addUndoSnapshot(core, false /*canUndoByBackspace*/, entityStates); - } else { + } + + if (shouldMarkNewContent) { core.undo.snapshotsManager.hasNewContent = true; } } finally { diff --git a/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts b/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts index 6583eff98a6..82de8c4fa4e 100644 --- a/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts +++ b/packages/roosterjs-content-model-core/test/coreApi/formatContentModel/formatContentModelTest.ts @@ -123,7 +123,7 @@ describe('formatContentModel', () => { expect(announce).not.toHaveBeenCalled(); }); - it('Skip undo snapshot', () => { + it('Skip undo snapshot = true', () => { const callback = jasmine.createSpy('callback').and.callFake((model, context) => { context.skipUndoSnapshot = true; return true; @@ -157,6 +157,155 @@ describe('formatContentModel', () => { true ); expect(announce).not.toHaveBeenCalled(); + expect(core.undo.snapshotsManager.hasNewContent).toBeTrue(); + }); + + it('Skip undo snapshot = false', () => { + const callback = jasmine.createSpy('callback').and.callFake((model, context) => { + context.skipUndoSnapshot = false; + return true; + }); + + formatContentModel(core, callback, { apiName }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + skipUndoSnapshot: false, + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).toHaveBeenCalledTimes(2); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: 'contentChanged', + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + formatApiName: apiName, + changedEntities: [], + }, + true + ); + expect(announce).not.toHaveBeenCalled(); + expect(core.undo.snapshotsManager.hasNewContent).toBeFalsy(); + }); + + it('Skip undo snapshot = DoNotSkip', () => { + const callback = jasmine.createSpy('callback').and.callFake((model, context) => { + context.skipUndoSnapshot = 'DoNotSkip'; + return true; + }); + + formatContentModel(core, callback, { apiName }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + skipUndoSnapshot: 'DoNotSkip', + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).toHaveBeenCalledTimes(2); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: 'contentChanged', + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + formatApiName: apiName, + changedEntities: [], + }, + true + ); + expect(announce).not.toHaveBeenCalled(); + expect(core.undo.snapshotsManager.hasNewContent).toBeFalsy(); + }); + + it('Skip undo snapshot = MarkNewContent', () => { + const callback = jasmine.createSpy('callback').and.callFake((model, context) => { + context.skipUndoSnapshot = 'MarkNewContent'; + return true; + }); + + formatContentModel(core, callback, { apiName }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + skipUndoSnapshot: 'MarkNewContent', + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).toHaveBeenCalledTimes(0); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: 'contentChanged', + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + formatApiName: apiName, + changedEntities: [], + }, + true + ); + expect(announce).not.toHaveBeenCalled(); + expect(core.undo.snapshotsManager.hasNewContent).toBeTruthy(); + }); + + it('Skip undo snapshot = SkipAll', () => { + const callback = jasmine.createSpy('callback').and.callFake((model, context) => { + context.skipUndoSnapshot = 'SkipAll'; + return true; + }); + + formatContentModel(core, callback, { apiName }); + + expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], + deletedEntities: [], + rawEvent: undefined, + skipUndoSnapshot: 'SkipAll', + newImages: [], + }); + expect(createContentModel).toHaveBeenCalledTimes(1); + expect(addUndoSnapshot).toHaveBeenCalledTimes(0); + expect(setContentModel).toHaveBeenCalledTimes(1); + expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); + expect(triggerEvent).toHaveBeenCalledTimes(1); + expect(triggerEvent).toHaveBeenCalledWith( + core, + { + eventType: 'contentChanged', + contentModel: mockedModel, + selection: mockedSelection, + source: ChangeSource.Format, + data: undefined, + formatApiName: apiName, + changedEntities: [], + }, + true + ); + expect(announce).not.toHaveBeenCalled(); + expect(core.undo.snapshotsManager.hasNewContent).toBeFalsy(); }); it('Customize change source', () => { @@ -1028,9 +1177,7 @@ describe('formatContentModel', () => { expect(setContentModel).toHaveBeenCalledWith(core, mockedModel, undefined, undefined); expect(core.undo).toEqual({ isNested: true, - snapshotsManager: { - hasNewContent: true, - }, + snapshotsManager: {}, } as any); }); }); diff --git a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelContext.ts b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelContext.ts index 7ee2b7f1894..eb27499b737 100644 --- a/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelContext.ts +++ b/packages/roosterjs-content-model-types/lib/parameter/FormatContentModelContext.ts @@ -71,8 +71,13 @@ export interface FormatContentModelContext { * @optional * When pass true, skip adding undo snapshot when write Content Model back to DOM. * Need to be set by the formatter function + * Default value is false, which means add undo snapshot + * When set to true, it will skip adding undo snapshot but mark "hasNewContent" so that next undo snapshot will be added, this is same with "MarkNewContent" + * When set to 'DoNotSkip', it will add undo snapshot (default behavior) + * When set to 'MarkNewContent', it will skip adding undo snapshot but mark "hasNewContent" so that next undo snapshot will be added + * When set to 'SkipAll', it will skip adding undo snapshot and not mark "hasNewContent", as if no change is made */ - skipUndoSnapshot?: boolean; + skipUndoSnapshot?: boolean | 'DoNotSkip' | 'MarkNewContent' | 'SkipAll'; /** * @optional