Skip to content

Commit

Permalink
use param instead
Browse files Browse the repository at this point in the history
  • Loading branch information
Rain-Zheng committed Nov 14, 2024
1 parent 8c5fdb4 commit 5290282
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 75 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { reducedModelChildProcessor } from '../../modelApi/common/reducedModelChildProcessor';
import { retrieveModelFormatState } from 'roosterjs-content-model-dom';
import type { IEditor, ContentModelFormatState, MergeFormatValueCallbacks } from 'roosterjs-content-model-types';
import type { IEditor, ContentModelFormatState, ConflictFormatSolution } from 'roosterjs-content-model-types';

/**
* Get current format state
* @param editor The editor to get format from
* @param callbacks Callbacks to customize the behavior of merging format values
* @param conflictSolution The strategy for handling format conflicts
*/
export function getFormatState(
editor: IEditor,
callbacks?: MergeFormatValueCallbacks
conflictSolution: ConflictFormatSolution = 'remove'
): ContentModelFormatState {
const pendingFormat = editor.getPendingFormat();
const manager = editor.getSnapshotsManager();
Expand All @@ -21,7 +21,7 @@ export function getFormatState(

editor.formatContentModel(
model => {
retrieveModelFormatState(model, pendingFormat, result, callbacks);
retrieveModelFormatState(model, pendingFormat, result, conflictSolution);

return false;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('getFormatState', () => {
canRedo: false,
isDarkMode: false,
},
undefined
'remove'
);
expect(result).toEqual(expectedFormat);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import { isBold } from '../../domUtils/style/isBold';
import { iterateSelections } from '../selection/iterateSelections';
import { parseValueWithUnit } from '../../formatHandlers/utils/parseValueWithUnit';
import type {
ConflictFormatSolution,
ContentModelFormatState,
ContentModelSegmentFormat,
MergeFormatValueCallback,
MergeFormatValueCallbacks,
ReadonlyContentModelBlockGroup,
ReadonlyContentModelBlock,
ReadonlyContentModelImage,
Expand All @@ -24,13 +23,13 @@ import type {
* @param model The Content Model to retrieve format state from
* @param pendingFormat Existing pending format, if any
* @param formatState Existing format state object, used for receiving the result
* @param callbacks Callbacks to customize the behavior of merging format values
* @param conflictSolution The strategy for handling format conflicts
*/
export function retrieveModelFormatState(
model: ReadonlyContentModelDocument,
pendingFormat: ContentModelSegmentFormat | null,
formatState: ContentModelFormatState,
callbacks?: MergeFormatValueCallbacks
conflictSolution: ConflictFormatSolution = 'remove'
) {
let firstTableContext: ReadonlyTableSelectionContext | undefined;
let firstBlock: ReadonlyContentModelBlock | undefined;
Expand All @@ -42,7 +41,7 @@ export function retrieveModelFormatState(
model,
(path, tableContext, block, segments) => {
// Structure formats
retrieveStructureFormat(formatState, path, isFirst, callbacks);
retrieveStructureFormat(formatState, path, isFirst, conflictSolution);

// Multiple line format
if (block) {
Expand All @@ -55,7 +54,7 @@ export function retrieveModelFormatState(

if (block?.blockType == 'Paragraph') {
// Paragraph formats
retrieveParagraphFormat(formatState, block, isFirst, callbacks);
retrieveParagraphFormat(formatState, block, isFirst, conflictSolution);

// Segment formats
segments?.forEach(segment => {
Expand All @@ -79,10 +78,10 @@ export function retrieveModelFormatState(
segment.link?.format,
pendingFormat
),
callbacks
conflictSolution
);

mergeValue(formatState, 'isCodeInline', !!segment?.code, isFirst, undefined, callbacks);
mergeValue(formatState, 'isCodeInline', !!segment?.code, isFirst, conflictSolution);
}

// We only care the format of selection marker when it is the first selected segment. This is because when selection marker
Expand Down Expand Up @@ -144,54 +143,54 @@ function retrieveSegmentFormat(
result: ContentModelFormatState,
isFirst: boolean,
mergedFormat: ContentModelSegmentFormat,
callbacks?: MergeFormatValueCallbacks
conflictSolution: ConflictFormatSolution = 'remove'
) {
const superOrSubscript = mergedFormat.superOrSubScriptSequence?.split(' ')?.pop();

mergeValue(result, 'isBold', isBold(mergedFormat.fontWeight), isFirst, undefined, callbacks);
mergeValue(result, 'isItalic', mergedFormat.italic, isFirst, undefined, callbacks);
mergeValue(result, 'isUnderline', mergedFormat.underline, isFirst, undefined, callbacks);
mergeValue(result, 'isStrikeThrough', mergedFormat.strikethrough, isFirst, undefined, callbacks);
mergeValue(result, 'isSuperscript', superOrSubscript == 'super', isFirst, undefined, callbacks);
mergeValue(result, 'isSubscript', superOrSubscript == 'sub', isFirst, undefined, callbacks);
mergeValue(result, 'letterSpacing', mergedFormat.letterSpacing, isFirst, undefined, callbacks);
mergeValue(result, 'isBold', isBold(mergedFormat.fontWeight), isFirst, conflictSolution);
mergeValue(result, 'isItalic', mergedFormat.italic, isFirst, conflictSolution);
mergeValue(result, 'isUnderline', mergedFormat.underline, isFirst, conflictSolution);
mergeValue(result, 'isStrikeThrough', mergedFormat.strikethrough, isFirst, conflictSolution);
mergeValue(result, 'isSuperscript', superOrSubscript == 'super', isFirst, conflictSolution);
mergeValue(result, 'isSubscript', superOrSubscript == 'sub', isFirst, conflictSolution);
mergeValue(result, 'letterSpacing', mergedFormat.letterSpacing, isFirst, conflictSolution);

mergeValue(result, 'fontName', mergedFormat.fontFamily, isFirst, undefined, callbacks);
mergeValue(result, 'fontName', mergedFormat.fontFamily, isFirst, conflictSolution);
mergeValue(
result,
'fontSize',
mergedFormat.fontSize,
isFirst,
val => parseValueWithUnit(val, undefined, 'pt') + 'pt',
callbacks
conflictSolution,
val => parseValueWithUnit(val, undefined, 'pt') + 'pt'
);
mergeValue(result, 'backgroundColor', mergedFormat.backgroundColor, isFirst, undefined, callbacks);
mergeValue(result, 'textColor', mergedFormat.textColor, isFirst, undefined, callbacks);
mergeValue(result, 'fontWeight', mergedFormat.fontWeight, isFirst, undefined, callbacks);
mergeValue(result, 'lineHeight', mergedFormat.lineHeight, isFirst, undefined, callbacks);
mergeValue(result, 'backgroundColor', mergedFormat.backgroundColor, isFirst, conflictSolution);
mergeValue(result, 'textColor', mergedFormat.textColor, isFirst, conflictSolution);
mergeValue(result, 'fontWeight', mergedFormat.fontWeight, isFirst, conflictSolution);
mergeValue(result, 'lineHeight', mergedFormat.lineHeight, isFirst, conflictSolution);
}

function retrieveParagraphFormat(
result: ContentModelFormatState,
paragraph: ReadonlyContentModelParagraph,
isFirst: boolean,
callbacks?: MergeFormatValueCallbacks
conflictSolution: ConflictFormatSolution = 'remove'
) {
const headingLevel = parseInt((paragraph.decorator?.tagName || '').substring(1));
const validHeadingLevel = headingLevel >= 1 && headingLevel <= 6 ? headingLevel : undefined;

mergeValue(result, 'marginBottom', paragraph.format.marginBottom, isFirst, undefined, callbacks);
mergeValue(result, 'marginTop', paragraph.format.marginTop, isFirst, undefined, callbacks);
mergeValue(result, 'headingLevel', validHeadingLevel, isFirst, undefined, callbacks);
mergeValue(result, 'textAlign', paragraph.format.textAlign, isFirst, undefined, callbacks);
mergeValue(result, 'direction', paragraph.format.direction, isFirst, undefined, callbacks);
mergeValue(result, 'marginBottom', paragraph.format.marginBottom, isFirst, conflictSolution);
mergeValue(result, 'marginTop', paragraph.format.marginTop, isFirst, conflictSolution);
mergeValue(result, 'headingLevel', validHeadingLevel, isFirst, conflictSolution);
mergeValue(result, 'textAlign', paragraph.format.textAlign, isFirst, conflictSolution);
mergeValue(result, 'direction', paragraph.format.direction, isFirst, conflictSolution);
}

function retrieveStructureFormat(
result: ContentModelFormatState,
path: ReadonlyContentModelBlockGroup[],
isFirst: boolean,
callbacks?: MergeFormatValueCallbacks
conflictSolution: ConflictFormatSolution = 'remove'
) {
const listItemIndex = getClosestAncestorBlockGroupIndex(path, ['ListItem'], []);
const containerIndex = getClosestAncestorBlockGroupIndex(path, ['FormatContainer'], []);
Expand All @@ -200,8 +199,8 @@ function retrieveStructureFormat(
const listItem = path[listItemIndex] as ReadonlyContentModelListItem;
const listType = listItem?.levels[listItem.levels.length - 1]?.listType;

mergeValue(result, 'isBullet', listType == 'UL', isFirst, undefined, callbacks);
mergeValue(result, 'isNumbering', listType == 'OL', isFirst, undefined, callbacks);
mergeValue(result, 'isBullet', listType == 'UL', isFirst, conflictSolution);
mergeValue(result, 'isNumbering', listType == 'OL', isFirst, conflictSolution);
}

mergeValue(
Expand All @@ -210,8 +209,7 @@ function retrieveStructureFormat(
containerIndex >= 0 &&
(path[containerIndex] as ReadonlyContentModelFormatContainer)?.tagName == 'blockquote',
isFirst,
undefined,
callbacks
conflictSolution
);
}

Expand Down Expand Up @@ -252,19 +250,27 @@ function mergeValue<K extends keyof ContentModelFormatState>(
key: K,
newValue: ContentModelFormatState[K] | undefined,
isFirst: boolean,
conflictSolution: ConflictFormatSolution = 'remove',
parseFn: (val: ContentModelFormatState[K]) => ContentModelFormatState[K] = val => val,
callbacks?: MergeFormatValueCallbacks
) {
if (isFirst) {
if (newValue !== undefined) {
format[key] = newValue;
}
} else if (parseFn(newValue) !== parseFn(format[key])) {
const callback = callbacks?.[key] as MergeFormatValueCallback | undefined;
if (callback) {
callback(format, newValue);
} else {
delete format[key];
switch (conflictSolution) {
case 'remove':
delete format[key];
break;
case 'keepFirst':
break;
case 'returnMultiple':
if (typeof format[key] === 'string') {
(format[key] as string) = 'Multiple';
} else {
delete format[key];
}
break;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as iterateSelections from '../../../lib/modelApi/selection/iterateSelec
import { addCode } from '../../../lib/modelApi/common/addDecorators';
import { addSegment } from '../../../lib/modelApi/common/addSegment';
import { applyTableFormat } from '../../../lib/modelApi/editing/applyTableFormat';
import { ContentModelFormatState, ContentModelSegmentFormat, MergeFormatValueCallbacks } from 'roosterjs-content-model-types';
import { ContentModelFormatState, ContentModelSegmentFormat } from 'roosterjs-content-model-types';
import { createContentModelDocument } from '../../../lib/modelApi/creators/createContentModelDocument';
import { createDivider } from '../../../lib/modelApi/creators/createDivider';
import { createFormatContainer } from '../../../lib/modelApi/creators/createFormatContainer';
Expand Down Expand Up @@ -810,16 +810,13 @@ describe('retrieveModelFormatState', () => {
});
});

it('Different format with callbacks', () => {
it('Returns multiple for conflict format', () => {
const model = createContentModelDocument({});
const result: ContentModelFormatState = {};
const para = createParagraph();
const text1 = createText('test1', { fontFamily: 'Aptos', fontSize: '16pt' });
const text2 = createText('test2', { fontFamily: 'Arial', fontSize: '12pt' });
const text1 = createText('test1', { isBold: true, fontFamily: 'Aptos', fontSize: '16pt' });

Check failure on line 817 in packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type '{ isBold: boolean; fontFamily: string; fontSize: string; }' is not assignable to parameter of type 'Readonly<ContentModelSegmentFormat>'.
const text2 = createText('test2', { isBold: false, fontFamily: 'Arial', fontSize: '12pt' });

Check failure on line 818 in packages/roosterjs-content-model-dom/test/modelApi/editing/retrieveModelFormatStateTest.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type '{ isBold: boolean; fontFamily: string; fontSize: string; }' is not assignable to parameter of type 'Readonly<ContentModelSegmentFormat>'.
para.segments.push(text1, text2);
const callbacks: MergeFormatValueCallbacks = {
fontName: (format, _newValue) => format.fontName = 'Multiple',
};

text1.isSelected = true;
text2.isSelected = true;
Expand All @@ -829,14 +826,14 @@ describe('retrieveModelFormatState', () => {
return false;
});

retrieveModelFormatState(model, null, result, callbacks);
retrieveModelFormatState(model, null, result, 'returnMultiple');

expect(result).toEqual({
isBlockQuote: false,
isBold: false,
isSuperscript: false,
isSubscript: false,
fontName: 'Multiple',
fontSize: 'Multiple',
isCodeInline: false,
canUnlink: false,
canAddImageAltText: false,
Expand Down
2 changes: 1 addition & 1 deletion packages/roosterjs-content-model-types/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ export {
ModelToTextCallbacks,
ModelToTextChecker,
} from './parameter/ModelToTextCallbacks';
export { MergeFormatValueCallback, MergeFormatValueCallbacks } from './parameter/MergeFormatValueCallbacks';
export { ConflictFormatSolution } from './parameter/ConflictFormatSolution';

export { BasePluginEvent, BasePluginDomEvent } from './event/BasePluginEvent';
export { BeforeCutCopyEvent } from './event/BeforeCutCopyEvent';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Specify how to handle conflicts when retrieving format state
* remove: removes the conflicting key from the result
* keepFirst: retains the first value of the conflicting key
* returnMultiple: sets 'Multiple' as the value if the conflicting value's type is string
*/
export type ConflictFormatSolution = 'remove' | 'keepFirst' | 'returnMultiple';

This file was deleted.

0 comments on commit 5290282

Please sign in to comment.