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

#2861 #2862 #2875

Closed
wants to merge 3 commits into from
Closed
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
16 changes: 16 additions & 0 deletions demo/scripts/controlsV2/sidePane/eventViewer/EventViewPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,22 @@ export default class ContentModelEventViewPane extends React.Component<
case 'input':
return <span>Input type={event.rawEvent.inputType}</span>;

case 'applyPendingFormat':
return (
<>
<span>
Text={event.text.text}
<br />
</span>
{getObjectKeys(event.format).map(key => (
<span>
{key}:{event.format[key]?.toString() ?? ''}
<br />
</span>
))}
</>
);

default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
PluginWithState,
EditorOptions,
TextColorFormat,
DOMHelper,
} from 'roosterjs-content-model-types';

// During IME input, KeyDown event will have "Process" as key
Expand Down Expand Up @@ -52,6 +53,7 @@
this.state = {
defaultFormat: { ...option.defaultSegmentFormat },
pendingFormat: null,
applyDefaultFormatChecker: option.applyDefaultFormatChecker ?? null,
};

this.defaultFormatKeys = new Set<keyof CSSStyleDeclaration>();
Expand Down Expand Up @@ -118,13 +120,16 @@
break;

case 'keyDown':
const isAndroidIME = this.editor.getEnvironment().isAndroid && event.rawEvent.key == UnidentifiedKey;
const isAndroidIME =
this.editor.getEnvironment().isAndroid && event.rawEvent.key == UnidentifiedKey;
if (isCursorMovingKey(event.rawEvent)) {
this.clearPendingFormat();
this.lastCheckedNode = null;
} else if (
this.defaultFormatKeys.size > 0 &&
(isAndroidIME || isCharacterValue(event.rawEvent) || event.rawEvent.key == ProcessKey) &&
(this.defaultFormatKeys.size > 0 || this.state.applyDefaultFormatChecker) &&
(isAndroidIME ||
isCharacterValue(event.rawEvent) ||
event.rawEvent.key == ProcessKey) &&
this.shouldApplyDefaultFormat(this.editor)
) {
applyDefaultFormat(this.editor, this.state.defaultFormat);
Expand Down Expand Up @@ -186,36 +191,45 @@
this.lastCheckedNode = posContainer;

const domHelper = editor.getDOMHelper();
let element: HTMLElement | null = isNodeOfType(posContainer, 'ELEMENT_NODE')

Check failure on line 194 in packages/roosterjs-content-model-core/lib/corePlugin/format/FormatPlugin.ts

View workflow job for this annotation

GitHub Actions / build

'element' is never reassigned. Use 'const' instead

Check failure on line 194 in packages/roosterjs-content-model-core/lib/corePlugin/format/FormatPlugin.ts

View workflow job for this annotation

GitHub Actions / build

'element' is never reassigned. Use 'const' instead
? posContainer
: posContainer.parentElement;
const foundFormatKeys = new Set<keyof CSSStyleDeclaration>();

while (element?.parentElement && domHelper.isNodeInEditor(element.parentElement)) {
if (element.getAttribute?.('style')) {
const style = element.style;
this.defaultFormatKeys.forEach(key => {
if (style[key]) {
foundFormatKeys.add(key);
}
});

if (foundFormatKeys.size == this.defaultFormatKeys.size) {
return false;

return (
(element && this.state.applyDefaultFormatChecker?.(element, domHelper)) ||
(this.defaultFormatKeys.size > 0 &&
this.cssDefaultFormatChecker(element, domHelper))
);
} else {
return false;
}
}

private cssDefaultFormatChecker(element: HTMLElement | null, domHelper: DOMHelper): boolean {
const foundFormatKeys = new Set<keyof CSSStyleDeclaration>();

while (element?.parentElement && domHelper.isNodeInEditor(element.parentElement)) {
if (element.getAttribute?.('style')) {
const style = element.style;
this.defaultFormatKeys.forEach(key => {
if (style[key]) {
foundFormatKeys.add(key);
}
}
});

if (isBlockElement(element)) {
break;
if (foundFormatKeys.size == this.defaultFormatKeys.size) {
return false;
}
}

element = element.parentElement;
if (isBlockElement(element)) {
break;
}

return true;
} else {
return false;
element = element.parentElement;
}

return true;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { deleteSelection, normalizeContentModel } from 'roosterjs-content-model-dom';
import { iterateSelections } from 'roosterjs-content-model-dom';
import type { ContentModelSegmentFormat, IEditor } from 'roosterjs-content-model-types';

/**
Expand All @@ -8,55 +8,59 @@ import type { ContentModelSegmentFormat, IEditor } from 'roosterjs-content-model
* @param defaultFormat The default segment format to apply
*/
export function applyDefaultFormat(editor: IEditor, defaultFormat: ContentModelSegmentFormat) {
editor.formatContentModel((model, context) => {
const result = deleteSelection(model, [], context);
const selection = editor.getDOMSelection();

if (result.deleteResult == 'range') {
normalizeContentModel(model);
if (selection?.type == 'range' && selection.range.collapsed) {
editor.formatContentModel((model, context) => {
iterateSelections(model, (path, _, paragraph, segments) => {
const marker = segments?.[0];
if (
paragraph?.blockType == 'Paragraph' &&
marker?.segmentType == 'SelectionMarker'
) {
const blocks = path[0].blocks;
const blockCount = blocks.length;
const blockIndex = blocks.indexOf(paragraph);

editor.takeSnapshot();
if (
paragraph.isImplicit &&
paragraph.segments.length == 1 &&
paragraph.segments[0] == marker &&
blockCount > 0 &&
blockIndex == blockCount - 1
) {
// Focus is in the last paragraph which is implicit and there is not other segments.
// This can happen when focus is moved after all other content under current block group.
// We need to check if browser will merge focus into previous paragraph by checking if
// previous block is block. If previous block is paragraph, browser will most likely merge
// the input into previous paragraph, then nothing need to do here. Otherwise we need to
// apply pending format since this input event will start a new real paragraph.
const previousBlock = blocks[blockIndex - 1];

return true;
} else if (result.deleteResult == 'notDeleted' && result.insertPoint) {
const { paragraph, path, marker } = result.insertPoint;
const blocks = path[0].blocks;
const blockCount = blocks.length;
const blockIndex = blocks.indexOf(paragraph);

if (
paragraph.isImplicit &&
paragraph.segments.length == 1 &&
paragraph.segments[0] == marker &&
blockCount > 0 &&
blockIndex == blockCount - 1
) {
// Focus is in the last paragraph which is implicit and there is not other segments.
// This can happen when focus is moved after all other content under current block group.
// We need to check if browser will merge focus into previous paragraph by checking if
// previous block is block. If previous block is paragraph, browser will most likely merge
// the input into previous paragraph, then nothing need to do here. Otherwise we need to
// apply pending format since this input event will start a new real paragraph.
const previousBlock = blocks[blockIndex - 1];

if (previousBlock?.blockType != 'Paragraph') {
context.newPendingFormat = getNewPendingFormat(
editor,
defaultFormat,
marker.format
);
if (previousBlock?.blockType != 'Paragraph') {
context.newPendingFormat = getNewPendingFormat(
editor,
defaultFormat,
marker.format
);
}
} else if (paragraph.segments.every(x => x.segmentType != 'Text')) {
context.newPendingFormat = getNewPendingFormat(
editor,
defaultFormat,
marker.format
);
}
}
} else if (paragraph.segments.every(x => x.segmentType != 'Text')) {
context.newPendingFormat = getNewPendingFormat(
editor,
defaultFormat,
marker.format
);
}
}

// We didn't do any change but just apply default format to pending format, so no need to write back
return false;
});
// Stop searching more selection
return true;
});

// We didn't do any change but just apply default format to pending format, so no need to write back
return false;
});
}
}

function getNewPendingFormat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function applyPendingFormat(

editor.formatContentModel(
(model, context) => {
iterateSelections(model, (_, __, block, segments) => {
iterateSelections(model, (path, _, block, segments) => {
if (
block?.blockType == 'Paragraph' &&
segments?.length == 1 &&
Expand All @@ -45,25 +45,36 @@ export function applyPendingFormat(
previousSegment.text = text.substring(0, text.length - data.length);
});

mutateSegment(block, marker, (marker, block) => {
marker.format = { ...format };
const newText = createText(
data == ANSI_SPACE ? NON_BREAK_SPACE : data,
{
...previousSegment.format,
...format,
}
);
const [mutableParagraph] = mutateSegment(
block,
marker,
(marker, block) => {
marker.format = { ...format };

const newText = createText(
data == ANSI_SPACE ? NON_BREAK_SPACE : data,
{
...previousSegment.format,
...format,
}
);
block.segments.splice(index, 0, newText);
setParagraphNotImplicit(block);
}
);

block.segments.splice(index, 0, newText);
setParagraphNotImplicit(block);
editor.triggerEvent('applyPendingFormat', {
paragraph: mutableParagraph,
text: newText,
path,
format,
});

isChanged = true;
}
}
}

return true;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ describe('formatContentModel', () => {
core.format = {
defaultFormat: {},
pendingFormat: null,
applyDefaultFormatChecker: null,
};

const mockedRange = {
Expand Down
Loading
Loading