Skip to content

Commit

Permalink
Standalone Editor: Support keyboard input (init step) (#2221)
Browse files Browse the repository at this point in the history
  • Loading branch information
JiuqingSong authored Nov 23, 2023
1 parent 3f7cba1 commit 1e5c3bd
Show file tree
Hide file tree
Showing 6 changed files with 414 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { keyboardDelete } from './keyboardDelete';
import { keyboardInput } from './keyboardInput';
import { PluginEventType } from 'roosterjs-editor-types';
import type { IContentModelEditor } from 'roosterjs-content-model-editor';
import type {
Expand Down Expand Up @@ -72,6 +73,11 @@ export class ContentModelEditPlugin implements EditorPlugin {
// No need to clear cache here since if we rely on browser's behavior, there will be Input event and its handler will reconcile cache
keyboardDelete(editor, rawEvent);
break;

case 'Enter':
default:
keyboardInput(editor, rawEvent);
break;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,18 @@ import {
forwardDeleteCollapsedSelection,
} from './deleteSteps/deleteCollapsedSelection';
import type { IContentModelEditor } from 'roosterjs-content-model-editor';
import type { DeleteSelectionStep } from 'roosterjs-content-model-types';
import type { DOMSelection, DeleteSelectionStep } from 'roosterjs-content-model-types';

/**
* @internal
* Do keyboard event handling for DELETE/BACKSPACE key
* @param editor The Content Model Editor
* @param rawEvent DOM keyboard event
* @returns True if the event is handled with this function, otherwise false
*/
export function keyboardDelete(editor: IContentModelEditor, rawEvent: KeyboardEvent): boolean {
export function keyboardDelete(editor: IContentModelEditor, rawEvent: KeyboardEvent) {
const selection = editor.getDOMSelection();
const range = selection?.type == 'range' ? selection.range : null;
let isDeleted = false;

if (shouldDeleteWithContentModel(range, rawEvent)) {
if (shouldDeleteWithContentModel(selection, rawEvent)) {
editor.formatContentModel(
(model, context) => {
const result = deleteSelection(
Expand All @@ -38,8 +35,6 @@ export function keyboardDelete(editor: IContentModelEditor, rawEvent: KeyboardEv
context
).deleteResult;

isDeleted = result != 'notDeleted';

return handleKeyboardEventResult(editor, model, rawEvent, result, context);
},
{
Expand All @@ -52,8 +47,6 @@ export function keyboardDelete(editor: IContentModelEditor, rawEvent: KeyboardEv

return true;
}

return isDeleted;
}

function getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] {
Expand All @@ -71,13 +64,21 @@ function getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelecti
return [deleteAllSegmentBeforeStep, deleteWordSelection, deleteCollapsedSelection];
}

function shouldDeleteWithContentModel(range: Range | null, rawEvent: KeyboardEvent) {
return !(
range?.collapsed &&
isNodeOfType(range.startContainer, 'TEXT_NODE') &&
!isModifierKey(rawEvent) &&
(canDeleteBefore(rawEvent, range) || canDeleteAfter(rawEvent, range))
);
function shouldDeleteWithContentModel(selection: DOMSelection | null, rawEvent: KeyboardEvent) {
if (!selection) {
return false; // Nothing to delete
} else if (selection.type != 'range' || !selection.range.collapsed) {
return true; // Selection is not collapsed, need to delete all selections
} else {
const range = selection.range;

// When selection is collapsed and is in middle of text node, no need to use Content Model to delete
return !(
isNodeOfType(range.startContainer, 'TEXT_NODE') &&
!isModifierKey(rawEvent) &&
(canDeleteBefore(rawEvent, range) || canDeleteAfter(rawEvent, range))
);
}
}

function canDeleteBefore(rawEvent: KeyboardEvent, range: Range) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { deleteSelection, isModifierKey } from 'roosterjs-content-model-core';
import type { IContentModelEditor } from 'roosterjs-content-model-editor';
import type { DOMSelection } from 'roosterjs-content-model-types';

/**
* @internal
*/
export function keyboardInput(editor: IContentModelEditor, rawEvent: KeyboardEvent) {
const selection = editor.getDOMSelection();

if (shouldInputWithContentModel(selection, rawEvent)) {
editor.addUndoSnapshot();

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

// We have deleted selection then we will let browser to handle the input.
// With this combined operation, we don't wan to mass up the cached model so clear it
context.clearModelCache = true;

// 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;

// Do not preventDefault since we still want browser to handle the final input for now
return result == 'range';
},
{
rawEvent,
}
);

return true;
}
}

function shouldInputWithContentModel(selection: DOMSelection | null, rawEvent: KeyboardEvent) {
if (!selection) {
return false; // Nothing to delete
} else if (
!isModifierKey(rawEvent) &&
(rawEvent.key == 'Enter' || rawEvent.key == 'Space' || rawEvent.key.length == 1)
) {
return selection.type != 'range' || !selection.range.collapsed; // TODO: Also handle Enter key even selection is collapsed
} else {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as keyboardDelete from '../../lib/edit/keyboardDelete';
import * as keyboardInput from '../../lib/edit/keyboardInput';
import { ContentModelEditPlugin } from '../../lib/edit/ContentModelEditPlugin';
import { EntityOperation, PluginEventType } from 'roosterjs-editor-types';
import { IContentModelEditor } from 'roosterjs-content-model-editor';
Expand All @@ -17,9 +18,11 @@ describe('ContentModelEditPlugin', () => {

describe('onPluginEvent', () => {
let keyboardDeleteSpy: jasmine.Spy;
let keyboardInputSpy: jasmine.Spy;

beforeEach(() => {
keyboardDeleteSpy = spyOn(keyboardDelete, 'keyboardDelete').and.returnValue(true);
keyboardDeleteSpy = spyOn(keyboardDelete, 'keyboardDelete');
keyboardInputSpy = spyOn(keyboardInput, 'keyboardInput');
});

it('Backspace', () => {
Expand All @@ -34,6 +37,7 @@ describe('ContentModelEditPlugin', () => {
});

expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent);
expect(keyboardInputSpy).not.toHaveBeenCalled();
});

it('Delete', () => {
Expand All @@ -48,11 +52,15 @@ describe('ContentModelEditPlugin', () => {
});

expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, rawEvent);
expect(keyboardInputSpy).not.toHaveBeenCalled();
});

it('Other key', () => {
const plugin = new ContentModelEditPlugin();
const rawEvent = { which: 41 } as any;
const rawEvent = { which: 41, key: 'A' } as any;
const addUndoSnapshotSpy = jasmine.createSpy('addUndoSnapshot');

editor.addUndoSnapshot = addUndoSnapshotSpy;

plugin.initialize(editor);

Expand All @@ -62,6 +70,7 @@ describe('ContentModelEditPlugin', () => {
});

expect(keyboardDeleteSpy).not.toHaveBeenCalled();
expect(keyboardInputSpy).toHaveBeenCalledWith(editor, rawEvent);
});

it('Default prevented', () => {
Expand All @@ -75,6 +84,7 @@ describe('ContentModelEditPlugin', () => {
});

expect(keyboardDeleteSpy).not.toHaveBeenCalled();
expect(keyboardInputSpy).not.toHaveBeenCalled();
});

it('Trigger entity event first', () => {
Expand Down Expand Up @@ -110,28 +120,7 @@ describe('ContentModelEditPlugin', () => {
expect(keyboardDeleteSpy).toHaveBeenCalledWith(editor, {
key: 'Delete',
} as any);
});

it('SelectionChanged event should clear cached model', () => {
const plugin = new ContentModelEditPlugin();

plugin.initialize(editor);
plugin.onPluginEvent({
eventType: PluginEventType.SelectionChanged,
selectionRangeEx: null!,
});
});

it('keyboardDelete returns false', () => {
const plugin = new ContentModelEditPlugin();

keyboardDeleteSpy.and.returnValue(false);

plugin.initialize(editor);
plugin.onPluginEvent({
eventType: PluginEventType.SelectionChanged,
selectionRangeEx: null!,
});
expect(keyboardInputSpy).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -442,9 +442,8 @@ describe('keyboardDelete', () => {
getDOMSelection: () => range,
} as any;

const result = keyboardDelete(editor, rawEvent);
keyboardDelete(editor, rawEvent);

expect(result).toBeFalse();
expect(formatWithContentModelSpy).not.toHaveBeenCalled();
});

Expand All @@ -464,9 +463,8 @@ describe('keyboardDelete', () => {
getDOMSelection: () => range,
} as any;

const result = keyboardDelete(editor, rawEvent);
keyboardDelete(editor, rawEvent);

expect(result).toBeFalse();
expect(formatWithContentModelSpy).not.toHaveBeenCalled();
});

Expand Down
Loading

0 comments on commit 1e5c3bd

Please sign in to comment.