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

Let Content Model handle ENTER key #2610

Merged
merged 33 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c30c84a
KeyboardEnter
JiuqingSong Apr 29, 2024
9865552
fix comment
JiuqingSong Apr 30, 2024
a8af367
fix test
JiuqingSong Apr 30, 2024
ac1a2c1
improve
JiuqingSong May 1, 2024
18ac906
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 1, 2024
817530f
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 2, 2024
bccb2ed
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 6, 2024
8304dc9
Scroll caret into view when call formatContentModel
JiuqingSong May 6, 2024
6506a6e
Merge branch 'u/jisong/scrollintoview' into u/jisong/keyboardenter
JiuqingSong May 6, 2024
65913c0
scroll caret into view
JiuqingSong May 6, 2024
844e1f7
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 6, 2024
7468a88
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 7, 2024
34f7b06
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 10, 2024
cd6fe10
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 23, 2024
66db4cd
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 29, 2024
a5ef968
Fix build
JiuqingSong May 29, 2024
472436e
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 29, 2024
09938f2
 'master' into u/jisong/keyboardenter
JiuqingSong May 30, 2024
ad9feeb
improve
JiuqingSong May 30, 2024
3a7d92e
fix build
JiuqingSong May 31, 2024
21a0bb0
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong May 31, 2024
fa4c2ff
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong Jun 3, 2024
0698c39
Improve
JiuqingSong Jun 3, 2024
f1dfe80
fix test
JiuqingSong Jun 3, 2024
37f6b4b
add test
JiuqingSong Jun 3, 2024
1f90c24
add test
JiuqingSong Jun 4, 2024
82d4ce3
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong Jun 5, 2024
aaa58f6
improve
JiuqingSong Jun 5, 2024
280345f
do not scroll caret into view for now
JiuqingSong Jun 5, 2024
ff29216
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong Jun 5, 2024
882388a
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong Jun 6, 2024
2ec3650
scroll the view when press enter if necessary
JiuqingSong Jun 6, 2024
b51def0
Merge branch 'master' into u/jisong/keyboardenter
JiuqingSong Jun 6, 2024
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
9 changes: 9 additions & 0 deletions packages/roosterjs-content-model-core/lib/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
CachedElementHandler,
DomToModelOptionForCreateModel,
AnnounceData,
ExperimentalFeature,
} from 'roosterjs-content-model-types';

/**
Expand Down Expand Up @@ -406,6 +407,14 @@ export class Editor implements IEditor {
core.api.announce(core, announceData);
}

/**
* Check if a given feature is enabled
* @param featureName The name of feature to check
*/
isExperimentalFeatureEnabled(featureName: ExperimentalFeature | string): boolean {
return this.getCore().experimentalFeatures.indexOf(featureName) >= 0;
}

