Skip to content

Commit

Permalink
Fix #2862 (#2886)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong authored Nov 24, 2024
1 parent 64eb364 commit f6b5dca
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 81 deletions.
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 @@ -234,14 +234,12 @@ describe('FormatPlugin for default format', () => {
let getDOMSelection: jasmine.Spy;
let getPendingFormatSpy: jasmine.Spy;
let cacheContentModelSpy: jasmine.Spy;
let takeSnapshotSpy: jasmine.Spy;
let formatContentModelSpy: jasmine.Spy;

beforeEach(() => {
getPendingFormatSpy = jasmine.createSpy('getPendingFormat');
getDOMSelection = jasmine.createSpy('getDOMSelection');
cacheContentModelSpy = jasmine.createSpy('cacheContentModel');
takeSnapshotSpy = jasmine.createSpy('takeSnapshot');
formatContentModelSpy = jasmine.createSpy('formatContentModelSpy');
contentDiv = document.createElement('div');

Expand All @@ -252,7 +250,6 @@ describe('FormatPlugin for default format', () => {
getDOMSelection,
getPendingFormat: getPendingFormatSpy,
cacheContentModel: cacheContentModelSpy,
takeSnapshot: takeSnapshotSpy,
formatContentModel: formatContentModelSpy,
getEnvironment: () => ({}),
} as any) as IEditor;
Expand Down Expand Up @@ -364,7 +361,6 @@ describe('FormatPlugin for default format', () => {
});

expect(context).toEqual({});
expect(takeSnapshotSpy).toHaveBeenCalledTimes(1);
});

it('Collapsed range, IME input, under editor directly', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as deleteSelection from 'roosterjs-content-model-dom/lib/modelApi/editing/deleteSelection';
import * as normalizeContentModel from 'roosterjs-content-model-dom/lib/modelApi/common/normalizeContentModel';
import { applyDefaultFormat } from '../../../lib/corePlugin/format/applyDefaultFormat';
import {
ContentModelDocument,
Expand All @@ -24,8 +23,6 @@ describe('applyDefaultFormat', () => {
let getDOMSelectionSpy: jasmine.Spy;
let formatContentModelSpy: jasmine.Spy;
let deleteSelectionSpy: jasmine.Spy;
let normalizeContentModelSpy: jasmine.Spy;
let takeSnapshotSpy: jasmine.Spy;
let getPendingFormatSpy: jasmine.Spy;
let isNodeInEditorSpy: jasmine.Spy;

Expand All @@ -46,8 +43,6 @@ describe('applyDefaultFormat', () => {

getDOMSelectionSpy = jasmine.createSpy('getDOMSelectionSpy');
deleteSelectionSpy = spyOn(deleteSelection, 'deleteSelection');
normalizeContentModelSpy = spyOn(normalizeContentModel, 'normalizeContentModel');
takeSnapshotSpy = jasmine.createSpy('takeSnapshot');
getPendingFormatSpy = jasmine.createSpy('getPendingFormat');
isNodeInEditorSpy = jasmine.createSpy('isNodeInEditor');

Expand All @@ -71,7 +66,6 @@ describe('applyDefaultFormat', () => {
}),
getDOMSelection: getDOMSelectionSpy,
formatContentModel: formatContentModelSpy,
takeSnapshot: takeSnapshotSpy,
getPendingFormat: getPendingFormatSpy,
} as any;
});
Expand All @@ -82,7 +76,7 @@ describe('applyDefaultFormat', () => {

applyDefaultFormat(editor, defaultFormat);

expect(formatContentModelSpy).toHaveBeenCalled();
expect(formatContentModelSpy).not.toHaveBeenCalled();
});

it('Selection already has style', () => {
Expand All @@ -99,6 +93,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: node,
startOffset: 0,
collapsed: true,
},
});
deleteSelectionSpy.and.returnValue({
Expand All @@ -124,6 +119,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: text,
startOffset: 0,
collapsed: true,
},
});
deleteSelectionSpy.and.returnValue({
Expand All @@ -143,6 +139,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: node,
startOffset: 0,
collapsed: true,
},
});

Expand All @@ -154,9 +151,7 @@ describe('applyDefaultFormat', () => {
applyDefaultFormat(editor, defaultFormat);

expect(formatContentModelSpy).toHaveBeenCalledTimes(1);
expect(normalizeContentModelSpy).toHaveBeenCalledWith(model);
expect(takeSnapshotSpy).toHaveBeenCalledTimes(1);
expect(formatResult).toBeTrue();
expect(formatResult).toBeFalse();
expect(context).toEqual({
deletedEntities: [],
newEntities: [],
Expand All @@ -174,6 +169,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: node,
startOffset: 0,
collapsed: true,
},
});

Expand All @@ -185,8 +181,6 @@ describe('applyDefaultFormat', () => {
applyDefaultFormat(editor, defaultFormat);

expect(formatContentModelSpy).toHaveBeenCalledTimes(1);
expect(normalizeContentModelSpy).not.toHaveBeenCalledWith();
expect(takeSnapshotSpy).not.toHaveBeenCalled();
expect(formatResult).toBeFalse();
expect(context).toEqual({
deletedEntities: [],
Expand All @@ -204,6 +198,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: node,
startOffset: 0,
collapsed: true,
},
});

Expand All @@ -215,8 +210,6 @@ describe('applyDefaultFormat', () => {
applyDefaultFormat(editor, defaultFormat);

expect(formatContentModelSpy).toHaveBeenCalledTimes(1);
expect(normalizeContentModelSpy).not.toHaveBeenCalledWith();
expect(takeSnapshotSpy).not.toHaveBeenCalled();
expect(formatResult).toBeFalse();
expect(context).toEqual({
deletedEntities: [],
Expand Down Expand Up @@ -246,6 +239,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: node,
startOffset: 0,
collapsed: true,
},
});

Expand All @@ -257,8 +251,6 @@ describe('applyDefaultFormat', () => {
applyDefaultFormat(editor, defaultFormat);

expect(formatContentModelSpy).toHaveBeenCalledTimes(1);
expect(normalizeContentModelSpy).not.toHaveBeenCalled();
expect(takeSnapshotSpy).not.toHaveBeenCalled();
expect(formatResult).toBeFalse();
expect(context).toEqual({
deletedEntities: [],
Expand Down Expand Up @@ -288,6 +280,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: node,
startOffset: 0,
collapsed: true,
},
});

Expand All @@ -299,8 +292,6 @@ describe('applyDefaultFormat', () => {
applyDefaultFormat(editor, defaultFormat);

expect(formatContentModelSpy).toHaveBeenCalledTimes(1);
expect(normalizeContentModelSpy).not.toHaveBeenCalled();
expect(takeSnapshotSpy).not.toHaveBeenCalled();
expect(formatResult).toBeFalse();
expect(context).toEqual({
deletedEntities: [],
Expand Down Expand Up @@ -331,6 +322,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: node,
startOffset: 0,
collapsed: true,
},
});

Expand All @@ -342,8 +334,6 @@ describe('applyDefaultFormat', () => {
applyDefaultFormat(editor, defaultFormat);

expect(formatContentModelSpy).toHaveBeenCalledTimes(1);
expect(normalizeContentModelSpy).not.toHaveBeenCalled();
expect(takeSnapshotSpy).not.toHaveBeenCalled();
expect(formatResult).toBeFalse();
expect(context).toEqual({
deletedEntities: [],
Expand Down Expand Up @@ -373,6 +363,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: node,
startOffset: 0,
collapsed: true,
},
});

Expand All @@ -384,8 +375,6 @@ describe('applyDefaultFormat', () => {
applyDefaultFormat(editor, defaultFormat);

expect(formatContentModelSpy).toHaveBeenCalledTimes(1);
expect(normalizeContentModelSpy).not.toHaveBeenCalled();
expect(takeSnapshotSpy).not.toHaveBeenCalled();
expect(formatResult).toBeFalse();
expect(context).toEqual({
deletedEntities: [],
Expand Down Expand Up @@ -419,6 +408,7 @@ describe('applyDefaultFormat', () => {
range: {
startContainer: node,
startOffset: 0,
collapsed: true,
},
});

Expand All @@ -435,8 +425,6 @@ describe('applyDefaultFormat', () => {
applyDefaultFormat(editor, defaultFormat);

expect(formatContentModelSpy).toHaveBeenCalledTimes(1);
expect(normalizeContentModelSpy).not.toHaveBeenCalled();
expect(takeSnapshotSpy).not.toHaveBeenCalled();
expect(formatResult).toBeFalse();
expect(context).toEqual({
deletedEntities: [],
Expand Down
23 changes: 21 additions & 2 deletions packages/roosterjs-content-model-plugins/lib/edit/keyboardInput.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import {
ChangeSource,
createText,
deleteSelection,
isModifierKey,
normalizeContentModel,
} from 'roosterjs-content-model-dom';
import type { DOMSelection, IEditor } from 'roosterjs-content-model-types';
import type { DeleteSelectionStep, DOMSelection, IEditor } from 'roosterjs-content-model-types';

// Insert a ZeroWidthSpace(ZWS) segment with selection before selection marker
// so that later browser will replace this selection with inputted text and keep format
const ZWS = '\u200B';
const insertZWS: DeleteSelectionStep = context => {
if (context.deleteResult == 'range') {
const { marker, paragraph } = context.insertPoint;
const index = paragraph.segments.indexOf(marker);

if (index >= 0) {
const text = createText(ZWS, marker.format, marker.link, marker.code);

text.isSelected = true;

paragraph.segments.splice(index, 0, text);
}
}
};

/**
* @internal
Expand All @@ -17,7 +36,7 @@ export function keyboardInput(editor: IEditor, rawEvent: KeyboardEvent) {

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

// Skip undo snapshot here and add undo snapshot before the operation so that we don't add another undo snapshot in middle of this replace operation
context.skipUndoSnapshot = true;
Expand Down
Loading

0 comments on commit f6b5dca

Please sign in to comment.