From b2ec6623e1a040fa4204421b0781f3611b8de739 Mon Sep 17 00:00:00 2001 From: JiuqingSong Date: Thu, 27 Oct 2022 15:30:50 -0700 Subject: [PATCH 1/3] Bug bash bug fix --- .../selection/getSelectedParagraphs.ts | 25 ++++ .../modelToDom/handlers/handleBlockGroup.ts | 13 +- .../selection/getSelectedParagraphsTest.ts | 115 ++++++++++++++++++ 3 files changed, 146 insertions(+), 7 deletions(-) diff --git a/packages/roosterjs-content-model/lib/modelApi/selection/getSelectedParagraphs.ts b/packages/roosterjs-content-model/lib/modelApi/selection/getSelectedParagraphs.ts index b4cadd9f7eb..d700d3ab1d1 100644 --- a/packages/roosterjs-content-model/lib/modelApi/selection/getSelectedParagraphs.ts +++ b/packages/roosterjs-content-model/lib/modelApi/selection/getSelectedParagraphs.ts @@ -18,9 +18,34 @@ export function getSelectedParagraphs(group: ContentModelBlockGroup): SelectedPa getSelectedParagraphsInternal([group], result); + // Remove tail paragraph if first selection marker is the only selection + if (result.length > 1 && isOnlySelectionMarkerSelected(result, false /*checkFirstParagraph*/)) { + result.pop(); + } + + // Remove head paragraph if first selection marker is the only selection + if (result.length > 1 && isOnlySelectionMarkerSelected(result, true /*checkFirstParagraph*/)) { + result.shift(); + } + return result; } +function isOnlySelectionMarkerSelected( + paragraphs: SelectedParagraphWithPath[], + checkFirstParagraph: boolean +): boolean { + const paragraph = paragraphs[checkFirstParagraph ? 0 : paragraphs.length - 1].paragraph; + const selectedSegments = paragraph.segments.filter(s => s.isSelected); + + return ( + selectedSegments.length == 1 && + selectedSegments[0].segmentType == 'SelectionMarker' && + selectedSegments[0] == + paragraph.segments[checkFirstParagraph ? paragraph.segments.length - 1 : 0] + ); +} + function getSelectedParagraphsInternal( path: ContentModelBlockGroup[], result: SelectedParagraphWithPath[], diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts index a0fa13d6952..80ac949edcc 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts @@ -1,4 +1,3 @@ -import { applyFormat } from '../utils/applyFormat'; import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; import { ContentModelGeneralBlock } from '../../publicTypes/block/group/ContentModelGeneralBlock'; import { ContentModelGeneralSegment } from '../../publicTypes/segment/ContentModelGeneralSegment'; @@ -23,12 +22,12 @@ export const handleBlockGroup: ContentModelHandler = ( context.modelHandlers.blockGroupChildren(doc, newParent, group, context); - if (isGeneralSegment(group) && isNodeOfType(newParent, NodeType.Element)) { - if (!group.element.firstChild) { - context.regularSelection.current.segment = newParent; - } - - applyFormat(newParent, context.formatAppliers.segment, group.format, context); + if ( + isGeneralSegment(group) && + isNodeOfType(newParent, NodeType.Element) && + !group.element.firstChild + ) { + context.regularSelection.current.segment = newParent; } break; diff --git a/packages/roosterjs-content-model/test/modelApi/selection/getSelectedParagraphsTest.ts b/packages/roosterjs-content-model/test/modelApi/selection/getSelectedParagraphsTest.ts index 7bd5a1ff704..bd7c218762a 100644 --- a/packages/roosterjs-content-model/test/modelApi/selection/getSelectedParagraphsTest.ts +++ b/packages/roosterjs-content-model/test/modelApi/selection/getSelectedParagraphsTest.ts @@ -2,6 +2,7 @@ import { createContentModelDocument } from '../../../lib/modelApi/creators/creat import { createListItem } from '../../../lib/modelApi/creators/createListItem'; import { createParagraph } from '../../../lib/modelApi/creators/createParagraph'; import { createQuote } from '../../../lib/modelApi/creators/createQuote'; +import { createSelectionMarker } from '../../../lib/modelApi/creators/createSelectionMarker'; import { createTable } from '../../../lib/modelApi/creators/createTable'; import { createTableCell } from '../../../lib/modelApi/creators/createTableCell'; import { createText } from '../../../lib/modelApi/creators/createText'; @@ -239,4 +240,118 @@ describe('getSelectedParagraphs', () => { }, ]); }); + + it('Select from the end of paragraph', () => { + const group = createContentModelDocument(document); + const para1 = createParagraph(); + const para2 = createParagraph(); + const marker = createSelectionMarker(); + const text = createText('test'); + + text.isSelected = true; + para1.segments.push(marker); + para2.segments.push(text); + group.blocks.push(para1); + group.blocks.push(para2); + + const result = getSelectedParagraphs(group); + + expect(result).toEqual([ + { + paragraph: para2, + path: [group], + }, + ]); + }); + + it('Select to the start of paragraph', () => { + const group = createContentModelDocument(document); + const para1 = createParagraph(); + const para2 = createParagraph(); + const marker = createSelectionMarker(); + const text = createText('test'); + + text.isSelected = true; + para1.segments.push(text); + para2.segments.push(marker); + group.blocks.push(para1); + group.blocks.push(para2); + + const result = getSelectedParagraphs(group); + + expect(result).toEqual([ + { + paragraph: para1, + path: [group], + }, + ]); + }); + + it('Select from the end to the start of paragraph', () => { + const group = createContentModelDocument(document); + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const marker1 = createSelectionMarker(); + const marker2 = createSelectionMarker(); + const text = createText('test'); + + text.isSelected = true; + para1.segments.push(marker1); + para2.segments.push(text); + para3.segments.push(marker2); + group.blocks.push(para1); + group.blocks.push(para2); + group.blocks.push(para3); + + const result = getSelectedParagraphs(group); + + expect(result).toEqual([ + { + paragraph: para2, + path: [group], + }, + ]); + }); + + it('Select not from the end, and not to the start of paragraph', () => { + const group = createContentModelDocument(document); + const para1 = createParagraph(); + const para2 = createParagraph(); + const para3 = createParagraph(); + const marker1 = createSelectionMarker(); + const marker2 = createSelectionMarker(); + const text1 = createText('test1'); + const text2 = createText('test2'); + const text3 = createText('test3'); + + text1.isSelected = true; + text2.isSelected = true; + text3.isSelected = true; + para1.segments.push(marker1); + para1.segments.push(text1); + para2.segments.push(text2); + para3.segments.push(text3); + para3.segments.push(marker2); + group.blocks.push(para1); + group.blocks.push(para2); + group.blocks.push(para3); + + const result = getSelectedParagraphs(group); + + expect(result).toEqual([ + { + paragraph: para1, + path: [group], + }, + { + paragraph: para2, + path: [group], + }, + { + paragraph: para3, + path: [group], + }, + ]); + }); }); From 3de59e5951856028b65e2e3efd899df9a25b0131 Mon Sep 17 00:00:00 2001 From: JiuqingSong Date: Fri, 28 Oct 2022 10:26:47 -0700 Subject: [PATCH 2/3] fix test --- .../modelToDom/handlers/handleBlockGroupTest.ts | 16 ++-------------- .../test/modelToDom/handlers/handleBlockTest.ts | 7 +------ 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockGroupTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockGroupTest.ts index 9a776be1130..5161fe40205 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockGroupTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockGroupTest.ts @@ -92,13 +92,7 @@ describe('handleBlockGroup', () => { group, context ); - expect(applyFormat.applyFormat).toHaveBeenCalledTimes(1); - expect(applyFormat.applyFormat).toHaveBeenCalledWith( - clonedChild, - context.formatAppliers.segment, - group.format, - context - ); + expect(applyFormat.applyFormat).not.toHaveBeenCalled(); }); it('General segment: element with child', () => { @@ -125,13 +119,7 @@ describe('handleBlockGroup', () => { group, context ); - expect(applyFormat.applyFormat).toHaveBeenCalledTimes(1); - expect(applyFormat.applyFormat).toHaveBeenCalledWith( - clonedChild, - context.formatAppliers.segment, - group.format, - context - ); + expect(applyFormat.applyFormat).not.toHaveBeenCalled(); }); it('Quote', () => { diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts index 03007ff882c..d77dce05ff5 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts @@ -106,12 +106,7 @@ describe('handleBlock', () => { expect(parent.innerHTML).toBe(''); expect(parent.firstChild).not.toBe(element); expect(context.regularSelection.current.segment).toBe(parent.firstChild); - expect(applyFormat.applyFormat).toHaveBeenCalledWith( - parent.firstChild as HTMLElement, - context.formatAppliers.segment, - block.format, - context - ); + expect(applyFormat.applyFormat).not.toHaveBeenCalled(); }); it('Entity block', () => { From 59c3add4952f63c6263f6e4b6199533ce42c8f1d Mon Sep 17 00:00:00 2001 From: JiuqingSong Date: Mon, 31 Oct 2022 09:54:52 -0700 Subject: [PATCH 3/3] improve --- .../domToModel/processors/generalProcessor.ts | 3 +- .../context/defaultContentModelHandlers.ts | 6 + .../modelToDom/handlers/handleBlockGroup.ts | 22 +--- .../lib/modelToDom/handlers/handleBr.ts | 22 ++++ .../modelToDom/handlers/handleGeneralModel.ts | 34 ++++++ .../lib/modelToDom/handlers/handleSegment.ts | 25 +--- .../lib/modelToDom/handlers/handleText.ts | 23 ++++ .../publicTypes/context/ModelToDomSettings.ts | 18 +++ .../handlers/handleBlockGroupTest.ts | 76 ++---------- .../modelToDom/handlers/handleBlockTest.ts | 2 +- .../test/modelToDom/handlers/handleBrTest.ts | 36 ++++++ .../modelToDom/handlers/handleEntityTest.ts | 3 + .../handlers/handleGeneralModelTest.ts | 111 ++++++++++++++++++ .../modelToDom/handlers/handleSegmentTest.ts | 102 +++++++++------- .../modelToDom/handlers/handleTextTest.ts | 38 ++++++ 15 files changed, 362 insertions(+), 159 deletions(-) create mode 100644 packages/roosterjs-content-model/lib/modelToDom/handlers/handleBr.ts create mode 100644 packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts create mode 100644 packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts create mode 100644 packages/roosterjs-content-model/test/modelToDom/handlers/handleBrTest.ts create mode 100644 packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts create mode 100644 packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts diff --git a/packages/roosterjs-content-model/lib/domToModel/processors/generalProcessor.ts b/packages/roosterjs-content-model/lib/domToModel/processors/generalProcessor.ts index fae8f4299d0..a33665589d8 100644 --- a/packages/roosterjs-content-model/lib/domToModel/processors/generalProcessor.ts +++ b/packages/roosterjs-content-model/lib/domToModel/processors/generalProcessor.ts @@ -29,6 +29,8 @@ const generalSegmentProcessor: ElementProcessor = (group, element, segment.isSelected = true; } + addSegment(group, segment); + stackFormat( context, { @@ -36,7 +38,6 @@ const generalSegmentProcessor: ElementProcessor = (group, element, 'empty' /*clearFormat, General segment will include all properties and styles when generate back to HTML, so no need to carry over existing segment format*/, }, () => { - addSegment(group, segment); context.elementProcessors.child(segment, element, context); } ); diff --git a/packages/roosterjs-content-model/lib/modelToDom/context/defaultContentModelHandlers.ts b/packages/roosterjs-content-model/lib/modelToDom/context/defaultContentModelHandlers.ts index 53730a3edf8..aa39017e273 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/context/defaultContentModelHandlers.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/context/defaultContentModelHandlers.ts @@ -2,7 +2,9 @@ import { ContentModelHandlerMap } from '../../publicTypes/context/ModelToDomSett import { handleBlock } from '../handlers/handleBlock'; import { handleBlockGroup } from '../handlers/handleBlockGroup'; import { handleBlockGroupChildren } from '../handlers/handleBlockGroupChildren'; +import { handleBr } from '../handlers/handleBr'; import { handleEntity } from '../handlers/handleEntity'; +import { handleGeneralModel } from '../handlers/handleGeneralModel'; import { handleHR } from '../handlers/handleHr'; import { handleImage } from '../handlers/handleImage'; import { handleList } from '../handlers/handleList'; @@ -11,6 +13,7 @@ import { handleParagraph } from '../handlers/handleParagraph'; import { handleQuote } from '../handlers/handleQuote'; import { handleSegment } from '../handlers/handleSegment'; import { handleTable } from '../handlers/handleTable'; +import { handleText } from '../handlers/handleText'; /** * @internal @@ -19,7 +22,9 @@ export const defaultContentModelHandlers: ContentModelHandlerMap = { block: handleBlock, blockGroup: handleBlockGroup, blockGroupChildren: handleBlockGroupChildren, + br: handleBr, entity: handleEntity, + general: handleGeneralModel, hr: handleHR, image: handleImage, list: handleList, @@ -28,4 +33,5 @@ export const defaultContentModelHandlers: ContentModelHandlerMap = { quote: handleQuote, segment: handleSegment, table: handleTable, + text: handleText, }; diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts index 80ac949edcc..579df3e2061 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBlockGroup.ts @@ -1,10 +1,6 @@ import { ContentModelBlockGroup } from '../../publicTypes/block/group/ContentModelBlockGroup'; -import { ContentModelGeneralBlock } from '../../publicTypes/block/group/ContentModelGeneralBlock'; -import { ContentModelGeneralSegment } from '../../publicTypes/segment/ContentModelGeneralSegment'; import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; -import { isNodeOfType } from '../../domUtils/isNodeOfType'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; -import { NodeType } from 'roosterjs-editor-types'; /** * @internal @@ -17,19 +13,7 @@ export const handleBlockGroup: ContentModelHandler = ( ) => { switch (group.blockGroupType) { case 'General': - const newParent = group.element.cloneNode(); - parent.appendChild(newParent); - - context.modelHandlers.blockGroupChildren(doc, newParent, group, context); - - if ( - isGeneralSegment(group) && - isNodeOfType(newParent, NodeType.Element) && - !group.element.firstChild - ) { - context.regularSelection.current.segment = newParent; - } - + context.modelHandlers.general(doc, parent, group, context); break; case 'Quote': @@ -45,7 +29,3 @@ export const handleBlockGroup: ContentModelHandler = ( break; } }; - -function isGeneralSegment(block: ContentModelGeneralBlock): block is ContentModelGeneralSegment { - return (block as ContentModelGeneralSegment).segmentType == 'General'; -} diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBr.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBr.ts new file mode 100644 index 00000000000..95194757f55 --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleBr.ts @@ -0,0 +1,22 @@ +import { applyFormat } from '../utils/applyFormat'; +import { ContentModelBr } from '../../publicTypes/segment/ContentModelBr'; +import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; +import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; + +/** + * @internal + */ +export const handleBr: ContentModelHandler = ( + doc: Document, + parent: Node, + segment: ContentModelBr, + context: ModelToDomContext +) => { + const br = doc.createElement('br'); + const element = doc.createElement('span'); + element.appendChild(br); + parent.appendChild(element); + + context.regularSelection.current.segment = br; + applyFormat(element, context.formatAppliers.segment, segment.format, context); +}; diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts new file mode 100644 index 00000000000..6e33e926dbe --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleGeneralModel.ts @@ -0,0 +1,34 @@ +import { applyFormat } from '../utils/applyFormat'; +import { ContentModelGeneralBlock } from '../../publicTypes/block/group/ContentModelGeneralBlock'; +import { ContentModelGeneralSegment } from '../../publicTypes/segment/ContentModelGeneralSegment'; +import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; +import { isNodeOfType } from '../../domUtils/isNodeOfType'; +import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; +import { NodeType } from 'roosterjs-editor-types'; + +/** + * @internal + */ +export const handleGeneralModel: ContentModelHandler = ( + doc: Document, + parent: Node, + group: ContentModelGeneralBlock, + context: ModelToDomContext +) => { + const newParent = group.element.cloneNode(); + parent.appendChild(newParent); + + context.modelHandlers.blockGroupChildren(doc, newParent, group, context); + + if (isGeneralSegment(group) && isNodeOfType(newParent, NodeType.Element)) { + if (!group.element.firstChild) { + context.regularSelection.current.segment = newParent; + } + + applyFormat(newParent, context.formatAppliers.segment, group.format, context); + } +}; + +function isGeneralSegment(block: ContentModelGeneralBlock): block is ContentModelGeneralSegment { + return (block as ContentModelGeneralSegment).segmentType == 'General'; +} diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleSegment.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleSegment.ts index 31b6526c1e5..ff960dbdf78 100644 --- a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleSegment.ts +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleSegment.ts @@ -1,4 +1,3 @@ -import { applyFormat } from '../utils/applyFormat'; import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegment'; import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; @@ -21,27 +20,13 @@ export const handleSegment: ContentModelHandler = ( }; } - let element: HTMLElement | null = null; - switch (segment.segmentType) { case 'Text': - const txt = doc.createTextNode(segment.text); - - element = doc.createElement('span'); - element.appendChild(txt); - regularSelection.current.segment = txt; - - applyFormat(element, context.formatAppliers.segment, segment.format, context); - + context.modelHandlers.text(doc, parent, segment, context); break; case 'Br': - const br = doc.createElement('br'); - element = doc.createElement('span'); - element.appendChild(br); - regularSelection.current.segment = br; - - applyFormat(element, context.formatAppliers.segment, segment.format, context); + context.modelHandlers.br(doc, parent, segment, context); break; case 'Image': @@ -49,7 +34,7 @@ export const handleSegment: ContentModelHandler = ( break; case 'General': - context.modelHandlers.block(doc, parent, segment, context); + context.modelHandlers.general(doc, parent, segment, context); break; case 'Entity': @@ -57,10 +42,6 @@ export const handleSegment: ContentModelHandler = ( break; } - if (element) { - parent.appendChild(element); - } - // If end position is not set, or it is not finalized, and current segment is still in selection, set end position // If there is other selection, we will overwrite regularSelection.end when we process that segment if (segment.isSelected && regularSelection.start) { diff --git a/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts new file mode 100644 index 00000000000..95fb8b2b0b9 --- /dev/null +++ b/packages/roosterjs-content-model/lib/modelToDom/handlers/handleText.ts @@ -0,0 +1,23 @@ +import { applyFormat } from '../utils/applyFormat'; +import { ContentModelHandler } from '../../publicTypes/context/ContentModelHandler'; +import { ContentModelText } from '../../publicTypes/segment/ContentModelText'; +import { ModelToDomContext } from '../../publicTypes/context/ModelToDomContext'; + +/** + * @internal + */ +export const handleText: ContentModelHandler = ( + doc: Document, + parent: Node, + segment: ContentModelText, + context: ModelToDomContext +) => { + const txt = doc.createTextNode(segment.text); + const element = doc.createElement('span'); + + element.appendChild(txt); + parent.appendChild(element); + + context.regularSelection.current.segment = txt; + applyFormat(element, context.formatAppliers.segment, segment.format, context); +}; diff --git a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts index cd94bd19cd5..d5aa469be4a 100644 --- a/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts +++ b/packages/roosterjs-content-model/lib/publicTypes/context/ModelToDomSettings.ts @@ -1,8 +1,10 @@ import { ContentModelBlock } from '../block/ContentModelBlock'; import { ContentModelBlockGroup } from '../block/group/ContentModelBlockGroup'; +import { ContentModelBr } from '../segment/ContentModelBr'; import { ContentModelEntity } from '../entity/ContentModelEntity'; import { ContentModelFormatBase } from '../format/ContentModelFormatBase'; import { ContentModelFormatMap } from '../format/ContentModelFormatMap'; +import { ContentModelGeneralBlock } from '../block/group/ContentModelGeneralBlock'; import { ContentModelHandler } from './ContentModelHandler'; import { ContentModelHR } from '../block/ContentModelHR'; import { ContentModelImage } from '../segment/ContentModelImage'; @@ -11,6 +13,7 @@ import { ContentModelParagraph } from '../block/ContentModelParagraph'; import { ContentModelQuote } from '../block/group/ContentModelQuote'; import { ContentModelSegment } from '../segment/ContentModelSegment'; import { ContentModelTable } from '../block/ContentModelTable'; +import { ContentModelText } from '../segment/ContentModelText'; import { FormatHandlerTypeMap, FormatKey } from '../format/FormatHandlerTypeMap'; import { ModelToDomContext } from './ModelToDomContext'; @@ -59,11 +62,21 @@ export interface ContentModelHandlerTypeMap { */ blockGroupChildren: ContentModelBlockGroup; + /** + * Content Model type for ContentModelBr + */ + br: ContentModelBr; + /** * Content Model type for child models of ContentModelEntity */ entity: ContentModelEntity; + /** + * Content Model type for ContentModelGeneralBlock + */ + general: ContentModelGeneralBlock; + /** * Content Model type for ContentModelHR */ @@ -103,6 +116,11 @@ export interface ContentModelHandlerTypeMap { * Content Model type for ContentModelTable */ table: ContentModelTable; + + /** + * Content Model type for ContentModelText + */ + text: ContentModelText; } /** diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockGroupTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockGroupTest.ts index 5161fe40205..90d573474de 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockGroupTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockGroupTest.ts @@ -1,11 +1,10 @@ -import * as applyFormat from '../../../lib/modelToDom/utils/applyFormat'; import { ContentModelBlockGroup } from '../../../lib/publicTypes/block/group/ContentModelBlockGroup'; +import { ContentModelGeneralBlock } from '../../../lib/publicTypes/block/group/ContentModelGeneralBlock'; import { ContentModelHandler } from '../../../lib/publicTypes/context/ContentModelHandler'; import { ContentModelListItem } from '../../../lib/publicTypes/block/group/ContentModelListItem'; import { ContentModelQuote } from '../../../lib/publicTypes/block/group/ContentModelQuote'; import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument'; import { createGeneralBlock } from '../../../lib/modelApi/creators/createGeneralBlock'; -import { createGeneralSegment } from '../../../lib/modelApi/creators/createGeneralSegment'; import { createListItem } from '../../../lib/modelApi/creators/createListItem'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { createQuote } from '../../../lib/modelApi/creators/createQuote'; @@ -18,17 +17,20 @@ describe('handleBlockGroup', () => { let handleBlockGroupChildren: jasmine.Spy>; let handleListItem: jasmine.Spy>; let handleQuote: jasmine.Spy>; + let handleGeneralModel: jasmine.Spy>; beforeEach(() => { handleBlockGroupChildren = jasmine.createSpy('handleBlockGroupChildren'); handleListItem = jasmine.createSpy('handleListItem'); handleQuote = jasmine.createSpy('handleQuote'); + handleGeneralModel = jasmine.createSpy('handleGeneralModel'); context = createModelToDomContext(undefined, { modelHandlerOverride: { blockGroupChildren: handleBlockGroupChildren, listItem: handleListItem, quote: handleQuote, + general: handleGeneralModel, }, }); parent = document.createElement('div'); @@ -51,75 +53,11 @@ describe('handleBlockGroup', () => { } as any) as HTMLElement; const group = createGeneralBlock(childMock); - spyOn(applyFormat, 'applyFormat'); - handleBlockGroup(document, parent, group, context); - expect(parent.outerHTML).toBe('
'); - expect(typeof parent.firstChild).toBe('object'); - expect(parent.firstChild).toBe(clonedChild); - expect(context.listFormat.nodeStack).toEqual([]); - expect(handleBlockGroupChildren).toHaveBeenCalledTimes(1); - expect(handleBlockGroupChildren).toHaveBeenCalledWith( - document, - clonedChild, - group, - context - ); - expect(applyFormat.applyFormat).not.toHaveBeenCalled(); - }); - - it('General segment: empty element', () => { - const clonedChild = document.createElement('span'); - const childMock = ({ - cloneNode: () => clonedChild, - } as any) as HTMLElement; - const group = createGeneralSegment(childMock); - - spyOn(applyFormat, 'applyFormat'); - - handleBlockGroup(document, parent, group, context); - - expect(parent.outerHTML).toBe('
'); - expect(context.regularSelection.current.segment).toBe(clonedChild); - expect(typeof parent.firstChild).toBe('object'); - expect(parent.firstChild).toBe(clonedChild); - expect(context.listFormat.nodeStack).toEqual([]); - expect(handleBlockGroupChildren).toHaveBeenCalledTimes(1); - expect(handleBlockGroupChildren).toHaveBeenCalledWith( - document, - clonedChild, - group, - context - ); - expect(applyFormat.applyFormat).not.toHaveBeenCalled(); - }); - - it('General segment: element with child', () => { - const clonedChild = document.createElement('span'); - const childMock = ({ - cloneNode: () => clonedChild, - firstChild: true, - } as any) as HTMLElement; - const group = createGeneralSegment(childMock); - - spyOn(applyFormat, 'applyFormat'); - - handleBlockGroup(document, parent, group, context); - - expect(parent.outerHTML).toBe('
'); - expect(context.regularSelection.current.segment).toBeNull(); - expect(typeof parent.firstChild).toBe('object'); - expect(parent.firstChild).toBe(clonedChild); - expect(context.listFormat.nodeStack).toEqual([]); - expect(handleBlockGroupChildren).toHaveBeenCalledTimes(1); - expect(handleBlockGroupChildren).toHaveBeenCalledWith( - document, - clonedChild, - group, - context - ); - expect(applyFormat.applyFormat).not.toHaveBeenCalled(); + expect(parent.outerHTML).toBe('
'); + expect(handleGeneralModel).toHaveBeenCalledTimes(1); + expect(handleGeneralModel).toHaveBeenCalledWith(document, parent, group, context); }); it('Quote', () => { diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts index d77dce05ff5..27db4efc9c9 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBlockTest.ts @@ -106,7 +106,7 @@ describe('handleBlock', () => { expect(parent.innerHTML).toBe(''); expect(parent.firstChild).not.toBe(element); expect(context.regularSelection.current.segment).toBe(parent.firstChild); - expect(applyFormat.applyFormat).not.toHaveBeenCalled(); + expect(applyFormat.applyFormat).toHaveBeenCalled(); }); it('Entity block', () => { diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleBrTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBrTest.ts new file mode 100644 index 00000000000..722cdc4bdf5 --- /dev/null +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleBrTest.ts @@ -0,0 +1,36 @@ +import { ContentModelBr } from '../../../lib/publicTypes/segment/ContentModelBr'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { handleBr } from '../../../lib/modelToDom/handlers/handleBr'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; + +describe('handleSegment', () => { + let parent: HTMLElement; + let context: ModelToDomContext; + + beforeEach(() => { + parent = document.createElement('div'); + context = createModelToDomContext(); + }); + + it('Br segment', () => { + const br: ContentModelBr = { + segmentType: 'Br', + format: {}, + }; + + handleBr(document, parent, br, context); + + expect(parent.innerHTML).toBe('
'); + }); + + it('Br segment with format', () => { + const br: ContentModelBr = { + segmentType: 'Br', + format: { textColor: 'red' }, + }; + + handleBr(document, parent, br, context); + + expect(parent.innerHTML).toBe('
'); + }); +}); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleEntityTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleEntityTest.ts index c1e74b51435..6390989868a 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleEntityTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleEntityTest.ts @@ -33,5 +33,8 @@ describe('handleEntity', () => { placeholder: parent.firstChild as Comment, }, ]); + expect(div.outerHTML).toBe( + '
' + ); }); }); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts new file mode 100644 index 00000000000..06f0d5db194 --- /dev/null +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleGeneralModelTest.ts @@ -0,0 +1,111 @@ +import * as applyFormat from '../../../lib/modelToDom/utils/applyFormat'; +import { ContentModelBlockGroup } from '../../../lib/publicTypes/block/group/ContentModelBlockGroup'; +import { ContentModelHandler } from '../../../lib/publicTypes/context/ContentModelHandler'; +import { ContentModelListItem } from '../../../lib/publicTypes/block/group/ContentModelListItem'; +import { ContentModelQuote } from '../../../lib/publicTypes/block/group/ContentModelQuote'; +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 { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; + +describe('handleBlockGroup', () => { + let context: ModelToDomContext; + let parent: HTMLDivElement; + let handleBlockGroupChildren: jasmine.Spy>; + let handleListItem: jasmine.Spy>; + let handleQuote: jasmine.Spy>; + + beforeEach(() => { + handleBlockGroupChildren = jasmine.createSpy('handleBlockGroupChildren'); + handleListItem = jasmine.createSpy('handleListItem'); + handleQuote = jasmine.createSpy('handleQuote'); + + context = createModelToDomContext(undefined, { + modelHandlerOverride: { + blockGroupChildren: handleBlockGroupChildren, + listItem: handleListItem, + quote: handleQuote, + }, + }); + parent = document.createElement('div'); + }); + + it('General block', () => { + const clonedChild = document.createElement('span'); + const childMock = ({ + cloneNode: () => clonedChild, + } as any) as HTMLElement; + const group = createGeneralBlock(childMock); + + spyOn(applyFormat, 'applyFormat'); + + handleGeneralModel(document, parent, group, context); + + expect(parent.outerHTML).toBe('
'); + expect(typeof parent.firstChild).toBe('object'); + expect(parent.firstChild).toBe(clonedChild); + expect(context.listFormat.nodeStack).toEqual([]); + expect(handleBlockGroupChildren).toHaveBeenCalledTimes(1); + expect(handleBlockGroupChildren).toHaveBeenCalledWith( + document, + clonedChild, + group, + context + ); + expect(applyFormat.applyFormat).not.toHaveBeenCalled(); + }); + + it('General segment: empty element', () => { + const clonedChild = document.createElement('span'); + const childMock = ({ + cloneNode: () => clonedChild, + } as any) as HTMLElement; + const group = createGeneralSegment(childMock); + + spyOn(applyFormat, 'applyFormat'); + + handleGeneralModel(document, parent, group, context); + + expect(parent.outerHTML).toBe('
'); + expect(context.regularSelection.current.segment).toBe(clonedChild); + expect(typeof parent.firstChild).toBe('object'); + expect(parent.firstChild).toBe(clonedChild); + expect(context.listFormat.nodeStack).toEqual([]); + expect(handleBlockGroupChildren).toHaveBeenCalledTimes(1); + expect(handleBlockGroupChildren).toHaveBeenCalledWith( + document, + clonedChild, + group, + context + ); + expect(applyFormat.applyFormat).toHaveBeenCalled(); + }); + + it('General segment: element with child', () => { + const clonedChild = document.createElement('span'); + const childMock = ({ + cloneNode: () => clonedChild, + firstChild: true, + } as any) as HTMLElement; + const group = createGeneralSegment(childMock); + + spyOn(applyFormat, 'applyFormat'); + + handleGeneralModel(document, parent, group, context); + + expect(parent.outerHTML).toBe('
'); + expect(context.regularSelection.current.segment).toBeNull(); + expect(typeof parent.firstChild).toBe('object'); + expect(parent.firstChild).toBe(clonedChild); + expect(context.listFormat.nodeStack).toEqual([]); + expect(handleBlockGroupChildren).toHaveBeenCalledTimes(1); + expect(handleBlockGroupChildren).toHaveBeenCalledWith( + document, + clonedChild, + group, + context + ); + expect(applyFormat.applyFormat).toHaveBeenCalled(); + }); +}); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentTest.ts index 7ae4fc44c9f..724ad009536 100644 --- a/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentTest.ts +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleSegmentTest.ts @@ -1,6 +1,10 @@ -import { ContentModelBlock } from '../../../lib/publicTypes/block/ContentModelBlock'; +import { ContentModelBr } from '../../../lib/publicTypes/segment/ContentModelBr'; +import { ContentModelEntity } from '../../../lib/publicTypes/entity/ContentModelEntity'; +import { ContentModelGeneralBlock } from '../../../lib/publicTypes/block/group/ContentModelGeneralBlock'; import { ContentModelHandler } from '../../../lib/publicTypes/context/ContentModelHandler'; +import { ContentModelImage } from '../../../lib/publicTypes/segment/ContentModelImage'; import { ContentModelSegment } from '../../../lib/publicTypes/segment/ContentModelSegment'; +import { ContentModelText } from '../../../lib/publicTypes/segment/ContentModelText'; import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; import { handleSegment } from '../../../lib/modelToDom/handlers/handleSegment'; import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; @@ -8,51 +12,53 @@ import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomCo describe('handleSegment', () => { let parent: HTMLElement; let context: ModelToDomContext; - let handleBlock: jasmine.Spy>; + let handleBr: jasmine.Spy>; + let handleText: jasmine.Spy>; + let handleGeneralModel: jasmine.Spy>; + let handleEntity: jasmine.Spy>; + let handleImage: jasmine.Spy>; beforeEach(() => { - handleBlock = jasmine.createSpy('handleBlock'); + parent = document.createElement('div'); + handleBr = jasmine.createSpy('handleBr'); + handleText = jasmine.createSpy('handleText'); + handleGeneralModel = jasmine.createSpy('handleGeneralModel'); + handleEntity = jasmine.createSpy('handleEntity'); + handleImage = jasmine.createSpy('handleImage'); + context = createModelToDomContext(undefined, { modelHandlerOverride: { - block: handleBlock, + br: handleBr, + text: handleText, + general: handleGeneralModel, + entity: handleEntity, + image: handleImage, }, }); }); - function runTest( - segment: ContentModelSegment, - expectedInnerHTML: string, - expectedCreateBlockFromContentModelCalledTimes: number - ) { - parent = document.createElement('div'); - - handleSegment(document, parent, segment, context); + it('Text segment', () => { + const text: ContentModelText = { + segmentType: 'Text', + text: 'test', + format: {}, + }; - expect(parent.innerHTML).toBe(expectedInnerHTML); - expect(handleBlock).toHaveBeenCalledTimes(expectedCreateBlockFromContentModelCalledTimes); - } + handleSegment(document, parent, text, context); - it('Text segment', () => { - runTest( - { - segmentType: 'Text', - text: 'test', - format: {}, - }, - 'test', - 0 - ); + expect(handleText).toHaveBeenCalledWith(document, parent, text, context); + expect(parent.innerHTML).toBe(''); }); it('Br segment', () => { - runTest( - { - segmentType: 'Br', - format: {}, - }, - '
', - 0 - ); + const br: ContentModelBr = { + segmentType: 'Br', + format: {}, + }; + handleSegment(document, parent, br, context); + + expect(parent.innerHTML).toBe(''); + expect(handleBr).toHaveBeenCalledWith(document, parent, br, context); }); it('general segment', () => { @@ -64,8 +70,10 @@ describe('handleSegment', () => { element: null!, format: {}, }; - runTest(segment, '', 1); - expect(handleBlock).toHaveBeenCalledWith(document, parent, segment, context); + + handleSegment(document, parent, segment, context); + expect(parent.innerHTML).toBe(''); + expect(handleGeneralModel).toHaveBeenCalledWith(document, parent, segment, context); }); it('entity segment', () => { @@ -79,17 +87,21 @@ describe('handleSegment', () => { wrapper: div, isReadonly: true, }; - runTest(segment, '', 0); - expect(context.entityPairs).toEqual([ - { - entityWrapper: div, - placeholder: document.createComment('Entity:entity_1'), - }, - ]); + handleSegment(document, parent, segment, context); + expect(parent.innerHTML).toBe(''); + expect(handleEntity).toHaveBeenCalledWith(document, parent, segment, context); + }); + + it('image segment', () => { + const segment: ContentModelSegment = { + segmentType: 'Image', + src: 'test', + format: {}, + }; - expect(div.outerHTML).toBe( - '
' - ); + handleSegment(document, parent, segment, context); + expect(parent.innerHTML).toBe(''); + expect(handleImage).toHaveBeenCalledWith(document, parent, segment, context); }); }); diff --git a/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts new file mode 100644 index 00000000000..d59ff33a97b --- /dev/null +++ b/packages/roosterjs-content-model/test/modelToDom/handlers/handleTextTest.ts @@ -0,0 +1,38 @@ +import { ContentModelText } from '../../../lib/publicTypes/segment/ContentModelText'; +import { createModelToDomContext } from '../../../lib/modelToDom/context/createModelToDomContext'; +import { handleText } from '../../../lib/modelToDom/handlers/handleText'; +import { ModelToDomContext } from '../../../lib/publicTypes/context/ModelToDomContext'; + +describe('handleSegment', () => { + let parent: HTMLElement; + let context: ModelToDomContext; + + beforeEach(() => { + parent = document.createElement('div'); + context = createModelToDomContext(); + }); + + it('Text segment', () => { + const text: ContentModelText = { + segmentType: 'Text', + text: 'test', + format: {}, + }; + + handleText(document, parent, text, context); + + expect(parent.innerHTML).toBe('test'); + }); + + it('Text segment', () => { + const text: ContentModelText = { + segmentType: 'Text', + text: 'test', + format: { textColor: 'red' }, + }; + + handleText(document, parent, text, context); + + expect(parent.innerHTML).toBe('test'); + }); +});