/**
* @returns the current EditorCore object
* @throws a standard Error if there's no core object
Expand Down
32 changes: 32 additions & 0 deletions packages/roosterjs-content-model-core/test/editor/EditorTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1116,4 +1116,36 @@ describe('Editor', () => {
expect(resetSpy).toHaveBeenCalledWith();
expect(() => editor.announce(mockedData)).toThrow();
});

it('isExperimentalFeatureEnabled', () => {
const div = document.createElement('div');
const resetSpy = jasmine.createSpy('reset');
const mockedCore = {
plugins: [],
darkColorHandler: {
updateKnownColor: updateKnownColorSpy,
reset: resetSpy,
},
api: {
setContentModel: setContentModelSpy,
},
experimentalFeatures: ['Feature1', 'Feature2'],
} as any;

createEditorCoreSpy.and.returnValue(mockedCore);

const editor = new Editor(div);

const result1 = editor.isExperimentalFeatureEnabled('Feature1');
const result2 = editor.isExperimentalFeatureEnabled('Feature2');
const result3 = editor.isExperimentalFeatureEnabled('Feature3');

expect(result1).toBeTrue();
expect(result2).toBeTrue();
expect(result3).toBeFalse();

editor.dispose();
expect(resetSpy).toHaveBeenCalledWith();
expect(() => editor.isExperimentalFeatureEnabled('Feature4')).toThrow();
});
});
4 changes: 2 additions & 2 deletions packages/roosterjs-content-model-dom/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ export { cacheGetEventData } from './domUtils/event/cacheGetEventData';

export { isBlockGroupOfType } from './modelApi/typeCheck/isBlockGroupOfType';

export { getClosestAncestorBlockGroupIndex } from './modelApi/editing/getClosestAncestorBlockGroupIndex';

export { iterateSelections } from './modelApi/selection/iterateSelections';
export {
getFirstSelectedListItem,
Expand Down Expand Up @@ -136,6 +134,8 @@ export { setTableCellBackgroundColor } from './modelApi/editing/setTableCellBack
export { retrieveModelFormatState } from './modelApi/editing/retrieveModelFormatState';
export { getListStyleTypeFromString } from './modelApi/editing/getListStyleTypeFromString';
export { getSegmentTextFormat } from './modelApi/editing/getSegmentTextFormat';
export { getClosestAncestorBlockGroupIndex } from './modelApi/editing/getClosestAncestorBlockGroupIndex';
export { runEditSteps } from './modelApi/editing/runEditSteps';

export { updateImageMetadata, getImageMetadata } from './modelApi/metadata/updateImageMetadata';
export {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { deleteExpandedSelection } from './deleteExpandedSelection';
import { mutateBlock } from '../common/mutate';
import { runEditSteps } from './runEditSteps';
import type {
DeleteSelectionContext,
DeleteSelectionResult,
DeleteSelectionStep,
FormatContentModelContext,
ReadonlyContentModelDocument,
ValidDeleteSelectionContext,
} from 'roosterjs-content-model-types';

/**
Expand All @@ -22,23 +22,16 @@ export function deleteSelection(
formatContext?: FormatContentModelContext
): DeleteSelectionResult {
const context = deleteExpandedSelection(model, formatContext);
const steps = additionalSteps.filter(
(x: DeleteSelectionStep | null): x is DeleteSelectionStep => !!x
);

additionalSteps.forEach(step => {
if (step && isValidDeleteSelectionContext(context)) {
step(context);
}
});
runEditSteps(steps, context);

mergeParagraphAfterDelete(context);
return context;
}

function isValidDeleteSelectionContext(
context: DeleteSelectionContext
): context is ValidDeleteSelectionContext {
return !!context.insertPoint;
}

// If we end up with multiple paragraphs impacted, we need to merge them
function mergeParagraphAfterDelete(context: DeleteSelectionContext) {
const { insertPoint, deleteResult, lastParagraph, lastTableContext } = context;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type {
DeleteSelectionContext,
DeleteSelectionResult,
DeleteSelectionStep,
ValidDeleteSelectionContext,
} from 'roosterjs-content-model-types';

/**
* Run editing steps on top of a given context object which includes current insert point and previous editing result
* @param steps The editing steps to run
* @param context Context for the editing steps.
*/
export function runEditSteps(steps: DeleteSelectionStep[], context: DeleteSelectionResult) {
steps.forEach(step => {
if (step && isValidDeleteSelectionContext(context)) {
step(context);
}
});
}

