@@ -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