Skip to content

Commit

Permalink
Merge branch 'master' into u/juliaroldi/list-bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong authored Nov 14, 2024
2 parents 53d1341 + b1bf4e3 commit 7550ba3
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { reducedModelChildProcessor } from '../../modelApi/common/reducedModelChildProcessor';
import { retrieveModelFormatState } from 'roosterjs-content-model-dom';
import type { IEditor, ContentModelFormatState } 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 conflictSolution The strategy for handling format conflicts
*/
export function getFormatState(editor: IEditor): ContentModelFormatState {
export function getFormatState(
editor: IEditor,
conflictSolution: ConflictFormatSolution = 'remove'
): ContentModelFormatState {
const pendingFormat = editor.getPendingFormat();
const manager = editor.getSnapshotsManager();
const result: ContentModelFormatState = {
Expand All @@ -17,7 +21,7 @@ export function getFormatState(editor: IEditor): ContentModelFormatState {

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

return false;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ describe('getFormatState', () => {
canUndo: false,
canRedo: false,
isDarkMode: false,
}
},
'remove'
);
expect(result).toEqual(expectedFormat);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { isBold } from '../../domUtils/style/isBold';
import { iterateSelections } from '../selection/iterateSelections';
import { parseValueWithUnit } from '../../formatHandlers/utils/parseValueWithUnit';
import type {
ConflictFormatSolution,
ContentModelFormatState,
ContentModelSegmentFormat,
ReadonlyContentModelBlockGroup,
Expand All @@ -22,11 +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 conflictSolution The strategy for handling format conflicts
*/
export function retrieveModelFormatState(
model: ReadonlyContentModelDocument,
pendingFormat: ContentModelSegmentFormat | null,
formatState: ContentModelFormatState
formatState: ContentModelFormatState,
conflictSolution: ConflictFormatSolution = 'remove'
) {
let firstTableContext: ReadonlyTableSelectionContext | undefined;
let firstBlock: ReadonlyContentModelBlock | undefined;
Expand All @@ -38,7 +41,7 @@ export function retrieveModelFormatState(
model,
(path, tableContext, block, segments) => {
// Structure formats
retrieveStructureFormat(formatState, path, isFirst);
retrieveStructureFormat(formatState, path, isFirst, conflictSolution);

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

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

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

mergeValue(formatState, 'isCodeInline', !!segment?.code, isFirst);
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 @@ -138,51 +142,55 @@ export function retrieveModelFormatState(
function retrieveSegmentFormat(
result: ContentModelFormatState,
isFirst: boolean,
mergedFormat: ContentModelSegmentFormat
mergedFormat: ContentModelSegmentFormat,
conflictSolution: ConflictFormatSolution = 'remove'
) {
const superOrSubscript = mergedFormat.superOrSubScriptSequence?.split(' ')?.pop();

mergeValue(result, 'isBold', isBold(mergedFormat.fontWeight), isFirst);
mergeValue(result, 'isItalic', mergedFormat.italic, isFirst);
mergeValue(result, 'isUnderline', mergedFormat.underline, isFirst);
mergeValue(result, 'isStrikeThrough', mergedFormat.strikethrough, isFirst);
mergeValue(result, 'isSuperscript', superOrSubscript == 'super', isFirst);
mergeValue(result, 'isSubscript', superOrSubscript == 'sub', isFirst);
mergeValue(result, 'letterSpacing', mergedFormat.letterSpacing, isFirst);
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);
mergeValue(result, 'fontName', mergedFormat.fontFamily, isFirst, conflictSolution);
mergeValue(
result,
'fontSize',
mergedFormat.fontSize,
isFirst,
conflictSolution,
val => parseValueWithUnit(val, undefined, 'pt') + 'pt'
);
mergeValue(result, 'backgroundColor', mergedFormat.backgroundColor, isFirst);
mergeValue(result, 'textColor', mergedFormat.textColor, isFirst);
mergeValue(result, 'fontWeight', mergedFormat.fontWeight, isFirst);
mergeValue(result, 'lineHeight', mergedFormat.lineHeight, isFirst);
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
isFirst: boolean,
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);
mergeValue(result, 'marginTop', paragraph.format.marginTop, isFirst);
mergeValue(result, 'headingLevel', validHeadingLevel, isFirst);
mergeValue(result, 'textAlign', paragraph.format.textAlign, isFirst);
mergeValue(result, 'direction', paragraph.format.direction, isFirst);
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
isFirst: boolean,
conflictSolution: ConflictFormatSolution = 'remove'
) {
const listItemIndex = getClosestAncestorBlockGroupIndex(path, ['ListItem'], []);
const containerIndex = getClosestAncestorBlockGroupIndex(path, ['FormatContainer'], []);
Expand All @@ -191,16 +199,17 @@ function retrieveStructureFormat(
const listItem = path[listItemIndex] as ReadonlyContentModelListItem;
const listType = listItem?.levels[listItem.levels.length - 1]?.listType;

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

mergeValue(
result,
'isBlockQuote',
containerIndex >= 0 &&
(path[containerIndex] as ReadonlyContentModelFormatContainer)?.tagName == 'blockquote',
isFirst
isFirst,
conflictSolution
);
}

Expand Down Expand Up @@ -241,14 +250,28 @@ function mergeValue<K extends keyof ContentModelFormatState>(
key: K,
newValue: ContentModelFormatState[K] | undefined,
isFirst: boolean,
parseFn: (val: ContentModelFormatState[K]) => ContentModelFormatState[K] = val => val
conflictSolution: ConflictFormatSolution = 'remove',
parseFn: (val: ContentModelFormatState[K]) => ContentModelFormatState[K] = val => val,
) {
if (isFirst) {
if (newValue !== undefined) {
format[key] = newValue;
}
} else if (parseFn(newValue) !== parseFn(format[key])) {
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 @@ -809,4 +809,35 @@ describe('retrieveModelFormatState', () => {
canAddImageAltText: false,
});
});

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

text1.isSelected = true;
text2.isSelected = true;

spyOn(iterateSelections, 'iterateSelections').and.callFake((path: any, callback) => {
callback([path], undefined, para, [text1, text2]);
return false;
});

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,
});
});
});
1 change: 1 addition & 0 deletions packages/roosterjs-content-model-types/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ export {
ModelToTextCallbacks,
ModelToTextChecker,
} from './parameter/ModelToTextCallbacks';
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';

0 comments on commit 7550ba3

Please sign in to comment.