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

Content model: fix keyboard delete issue on Android #2402

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ export default class ContentModelEventViewPane extends React.Component<
case PluginEventType.BeforeKeyboardEditing:
return <span>Key code={event.rawEvent.which}</span>;

case PluginEventType.Input:
return <span>Input type={event.rawEvent.inputType}</span>;

default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import type {
PluginEvent,
} from 'roosterjs-content-model-types';

const BACKSPACE_KEY = 8;
const DELETE_KEY = 46;

/**
* ContentModel edit plugins helps editor to do editing operation on top of content model.
* This includes:
Expand All @@ -15,6 +18,8 @@ import type {
*/
export class EditPlugin implements EditorPlugin {
private editor: IStandaloneEditor | null = null;
private disposer: (() => void) | null = null;
private shouldHandleNextInputEvent = false;

/**
* Get name of this plugin
Expand All @@ -31,6 +36,13 @@ export class EditPlugin implements EditorPlugin {
*/
initialize(editor: IStandaloneEditor) {
this.editor = editor;
if (editor.getEnvironment().isAndroid) {
this.disposer = this.editor.attachDomEvent({
beforeinput: {
beforeDispatch: e => this.handleBeforeInputEvent(editor, e),
},
});
}
}

/**
Expand All @@ -40,6 +52,8 @@ export class EditPlugin implements EditorPlugin {
*/
dispose() {
this.editor = null;
this.disposer?.();
this.disposer = null;
}

/**
Expand Down Expand Up @@ -70,11 +84,58 @@ export class EditPlugin implements EditorPlugin {
keyboardDelete(editor, rawEvent);
break;

case 'Unidentified':
if (editor.getEnvironment().isAndroid) {
this.shouldHandleNextInputEvent = true;
}
break;

case 'Enter':
default:
keyboardInput(editor, rawEvent);
break;
}
}
}

private handleBeforeInputEvent(editor: IStandaloneEditor, rawEvent: Event) {
// Some Android IMEs doesn't fire correct keydown event for BACKSPACE/DELETE key
// Here we translate input event to BACKSPACE/DELETE keydown event to be compatible with existing logic
if (
!this.shouldHandleNextInputEvent ||
!(rawEvent instanceof InputEvent) ||
rawEvent.defaultPrevented
) {
return;
}
this.shouldHandleNextInputEvent = false;

let handled = false;
switch (rawEvent.inputType) {
case 'deleteContentBackward':
handled = keyboardDelete(
editor,
new KeyboardEvent('keydown', {
key: 'Backspace',
keyCode: BACKSPACE_KEY,
which: BACKSPACE_KEY,
})
);
break;
case 'deleteContentForward':
handled = keyboardDelete(
editor,
new KeyboardEvent('keydown', {
key: 'Delete',
keyCode: DELETE_KEY,
which: DELETE_KEY,
})
);
break;
}

if (handled) {
rawEvent.preventDefault();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import type {
* 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 by content model, otherwise false
*/
export function keyboardDelete(editor: IStandaloneEditor, rawEvent: KeyboardEvent) {
let handled = false;
const selection = editor.getDOMSelection();

if (shouldDeleteWithContentModel(selection, rawEvent)) {
Expand All @@ -39,7 +41,8 @@ export function keyboardDelete(editor: IStandaloneEditor, rawEvent: KeyboardEven
context
).deleteResult;

return handleKeyboardEventResult(editor, model, rawEvent, result, context);
handled = handleKeyboardEventResult(editor, model, rawEvent, result, context);
return handled;
},
{
rawEvent,
Expand All @@ -48,9 +51,9 @@ export function keyboardDelete(editor: IStandaloneEditor, rawEvent: KeyboardEven
apiName: rawEvent.key == 'Delete' ? 'handleDeleteKey' : 'handleBackspaceKey',
}
);

return true;
}

return handled;
}

