Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content Model Support PRE and CODE: step 2 #1440

Merged
merged 4 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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