Skip to content

Commit

Permalink
Content Model: Improve getFormatState (#1594)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong authored Feb 23, 2023
1 parent c53ddf6 commit ce3cfcd
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ContentModelBlock } from '../../publicTypes/block/ContentModelBlock';
import { ContentModelBlockGroup } from '../../publicTypes/group/ContentModelBlockGroup';
import { ContentModelDocument } from '../../publicTypes/group/ContentModelDocument';
import { ContentModelListItem } from '../../publicTypes/group/ContentModelListItem';
import { ContentModelParagraph } from '../../publicTypes/block/ContentModelParagraph';
import { ContentModelSegment } from '../../publicTypes/segment/ContentModelSegment';
import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat';
import { FormatState } from 'roosterjs-editor-types';
import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex';
Expand All @@ -18,106 +18,130 @@ export function retrieveModelFormatState(
pendingFormat: ContentModelSegmentFormat | null,
formatState: FormatState
) {
let isFirst = true;
let firstTableContext: TableSelectionContext | undefined;
let firstBlock: ContentModelBlock | undefined;
let isFirst = true;

if (pendingFormat) {
// Pending format
retrieveSegmentFormat(formatState, pendingFormat, isFirst);
}

iterateSelections(
[model],
(path, tableContext, block, segments) => {
if (tableContext && !firstTableContext) {
firstTableContext = tableContext;
// Structure formats
retrieveStructureFormat(formatState, path, isFirst);

// Multiple line format
if (block) {
if (firstBlock) {
formatState.isMultilineSelection = true;
} else {
firstBlock = block;
}
}

if (isFirst) {
if (block?.blockType == 'Paragraph' && segments?.[0]) {
retrieveFormatStateInternal(
formatState,
path,
tableContext,
block,
segments,
pendingFormat
);
} else if (tableContext) {
retrieveTableFormat(tableContext, formatState);
}
if (block?.blockType == 'Paragraph') {
// Paragraph formats
retrieveParagraphFormat(formatState, block, isFirst);

// Segment formats
segments?.forEach(segment => {
if (!pendingFormat) {
retrieveSegmentFormat(formatState, segment.format, isFirst);
}

formatState.canUnlink = formatState.canUnlink || !!segment.link;
formatState.canAddImageAltText =
formatState.canAddImageAltText ||
segments.some(segment => segment.segmentType == 'Image');

isFirst = false;
});

isFirst = false;
} else {
formatState.isMultilineSelection = true;
}

if (tableContext && firstTableContext) {
if (tableContext) {
if (firstTableContext) {
const { table, colIndex, rowIndex } = firstTableContext;

// Merge table format
if (
tableContext.table == table &&
(tableContext.colIndex != colIndex || tableContext.rowIndex != rowIndex)
) {
formatState.canMergeTableCell = true;
formatState.isMultilineSelection = true;
}
} else {
// Table formats
retrieveTableFormat(tableContext, formatState);
firstTableContext = tableContext;
}
}

// TODO: Support Code block in format state for Content Model
},
{
includeListFormatHolder: 'never',
}
);
}

function retrieveFormatStateInternal(
function retrieveSegmentFormat(
result: FormatState,
path: ContentModelBlockGroup[],
tableContext: TableSelectionContext | undefined,
paragraph: ContentModelParagraph,
segments: ContentModelSegment[],
pendingFormat: ContentModelSegmentFormat | null
format: ContentModelSegmentFormat,
isFirst: boolean
) {
const segment = segments[0];
const format = pendingFormat || segment.format;
const superOrSubscript = format.superOrSubScriptSequence?.split(' ')?.pop();
const listItemIndex = getClosestAncestorBlockGroupIndex(path, ['ListItem'], []);
const quoteIndex = getClosestAncestorBlockGroupIndex(path, ['Quote'], []);
const headerLevel = parseInt((paragraph.decorator?.tagName || '').substring(1));
mergeValue(result, 'isBold', isBold(format.fontWeight), isFirst);
mergeValue(result, 'isItalic', format.italic, isFirst);
mergeValue(result, 'isUnderline', format.underline, isFirst);
mergeValue(result, 'isStrikeThrough', format.strikethrough, isFirst);
mergeValue(result, 'isSuperscript', superOrSubscript == 'super', isFirst);
mergeValue(result, 'isSubscript', superOrSubscript == 'sub', isFirst);

mergeValue(result, 'fontName', format.fontFamily, isFirst);
mergeValue(result, 'fontSize', format.fontSize, isFirst);
mergeValue(result, 'backgroundColor', format.backgroundColor, isFirst);
mergeValue(result, 'textColor', format.textColor, isFirst);

result.fontName = format.fontFamily;
result.fontSize = format.fontSize;
result.backgroundColor = format.backgroundColor;
result.textColor = format.textColor;
//TODO: handle block owning segments with different line-heights
result.lineHeight = paragraph.format.lineHeight || format.lineHeight;
result.marginBottom = paragraph.format.marginBottom;
result.marginTop = paragraph.format.marginTop;
mergeValue(result, 'lineHeight', format.lineHeight, isFirst);
}

result.isBold = isBold(format.fontWeight);
result.isItalic = format.italic;
result.isUnderline = format.underline;
result.isStrikeThrough = format.strikethrough;
result.isSuperscript = superOrSubscript == 'super';
result.isSubscript = superOrSubscript == 'sub';
function retrieveParagraphFormat(
result: FormatState,
paragraph: ContentModelParagraph,
isFirst: boolean
) {
const headerLevel = parseInt((paragraph.decorator?.tagName || '').substring(1));
const validHeaderLevel = headerLevel >= 1 && headerLevel <= 6 ? headerLevel : undefined;

result.canUnlink = !!segment.link;
result.canAddImageAltText = segments.some(segment => segment.segmentType == 'Image');
mergeValue(result, 'marginBottom', paragraph.format.marginBottom, isFirst);
mergeValue(result, 'marginTop', paragraph.format.marginTop, isFirst);
mergeValue(result, 'headerLevel', validHeaderLevel, isFirst);
}

function retrieveStructureFormat(
result: FormatState,
path: ContentModelBlockGroup[],
isFirst: boolean
) {
const listItemIndex = getClosestAncestorBlockGroupIndex(path, ['ListItem'], []);
const quoteIndex = getClosestAncestorBlockGroupIndex(path, ['Quote'], []);

if (listItemIndex >= 0) {
const listItem = path[listItemIndex] as ContentModelListItem;
const listType = listItem?.levels[listItem.levels.length - 1]?.listType;

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

if (quoteIndex >= 0) {
result.isBlockQuote = true;
}

if (headerLevel >= 1 && headerLevel <= 6) {
result.headerLevel = headerLevel;
}

if (tableContext) {
retrieveTableFormat(tableContext, result);
}

// TODO: Support Code block in format state for Content Model
mergeValue(result, 'isBlockQuote', quoteIndex >= 0, isFirst);
}

function retrieveTableFormat(tableContext: TableSelectionContext, result: FormatState) {
Expand All @@ -130,3 +154,18 @@ function retrieveTableFormat(tableContext: TableSelectionContext, result: Format
result.tableFormat = tableFormat;
}
}

function mergeValue<K extends keyof FormatState>(
format: FormatState,
key: K,
newValue: FormatState[K] | undefined,
isFirst: boolean
) {
if (isFirst) {
if (newValue !== undefined) {
format[key] = newValue;
}
} else if (newValue !== format[key]) {
delete format[key];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ describe('retrieveModelFormatState', () => {
isUnderline: true,
canUnlink: false,
canAddImageAltText: false,
lineHeight: undefined,
marginTop: undefined,
marginBottom: undefined,
};

it('Empty model', () => {
Expand All @@ -67,7 +64,7 @@ describe('retrieveModelFormatState', () => {

retrieveModelFormatState(model, null, result);

expect(result).toEqual(baseFormatResult);
expect(result).toEqual({ ...baseFormatResult, isBlockQuote: false });
});

it('Single selection with list', () => {
Expand All @@ -88,6 +85,7 @@ describe('retrieveModelFormatState', () => {
...baseFormatResult,
isBullet: false,
isNumbering: true,
isBlockQuote: false,
});
});

Expand Down Expand Up @@ -130,6 +128,7 @@ describe('retrieveModelFormatState', () => {
expect(result).toEqual({
...baseFormatResult,
headerLevel: 1,
isBlockQuote: false,
});
});

Expand All @@ -153,6 +152,7 @@ describe('retrieveModelFormatState', () => {
expect(result).toEqual({
...baseFormatResult,
...paraFormat,
isBlockQuote: false,
});
});

Expand Down Expand Up @@ -187,6 +187,7 @@ describe('retrieveModelFormatState', () => {
...baseFormatResult,
isInTable: true,
tableHasHeader: false,
isBlockQuote: false,
});
});

Expand Down Expand Up @@ -222,6 +223,7 @@ describe('retrieveModelFormatState', () => {
...baseFormatResult,
isInTable: true,
tableHasHeader: false,
isBlockQuote: false,
tableFormat: {
topBorderColor: '#ABABAB',
bottomBorderColor: '#ABABAB',
Expand Down Expand Up @@ -258,6 +260,7 @@ describe('retrieveModelFormatState', () => {
expect(result).toEqual({
isInTable: true,
tableHasHeader: true,
isBlockQuote: false,
});
});

Expand All @@ -278,7 +281,10 @@ describe('retrieveModelFormatState', () => {
retrieveModelFormatState(model, null, result);

expect(result).toEqual({
...baseFormatResult,
canAddImageAltText: false,
canUnlink: false,
isBlockQuote: false,
isSubscript: false,
isMultilineSelection: true,
});
});
Expand All @@ -301,6 +307,7 @@ describe('retrieveModelFormatState', () => {
expect(result).toEqual({
...baseFormatResult,
isMultilineSelection: true,
isBlockQuote: false,
});
});

Expand All @@ -320,7 +327,9 @@ describe('retrieveModelFormatState', () => {
retrieveModelFormatState(model, null, result);

expect(result).toEqual({
...baseFormatResult,
isMultilineSelection: true,
isBlockQuote: false,
});
});

Expand All @@ -344,20 +353,14 @@ describe('retrieveModelFormatState', () => {

expect(result).toEqual({
fontName: 'Test',
fontSize: undefined,
backgroundColor: 'blue',
textColor: 'block',
isBold: false,
isItalic: undefined,
isUnderline: undefined,
isStrikeThrough: undefined,
isSuperscript: false,
isSubscript: false,
canUnlink: false,
canAddImageAltText: false,
lineHeight: undefined,
marginTop: undefined,
marginBottom: undefined,
isBlockQuote: false,
});
});

Expand All @@ -378,6 +381,7 @@ describe('retrieveModelFormatState', () => {
expect(result).toEqual({
isInTable: true,
tableHasHeader: false,
isBlockQuote: false,
});
});

Expand All @@ -401,6 +405,7 @@ describe('retrieveModelFormatState', () => {
tableHasHeader: false,
isMultilineSelection: true,
canMergeTableCell: true,
isBlockQuote: false,
});
});

Expand Down Expand Up @@ -435,6 +440,12 @@ describe('retrieveModelFormatState', () => {
tableHasHeader: false,
isMultilineSelection: true,
canMergeTableCell: true,
isBold: false,
isSuperscript: false,
isSubscript: false,
canUnlink: false,
canAddImageAltText: false,
isBlockQuote: false,
});
});

Expand Down Expand Up @@ -462,16 +473,7 @@ describe('retrieveModelFormatState', () => {
canAddImageAltText: false,
isInTable: true,
tableHasHeader: false,
fontName: undefined,
fontSize: undefined,
backgroundColor: undefined,
textColor: undefined,
isItalic: undefined,
isUnderline: undefined,
isStrikeThrough: undefined,
lineHeight: undefined,
marginTop: undefined,
marginBottom: undefined,
isBlockQuote: false,
});
});
});

0 comments on commit ce3cfcd

Please sign in to comment.