From 425b217f49bcebbc71e07edbf8fe77be1572daea Mon Sep 17 00:00:00 2001 From: Bryan Valverde U Date: Thu, 6 Jun 2024 12:48:49 -0600 Subject: [PATCH] Patch RoosterJS to 9.4.1 (#2686) * 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 * update versions * Merge pull request #2684 from microsoft/u/juliaroldi/trigger-auto-format Auto format trigger --------- Co-authored-by: Jiuqing Song Co-authored-by: Julia Roldi <87443959+juliaroldi@users.noreply.github.com> --- .../formatContentModel/formatContentModel.ts | 15 +- .../lib/modelApi/editing/mergeModel.ts | 60 ++++ .../test/modelApi/editing/mergeModelTest.ts | 302 ++++++++++++++++++ versions.json | 2 +- 4 files changed, 367 insertions(+), 12 deletions(-) diff --git a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts index bcebd23f23b..6a54afab304 100644 --- a/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts +++ b/packages/roosterjs-content-model-core/lib/coreApi/formatContentModel/formatContentModel.ts @@ -24,15 +24,8 @@ export const formatContentModel: FormatContentModel = ( options, domToModelOptions ) => { - const { - apiName, - onNodeCreated, - getChangeData, - changeSource, - rawEvent, - selectionOverride, - scrollCaretIntoView, - } = options || {}; + const { onNodeCreated, getChangeData, rawEvent, selectionOverride, scrollCaretIntoView } = + options || {}; const model = core.api.createContentModel(core, domToModelOptions, selectionOverride); const context: FormatContentModelContext = { newEntities: [], @@ -82,9 +75,9 @@ export const formatContentModel: FormatContentModel = ( eventType: 'contentChanged', contentModel: clearModelCache ? undefined : model, selection: clearModelCache ? undefined : selection, - source: changeSource || ChangeSource.Format, + source: options?.changeSource || ChangeSource.Format, data: getChangeData?.(), - formatApiName: apiName, + formatApiName: options?.apiName, changedEntities: getChangedEntities(context, rawEvent), }; 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', + }); + }); }); diff --git a/versions.json b/versions.json index 25149e1d91b..c116fe8c4fb 100644 --- a/versions.json +++ b/versions.json @@ -1,7 +1,7 @@ { "legacy": "8.62.0", "react": "8.56.0", - "main": "9.4.0", + "main": "9.4.1", "legacyAdapter": "8.62.0", "overrides": {} }