Skip to content

Commit e0deabe

Browse files
authored
Merge pull request #4017 from aashu2006/fix/mobile-autocomplete
Fix autocomplete not triggering on mobile (CM5 input handling)
2 parents 9c8dbff + 911519a commit e0deabe

1 file changed

Lines changed: 77 additions & 11 deletions

File tree

  • client/modules/IDE/components/Editor

client/modules/IDE/components/Editor/index.jsx

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -228,21 +228,73 @@ class Editor extends React.Component {
228228
this._cm.on('keyup', this.handleKeyUp);
229229
}
230230

231-
this._cm.on('keydown', (_cm, e) => {
232-
// Skip hinting if the user is pasting (Ctrl/Cmd+V) or using modifier keys (Ctrl/Alt)
233-
if (
234-
((e.ctrlKey || e.metaKey) && e.key === 'v') ||
235-
e.ctrlKey ||
236-
e.altKey
237-
) {
238-
return;
231+
// Mobile autocomplete support (CM5 IME + contenteditable input)
232+
const triggerHint = (cm) => {
233+
const mode = cm.getOption('mode');
234+
if (mode !== 'css' && mode !== 'javascript') return;
235+
236+
const cursor = cm.getCursor();
237+
const token = cm.getTokenAt(cursor);
238+
239+
// Android keyboards often append a trailing space after each word.
240+
// When that happens, stripping the space so the hinter sees the word.
241+
if (token.string === ' ' && cursor.ch > 0 && cursor.ch === token.end) {
242+
const prevToken = cm.getTokenAt({
243+
line: cursor.line,
244+
ch: cursor.ch - 1
245+
});
246+
if (prevToken.string && /[a-z]/i.test(prevToken.string)) {
247+
cm.replaceRange(
248+
'',
249+
{ line: cursor.line, ch: cursor.ch - 1 },
250+
cursor,
251+
'+trimHint'
252+
);
253+
this.showHint(cm);
254+
return;
255+
}
239256
}
240-
const mode = this._cm.getOption('mode');
241-
if (/^[a-z]$/i.test(e.key) && (mode === 'css' || mode === 'javascript')) {
242-
this.showHint(_cm);
257+
if (token.string && /[a-z]/i.test(token.string)) {
258+
this.showHint(cm);
259+
}
260+
};
261+
262+
// Desktop: fires on each keystroke via CM5's textarea input path.
263+
this._cm.on('change', (_cm, changeObj) => {
264+
if (changeObj.origin !== '+input') return;
265+
if (/[a-z]/i.test(changeObj.text.join(''))) {
266+
triggerHint(_cm);
243267
}
244268
});
245269

270+
// Mobile (word commit): fires when a composed word is accepted.
271+
this._compositionEndHandler = () => {
272+
setTimeout(() => {
273+
if (this._cm) triggerHint(this._cm);
274+
}, 150);
275+
};
276+
this._cm
277+
.getInputField()
278+
.addEventListener('compositionend', this._compositionEndHandler);
279+
280+
// Mobile (per-character): forces CM5 to process composing text
281+
// during typing so autocomplete appears before keyboard dismissal.
282+
this._compositionFlushTimer = null;
283+
this._compositionUpdateHandler = (e) => {
284+
if (!e.data || !/[a-z]/i.test(e.data)) return;
285+
clearTimeout(this._compositionFlushTimer);
286+
this._compositionFlushTimer = setTimeout(() => {
287+
const display = this._cm && this._cm.display;
288+
if (display && display.input && display.input.composing) {
289+
display.input.composing.done = true;
290+
display.input.readFromDOMSoon();
291+
}
292+
}, 200);
293+
};
294+
this._cm
295+
.getInputField()
296+
.addEventListener('compositionupdate', this._compositionUpdateHandler);
297+
246298
this._cm.getWrapperElement().style[
247299
'font-size'
248300
] = `${this.props.fontSize}px`;
@@ -372,6 +424,20 @@ class Editor extends React.Component {
372424
componentWillUnmount() {
373425
if (this._cm) {
374426
this._cm.off('keyup', this.handleKeyUp);
427+
const inputField = this._cm.getInputField();
428+
if (this._compositionEndHandler) {
429+
inputField.removeEventListener(
430+
'compositionend',
431+
this._compositionEndHandler
432+
);
433+
}
434+
if (this._compositionUpdateHandler) {
435+
inputField.removeEventListener(
436+
'compositionupdate',
437+
this._compositionUpdateHandler
438+
);
439+
}
440+
clearTimeout(this._compositionFlushTimer);
375441
}
376442
this.props.provideController(null);
377443
}

0 commit comments

Comments
 (0)