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: Fix #1802 Default format is not applied when type in a not-empty line #1805

Merged
merged 17 commits into from
Jun 2, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,14 @@ const styles = require('./ContentModelParagraphView.scss');
export function ContentModelParagraphView(props: { paragraph: ContentModelParagraph }) {
const { paragraph } = props;
const implicitCheckbox = React.useRef<HTMLInputElement>(null);
const zeroFontCheckbox = React.useRef<HTMLInputElement>(null);
const [isImplicit, setIsImplicit] = useProperty(!!paragraph.isImplicit);
const [isZeroFont, setZeroFont] = useProperty(!!paragraph.zeroFontSize);

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

const onChangeZeroFontSize = React.useCallback(() => {
const newValue = zeroFontCheckbox.current.checked;
paragraph.zeroFontSize = newValue;
setZeroFont(newValue);
}, [paragraph, setIsImplicit]);

const getContent = React.useCallback(() => {
return (
<>
Expand All @@ -43,15 +35,6 @@ export function ContentModelParagraphView(props: { paragraph: ContentModelParagr
/>
Implicit
</div>
<div>
<input
type="checkbox"
checked={isZeroFont}
ref={zeroFontCheckbox}
onChange={onChangeZeroFontSize}
/>
Zero font size
</div>
{paragraph.decorator && (
<ContentModelParagraphDecoratorView decorator={paragraph.decorator} />
)}
Expand All @@ -60,14 +43,15 @@ export function ContentModelParagraphView(props: { paragraph: ContentModelParagr
))}
</>
);
}, [
paragraph,
isImplicit,
// headerLevel
]);
}, [paragraph, isImplicit]);

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

