From fb0febca1724ed791d5bf816627ec3b00fb45400 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 8 Sep 2023 13:02:10 -0700 Subject: [PATCH 1/2] Content Model: Add solid paragraph in new table cell (#2055) --- .../lib/modelApi/table/normalizeTable.ts | 10 +++++++++- .../test/modelApi/table/normalizeTableTest.ts | 4 +--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/normalizeTable.ts b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/normalizeTable.ts index ac53ef20bd7..e150418fa9e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/normalizeTable.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/table/normalizeTable.ts @@ -1,4 +1,4 @@ -import { addSegment, createBr } from 'roosterjs-content-model-dom'; +import { addBlock, addSegment, createBr, createParagraph } from 'roosterjs-content-model-dom'; import { arrayPush } from 'roosterjs-editor-dom'; import { ContentModelSegment, @@ -30,6 +30,14 @@ export function normalizeTable( table.rows.forEach((row, rowIndex) => { row.cells.forEach((cell, colIndex) => { if (cell.blocks.length == 0) { + addBlock( + cell, + createParagraph( + undefined /*isImplicit*/, + undefined /*blockFormat*/, + defaultSegmentFormat + ) + ); addSegment(cell, createBr(defaultSegmentFormat)); } diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/normalizeTableTest.ts b/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/normalizeTableTest.ts index 0378bc1c830..f2379aa2f28 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/normalizeTableTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/modelApi/table/normalizeTableTest.ts @@ -78,7 +78,6 @@ describe('normalizeTable', () => { blocks: [ { blockType: 'Paragraph', - isImplicit: true, segments: [ { segmentType: 'Br', @@ -678,7 +677,6 @@ describe('normalizeTable', () => { blocks: [ { blockType: 'Paragraph', - isImplicit: true, segments: [ { segmentType: 'Br', @@ -688,6 +686,7 @@ describe('normalizeTable', () => { }, ], format: {}, + segmentFormat: { fontSize: '10px' }, }, ], dataset: {}, @@ -725,7 +724,6 @@ describe('normalizeTable', () => { const block: ContentModelParagraph = { blockType: 'Paragraph', - isImplicit: true, segments: [ { segmentType: 'Br', From f3f683159d616fe4ae22207ad5cfa05ed0491956 Mon Sep 17 00:00:00 2001 From: Jiuqing Song Date: Fri, 8 Sep 2023 13:44:36 -0700 Subject: [PATCH 2/2] Content Model: Do color transform for entity when copy/paste (#2056) * Content Model: Do color transform for entity when copy/paste * Call normalizeContentModel --- .../ContentModelCopyPastePlugin.ts | 20 +- .../lib/modelApi/common/cloneModel.ts | 75 ++++-- .../lib/modelApi/common/mergeModel.ts | 19 +- .../lib/publicApi/entity/insertEntity.ts | 9 +- .../publicApi/utils/formatWithContentModel.ts | 14 ++ .../FormatWithContentModelContext.ts | 5 + .../ContentModelCopyPastePluginTest.ts | 88 ++++++- .../plugins/ContentModelFormatPluginTest.ts | 3 + .../utils/handleKeyboardEventCommonTest.ts | 8 +- .../test/modelApi/common/cloneModelTest.ts | 230 ++++++++++++++++++ .../test/modelApi/common/mergeModelTest.ts | 112 +++++++-- .../test/modelApi/edit/deleteSelectionTest.ts | 11 +- .../publicApi/block/paragraphTestCommon.ts | 1 + .../test/publicApi/block/setAlignmentTest.ts | 2 + .../publicApi/editing/editingTestCommon.ts | 1 + .../editing/handleKeyDownEventTest.ts | 1 + .../test/publicApi/entity/insertEntityTest.ts | 21 +- .../format/applyPendingFormatTest.ts | 11 +- .../test/publicApi/format/clearFormatTest.ts | 2 +- .../test/publicApi/image/changeImageTest.ts | 1 + .../test/publicApi/image/insertImageTest.ts | 1 + .../publicApi/link/adjustLinkSelectionTest.ts | 1 + .../test/publicApi/link/insertLinkTest.ts | 1 + .../test/publicApi/link/removeLinkTest.ts | 1 + .../publicApi/list/setListStartNumberTest.ts | 2 +- .../test/publicApi/list/setListStyleTest.ts | 2 +- .../test/publicApi/list/toggleBulletTest.ts | 1 + .../publicApi/list/toggleNumberingTest.ts | 1 + .../publicApi/segment/changeFontSizeTest.ts | 1 + .../publicApi/segment/segmentTestCommon.ts | 3 +- .../publicApi/table/setTableCellShadeTest.ts | 1 + .../utils/formatImageWithContentModelTest.ts | 1 + .../formatParagraphWithContentModelTest.ts | 5 +- .../formatSegmentWithContentModelTest.ts | 1 + .../utils/formatWithContentModelTest.ts | 38 +++ .../test/publicApi/utils/pasteTest.ts | 11 +- .../lib/editor/EditorBase.ts | 17 +- .../lib/interface/IEditor.ts | 8 +- 38 files changed, 645 insertions(+), 85 deletions(-) diff --git a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts b/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts index aa8bcaca30c..f23f8a65870 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/editor/corePlugins/ContentModelCopyPastePlugin.ts @@ -33,6 +33,7 @@ import { ClipboardData, SelectionRangeTypes, SelectionRangeEx, + ColorTransformDirection, } from 'roosterjs-editor-types'; /** @@ -94,7 +95,24 @@ export default class ContentModelCopyPastePlugin implements PluginWithState { + if (type == 'cache') { + return undefined; + } else { + const result = node.cloneNode(true /*deep*/) as HTMLElement; + + this.editor?.transformToDarkColor( + result, + ColorTransformDirection.DarkToLight + ); + + return result; + } + } + : false, + }); if (selection.type === SelectionRangeTypes.TableSelection) { iterateSelections([pasteModel], (path, tableContext) => { if (tableContext?.table) { diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/cloneModel.ts b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/cloneModel.ts index 9c8a0fb3012..e038963c0c4 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/cloneModel.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/cloneModel.ts @@ -27,18 +27,27 @@ import type { ContentModelListLevel, } from 'roosterjs-content-model-types'; +/** + * @internal + */ +export type CachedElementHandler = ( + node: HTMLElement, + type: 'general' | 'entity' | 'cache' +) => HTMLElement | undefined; + /** * @internal * Options for cloneModel API */ export interface CloneModelOptions { /** - * When pass false or not passed, the cloned model will not have cached element even they exist in original model. - * For entity and general model, a cloned wrapper element will be added into cloned model. So that the cloned model will be fully disconnected from the original one - * When pass true, cloned model will have the same cached element and element wrapper with the original model - * @default true + * Specify how to deal with cached element, including cached block element, element in General Model, and wrapper element in Entity + * - True: Cloned model will have the same reference to the cached element + * - False/Not passed: For cached block element, cached element will be undefined. For General Model and Entity, the element will have deep clone and assign to the cloned model + * - A callback: invoke the callback with the source cached element and a string to specify model type, let the callback return the expected value of cached element. + * For General Model and Entity, the callback must return a valid element, otherwise there will be exception thrown. */ - includeCachedElement?: boolean; + includeCachedElement?: boolean | CachedElementHandler; } /** @@ -167,9 +176,7 @@ function cloneEntity(entity: ContentModelEntity, options: CloneModelOptions): Co return Object.assign( { - wrapper: options.includeCachedElement - ? wrapper - : (wrapper.cloneNode(true /*deep*/) as HTMLElement), + wrapper: handleCachedElement(wrapper, 'entity', options), isReadonly, type, id, @@ -187,7 +194,7 @@ function cloneParagraph( const newParagraph: ContentModelParagraph = Object.assign( { - cachedElement: options.includeCachedElement ? cachedElement : undefined, + cachedElement: handleCachedElement(cachedElement, 'cache', options), isImplicit, segments: segments.map(segment => cloneSegment(segment, options)), segmentFormat: segmentFormat ? { ...segmentFormat } : undefined, @@ -213,7 +220,7 @@ function cloneTable(table: ContentModelTable, options: CloneModelOptions): Conte return Object.assign( { - cachedElement: options.includeCachedElement ? cachedElement : undefined, + cachedElement: handleCachedElement(cachedElement, 'cache', options), widths: Array.from(widths), rows: rows.map(row => cloneTableRow(row, options)), }, @@ -231,7 +238,7 @@ function cloneTableRow( return Object.assign( { height, - cachedElement: options.includeCachedElement ? cachedElement : undefined, + cachedElement: handleCachedElement(cachedElement, 'cache', options), cells: cells.map(cell => cloneTableCell(cell, options)), }, cloneModelWithFormat(row) @@ -246,7 +253,7 @@ function cloneTableCell( return Object.assign( { - cachedElement: options.includeCachedElement ? cachedElement : undefined, + cachedElement: handleCachedElement(cachedElement, 'cache', options), isSelected, spanAbove, spanLeft, @@ -264,7 +271,7 @@ function cloneFormatContainer( ): ContentModelFormatContainer { const { tagName, cachedElement } = container; const newContainer: ContentModelFormatContainer = Object.assign( - { tagName, cachedElement: options.includeCachedElement ? cachedElement : undefined }, + { tagName, cachedElement: handleCachedElement(cachedElement, 'cache', options) }, cloneBlockBase(container), cloneBlockGroupBase(container, options) ); @@ -307,7 +314,7 @@ function cloneDivider( { isSelected, tagName, - cachedElement: options.includeCachedElement ? cachedElement : undefined, + cachedElement: handleCachedElement(cachedElement, 'cache', options), }, cloneBlockBase(divider) ); @@ -321,9 +328,7 @@ function cloneGeneralBlock( return Object.assign( { - element: options.includeCachedElement - ? element - : (element.cloneNode(true /*deep*/) as HTMLElement), + element: handleCachedElement(element, 'general', options), }, cloneBlockBase(general), cloneBlockGroupBase(general, options) @@ -355,3 +360,39 @@ function cloneText(textSegment: ContentModelText): ContentModelText { const { text } = textSegment; return Object.assign({ text }, cloneSegmentBase(textSegment)); } + +function handleCachedElement( + node: T, + type: 'general' | 'entity', + options: CloneModelOptions +): T; + +function handleCachedElement( + node: T | undefined, + type: 'cache', + options: CloneModelOptions +): T | undefined; + +function handleCachedElement( + node: T | undefined, + type: 'general' | 'entity' | 'cache', + options: CloneModelOptions +): T | undefined { + const { includeCachedElement } = options; + + if (!node) { + return undefined; + } else if (!includeCachedElement) { + return type == 'cache' ? undefined : (node.cloneNode(true /*deep*/) as T); + } else if (includeCachedElement === true) { + return node; + } else { + const result = includeCachedElement(node, type) as T | undefined; + + if ((type == 'general' || type == 'entity') && !result) { + throw new Error('Entity and General Model must has wrapper element'); + } + + return result; + } +} diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts index 0e28c4c3847..01ce919a8ba 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/common/mergeModel.ts @@ -83,12 +83,16 @@ export function mergeModel( switch (block.blockType) { case 'Paragraph': - mergeParagraph(insertPosition, block, i == 0); + mergeParagraph(insertPosition, block, i == 0, context); break; case 'Divider': + insertBlock(insertPosition, block); + break; + case 'Entity': insertBlock(insertPosition, block); + context?.newEntities.push(block); break; case 'Table': @@ -120,7 +124,8 @@ export function mergeModel( function mergeParagraph( markerPosition: InsertPoint, newPara: ContentModelParagraph, - mergeToCurrentParagraph: boolean + mergeToCurrentParagraph: boolean, + context?: FormatWithContentModelContext ) { const { paragraph, marker } = markerPosition; const newParagraph = mergeToCurrentParagraph @@ -129,7 +134,15 @@ function mergeParagraph( const segmentIndex = newParagraph.segments.indexOf(marker); if (segmentIndex >= 0) { - newParagraph.segments.splice(segmentIndex, 0, ...newPara.segments); + for (let i = 0; i < newPara.segments.length; i++) { + const segment = newPara.segments[i]; + + newParagraph.segments.splice(segmentIndex + i, 0, segment); + + if (context && segment.segmentType == 'Entity') { + context.newEntities.push(segment); + } + } } if (newPara.decorator) { diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/entity/insertEntity.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/entity/insertEntity.ts index fb65075c93f..00559f98260 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/entity/insertEntity.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/entity/insertEntity.ts @@ -1,6 +1,6 @@ import { ChangeSource, Entity, SelectionRangeEx } from 'roosterjs-editor-types'; import { commitEntity, getEntityFromElement } from 'roosterjs-editor-dom'; -import { createEntity } from 'roosterjs-content-model-dom'; +import { createEntity, normalizeContentModel } from 'roosterjs-content-model-dom'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import { insertEntityModel } from '../../modelApi/entity/insertEntityModel'; @@ -84,7 +84,10 @@ export default function insertEntity( context ); + normalizeContentModel(model); + context.skipUndoSnapshot = skipUndoSnapshot; + context.newEntities.push(entityModel); return true; }, @@ -93,10 +96,6 @@ export default function insertEntity( } ); - if (editor.isDarkMode()) { - editor.transformToDarkColor(wrapper); - } - const newEntity = getEntityFromElement(wrapper); editor.triggerContentChangedEvent(ChangeSource.InsertEntity, newEntity); diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatWithContentModel.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatWithContentModel.ts index 1d3d9ec6a4d..4979e5d3e4c 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatWithContentModel.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicApi/utils/formatWithContentModel.ts @@ -36,12 +36,14 @@ export function formatWithContentModel( const model = editor.createContentModel(undefined /*option*/, selectionOverride); const context: FormatWithContentModelContext = { + newEntities: [], deletedEntities: [], rawEvent, }; if (formatter(model, context)) { const callback = () => { + handleNewEntities(editor, context); handleDeletedEntities(editor, context); if (model) { @@ -81,6 +83,18 @@ export function formatWithContentModel( } } +function handleNewEntities(editor: IContentModelEditor, context: FormatWithContentModelContext) { + // TODO: Ideally we can trigger NewEntity event here. But to be compatible with original editor code, we don't do it here for now. + // Once Content Model Editor can be standalone, we can change this behavior to move triggering NewEntity event code + // from EntityPlugin to here + + if (editor.isDarkMode()) { + context.newEntities.forEach(entity => { + editor.transformToDarkColor(entity.wrapper); + }); + } +} + function handleDeletedEntities( editor: IContentModelEditor, context: FormatWithContentModelContext diff --git a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/FormatWithContentModelContext.ts b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/FormatWithContentModelContext.ts index da2f5627b76..37e7b13284e 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/FormatWithContentModelContext.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/publicTypes/parameter/FormatWithContentModelContext.ts @@ -24,6 +24,11 @@ export interface DeletedEntity { * Context object for API formatWithContentModel */ export interface FormatWithContentModelContext { + /** + * New entities added during the format process + */ + readonly newEntities: ContentModelEntity[]; + /** * Entities got deleted during formatting. Need to be set by the formatter function */ diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCopyPastePluginTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCopyPastePluginTest.ts index 639319f96b4..11d282ee014 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCopyPastePluginTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelCopyPastePluginTest.ts @@ -1,18 +1,20 @@ import * as cloneModelFile from '../../../lib/modelApi/common/cloneModel'; import * as contentModelToDomFile from 'roosterjs-content-model-dom/lib/modelToDom/contentModelToDom'; -import * as createRangeF from 'roosterjs-editor-dom/lib/selection/createRange'; import * as deleteSelectionsFile from '../../../lib/modelApi/edit/deleteSelection'; import * as extractClipboardItemsFile from 'roosterjs-editor-dom/lib/clipboard/extractClipboardItems'; import * as iterateSelectionsFile from '../../../lib/modelApi/selection/iterateSelections'; import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; import * as PasteFile from '../../../lib/publicApi/utils/paste'; +import { commitEntity } from 'roosterjs-editor-dom'; import { DeleteResult } from '../../../lib/modelApi/edit/utils/DeleteSelectionStep'; import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import createRange, * as createRangeF from 'roosterjs-editor-dom/lib/selection/createRange'; import ContentModelCopyPastePlugin, { onNodeCreated, } from '../../../lib/editor/corePlugins/ContentModelCopyPastePlugin'; import { ClipboardData, + ColorTransformDirection, DOMEventHandlerFunction, IEditor, SelectionRangeEx, @@ -45,6 +47,8 @@ describe('ContentModelCopyPastePlugin |', () => { let isDisposed: jasmine.Spy; let pasteSpy: jasmine.Spy; + let cloneModelSpy: jasmine.Spy; + let transformToDarkColorSpy: jasmine.Spy; beforeEach(() => { div = document.createElement('div'); @@ -63,7 +67,10 @@ describe('ContentModelCopyPastePlugin |', () => { pasteSpy = jasmine.createSpy('paste_'); isDisposed = jasmine.createSpy('isDisposed'); - spyOn(cloneModelFile, 'cloneModel').and.callFake((model: any) => pasteModelValue); + cloneModelSpy = spyOn(cloneModelFile, 'cloneModel').and.callFake( + (model: any) => pasteModelValue + ); + transformToDarkColorSpy = jasmine.createSpy('transformToDarkColor'); plugin = new ContentModelCopyPastePlugin({ allowedCustomPasteType, @@ -119,6 +126,7 @@ describe('ContentModelCopyPastePlugin |', () => { paste: (ar1: any) => { pasteSpy(ar1); }, + transformToDarkColor: transformToDarkColorSpy, isDisposed, }); @@ -311,6 +319,82 @@ describe('ContentModelCopyPastePlugin |', () => { expect(setContentModelSpy).not.toHaveBeenCalledWith(); expect(iterateSelectionsFile.iterateSelections).toHaveBeenCalledTimes(0); }); + + it('Selection not Collapsed and entity selection in Dark mode', () => { + // Arrange + const wrapper = document.createElement('span'); + + document.body.appendChild(wrapper); + + commitEntity(wrapper, 'Entity', true, 'Entity'); + selectionRangeExValue = { + type: SelectionRangeTypes.Normal, + ranges: [createRange(wrapper)], + areAllCollapsed: false, + }; + + spyOn(deleteSelectionsFile, 'deleteSelection'); + spyOn(contentModelToDomFile, 'contentModelToDom').and.callFake(() => { + div.appendChild(wrapper); + return selectionRangeExValue; + }); + spyOn(iterateSelectionsFile, 'iterateSelections').and.returnValue(undefined); + + triggerPluginEventSpy.and.callThrough(); + focusSpy.and.callThrough(); + selectSpy.and.callThrough(); + setContentModelSpy.and.callThrough(); + + editor.isDarkMode = () => true; + + cloneModelSpy.and.callFake((model, options) => { + expect(model).toEqual(modelValue); + expect(typeof options.includeCachedElement).toBe('function'); + + const cloneCache = options.includeCachedElement(wrapper, 'cache'); + const cloneEntity = options.includeCachedElement(wrapper, 'entity'); + + expect(cloneCache).toBeUndefined(); + expect(cloneEntity).toEqual(wrapper); + expect(cloneEntity).not.toBe(wrapper); + expect(transformToDarkColorSpy).toHaveBeenCalledTimes(1); + expect(transformToDarkColorSpy).toHaveBeenCalledWith( + cloneEntity, + ColorTransformDirection.DarkToLight + ); + + return pasteModelValue; + }); + + // Act + domEvents.copy?.({}); + + // Assert + expect(getSelectionRangeEx).toHaveBeenCalled(); + expect(deleteSelectionsFile.deleteSelection).not.toHaveBeenCalled(); + expect(contentModelToDomFile.contentModelToDom).toHaveBeenCalledWith( + document, + div, + pasteModelValue, + undefined, + { onNodeCreated } + ); + expect(createContentModelSpy).toHaveBeenCalled(); + expect(triggerPluginEventSpy).toHaveBeenCalledTimes(1); + expect(focusSpy).toHaveBeenCalled(); + expect(selectSpy).toHaveBeenCalledWith( + selectionRangeExValue, + undefined, + undefined, + undefined + ); + expect(cloneModelSpy).toHaveBeenCalledTimes(1); + + // On Cut Spy + expect(undoSnapShotSpy).not.toHaveBeenCalled(); + expect(setContentModelSpy).not.toHaveBeenCalledWith(); + expect(iterateSelectionsFile.iterateSelections).toHaveBeenCalledTimes(0); + }); }); describe('Cut |', () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelFormatPluginTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelFormatPluginTest.ts index d25c9c4bff7..8e0651cf8ce 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelFormatPluginTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/plugins/ContentModelFormatPluginTest.ts @@ -16,6 +16,7 @@ describe('ContentModelFormatPlugin', () => { const editor = ({ cacheContentModel: () => {}, + isDarkMode: () => false, } as any) as IContentModelEditor; const plugin = new ContentModelFormatPlugin(); @@ -149,6 +150,7 @@ describe('ContentModelFormatPlugin', () => { callback(); }, cacheContentModel: () => {}, + isDarkMode: () => false, } as any) as IContentModelEditor; const plugin = new ContentModelFormatPlugin(); @@ -211,6 +213,7 @@ describe('ContentModelFormatPlugin', () => { callback(); }, cacheContentModel: () => {}, + isDarkMode: () => false, } as any) as IContentModelEditor; const plugin = new ContentModelFormatPlugin(); diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/utils/handleKeyboardEventCommonTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/utils/handleKeyboardEventCommonTest.ts index 613500dc825..faa405fd541 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/utils/handleKeyboardEventCommonTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/utils/handleKeyboardEventCommonTest.ts @@ -42,7 +42,7 @@ describe('handleKeyboardEventResult', () => { const mockedModel = 'MODEL' as any; const which = 'WHICH' as any; (mockedEvent).which = which; - const context: FormatWithContentModelContext = { deletedEntities: [] }; + const context: FormatWithContentModelContext = { newEntities: [], deletedEntities: [] }; const result = handleKeyboardEventResult( mockedEditor, mockedModel, @@ -64,7 +64,7 @@ describe('handleKeyboardEventResult', () => { it('DeleteResult.NotDeleted', () => { const mockedModel = 'MODEL' as any; - const context: FormatWithContentModelContext = { deletedEntities: [] }; + const context: FormatWithContentModelContext = { newEntities: [], deletedEntities: [] }; const result = handleKeyboardEventResult( mockedEditor, mockedModel, @@ -84,7 +84,7 @@ describe('handleKeyboardEventResult', () => { it('DeleteResult.Range', () => { const mockedModel = 'MODEL' as any; - const context: FormatWithContentModelContext = { deletedEntities: [] }; + const context: FormatWithContentModelContext = { newEntities: [], deletedEntities: [] }; const result = handleKeyboardEventResult( mockedEditor, mockedModel, @@ -106,7 +106,7 @@ describe('handleKeyboardEventResult', () => { it('DeleteResult.NothingToDelete', () => { const mockedModel = 'MODEL' as any; - const context: FormatWithContentModelContext = { deletedEntities: [] }; + const context: FormatWithContentModelContext = { newEntities: [], deletedEntities: [] }; const result = handleKeyboardEventResult( mockedEditor, mockedModel, diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/cloneModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/cloneModelTest.ts index 4299f581179..c83e226848c 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/cloneModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/cloneModelTest.ts @@ -1,5 +1,6 @@ import { cloneModel } from '../../../lib/modelApi/common/cloneModel'; import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { createEntity } from 'roosterjs-content-model-dom'; describe('cloneModel', () => { function compareObjects(o1: any, o2: any, allowCache: boolean, path: string = '/') { @@ -281,4 +282,233 @@ describe('cloneModel', () => { ], }); }); + + describe('Clone with callback', () => { + it('Paragraph without cache', () => { + const callback = jasmine + .createSpy('callback') + .and.callFake((node: Node, type: string) => { + return undefined; + }); + const cloneWithCallback = cloneModel( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segmentFormat: { fontSize: '20px' }, + segments: [], + }, + ], + }, + { includeCachedElement: callback } + ); + + expect(cloneWithCallback).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segmentFormat: { fontSize: '20px' }, + segments: [], + cachedElement: undefined, + isImplicit: undefined, + }, + ], + }); + expect(callback).not.toHaveBeenCalled(); + }); + + it('Paragraph with cache, return undefined', () => { + const callback = jasmine + .createSpy('callback') + .and.callFake((node: Node, type: string) => { + return undefined; + }); + const div = document.createElement('div'); + const cloneWithCallback = cloneModel( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segmentFormat: { fontSize: '20px' }, + segments: [], + cachedElement: div, + }, + ], + }, + { includeCachedElement: callback } + ); + + expect(cloneWithCallback).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segmentFormat: { fontSize: '20px' }, + segments: [], + cachedElement: undefined, + isImplicit: undefined, + }, + ], + }); + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(div, 'cache'); + }); + + it('Paragraph with cache, return span', () => { + const div = document.createElement('div'); + const span = document.createElement('span'); + const callback = jasmine + .createSpy('callback') + .and.callFake((node: Node, type: string) => { + return span; + }); + const cloneWithCallback = cloneModel( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segmentFormat: { fontSize: '20px' }, + segments: [], + cachedElement: div, + }, + ], + }, + { includeCachedElement: callback } + ); + + expect(cloneWithCallback).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segmentFormat: { fontSize: '20px' }, + segments: [], + cachedElement: span, + isImplicit: undefined, + }, + ], + }); + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(div, 'cache'); + }); + + it('Entity, return undefined', () => { + const div = document.createElement('div'); + const callback = jasmine + .createSpy('callback') + .and.callFake((node: Node, type: string) => { + return undefined; + }); + expect(() => + cloneModel( + { + blockGroupType: 'Document', + blocks: [createEntity(div, true)], + }, + { includeCachedElement: callback } + ) + ).toThrow(); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(div, 'entity'); + }); + }); + + it('Entity, return span', () => { + const div = document.createElement('div'); + const span = document.createElement('span'); + const callback = jasmine.createSpy('callback').and.callFake((node: Node, type: string) => { + return span; + }); + const cloneWithCallback = cloneModel( + { + blockGroupType: 'Document', + blocks: [createEntity(div, true)], + }, + { includeCachedElement: callback } + ); + + expect(cloneWithCallback).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Entity', + format: {}, + wrapper: span, + isReadonly: true, + type: undefined, + id: undefined, + segmentType: 'Entity', + isSelected: undefined, + }, + ], + }); + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(div, 'entity'); + }); + + it('Inline entity, return span', () => { + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + + div1.id = 'div1'; + div2.id = 'div2'; + + const span = document.createElement('span'); + const callback = jasmine.createSpy('callback').and.callFake((node: Node, type: string) => { + return node == div1 ? span : node; + }); + const cloneWithCallback = cloneModel( + { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [createEntity(div1, true)], + cachedElement: div2, + }, + ], + }, + { includeCachedElement: callback } + ); + + expect(cloneWithCallback).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + { + blockType: 'Entity', + format: {}, + wrapper: span, + isReadonly: true, + type: undefined, + id: undefined, + segmentType: 'Entity', + isSelected: undefined, + }, + ], + cachedElement: div2, + isImplicit: undefined, + segmentFormat: undefined, + }, + ], + }); + expect(callback).toHaveBeenCalledTimes(2); + expect(callback).toHaveBeenCalledWith(div1, 'entity'); + expect(callback).toHaveBeenCalledWith(div2, 'cache'); + }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts index 3e43abcc795..a3a98253fc3 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/modelApi/common/mergeModelTest.ts @@ -1,8 +1,11 @@ import * as applyTableFormat from '../../../lib/modelApi/table/applyTableFormat'; import * as normalizeTable from '../../../lib/modelApi/table/normalizeTable'; import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { EntityOperation } from 'roosterjs-editor-types'; +import { FormatWithContentModelContext } from '../../../lib/publicTypes/parameter/FormatWithContentModelContext'; import { mergeModel } from '../../../lib/modelApi/common/mergeModel'; import { + createBr, createContentModelDocument, createDivider, createEntity, @@ -25,7 +28,7 @@ describe('mergeModel', () => { para.segments.push(marker); majorModel.blocks.push(para); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -65,7 +68,7 @@ describe('mergeModel', () => { para2.segments.push(text1, text2); sourceModel.blocks.push(para2); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -115,7 +118,7 @@ describe('mergeModel', () => { majorModel.blocks.push(para1); sourceModel.blocks.push(para2); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -194,7 +197,7 @@ describe('mergeModel', () => { sourceModel.blocks.push(newPara1); sourceModel.blocks.push(newPara2); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -289,7 +292,7 @@ describe('mergeModel', () => { sourceModel.blocks.push(newPara2); sourceModel.blocks.push(newPara3); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -436,7 +439,7 @@ describe('mergeModel', () => { sourceModel.blocks.push(newList1); sourceModel.blocks.push(newList2); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -602,7 +605,7 @@ describe('mergeModel', () => { sourceModel.blocks.push(newList1); sourceModel.blocks.push(newList2); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -790,7 +793,7 @@ describe('mergeModel', () => { sourceModel.blocks.push(newTable1); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -891,7 +894,7 @@ describe('mergeModel', () => { spyOn(applyTableFormat, 'applyTableFormat'); spyOn(normalizeTable, 'normalizeTable'); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(normalizeTable.normalizeTable).not.toHaveBeenCalled(); expect(majorModel).toEqual({ @@ -1011,7 +1014,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeTable: true, } @@ -1153,7 +1156,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeTable: true, } @@ -1284,7 +1287,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeTable: true, } @@ -1398,7 +1401,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { insertPosition: { marker: marker2, @@ -1481,7 +1484,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeFormat: 'mergeAll', } @@ -1543,7 +1546,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeFormat: 'keepSourceEmphasisFormat', } @@ -1611,7 +1614,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeFormat: 'keepSourceEmphasisFormat', } @@ -1706,7 +1709,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeFormat: 'keepSourceEmphasisFormat', } @@ -1782,7 +1785,7 @@ describe('mergeModel', () => { sourceModel.blocks.push(divider); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -1849,7 +1852,7 @@ describe('mergeModel', () => { sourceModel.blocks.push(newPara1); sourceModel.blocks.push(newPara2); - mergeModel(majorModel, sourceModel, { deletedEntities: [] }); + mergeModel(majorModel, sourceModel, { newEntities: [], deletedEntities: [] }); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -1949,7 +1952,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeFormat: 'keepSourceEmphasisFormat', } @@ -2030,7 +2033,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeFormat: 'mergeAll', } @@ -2214,7 +2217,7 @@ describe('mergeModel', () => { mergeModel( majorModel, sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeFormat: 'mergeAll', } @@ -2752,9 +2755,13 @@ describe('mergeModel', () => { textColor: 'aliceblue', italic: true, }); + const context: FormatWithContentModelContext = { + deletedEntities: [], + newEntities: [], + }; sourceModel.blocks.push(newEntity); - mergeModel(majorModel, sourceModel); + mergeModel(majorModel, sourceModel, context); expect(majorModel).toEqual({ blockGroupType: 'Document', @@ -2807,5 +2814,64 @@ describe('mergeModel', () => { }, ], }); + expect(context).toEqual({ + newEntities: [newEntity], + deletedEntities: [], + }); + }); + + it('Merge and replace inline entities', () => { + const majorModel = createContentModelDocument(); + const para1 = createParagraph(); + const sourceEntity = createEntity('wrapper1' as any, true, 'E0'); + const sourceBr = createBr(); + + sourceEntity.isSelected = true; + para1.segments.push(sourceEntity, sourceBr); + majorModel.blocks.push(para1); + + const sourceModel: ContentModelDocument = createContentModelDocument(); + const newPara = createParagraph(); + const newEntity1 = createEntity('wrapper2' as any, true, 'E1'); + const newEntity2 = createEntity('wrapper2' as any, true, 'E2'); + const text = createText('test'); + + newPara.segments.push(newEntity1, text, newEntity2); + sourceModel.blocks.push(newPara); + + const context: FormatWithContentModelContext = { + deletedEntities: [], + newEntities: [], + }; + mergeModel(majorModel, sourceModel, context); + + expect(majorModel).toEqual({ + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + format: {}, + segments: [ + newEntity1, + text, + newEntity2, + { + segmentType: 'SelectionMarker', + format: {}, + isSelected: true, + }, + ], + }, + ], + }); + expect(context).toEqual({ + newEntities: [newEntity1, newEntity2], + deletedEntities: [ + { + entity: sourceEntity, + operation: EntityOperation.Overwrite, + }, + ], + }); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/deleteSelectionTest.ts b/packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/deleteSelectionTest.ts index d85e654052e..abf0459143e 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/deleteSelectionTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/modelApi/edit/deleteSelectionTest.ts @@ -1,4 +1,4 @@ -import { ContentModelSelectionMarker } from 'roosterjs-content-model-types'; +import { ContentModelEntity, ContentModelSelectionMarker } from 'roosterjs-content-model-types'; import { DeletedEntity } from '../../../lib/publicTypes/parameter/FormatWithContentModelContext'; import { DeleteResult } from '../../../lib/modelApi/edit/utils/DeleteSelectionStep'; import { deleteSelection } from '../../../lib/modelApi/edit/deleteSelection'; @@ -527,7 +527,7 @@ describe('deleteSelection - selectionOnly', () => { entity.isSelected = true; - const result = deleteSelection(model, [], { deletedEntities }); + const result = deleteSelection(model, [], { newEntities: [], deletedEntities }); expect(result.deleteResult).toBe(DeleteResult.Range); expect(result.insertPoint).toEqual({ @@ -582,7 +582,7 @@ describe('deleteSelection - selectionOnly', () => { entity.isSelected = true; const deletedEntities: DeletedEntity[] = []; - const result = deleteSelection(model, [], { deletedEntities }); + const result = deleteSelection(model, [], { newEntities: [], deletedEntities }); expect(result.deleteResult).toBe(DeleteResult.Range); expect(result.insertPoint).toEqual({ @@ -1485,6 +1485,7 @@ describe('deleteSelection - forward', () => { const deletedEntities: DeletedEntity[] = []; const result = deleteSelection(model, [forwardDeleteCollapsedSelection], { + newEntities: [], deletedEntities, }); @@ -1531,6 +1532,7 @@ describe('deleteSelection - forward', () => { const deletedEntities: DeletedEntity[] = []; const result = deleteSelection(model, [forwardDeleteCollapsedSelection], { + newEntities: [], deletedEntities, }); @@ -3239,6 +3241,7 @@ describe('deleteSelection - backward', () => { const deletedEntities: DeletedEntity[] = []; const result = deleteSelection(model, [backwardDeleteCollapsedSelection], { + newEntities: [], deletedEntities, }); @@ -3284,7 +3287,9 @@ describe('deleteSelection - backward', () => { model.blocks.push(entity, para); const deletedEntities: DeletedEntity[] = []; + const newEntities: ContentModelEntity[] = []; const result = deleteSelection(model, [backwardDeleteCollapsedSelection], { + newEntities, deletedEntities, }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/paragraphTestCommon.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/paragraphTestCommon.ts index 3d03ff1451d..ad3bae0da25 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/paragraphTestCommon.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/paragraphTestCommon.ts @@ -25,6 +25,7 @@ export function paragraphTestCommon( setContentModel, getCustomData: () => ({}), getFocusedPosition: () => ({}), + isDarkMode: () => false, } as any) as IContentModelEditor; executionCallback(editor); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setAlignmentTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setAlignmentTest.ts index 1aff35e23a8..dbf14ce2738 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setAlignmentTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/block/setAlignmentTest.ts @@ -426,6 +426,7 @@ describe('setAlignment in table', () => { addUndoSnapshot: (callback: Function) => callback(), setContentModel, createContentModel, + isDarkMode: () => false, } as any) as IContentModelEditor; }); @@ -817,6 +818,7 @@ describe('setAlignment in list', () => { addUndoSnapshot: (callback: Function) => callback(), setContentModel, createContentModel, + isDarkMode: () => false, } as any) as IContentModelEditor; }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts index 42b6e0b051f..be7642aab81 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/editingTestCommon.ts @@ -38,6 +38,7 @@ export function editingTestCommon( isDisposed: () => false, getFocusedPosition: () => null as NodePosition, triggerContentChangedEvent, + isDarkMode: () => false, } as any) as IContentModelEditor; executionCallback(editor); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/handleKeyDownEventTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/handleKeyDownEventTest.ts index eef61461c7b..9e54e3e3a24 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/handleKeyDownEventTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/editing/handleKeyDownEventTest.ts @@ -59,6 +59,7 @@ describe('handleKeyDownEvent', () => { ); expect(deleteSelectionSpy).toHaveBeenCalledWith(input, expectedSteps, { + newEntities: [], deletedEntities: [], rawEvent: mockedEvent, skipUndoSnapshot: true, diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/entity/insertEntityTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/entity/insertEntityTest.ts index 78d865ad001..583cf71d2e0 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/entity/insertEntityTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/entity/insertEntityTest.ts @@ -2,6 +2,7 @@ import * as commitEntity from 'roosterjs-editor-dom/lib/entity/commitEntity'; import * as formatWithContentModel from '../../../lib/publicApi/utils/formatWithContentModel'; import * as getEntityFromElement from 'roosterjs-editor-dom/lib/entity/getEntityFromElement'; import * as insertEntityModel from '../../../lib/modelApi/entity/insertEntityModel'; +import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel'; import insertEntity from '../../../lib/publicApi/entity/insertEntity'; import { ChangeSource } from 'roosterjs-editor-types'; import { FormatWithContentModelContext } from '../../../lib/publicTypes/parameter/FormatWithContentModelContext'; @@ -25,12 +26,14 @@ describe('insertEntity', () => { let insertEntityModelSpy: jasmine.Spy; let isDarkModeSpy: jasmine.Spy; let transformToDarkColorSpy: jasmine.Spy; + let normalizeContentModelSpy: jasmine.Spy; const type = 'Entity'; const apiName = 'insertEntity'; beforeEach(() => { context = { + newEntities: [], deletedEntities: [], }; @@ -60,6 +63,7 @@ describe('insertEntity', () => { getDocumentSpy = jasmine.createSpy('getDocumentSpy').and.returnValue({ createElement: createElementSpy, }); + normalizeContentModelSpy = spyOn(normalizeContentModel, 'normalizeContentModel'); editor = { triggerContentChangedEvent: triggerContentChangedEventSpy, @@ -103,6 +107,7 @@ describe('insertEntity', () => { newEntity ); expect(transformToDarkColorSpy).not.toHaveBeenCalled(); + expect(normalizeContentModelSpy).toHaveBeenCalled(); expect(entity).toBe(newEntity); }); @@ -141,6 +146,7 @@ describe('insertEntity', () => { newEntity ); expect(transformToDarkColorSpy).not.toHaveBeenCalled(); + expect(normalizeContentModelSpy).toHaveBeenCalled(); expect(entity).toBe(newEntity); }); @@ -186,6 +192,7 @@ describe('insertEntity', () => { newEntity ); expect(transformToDarkColorSpy).not.toHaveBeenCalled(); + expect(normalizeContentModelSpy).toHaveBeenCalled(); expect(entity).toBe(newEntity); }); @@ -225,7 +232,19 @@ describe('insertEntity', () => { ChangeSource.InsertEntity, newEntity ); - expect(transformToDarkColorSpy).toHaveBeenCalled(); + expect(normalizeContentModelSpy).toHaveBeenCalled(); + + expect(context.newEntities).toEqual([ + { + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + id: undefined, + type: 'Entity', + isReadonly: true, + wrapper, + }, + ]); expect(entity).toBe(newEntity); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/applyPendingFormatTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/applyPendingFormatTest.ts index 40e2e030e61..89d881397da 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/applyPendingFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/applyPendingFormatTest.ts @@ -48,6 +48,7 @@ describe('applyPendingFormat', () => { (_, apiName, callback) => { expect(apiName).toEqual('applyPendingFormat'); callback(model, { + newEntities: [], deletedEntities: [], }); } @@ -118,7 +119,7 @@ describe('applyPendingFormat', () => { spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( (_, apiName, callback) => { expect(apiName).toEqual('applyPendingFormat'); - callback(model, { deletedEntities: [] }); + callback(model, { newEntities: [], deletedEntities: [] }); } ); spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { @@ -178,7 +179,7 @@ describe('applyPendingFormat', () => { spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( (_, apiName, callback) => { expect(apiName).toEqual('applyPendingFormat'); - callback(model, { deletedEntities: [] }); + callback(model, { newEntities: [], deletedEntities: [] }); } ); spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { @@ -236,7 +237,7 @@ describe('applyPendingFormat', () => { spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( (_, apiName, callback) => { expect(apiName).toEqual('applyPendingFormat'); - callback(model, { deletedEntities: [] }); + callback(model, { newEntities: [], deletedEntities: [] }); } ); spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { @@ -281,9 +282,7 @@ describe('applyPendingFormat', () => { spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( (_, apiName, callback) => { expect(apiName).toEqual('applyPendingFormat'); - callback(model, { - deletedEntities: [], - }); + callback(model, { newEntities: [], deletedEntities: [] }); } ); spyOn(iterateSelections, 'iterateSelections').and.callFake((_, callback) => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/clearFormatTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/clearFormatTest.ts index 65a87687c0e..e1c986404bf 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/clearFormatTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/format/clearFormatTest.ts @@ -13,7 +13,7 @@ describe('clearFormat', () => { spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( (_, apiName, callback) => { expect(apiName).toEqual('clearFormat'); - callback(model, { deletedEntities: [] }); + callback(model, { newEntities: [], deletedEntities: [] }); } ); spyOn(clearModelFormat, 'clearModelFormat'); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts index 95466be864c..3bba5147859 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/changeImageTest.ts @@ -50,6 +50,7 @@ describe('changeImage', () => { getDocument: () => document, getSelectionRangeEx, triggerPluginEvent, + isDarkMode: () => false, } as any) as IContentModelEditor; executionCallback(editor); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts index c52438ad26a..935871f07cb 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/image/insertImageTest.ts @@ -37,6 +37,7 @@ describe('insertImage', () => { setContentModel, isDisposed: () => false, getDocument: () => document, + isDarkMode: () => false, } as any) as IContentModelEditor; executionCallback(editor); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/adjustLinkSelectionTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/adjustLinkSelectionTest.ts index 485077fc64c..b0dc6c99104 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/adjustLinkSelectionTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/adjustLinkSelectionTest.ts @@ -25,6 +25,7 @@ describe('adjustLinkSelection', () => { addUndoSnapshot: (callback: Function) => callback(), setContentModel, createContentModel, + isDarkMode: () => false, } as any) as IContentModelEditor; }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/insertLinkTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/insertLinkTest.ts index 2a46ec23de4..772c861e11a 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/insertLinkTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/insertLinkTest.ts @@ -27,6 +27,7 @@ describe('insertLink', () => { createContentModel, getCustomData: () => ({}), getFocusedPosition: () => ({}), + isDarkMode: () => false, } as any) as IContentModelEditor; }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/removeLinkTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/removeLinkTest.ts index 63eb83eaf86..41a82f6be53 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/removeLinkTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/link/removeLinkTest.ts @@ -23,6 +23,7 @@ describe('removeLink', () => { addUndoSnapshot: (callback: Function) => callback(), setContentModel, createContentModel, + isDarkMode: () => false, } as any) as IContentModelEditor; }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStartNumberTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStartNumberTest.ts index b78705d7134..360653666db 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStartNumberTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStartNumberTest.ts @@ -11,7 +11,7 @@ describe('setListStartNumber', () => { spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( (editor, apiName, callback) => { expect(apiName).toBe('setListStartNumber'); - const result = callback(input, { deletedEntities: [] }); + const result = callback(input, { newEntities: [], deletedEntities: [] }); expect(result).toBe(expectedResult); } diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStyleTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStyleTest.ts index 2212976a091..fd10b6896f3 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStyleTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/setListStyleTest.ts @@ -12,7 +12,7 @@ describe('setListStyle', () => { spyOn(formatWithContentModel, 'formatWithContentModel').and.callFake( (editor, apiName, callback) => { expect(apiName).toBe('setListStyle'); - const result = callback(input, { deletedEntities: [] }); + const result = callback(input, { newEntities: [], deletedEntities: [] }); expect(result).toBe(expectedResult); } diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleBulletTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleBulletTest.ts index cef67708fb4..f92a13c6639 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleBulletTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleBulletTest.ts @@ -26,6 +26,7 @@ describe('toggleBullet', () => { setContentModel, getCustomData: () => ({}), getFocusedPosition: () => ({}), + isDarkMode: () => false, } as any) as IContentModelEditor; spyOn(setListType, 'setListType').and.returnValue(true); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleNumberingTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleNumberingTest.ts index 143c00c7130..60641a7cef1 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleNumberingTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/list/toggleNumberingTest.ts @@ -26,6 +26,7 @@ describe('toggleNumbering', () => { setContentModel, getCustomData: () => ({}), getFocusedPosition: () => ({}), + isDarkMode: () => false, } as any) as IContentModelEditor; spyOn(setListType, 'setListType').and.returnValue(true); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts index 25846d896db..d1aa4f7b317 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/changeFontSizeTest.ts @@ -351,6 +351,7 @@ describe('changeFontSize', () => { addUndoSnapshot, focus: jasmine.createSpy(), setContentModel, + isDarkMode: () => false, } as any) as IContentModelEditor; changeFontSize(editor, 'increase'); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/segmentTestCommon.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/segmentTestCommon.ts index 1e3264a3025..d090ec0ed72 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/segmentTestCommon.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/segment/segmentTestCommon.ts @@ -1,7 +1,7 @@ import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; +import { ContentModelDocument } from 'roosterjs-content-model-types'; import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; import { NodePosition } from 'roosterjs-editor-types'; -import { ContentModelDocument } from 'roosterjs-content-model-types'; export function segmentTestCommon( apiName: string, @@ -30,6 +30,7 @@ export function segmentTestCommon( setContentModel, isDisposed: () => false, getFocusedPosition: () => null as NodePosition, + isDarkMode: () => false, } as any) as IContentModelEditor; executionCallback(editor); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/table/setTableCellShadeTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/table/setTableCellShadeTest.ts index f037f540a66..4ac166616ab 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/table/setTableCellShadeTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/table/setTableCellShadeTest.ts @@ -20,6 +20,7 @@ describe('setTableCellShade', () => { addUndoSnapshot: (callback: Function) => callback(), setContentModel, createContentModel, + isDarkMode: () => false, } as any) as IContentModelEditor; }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts index fc76f5f9ca1..d0aa129e122 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatImageWithContentModelTest.ts @@ -225,6 +225,7 @@ function segmentTestForPluginEvent( isDisposed: () => false, getFocusedPosition: () => null as NodePosition, triggerPluginEvent, + isDarkMode: () => false, } as any) as IContentModelEditor; executionCallback(editor); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatParagraphWithContentModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatParagraphWithContentModelTest.ts index 2812d94f958..cb955206700 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatParagraphWithContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatParagraphWithContentModelTest.ts @@ -1,12 +1,12 @@ import * as pendingFormat from '../../../lib/modelApi/format/pendingFormat'; import { ContentModelDocument } from 'roosterjs-content-model-types'; +import { formatParagraphWithContentModel } from '../../../lib/publicApi/utils/formatParagraphWithContentModel'; +import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; import { createContentModelDocument, createParagraph, createText, } from 'roosterjs-content-model-dom'; -import { formatParagraphWithContentModel } from '../../../lib/publicApi/utils/formatParagraphWithContentModel'; -import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; describe('formatParagraphWithContentModel', () => { let editor: IContentModelEditor; @@ -27,6 +27,7 @@ describe('formatParagraphWithContentModel', () => { addUndoSnapshot, createContentModel: () => model, setContentModel, + isDarkMode: () => false, getCustomData: () => ({}), getFocusedPosition: () => 'NewPosition', } as any) as IContentModelEditor; diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatSegmentWithContentModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatSegmentWithContentModelTest.ts index 9cd9442b220..ed70deb83d4 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatSegmentWithContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatSegmentWithContentModelTest.ts @@ -35,6 +35,7 @@ describe('formatSegmentWithContentModel', () => { createContentModel: () => model, setContentModel, getFocusedPosition: () => null as NodePosition, + isDarkMode: () => false, } as any) as IContentModelEditor; }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatWithContentModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatWithContentModelTest.ts index ee12942f250..292ceb6dc16 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatWithContentModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/formatWithContentModelTest.ts @@ -40,6 +40,7 @@ describe('formatWithContentModel', () => { getFocusedPosition, triggerContentChangedEvent, triggerPluginEvent, + isDarkMode: () => false, } as any) as IContentModelEditor; }); @@ -49,6 +50,7 @@ describe('formatWithContentModel', () => { formatWithContentModel(editor, apiName, callback); expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], deletedEntities: [], rawEvent: undefined, }); @@ -64,6 +66,7 @@ describe('formatWithContentModel', () => { formatWithContentModel(editor, apiName, callback); expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], deletedEntities: [], rawEvent: undefined, }); @@ -91,6 +94,7 @@ describe('formatWithContentModel', () => { }); expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], deletedEntities: [], rawEvent: undefined, }); @@ -122,6 +126,7 @@ describe('formatWithContentModel', () => { formatWithContentModel(editor, apiName, callback); expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], deletedEntities: [], rawEvent: undefined, skipUndoSnapshot: true, @@ -136,6 +141,7 @@ describe('formatWithContentModel', () => { formatWithContentModel(editor, apiName, callback, { changeSource: 'TEST' }); expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], deletedEntities: [], rawEvent: undefined, }); @@ -155,6 +161,7 @@ describe('formatWithContentModel', () => { }); expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], deletedEntities: [], rawEvent: undefined, skipUndoSnapshot: true, @@ -171,6 +178,7 @@ describe('formatWithContentModel', () => { formatWithContentModel(editor, apiName, callback, { onNodeCreated: onNodeCreated }); expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], deletedEntities: [], rawEvent: undefined, }); @@ -187,6 +195,7 @@ describe('formatWithContentModel', () => { formatWithContentModel(editor, apiName, callback, { getChangeData }); expect(callback).toHaveBeenCalledWith(mockedModel, { + newEntities: [], deletedEntities: [], rawEvent: undefined, }); @@ -240,6 +249,35 @@ describe('formatWithContentModel', () => { }); }); + it('Has new entity in dark mode', () => { + const wrapper1 = 'W1' as any; + const wrapper2 = 'W2' as any; + const entity1 = { id: 'E1', type: 'E', wrapper: wrapper1, isReadonly: true } as any; + const entity2 = { id: 'E2', type: 'E', wrapper: wrapper2, isReadonly: true } as any; + const rawEvent = 'RawEvent' as any; + const transformToDarkColorSpy = jasmine.createSpy('transformToDarkColor'); + + editor.isDarkMode = () => true; + editor.transformToDarkColor = transformToDarkColorSpy; + + formatWithContentModel( + editor, + apiName, + (model, context) => { + context.newEntities.push(entity1, entity2); + return true; + }, + { + rawEvent: rawEvent, + } + ); + + expect(triggerPluginEvent).not.toHaveBeenCalled(); + expect(transformToDarkColorSpy).toHaveBeenCalledTimes(2); + expect(transformToDarkColorSpy).toHaveBeenCalledWith(wrapper1); + expect(transformToDarkColorSpy).toHaveBeenCalledWith(wrapper2); + }); + it('With selectionOverride', () => { const range = 'MockedRangeEx' as any; diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts index 8f6bed17ffb..1d7fe68b040 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/utils/pasteTest.ts @@ -109,6 +109,7 @@ describe('Paste ', () => { getDocument, getTrustedHTMLHandler, triggerPluginEvent, + isDarkMode: () => false, } as any) as IContentModelEditor; }); @@ -429,7 +430,7 @@ describe('mergePasteContent', () => { pasteF.mergePasteContent( sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, pasteModel, false /* applyCurrentFormat */, undefined /* customizedMerge */ @@ -438,7 +439,7 @@ describe('mergePasteContent', () => { expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, pasteModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeFormat: 'none', mergeTable: true, @@ -517,7 +518,7 @@ describe('mergePasteContent', () => { pasteF.mergePasteContent( sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, pasteModel, false /* applyCurrentFormat */, customizedMerge /* customizedMerge */ @@ -535,7 +536,7 @@ describe('mergePasteContent', () => { pasteF.mergePasteContent( sourceModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, pasteModel, true /* applyCurrentFormat */, undefined /* customizedMerge */ @@ -544,7 +545,7 @@ describe('mergePasteContent', () => { expect(mergeModelFile.mergeModel).toHaveBeenCalledWith( sourceModel, pasteModel, - { deletedEntities: [] }, + { newEntities: [], deletedEntities: [] }, { mergeFormat: 'keepSourceEmphasisFormat', mergeTable: false, diff --git a/packages/roosterjs-editor-core/lib/editor/EditorBase.ts b/packages/roosterjs-editor-core/lib/editor/EditorBase.ts index 861d4146acc..4b949700a32 100644 --- a/packages/roosterjs-editor-core/lib/editor/EditorBase.ts +++ b/packages/roosterjs-editor-core/lib/editor/EditorBase.ts @@ -59,6 +59,7 @@ import { } from 'roosterjs-editor-dom'; import type { CompatibleChangeSource, + CompatibleColorTransformDirection, CompatibleContentPosition, CompatibleExperimentalFeatures, CompatibleGetContentMode, @@ -899,16 +900,16 @@ export class EditorBase