function isValidDeleteSelectionContext(
context: DeleteSelectionContext
): context is ValidDeleteSelectionContext {
return !!context.insertPoint;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { keyboardDelete } from './keyboardDelete';
import { keyboardEnter } from './keyboardEnter';
import { keyboardInput } from './keyboardInput';
import { keyboardTab } from './keyboardTab';
import { parseTableCells } from 'roosterjs-content-model-dom';
Expand All @@ -25,6 +26,7 @@ export class EditPlugin implements EditorPlugin {
private disposer: (() => void) | null = null;
private shouldHandleNextInputEvent = false;
private selectionAfterDelete: DOMSelection | null = null;
private handleNormalEnter = false;

/**
* Get name of this plugin
Expand All @@ -41,6 +43,8 @@ export class EditPlugin implements EditorPlugin {
*/
initialize(editor: IEditor) {
this.editor = editor;
this.handleNormalEnter = this.editor.isExperimentalFeatureEnabled('PersistCache');

if (editor.getEnvironment().isAndroid) {
this.disposer = this.editor.attachDomEvent({
beforeinput: {
Expand Down Expand Up @@ -153,6 +157,9 @@ export class EditPlugin implements EditorPlugin {
break;

case 'Enter':
keyboardEnter(editor, rawEvent, this.handleNormalEnter);
break;

default:
keyboardInput(editor, rawEvent);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,88 +1,103 @@
import {
createParagraph,
createSelectionMarker,
unwrapBlock,
getClosestAncestorBlockGroupIndex,
isBlockGroupOfType,
createFormatContainer,
mutateBlock,
} from 'roosterjs-content-model-dom';
import type {
ContentModelFormatContainer,
DeleteSelectionStep,
ReadonlyContentModelBlockGroup,
ReadonlyContentModelFormatContainer,
ReadonlyContentModelParagraph,
ShallowMutableContentModelFormatContainer,
ShallowMutableContentModelParagraph,
} from 'roosterjs-content-model-types';

/**
* @internal
*/
export const deleteEmptyQuote: DeleteSelectionStep = context => {
const { deleteResult } = context;

if (
deleteResult == 'nothingToDelete' ||
deleteResult == 'notDeleted' ||
deleteResult == 'range'
) {
const { insertPoint, formatContext } = context;
const { path } = insertPoint;
const { path, paragraph } = insertPoint;
const rawEvent = formatContext?.rawEvent as KeyboardEvent;
const index = getClosestAncestorBlockGroupIndex(
path,
['FormatContainer', 'ListItem'],
['TableCell']
['FormatContainer'],
['TableCell', 'ListItem']
);
const quote = path[index];

if (quote && quote.blockGroupType === 'FormatContainer' && quote.tagName == 'blockquote') {
const parent = path[index + 1];
const quoteBlockIndex = parent.blocks.indexOf(quote);
const blockQuote = parent.blocks[quoteBlockIndex];
if (
isBlockGroupOfType<ContentModelFormatContainer>(blockQuote, 'FormatContainer') &&
blockQuote.tagName === 'blockquote'

if (isEmptyQuote(quote)) {
unwrapBlock(parent, quote);
rawEvent?.preventDefault();
context.deleteResult = 'range';
} else if (
rawEvent?.key === 'Enter' &&
quote.blocks.indexOf(paragraph) >= 0 &&
isEmptyParagraph(paragraph)
) {
if (isEmptyQuote(blockQuote)) {
unwrapBlock(parent, blockQuote);
rawEvent?.preventDefault();
context.deleteResult = 'range';
} else if (isSelectionOnEmptyLine(blockQuote) && rawEvent?.key === 'Enter') {
insertNewLine(blockQuote, parent, quoteBlockIndex);
rawEvent?.preventDefault();
context.deleteResult = 'range';
}
insertNewLine(mutateBlock(quote), parent, quoteBlockIndex, paragraph);
rawEvent?.preventDefault();
context.deleteResult = 'range';
}
}
}
};

const isEmptyQuote = (quote: ContentModelFormatContainer) => {
const isEmptyQuote = (quote: ReadonlyContentModelFormatContainer) => {
return (
quote.blocks.length === 1 &&
quote.blocks[0].blockType === 'Paragraph' &&
quote.blocks[0].segments.every(
s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br'
)
isEmptyParagraph(quote.blocks[0])
);
};

const isSelectionOnEmptyLine = (quote: ContentModelFormatContainer) => {
const quoteLength = quote.blocks.length;
const lastParagraph = quote.blocks[quoteLength - 1];
if (lastParagraph && lastParagraph.blockType === 'Paragraph') {
return lastParagraph.segments.every(
s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br'
);
}
const isEmptyParagraph = (paragraph: ReadonlyContentModelParagraph) => {
return paragraph.segments.every(
s => s.segmentType === 'SelectionMarker' || s.segmentType === 'Br'
);
};

const insertNewLine = (
quote: ContentModelFormatContainer,
quote: ShallowMutableContentModelFormatContainer,
parent: ReadonlyContentModelBlockGroup,
index: number
quoteIndex: number,
paragraph: ShallowMutableContentModelParagraph
) => {
const quoteLength = quote.blocks.length;
mutateBlock(quote).blocks.splice(quoteLength - 1, 1);
const marker = createSelectionMarker();
const newParagraph = createParagraph(false /* isImplicit */);
newParagraph.segments.push(marker);
mutateBlock(parent).blocks.splice(index + 1, 0, newParagraph);
const paraIndex = quote.blocks.indexOf(paragraph);

if (paraIndex >= 0) {
const mutableParent = mutateBlock(parent);

if (paraIndex < quote.blocks.length - 1) {
const newQuote: ShallowMutableContentModelFormatContainer = createFormatContainer(
quote.tagName,
quote.format
);

newQuote.blocks.push(
...quote.blocks.splice(paraIndex + 1, quote.blocks.length - paraIndex - 1)
);

mutableParent.blocks.splice(quoteIndex + 1, 0, newQuote);
}

mutableParent.blocks.splice(quoteIndex + 1, 0, paragraph);
quote.blocks.splice(paraIndex, 1);

if (quote.blocks.length == 0) {
mutableParent.blocks.splice(quoteIndex, 0);
}
}
};
Loading
Loading