Skip to content

Commit 3c20cc9

Browse files
authored
backport fix for IME bug (#745)
ref #738
1 parent 958f4cd commit 3c20cc9

File tree

4 files changed

+171
-1
lines changed

4 files changed

+171
-1
lines changed

src/js/editor/editor.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ class Editor {
148148
this._callbacks = new LifecycleCallbacks(values(CALLBACK_QUEUES));
149149
this._beforeHooks = { toggleMarkup: [] };
150150

151+
this._isComposingOnBlankLine = false;
152+
151153
DEFAULT_TEXT_INPUT_HANDLERS.forEach(handler => this.onTextInput(handler));
152154

153155
this.hasRendered = false;

src/js/editor/event-manager.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@ import SelectionManager from 'mobiledoc-kit/editor/selection-manager';
1111
import Browser from 'mobiledoc-kit/utils/browser';
1212

1313
const ELEMENT_EVENT_TYPES = [
14-
'keydown', 'keyup', 'cut', 'copy', 'paste', 'keypress', 'drop'
14+
'keydown',
15+
'keyup',
16+
'cut',
17+
'copy',
18+
'paste',
19+
'keypress',
20+
'drop',
21+
'compositionstart',
22+
'compositionend',
1523
];
1624

1725
export default class EventManager {
@@ -146,6 +154,13 @@ export default class EventManager {
146154
event.preventDefault();
147155
}
148156

157+
// Handle carriage returns
158+
if (!key.isEnter() && key.keyCode === 13) {
159+
_textInputHandler.handleNewLine();
160+
editor.handleNewline(event);
161+
return;
162+
}
163+
149164
_textInputHandler.handle(key.toString());
150165
}
151166

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

168183
switch(true) {
184+
// Ignore keydown events when using an IME
185+
case key.isIME(): {
186+
break;
187+
}
169188
// FIXME This should be restricted to only card/atom boundaries
170189
case key.isHorizontalArrowWithoutModifiersOtherThanShift(): {
171190
let newRange;
@@ -210,6 +229,59 @@ export default class EventManager {
210229
this._updateModifiersFromKey(key, {isDown:false});
211230
}
212231

232+
// The mutation handler interferes with IMEs when composing
233+
// on a blank line. These two event handlers are for suppressing
234+
// mutation handling in this scenario.
235+
compositionstart(event) { // eslint-disable-line
236+
let { editor } = this;
237+
// Ignore compositionstart if not on a blank line
238+
if (editor.range.headMarker) {
239+
return;
240+
}
241+
this._isComposingOnBlankLine = true;
242+
243+
if (editor.post.isBlank) {
244+
editor._insertEmptyMarkupSectionAtCursor();
245+
}
246+
247+
// Stop listening for mutations on Chrome browsers and suppress
248+
// mutations by prepending a character for other browsers.
249+
// The reason why we treat these separately is because
250+
// of the way each browser processes IME inputs.
251+
if (Browser.isChrome()) {
252+
editor.setPlaceholder('');
253+
editor._mutationHandler.stopObserving();
254+
} else {
255+
this._textInputHandler.handle(' ');
256+
}
257+
}
258+
259+
compositionend(event) {
260+
let { editor } = this;
261+
262+
// Ignore compositionend if not composing on blank line
263+
if (!this._isComposingOnBlankLine) {
264+
return;
265+
}
266+
this._isComposingOnBlankLine = false;
267+
268+
// Start listening for mutations on Chrome browsers and
269+
// delete the prepended character introduced by compositionstart
270+
// for other browsers.
271+
if (Browser.isChrome()) {
272+
editor.insertText(event.data);
273+
editor.setPlaceholder(editor.placeholder);
274+
editor._mutationHandler.startObserving();
275+
} else {
276+
let startOfCompositionLine = editor.range.headSection.toPosition(0);
277+
let endOfCompositionLine = editor.range.headSection.toPosition(event.data.length);
278+
editor.run(postEditor => {
279+
postEditor.deleteAtPosition(startOfCompositionLine, 1, { unit: 'char' });
280+
postEditor.setRange(endOfCompositionLine);
281+
});
282+
}
283+
}
284+
213285
cut(event) {
214286
event.preventDefault();
215287

src/js/utils/browser.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ export default {
44
},
55
isWin() {
66
return (typeof window !== 'undefined') && window.navigator && /Win/.test(window.navigator.platform);
7+
},
8+
isChrome() {
9+
return (typeof window !== 'undefined') && ('chrome' in window);
710
}
811
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import Keycodes from 'mobiledoc-kit/utils/keycodes';
2+
import Browser from 'mobiledoc-kit/utils/browser';
3+
import Helpers from '../test-helpers';
4+
5+
let editor, editorElement;
6+
7+
const { test, module } = Helpers;
8+
9+
module('Acceptance: editor: IME Composition Event Handler', {
10+
beforeEach() {
11+
editorElement = $('#editor')[0];
12+
},
13+
afterEach() {
14+
if (editor) { editor.destroy(); }
15+
}
16+
});
17+
18+
['Enter', 'Tab', 'Backspace'].forEach((key) => {
19+
test(`ignore ${key} keydowns when using an IME`, (assert) => {
20+
let { post: expected } = Helpers.postAbstract.buildFromText('你好');
21+
editor = Helpers.editor.buildFromText('你好', { element: editorElement });
22+
23+
Helpers.dom.moveCursorTo(editor, editorElement.firstChild, 1);
24+
25+
Helpers.dom.triggerKeyEvent(editor, 'keydown', {
26+
key,
27+
keyCode: Keycodes.IME,
28+
charCode: Keycodes[key.toUpperCase()]
29+
});
30+
31+
assert.postIsSimilar(editor.post, expected);
32+
});
33+
});
34+
35+
test('ignore horizontal arrow keydowns when using an IME', (assert) => {
36+
editor = Helpers.editor.buildFromText("안녕하세요", { element: editorElement });
37+
38+
Helpers.dom.moveCursorTo(editor, editorElement.firstChild);
39+
40+
Helpers.dom.triggerKeyEvent(editor, 'keydown', {
41+
key: 'ArrowRight',
42+
keyCode: Keycodes.IME,
43+
charCode: Keycodes.RIGHT
44+
});
45+
46+
assert.positionIsEqual(editor.range.head, editor.post.headPosition());
47+
48+
Helpers.dom.moveCursorTo(editor, editorElement.firstChild, 1);
49+
50+
Helpers.dom.triggerKeyEvent(editor, 'keydown', {
51+
key: 'ArrowLeft',
52+
keyCode: Keycodes.IME,
53+
charCode: Keycodes.LEFT
54+
});
55+
56+
assert.positionIsEqual(editor.range.head, editor.post.tailPosition());
57+
});
58+
59+
// There doesn't seem to be a way to directly test the usage
60+
// of an OS-level IME, however this test roughly simulates
61+
// how the IME inputs text into the DOM.
62+
test('test handling of IME composition events', (assert) => {
63+
let done = assert.async();
64+
65+
editor = Helpers.editor.buildFromText("", { element: editorElement });
66+
67+
Helpers.dom.moveCursorTo(editor, editorElement);
68+
69+
editor.element.dispatchEvent(
70+
new CompositionEvent('compositionstart', { 'data': 'n' })
71+
);
72+
73+
Helpers.wait(() => {
74+
if(Browser.isChrome()) {
75+
editorElement.firstChild.innerHTML = "こんにちは";
76+
} else {
77+
editorElement.firstChild.innerHTML += "こんにちは";
78+
}
79+
80+
Helpers.wait(() => {
81+
editor.element.dispatchEvent(
82+
new CompositionEvent('compositionend', { 'data': 'こんにちは' })
83+
);
84+
85+
Helpers.wait(() => {
86+
assert.positionIsEqual(editor.range.head, editor.post.tailPosition());
87+
assert.hasElement('#editor p:contains(こんにちは)');
88+
89+
done();
90+
});
91+
});
92+
});
93+
});

0 commit comments

Comments
 (0)