Skip to content

Commit

Permalink
Content Model: improve formatWithContentModel 2 (#2002)
Browse files Browse the repository at this point in the history
* Content Model: Improve cache behavior

* fix build

* Content Model: improve formatWithContentModel

* Content Model: improve formatWithContentModel 2

* fix format
  • Loading branch information
JiuqingSong authored Aug 9, 2023
1 parent 6eecbc7 commit 991fed1
Show file tree
Hide file tree
Showing 31 changed files with 613 additions and 756 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { cloneModel } from '../../modelApi/common/cloneModel';
import { contentModelToDom } from 'roosterjs-content-model-dom';
import { deleteSelection } from '../../modelApi/edit/deleteSelection';
import { formatWithContentModel } from '../../publicApi/utils/formatWithContentModel';
import { getOnDeleteEntityCallback } from '../utils/handleKeyboardEventCommon';
import { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import { iterateSelections } from '../../modelApi/selection/iterateSelections';
import type {
Expand All @@ -20,7 +19,6 @@ import {
createRange,
extractClipboardItems,
toArray,
Browser,
wrap,
safeInstanceOf,
} from 'roosterjs-editor-dom';
Expand Down Expand Up @@ -146,11 +144,8 @@ export default class ContentModelCopyPastePlugin implements PluginWithState<Copy
formatWithContentModel(
editor as IContentModelEditor,
'cut',
model => {
deleteSelection(
model,
getOnDeleteEntityCallback(editor as IContentModelEditor)
);
(model, context) => {
deleteSelection(model, [], context);

return true;
},
Expand Down Expand Up @@ -180,7 +175,6 @@ export default class ContentModelCopyPastePlugin implements PluginWithState<Copy
true /*pasteNativeEvent*/
).then((clipboardData: ClipboardData) => {
if (!editor.isDisposed()) {
removeContentForAndroid(editor);
paste(editor, clipboardData);
}
});
Expand Down Expand Up @@ -221,16 +215,11 @@ function cleanUpAndRestoreSelection(tempDiv: HTMLDivElement) {
tempDiv.style.display = 'none';
moveChildNodes(tempDiv);
}

function isClipboardEvent(event: Event): event is ClipboardEvent {
return !!(event as ClipboardEvent).clipboardData;
}
function removeContentForAndroid(editor: IContentModelEditor) {
if (Browser.isAndroid) {
const model = editor.createContentModel();
deleteSelection(model, getOnDeleteEntityCallback(editor));
editor.setContentModel(model);
}
}

function selectionExToRange(
selection: SelectionRangeEx | null,
tempDiv: HTMLDivElement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import { ContentModelSegmentFormat } from 'roosterjs-content-model-types';
import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep';
import { deleteSelection } from '../../modelApi/edit/deleteSelection';
import { formatWithContentModel } from '../../publicApi/utils/formatWithContentModel';
import { getOnDeleteEntityCallback } from '../utils/handleKeyboardEventCommon';
import { getPendingFormat, setPendingFormat } from '../../modelApi/format/pendingFormat';
import { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import { isNodeOfType, normalizeContentModel } from 'roosterjs-content-model-dom';
import {
EditorPlugin,
EntityOperationEvent,
IEditor,
Keys,
NodePosition,
Expand Down Expand Up @@ -38,7 +36,6 @@ const ProcessKey = 'Process';
*/
export default class ContentModelEditPlugin implements EditorPlugin {
private editor: IContentModelEditor | null = null;
private triggeredEntityEvents: EntityOperationEvent[] = [];
private hasDefaultFormat = false;

/**
Expand Down Expand Up @@ -82,10 +79,6 @@ export default class ContentModelEditPlugin implements EditorPlugin {
onPluginEvent(event: PluginEvent) {
if (this.editor) {
switch (event.eventType) {
case PluginEventType.EntityOperation:
this.handleEntityOperationEvent(this.editor, event);
break;

case PluginEventType.KeyDown:
this.handleKeyDownEvent(this.editor, event);
break;
Expand All @@ -99,15 +92,6 @@ export default class ContentModelEditPlugin implements EditorPlugin {
}
}

private handleEntityOperationEvent(editor: IContentModelEditor, event: EntityOperationEvent) {
if (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);
}
}

private handleKeyDownEvent(editor: IContentModelEditor, event: PluginKeyDownEvent) {
const rawEvent = event.rawEvent;
const which = rawEvent.which;
Expand All @@ -125,7 +109,7 @@ export default class ContentModelEditPlugin implements EditorPlugin {
rangeEx.type == SelectionRangeTypes.Normal ? rangeEx.ranges[0] : null;

if (this.shouldDeleteWithContentModel(range, rawEvent)) {
handleKeyDownEvent(editor, rawEvent, this.triggeredEntityEvents);
handleKeyDownEvent(editor, rawEvent);
} else {
editor.cacheContentModel(null);
}
Expand All @@ -144,10 +128,6 @@ export default class ContentModelEditPlugin implements EditorPlugin {
break;
}
}

if (this.triggeredEntityEvents.length > 0) {
this.triggeredEntityEvents = [];
}
}

private tryApplyDefaultFormat(editor: IContentModelEditor) {
Expand All @@ -166,15 +146,8 @@ export default class ContentModelEditPlugin implements EditorPlugin {
}
}

formatWithContentModel(editor, 'input', model => {
const result = deleteSelection(
model,
getOnDeleteEntityCallback(
editor,
undefined /*rawEvent*/,
this.triggeredEntityEvents
)
);
formatWithContentModel(editor, 'input', (model, context) => {
const result = deleteSelection(model, [], context);

if (result.deleteResult == DeleteResult.Range) {
normalizeContentModel(model);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,9 @@
import { ContentModelDocument } from 'roosterjs-content-model-types';
import { DeleteResult, OnDeleteEntity } from '../../modelApi/edit/utils/DeleteSelectionStep';
import { EntityOperationEvent, PluginEventType } from 'roosterjs-editor-types';
import { DeleteResult } from '../../modelApi/edit/utils/DeleteSelectionStep';
import { FormatWithContentModelContext } from '../../publicTypes/parameter/FormatWithContentModelContext';
import { IContentModelEditor } from '../../publicTypes/IContentModelEditor';
import { normalizeContentModel } from 'roosterjs-content-model-dom';

/**
* @internal
*/
export function getOnDeleteEntityCallback(
editor: IContentModelEditor,
rawEvent?: KeyboardEvent,
triggeredEntityEvents: EntityOperationEvent[] = []
): OnDeleteEntity {
return (entity, operation) => {
if (entity.id && entity.type) {
// 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,
});
}
}

// If entity is still in editor and default behavior of event is prevented, that means plugin wants to keep this entity
// Return true to tell caller we should keep it.
return !!rawEvent?.defaultPrevented && editor.contains(entity.wrapper);
};
}
import { PluginEventType } from 'roosterjs-editor-types';

/**
* @internal
Expand All @@ -45,8 +13,11 @@ export function handleKeyboardEventResult(
editor: IContentModelEditor,
model: ContentModelDocument,
rawEvent: KeyboardEvent,
result: DeleteResult
result: DeleteResult,
context: FormatWithContentModelContext
): boolean {
context.skipUndoSnapshot = true;

switch (result) {
case DeleteResult.NotDeleted:
// We have not delete anything, we will let browser handle this event, so clear cached model if any since the content will be changed by browser
Expand All @@ -66,7 +37,7 @@ export function handleKeyboardEventResult(

if (result == DeleteResult.Range) {
// A range is about to be deleted, so add an undo snapshot immediately
editor.addUndoSnapshot();
context.skipUndoSnapshot = false;
}

// Trigger an event to let plugins know the content is about to be changed by Content Model keyboard editing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export {
export { IContentModelEditor, ContentModelEditorOptions } from './publicTypes/IContentModelEditor';
export { InsertPoint } from './publicTypes/selection/InsertPoint';
export { TableSelectionContext } from './publicTypes/selection/TableSelectionContext';
export {
DeletedEntity,
FormatWithContentModelContext,
FormatWithContentModelOptions,
ContentModelFormatter,
} from './publicTypes/parameter/FormatWithContentModelContext';

export { default as insertTable } from './publicApi/table/insertTable';
export { default as formatTable } from './publicApi/table/formatTable';
Expand Down Expand Up @@ -63,6 +69,7 @@ export { default as adjustImageSelection } from './publicApi/image/adjustImageSe
export { default as setParagraphMargin } from './publicApi/block/setParagraphMargin';
export { default as toggleCode } from './publicApi/segment/toggleCode';
export { default as paste } from './publicApi/utils/paste';
export { formatWithContentModel } from './publicApi/utils/formatWithContentModel';

export { default as ContentModelEditor } from './editor/ContentModelEditor';
export { default as isContentModelEditor } from './editor/isContentModelEditor';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { addSegment } from 'roosterjs-content-model-dom';
import { applyTableFormat } from '../table/applyTableFormat';
import { deleteSelection } from '../edit/deleteSelection';
import { FormatWithContentModelContext } from '../../publicTypes/parameter/FormatWithContentModelContext';
import { getClosestAncestorBlockGroupIndex } from './getClosestAncestorBlockGroupIndex';
import { getObjectKeys } from 'roosterjs-editor-dom';
import { InsertPoint } from '../../publicTypes/selection/InsertPoint';
import { normalizeTable } from '../table/normalizeTable';
import { OnDeleteEntity } from '../edit/utils/DeleteSelectionStep';
import {
createListItem,
createParagraph,
Expand Down Expand Up @@ -62,11 +62,11 @@ export interface MergeModelOption {
export function mergeModel(
target: ContentModelDocument,
source: ContentModelDocument,
onDeleteEntity: OnDeleteEntity,
context?: FormatWithContentModelContext,
options?: MergeModelOption
) {
const insertPosition =
options?.insertPosition ?? deleteSelection(target, onDeleteEntity).insertPoint;
options?.insertPosition ?? deleteSelection(target, [], context).insertPoint;

if (insertPosition) {
if (options?.mergeFormat && options.mergeFormat != 'none') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import { ContentModelDocument } from 'roosterjs-content-model-types';
import { deleteExpandedSelection } from './utils/deleteExpandedSelection';
import { FormatWithContentModelContext } from '../../publicTypes/parameter/FormatWithContentModelContext';
import {
DeleteResult,
DeleteSelectionContext,
DeleteSelectionResult,
DeleteSelectionStep,
ValidDeleteSelectionContext,
OnDeleteEntity,
} from './utils/DeleteSelectionStep';

/**
* @internal
*/
export function deleteSelection(
model: ContentModelDocument,
onDeleteEntity: OnDeleteEntity,
additionalSteps: (DeleteSelectionStep | null)[] = []
additionalSteps: (DeleteSelectionStep | null)[] = [],
formatContext?: FormatWithContentModelContext
): DeleteSelectionResult {
const context = deleteExpandedSelection(model, onDeleteEntity);
const context = deleteExpandedSelection(model, formatContext);

additionalSteps.forEach(step => {
if (
step &&
isValidDeleteSelectionContext(context) &&
context.deleteResult == DeleteResult.NotDeleted
) {
step(context, onDeleteEntity);
step(context);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { deleteSegment } from '../utils/deleteSegment';
/**
* @internal
*/
export const deleteAllSegmentBefore: DeleteSelectionStep = (context, onDeleteEntity) => {
export const deleteAllSegmentBefore: DeleteSelectionStep = context => {
const { paragraph, marker } = context.insertPoint;
const index = paragraph.segments.indexOf(marker);

Expand All @@ -13,7 +13,7 @@ export const deleteAllSegmentBefore: DeleteSelectionStep = (context, onDeleteEnt

segment.isSelected = true;

if (deleteSegment(paragraph, segment, onDeleteEntity)) {
if (deleteSegment(paragraph, segment, context.formatContext)) {
context.deleteResult = DeleteResult.Range;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { deleteSegment } from '../utils/deleteSegment';
import { setParagraphNotImplicit } from 'roosterjs-content-model-dom';

function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteSelectionStep {
return (context, onDeleteEntity) => {
return context => {
const isForward = direction == 'forward';
const { paragraph, marker, path, tableContext } = context.insertPoint;
const segments = paragraph.segments;
Expand All @@ -19,7 +19,7 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS
let blockToDelete: BlockAndPath | null;

if (segmentToDelete) {
if (deleteSegment(paragraph, segmentToDelete, onDeleteEntity, direction)) {
if (deleteSegment(paragraph, segmentToDelete, context.formatContext, direction)) {
context.deleteResult = DeleteResult.SingleChar;

// It is possible that we have deleted everything from this paragraph, so we need to mark it as not implicit
Expand All @@ -32,7 +32,7 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS
if (block.blockType == 'Paragraph') {
if (siblingSegment) {
// When selection is under general segment, need to check if it has a sibling sibling, and delete from it
if (deleteSegment(block, siblingSegment, onDeleteEntity, direction)) {
if (deleteSegment(block, siblingSegment, context.formatContext, direction)) {
context.deleteResult = DeleteResult.Range;
}
} else {
Expand All @@ -58,8 +58,8 @@ function getDeleteCollapsedSelection(direction: 'forward' | 'backward'): DeleteS
deleteBlock(
path[0].blocks,
block,
onDeleteEntity,
undefined /*replacement*/,
context.formatContext,
direction
)
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ContentModelEntity, ContentModelParagraph } from 'roosterjs-content-model-types';
import { EntityOperation } from 'roosterjs-editor-types';
import { ContentModelParagraph } from 'roosterjs-content-model-types';
import { FormatWithContentModelContext } from '../../../publicTypes/parameter/FormatWithContentModelContext';
import { InsertPoint } from '../../../publicTypes/selection/InsertPoint';
import { TableSelectionContext } from '../../../publicTypes/selection/TableSelectionContext';
import type { CompatibleEntityOperation } from 'roosterjs-editor-types/lib/compatibleTypes';

/**
* @internal
Expand All @@ -28,6 +27,7 @@ export interface DeleteSelectionResult {
export interface DeleteSelectionContext extends DeleteSelectionResult {
lastParagraph?: ContentModelParagraph;
lastTableContext?: TableSelectionContext;
formatContext?: FormatWithContentModelContext;
}

/**
Expand All @@ -39,27 +39,5 @@ export interface ValidDeleteSelectionContext extends DeleteSelectionContext {

/**
* @internal
* A callback for deleteSelection API to decide how to handle an entity
* @param entity The entity to delete
* @param operation The operation of entity
* @returns True means we want to keep this entity, so deleteSelection() will not remove it. Otherwise false,
* the entity will be removed from Content Model
*/
export type OnDeleteEntity = (
entity: ContentModelEntity,
operation:
| EntityOperation.RemoveFromStart
| EntityOperation.RemoveFromEnd
| EntityOperation.Overwrite
| CompatibleEntityOperation.RemoveFromStart
| CompatibleEntityOperation.RemoveFromEnd
| CompatibleEntityOperation.Overwrite
) => boolean;

/**
* @internal
*/
export type DeleteSelectionStep = (
context: ValidDeleteSelectionContext,
onDeleteEntity: OnDeleteEntity
) => void;
export type DeleteSelectionStep = (context: ValidDeleteSelectionContext) => void;
Loading

0 comments on commit 991fed1

Please sign in to comment.