Skip to content

Commit

Permalink
backport fix for IME bug (#745)
Browse files Browse the repository at this point in the history
ref #738
  • Loading branch information
tnmt authored Oct 6, 2020
1 parent 958f4cd commit 3c20cc9
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ class Editor {
this._callbacks = new LifecycleCallbacks(values(CALLBACK_QUEUES));
this._beforeHooks = { toggleMarkup: [] };

this._isComposingOnBlankLine = false;

DEFAULT_TEXT_INPUT_HANDLERS.forEach(handler => this.onTextInput(handler));

this.hasRendered = false;
Expand Down
74 changes: 73 additions & 1 deletion src/js/editor/event-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import SelectionManager from 'mobiledoc-kit/editor/selection-manager';
import Browser from 'mobiledoc-kit/utils/browser';

const ELEMENT_EVENT_TYPES = [
'keydown', 'keyup', 'cut', 'copy', 'paste', 'keypress', 'drop'
'keydown',
'keyup',
'cut',
'copy',
'paste',
'keypress',
'drop',
'compositionstart',
'compositionend',
];

export default class EventManager {
Expand Down Expand Up @@ -146,6 +154,13 @@ export default class EventManager {
event.preventDefault();
}

// Handle carriage returns
if (!key.isEnter() && key.keyCode === 13) {
_textInputHandler.handleNewLine();
editor.handleNewline(event);
return;
}

_textInputHandler.handle(key.toString());
}

Expand All @@ -166,6 +181,10 @@ export default class EventManager {
let range = editor.range;

switch(true) {
// Ignore keydown events when using an IME
case key.isIME(): {
break;
}
// FIXME This should be restricted to only card/atom boundaries
case key.isHorizontalArrowWithoutModifiersOtherThanShift(): {
let newRange;
Expand Down Expand Up @@ -210,6 +229,59 @@ export default class EventManager {
this._updateModifiersFromKey(key, {isDown:false});
}

// The mutation handler interferes with IMEs when composing
// on a blank line. These two event handlers are for suppressing
// mutation handling in this scenario.
compositionstart(event) { // eslint-disable-line
let { editor } = this;
// Ignore compositionstart if not on a blank line
if (editor.range.headMarker) {
return;
}
this._isComposingOnBlankLine = true;

if (editor.post.isBlank) {
editor._insertEmptyMarkupSectionAtCursor();
}

// Stop listening for mutations on Chrome browsers and suppress
// mutations by prepending a character for other browsers.
// The reason why we treat these separately is because
// of the way each browser processes IME inputs.
if (Browser.isChrome()) {
editor.setPlaceholder('');
editor._mutationHandler.stopObserving();
} else {
this._textInputHandler.handle(' ');
}
}

compositionend(event) {
let { editor } = this;

// Ignore compositionend if not composing on blank line
if (!this._isComposingOnBlankLine) {
return;
}
this._isComposingOnBlankLine = false;

// Start listening for mutations on Chrome browsers and
// delete the prepended character introduced by compositionstart
// for other browsers.
if (Browser.isChrome()) {
editor.insertText(event.data);
editor.setPlaceholder(editor.placeholder);
editor._mutationHandler.startObserving();
} else {
let startOfCompositionLine = editor.range.headSection.toPosition(0);
let endOfCompositionLine = editor.range.headSection.toPosition(event.data.length);
editor.run(postEditor => {
postEditor.deleteAtPosition(startOfCompositionLine, 1, { unit: 'char' });
postEditor.setRange(endOfCompositionLine);
});
}
}

cut(event) {
event.preventDefault();

Expand Down
3 changes: 3 additions & 0 deletions src/js/utils/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ export default {
},
isWin() {
return (typeof window !== 'undefined') && window.navigator && /Win/.test(window.navigator.platform);
},
isChrome() {
return (typeof window !== 'undefined') && ('chrome' in window);
}
};
93 changes: 93 additions & 0 deletions tests/acceptance/editor-ime-handler-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Keycodes from 'mobiledoc-kit/utils/keycodes';
import Browser from 'mobiledoc-kit/utils/browser';
import Helpers from '../test-helpers';

let editor, editorElement;

const { test, module } = Helpers;

module('Acceptance: editor: IME Composition Event Handler', {
beforeEach() {
editorElement = $('#editor')[0];
},
afterEach() {
if (editor) { editor.destroy(); }
}
});

['Enter', 'Tab', 'Backspace'].forEach((key) => {
test(`ignore ${key} keydowns when using an IME`, (assert) => {
let { post: expected } = Helpers.postAbstract.buildFromText('你好');
editor = Helpers.editor.buildFromText('你好', { element: editorElement });

Helpers.dom.moveCursorTo(editor, editorElement.firstChild, 1);

Helpers.dom.triggerKeyEvent(editor, 'keydown', {
key,
keyCode: Keycodes.IME,
charCode: Keycodes[key.toUpperCase()]
});

assert.postIsSimilar(editor.post, expected);
});
});

test('ignore horizontal arrow keydowns when using an IME', (assert) => {
editor = Helpers.editor.buildFromText("안녕하세요", { element: editorElement });

Helpers.dom.moveCursorTo(editor, editorElement.firstChild);

Helpers.dom.triggerKeyEvent(editor, 'keydown', {
key: 'ArrowRight',
keyCode: Keycodes.IME,
charCode: Keycodes.RIGHT
});

assert.positionIsEqual(editor.range.head, editor.post.headPosition());

Helpers.dom.moveCursorTo(editor, editorElement.firstChild, 1);

Helpers.dom.triggerKeyEvent(editor, 'keydown', {
key: 'ArrowLeft',
keyCode: Keycodes.IME,
charCode: Keycodes.LEFT
});

assert.positionIsEqual(editor.range.head, editor.post.tailPosition());
});

// There doesn't seem to be a way to directly test the usage
// of an OS-level IME, however this test roughly simulates
// how the IME inputs text into the DOM.
test('test handling of IME composition events', (assert) => {
let done = assert.async();

editor = Helpers.editor.buildFromText("", { element: editorElement });

Helpers.dom.moveCursorTo(editor, editorElement);

editor.element.dispatchEvent(
new CompositionEvent('compositionstart', { 'data': 'n' })
);

Helpers.wait(() => {
if(Browser.isChrome()) {
editorElement.firstChild.innerHTML = "こんにちは";
} else {
editorElement.firstChild.innerHTML += "こんにちは";
}

Helpers.wait(() => {
editor.element.dispatchEvent(
new CompositionEvent('compositionend', { 'data': 'こんにちは' })
);

Helpers.wait(() => {
assert.positionIsEqual(editor.range.head, editor.post.tailPosition());
assert.hasElement('#editor p:contains(こんにちは)');

done();
});
});
});
});

0 comments on commit 3c20cc9

Please sign in to comment.