Skip to content

Commit

Permalink
Content Model: Improve edit plugin (#1728)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong authored Apr 18, 2023
1 parent e28fef1 commit 077d8da
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 245 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import handleBackspaceKey from '../../publicApi/editing/handleBackspaceKey';
import handleDeleteKey from '../../publicApi/editing/handleDeleteKey';
import { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import { EditorPlugin, IEditor, Keys, PluginEvent, PluginEventType } from 'roosterjs-editor-types';
import {
EditorPlugin,
EntityOperationEvent,
IEditor,
Keys,
PluginEvent,
PluginEventType,
} from 'roosterjs-editor-types';

/**
* ContentModel plugins helps editor to do editing operation on top of content model.
Expand All @@ -11,6 +18,7 @@ import { EditorPlugin, IEditor, Keys, PluginEvent, PluginEventType } from 'roost
*/
export default class ContentModelEditPlugin implements EditorPlugin {
private editor: IContentModelEditor | null = null;
private triggeredEntityEvents: EntityOperationEvent[] = [];

/**
* Get name of this plugin
Expand Down Expand Up @@ -39,43 +47,47 @@ export default class ContentModelEditPlugin implements EditorPlugin {
this.editor = null;
}

/**
* Check if the plugin should handle the given event exclusively.
* Handle an event exclusively means other plugin will not receive this event in
* onPluginEvent method.
* If two plugins will return true in willHandleEventExclusively() for the same event,
* the final result depends on the order of the plugins are added into editor
* @param event The event to check:
*/
willHandleEventExclusively(event: PluginEvent) {
if (
event.eventType == PluginEventType.KeyDown &&
(event.rawEvent.which == Keys.DELETE || event.rawEvent.which == Keys.BACKSPACE)
) {
// TODO: Consider use ContentEditFeature and need to hide other conflict features that are not based on Content Model
return true;
}

return false;
}

/**
* 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) {
if (this.editor && event.eventType == PluginEventType.KeyDown) {
// TODO: Consider use ContentEditFeature and need to hide other conflict features that are not based on Content Model
switch (event.rawEvent.which) {
case Keys.BACKSPACE:
handleBackspaceKey(this.editor, event.rawEvent);
break;
if (this.editor) {
if (
event.eventType == PluginEventType.EntityOperation &&
event.rawEvent?.type == 'keydown'
) {
// If we see an entity operation event triggered from keydown event, it means the event can be triggered from original
// EntityFeatures or EntityPlugin, so we don't need to trigger the same event again from ContentModel.
// TODO: This is a temporary solution. Once Content Model can fully replace Entity Features, we can remove this.
this.triggeredEntityEvents.push(event);
} else if (event.eventType == PluginEventType.KeyDown) {
if (!event.rawEvent.defaultPrevented) {
// TODO: Consider use ContentEditFeature and need to hide other conflict features that are not based on Content Model
switch (event.rawEvent.which) {
case Keys.BACKSPACE:
handleBackspaceKey(
this.editor,
event.rawEvent,
this.triggeredEntityEvents
);
break;

case Keys.DELETE:
handleDeleteKey(
this.editor,
event.rawEvent,
this.triggeredEntityEvents
);
break;
}
}

case Keys.DELETE:
handleDeleteKey(this.editor, event.rawEvent);
break;
if (this.triggeredEntityEvents.length > 0) {
this.triggeredEntityEvents = [];
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChangeSource, PluginEventType } from 'roosterjs-editor-types';
import { ContentModelDocument } from '../../publicTypes/group/ContentModelDocument';
import { EntityOperationEvent, PluginEventType } from 'roosterjs-editor-types';
import { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import { normalizeContentModel } from '../../modelApi/common/normalizeContentModel';
import { OnDeleteEntity } from '../../modelApi/selection/deleteSelections';
Expand All @@ -9,20 +9,26 @@ import { OnDeleteEntity } from '../../modelApi/selection/deleteSelections';
*/
export function getOnDeleteEntityCallback(
editor: IContentModelEditor,
rawEvent: KeyboardEvent
rawEvent: KeyboardEvent,
triggeredEntityEvents: EntityOperationEvent[]
): OnDeleteEntity {
return (entity, operation) => {
if (entity.id && entity.type) {
editor.triggerPluginEvent(PluginEventType.EntityOperation, {
entity: {
id: entity.id,
isReadonly: entity.isReadonly,
type: entity.type,
wrapper: entity.wrapper,
},
operation,
rawEvent: rawEvent,
});
// Only trigger entity operation event when the same event was not triggered before.
// TODO: This is a temporary solution as the event deletion is handled by both original EntityPlugin/EntityFeatures and ContentModel.
// Later when Content Model can fully replace Content Edit Features, we can remove this check.
if (!triggeredEntityEvents.some(x => x.entity.wrapper == entity.wrapper)) {
editor.triggerPluginEvent(PluginEventType.EntityOperation, {
entity: {
id: entity.id,
isReadonly: entity.isReadonly,
type: entity.type,
wrapper: entity.wrapper,
},
operation,
rawEvent: rawEvent,
});
}
}

return rawEvent.defaultPrevented;
Expand All @@ -42,8 +48,6 @@ export function handleKeyboardEventResult(
// We have deleted what we need from content model, no need to let browser keep handling the event
rawEvent.preventDefault();
normalizeContentModel(model);

editor.triggerContentChangedEvent(ChangeSource.Keyboard, rawEvent.which);
} else {
// We didn't delete anything from content model, so browser will handle this event and we need to clear the cache
editor.cacheContentModel(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ChangeSource, EntityOperationEvent } from 'roosterjs-editor-types';
import { deleteSelection } from '../../modelApi/selection/deleteSelections';
import { formatWithContentModel } from '../utils/formatWithContentModel';
import { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
Expand All @@ -9,14 +10,18 @@ import {
/**
* Handle Backspace key event
*/
export default function handleBackspaceKey(editor: IContentModelEditor, rawEvent: KeyboardEvent) {
export default function handleBackspaceKey(
editor: IContentModelEditor,
rawEvent: KeyboardEvent,
triggeredEntityEvents: EntityOperationEvent[]
) {
formatWithContentModel(
editor,
'handleBackspaceKey',
model => {
const { isChanged } = deleteSelection(model, {
direction: 'backward',
onDeleteEntity: getOnDeleteEntityCallback(editor, rawEvent),
onDeleteEntity: getOnDeleteEntityCallback(editor, rawEvent, triggeredEntityEvents),
});

handleKeyboardEventResult(editor, model, rawEvent, isChanged);
Expand All @@ -25,6 +30,8 @@ export default function handleBackspaceKey(editor: IContentModelEditor, rawEvent
},
{
skipUndoSnapshot: true, // No need to add undo snapshot for each key down event. We will trigger a ContentChanged event and let UndoPlugin decide when to add undo snapshot
changeSource: ChangeSource.Keyboard,
getChangeData: () => rawEvent.which,
}
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ChangeSource, EntityOperationEvent } from 'roosterjs-editor-types';
import { deleteSelection } from '../../modelApi/selection/deleteSelections';
import { formatWithContentModel } from '../utils/formatWithContentModel';
import { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
Expand All @@ -9,14 +10,18 @@ import {
/**
* Handle Delete key event
*/
export default function handleDeleteKey(editor: IContentModelEditor, rawEvent: KeyboardEvent) {
export default function handleDeleteKey(
editor: IContentModelEditor,
rawEvent: KeyboardEvent,
triggeredEntityEvents: EntityOperationEvent[]
) {
formatWithContentModel(
editor,
'handleDeleteKey',
model => {
const { isChanged } = deleteSelection(model, {
direction: 'forward',
onDeleteEntity: getOnDeleteEntityCallback(editor, rawEvent),
onDeleteEntity: getOnDeleteEntityCallback(editor, rawEvent, triggeredEntityEvents),
});

handleKeyboardEventResult(editor, model, rawEvent, isChanged);
Expand All @@ -25,6 +30,8 @@ export default function handleDeleteKey(editor: IContentModelEditor, rawEvent: K
},
{
skipUndoSnapshot: true, // No need to add undo snapshot for each key down event. We will trigger a ContentChanged event and let UndoPlugin decide when to add undo snapshot
changeSource: ChangeSource.Keyboard,
getChangeData: () => rawEvent.which,
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ export function formatWithContentModel(

if (skipUndoSnapshot) {
callback();

if (changeSource) {
editor.triggerContentChangedEvent(changeSource, getChangeData?.());
}
} else {
editor.addUndoSnapshot(
callback,
Expand Down
Loading

0 comments on commit 077d8da

Please sign in to comment.