From f8a23bb492e1d08165f182dd27d6f9a3327d9cd4 Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Thu, 6 Jun 2024 08:08:58 -0600 Subject: [PATCH] Merge Link & Image Format when using MergeModel (#2681) * mergeLinkFormat * Also fix for images * remove unneeded changes * Remove more unneeded changes * Address comment * nit --------- Co-authored-by: Jiuqing Song --- .../lib/modelApi/editing/mergeModel.ts | 60 ++++ .../test/modelApi/editing/mergeModelTest.ts | 302 ++++++++++++++++++ 2 files changed, 362 insertions(+) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index b440afd1f69..94ed4f65319 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -14,6 +14,7 @@ import type { ContentModelBlock, ContentModelBlockFormat, ContentModelDocument, + ContentModelHyperLinkFormat, ContentModelListItem, ContentModelParagraph, ContentModelSegmentFormat, @@ -28,6 +29,21 @@ import type { } from 'roosterjs-content-model-types'; const HeadingTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; +// An object to provide keys of required properties of segment format, do NOT use any of its values +const RequiredEmptySegmentFormat: Required = { + backgroundColor: null!, + fontFamily: null!, + fontSize: null!, + fontWeight: null!, + italic: null!, + letterSpacing: null!, + lineHeight: null!, + strikethrough: null!, + superOrSubScriptSequence: null!, + textColor: null!, + underline: null!, +}; +const KeysOfSegmentFormat = getObjectKeys(RequiredEmptySegmentFormat); /** * Merge source model into target mode @@ -359,6 +375,14 @@ function applyDefaultFormat( ...paragraphFormat, ...segment.format, }); + + if (segment.link) { + segment.link.format = mergeSegmentFormat( + applyDefaultFormatOption, + getSegmentFormatInLinkFormat(format), + segment.link.format + ); + } }); if (applyDefaultFormatOption === 'keepSourceEmphasisFormat') { @@ -375,6 +399,27 @@ function mergeBlockFormat(applyDefaultFormatOption: string, block: ReadonlyConte } } +/** + * Hyperlink format type definition only contains textColor, backgroundColor and underline. + * So create a minimum object with the styles supported in Hyperlink to be used in merge. + */ +function getSegmentFormatInLinkFormat( + targetFormat: ContentModelSegmentFormat +): ContentModelSegmentFormat { + const result: ContentModelHyperLinkFormat = {}; + if (targetFormat.textColor) { + result.textColor = targetFormat.textColor; + } + if (targetFormat.backgroundColor) { + result.backgroundColor = targetFormat.backgroundColor; + } + if (targetFormat.underline) { + result.underline = targetFormat.underline; + } + + return result; +} + function mergeSegmentFormat( applyDefaultFormatOption: 'mergeAll' | 'keepSourceEmphasisFormat', targetFormat: ContentModelSegmentFormat, @@ -383,6 +428,7 @@ function mergeSegmentFormat( return applyDefaultFormatOption == 'mergeAll' ? { ...targetFormat, ...sourceFormat } : { + ...getFormatWithoutSegmentFormat(sourceFormat), ...targetFormat, ...getSemanticFormat(sourceFormat), }; @@ -405,3 +451,17 @@ function getSemanticFormat(segmentFormat: ContentModelSegmentFormat): ContentMod return result; } + +/** + * Segment format can also contain other type of metadata, for example in Images/Hyperlink, + * we want to preserve these properties when merging format + */ +function getFormatWithoutSegmentFormat( + sourceFormat: ContentModelSegmentFormat +): ContentModelSegmentFormat { + const resultFormat = { + ...sourceFormat, + }; + KeysOfSegmentFormat.forEach(key => delete resultFormat[key]); + return resultFormat; +} diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts index 538aaee4bd7..b22a2988f7b 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts @@ -3533,4 +3533,306 @@ describe('mergeModel', () => { tableContext: undefined, }); }); + + it('Merge Link Format with mergeAll option', () => { + const newTarget: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + const mergeLinkSourceModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Work Item 222824', + format: { + fontFamily: + '"Segoe UI VSS (Regular)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + fontSize: '14px', + textColor: 'var(--communication-foreground,rgba(0, 90, 158, 1))', + underline: true, + italic: false, + backgroundColor: 'rgb(32, 31, 30)', + }, + link: { + format: { + underline: true, + href: 'https://www.bing.com', + anchorClass: 'bolt-link', + textColor: + 'var(--communication-foreground,rgba(0, 90, 158, 1))', + backgroundColor: 'rgb(32, 31, 30)', + borderRadius: '2px', + textAlign: 'start', + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: 'Test', + format: { + fontFamily: + '"Segoe UI VSS (Regular)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + fontSize: '14px', + textColor: 'rgb(255, 255, 255)', + italic: false, + backgroundColor: 'rgb(32, 31, 30)', + }, + }, + ], + format: {}, + isImplicit: true, + segmentFormat: { + fontFamily: + '"Segoe UI VSS (Regular)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + fontSize: '14px', + }, + }, + ], + }; + mergeModel(newTarget, mergeLinkSourceModel, undefined, { + mergeFormat: 'mergeAll', + }); + + const para = newTarget.blocks[0] as ContentModelParagraph; + expect(para.segments[0].link).toEqual({ + format: { + href: 'https://www.bing.com', + anchorClass: 'bolt-link', + borderRadius: '2px', + textAlign: 'start', + textColor: 'var(--communication-foreground,rgba(0, 90, 158, 1))', + backgroundColor: 'rgb(32, 31, 30)', + underline: true, + }, + dataset: {}, + }); + }); + + it('Merge Link Format with keepSourceEmphasisFormat option', () => { + const targetModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + segments: [ + { + isSelected: true, + segmentType: 'SelectionMarker', + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }, + ], + segmentFormat: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + blockType: 'Paragraph', + format: {}, + }, + ], + format: { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }, + }; + const sourceModel: ContentModelDocument = { + blockGroupType: 'Document', + blocks: [ + { + blockType: 'Paragraph', + segments: [ + { + segmentType: 'Text', + text: 'Work Item 222824', + format: { + fontFamily: + '"Segoe UI VSS (Regular)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + fontSize: '14px', + textColor: 'var(--communication-foreground,rgba(0, 90, 158, 1))', + underline: true, + italic: false, + backgroundColor: 'rgb(32, 31, 30)', + }, + link: { + format: { + underline: true, + href: 'https://www.bing.com', + anchorClass: 'bolt-link', + textColor: + 'var(--communication-foreground,rgba(0, 90, 158, 1))', + backgroundColor: 'rgb(32, 31, 30)', + borderRadius: '2px', + textAlign: 'start', + }, + dataset: {}, + }, + }, + { + segmentType: 'Text', + text: 'Test', + format: { + fontFamily: + '"Segoe UI VSS (Regular)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + fontSize: '14px', + textColor: 'rgb(255, 255, 255)', + italic: false, + backgroundColor: 'rgb(32, 31, 30)', + }, + }, + ], + format: {}, + isImplicit: true, + segmentFormat: { + fontFamily: + '"Segoe UI VSS (Regular)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', + fontSize: '14px', + }, + }, + ], + }; + + mergeModel(targetModel, sourceModel, undefined, { + mergeFormat: 'keepSourceEmphasisFormat', + }); + + const para = targetModel.blocks[0] as ContentModelParagraph; + expect(para.segments[0].link).toEqual({ + format: { + href: 'https://www.bing.com', + anchorClass: 'bolt-link', + borderRadius: '2px', + textAlign: 'start', + textColor: '#000000', + underline: true, + }, + dataset: {}, + }); + }); + + it('Keep image width when merging with keepSourceEmphasisFormat', () => { + const targetModel = createContentModelDocument(); + const para = createParagraph(); + const marker = createSelectionMarker(); + para.segments.push(marker); + targetModel.blocks.push(para); + + marker.format = { + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + }; + + const sourceModel = createContentModelDocument(); + sourceModel.blocks.push({ + blockType: 'Paragraph', + format: {}, + segments: [ + { + segmentType: 'Image', + format: { + fontFamily: 'Remove this', + fontSize: 'Remove this', + textColor: 'Remove this', + backgroundColor: 'imageColor', + width: 'imageWidth', + maxWidth: 'imageMaxWidth', + height: 'imageHeight', + maxHeight: 'imageMaxHeight', + id: 'imageId', + marginBottom: '0px', + marginLeft: '0px', + marginRight: '0px', + marginTop: '0px', + borderBottom: 'border', + borderBottomLeftRadius: 'border', + borderBottomRightRadius: 'border', + borderLeft: 'border', + borderRadius: 'border', + borderTop: 'border', + borderRight: 'border', + borderTopLeftRadius: 'border', + borderTopRightRadius: 'border', + boxShadow: 'border', + display: 'display', + float: 'float', + minHeight: 'minHeight', + minWidth: 'minWidth', + verticalAlign: 'top', + }, + dataset: {}, + src: 'https://www.bing.com', + }, + ], + }); + + mergeModel(targetModel, sourceModel, undefined, { + mergeFormat: 'keepSourceEmphasisFormat', + }); + + const block = targetModel.blocks[0] as ContentModelParagraph; + expect(block.segments[0].format).toEqual({ + fontFamily: 'Calibri', + fontSize: '11pt', + textColor: '#000000', + width: 'imageWidth', + maxWidth: 'imageMaxWidth', + height: 'imageHeight', + maxHeight: 'imageMaxHeight', + id: 'imageId', + marginBottom: '0px', + marginLeft: '0px', + marginRight: '0px', + marginTop: '0px', + borderBottom: 'border', + borderBottomLeftRadius: 'border', + borderBottomRightRadius: 'border', + borderLeft: 'border', + borderRadius: 'border', + borderTop: 'border', + borderRight: 'border', + borderTopLeftRadius: 'border', + borderTopRightRadius: 'border', + boxShadow: 'border', + display: 'display', + float: 'float', + minHeight: 'minHeight', + minWidth: 'minWidth', + verticalAlign: 'top', + }); + }); });