Skip to content

Commit

Permalink
Content Model Support PRE and CODE: step 2 (#1440)
Browse files Browse the repository at this point in the history
* Support PRE and CODE: step 1

* improve

* Support PRE and CODE step 2
  • Loading branch information
JiuqingSong authored Nov 29, 2022
1 parent 3d74e90 commit e12834e
Show file tree
Hide file tree
Showing 16 changed files with 323 additions and 128 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.modelParagraph {
background-color: #bdf;
}

.modelDecorator {
background-color: #ccf;
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,28 @@
import * as React from 'react';
import { BlockFormatView } from '../format/BlockFormatView';
import { ContentModelParagraph, hasSelectionInBlock } from 'roosterjs-content-model';
import { ContentModelSegmentView } from './ContentModelSegmentView';
import { ContentModelView } from '../ContentModelView';
import { SegmentFormatView } from '../format/SegmentFormatView';
import { useProperty } from '../../hooks/useProperty';
import {
ContentModelParagraph,
ContentModelParagraphDecorator,
hasSelectionInBlock,
} from 'roosterjs-content-model';

const styles = require('./ContentModelParagraphView.scss');

export function ContentModelParagraphView(props: { paragraph: ContentModelParagraph }) {
const { paragraph } = props;
const implicitCheckbox = React.useRef<HTMLInputElement>(null);
const headerLevelDropDown = React.useRef<HTMLSelectElement>(null);
const [value, setValue] = useProperty(!!paragraph.isImplicit);
const [headerLevel, setHeaderLevel] = useProperty((paragraph.header?.headerLevel || '') + '');

const onChange = React.useCallback(() => {
const newValue = implicitCheckbox.current.checked;
paragraph.isImplicit = newValue;
setValue(newValue);
}, [paragraph, setValue]);

const onHeaderLevelChange = React.useCallback(() => {
const newValue = headerLevelDropDown.current.value;

if (paragraph.header) {
paragraph.header.headerLevel = parseInt(newValue);
}
setHeaderLevel(newValue);
}, [paragraph, setHeaderLevel]);

const getContent = React.useCallback(() => {
return (
<>
Expand All @@ -42,29 +35,19 @@ export function ContentModelParagraphView(props: { paragraph: ContentModelParagr
/>
Implicit
</div>
{paragraph.header ? (
<div>
Header level:
<select
value={headerLevel}
ref={headerLevelDropDown}
onChange={onHeaderLevelChange}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
<SegmentFormatView format={paragraph.header.format} />
</div>
) : null}
{paragraph.decorator && (
<ContentModelParagraphDecoratorView decorator={paragraph.decorator} />
)}
{paragraph.segments.map((segment, index) => (
<ContentModelSegmentView segment={segment} key={index} />
))}
</>
);
}, [paragraph, value, headerLevel]);
}, [
paragraph,
value,
// headerLevel
]);

const getFormat = React.useCallback(() => {
return <BlockFormatView format={paragraph.format} />;
Expand All @@ -83,3 +66,48 @@ export function ContentModelParagraphView(props: { paragraph: ContentModelParagr
/>
);
}

function ContentModelParagraphDecoratorView(props: { decorator: ContentModelParagraphDecorator }) {
const { decorator } = props;
const tagNameDropDown = React.useRef<HTMLSelectElement>(null);
const [tagName, setTagName] = useProperty(decorator.tagName || '');

const onTagNameChange = React.useCallback(() => {
const newValue = tagNameDropDown.current.value;

decorator.tagName = newValue;
setTagName(newValue);
}, [decorator, setTagName]);

const getContent = React.useCallback(() => {
return (
<div>
Tag name:
<select value={tagName} ref={tagNameDropDown} onChange={onTagNameChange}>
<option value="p">P</option>
<option value="h1">H1</option>
<option value="h2">H2</option>
<option value="h3">H3</option>
<option value="h4">H4</option>
<option value="h5">H5</option>
<option value="h6">H6</option>
</select>
</div>
);
}, [decorator, tagName]);

const getFormat = React.useCallback(() => {
return <SegmentFormatView format={decorator.format} />;
}, [decorator.format]);

return (
<ContentModelView
title="Decorator"
subTitle={decorator.tagName}
className={styles.modelDecorator}
jsonSource={decorator}
getContent={getContent}
getFormat={getFormat}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { addBlock } from '../../modelApi/common/addBlock';
import { ContentModelHeader } from '../../publicTypes/decorator/ContentModelHeader';
import { ContentModelParagraphDecorator } from '../../publicTypes/decorator/ContentModelParagraphDecorator';
import { createParagraph } from '../../modelApi/creators/createParagraph';
import { DomToModelContext } from '../../publicTypes/context/DomToModelContext';
import { createParagraphDecorator } from '../../modelApi/creators/createParagraphDecorator';
import { ElementProcessor } from '../../publicTypes/context/ElementProcessor';
import { isBlockElement } from '../utils/isBlockElement';
import { parseFormat } from '../utils/parseFormat';
import { safeInstanceOf } from 'roosterjs-editor-dom';
import { stackFormat } from '../utils/stackFormat';

/**
Expand All @@ -30,23 +29,38 @@ export const knownElementProcessor: ElementProcessor<HTMLElement> = (group, elem

if (isBlock) {
parseFormat(element, context.formatParsers.block, context.blockFormat, context);
parseFormat(
element,
context.formatParsers.segmentOnBlock,
context.segmentFormat,
context
);

const paragraph = createParagraph(false /*isImplicit*/, context.blockFormat);
let decorator: ContentModelParagraphDecorator | undefined;

if (safeInstanceOf(element, 'HTMLHeadingElement')) {
// For headers, inline format won't go into its child nodes, so we parse its format here and clear the format of context
paragraph.header = headerProcessor(element, context);

Object.assign(context.segmentFormat, paragraph.header.format);
} else {
parseFormat(
element,
context.formatParsers.segmentOnBlock,
context.segmentFormat,
context
);
switch (element.tagName) {
case 'P':
case 'H1':
case 'H2':
case 'H3':
case 'H4':
case 'H5':
case 'H6':
decorator = createParagraphDecorator(
element.tagName,
context.segmentFormat
);
break;
default:
break;
}

const paragraph = createParagraph(
false /*isImplicit*/,
context.blockFormat,
decorator
);

addBlock(group, paragraph);
} else {
parseFormat(element, context.formatParsers.segment, context.segmentFormat, context);
Expand All @@ -60,20 +74,3 @@ export const knownElementProcessor: ElementProcessor<HTMLElement> = (group, elem
addBlock(group, createParagraph(true /*isImplicit*/, context.blockFormat));
}
};

function headerProcessor(
element: HTMLHeadingElement,
context: DomToModelContext
): ContentModelHeader {
// Parse the header level from tag name
// e.g. "H1" will return 1
const headerLevel = parseInt(element.tagName.substring(1));
const result: ContentModelHeader = {
format: {},
headerLevel,
};

parseFormat(element, context.formatParsers.segmentOnBlock, result.format, context);

return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,8 @@ export const defaultImplicitFormatMap: DefaultImplicitFormatMap = {
fontWeight: 'bold',
fontSize: '0.67em',
},
p: {
marginTop: '1em',
marginBottom: '1em',
},
};
3 changes: 2 additions & 1 deletion packages/roosterjs-content-model/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export { ContentModelGeneralSegment } from './publicTypes/segment/ContentModelGe
export { ContentModelSegment } from './publicTypes/segment/ContentModelSegment';
export { ContentModelEntity } from './publicTypes/entity/ContentModelEntity';
export { ContentModelHR } from './publicTypes/block/ContentModelHR';
export { ContentModelHeader } from './publicTypes/decorator/ContentModelHeader';

export { ContentModelParagraphDecorator } from './publicTypes/decorator/ContentModelParagraphDecorator';
export { ContentModelLink } from './publicTypes/decorator/ContentModelLink';

export { FormatHandlerTypeMap, FormatKey } from './publicTypes/format/FormatHandlerTypeMap';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { ContentModelBlockFormat } from '../../publicTypes/format/ContentModelBlockFormat';
import { ContentModelParagraph } from '../../publicTypes/block/ContentModelParagraph';
import { ContentModelParagraphDecorator } from '../../publicTypes/decorator/ContentModelParagraphDecorator';

/**
* @internal
*/
export function createParagraph(
isImplicit?: boolean,
format?: ContentModelBlockFormat
format?: ContentModelBlockFormat,
decorator?: ContentModelParagraphDecorator
): ContentModelParagraph {
const result: ContentModelParagraph = {
blockType: 'Paragraph',
Expand All @@ -18,5 +20,12 @@ export function createParagraph(
result.isImplicit = true;
}

if (decorator) {
result.decorator = {
tagName: decorator.tagName,
format: { ...decorator.format },
};
}

return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ContentModelParagraphDecorator } from '../../publicTypes/decorator/ContentModelParagraphDecorator';
import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat';

/**
* @internal
*/
export function createParagraphDecorator(
tagName: string,
format?: ContentModelSegmentFormat
): ContentModelParagraphDecorator {
return {
tagName: tagName.toLocaleLowerCase(),
format: { ...(format || {}) },
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,16 @@ export const handleParagraph: ContentModelHandler<ContentModelParagraph> = (
) => {
let container: HTMLElement;

stackFormat(context, paragraph.header ? 'h' + paragraph.header.headerLevel : null, () => {
if (paragraph.header) {
const tag = 'h' + paragraph.header.headerLevel;
stackFormat(context, paragraph.decorator?.tagName || null, () => {
if (paragraph.decorator) {
const { tagName, format } = paragraph.decorator;

container = doc.createElement(tagName);

container = doc.createElement(tag);
parent.appendChild(container);

applyFormat(container, context.formatAppliers.block, paragraph.format, context);
applyFormat(
container,
context.formatAppliers.segmentOnBlock,
paragraph.header.format,
context
);
applyFormat(container, context.formatAppliers.segmentOnBlock, format, context);
} else if (
!paragraph.isImplicit ||
(getObjectKeys(paragraph.format).length > 0 &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ContentModelParagraphDecorator } from '../../publicTypes/decorator/ContentModelParagraphDecorator';
import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat';
import { defaultImplicitFormatMap } from '../../formatHandlers/utils/defaultStyles';
import { formatParagraphWithContentModel } from '../utils/formatParagraphWithContentModel';
Expand All @@ -16,25 +17,24 @@ export default function setHeaderLevel(
headerLevel: 0 | 1 | 2 | 3 | 4 | 5 | 6
) {
formatParagraphWithContentModel(editor, 'setHeaderLevel', para => {
const tag = (headerLevel > 0
? 'h' + headerLevel
: para.header && para.header.headerLevel > 0
? 'h' + para.header.headerLevel
: null) as HeaderLevelTags | null;
const tagName =
headerLevel > 0
? (('h' + headerLevel) as HeaderLevelTags | null)
: getExistingHeaderHeaderTag(para.decorator);
const headerStyle =
((tag && defaultImplicitFormatMap[tag]) as ContentModelSegmentFormat) || {};
(tagName && (defaultImplicitFormatMap[tagName] as ContentModelSegmentFormat)) || {};

if (headerLevel > 0) {
para.header = {
headerLevel,
para.decorator = {
tagName: tagName!,
format: { ...headerStyle },
};

para.segments.forEach(segment => {
Object.assign(segment.format, headerStyle);
});
} else {
delete para.header;
} else if (tagName) {
delete para.decorator;

const headerStyleKeys = getObjectKeys(headerStyle);

Expand All @@ -46,3 +46,12 @@ export default function setHeaderLevel(
}
});
}

function getExistingHeaderHeaderTag(
decorator?: ContentModelParagraphDecorator
): HeaderLevelTags | null {
const tag = decorator?.tagName || '';
const level = parseInt(tag.substring(1));

return level >= 1 && level <= 6 ? (tag as HeaderLevelTags) : null;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ContentModelBlockBase } from './ContentModelBlockBase';
import { ContentModelHeader } from '../decorator/ContentModelHeader';
import { ContentModelParagraphDecorator } from '../decorator/ContentModelParagraphDecorator';
import { ContentModelSegment } from '../segment/ContentModelSegment';

/**
Expand All @@ -14,7 +14,7 @@ export interface ContentModelParagraph extends ContentModelBlockBase<'Paragraph'
/**
* Header info for this paragraph if it is a header
*/
header?: ContentModelHeader;
decorator?: ContentModelParagraphDecorator;

/**
* Whether this block was created from a block HTML element or just some simple segment between other block elements.
Expand Down

This file was deleted.

Loading

0 comments on commit e12834e

Please sign in to comment.