diff --git a/demo/scripts/controls/contentModel/components/model/ContentModelEntityView.tsx b/demo/scripts/controls/contentModel/components/model/ContentModelEntityView.tsx index 4f7c24b3c1c..74c65e90c6a 100644 --- a/demo/scripts/controls/contentModel/components/model/ContentModelEntityView.tsx +++ b/demo/scripts/controls/contentModel/components/model/ContentModelEntityView.tsx @@ -10,9 +10,9 @@ const styles = require('./ContentModelEntityView.scss'); export function ContentModelEntityView(props: { entity: ContentModelEntity }) { const { entity } = props; - const [id, setId] = useProperty(entity.id); - const [isReadonly, setIsReadonly] = useProperty(entity.isReadonly); - const [type, setType] = useProperty(entity.type); + const [id, setId] = useProperty(entity.entityFormat.id); + const [isReadonly, setIsReadonly] = useProperty(entity.entityFormat.isReadonly); + const [type, setType] = useProperty(entity.entityFormat.entityType); const idTextBox = React.useRef(null); const isReadonlyCheckBox = React.useRef(null); @@ -20,17 +20,17 @@ export function ContentModelEntityView(props: { entity: ContentModelEntity }) { const onIdChange = React.useCallback(() => { const newValue = idTextBox.current.value; - entity.id = newValue; + entity.entityFormat.id = newValue; setId(newValue); }, [id, setId]); const onTypeChange = React.useCallback(() => { const newValue = typeTextBox.current.value; - entity.type = newValue; + entity.entityFormat.entityType = newValue; setType(newValue); }, [type, setType]); const onReadonlyChange = React.useCallback(() => { const newValue = isReadonlyCheckBox.current.checked; - entity.isReadonly = newValue; + entity.entityFormat.isReadonly = newValue; setIsReadonly(newValue); }, [id, setId]); diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/elementProcessor.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/elementProcessor.ts index 58dff408827..fc89676a175 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/elementProcessor.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/elementProcessor.ts @@ -1,4 +1,5 @@ -import { getDelimiterFromElement, getEntityFromElement } from 'roosterjs-editor-dom'; +import { getDelimiterFromElement } from 'roosterjs-editor-dom'; +import { isEntityElement } from '../../domUtils/entityUtils'; import type { DomToModelContext, ElementProcessor, @@ -22,8 +23,7 @@ export const elementProcessor: ElementProcessor = (group, element, }; function tryGetProcessorForEntity(element: HTMLElement, context: DomToModelContext) { - return (element.className && getEntityFromElement(element)) || - element.contentEditable == 'false' // For readonly element, treat as an entity + return isEntityElement(element) || element.contentEditable == 'false' // For readonly element, treat as an entity ? context.elementProcessors.entity : null; } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/entityProcessor.ts b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/entityProcessor.ts index 336641b72ca..b404137b30e 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/entityProcessor.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/domToModel/processors/entityProcessor.ts @@ -1,8 +1,8 @@ import { addBlock } from '../../modelApi/common/addBlock'; import { addSegment } from '../../modelApi/common/addSegment'; import { createEntity } from '../../modelApi/creators/createEntity'; -import { getEntityFromElement } from 'roosterjs-editor-dom'; import { isBlockElement } from '../utils/isBlockElement'; +import { parseFormat } from '../utils/parseFormat'; import { stackFormat } from '../utils/stackFormat'; import type { ElementProcessor } from 'roosterjs-content-model-types'; @@ -13,17 +13,15 @@ import type { ElementProcessor } from 'roosterjs-content-model-types'; * @param context DOM to Content Model context */ export const entityProcessor: ElementProcessor = (group, element, context) => { - const entity = getEntityFromElement(element); - - // In Content Model we also treat read only element as an entity since we cannot edit it - const { id, type, isReadonly } = entity || { isReadonly: true }; const isBlockEntity = isBlockElement(element, context); stackFormat( context, { segment: isBlockEntity ? 'empty' : undefined, paragraph: 'empty' }, () => { - const entityModel = createEntity(element, isReadonly, type, context.segmentFormat, id); + const entityModel = createEntity(element, true /*isReadonly*/, context.segmentFormat); + + parseFormat(element, context.formatParsers.entity, entityModel.entityFormat, context); // TODO: Need to handle selection for editable entity if (context.isInSelection) { 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 new file mode 100644 index 00000000000..7efd8fad0b0 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-dom/lib/domUtils/entityUtils.ts @@ -0,0 +1,43 @@ +import { isNodeOfType } from './isNodeOfType'; +import type { ContentModelEntityFormat } from 'roosterjs-content-model-types'; + +const ENTITY_INFO_NAME = '_Entity'; +const ENTITY_TYPE_PREFIX = '_EType_'; +const ENTITY_ID_PREFIX = '_EId_'; +const ENTITY_READONLY_PREFIX = '_EReadonly_'; + +/** + * @internal + */ +export function isEntityElement(node: Node): boolean { + return isNodeOfType(node, 'ELEMENT_NODE') && node.classList.contains(ENTITY_INFO_NAME); +} + +/** + * @internal + */ +export function parseEntityClassName( + className: string, + format: ContentModelEntityFormat +): boolean | undefined { + if (className == ENTITY_INFO_NAME) { + return true; + } else if (className.indexOf(ENTITY_TYPE_PREFIX) == 0) { + format.entityType = className.substring(ENTITY_TYPE_PREFIX.length); + } else if (className.indexOf(ENTITY_ID_PREFIX) == 0) { + format.id = className.substring(ENTITY_ID_PREFIX.length); + } else if (className.indexOf(ENTITY_READONLY_PREFIX) == 0) { + format.isReadonly = className.substring(ENTITY_READONLY_PREFIX.length) == '1'; + } +} + +/** + * @internal + */ +export function generateEntityClassNames(format: ContentModelEntityFormat): string { + return format.isFakeEntity + ? '' + : `${ENTITY_INFO_NAME} ${ENTITY_TYPE_PREFIX}${format.entityType ?? ''} ${ + format.id ? `${ENTITY_ID_PREFIX}${format.id} ` : '' + }${ENTITY_READONLY_PREFIX}${format.isReadonly ? '1' : '0'}`; +} diff --git a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/defaultFormatHandlers.ts b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/defaultFormatHandlers.ts index ea61df998f0..f7cba066bfb 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/defaultFormatHandlers.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/defaultFormatHandlers.ts @@ -6,6 +6,7 @@ import { boxShadowFormatHandler } from './common/boxShadowFormatHandler'; import { datasetFormatHandler } from './common/datasetFormatHandler'; import { directionFormatHandler } from './block/directionFormatHandler'; import { displayFormatHandler } from './block/displayFormatHandler'; +import { entityFormatHandler } from './entity/entityFormatHandler'; import { floatFormatHandler } from './common/floatFormatHandler'; import { fontFamilyFormatHandler } from './segment/fontFamilyFormatHandler'; import { fontSizeFormatHandler } from './segment/fontSizeFormatHandler'; @@ -60,6 +61,7 @@ const defaultFormatHandlerMap: FormatHandlers = { float: floatFormatHandler, fontFamily: fontFamilyFormatHandler, fontSize: fontSizeFormatHandler, + entity: entityFormatHandler, htmlAlign: htmlAlignFormatHandler, id: idFormatHandler, italic: italicFormatHandler, @@ -196,6 +198,7 @@ export const defaultFormatKeysPerCategory: { dataset: ['dataset'], divider: [...sharedBlockFormats, ...sharedContainerFormats, 'display', 'size', 'htmlAlign'], container: [...sharedContainerFormats, 'htmlAlign', 'size', 'display'], + entity: ['entity'], }; /** 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 new file mode 100644 index 00000000000..2c360c3f243 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-dom/lib/formatHandlers/entity/entityFormatHandler.ts @@ -0,0 +1,33 @@ +import { generateEntityClassNames, parseEntityClassName } from '../../domUtils/entityUtils'; +import type { EntityInfoFormat, IdFormat } from 'roosterjs-content-model-types'; +import type { FormatHandler } from '../FormatHandler'; + +/** + * @internal + */ +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; + } + }, + + apply: (format, element) => { + if (!format.isFakeEntity) { + element.className = generateEntityClassNames(format); + } + + if (format.isReadonly) { + element.contentEditable = 'false'; + } else { + element.removeAttribute('contenteditable'); + } + }, +}; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/creators/createEntity.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelApi/creators/createEntity.ts index 98e9b363f68..da66859a67b 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelApi/creators/createEntity.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelApi/creators/createEntity.ts @@ -3,25 +3,27 @@ import type { ContentModelEntity, ContentModelSegmentFormat } from 'roosterjs-co /** * Create a ContentModelEntity model * @param wrapper Wrapper element of this entity - * @param isReadonly Whether this is a readonly entity - * @param type @optional Type of this entity + * @param isReadonly Whether this is a readonly entity @default true * @param segmentFormat @optional Segment format of this entity + * @param type @optional Type of this entity * @param id @optional Id of this entity */ export function createEntity( wrapper: HTMLElement, - isReadonly: boolean, - type?: string, + isReadonly: boolean = true, segmentFormat?: ContentModelSegmentFormat, + type?: string, id?: string ): ContentModelEntity { return { segmentType: 'Entity', blockType: 'Entity', format: { ...segmentFormat }, - id, - type, - isReadonly, + entityFormat: { + id, + entityType: type, + isReadonly, + }, wrapper, }; } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleEntity.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleEntity.ts index ea22110bb67..a0402322148 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleEntity.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleEntity.ts @@ -1,12 +1,10 @@ -import { addDelimiters, commitEntity, getObjectKeys, wrap } from 'roosterjs-editor-dom'; +import { addDelimiters, getObjectKeys, wrap } from 'roosterjs-editor-dom'; import { applyFormat } from '../utils/applyFormat'; import { reuseCachedElement } from '../utils/reuseCachedElement'; -import type { Entity } from 'roosterjs-editor-types'; import type { ContentModelBlockHandler, ContentModelEntity, ContentModelSegmentHandler, - ModelToDomContext, } from 'roosterjs-content-model-types'; /** @@ -19,7 +17,9 @@ export const handleEntityBlock: ContentModelBlockHandler = ( context, refNode ) => { - const wrapper = preprocessEntity(entityModel, context); + let { entityFormat, wrapper } = entityModel; + + applyFormat(wrapper, context.formatAppliers.entity, entityFormat, context); refNode = reuseCachedElement(parent, wrapper, refNode); context.onNodeCreated?.(entityModel, wrapper); @@ -37,8 +37,7 @@ export const handleEntitySegment: ContentModelSegmentHandler context, newSegments ) => { - const wrapper = preprocessEntity(entityModel, context); - const { format, isReadonly } = entityModel; + let { entityFormat, wrapper, format } = entityModel; parent.appendChild(wrapper); newSegments?.push(wrapper); @@ -49,7 +48,9 @@ export const handleEntitySegment: ContentModelSegmentHandler applyFormat(span, context.formatAppliers.segment, format, context); } - if (context.addDelimiterForEntity && isReadonly) { + applyFormat(wrapper, context.formatAppliers.entity, entityFormat, context); + + if (context.addDelimiterForEntity && entityFormat.isReadonly) { const [after, before] = addDelimiters(wrapper); newSegments?.push(after, before); @@ -60,23 +61,3 @@ export const handleEntitySegment: ContentModelSegmentHandler context.onNodeCreated?.(entityModel, wrapper); }; - -function preprocessEntity(entityModel: ContentModelEntity, context: ModelToDomContext) { - let { id, type, isReadonly, wrapper } = entityModel; - - const entity: Entity | null = - id && type - ? { - wrapper, - id, - type, - isReadonly: !!isReadonly, - } - : null; - - if (entity) { - // Commit the entity attributes in case there is any change - commitEntity(wrapper, entity.type, entity.isReadonly, entity.id); - } - return wrapper; -} diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/optimizers/optimize.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/optimizers/optimize.ts index f1a8f9ba187..eed720aa157 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/optimizers/optimize.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/optimizers/optimize.ts @@ -1,5 +1,4 @@ -import { EntityClasses } from 'roosterjs-editor-types'; -import { isNodeOfType } from '../../domUtils/isNodeOfType'; +import { isEntityElement } from '../../domUtils/entityUtils'; import { mergeNode } from './mergeNode'; import { removeUnnecessarySpan } from './removeUnnecessarySpan'; @@ -10,10 +9,7 @@ export function optimize(root: Node) { /** * Do no do any optimization to entity */ - if ( - isNodeOfType(root, 'ELEMENT_NODE') && - root.classList.contains(EntityClasses.ENTITY_INFO_NAME) - ) { + if (isEntityElement(root)) { return; } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/utils/reuseCachedElement.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/utils/reuseCachedElement.ts index 5d2c09b6c6f..58827aeaa3d 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/utils/reuseCachedElement.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/utils/reuseCachedElement.ts @@ -1,5 +1,4 @@ -import { getEntityFromElement } from 'roosterjs-editor-dom'; -import { isNodeOfType } from '../../domUtils/isNodeOfType'; +import { isEntityElement } from '../../domUtils/entityUtils'; /** * @internal @@ -9,7 +8,7 @@ export function reuseCachedElement(parent: Node, element: Node, refNode: Node | // Remove nodes before the one we are hitting since they don't appear in Content Model at this position. // But we don't want to touch entity since it would better to keep entity at its place unless it is removed // In that case we will remove it after we have handled all other nodes - while (refNode && refNode != element && !isEntity(refNode)) { + while (refNode && refNode != element && !isEntityElement(refNode)) { const next = refNode.nextSibling; refNode.parentNode?.removeChild(refNode); @@ -37,7 +36,3 @@ export function removeNode(node: Node): Node | null { return next; } - -function isEntity(node: Node) { - return isNodeOfType(node, 'ELEMENT_NODE') && !!getEntityFromElement(node); -} diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/elementProcessorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/elementProcessorTest.ts index 219b3804f45..83d95b35df4 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/elementProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/elementProcessorTest.ts @@ -1,8 +1,8 @@ import * as getDelimiterFromElement from 'roosterjs-editor-dom/lib/delimiter/getDelimiterFromElement'; -import { commitEntity } from 'roosterjs-editor-dom'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { elementProcessor } from '../../../lib/domToModel/processors/elementProcessor'; +import { setEntityElementClasses } from '../../domUtils/entityUtilTest'; import { ContentModelDocument, DomToModelContext, @@ -59,7 +59,7 @@ describe('elementProcessor', () => { it('Entity', () => { const div = document.createElement('div'); - commitEntity(div, 'entity', true, 'entity_1'); + setEntityElementClasses(div, 'entity', true, 'entity_1'); elementProcessor(group, div, context); diff --git a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts index 1cca6e2fe56..26826c063a4 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/domToModel/processors/entityProcessorTest.ts @@ -1,7 +1,7 @@ -import { commitEntity } from 'roosterjs-editor-dom'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; import { entityProcessor } from '../../../lib/domToModel/processors/entityProcessor'; +import { setEntityElementClasses } from '../../domUtils/entityUtilTest'; import { ContentModelDomIndexer, ContentModelEntity, @@ -30,9 +30,12 @@ describe('entityProcessor', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: undefined, - type: undefined, - isReadonly: true, + entityFormat: { + isFakeEntity: true, + id: undefined, + entityType: undefined, + isReadonly: true, + }, wrapper: div, }, ], @@ -43,7 +46,7 @@ describe('entityProcessor', () => { const group = createContentModelDocument(); const div = document.createElement('div'); - commitEntity(div, 'entity', true, 'entity_1'); + setEntityElementClasses(div, 'entity', true, 'entity_1'); entityProcessor(group, div, context); @@ -55,9 +58,11 @@ describe('entityProcessor', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: div, }, ], @@ -68,7 +73,7 @@ describe('entityProcessor', () => { const group = createContentModelDocument(); const span = document.createElement('span'); - commitEntity(span, 'entity', true, 'entity_1'); + setEntityElementClasses(span, 'entity', true, 'entity_1'); entityProcessor(group, span, context); @@ -84,9 +89,11 @@ describe('entityProcessor', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: span, }, ], @@ -106,7 +113,40 @@ describe('entityProcessor', () => { expect(group).toEqual({ blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + isImplicit: true, + segments: [ + { + blockType: 'Entity', + segmentType: 'Entity', + format: {}, + entityFormat: { + isFakeEntity: true, + id: undefined, + entityType: undefined, + isReadonly: true, + }, + wrapper: span, + }, + ], + format: {}, + }, + ], + }); + }); + + it('Readonly element (editable fake entity)', () => { + const group = createContentModelDocument(); + const span = document.createElement('span'); + + span.contentEditable = 'true'; + + entityProcessor(group, span, context); + expect(group).toEqual({ + blockGroupType: 'Document', blocks: [ { blockType: 'Paragraph', @@ -116,9 +156,12 @@ describe('entityProcessor', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: undefined, - type: undefined, - isReadonly: true, + entityFormat: { + isFakeEntity: true, + id: undefined, + entityType: undefined, + isReadonly: false, + }, wrapper: span, }, ], @@ -132,7 +175,7 @@ describe('entityProcessor', () => { const group = createContentModelDocument(); const span = document.createElement('span'); - commitEntity(span, 'entity', true, 'entity_1'); + setEntityElementClasses(span, 'entity', true, 'entity_1'); context.isInSelection = true; entityProcessor(group, span, context); @@ -149,9 +192,11 @@ describe('entityProcessor', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: span, isSelected: true, }, @@ -166,7 +211,7 @@ describe('entityProcessor', () => { const group = createContentModelDocument(); const div = document.createElement('div'); - commitEntity(div, 'entity', true, 'entity_1'); + setEntityElementClasses(div, 'entity', true, 'entity_1'); context.isInSelection = true; context.segmentFormat = { fontFamily: 'Arial', @@ -185,9 +230,7 @@ describe('entityProcessor', () => { segmentType: 'Entity', blockType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { id: 'entity_1', entityType: 'entity', isReadonly: true }, wrapper: div, isSelected: true, }, @@ -207,7 +250,7 @@ describe('entityProcessor', () => { const group = createContentModelDocument(); const span = document.createElement('span'); - commitEntity(span, 'entity', true, 'entity_1'); + setEntityElementClasses(span, 'entity', true, 'entity_1'); const onSegmentSpy = jasmine.createSpy('onSegment'); const domIndexer: ContentModelDomIndexer = { @@ -226,9 +269,7 @@ describe('entityProcessor', () => { blockType: 'Entity', format: {}, wrapper: span, - type: 'entity', - id: 'entity_1', - isReadonly: true, + entityFormat: { entityType: 'entity', id: 'entity_1', isReadonly: true }, }; const paragraphModel: ContentModelParagraph = { blockType: 'Paragraph', 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 new file mode 100644 index 00000000000..7cdcac33cd7 --- /dev/null +++ b/packages-content-model/roosterjs-content-model-dom/test/domUtils/entityUtilTest.ts @@ -0,0 +1,153 @@ +import { ContentModelEntityFormat } from 'roosterjs-content-model-types'; +import { + generateEntityClassNames, + isEntityElement, + parseEntityClassName, +} from '../../lib/domUtils/entityUtils'; + +export function setEntityElementClasses( + wrapper: HTMLElement, + type: string, + isReadonly: boolean, + id?: string +) { + wrapper.className = `_Entity _EType_${type} ${id ? `_EId_${id} ` : ''}_EReadonly_${ + isReadonly ? '1' : '0' + }`; + + if (isReadonly) { + wrapper.contentEditable = 'false'; + } +} + +describe('isEntityElement', () => { + it('Not an entity', () => { + const div = document.createElement('div'); + + const result = isEntityElement(div); + + expect(result).toBeFalse(); + }); + + it('Is an entity', () => { + const div = document.createElement('div'); + + div.className = '_Entity'; + + const result = isEntityElement(div); + + expect(result).toBeTrue(); + }); +}); + +describe('parseEntityClassName', () => { + it('No entity class', () => { + const format: ContentModelEntityFormat = {}; + + const result = parseEntityClassName('test', format); + + expect(result).toBeFalsy(); + expect(format).toEqual({}); + }); + + it('Entity class', () => { + const format: ContentModelEntityFormat = {}; + + const result = parseEntityClassName('_Entity', format); + + expect(result).toBeTrue(); + expect(format).toEqual({}); + }); + + it('EntityId class', () => { + const format: ContentModelEntityFormat = {}; + + const result = parseEntityClassName('_EId_A', format); + + expect(result).toBeFalsy(); + expect(format).toEqual({ + id: 'A', + }); + }); + + it('EntityType class', () => { + const format: ContentModelEntityFormat = {}; + + const result = parseEntityClassName('_EType_B', format); + + expect(result).toBeFalsy(); + expect(format).toEqual({ + entityType: 'B', + }); + }); + + it('Entity readonly class', () => { + const format: ContentModelEntityFormat = {}; + + const result = parseEntityClassName('_EReadonly_1', format); + + expect(result).toBeFalsy(); + expect(format).toEqual({ + isReadonly: true, + }); + }); + + it('Parse class on existing format', () => { + const format: ContentModelEntityFormat = { + id: 'A', + }; + + const result = parseEntityClassName('_EType_B', format); + + expect(result).toBeFalsy(); + expect(format).toEqual({ + id: 'A', + entityType: 'B', + }); + }); +}); + +describe('generateEntityClassNames', () => { + it('Empty format', () => { + const format: ContentModelEntityFormat = {}; + + const className = generateEntityClassNames(format); + + expect(className).toBe('_Entity _EType_ _EReadonly_0'); + }); + + it('Format with type', () => { + const format: ContentModelEntityFormat = { + entityType: 'A', + }; + + const className = generateEntityClassNames(format); + + expect(className).toBe('_Entity _EType_A _EReadonly_0'); + }); + + it('Format with type and id and readonly', () => { + const format: ContentModelEntityFormat = { + entityType: 'A', + id: 'B', + isReadonly: true, + }; + + const className = generateEntityClassNames(format); + + expect(className).toBe('_Entity _EType_A _EId_B _EReadonly_1'); + }); + + it('Fake entity format with type and id and readonly', () => { + const format: ContentModelEntityFormat = { + entityType: 'A', + id: 'B', + isReadonly: true, + isFakeEntity: true, + }; + + const className = generateEntityClassNames(format); + + expect(className).toBe(''); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/entity/entityFormatHandlerTest.ts b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/entity/entityFormatHandlerTest.ts new file mode 100644 index 00000000000..5bbce49a3fe --- /dev/null +++ b/packages-content-model/roosterjs-content-model-dom/test/formatHandlers/entity/entityFormatHandlerTest.ts @@ -0,0 +1,84 @@ +import { createDomToModelContext } from '../../../lib/domToModel/context/createDomToModelContext'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { entityFormatHandler } from '../../../lib/formatHandlers/entity/entityFormatHandler'; +import { + DomToModelContext, + EntityInfoFormat, + IdFormat, + ModelToDomContext, +} from 'roosterjs-content-model-types'; + +describe('entityFormatHandler.parse', () => { + let div: HTMLElement; + let format: EntityInfoFormat & IdFormat; + let context: DomToModelContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createDomToModelContext(); + }); + + it('Not an entity', () => { + entityFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + isFakeEntity: true, + isReadonly: true, + }); + }); + + it('Not an entity, content editable', () => { + div.contentEditable = 'true'; + entityFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + isFakeEntity: true, + isReadonly: false, + }); + }); + + it('Real entity', () => { + div.className = '_Entity _EId_A _EType_B _EReadonly_1'; + entityFormatHandler.parse(format, div, context, {}); + expect(format).toEqual({ + id: 'A', + entityType: 'B', + isReadonly: true, + }); + }); +}); + +describe('entityFormatHandler.apply', () => { + let div: HTMLElement; + let format: EntityInfoFormat & IdFormat; + let context: ModelToDomContext; + + beforeEach(() => { + div = document.createElement('div'); + format = {}; + context = createModelToDomContext(); + }); + + it('No format', () => { + entityFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe('
'); + }); + + it('Fake entity with entity info', () => { + format.isFakeEntity = true; + format.id = 'A'; + format.entityType = 'B'; + format.isReadonly = true; + entityFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe('
'); + }); + + it('Real entity with entity info', () => { + format.id = 'A'; + format.entityType = 'B'; + format.isReadonly = true; + entityFormatHandler.apply(format, div, context); + expect(div.outerHTML).toBe( + '
' + ); + }); +}); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelApi/common/isEmptyTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelApi/common/isEmptyTest.ts index be7e70e3d27..f5bf8275abb 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelApi/common/isEmptyTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelApi/common/isEmptyTest.ts @@ -197,10 +197,12 @@ describe('isEmpty', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - type: 'Test', - id: 'Test', + entityFormat: { + entityType: 'Test', + id: 'Test', + isReadonly: false, + }, wrapper: document.createElement('div'), - isReadonly: false, }); expect(result).toBeFalse(); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts index 42f484fae4c..b86ef0684eb 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelApi/creators/creatorsTest.ts @@ -472,34 +472,36 @@ describe('Creators', () => { it('createEntity', () => { const id = 'entity_1'; - const type = 'entity'; + const entityType = 'entity'; const isReadonly = true; const wrapper = document.createElement('div'); - const entityModel = createEntity(wrapper, isReadonly, type, undefined, id); + const entityModel = createEntity(wrapper, isReadonly, undefined, entityType, id); expect(entityModel).toEqual({ blockType: 'Entity', segmentType: 'Entity', format: {}, - id, - type, - isReadonly, + entityFormat: { + id, + entityType, + isReadonly, + }, wrapper, }); }); it('createEntity with format', () => { const id = 'entity_1'; - const type = 'entity'; + const entityType = 'entity'; const isReadonly = true; const wrapper = document.createElement('div'); const entityModel = createEntity( wrapper, isReadonly, - type, { fontSize: '10pt', }, + entityType, id ); @@ -507,9 +509,11 @@ describe('Creators', () => { blockType: 'Entity', segmentType: 'Entity', format: { fontSize: '10pt' }, - id, - type, - isReadonly, + entityFormat: { + id, + entityType, + isReadonly, + }, wrapper, }); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts index 08c8857d360..489c5d9c097 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockGroupChildrenTest.ts @@ -1,10 +1,10 @@ -import { commitEntity } from 'roosterjs-editor-dom'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createListItem } from '../../../lib/modelApi/creators/createListItem'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; import { handleBlock as originalHandleBlock } from '../../../lib/modelToDom/handlers/handleBlock'; import { handleBlockGroupChildren } from '../../../lib/modelToDom/handlers/handleBlockGroupChildren'; +import { setEntityElementClasses } from '../../domUtils/entityUtilTest'; import { ContentModelBlock, ContentModelBlockGroup, @@ -259,7 +259,10 @@ describe('handleBlockGroupChildren', () => { blockType: 'Entity', format: {}, wrapper: div2, - isReadonly: false, + entityFormat: { + entityType: 'TEST', + isReadonly: false, + }, segmentType: 'Entity', }, { @@ -276,7 +279,7 @@ describe('handleBlockGroupChildren', () => { handleBlockGroupChildren(document, parent, group, context); expect(parent.outerHTML).toBe( - '
test2
' + '
test2
' ); expect(parent.firstChild).toBe(div2); expect(parent.firstChild?.nextSibling).toBe(quote); @@ -336,7 +339,7 @@ describe('handleBlockGroupChildren', () => { div.innerHTML = '
'; const span = document.createElement('span'); - commitEntity(span, 'MyEntity', false); + setEntityElementClasses(span, 'MyEntity', false); parent.appendChild(div); parent.appendChild(span); @@ -359,8 +362,10 @@ describe('handleBlockGroupChildren', () => { segmentType: 'Entity', blockType: 'Entity', wrapper: span, - isReadonly: false, - type: 'MyEntity', + entityFormat: { + isReadonly: false, + entityType: 'MyEntity', + }, format: {}, }, ], diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockTest.ts index 6f04077a554..dc3a20e4785 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBlockTest.ts @@ -143,9 +143,11 @@ describe('handleBlock', () => { segmentType: 'Entity', format: {}, wrapper: element, - type: 'entity', - id: 'entity_1', - isReadonly: true, + entityFormat: { + entityType: 'entity', + id: 'entity_1', + isReadonly: true, + }, }; parent = document.createElement('div'); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts index b6352985ff7..ded0d13fe79 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleEntityTest.ts @@ -22,9 +22,11 @@ describe('handleEntity', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: div, }; @@ -49,7 +51,10 @@ describe('handleEntity', () => { segmentType: 'Entity', format: {}, wrapper: div, - isReadonly: true, + entityFormat: { + isFakeEntity: true, + isReadonly: false, + }, }; div.textContent = 'test'; @@ -63,15 +68,41 @@ describe('handleEntity', () => { expect(addDelimiters.default).toHaveBeenCalledTimes(0); }); + it('Readonly fake entity', () => { + const div = document.createElement('div'); + const entityModel: ContentModelEntity = { + blockType: 'Entity', + segmentType: 'Entity', + format: {}, + wrapper: div, + entityFormat: { + isFakeEntity: true, + isReadonly: true, + }, + }; + + div.textContent = 'test'; + + const parent = document.createElement('div'); + + handleEntityBlock(document, parent, entityModel, context, null); + + expect(parent.innerHTML).toBe('
test
'); + expect(div.outerHTML).toBe('
test
'); + expect(addDelimiters.default).toHaveBeenCalledTimes(0); + }); + it('Simple inline readonly entity', () => { const span = document.createElement('span'); const entityModel: ContentModelEntity = { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: span, }; @@ -94,9 +125,11 @@ describe('handleEntity', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: div, }; @@ -131,9 +164,11 @@ describe('handleEntity', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: entityDiv, }; @@ -151,9 +186,11 @@ describe('handleEntity', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: span, }; @@ -182,9 +219,11 @@ describe('handleEntity', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: span, }; @@ -208,9 +247,11 @@ describe('handleEntity', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: entityDiv, }; @@ -234,9 +275,11 @@ describe('handleEntity', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: span, }; @@ -265,9 +308,11 @@ describe('handleEntity', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: 'entity_1', - type: 'entity', - isReadonly: true, + entityFormat: { + id: 'entity_1', + entityType: 'entity', + isReadonly: true, + }, wrapper: span, }; diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleSegmentTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleSegmentTest.ts index 0c6adfc2de2..d655674001c 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleSegmentTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleSegmentTest.ts @@ -107,10 +107,12 @@ describe('handleSegment', () => { segmentType: 'Entity', blockType: 'Entity', format: {}, - type: 'entity', - id: 'entity_1', + entityFormat: { + entityType: 'entity', + id: 'entity_1', + isReadonly: true, + }, wrapper: div, - isReadonly: true, }; handleSegment(document, parent, segment, context, mockedSegmentNodes); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/optimizers/optimizeTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/optimizers/optimizeTest.ts index 6ae760edabb..3d3a934706f 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/optimizers/optimizeTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/optimizers/optimizeTest.ts @@ -1,7 +1,7 @@ import * as mergeNode from '../../../lib/modelToDom/optimizers/mergeNode'; import * as removeUnnecessarySpan from '../../../lib/modelToDom/optimizers/removeUnnecessarySpan'; -import { commitEntity } from 'roosterjs-editor-dom'; import { optimize } from '../../../lib/modelToDom/optimizers/optimize'; +import { setEntityElementClasses } from '../../domUtils/entityUtilTest'; describe('optimize', () => { beforeEach(() => { @@ -43,7 +43,7 @@ describe('real optimization', () => { span1.textContent = 'test1'; childSpan.textContent = 'entity'; - commitEntity(span2, 'test', true); + setEntityElementClasses(span2, 'test', true); span2.appendChild(childSpan); div.appendChild(span1); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/utils/reuseCachedElementTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/utils/reuseCachedElementTest.ts index 431a62caacb..d09d51e0f0f 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/utils/reuseCachedElementTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/utils/reuseCachedElementTest.ts @@ -1,5 +1,5 @@ -import { commitEntity } from 'roosterjs-editor-dom'; import { reuseCachedElement } from '../../../lib/modelToDom/utils/reuseCachedElement'; +import { setEntityElementClasses } from '../../domUtils/entityUtilTest'; describe('reuseCachedElement', () => { it('No refNode', () => { @@ -71,7 +71,7 @@ describe('reuseCachedElement', () => { parent.appendChild(element); parent.appendChild(nextNode); - commitEntity(refNode, 'TestEntity', true); + setEntityElementClasses(refNode, 'TestEntity', true); const result = reuseCachedElement(parent, element, refNode); 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 e038963c0c4..a92d5e1fe58 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 @@ -172,14 +172,12 @@ function cloneSegmentBase( } function cloneEntity(entity: ContentModelEntity, options: CloneModelOptions): ContentModelEntity { - const { wrapper, isReadonly, type, id } = entity; + const { wrapper, entityFormat } = entity; return Object.assign( { wrapper: handleCachedElement(wrapper, 'entity', options), - isReadonly, - type, - id, + entityFormat: { ...entityFormat }, }, cloneBlockBase(entity), cloneSegmentBase(entity) diff --git a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/collectSelections.ts b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/collectSelections.ts index de7c2cc909b..1089c46eeb4 100644 --- a/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/collectSelections.ts +++ b/packages-content-model/roosterjs-content-model-editor/lib/modelApi/selection/collectSelections.ts @@ -38,7 +38,7 @@ export function getSelectedSegmentsAndParagraphs( selections.forEach(({ segments, block }) => { if (segments && ((includingFormatHolder && !block) || block?.blockType == 'Paragraph')) { segments.forEach(segment => { - if (segment.segmentType != 'Entity' || !segment.isReadonly) { + if (segment.segmentType != 'Entity' || !segment.entityFormat.isReadonly) { result.push([segment, block?.blockType == 'Paragraph' ? block : null]); } }); 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 eb76b5bac9e..ca907e6809e 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,9 +1,8 @@ import { ChangeSource } from 'roosterjs-editor-types'; -import { commitEntity, getEntityFromElement } from 'roosterjs-editor-dom'; import { createEntity, normalizeContentModel } from 'roosterjs-content-model-dom'; import { formatWithContentModel } from '../utils/formatWithContentModel'; import { insertEntityModel } from '../../modelApi/entity/insertEntityModel'; -import type { DOMSelection } from 'roosterjs-content-model-types'; +import type { ContentModelEntity, DOMSelection } from 'roosterjs-content-model-types'; import type { Entity } from 'roosterjs-editor-types'; import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import type { @@ -31,7 +30,7 @@ export default function insertEntity( isBlock: boolean, position: 'focus' | 'begin' | 'end' | DOMSelection, options?: InsertEntityOptions -): Entity | null; +): ContentModelEntity | null; /** * Insert a block entity into editor @@ -50,7 +49,7 @@ export default function insertEntity( isBlock: true, position: InsertEntityPosition | DOMSelection, options?: InsertEntityOptions -): Entity | null; +): ContentModelEntity | null; export default function insertEntity( editor: IContentModelEditor, @@ -58,7 +57,7 @@ export default function insertEntity( isBlock: boolean, position?: InsertEntityPosition | DOMSelection, options?: InsertEntityOptions -): Entity | null { +): ContentModelEntity | null { const { contentNode, focusAfterEntity, wrapperDisplay, skipUndoSnapshot } = options || {}; const wrapper = editor.getDocument().createElement(isBlock ? BlockEntityTag : InlineEntityTag); const display = wrapperDisplay ?? (isBlock ? undefined : 'inline-block'); @@ -69,10 +68,7 @@ export default function insertEntity( wrapper.appendChild(contentNode); } - commitEntity(wrapper, type, true /*isReadonly*/); - - const entityModel = createEntity(wrapper, true /*isReadonly*/, type); - let newEntity: Entity | null = null; + const entityModel = createEntity(wrapper, true /*isReadonly*/, undefined /*format*/, type); formatWithContentModel( editor, @@ -98,11 +94,18 @@ export default function insertEntity( selectionOverride: typeof position === 'object' ? position : undefined, changeSource: ChangeSource.InsertEntity, getChangeData: () => { - newEntity = getEntityFromElement(wrapper); - return newEntity; + // TODO: Remove this entity when we have standalone editor + const entity: Entity = { + wrapper, + type, + id: '', + isReadonly: true, + }; + + return entity; }, } ); - return newEntity; + return entityModel; } 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 c3ebda36b30..8ffbeeda8ff 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 @@ -1,5 +1,6 @@ import { ChangeSource, PluginEventType } from 'roosterjs-editor-types'; import { getPendingFormat, setPendingFormat } from '../../modelApi/format/pendingFormat'; +import type { Entity } from 'roosterjs-editor-types'; import type { ContentModelContentChangedEventData } from '../../publicTypes/event/ContentModelContentChangedEvent'; import type { IContentModelEditor } from '../../publicTypes/IContentModelEditor'; import type { @@ -104,18 +105,28 @@ function handleDeletedEntities( editor: IContentModelEditor, context: FormatWithContentModelContext ) { - context.deletedEntities.forEach(({ entity, operation }) => { - if (entity.id && entity.type) { - editor.triggerPluginEvent(PluginEventType.EntityOperation, { - entity: { - id: entity.id, - isReadonly: entity.isReadonly, - type: entity.type, - wrapper: entity.wrapper, - }, - operation, - rawEvent: context.rawEvent, - }); + context.deletedEntities.forEach( + ({ + entity: { + wrapper, + entityFormat: { id, entityType, isReadonly }, + }, + operation, + }) => { + if (id && entityType) { + // TODO: Revisit this entity parameter for standalone editor, we may just directly pass ContentModelEntity object instead + const entity: Entity = { + id, + type: entityType, + isReadonly: !!isReadonly, + wrapper, + }; + editor.triggerPluginEvent(PluginEventType.EntityOperation, { + entity, + operation, + rawEvent: context.rawEvent, + }); + } } - }); + ); } diff --git a/packages-content-model/roosterjs-content-model-editor/test/editor/overrides/tablePreProcessorTest.ts b/packages-content-model/roosterjs-content-model-editor/test/editor/overrides/tablePreProcessorTest.ts index c5a463cc2b0..4dd8d30ef19 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/editor/overrides/tablePreProcessorTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/editor/overrides/tablePreProcessorTest.ts @@ -17,9 +17,12 @@ describe('tablePreProcessor', () => { blockType: 'Entity', segmentType: 'Entity', format: {}, - id: undefined, - type: undefined, - isReadonly: true, + entityFormat: { + isFakeEntity: true, + id: undefined, + entityType: undefined, + isReadonly: true, + }, wrapper: table, }, ], 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 9e7f8aa40b0..24577563afc 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 @@ -5,11 +5,11 @@ import * as extractClipboardItemsFile from 'roosterjs-editor-dom/lib/clipboard/e 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 { createModelToDomContext } from 'roosterjs-content-model-dom'; import { DeleteResult } from '../../../lib/modelApi/edit/utils/DeleteSelectionStep'; import { DOMSelection } from 'roosterjs-content-model-types'; import { IContentModelEditor } from '../../../lib/publicTypes/IContentModelEditor'; +import { setEntityElementClasses } from 'roosterjs-content-model-dom/test/domUtils/entityUtilTest'; import createRange, * as createRangeF from 'roosterjs-editor-dom/lib/selection/createRange'; import ContentModelCopyPastePlugin, { onNodeCreated, @@ -297,7 +297,7 @@ describe('ContentModelCopyPastePlugin |', () => { document.body.appendChild(wrapper); - commitEntity(wrapper, 'Entity', true, 'Entity'); + setEntityElementClasses(wrapper, 'Entity', true, 'Entity'); selectionValue = { type: 'range', range: createRange(wrapper), 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 c83e226848c..017f0e37797 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 @@ -107,9 +107,11 @@ describe('cloneModel', () => { segmentType: 'Entity', blockType: 'Entity', format: {}, - id: 'e1', + entityFormat: { + id: 'e1', + isReadonly: true, + }, wrapper: document.createElement('span'), - isReadonly: true, }, { segmentType: 'General', @@ -140,9 +142,11 @@ describe('cloneModel', () => { segmentType: 'Entity', blockType: 'Entity', format: { underline: true }, - id: 'e2', + entityFormat: { + id: 'e2', + isReadonly: true, + }, wrapper: document.createElement('span'), - isReadonly: true, }, ], }); @@ -445,9 +449,11 @@ describe('cloneModel', () => { blockType: 'Entity', format: {}, wrapper: span, - isReadonly: true, - type: undefined, - id: undefined, + entityFormat: { + isReadonly: true, + entityType: undefined, + id: undefined, + }, segmentType: 'Entity', isSelected: undefined, }, @@ -494,9 +500,11 @@ describe('cloneModel', () => { blockType: 'Entity', format: {}, wrapper: span, - isReadonly: true, - type: undefined, - id: undefined, + entityFormat: { + isReadonly: true, + entityType: undefined, + id: undefined, + }, segmentType: 'Entity', isSelected: undefined, }, 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 b0cd8ab9fda..5fc4fccebd5 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 @@ -2851,7 +2851,7 @@ describe('mergeModel', () => { majorModel.blocks.push(para1); const sourceModel: ContentModelDocument = createContentModelDocument(); - const newEntity = createEntity(document.createElement('div'), false, undefined, { + const newEntity = createEntity(document.createElement('div'), false, { fontFamily: 'Corbel', fontSize: '20px', backgroundColor: 'blue', @@ -2891,9 +2891,11 @@ describe('mergeModel', () => { textColor: 'aliceblue', italic: true, }, - id: undefined, - type: undefined, - isReadonly: false, + entityFormat: { + id: undefined, + entityType: undefined, + isReadonly: false, + }, wrapper: newEntity.wrapper, }, { @@ -2926,7 +2928,7 @@ describe('mergeModel', () => { it('Merge and replace inline entities', () => { const majorModel = createContentModelDocument(); const para1 = createParagraph(); - const sourceEntity = createEntity('wrapper1' as any, true, 'E0'); + const sourceEntity = createEntity('wrapper1' as any, true, undefined, 'E0'); const sourceBr = createBr(); sourceEntity.isSelected = true; @@ -2935,8 +2937,8 @@ describe('mergeModel', () => { const sourceModel: ContentModelDocument = createContentModelDocument(); const newPara = createParagraph(); - const newEntity1 = createEntity('wrapper2' as any, true, 'E1'); - const newEntity2 = createEntity('wrapper2' as any, true, 'E2'); + const newEntity1 = createEntity('wrapper2' as any, true, undefined, 'E1'); + const newEntity2 = createEntity('wrapper2' as any, true, undefined, 'E2'); const text = createText('test'); newPara.segments.push(newEntity1, text, newEntity2); 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 abf0459143e..4bd0a105ac4 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 @@ -469,7 +469,7 @@ describe('deleteSelection - selectionOnly', () => { it('Entity selection, no callback', () => { const model = createContentModelDocument(); const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper, true); + const entity = createEntity(wrapper); model.blocks.push(entity); entity.isSelected = true; @@ -521,7 +521,7 @@ describe('deleteSelection - selectionOnly', () => { it('Entity selection, callback returns false', () => { const model = createContentModelDocument(); const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper, true); + const entity = createEntity(wrapper); const deletedEntities: DeletedEntity[] = []; model.blocks.push(entity); @@ -576,7 +576,7 @@ describe('deleteSelection - selectionOnly', () => { it('Entity selection, callback returns true', () => { const model = createContentModelDocument(); const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper, true); + const entity = createEntity(wrapper); model.blocks.push(entity); entity.isSelected = true; @@ -1436,7 +1436,7 @@ describe('deleteSelection - forward', () => { const marker = createSelectionMarker({ fontSize: '10px' }); const br = createBr(); const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper, true); + const entity = createEntity(wrapper); para.segments.push(marker, br); model.blocks.push(para, entity); @@ -1478,7 +1478,7 @@ describe('deleteSelection - forward', () => { const marker = createSelectionMarker({ fontSize: '10px' }); const br = createBr(); const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper, true); + const entity = createEntity(wrapper); para.segments.push(marker, br); model.blocks.push(para, entity); @@ -1525,7 +1525,7 @@ describe('deleteSelection - forward', () => { const marker = createSelectionMarker({ fontSize: '10px' }); const br = createBr(); const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper, true); + const entity = createEntity(wrapper); para.segments.push(marker, br); model.blocks.push(para, entity); @@ -3192,7 +3192,7 @@ describe('deleteSelection - backward', () => { const marker = createSelectionMarker({ fontSize: '10px' }); const br = createBr(); const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper, true); + const entity = createEntity(wrapper); para.segments.push(marker, br); model.blocks.push(entity, para); @@ -3234,7 +3234,7 @@ describe('deleteSelection - backward', () => { const marker = createSelectionMarker({ fontSize: '10px' }); const br = createBr(); const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper, true); + const entity = createEntity(wrapper); para.segments.push(marker, br); model.blocks.push(entity, para); @@ -3281,7 +3281,7 @@ describe('deleteSelection - backward', () => { const marker = createSelectionMarker({ fontSize: '10px' }); const br = createBr(); const wrapper = 'WRAPPER' as any; - const entity = createEntity(wrapper, true); + const entity = createEntity(wrapper); para.segments.push(marker, br); model.blocks.push(entity, para); diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/entity/insertEntityModelTest.ts b/packages-content-model/roosterjs-content-model-editor/test/modelApi/entity/insertEntityModelTest.ts index a6ea985581d..1c59a012554 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/entity/insertEntityModelTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/modelApi/entity/insertEntityModelTest.ts @@ -416,7 +416,7 @@ describe('insertEntityModel, block element, not focus after entity', () => { }); it('Before another entity', () => { - const entity2 = createEntity({} as any, true); + const entity2 = createEntity({} as any); const br = createBr(); runTest( @@ -978,7 +978,7 @@ describe('insertEntityModel, block element, focus after entity', () => { }); it('Before another entity', () => { - const entity2 = createEntity({} as any, true); + const entity2 = createEntity({} as any); runTest( () => { @@ -1516,7 +1516,7 @@ describe('insertEntityModel, inline element, not focus after entity', () => { }); it('Before another entity', () => { - const entity2 = createEntity({} as any, true); + const entity2 = createEntity({} as any); runTest( () => { @@ -2033,7 +2033,7 @@ describe('insertEntityModel, inline element, focus after entity', () => { }); it('Before another entity', () => { - const entity2 = createEntity({} as any, true); + const entity2 = createEntity({} as any); runTest( () => { diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/collectSelectionsTest.ts b/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/collectSelectionsTest.ts index cb7806b1f0d..a9080dc1aa4 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/collectSelectionsTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/collectSelectionsTest.ts @@ -186,7 +186,7 @@ describe('getSelectedSegmentsAndParagraphs', () => { }); it('Include editable entity, but filter out readonly entity', () => { - const e1 = createEntity(null!, true); + const e1 = createEntity(null!); const e2 = createEntity(null!, false); const p1 = createParagraph(); diff --git a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/iterateSelectionsTest.ts b/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/iterateSelectionsTest.ts index c5d95c461d8..fba405cde0f 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/iterateSelectionsTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/modelApi/selection/iterateSelectionsTest.ts @@ -1329,7 +1329,7 @@ describe('iterateSelections', () => { it('With selected entity', () => { const doc = createContentModelDocument(); const para = createParagraph(); - const entity = createEntity(null!, true); + const entity = createEntity(null!); entity.isSelected = 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 66ec3b8aa7c..f361b3ec7e8 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 @@ -1,6 +1,4 @@ -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'; @@ -13,14 +11,11 @@ describe('insertEntity', () => { let context: FormatWithContentModelContext; let wrapper: HTMLElement; const model = 'MockedModel' as any; - const newEntity = 'MockedEntity' as any; let formatWithContentModelSpy: jasmine.Spy; - let getEntityFromElementSpy: jasmine.Spy; let triggerContentChangedEventSpy: jasmine.Spy; let getDocumentSpy: jasmine.Spy; let createElementSpy: jasmine.Spy; - let commitEntitySpy: jasmine.Spy; let setPropertySpy: jasmine.Spy; let appendChildSpy: jasmine.Spy; let insertEntityModelSpy: jasmine.Spy; @@ -57,8 +52,6 @@ describe('insertEntity', () => { formatter(model, context); options?.getChangeData?.(); }); - getEntityFromElementSpy = spyOn(getEntityFromElement, 'default').and.returnValue(newEntity); - commitEntitySpy = spyOn(commitEntity, 'default'); triggerContentChangedEventSpy = jasmine.createSpy('triggerContentChangedEventSpy'); createElementSpy = jasmine.createSpy('createElementSpy').and.returnValue(wrapper); getDocumentSpy = jasmine.createSpy('getDocumentSpy').and.returnValue({ @@ -80,7 +73,6 @@ describe('insertEntity', () => { expect(createElementSpy).toHaveBeenCalledWith('span'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'inline-block'); expect(appendChildSpy).not.toHaveBeenCalled(); - expect(commitEntitySpy).toHaveBeenCalledWith(wrapper, type, true); expect(formatWithContentModelSpy.calls.argsFor(0)[0]).toBe(editor); expect(formatWithContentModelSpy.calls.argsFor(0)[1]).toBe(apiName); expect(formatWithContentModelSpy.calls.argsFor(0)[3].changeSource).toEqual( @@ -92,9 +84,11 @@ describe('insertEntity', () => { segmentType: 'Entity', blockType: 'Entity', format: {}, - id: undefined, - type: type, - isReadonly: true, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, wrapper: wrapper, }, 'begin', @@ -102,12 +96,21 @@ describe('insertEntity', () => { undefined, context ); - expect(getEntityFromElementSpy).toHaveBeenCalledWith(wrapper); expect(triggerContentChangedEventSpy).not.toHaveBeenCalled(); expect(transformToDarkColorSpy).not.toHaveBeenCalled(); expect(normalizeContentModelSpy).toHaveBeenCalled(); - expect(entity).toBe(newEntity); + expect(entity).toEqual({ + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, + wrapper: wrapper, + }); }); it('block inline entity to root', () => { @@ -116,7 +119,6 @@ describe('insertEntity', () => { expect(createElementSpy).toHaveBeenCalledWith('div'); expect(setPropertySpy).toHaveBeenCalledWith('display', null); expect(appendChildSpy).not.toHaveBeenCalled(); - expect(commitEntitySpy).toHaveBeenCalledWith(wrapper, type, true); expect(formatWithContentModelSpy.calls.argsFor(0)[0]).toBe(editor); expect(formatWithContentModelSpy.calls.argsFor(0)[1]).toBe(apiName); expect(formatWithContentModelSpy.calls.argsFor(0)[3].changeSource).toEqual( @@ -128,9 +130,11 @@ describe('insertEntity', () => { segmentType: 'Entity', blockType: 'Entity', format: {}, - id: undefined, - type: type, - isReadonly: true, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, wrapper: wrapper, }, 'root', @@ -138,12 +142,21 @@ describe('insertEntity', () => { undefined, context ); - expect(getEntityFromElementSpy).toHaveBeenCalledWith(wrapper); expect(triggerContentChangedEventSpy).not.toHaveBeenCalled(); expect(transformToDarkColorSpy).not.toHaveBeenCalled(); expect(normalizeContentModelSpy).toHaveBeenCalled(); - expect(entity).toBe(newEntity); + expect(entity).toEqual({ + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, + wrapper: wrapper, + }); }); it('block inline entity with more options', () => { @@ -159,7 +172,6 @@ describe('insertEntity', () => { expect(createElementSpy).toHaveBeenCalledWith('div'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'none'); expect(appendChildSpy).toHaveBeenCalledWith(contentNode); - expect(commitEntitySpy).toHaveBeenCalledWith(wrapper, type, true); expect(formatWithContentModelSpy.calls.argsFor(0)[0]).toBe(editor); expect(formatWithContentModelSpy.calls.argsFor(0)[1]).toBe(apiName); expect(formatWithContentModelSpy.calls.argsFor(0)[3].changeSource).toEqual( @@ -172,9 +184,11 @@ describe('insertEntity', () => { segmentType: 'Entity', blockType: 'Entity', format: {}, - id: undefined, - type: type, - isReadonly: true, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, wrapper: wrapper, }, 'focus', @@ -182,12 +196,21 @@ describe('insertEntity', () => { true, context ); - expect(getEntityFromElementSpy).toHaveBeenCalledWith(wrapper); expect(triggerContentChangedEventSpy).not.toHaveBeenCalled(); expect(transformToDarkColorSpy).not.toHaveBeenCalled(); expect(normalizeContentModelSpy).toHaveBeenCalled(); - expect(entity).toBe(newEntity); + expect(entity).toEqual({ + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, + wrapper: wrapper, + }); }); it('In dark mode', () => { @@ -198,7 +221,6 @@ describe('insertEntity', () => { expect(createElementSpy).toHaveBeenCalledWith('span'); expect(setPropertySpy).toHaveBeenCalledWith('display', 'inline-block'); expect(appendChildSpy).not.toHaveBeenCalled(); - expect(commitEntitySpy).toHaveBeenCalledWith(wrapper, type, true); expect(formatWithContentModelSpy.calls.argsFor(0)[0]).toBe(editor); expect(formatWithContentModelSpy.calls.argsFor(0)[1]).toBe(apiName); expect(formatWithContentModelSpy.calls.argsFor(0)[3].changeSource).toEqual( @@ -210,9 +232,11 @@ describe('insertEntity', () => { segmentType: 'Entity', blockType: 'Entity', format: {}, - id: undefined, - type: type, - isReadonly: true, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, wrapper: wrapper, }, 'begin', @@ -220,7 +244,6 @@ describe('insertEntity', () => { undefined, context ); - expect(getEntityFromElementSpy).toHaveBeenCalledWith(wrapper); expect(triggerContentChangedEventSpy).not.toHaveBeenCalled(); expect(normalizeContentModelSpy).toHaveBeenCalled(); @@ -229,13 +252,25 @@ describe('insertEntity', () => { segmentType: 'Entity', blockType: 'Entity', format: {}, - id: undefined, - type: 'Entity', - isReadonly: true, + entityFormat: { + id: undefined, + entityType: 'Entity', + isReadonly: true, + }, wrapper, }, ]); - expect(entity).toBe(newEntity); + expect(entity).toEqual({ + segmentType: 'Entity', + blockType: 'Entity', + format: {}, + entityFormat: { + id: undefined, + entityType: type, + isReadonly: true, + }, + wrapper: wrapper, + }); }); }); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/getSelectedSegmentsTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/getSelectedSegmentsTest.ts index b29fbda0d93..e091fbf6812 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/getSelectedSegmentsTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/getSelectedSegmentsTest.ts @@ -157,7 +157,7 @@ describe('getSelectedSegments', () => { }); it('Include editable entity, but filter out readonly entity', () => { - const e1 = createEntity(null!, true); + const e1 = createEntity(null!); const e2 = createEntity(null!, false); const p1 = createParagraph(); diff --git a/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/hasSelectionInBlockTest.ts b/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/hasSelectionInBlockTest.ts index 80e309e7939..788e9e3efea 100644 --- a/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/hasSelectionInBlockTest.ts +++ b/packages-content-model/roosterjs-content-model-editor/test/publicApi/selection/hasSelectionInBlockTest.ts @@ -250,10 +250,12 @@ describe('hasSelectionInBlock', () => { segmentType: 'Entity', format: {}, isSelected: true, - type: 'entity', - id: 'entity', + entityFormat: { + entityType: 'entity', + id: 'entity', + isReadonly: false, + }, wrapper: null!, - isReadonly: false, }; const result = hasSelectionInBlock(block); 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 cb062bbb93e..b4c171ba15a 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 @@ -203,8 +203,14 @@ describe('formatWithContentModel', () => { }); it('Has entity got deleted', () => { - const entity1 = { id: 'E1', type: 'E', wrapper: {}, isReadonly: true } as any; - const entity2 = { id: 'E2', type: 'E', wrapper: {}, isReadonly: true } as any; + const entity1 = { + entityFormat: { id: 'E1', entityType: 'E', isReadonly: true }, + wrapper: {}, + } as any; + const entity2 = { + entityFormat: { id: 'E2', entityType: 'E', isReadonly: true }, + wrapper: {}, + } as any; const rawEvent = 'RawEvent' as any; formatWithContentModel( @@ -230,12 +236,12 @@ describe('formatWithContentModel', () => { expect(triggerPluginEvent).toHaveBeenCalledTimes(3); expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - entity: entity1, + entity: { id: 'E1', type: 'E', isReadonly: true, wrapper: entity1.wrapper }, operation: EntityOperation.RemoveFromStart, rawEvent: rawEvent, }); expect(triggerPluginEvent).toHaveBeenCalledWith(PluginEventType.EntityOperation, { - entity: entity2, + entity: { id: 'E2', type: 'E', isReadonly: true, wrapper: entity2.wrapper }, operation: EntityOperation.RemoveFromEnd, rawEvent: rawEvent, }); @@ -244,8 +250,14 @@ 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 entity1 = { + entityFormat: { id: 'E1', entityType: 'E', isReadonly: true }, + wrapper: wrapper1, + } as any; + const entity2 = { + entityFormat: { id: 'E2', entityType: 'E', isReadonly: true }, + wrapper: wrapper2, + } as any; const rawEvent = 'RawEvent' as any; const transformToDarkColorSpy = jasmine.createSpy('transformToDarkColor'); const mockedData = 'DATA'; diff --git a/packages-content-model/roosterjs-content-model-types/lib/entity/ContentModelEntity.ts b/packages-content-model/roosterjs-content-model-types/lib/entity/ContentModelEntity.ts index 357125b9dc5..897a572920e 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/entity/ContentModelEntity.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/entity/ContentModelEntity.ts @@ -1,5 +1,6 @@ import type { ContentModelBlockBase } from '../block/ContentModelBlockBase'; import type { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; +import type { ContentModelEntityFormat } from '../format/ContentModelEntityFormat'; import type { ContentModelSegmentBase } from '../segment/ContentModelSegmentBase'; import type { ContentModelSegmentFormat } from '../format/ContentModelSegmentFormat'; @@ -15,17 +16,7 @@ export interface ContentModelEntity wrapper: HTMLElement; /** - * Whether this is a readonly entity + * Format of this entity */ - isReadonly: boolean; - - /** - * Type of this entity. Specified when insert an entity, can be an valid CSS class-like string. - */ - type?: string; - - /** - * Id of this entity, generated by editor code and will be unique within an editor - */ - id?: string; + entityFormat: ContentModelEntityFormat; } diff --git a/packages-content-model/roosterjs-content-model-types/lib/format/ContentModelEntityFormat.ts b/packages-content-model/roosterjs-content-model-types/lib/format/ContentModelEntityFormat.ts new file mode 100644 index 00000000000..0570d49228d --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/format/ContentModelEntityFormat.ts @@ -0,0 +1,7 @@ +import type { IdFormat } from './formatParts/IdFormat'; +import type { EntityInfoFormat } from './formatParts/EntityInfoFormat'; + +/** + * The format object for an entity in Content Model + */ +export type ContentModelEntityFormat = EntityInfoFormat & IdFormat; diff --git a/packages-content-model/roosterjs-content-model-types/lib/format/ContentModelFormatMap.ts b/packages-content-model/roosterjs-content-model-types/lib/format/ContentModelFormatMap.ts index 0c1ee3fd480..7c2efc571d6 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/format/ContentModelFormatMap.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/format/ContentModelFormatMap.ts @@ -1,5 +1,6 @@ import type { ContentModelBlockFormat } from './ContentModelBlockFormat'; import type { ContentModelDividerFormat } from './ContentModelDividerFormat'; +import type { ContentModelEntityFormat } from './ContentModelEntityFormat'; import type { ContentModelFormatContainerFormat } from './ContentModelFormatContainerFormat'; import type { ContentModelHyperLinkFormat } from './ContentModelHyperLinkFormat'; import type { ContentModelImageFormat } from './ContentModelImageFormat'; @@ -124,4 +125,9 @@ export interface ContentModelFormatMap { * Format type for format container */ container: ContentModelFormatContainerFormat; + + /** + * Format type for entity + */ + entity: ContentModelEntityFormat; } diff --git a/packages-content-model/roosterjs-content-model-types/lib/format/FormatHandlerTypeMap.ts b/packages-content-model/roosterjs-content-model-types/lib/format/FormatHandlerTypeMap.ts index 89f2622f602..d3d32c43763 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/format/FormatHandlerTypeMap.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/format/FormatHandlerTypeMap.ts @@ -6,6 +6,7 @@ import type { BoxShadowFormat } from './formatParts/BoxShadowFormat'; import type { DatasetFormat } from './metadata/DatasetFormat'; import type { DirectionFormat } from './formatParts/DirectionFormat'; import type { DisplayFormat } from './formatParts/DisplayFormat'; +import type { EntityInfoFormat } from './formatParts/EntityInfoFormat'; import type { FloatFormat } from './formatParts/FloatFormat'; import type { FontFamilyFormat } from './formatParts/FontFamilyFormat'; import type { FontSizeFormat } from './formatParts/FontSizeFormat'; @@ -75,6 +76,11 @@ export interface FormatHandlerTypeMap { */ display: DisplayFormat; + /** + * Format for EntityInfoFormat and IdFormat + */ + entity: EntityInfoFormat & IdFormat; + /** * Format for FloatFormat */ diff --git a/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/EntityInfoFormat.ts b/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/EntityInfoFormat.ts new file mode 100644 index 00000000000..1476ef1b32f --- /dev/null +++ b/packages-content-model/roosterjs-content-model-types/lib/format/formatParts/EntityInfoFormat.ts @@ -0,0 +1,19 @@ +/** + * Format of entity type + */ +export type EntityInfoFormat = { + /** + * For a readonly DOM element, we also treat it as entity, with isFakeEntity set to true + */ + isFakeEntity?: boolean; + + /** + * Whether the entity is readonly + */ + isReadonly?: boolean; + + /** + * Type of this entity + */ + entityType?: string; +}; diff --git a/packages-content-model/roosterjs-content-model-types/lib/index.ts b/packages-content-model/roosterjs-content-model-types/lib/index.ts index 1cf171c4b1a..5a6114299e6 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -13,6 +13,7 @@ export { ContentModelDividerFormat } from './format/ContentModelDividerFormat'; export { ContentModelFormatBase } from './format/ContentModelFormatBase'; export { ContentModelFormatMap } from './format/ContentModelFormatMap'; export { ContentModelImageFormat } from './format/ContentModelImageFormat'; +export { ContentModelEntityFormat } from './format/ContentModelEntityFormat'; export { FormatHandlerTypeMap, FormatKey } from './format/FormatHandlerTypeMap'; export { BackgroundColorFormat } from './format/formatParts/BackgroundColorFormat'; @@ -46,6 +47,7 @@ export { BoxShadowFormat } from './format/formatParts/BoxShadowFormat'; export { ListThreadFormat } from './format/formatParts/ListThreadFormat'; export { ListStylePositionFormat } from './format/formatParts/ListStylePositionFormat'; export { FloatFormat } from './format/formatParts/FloatFormat'; +export { EntityInfoFormat } from './format/formatParts/EntityInfoFormat'; export { DatasetFormat } from './format/metadata/DatasetFormat'; export { TableMetadataFormat } from './format/metadata/TableMetadataFormat';