diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/context/defaultContentModelHandlers.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/context/defaultContentModelHandlers.ts index d05cf307ebd..b43cf1c7af3 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/context/defaultContentModelHandlers.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/context/defaultContentModelHandlers.ts @@ -3,9 +3,9 @@ import { handleBlock } from '../handlers/handleBlock'; import { handleBlockGroupChildren } from '../handlers/handleBlockGroupChildren'; import { handleBr } from '../handlers/handleBr'; import { handleDivider } from '../handlers/handleDivider'; -import { handleEntity } from '../handlers/handleEntity'; +import { handleEntityBlock, handleEntitySegment } from '../handlers/handleEntity'; import { handleFormatContainer } from '../handlers/handleFormatContainer'; -import { handleGeneralModel } from '../handlers/handleGeneralModel'; +import { handleGeneralBlock, handleGeneralSegment } from '../handlers/handleGeneralModel'; import { handleImage } from '../handlers/handleImage'; import { handleList } from '../handlers/handleList'; import { handleListItem } from '../handlers/handleListItem'; @@ -22,8 +22,10 @@ export const defaultContentModelHandlers: ContentModelHandlerMap = { block: handleBlock, blockGroupChildren: handleBlockGroupChildren, br: handleBr, - entity: handleEntity, - general: handleGeneralModel, + entityBlock: handleEntityBlock, + entitySegment: handleEntitySegment, + generalBlock: handleGeneralBlock, + generalSegment: handleGeneralSegment, divider: handleDivider, image: handleImage, list: handleList, diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlock.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlock.ts index 10a3760a00e..b45e9bb05cb 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlock.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBlock.ts @@ -24,7 +24,7 @@ export const handleBlock: ContentModelBlockHandler = ( refNode = handlers.paragraph(doc, parent, block, context, refNode); break; case 'Entity': - refNode = handlers.entity(doc, parent, block, context, refNode); + refNode = handlers.entityBlock(doc, parent, block, context, refNode); break; case 'Divider': refNode = handlers.divider(doc, parent, block, context, refNode); @@ -32,7 +32,7 @@ export const handleBlock: ContentModelBlockHandler = ( case 'BlockGroup': switch (block.blockGroupType) { case 'General': - refNode = handlers.general(doc, parent, block, context, refNode); + refNode = handlers.generalBlock(doc, parent, block, context, refNode); break; case 'FormatContainer': diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBr.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBr.ts index 0eb370d1360..e3decb24b96 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBr.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleBr.ts @@ -1,23 +1,20 @@ +import { ContentModelBr, ContentModelSegmentHandler } from 'roosterjs-content-model-types'; import { handleSegmentCommon } from '../utils/handleSegmentCommon'; -import { - ContentModelBr, - ContentModelHandler, - ModelToDomContext, -} from 'roosterjs-content-model-types'; /** * @internal */ -export const handleBr: ContentModelHandler = ( - doc: Document, - parent: Node, - segment: ContentModelBr, - context: ModelToDomContext +export const handleBr: ContentModelSegmentHandler = ( + doc, + parent, + segment, + context, + segmentNodes ) => { const br = doc.createElement('br'); const element = doc.createElement('span'); element.appendChild(br); parent.appendChild(element); - handleSegmentCommon(doc, br, element, segment, context); + handleSegmentCommon(doc, br, element, segment, context, segmentNodes); }; 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 7d3c7c8e810..0b48eaf731a 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,31 +1,68 @@ +import { addDelimiters, commitEntity, getObjectKeys, wrap } from 'roosterjs-editor-dom'; import { applyFormat } from '../utils/applyFormat'; import { Entity } from 'roosterjs-editor-types'; import { reuseCachedElement } from '../utils/reuseCachedElement'; import { ContentModelBlockHandler, ContentModelEntity, + ContentModelSegmentHandler, ModelToDomContext, } from 'roosterjs-content-model-types'; -import { - addDelimiters, - commitEntity, - getObjectKeys, - isBlockElement, - wrap, -} from 'roosterjs-editor-dom'; /** * @internal */ -export const handleEntity: ContentModelBlockHandler = ( - doc: Document, - parent: Node, - entityModel: ContentModelEntity, - context: ModelToDomContext, - refNode: Node | null +export const handleEntityBlock: ContentModelBlockHandler = ( + _, + parent, + entityModel, + context, + refNode +) => { + const wrapper = preprocessEntity(entityModel, context); + + refNode = reuseCachedElement(parent, wrapper, refNode); + context.onNodeCreated?.(entityModel, wrapper); + + return refNode; +}; + +/** + * @internal + */ +export const handleEntitySegment: ContentModelSegmentHandler = ( + _, + parent, + entityModel, + context, + newSegments ) => { - const { id, type, isReadonly, format } = entityModel; - let wrapper = entityModel.wrapper; + const wrapper = preprocessEntity(entityModel, context); + const { format, isReadonly } = entityModel; + + parent.appendChild(wrapper); + newSegments?.push(wrapper); + + if (getObjectKeys(format).length > 0) { + const span = wrap(wrapper, 'span'); + + applyFormat(span, context.formatAppliers.segment, format, context); + } + + if (context.addDelimiterForEntity && isReadonly) { + const [after, before] = addDelimiters(wrapper); + + newSegments?.push(after, before); + context.regularSelection.current.segment = after; + } else { + context.regularSelection.current.segment = wrapper; + } + + context.onNodeCreated?.(entityModel, wrapper); +}; + +function preprocessEntity(entityModel: ContentModelEntity, context: ModelToDomContext) { + let { id, type, isReadonly, wrapper } = entityModel; if (!context.allowCacheElement) { wrapper = wrapper.cloneNode(true /*deep*/) as HTMLElement; @@ -42,30 +79,10 @@ export const handleEntity: ContentModelBlockHandler = ( isReadonly: !!isReadonly, } : null; - const isInlineEntity = !isBlockElement(wrapper); if (entity) { // Commit the entity attributes in case there is any change commitEntity(wrapper, entity.type, entity.isReadonly, entity.id); } - - refNode = reuseCachedElement(parent, wrapper, refNode); - - if (isInlineEntity && getObjectKeys(format).length > 0) { - const span = wrap(wrapper, 'span'); - - applyFormat(span, context.formatAppliers.segment, format, context); - } - - if (context.addDelimiterForEntity && isInlineEntity && isReadonly) { - const [after] = addDelimiters(wrapper); - - context.regularSelection.current.segment = after; - } else if (isInlineEntity) { - context.regularSelection.current.segment = wrapper; - } - - context.onNodeCreated?.(entityModel, wrapper); - - return refNode; -}; + return wrapper; +} diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleGeneralModel.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleGeneralModel.ts index 7fb1c37aa97..30f04d2200d 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleGeneralModel.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleGeneralModel.ts @@ -1,5 +1,4 @@ import { handleSegmentCommon } from '../utils/handleSegmentCommon'; -import { isGeneralSegment } from '../../modelApi/common/isGeneralSegment'; import { isNodeOfType } from '../../domUtils/isNodeOfType'; import { NodeType } from 'roosterjs-editor-types'; import { reuseCachedElement } from '../utils/reuseCachedElement'; @@ -7,18 +6,19 @@ import { wrap } from 'roosterjs-editor-dom'; import { ContentModelBlockHandler, ContentModelGeneralBlock, - ModelToDomContext, + ContentModelGeneralSegment, + ContentModelSegmentHandler, } from 'roosterjs-content-model-types'; /** * @internal */ -export const handleGeneralModel: ContentModelBlockHandler = ( - doc: Document, - parent: Node, - group: ContentModelGeneralBlock, - context: ModelToDomContext, - refNode: Node | null +export const handleGeneralBlock: ContentModelBlockHandler = ( + doc, + parent, + group, + context, + refNode ) => { let node: Node = group.element; @@ -31,15 +31,32 @@ export const handleGeneralModel: ContentModelBlockHandler = ( + doc, + parent, + group, + context, + segmentNodes +) => { + const node = group.element.cloneNode() as HTMLElement; + group.element = node; + parent.appendChild(node); + + if (isNodeOfType(node, NodeType.Element)) { const element = wrap(node, 'span'); - handleSegmentCommon(doc, node, element, group, context); - } else { + handleSegmentCommon(doc, node, element, group, context, segmentNodes); context.onNodeCreated?.(group, node); } context.modelHandlers.blockGroupChildren(doc, node, group, context); - - return refNode; }; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleImage.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleImage.ts index 342b81d3013..70c7f593c83 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleImage.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleImage.ts @@ -1,20 +1,17 @@ import { applyFormat } from '../utils/applyFormat'; +import { ContentModelImage, ContentModelSegmentHandler } from 'roosterjs-content-model-types'; import { handleSegmentCommon } from '../utils/handleSegmentCommon'; import { parseValueWithUnit } from '../../formatHandlers/utils/parseValueWithUnit'; -import { - ContentModelHandler, - ContentModelImage, - ModelToDomContext, -} from 'roosterjs-content-model-types'; /** * @internal */ -export const handleImage: ContentModelHandler = ( - doc: Document, - parent: Node, - imageModel: ContentModelImage, - context: ModelToDomContext +export const handleImage: ContentModelSegmentHandler = ( + doc, + parent, + imageModel, + context, + segmentNodes ) => { const img = doc.createElement('img'); const element = document.createElement('span'); @@ -53,5 +50,5 @@ export const handleImage: ContentModelHandler = ( }; } - handleSegmentCommon(doc, img, element, imageModel, context); + handleSegmentCommon(doc, img, element, imageModel, context, segmentNodes); }; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts index 2b3fbc13ed2..eca8d501e7c 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleParagraph.ts @@ -66,12 +66,14 @@ export const handleParagraph: ContentModelBlockHandler = segmentType: 'Text', text: '', }, - context + context, + [] ); } paragraph.segments.forEach(segment => { - context.modelHandlers.segment(doc, parent, segment, context); + const newSegments: Node[] = []; + context.modelHandlers.segment(doc, parent, segment, context, newSegments); }); } }; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleSegment.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleSegment.ts index 83f96817f09..4376dd6f750 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleSegment.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleSegment.ts @@ -1,17 +1,14 @@ -import { - ContentModelHandler, - ContentModelSegment, - ModelToDomContext, -} from 'roosterjs-content-model-types'; +import { ContentModelSegment, ContentModelSegmentHandler } from 'roosterjs-content-model-types'; /** * @internal */ -export const handleSegment: ContentModelHandler = ( - doc: Document, - parent: Node, - segment: ContentModelSegment, - context: ModelToDomContext +export const handleSegment: ContentModelSegmentHandler = ( + doc, + parent, + segment, + context, + segmentNodes ) => { const regularSelection = context.regularSelection; @@ -24,23 +21,23 @@ export const handleSegment: ContentModelHandler = ( switch (segment.segmentType) { case 'Text': - context.modelHandlers.text(doc, parent, segment, context); + context.modelHandlers.text(doc, parent, segment, context, segmentNodes); break; case 'Br': - context.modelHandlers.br(doc, parent, segment, context); + context.modelHandlers.br(doc, parent, segment, context, segmentNodes); break; case 'Image': - context.modelHandlers.image(doc, parent, segment, context); + context.modelHandlers.image(doc, parent, segment, context, segmentNodes); break; case 'General': - context.modelHandlers.general(doc, parent, segment, context, null /*refNode*/); + context.modelHandlers.generalSegment(doc, parent, segment, context, segmentNodes); break; case 'Entity': - context.modelHandlers.entity(doc, parent, segment, context, null /*refNode*/); + context.modelHandlers.entitySegment(doc, parent, segment, context, segmentNodes); break; } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleSegmentDecorator.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleSegmentDecorator.ts index 55bd39fd691..933b8b6eac1 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleSegmentDecorator.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleSegmentDecorator.ts @@ -1,16 +1,17 @@ import { applyFormat } from '../utils/applyFormat'; -import { ContentModelHandler, ContentModelSegment } from 'roosterjs-content-model-types'; +import { ContentModelSegment, ContentModelSegmentHandler } from 'roosterjs-content-model-types'; import { moveChildNodes } from 'roosterjs-editor-dom'; import { stackFormat } from '../utils/stackFormat'; /** * @internal */ -export const handleSegmentDecorator: ContentModelHandler = ( - doc, +export const handleSegmentDecorator: ContentModelSegmentHandler = ( + _, parent, segment, - context + context, + segmentNodes ) => { const { code, link } = segment; @@ -24,6 +25,7 @@ export const handleSegmentDecorator: ContentModelHandler = applyFormat(a, context.formatAppliers.link, link.format, context); applyFormat(a, context.formatAppliers.dataset, link.dataset, context); + segmentNodes?.push(a); context.onNodeCreated?.(link, a); }); } @@ -37,6 +39,7 @@ export const handleSegmentDecorator: ContentModelHandler = applyFormat(codeNode, context.formatAppliers.code, code.format, context); + segmentNodes?.push(codeNode); context.onNodeCreated?.(code, codeNode); }); } diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleText.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleText.ts index c54458cccfd..5974db1cb0a 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleText.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/handlers/handleText.ts @@ -1,18 +1,15 @@ +import { ContentModelSegmentHandler, ContentModelText } from 'roosterjs-content-model-types'; import { handleSegmentCommon } from '../utils/handleSegmentCommon'; -import { - ContentModelHandler, - ContentModelText, - ModelToDomContext, -} from 'roosterjs-content-model-types'; /** * @internal */ -export const handleText: ContentModelHandler = ( - doc: Document, - parent: Node, - segment: ContentModelText, - context: ModelToDomContext +export const handleText: ContentModelSegmentHandler = ( + doc, + parent, + segment, + context, + segmentNodes ) => { const txt = doc.createTextNode(segment.text); const element = doc.createElement('span'); @@ -20,5 +17,5 @@ export const handleText: ContentModelHandler = ( parent.appendChild(element); element.appendChild(txt); - handleSegmentCommon(doc, txt, element, segment, context); + handleSegmentCommon(doc, txt, element, segment, context, segmentNodes); }; diff --git a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/utils/handleSegmentCommon.ts b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/utils/handleSegmentCommon.ts index 921804d0fda..46d5ad698bd 100644 --- a/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/utils/handleSegmentCommon.ts +++ b/packages-content-model/roosterjs-content-model-dom/lib/modelToDom/utils/handleSegmentCommon.ts @@ -9,7 +9,8 @@ export function handleSegmentCommon( segmentNode: Node, containerNode: HTMLElement, segment: ContentModelSegment, - context: ModelToDomContext + context: ModelToDomContext, + segmentNodes: Node[] ) { if (!segmentNode.firstChild) { context.regularSelection.current.segment = segmentNode; @@ -17,7 +18,8 @@ export function handleSegmentCommon( applyFormat(containerNode, context.formatAppliers.styleBasedSegment, segment.format, context); - context.modelHandlers.segmentDecorator(doc, containerNode, segment, context); + segmentNodes?.push(segmentNode); + context.modelHandlers.segmentDecorator(doc, containerNode, segment, context, segmentNodes); applyFormat(containerNode, context.formatAppliers.elementBasedSegment, segment.format, context); 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 eea6ccd1fc3..6f04077a554 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 @@ -15,8 +15,8 @@ import { ContentModelListItem, ContentModelParagraph, ContentModelBlockHandler, - ContentModelHandler, ModelToDomContext, + ContentModelHandler, } from 'roosterjs-content-model-types'; describe('handleBlock', () => { @@ -33,7 +33,7 @@ describe('handleBlock', () => { context = createModelToDomContext(undefined, { modelHandlerOverride: { - entity: handleEntity, + entityBlock: handleEntity, paragraph: handleParagraph, divider: handleDivider, }, @@ -128,12 +128,12 @@ describe('handleBlock', () => { spyOn(applyFormat, 'applyFormat'); handleBlock(document, parent, block, context, null); - expect(parent.innerHTML).toBe(''); + expect(parent.innerHTML).toBe(''); expect(parent.firstChild).not.toBe(element); expect(context.regularSelection.current.segment).toBe(parent.firstChild!.firstChild); - expect(applyFormat.applyFormat).toHaveBeenCalled(); + expect(applyFormat.applyFormat).not.toHaveBeenCalled(); - runTestWithRefNode(block, '
'); + runTestWithRefNode(block, '
'); }); it('Entity block', () => { @@ -193,7 +193,7 @@ describe('handleBlockGroup', () => { blockGroupChildren: handleBlockGroupChildren, listItem: handleListItem, formatContainer: handleQuote, - general: handleGeneralModel, + generalBlock: handleGeneralModel, }, }); parent = document.createElement('div'); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBrTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBrTest.ts index 7119dfe6c03..9f3dfbc414f 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBrTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleBrTest.ts @@ -19,7 +19,7 @@ describe('handleSegment', () => { format: {}, }; - handleBr(document, parent, br, context); + handleBr(document, parent, br, context, []); expect(parent.innerHTML).toBe('
'); }); @@ -30,7 +30,7 @@ describe('handleSegment', () => { format: { textColor: 'red' }, }; - handleBr(document, parent, br, context); + handleBr(document, parent, br, context, []); expect(parent.innerHTML).toBe('
'); }); @@ -43,10 +43,45 @@ describe('handleSegment', () => { const onNodeCreated = jasmine.createSpy('onNodeCreated'); context.onNodeCreated = onNodeCreated; - handleBr(document, parent, br, context); + handleBr(document, parent, br, context, []); expect(parent.innerHTML).toBe('
'); expect(onNodeCreated.calls.argsFor(0)[0]).toBe(br); expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('br')); }); + + it('With segmentNodes', () => { + const br: ContentModelBr = { + segmentType: 'Br', + format: {}, + }; + const newSegments: Node[] = []; + + handleBr(document, parent, br, context, newSegments); + + expect(parent.innerHTML).toBe('
'); + expect(newSegments.length).toBe(1); + expect((newSegments[0] as HTMLElement).outerHTML).toBe('
'); + }); + + it('With segmentNodes and decorator', () => { + const br: ContentModelBr = { + segmentType: 'Br', + format: {}, + link: { + dataset: {}, + format: { + href: '/test', + }, + }, + }; + const newSegments: Node[] = []; + + handleBr(document, parent, br, context, newSegments); + + expect(parent.innerHTML).toBe('
'); + expect(newSegments.length).toBe(2); + expect((newSegments[0] as HTMLElement).outerHTML).toBe('
'); + expect((newSegments[1] as HTMLElement).outerHTML).toBe('
'); + }); }); 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 3347d504c2f..b6352985ff7 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 @@ -1,7 +1,10 @@ import * as addDelimiters from 'roosterjs-editor-dom/lib/delimiter/addDelimiters'; import { ContentModelEntity, ModelToDomContext } from 'roosterjs-content-model-types'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; -import { handleEntity } from '../../../lib/modelToDom/handlers/handleEntity'; +import { + handleEntityBlock, + handleEntitySegment, +} from '../../../lib/modelToDom/handlers/handleEntity'; describe('handleEntity', () => { let context: ModelToDomContext; @@ -28,7 +31,7 @@ describe('handleEntity', () => { const parent = document.createElement('div'); context.addDelimiterForEntity = false; - handleEntity(document, parent, entityModel, context, null); + handleEntityBlock(document, parent, entityModel, context, null); expect(parent.innerHTML).toBe( '
' @@ -53,7 +56,7 @@ describe('handleEntity', () => { const parent = document.createElement('div'); - handleEntity(document, parent, entityModel, context, null); + handleEntityBlock(document, parent, entityModel, context, null); expect(parent.innerHTML).toBe('
test
'); expect(div.outerHTML).toBe('
test
'); @@ -74,7 +77,7 @@ describe('handleEntity', () => { const parent = document.createElement('div'); context.addDelimiterForEntity = true; - handleEntity(document, parent, entityModel, context, null); + handleEntitySegment(document, parent, entityModel, context, []); expect(parent.innerHTML).toBe( '' @@ -103,7 +106,7 @@ describe('handleEntity', () => { const br = document.createElement('br'); parent.appendChild(br); - const result = handleEntity(document, parent, entityModel, context, br); + const result = handleEntityBlock(document, parent, entityModel, context, br); expect(parent.innerHTML).toBe( '
test

' @@ -136,7 +139,7 @@ describe('handleEntity', () => { entityDiv.textContent = 'test'; - const result = handleEntity(document, parent, entityModel, context, entityDiv); + const result = handleEntityBlock(document, parent, entityModel, context, entityDiv); expect(insertBefore).not.toHaveBeenCalled(); expect(result).toBe(br); @@ -162,15 +165,14 @@ describe('handleEntity', () => { context.addDelimiterForEntity = true; - const result = handleEntity(document, parent, entityModel, context, br); + handleEntitySegment(document, parent, entityModel, context, []); expect(parent.innerHTML).toBe( - 'test
' + '
test' ); expect(span.outerHTML).toBe( 'test' ); - expect(result).toBe(br); expect(context.regularSelection.current.segment).toBe(span.nextSibling); }); @@ -189,7 +191,7 @@ describe('handleEntity', () => { span.textContent = 'test'; const parent = document.createElement('div'); - const result = handleEntity(document, parent, entityModel, context, null); + handleEntitySegment(document, parent, entityModel, context, []); expect(parent.innerHTML).toBe( 'test' @@ -197,7 +199,6 @@ describe('handleEntity', () => { expect(span.outerHTML).toBe( 'test' ); - expect(result).toBe(null); expect(context.regularSelection.current.segment).toBe(span); }); @@ -218,7 +219,7 @@ describe('handleEntity', () => { context.onNodeCreated = onNodeCreated; - handleEntity(document, parent, entityModel, context, null); + handleEntityBlock(document, parent, entityModel, context, null); expect(parent.innerHTML).toBe( '
' @@ -226,4 +227,64 @@ describe('handleEntity', () => { expect(onNodeCreated.calls.argsFor(0)[0]).toBe(entityModel); expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('div')); }); + + it('Inline entity with newSegments and delimiter', () => { + const span = document.createElement('span'); + const entityModel: ContentModelEntity = { + blockType: 'Entity', + segmentType: 'Entity', + format: {}, + id: 'entity_1', + type: 'entity', + isReadonly: true, + wrapper: span, + }; + + const parent = document.createElement('div'); + const newSegments: Node[] = []; + + context.addDelimiterForEntity = true; + handleEntitySegment(document, parent, entityModel, context, newSegments); + + expect(parent.innerHTML).toBe( + '' + ); + expect(span.outerHTML).toBe( + '' + ); + expect(addDelimiters.default).toHaveBeenCalledTimes(1); + expect(newSegments.length).toBe(3); + expect(newSegments[0]).toBe(span); + expect(newSegments[1]).toBe(span.nextSibling!); + expect(newSegments[2]).toBe(span.previousSibling!); + }); + + it('Inline entity with newSegments but no delimiter', () => { + const span = document.createElement('span'); + const entityModel: ContentModelEntity = { + blockType: 'Entity', + segmentType: 'Entity', + format: {}, + id: 'entity_1', + type: 'entity', + isReadonly: true, + wrapper: span, + }; + + const parent = document.createElement('div'); + const newSegments: Node[] = []; + + context.addDelimiterForEntity = false; + handleEntitySegment(document, parent, entityModel, context, newSegments); + + expect(parent.innerHTML).toBe( + '' + ); + expect(span.outerHTML).toBe( + '' + ); + expect(addDelimiters.default).toHaveBeenCalledTimes(0); + expect(newSegments.length).toBe(1); + expect(newSegments[0]).toBe(span); + }); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleGeneralModelTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleGeneralModelTest.ts index 0f4ee2649e7..b8aade3edeb 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleGeneralModelTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleGeneralModelTest.ts @@ -3,7 +3,10 @@ import * as stackFormat from '../../../lib/modelToDom/utils/stackFormat'; import { createGeneralBlock } from '../../../lib/modelApi/creators/createGeneralBlock'; import { createGeneralSegment } from '../../../lib/modelApi/creators/createGeneralSegment'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; -import { handleGeneralModel } from '../../../lib/modelToDom/handlers/handleGeneralModel'; +import { + handleGeneralBlock, + handleGeneralSegment, +} from '../../../lib/modelToDom/handlers/handleGeneralModel'; import { ContentModelBlockGroup, ContentModelFormatContainer, @@ -44,7 +47,7 @@ describe('handleBlockGroup', () => { spyOn(applyFormat, 'applyFormat'); - handleGeneralModel(document, parent, group, context, null); + handleGeneralBlock(document, parent, group, context, null); expect(parent.outerHTML).toBe('
'); expect(typeof parent.firstChild).toBe('object'); @@ -69,7 +72,7 @@ describe('handleBlockGroup', () => { spyOn(applyFormat, 'applyFormat'); - handleGeneralModel(document, parent, group, context, null); + handleGeneralSegment(document, parent, group, context, []); expect(parent.outerHTML).toBe('
'); expect(context.regularSelection.current.segment).toBe(clonedChild); @@ -96,7 +99,7 @@ describe('handleBlockGroup', () => { spyOn(applyFormat, 'applyFormat'); - handleGeneralModel(document, parent, group, context, null); + handleGeneralSegment(document, parent, group, context, []); expect(parent.outerHTML).toBe('
'); expect(context.regularSelection.current.segment).toBe(clonedChild); @@ -131,7 +134,7 @@ describe('handleBlockGroup', () => { spyOn(applyFormat, 'applyFormat').and.callThrough(); - handleGeneralModel(document, parent, group, context, null); + handleGeneralSegment(document, parent, group, context, []); expect(parent.outerHTML).toBe('
'); expect(context.regularSelection.current.segment).toBe(clonedChild); @@ -165,7 +168,7 @@ describe('handleBlockGroup', () => { spyOn(stackFormat, 'stackFormat').and.callThrough(); - handleGeneralModel(document, parent, group, context, null); + handleGeneralSegment(document, parent, group, context, []); expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1); expect((stackFormat.stackFormat).calls.argsFor(0)[1]).toBe('a'); @@ -183,7 +186,7 @@ describe('handleBlockGroup', () => { const br = document.createElement('br'); parent.appendChild(br); - const result = handleGeneralModel(document, parent, group, context, br); + const result = handleGeneralBlock(document, parent, group, context, br); expect(parent.outerHTML).toBe('

'); expect(typeof parent.firstChild).toBe('object'); @@ -209,7 +212,7 @@ describe('handleBlockGroup', () => { parent.appendChild(node); parent.appendChild(br); - const result = handleGeneralModel(document, parent, group, context, node); + const result = handleGeneralBlock(document, parent, group, context, node); expect(parent.outerHTML).toBe('

'); expect(parent.firstChild).toBe(node); @@ -229,11 +232,40 @@ describe('handleBlockGroup', () => { context.onNodeCreated = onNodeCreated; - handleGeneralModel(document, parent, group, context, null); + handleGeneralBlock(document, parent, group, context, null); expect(parent.innerHTML).toBe(''); expect(onNodeCreated).toHaveBeenCalledTimes(1); expect(onNodeCreated.calls.argsFor(0)[0]).toBe(group); expect(onNodeCreated.calls.argsFor(0)[1]).toBe(parent.querySelector('span')); }); + + it('General segment and newElements', () => { + const clonedChild = document.createElement('span'); + const childMock = ({ + cloneNode: () => clonedChild, + } as any) as HTMLElement; + const group = createGeneralSegment(childMock); + const newElements: Node[] = []; + + spyOn(applyFormat, 'applyFormat'); + + handleGeneralSegment(document, parent, group, context, newElements); + + expect(parent.outerHTML).toBe('
'); + expect(context.regularSelection.current.segment).toBe(clonedChild); + expect(typeof parent.firstChild).toBe('object'); + expect(parent.firstChild?.firstChild).toBe(clonedChild); + expect(context.listFormat.nodeStack).toEqual([]); + expect(handleBlockGroupChildren).toHaveBeenCalledTimes(1); + expect(handleBlockGroupChildren).toHaveBeenCalledWith( + document, + clonedChild, + group, + context + ); + expect(applyFormat.applyFormat).toHaveBeenCalled(); + expect(newElements.length).toBe(1); + expect(newElements[0]).toBe(clonedChild); + }); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleImageTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleImageTest.ts index 9dae22c4f25..f9c0a109424 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleImageTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleImageTest.ts @@ -30,7 +30,7 @@ describe('handleSegment', () => { ) { parent = document.createElement('div'); - handleImage(document, parent, segment, context); + handleImage(document, parent, segment, context, []); expect(parent.innerHTML).toBe(expectedInnerHTML); expect(handleBlock).toHaveBeenCalledTimes(expectedCreateBlockFromContentModelCalledTimes); @@ -152,7 +152,7 @@ describe('handleSegment', () => { context.onNodeCreated = onNodeCreated; - handleImage(document, parent, segment, context); + handleImage(document, parent, segment, context, []); expect(parent.innerHTML).toBe(''); expect(onNodeCreated).toHaveBeenCalledTimes(1); @@ -169,10 +169,29 @@ describe('handleSegment', () => { }; const parent = document.createElement('div'); - handleImage(document, parent, segment, context); + handleImage(document, parent, segment, context, []); expect(parent.innerHTML).toBe( '' ); }); + + it('With segmentNodes', () => { + const segment: ContentModelImage = { + segmentType: 'Image', + src: 'http://test.com/test', + format: { display: 'block' }, + dataset: {}, + }; + const parent = document.createElement('div'); + const segmentNodes: Node[] = []; + + handleImage(document, parent, segment, context, segmentNodes); + + expect(parent.innerHTML).toBe( + '' + ); + expect(segmentNodes.length).toBe(1); + expect(segmentNodes[0]).toBe(parent.firstChild!.firstChild!); + }); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts index 32cbe0cb717..57b45c46787 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleParagraphTest.ts @@ -6,16 +6,16 @@ import { handleParagraph } from '../../../lib/modelToDom/handlers/handleParagrap import { handleSegment as originalHandleSegment } from '../../../lib/modelToDom/handlers/handleSegment'; import { optimize } from '../../../lib/modelToDom/optimizers/optimize'; import { - ContentModelHandler, ContentModelParagraph, ContentModelSegment, + ContentModelSegmentHandler, ModelToDomContext, } from 'roosterjs-content-model-types'; describe('handleParagraph', () => { let parent: HTMLElement; let context: ModelToDomContext; - let handleSegment: jasmine.Spy>; + let handleSegment: jasmine.Spy>; beforeEach(() => { parent = document.createElement('div'); @@ -89,7 +89,8 @@ describe('handleParagraph', () => { document, parent.firstChild as HTMLElement, segment, - context + context, + [] ); }); @@ -110,7 +111,7 @@ describe('handleParagraph', () => { 1 ); - expect(handleSegment).toHaveBeenCalledWith(document, parent, segment, context); + expect(handleSegment).toHaveBeenCalledWith(document, parent, segment, context, []); }); it('Handle multiple segments', () => { @@ -141,13 +142,15 @@ describe('handleParagraph', () => { document, parent.firstChild as HTMLElement, segment1, - context + context, + [] ); expect(handleSegment).toHaveBeenCalledWith( document, parent.firstChild as HTMLElement, segment2, - context + context, + [] ); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleSegmentDecoratorTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleSegmentDecoratorTest.ts index e2b9c8d90c0..242074a48f1 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleSegmentDecoratorTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleSegmentDecoratorTest.ts @@ -1,5 +1,6 @@ import DarkColorHandlerImpl from 'roosterjs-editor-core/lib/editor/DarkColorHandlerImpl'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { expectHtml } from 'roosterjs-editor-dom/test/DomTestHelper'; import { handleSegmentDecorator } from '../../../lib/modelToDom/handlers/handleSegmentDecorator'; import { ContentModelCode, @@ -20,7 +21,8 @@ describe('handleSegmentDecorator', () => { function runTest( link: ContentModelLink | undefined, code: ContentModelCode | undefined, - expectedInnerHTML: string + expectedInnerHTML: string, + expectedSegmentNodesHTML: (string | string[])[] ) { parent = document.createElement('span'); parent.textContent = 'test'; @@ -31,10 +33,15 @@ describe('handleSegmentDecorator', () => { link: link, code: code, }; + const segmentNodes: Node[] = []; - handleSegmentDecorator(document, parent, segment, context); + handleSegmentDecorator(document, parent, segment, context, segmentNodes); expect(parent.innerHTML).toBe(expectedInnerHTML); + expect(segmentNodes.length).toBe(expectedSegmentNodesHTML.length); + expectedSegmentNodesHTML.forEach((expectedHTML, i) => { + expectHtml((segmentNodes[i] as HTMLElement).outerHTML, expectedHTML); + }); } it('simple link', () => { @@ -46,7 +53,9 @@ describe('handleSegmentDecorator', () => { dataset: {}, }; - runTest(link, undefined, 'test'); + runTest(link, undefined, 'test', [ + 'test', + ]); }); it('link with color', () => { @@ -61,7 +70,9 @@ describe('handleSegmentDecorator', () => { context.darkColorHandler = new DarkColorHandlerImpl({} as any, s => 'darkMock: ' + s); - runTest(link, undefined, 'test'); + runTest(link, undefined, 'test', [ + 'test', + ]); }); it('link without underline', () => { @@ -76,7 +87,8 @@ describe('handleSegmentDecorator', () => { runTest( link, undefined, - 'test' + 'test', + ['test'] ); }); @@ -92,7 +104,9 @@ describe('handleSegmentDecorator', () => { }, }; - runTest(link, undefined, 'test'); + runTest(link, undefined, 'test', [ + 'test', + ]); }); it('simple code', () => { @@ -102,7 +116,7 @@ describe('handleSegmentDecorator', () => { }, }; - runTest(undefined, code, 'test'); + runTest(undefined, code, 'test', ['test']); }); it('code with font', () => { @@ -112,7 +126,9 @@ describe('handleSegmentDecorator', () => { }, }; - runTest(undefined, code, 'test'); + runTest(undefined, code, 'test', [ + 'test', + ]); }); it('link and code', () => { @@ -129,7 +145,10 @@ describe('handleSegmentDecorator', () => { }, }; - runTest(link, code, 'test'); + runTest(link, code, 'test', [ + 'test', + 'test', + ]); }); it('Link with onNodeCreated', () => { @@ -156,7 +175,7 @@ describe('handleSegmentDecorator', () => { context.onNodeCreated = onNodeCreated; - handleSegmentDecorator(document, span, segment, context); + handleSegmentDecorator(document, span, segment, context, []); expect(parent.innerHTML).toBe( '' @@ -178,7 +197,12 @@ describe('handleSegmentDecorator', () => { dataset: {}, }; - runTest(link, undefined, 'test'); + runTest( + link, + undefined, + 'test', + ['test'] + ); }); it('code with display: block', () => { @@ -189,7 +213,9 @@ describe('handleSegmentDecorator', () => { }, }; - runTest(undefined, code, 'test'); + runTest(undefined, code, 'test', [ + 'test', + ]); }); it('link with background color', () => { @@ -206,7 +232,8 @@ describe('handleSegmentDecorator', () => { runTest( link, undefined, - 'test' + 'test', + ['test'] ); }); }); 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 dafbf64b120..0c6adfc2de2 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 @@ -9,32 +9,41 @@ import { ContentModelText, ModelToDomContext, ContentModelBlockHandler, - ContentModelHandler, + ContentModelSegmentHandler, + ContentModelGeneralSegment, } from 'roosterjs-content-model-types'; describe('handleSegment', () => { let parent: HTMLElement; let context: ModelToDomContext; - let handleBr: jasmine.Spy>; - let handleText: jasmine.Spy>; - let handleGeneralModel: jasmine.Spy>; - let handleEntity: jasmine.Spy>; - let handleImage: jasmine.Spy>; + let handleBr: jasmine.Spy>; + let handleText: jasmine.Spy>; + let handleGeneralBlock: jasmine.Spy>; + let handleGeneralSegment: jasmine.Spy>; + let handleEntityBlock: jasmine.Spy>; + let handleEntitySegment: jasmine.Spy>; + let handleImage: jasmine.Spy>; + let mockedSegmentNodes: any; beforeEach(() => { parent = document.createElement('div'); handleBr = jasmine.createSpy('handleBr'); handleText = jasmine.createSpy('handleText'); - handleGeneralModel = jasmine.createSpy('handleGeneralModel'); - handleEntity = jasmine.createSpy('handleEntity'); + handleGeneralBlock = jasmine.createSpy('handleGeneralBlock'); + handleEntityBlock = jasmine.createSpy('handleEntityBlock'); + handleGeneralSegment = jasmine.createSpy('handleGeneralSegment'); + handleEntitySegment = jasmine.createSpy('handleEntitySegment'); handleImage = jasmine.createSpy('handleImage'); + mockedSegmentNodes = 'SEGMENTNODES' as any; context = createModelToDomContext(undefined, { modelHandlerOverride: { br: handleBr, text: handleText, - general: handleGeneralModel, - entity: handleEntity, + generalSegment: handleGeneralSegment, + entitySegment: handleEntitySegment, + generalBlock: handleGeneralBlock, + entityBlock: handleEntityBlock, image: handleImage, }, }); @@ -47,9 +56,15 @@ describe('handleSegment', () => { format: {}, }; - handleSegment(document, parent, text, context); + handleSegment(document, parent, text, context, mockedSegmentNodes); - expect(handleText).toHaveBeenCalledWith(document, parent, text, context); + expect(handleText).toHaveBeenCalledWith( + document, + parent, + text, + context, + mockedSegmentNodes + ); expect(parent.innerHTML).toBe(''); }); @@ -58,10 +73,10 @@ describe('handleSegment', () => { segmentType: 'Br', format: {}, }; - handleSegment(document, parent, br, context); + handleSegment(document, parent, br, context, mockedSegmentNodes); expect(parent.innerHTML).toBe(''); - expect(handleBr).toHaveBeenCalledWith(document, parent, br, context); + expect(handleBr).toHaveBeenCalledWith(document, parent, br, context, mockedSegmentNodes); }); it('general segment', () => { @@ -74,9 +89,16 @@ describe('handleSegment', () => { format: {}, }; - handleSegment(document, parent, segment, context); + handleSegment(document, parent, segment, context, mockedSegmentNodes); expect(parent.innerHTML).toBe(''); - expect(handleGeneralModel).toHaveBeenCalledWith(document, parent, segment, context, null); + expect(handleGeneralSegment).toHaveBeenCalledWith( + document, + parent, + segment, + context, + mockedSegmentNodes + ); + expect(handleGeneralBlock).not.toHaveBeenCalled(); }); it('entity segment', () => { @@ -91,9 +113,16 @@ describe('handleSegment', () => { isReadonly: true, }; - handleSegment(document, parent, segment, context); + handleSegment(document, parent, segment, context, mockedSegmentNodes); expect(parent.innerHTML).toBe(''); - expect(handleEntity).toHaveBeenCalledWith(document, parent, segment, context, null); + expect(handleEntitySegment).toHaveBeenCalledWith( + document, + parent, + segment, + context, + mockedSegmentNodes + ); + expect(handleEntityBlock).not.toHaveBeenCalled(); }); it('image segment', () => { @@ -104,8 +133,14 @@ describe('handleSegment', () => { dataset: {}, }; - handleSegment(document, parent, segment, context); + handleSegment(document, parent, segment, context, mockedSegmentNodes); expect(parent.innerHTML).toBe(''); - expect(handleImage).toHaveBeenCalledWith(document, parent, segment, context); + expect(handleImage).toHaveBeenCalledWith( + document, + parent, + segment, + context, + mockedSegmentNodes + ); }); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleTextTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleTextTest.ts index df8c501aa94..4737c071f3d 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleTextTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/handlers/handleTextTest.ts @@ -20,7 +20,7 @@ describe('handleText', () => { format: {}, }; - handleText(document, parent, text, context); + handleText(document, parent, text, context, []); expect(parent.innerHTML).toBe('test'); }); @@ -33,7 +33,7 @@ describe('handleText', () => { }; context.darkColorHandler = new DarkColorHandlerImpl({} as any, s => 'darkMock: ' + s); - handleText(document, parent, text, context); + handleText(document, parent, text, context, []); expect(parent.innerHTML).toBe('test'); }); @@ -46,7 +46,7 @@ describe('handleText', () => { link: { format: { href: '/test', underline: true }, dataset: {} }, }; - handleText(document, parent, text, context); + handleText(document, parent, text, context, []); expect(parent.innerHTML).toBe('test'); }); @@ -63,7 +63,7 @@ describe('handleText', () => { }, }; - handleText(document, parent, text, context); + handleText(document, parent, text, context, []); expect(parent.innerHTML).toBe('test'); }); @@ -78,7 +78,7 @@ describe('handleText', () => { spyOn(stackFormat, 'stackFormat').and.callThrough(); - handleText(document, parent, text, context); + handleText(document, parent, text, context, []); expect(parent.innerHTML).toBe('test'); expect(stackFormat.stackFormat).toHaveBeenCalledTimes(1); @@ -97,7 +97,7 @@ describe('handleText', () => { context.onNodeCreated = onNodeCreated; - handleText(document, parent, text, context); + handleText(document, parent, text, context, []); expect(parent.innerHTML).toBe('test'); expect(onNodeCreated).toHaveBeenCalledTimes(1); @@ -120,8 +120,23 @@ describe('handleText', () => { }, }; - handleText(document, parent, text, context); + handleText(document, parent, text, context, []); expect(parent.innerHTML).toBe('test'); }); + + it('Text segment with segmentNodes', () => { + const text: ContentModelText = { + segmentType: 'Text', + text: 'test', + format: {}, + }; + const segmentNodes: Node[] = []; + + handleText(document, parent, text, context, segmentNodes); + + expect(parent.innerHTML).toBe('test'); + expect(segmentNodes.length).toBe(1); + expect(segmentNodes[0]).toBe(parent.firstChild!.firstChild!); + }); }); diff --git a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/utils/handleSegmentCommonTest.ts b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/utils/handleSegmentCommonTest.ts index 7f9cde63d16..5ed00d3f49f 100644 --- a/packages-content-model/roosterjs-content-model-dom/test/modelToDom/utils/handleSegmentCommonTest.ts +++ b/packages-content-model/roosterjs-content-model-dom/test/modelToDom/utils/handleSegmentCommonTest.ts @@ -28,14 +28,19 @@ describe('handleSegmentCommon', () => { href: 'href', }, }; + container.appendChild(txt); + const segmentNodes: Node[] = []; - handleSegmentCommon(document, txt, container, segment, context); + handleSegmentCommon(document, txt, container, segment, context, segmentNodes); expect(context.regularSelection.current.segment).toBe(txt); expect(container.outerHTML).toBe( - '' + 'test' ); expect(onNodeCreated).toHaveBeenCalledWith(segment, txt); + expect(segmentNodes.length).toBe(2); + expect(segmentNodes[0]).toBe(txt); + expect(segmentNodes[1]).toBe(txt.parentNode!); }); it('element with child', () => { @@ -48,12 +53,16 @@ describe('handleSegmentCommon', () => { const segment = createText('test', {}); const onNodeCreated = jasmine.createSpy('onNodeCreated'); const context = createModelToDomContext(); + const segmentNodes: Node[] = []; context.onNodeCreated = onNodeCreated; - handleSegmentCommon(document, parent, container, segment, context); + container.appendChild(parent); + handleSegmentCommon(document, parent, container, segment, context, segmentNodes); expect(context.regularSelection.current.segment).toBe(null); - expect(container.outerHTML).toBe(''); + expect(container.outerHTML).toBe('child'); expect(onNodeCreated).toHaveBeenCalledWith(segment, parent); + expect(segmentNodes.length).toBe(1); + expect(segmentNodes[0]).toBe(parent); }); }); diff --git a/packages-content-model/roosterjs-content-model-types/lib/context/ContentModelHandler.ts b/packages-content-model/roosterjs-content-model-types/lib/context/ContentModelHandler.ts index 74dbb5578c8..ad68a9422cf 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/context/ContentModelHandler.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/context/ContentModelHandler.ts @@ -31,3 +31,21 @@ export type ContentModelBlockHandler Node | null; + +/** + * Type of Content Model to DOM handler for block + * @param doc Target HTML Document object + * @param parent Parent HTML node to append the new node from the given model + * @param model The Content Model to handle + * @param context The context object to provide related information + * @param segmentNodes Nodes that created to represent this segment. In most cases there will be one node returned, except + * - For segments with decorators: decorator elements will also be included + * - For inline entity segment, the delimiter SPANs will also be included + */ +export type ContentModelSegmentHandler = ( + doc: Document, + parent: Node, + model: T, + context: ModelToDomContext, + segmentNodes: Node[] +) => void; diff --git a/packages-content-model/roosterjs-content-model-types/lib/context/ModelToDomSettings.ts b/packages-content-model/roosterjs-content-model-types/lib/context/ModelToDomSettings.ts index aac581401cb..4802f068898 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/context/ModelToDomSettings.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/context/ModelToDomSettings.ts @@ -1,7 +1,6 @@ import { ContentModelBlock } from '../block/ContentModelBlock'; import { ContentModelBlockFormat } from '../format/ContentModelBlockFormat'; import { ContentModelBlockGroup } from '../group/ContentModelBlockGroup'; -import { ContentModelBlockHandler, ContentModelHandler } from './ContentModelHandler'; import { ContentModelBr } from '../segment/ContentModelBr'; import { ContentModelDecorator } from '../decorator/ContentModelDecorator'; import { ContentModelDivider } from '../block/ContentModelDivider'; @@ -10,6 +9,7 @@ import { ContentModelFormatBase } from '../format/ContentModelFormatBase'; import { ContentModelFormatContainer } from '../group/ContentModelFormatContainer'; import { ContentModelFormatMap } from '../format/ContentModelFormatMap'; import { ContentModelGeneralBlock } from '../group/ContentModelGeneralBlock'; +import { ContentModelGeneralSegment } from '../segment/ContentModelGeneralSegment'; import { ContentModelImage } from '../segment/ContentModelImage'; import { ContentModelListItem } from '../group/ContentModelListItem'; import { ContentModelParagraph } from '../block/ContentModelParagraph'; @@ -20,6 +20,11 @@ import { ContentModelTableRow } from '../block/ContentModelTableRow'; import { ContentModelText } from '../segment/ContentModelText'; import { FormatHandlerTypeMap, FormatKey } from '../format/FormatHandlerTypeMap'; import { ModelToDomContext } from './ModelToDomContext'; +import { + ContentModelHandler, + ContentModelBlockHandler, + ContentModelSegmentHandler, +} from './ContentModelHandler'; /** * Default implicit format map from tag name (lower case) to segment format @@ -72,17 +77,27 @@ export type ContentModelHandlerMap = { /** * Content Model type for ContentModelBr */ - br: ContentModelHandler; + br: ContentModelSegmentHandler; /** * Content Model type for child models of ContentModelEntity */ - entity: ContentModelBlockHandler; + entityBlock: ContentModelBlockHandler; + + /** + * Content Model type for child models of ContentModelEntity + */ + entitySegment: ContentModelSegmentHandler; + + /** + * Content Model type for ContentModelGeneralBlock + */ + generalBlock: ContentModelBlockHandler; /** * Content Model type for ContentModelGeneralBlock */ - general: ContentModelBlockHandler; + generalSegment: ContentModelSegmentHandler; /** * Content Model type for ContentModelHR @@ -92,7 +107,7 @@ export type ContentModelHandlerMap = { /** * Content Model type for ContentModelImage */ - image: ContentModelHandler; + image: ContentModelSegmentHandler; /** * Content Model type for list group of ContentModelListItem @@ -117,12 +132,12 @@ export type ContentModelHandlerMap = { /** * Content Model type for ContentModelSegment */ - segment: ContentModelHandler; + segment: ContentModelSegmentHandler; /** * Content Model type for ContentModelCode */ - segmentDecorator: ContentModelHandler; + segmentDecorator: ContentModelSegmentHandler; /** * Content Model type for ContentModelTable @@ -132,7 +147,7 @@ export type ContentModelHandlerMap = { /** * Content Model type for ContentModelText */ - text: ContentModelHandler; + text: ContentModelSegmentHandler; }; /** 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 22e154bf4a5..e2a4ad5d7c6 100644 --- a/packages-content-model/roosterjs-content-model-types/lib/index.ts +++ b/packages-content-model/roosterjs-content-model-types/lib/index.ts @@ -134,6 +134,10 @@ export { ModelToDomListContext, ModelToDomFormatContext, } from './context/ModelToDomFormatContext'; -export { ContentModelHandler, ContentModelBlockHandler } from './context/ContentModelHandler'; +export { + ContentModelHandler, + ContentModelSegmentHandler, + ContentModelBlockHandler, +} from './context/ContentModelHandler'; export { DomToModelOption } from './context/DomToModelOption'; export { ModelToDomOption } from './context/ModelToDomOption';