Skip to content

Commit e40114d

Browse files
committed
Fix IME bugs
* Suppress mutations when using an IME on a blank line * Ignore control keydowns when IME is active * Handle carriage return keypresses
1 parent 3d96e5a commit e40114d

File tree

4 files changed

+173
-1
lines changed

4 files changed

+173
-1
lines changed

src/js/editor/editor.js

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

153+
this._isComposingOnBlankLine = false
154+
153155
DEFAULT_TEXT_INPUT_HANDLERS.forEach(handler => this.onTextInput(handler))
154156

155157
this.hasRendered = false

src/js/editor/event-manager.js

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@ import TextInputHandler from 'mobiledoc-kit/editor/text-input-handler'
66
import SelectionManager from 'mobiledoc-kit/editor/selection-manager'
77
import Browser from 'mobiledoc-kit/utils/browser'
88

9-
const ELEMENT_EVENT_TYPES = ['keydown', 'keyup', 'cut', 'copy', 'paste', 'keypress', 'drop']
9+
const ELEMENT_EVENT_TYPES = [
10+
'keydown',
11+
'keyup',
12+
'cut',
13+
'copy',
14+
'paste',
15+
'keypress',
16+
'drop',
17+
'compositionstart',
18+
'compositionend',
19+
]
1020

1121
export default class EventManager {
1222
constructor(editor) {
@@ -143,6 +153,13 @@ export default class EventManager {
143153
event.preventDefault()
144154
}
145155

156+
// Handle carriage returns
157+
if (!key.isEnter() && key.keyCode === 13) {
158+
_textInputHandler.handleNewLine()
159+
editor.handleNewline(event)
160+
return
161+
}
162+
146163
_textInputHandler.handle(key.toString())
147164
}
148165

@@ -169,6 +186,10 @@ export default class EventManager {
169186
let range = editor.range
170187

171188
switch (true) {
189+
// Ignore keydown events when using an IME
190+
case key.isIME(): {
191+
break
192+
}
172193
// FIXME This should be restricted to only card/atom boundaries
173194
case key.isHorizontalArrowWithoutModifiersOtherThanShift(): {
174195
let newRange
@@ -215,6 +236,59 @@ export default class EventManager {
215236
this._updateModifiersFromKey(key, { isDown: false })
216237
}
217238

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

src/js/utils/browser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ export default {
55
isWin() {
66
return typeof window !== 'undefined' && window.navigator && /Win/.test(window.navigator.platform)
77
},
8+
isChrome() {
9+
return typeof window !== 'undefined' && 'chrome' in window
10+
},
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 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)