Skip to content

Commit

Permalink
Improve default format behavior (#2406)
Browse files Browse the repository at this point in the history
* apply default format

* improve

* improve

* add test

* improve
  • Loading branch information
JiuqingSong authored Feb 15, 2024
1 parent 55ed264 commit bd5e1b1
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import { applyDefaultFormat } from './utils/applyDefaultFormat';
import { applyPendingFormat } from './utils/applyPendingFormat';
import { getObjectKeys } from 'roosterjs-content-model-dom';
import { getObjectKeys, isBlockElement, isNodeOfType } from 'roosterjs-content-model-dom';
import { isCharacterValue, isCursorMovingKey } from '../publicApi/domUtils/eventUtils';
import type {
BackgroundColorFormat,
FontFamilyFormat,
FontSizeFormat,
FormatPluginState,
IStandaloneEditor,
PluginEvent,
PluginWithState,
StandaloneEditorOptions,
TextColorFormat,
} from 'roosterjs-content-model-types';

// During IME input, KeyDown event will have "Process" as key
const ProcessKey = 'Process';
const DefaultStyleKeyMap: Record<
keyof (FontFamilyFormat & FontSizeFormat & TextColorFormat & BackgroundColorFormat),
keyof CSSStyleDeclaration
> = {
backgroundColor: 'backgroundColor',
textColor: 'color',
fontFamily: 'fontFamily',
fontSize: 'fontSize',
};

/**
* FormatPlugin plugins helps editor to do formatting on top of content model.
Expand All @@ -20,8 +33,9 @@ const ProcessKey = 'Process';
*/
class FormatPlugin implements PluginWithState<FormatPluginState> {
private editor: IStandaloneEditor | null = null;
private hasDefaultFormat = false;
private defaultFormatKeys: Set<keyof CSSStyleDeclaration>;
private state: FormatPluginState;
private lastCheckedNode: Node | null = null;

/**
* Construct a new instance of FormatPlugin class
Expand All @@ -32,6 +46,14 @@ class FormatPlugin implements PluginWithState<FormatPluginState> {
defaultFormat: { ...option.defaultSegmentFormat },
pendingFormat: null,
};

this.defaultFormatKeys = new Set<keyof CSSStyleDeclaration>();

getObjectKeys(DefaultStyleKeyMap).forEach(key => {
if (this.state.defaultFormat[key]) {
this.defaultFormatKeys.add(DefaultStyleKeyMap[key]);
}
});
}

/**
Expand All @@ -49,10 +71,6 @@ class FormatPlugin implements PluginWithState<FormatPluginState> {
*/
initialize(editor: IStandaloneEditor) {
this.editor = editor;
this.hasDefaultFormat =
getObjectKeys(this.state.defaultFormat).filter(
x => typeof this.state.defaultFormat[x] !== 'undefined'
).length > 0;
}

/**
Expand Down Expand Up @@ -102,9 +120,11 @@ class FormatPlugin implements PluginWithState<FormatPluginState> {
case 'keyDown':
if (isCursorMovingKey(event.rawEvent)) {
this.clearPendingFormat();
this.lastCheckedNode = null;
} else if (
this.hasDefaultFormat &&
(isCharacterValue(event.rawEvent) || event.rawEvent.key == ProcessKey)
this.defaultFormatKeys.size > 0 &&
(isCharacterValue(event.rawEvent) || event.rawEvent.key == ProcessKey) &&
this.shouldApplyDefaultFormat(this.editor)
) {
applyDefaultFormat(this.editor, this.state.defaultFormat);
}
Expand All @@ -113,6 +133,8 @@ class FormatPlugin implements PluginWithState<FormatPluginState> {

case 'mouseUp':
case 'contentChanged':
this.lastCheckedNode = null;

if (!this.canApplyPendingFormat()) {
this.clearPendingFormat();
}
Expand Down Expand Up @@ -152,6 +174,48 @@ class FormatPlugin implements PluginWithState<FormatPluginState> {

return result;
}

private shouldApplyDefaultFormat(editor: IStandaloneEditor): boolean {
const selection = editor.getDOMSelection();
const range = selection?.type == 'range' ? selection.range : null;
const posContainer = range?.startContainer ?? null;

if (posContainer && posContainer != this.lastCheckedNode) {
// Cache last checked parent node so no need to check it again if user is keep typing under the same node
this.lastCheckedNode = posContainer;

const domHelper = editor.getDOMHelper();
let element: HTMLElement | null = isNodeOfType(posContainer, 'ELEMENT_NODE')
? 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;
}
}

if (isBlockElement(element)) {
break;
}

element = element.parentElement;
}

return true;
} else {
return false;
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { deleteSelection } from '../../publicApi/selection/deleteSelection';
import { isBlockElement, isNodeOfType, normalizeContentModel } from 'roosterjs-content-model-dom';
import { normalizeContentModel } from 'roosterjs-content-model-dom';
import type { ContentModelSegmentFormat, IStandaloneEditor } from 'roosterjs-content-model-types';

/**
Expand All @@ -12,29 +12,6 @@ export function applyDefaultFormat(
editor: IStandaloneEditor,
defaultFormat: ContentModelSegmentFormat
) {
const selection = editor.getDOMSelection();
const range = selection?.type == 'range' ? selection.range : null;
const posContainer = range?.startContainer ?? null;
const posOffset = range?.startOffset ?? null;

if (posContainer) {
let node: Node | null = posContainer;

while (node && editor.getDOMHelper().isNodeInEditor(node)) {
if (isNodeOfType(node, 'ELEMENT_NODE')) {
if (node.getAttribute?.('style')) {
return;
} else if (isBlockElement(node)) {
break;
}
}

node = node.parentNode;
}
} else {
return;
}

editor.formatContentModel((model, context) => {
const result = deleteSelection(model, [], context);

Expand All @@ -44,12 +21,7 @@ export function applyDefaultFormat(
editor.takeSnapshot();

return true;
} else if (
result.deleteResult == 'notDeleted' &&
result.insertPoint &&
posContainer &&
posOffset !== null
) {
} else if (result.deleteResult == 'notDeleted' && result.insertPoint) {
const { paragraph, path, marker } = result.insertPoint;
const blocks = path[0].blocks;
const blockCount = blocks.length;
Expand Down
Loading

0 comments on commit bd5e1b1

Please sign in to comment.