return (
Expand Down
3 changes: 2 additions & 1 deletion demo/scripts/controls/sidePane/snapshot/SnapshotPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as React from 'react';
import SidePanePlugin from '../../SidePanePlugin';
import SnapshotPane from './SnapshotPane';
import UndoSnapshots from './UndoSnapshots';
import { ChangeSource, IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types';
import { createSnapshots } from 'roosterjs-editor-dom';
import { IEditor, PluginEvent, PluginEventType } from 'roosterjs-editor-types';
import { Snapshot } from 'roosterjs-editor-types';

export default class SnapshotPlugin implements SidePanePlugin {
Expand Down Expand Up @@ -87,6 +87,7 @@ export default class SnapshotPlugin implements SidePanePlugin {
this.component.snapshotToString(snapshot),
false /*triggerContentChangedEvent*/
);
this.editorInstance.triggerContentChangedEvent(ChangeSource.SetContent);
};

private updateSnapshots = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,47 @@
import { addBlock } from '../../modelApi/common/addBlock';
import { ContentModelBlockGroup } from '../../publicTypes/group/ContentModelBlockGroup';
import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat';
import { ContextStyles } from './formatContainerProcessor';
import { createParagraph } from '../../modelApi/creators/createParagraph';
import { ElementProcessor } from '../../publicTypes/context/ElementProcessor';
import { DomToModelContext } from '../../publicTypes/context/DomToModelContext';
import { parseFormat } from '../utils/parseFormat';

const SegmentDecoratorTags = ['A', 'CODE'];

/**
* @internal
*/
export const blockProcessor: ElementProcessor<HTMLElement> = (group, element, context) => {
export function blockProcessor(
group: ContentModelBlockGroup,
element: HTMLElement,
context: DomToModelContext,
segmentFormat?: ContentModelSegmentFormat
) {
const decorator = context.blockDecorator.tagName ? context.blockDecorator : undefined;
const isSegmentDecorator = SegmentDecoratorTags.indexOf(element.tagName) >= 0;

parseFormat(element, context.formatParsers.block, context.blockFormat, context);

const format = { ...context.blockFormat };
const blockFormat = { ...context.blockFormat };

parseFormat(element, context.formatParsers.container, format, context);
parseFormat(element, context.formatParsers.container, blockFormat, context);

ContextStyles.forEach(style => {
if (format[style]) {
context.blockFormat[style] = format[style];
if (blockFormat[style]) {
context.blockFormat[style] = blockFormat[style];
}
});

if (!isSegmentDecorator) {
const paragraph = createParagraph(false /*isImplicit*/, format, decorator);

if (element.style.fontSize && parseInt(element.style.fontSize) == 0) {
paragraph.zeroFontSize = true;
}
const paragraph = createParagraph(
false /*isImplicit*/,
blockFormat,
segmentFormat,
decorator
);

addBlock(group, paragraph);
}

context.elementProcessors.child(group, element, context);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export const formatContainerProcessor: ElementProcessor<HTMLElement> = (
const paragraph = formatContainer.blocks[0] as ContentModelParagraph;

if (formatContainer.zeroFontSize) {
paragraph.zeroFontSize = true;
paragraph.segmentFormat = Object.assign({}, paragraph.segmentFormat, {
fontSize: '0',
});
}

Object.assign(paragraph.format, formatContainer.format);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { addBlock } from '../../modelApi/common/addBlock';
import { blockProcessor } from './blockProcessor';
import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat';
import { createParagraph } from '../../modelApi/creators/createParagraph';
import { DomToModelContext } from '../../publicTypes/context/DomToModelContext';
import { ElementProcessor } from '../../publicTypes/context/ElementProcessor';
Expand Down Expand Up @@ -46,18 +47,24 @@ export const knownElementProcessor: ElementProcessor<HTMLElement> = (group, elem
const isSegmentDecorator = SegmentDecoratorTags.indexOf(element.tagName) >= 0;

stackFormat(context, { segment: 'shallowCloneForBlock', paragraph: 'shallowClone' }, () => {
parseFormat(
element,
context.formatParsers.segmentOnBlock,
context.segmentFormat,
context
);
const segmentFormat: ContentModelSegmentFormat = {};

parseFormat(element, context.formatParsers.segmentOnBlock, segmentFormat, context);
Object.assign(context.segmentFormat, segmentFormat);

blockProcessor(group, element, context);
blockProcessor(group, element, context, segmentFormat);
});

if (isBlock && !isSegmentDecorator) {
addBlock(group, createParagraph(true /*isImplicit*/, context.blockFormat, decorator));
addBlock(
group,
createParagraph(
true /*isImplicit*/,
context.blockFormat,
undefined /*segmentFormat*/,
decorator
)
);
}
} else {
stackFormat(context, { segment: 'shallowClone', paragraph: 'shallowClone' }, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { addBlock } from '../../modelApi/common/addBlock';
import { blockProcessor } from './blockProcessor';
import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat';
import { createParagraph } from '../../modelApi/creators/createParagraph';
import { createParagraphDecorator } from '../../modelApi/creators/createParagraphDecorator';
import { ElementProcessor } from '../../publicTypes/context/ElementProcessor';
Expand All @@ -13,9 +14,12 @@ export const pProcessor: ElementProcessor<HTMLElement> = (group, element, contex
stackFormat(context, { blockDecorator: 'empty' }, () => {
context.blockDecorator = createParagraphDecorator(element.tagName);

parseFormat(element, context.formatParsers.segmentOnBlock, context.segmentFormat, context);
const segmentFormat: ContentModelSegmentFormat = {};

blockProcessor(group, element, context);
parseFormat(element, context.formatParsers.segmentOnBlock, segmentFormat, context);
Object.assign(context.segmentFormat, segmentFormat);

blockProcessor(group, element, context, segmentFormat);
});

addBlock(group, createParagraph(true /*isImplicit*/, context.blockFormat));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,9 @@
import { ContentModelSegmentFormat } from '../../publicTypes/format/ContentModelSegmentFormat';
import { getPendingFormat } from '../../modelApi/format/pendingFormat';
import { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import { isCharacterValue, Position } from 'roosterjs-editor-dom';
import { setPendingFormat } from '../../modelApi/format/pendingFormat';
import {
EditorPlugin,
IEditor,
PluginEvent,
PluginEventType,
SelectionRangeTypes,
} from 'roosterjs-editor-types';

// During IME input, KeyDown event will have "Process" as key
const ProcessKey = 'Process';
import { EditorPlugin } from 'roosterjs-editor-types';

/**
* ContentModelTypeInContainer plugin helps editor handle keyDown event and make sure typing happens
* under a valid container with default format applied. This is a replacement of original ContentModelTypeInContainer plugin
* Dummy plugin, just to skip original TypeInContainerPlugin's behavior
*/
export default class ContentModelTypeInContainerPlugin implements EditorPlugin {
private editor: IContentModelEditor | null = null;

/**
* Get name of this plugin
*/
Expand All @@ -34,50 +17,12 @@ export default class ContentModelTypeInContainerPlugin implements EditorPlugin {
* editor reference so that it can call to any editor method or format API later.
* @param editor The editor object
*/
initialize(editor: IEditor) {
// TODO: Later we may need a different interface for Content Model editor plugin
this.editor = editor as IContentModelEditor;
}
initialize() {}

/**
* The last method that editor will call to a plugin before it is disposed.
* Plugin can take this chance to clear the reference to editor. After this method is
* called, plugin should not call to any editor method since it will result in error.
*/
dispose() {
this.editor = null;
}

/**
* Core method for a plugin. Once an event happens in editor, editor will call this
* method of each plugin to handle the event as long as the event is not handled
* exclusively by another plugin.
* @param event The event to handle:
*/
onPluginEvent(event: PluginEvent) {
const editor = this.editor;

if (
editor &&
event.eventType == PluginEventType.KeyDown &&
(isCharacterValue(event.rawEvent) || event.rawEvent.key == ProcessKey)
) {
const range = editor.getSelectionRangeEx();

if (
range.type == SelectionRangeTypes.Normal &&
range.ranges[0]?.collapsed &&
!editor.contains(range.ranges[0].startContainer)
) {
const pendingFormat = getPendingFormat(editor) || {};
const defaultFormat = editor.getContentModelDefaultFormat();
const newFormat: ContentModelSegmentFormat = {
...defaultFormat,
...pendingFormat,
};

setPendingFormat(editor, newFormat, Position.getStart(range.ranges[0]));
}
}
}
dispose() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const createContentModelEditorCore: CoreCreator<
new ContentModelEditPlugin(),
],
corePluginOverride: {
typeInContainer: new ContentModelTypeInContainerPlugin(),
typeInContainer: isFeatureEnabled(
options.experimentalFeatures,
ExperimentalFeatures.EditWithContentModel
)
? new ContentModelTypeInContainerPlugin()
: undefined,
copyPaste: new ContentModelCopyPastePlugin(options),
...(options.corePluginOverride || {}),
},
Expand Down
Loading