function getDeleteSteps(rawEvent: KeyboardEvent, isMac: boolean): (DeleteSelectionStep | null)[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
import * as keyboardDelete from '../../lib/edit/keyboardDelete';
import * as keyboardInput from '../../lib/edit/keyboardInput';
import { EditPlugin } from '../../lib/edit/EditPlugin';
import { IStandaloneEditor } from 'roosterjs-content-model-types';
import { DOMEventRecord, IStandaloneEditor } from 'roosterjs-content-model-types';

describe('EditPlugin', () => {
let plugin: EditPlugin;
let editor: IStandaloneEditor;
let eventMap: Record<string, any>;
let attachDOMEventSpy: jasmine.Spy;
let getEnvironmentSpy: jasmine.Spy;

beforeEach(() => {
attachDOMEventSpy = jasmine
.createSpy('attachDOMEvent')
.and.callFake((handlers: Record<string, DOMEventRecord>) => {
eventMap = handlers;
});

getEnvironmentSpy = jasmine.createSpy('getEnvironment').and.returnValue({
isAndroid: true,
});

editor = ({
attachDomEvent: attachDOMEventSpy,
getEnvironment: getEnvironmentSpy,
getDOMSelection: () =>
({
type: -1,
} as any), // Force return invalid range to go through content model code
} as any) as IStandaloneEditor;
});

afterEach(() => {
plugin.dispose();
});

describe('onPluginEvent', () => {
let keyboardDeleteSpy: jasmine.Spy;
let keyboardInputSpy: jasmine.Spy;
Expand All @@ -25,7 +45,7 @@ describe('EditPlugin', () => {
});

it('Backspace', () => {
const plugin = new EditPlugin();
plugin = new EditPlugin();
const rawEvent = { key: 'Backspace' } as any;

plugin.initialize(editor);
Expand All @@ -40,7 +60,7 @@ describe('EditPlugin', () => {
});

it('Delete', () => {
const plugin = new EditPlugin();
plugin = new EditPlugin();
const rawEvent = { key: 'Delete' } as any;

plugin.initialize(editor);
Expand All @@ -55,7 +75,7 @@ describe('EditPlugin', () => {
});

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

Expand All @@ -73,7 +93,7 @@ describe('EditPlugin', () => {
});

it('Default prevented', () => {
const plugin = new EditPlugin();
plugin = new EditPlugin();
const rawEvent = { key: 'Delete', defaultPrevented: true } as any;

plugin.initialize(editor);
Expand All @@ -87,7 +107,7 @@ describe('EditPlugin', () => {
});

it('Trigger entity event first', () => {
const plugin = new EditPlugin();
plugin = new EditPlugin();
const wrapper = 'WRAPPER' as any;

plugin.initialize(editor);
Expand Down Expand Up @@ -122,4 +142,68 @@ describe('EditPlugin', () => {
expect(keyboardInputSpy).not.toHaveBeenCalled();
});
});

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

beforeEach(() => {
keyboardDeleteSpy = spyOn(keyboardDelete, 'keyboardDelete');
});

it('Handle deleteContentBackward event when key is unidentified', () => {
plugin = new EditPlugin();
const rawEvent = { key: 'Unidentified' } as any;

plugin.initialize(editor);

plugin.onPluginEvent({
eventType: 'keyDown',
rawEvent,
});

eventMap.beforeinput.beforeDispatch(
new InputEvent('beforeinput', {
inputType: 'deleteContentBackward',
})
);

expect(keyboardDeleteSpy).toHaveBeenCalledTimes(1);
expect(keyboardDeleteSpy).toHaveBeenCalledWith(
editor,
new KeyboardEvent('keydown', {
key: 'Backspace',
keyCode: 8,
which: 8,
})
);
});

it('Handle deleteContentForward event when key is unidentified', () => {
plugin = new EditPlugin();
const rawEvent = { key: 'Unidentified' } as any;

plugin.initialize(editor);

plugin.onPluginEvent({
eventType: 'keyDown',
rawEvent,
});

eventMap.beforeinput.beforeDispatch(
new InputEvent('beforeinput', {
inputType: 'deleteContentForward',
})
);

expect(keyboardDeleteSpy).toHaveBeenCalledTimes(1);
expect(keyboardDeleteSpy).toHaveBeenCalledWith(
editor,
new KeyboardEvent('keydown', {
key: 'Delete',
keyCode: 46,
which: 46,
})
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('keyboardDelete', () => {

const result = keyboardDelete(editor, mockedEvent);

expect(result).toBeTrue();
expect(result).toBe(expectedDelete == 'range' || expectedDelete == 'singleChar');
},
input,
expectedResult,
Expand Down Expand Up @@ -589,9 +589,8 @@ describe('keyboardDelete', () => {
getDOMSelection: () => range,
} as any;

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

expect(result).toBeTrue();
expect(formatWithContentModelSpy).toHaveBeenCalledTimes(1);
});

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

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

expect(result).toBeTrue();
expect(formatWithContentModelSpy).toHaveBeenCalledTimes(1);
});
});
Loading