Skip to content

Commit

Permalink
Add support for paragraph line spacing for content model (#1543)
Browse files Browse the repository at this point in the history
* Fetch line height from children

* Create new content model api

* Spacing btn

* Fix tests

* Remove key from roosterjs-react

* testing

* Allow segment to hold lineHeight format

* Remove lineHeight from segments whenever possible

* Fix imports

* Remove normalization

* Add todo for edge case

* Render segment line height
  • Loading branch information
ianeli1 authored Feb 2, 2023
1 parent 0482470 commit 3f75682
Show file tree
Hide file tree
Showing 12 changed files with 305 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
UnderlineFormatRenderer,
SuperOrSubScriptFormatRenderer,
} from './formatPart/BasicFormatRenderers';
import { LineHeightFormatRenderer } from './formatPart/LineHeightFormatRenderer';

const SegmentFormatRenders: FormatRenderer<ContentModelSegmentFormat>[] = [
TextColorFormatRenderer,
Expand All @@ -24,6 +25,7 @@ const SegmentFormatRenders: FormatRenderer<ContentModelSegmentFormat>[] = [
UnderlineFormatRenderer,
StrikeFormatRenderer,
SuperOrSubScriptFormatRenderer,
LineHeightFormatRenderer,
];

export function SegmentFormatView(props: { format: ContentModelSegmentFormat }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
tableMergeButton,
tableSplitButton,
} from './tableEditButtons';
import { spacingButton } from './spacingButton';

const buttons = [
formatPainterButton,
Expand Down Expand Up @@ -96,6 +97,7 @@ const buttons = [
imageBorderStyleButton,
changeImageButton,
imageBoxShadowButton,
spacingButton,
];

export default function ContentModelRibbon(props: { ribbonPlugin: RibbonPlugin; isRtl: boolean }) {
Expand Down
38 changes: 38 additions & 0 deletions demo/scripts/controls/ribbonButtons/contentModel/spacingButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import isContentModelEditor from '../../editor/isContentModelEditor';
import type { RibbonButton } from 'roosterjs-react';
import { setSpacing } from 'roosterjs-content-model';

const SPACING_OPTIONS = ['1.0', '1.15', '1.5', '2.0'];
const NORMAL_SPACING = 1.2;
const spacingButtonKey = 'buttonNameSpacing';

function findClosest(lineHeight?: string) {
if (Number.isNaN(+lineHeight)) {
return '';
}
const query = +lineHeight / NORMAL_SPACING;
return SPACING_OPTIONS.find(opt => Math.abs(query - +opt) < 0.05);
}

/**
* @internal
* "Spacing" button on the format ribbon
*/
export const spacingButton: RibbonButton<typeof spacingButtonKey> = {
key: spacingButtonKey,
unlocalizedText: 'Spacing',
iconName: 'LineSpacing',
dropDownMenu: {
items: SPACING_OPTIONS.reduce((map, size) => {
map[size] = size;
return map;
}, <Record<string, string>>{}),
getSelectedItemKey: formatState => findClosest(formatState.lineHeight),
allowLivePreview: true,
},
onClick: (editor, size) => {
if (isContentModelEditor(editor)) {
setSpacing(editor, +size * NORMAL_SPACING);
}
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ const defaultFormatKeysPerCategory: {
'bold',
'textColor',
'backgroundColor',
'lineHeight',
],
segmentOnBlock: ['fontFamily', 'fontSize', 'underline', 'italic', 'bold', 'textColor'],
segmentOnTableCell: ['fontFamily', 'fontSize', 'underline', 'italic', 'bold'],
Expand Down
1 change: 1 addition & 0 deletions packages/roosterjs-content-model/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export { default as setAlignment } from './publicApi/block/setAlignment';
export { default as setDirection } from './publicApi/block/setDirection';
export { default as setHeaderLevel } from './publicApi/block/setHeaderLevel';
export { default as toggleBlockQuote } from './publicApi/block/toggleBlockQuote';
export { default as setSpacing } from './publicApi/block/setSpacing';
export { default as setImageBorder } from './publicApi/image/setImageBorder';
export { default as setImageBoxShadow } from './publicApi/image/setImageBoxShadow';
export { default as changeImage } from './publicApi/image/changeImage';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ function retrieveFormatStateInternal(
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.isBold = isBold(format.fontWeight);
result.isItalic = format.italic;
Expand Down
16 changes: 16 additions & 0 deletions packages/roosterjs-content-model/lib/publicApi/block/setSpacing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IExperimentalContentModelEditor } from '../../publicTypes/IExperimentalContentModelEditor';
import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel';

export default function setSpacing(
editor: IExperimentalContentModelEditor,
spacing: number | string
) {
formatParagraphWithContentModel(editor, 'setSpacing', paragraph => {
paragraph.format.lineHeight = spacing.toString();
paragraph.segments.forEach(segment => {
if (segment.format.lineHeight) {
delete segment.format.lineHeight;
}
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BoldFormat } from './formatParts/BoldFormat';
import { FontFamilyFormat } from './formatParts/FontFamilyFormat';
import { FontSizeFormat } from './formatParts/FontSizeFormat';
import { ItalicFormat } from './formatParts/ItalicFormat';
import { LineHeightFormat } from './formatParts/LineHeightFormat';
import { StrikeFormat } from './formatParts/StrikeFormat';
import { SuperOrSubScriptFormat } from './formatParts/SuperOrSubScriptFormat';
import { TextColorFormat } from './formatParts/TextColorFormat';
Expand All @@ -19,4 +20,5 @@ export type ContentModelSegmentFormat = TextColorFormat &
ItalicFormat &
UnderlineFormat &
StrikeFormat &
SuperOrSubScriptFormat;
SuperOrSubScriptFormat &
LineHeightFormat;
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('retrieveModelFormatState', () => {
isUnderline: true,
canUnlink: false,
canAddImageAltText: false,
lineHeight: undefined,
};

it('Empty model', () => {
Expand Down Expand Up @@ -329,6 +330,7 @@ describe('retrieveModelFormatState', () => {
isSubscript: false,
canUnlink: false,
canAddImageAltText: false,
lineHeight: undefined,
});
});

Expand Down Expand Up @@ -440,6 +442,7 @@ describe('retrieveModelFormatState', () => {
isItalic: undefined,
isUnderline: undefined,
isStrikeThrough: undefined,
lineHeight: undefined,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { ContentModelDocument } from '../../../lib/publicTypes/group/ContentModelDocument';
import { paragraphTestCommon } from './paragraphTestCommon';
import setSpacing from '../../../lib/publicApi/block/setSpacing';

describe('setSpacing', () => {
function runTest(
model: ContentModelDocument,
result: ContentModelDocument,
spacing: number,
calledTimes: number = 1
) {
paragraphTestCommon(
'setSpacing',
editor => setSpacing(editor, spacing),
model,
result,
calledTimes
);
}

it('empty content', () => {
runTest(
{
blockGroupType: 'Document',
blocks: [],
},
{
blockGroupType: 'Document',
blocks: [],
},
1.5,
0
);
});

it('no selection', () => {
runTest(
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
},
],
},
],
},
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
},
],
},
],
},
1.5,
0
);
});

it('Collapsed selection', () => {
runTest(
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
},
{
segmentType: 'SelectionMarker',
format: {},
isSelected: true,
},
],
},
],
},
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {
lineHeight: '1.5',
},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
},
{
segmentType: 'SelectionMarker',
format: {},
isSelected: true,
},
],
},
],
},
1.5,
1
);
});

it('With selection', () => {
runTest(
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
isSelected: true,
},
],
},
],
},
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {
lineHeight: '1.5',
},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
isSelected: true,
},
],
},
],
},
1.5,
1
);
});

it('Removes line-height from segment children', () => {
runTest(
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {
lineHeight: '123',
},
},
{
segmentType: 'SelectionMarker',
format: {},
isSelected: true,
},
],
},
],
},
{
blockGroupType: 'Document',
blocks: [
{
blockType: 'Paragraph',
format: {
lineHeight: '1.5',
},
segments: [
{
segmentType: 'Text',
text: 'test',
format: {},
},
{
segmentType: 'SelectionMarker',
format: {},
isSelected: true,
},
],
},
],
},
1.5,
1
);
});
});
Loading

0 comments on commit 3f75682

Please sign in to comment.