Capture keys typed on android virtual keyboard using javascript

43k views Asked by At

I have a web page with a textarea, and I need to capture the keys typed by the user (so that I can substitute different unicode characters for the keys typed). My current code is as follows:

$("#myTextArea").bind('keypress', function(event) {

    var keyInput = event.which;
   // call other functions

});

This above code works on PCs, and iPhone/Safari. However, it fails when using Chrome on an android (samsung) tablet. For some reason when I type on the android virtual (soft) keyboard, the "keypress" event is not triggered. The android version is 5.0.2.

If I try using "keyUp" or "keyDown", it always returns 229 for all characters (except for return key, space, backspace, etc).

Even though the keyCode is always 229, the textarea displays the correct characters typed by the user. Which means the device knows which key was entered, but somehow I'm unable to get a handle on this event (and the key code) using javascript.

Here are the alternatives that I have tried so far, and their outcomes:

$("#mainTextArea").on("keydown keyup", function(event) { 
    // event.which and event.keyCode both return 229

$(document).on('keypress', function(event) { 
    // function is not triggered

$('#myTextArea').bind('input keypress', function(event) { 
   // comes inside function, but keyCode and which are undefined

Any help regarding this issue is appreciated..

10

There are 10 answers

1
reyiyo On

There is a textInput event that gives you the entered character

const inputField = document.getElementById('wanted-input-field');

inputField.addEventListener('textInput', function(e) {
    // e.data will be the 1:1 input you done
    const char = e.data; // In our example = "a"

    // If you want the keyCode..
    const keyCode = char.charCodeAt(0); // a = 97

    // Stop processing if "a" is pressed
    if (keyCode === 97) {
        e.preventDefault();
        return false;
    }
    return true;
});
1
thdoan On

You can approach it from a different perspective by using the selectionstart property:

Returns / Sets the beginning index of the selected text. When nothing is selected, this returns the position of the text input cursor (caret) inside of the <input> element [also applies to <textarea>].

Source: MDN

I do something like this in my own app and it has worked reliably...

document.querySelector('textarea').addEventListener('input', (e) => {
  const elInput = e.target;
  // Get start of selection (caret offset when typing)
  const nSelStart = elInput.selectionStart;
  // Get last typed character (modify for your own needs)
  const sLastTyped = elInput.value.substr(nSelStart-1, 1);
  console.log('Last typed character:', sLastTyped);
});
textarea {
  width: 99%;
  height: 4rem;
}
<textarea placeholder="Enter something using Android soft keyboard"></textarea>

Codepen: https://codepen.io/thdoan/full/dymPwVY

3
Pavel Donchev On

Unfortunately it seems you cannot do much here. Keypress event is deprecated, thus not fired. 229 on keyup and keydown indicates the keyboard buffer is busy. The reason - when you press a key - the input is still not guaranteed to be what the user pressed, because of auto suggest and other events that may follow immediately and invalidate the event. Although in my opinion it would have been better to send the key first, then fire another event perhaps on auto suggest so you can act upon it separately...

The only thing that I currently know of is to attach to both - keydown and keyup, store the value on keydown, get the value on keyup and find the delta, which is what user pressed. Unfortunately this will not work for non-input controls (e.g. - the body or something like that). Maybe not what you want to hear as answer but still.

6
Akshay Tilekar On

Just check your input characters keyCode, if it is 0 or 229 then here is the function getKeyCode() which uses charCodeAt of JS to return the KeyCode which takes input string a parameter and returns keycode of last character.

var getKeyCode = function (str) {
    return str.charCodeAt(str.length);
}

$('#myTextfield').on('keyup',function(e) {
    // For android chrome keycode fix
    if (navigator.userAgent.match(/Android/i)) {
        var inputValue = this.value;
        var charKeyCode = e.keyCode || e.which;
        if (charKeyCode == 0 || charKeyCode == 229) {
            charKeyCode = getKeyCode(inputValue);
            alert(charKeyCode+' key Pressed');
        } else {
           alert(charKeyCode+' key Pressed');
        }
    }
});
2
Matheus Moraes On

We are in 2024 and it stills seems to be a problem. Why does it work this way?? Why the inputs from the virtual keyboard are not captured?? Is there any good reason?

And I don't find this specified anywhere in the web, even MDN doesn't talks about it, but if you go on theirs KeyboardEvent Sequence Example and try to stroke keys on android, nothing happens. Is it a really underground problem?

Is it just that even major names are unaware about this or am I missing something and this was supposed to be like this?

Edit: If someone comes here after me, this was the most usefull information I found, I think I'm gonna giveup on keyboard events for mobile and go with some workaround with "input" events.

0
Faber_Hozz On

For Angular, you can use the following directive:

import { Directive, ElementRef, HostListener, Input } from'@angular/core';

@Directive({
  selector: '[appAndroidInputRestriction]'
})
export class AndroidInputRestrictionDirective {
  @Input() appInputRestriction: string; // Assuming you have an input for input restriction type

  private oldValue: string;
  private newValue: string;

  constructor(private el: ElementRef) {}

  @HostListener('keydown', ['$event']) onKeydown(event: KeyboardEvent) {
    const userAgent = navigator.userAgent;
    const isAndroid: boolean = !!/android/i.test(userAgent);
    if (!isAndroid) return;

    this.oldValue = this.el.nativeElement.value;
  }

  @HostListener('input', ['$event']) onInput(event: InputEvent) {
    const userAgent = navigator.userAgent;
    const isAndroid: boolean = !!/android/i.test(userAgent);
    if (!isAndroid) return;

    if (event.inputType === 'deleteContentBackward') return;

    this.newValue = this.el.nativeElement.value;

    const difference: string = this.difference(this.oldValue, this.newValue);

    const typingRegEx = /[0-9]/; // You can customize the regex here

    if (typingRegEx.test(difference)) {
      const controlValue = this.newValue;
      this.el.nativeElement.value = controlValue;
    }
  }

  private difference(value1: string, value2: string): string {
    const output = [];
    for (let i = 0; i < value2.length; i++) {
      if (value1[i] !== value2[i]) {
        output.push(value2[i]);
      }
    }
    return output.join('');
  }
}

This is the input element:

<input appAndroidInputRestriction [appInputRestriction]="'your-restriction-type'" />
0
Ori On

I ran into the same issue. Several explanations are out there but anyhow it seems strange that no solution is offered. For the moment I solved it by capturing the oninput event.

"This event is similar to the onchange event. The difference is that the oninput event occurs immediately after the value of an element has changed, while onchange occurs when the element loses focus, after the content has been changed"

This event support inserted text too, from pasting text or from corrections & suggestions.

it doesn't give me the perfect solution cause I can only manipulate the text AFTER it has been entered, but for the moment it is the best I have.

If anyone has a better solution I will be glad to hear about it.

1
Glauber Borges On

I came across this discussion while doing research for a project I was working on. I had to create input masks for a mobile app, and Pavel Donchev's answer got me thinking about what could work to capture keys in Android. In my specific project, keydown and keyup would not be enough because keyup event is only triggered after a key is released, so it would imply in a late validation, so I did some more research (and lots of trial and error) with input events and got it working.

var input = document.getElementById('credit-card-mask'),
    oldValue,
    newValue,
    difference = function(value1, value2) {
      var output = [];
      for(i = 0; i < value2.length; i++) {
        if(value1[i] !== value2[i]) {
          output.push(value2[i]);
        }
      }
      return output.join("");
    },
    keyDownHandler = function(e) {
      oldValue = input.value;
      document.getElementById("onkeydown-result").innerHTML = input.value;
    },
    inputHandler = function(e) {
      newValue = input.value;
      document.getElementById("oninput-result").innerHTML = input.value;
      document.getElementById("typedvalue-result").innerHTML = difference(oldValue, newValue);
    }
;

input.addEventListener('keydown', keyDownHandler);
input.addEventListener('input', inputHandler);
<input type="text" id="credit-card-mask" />
<div id="result">
  <h4>on keydown value</h4>
  <div id="onkeydown-result"></div>
  <h4>on input value</h4>
  <div id="oninput-result"></div>
  <h4>typed value</h4>
  <div id="typedvalue-result"></div>
</div>

The oninput event is triggered right after the keydown event, which is the perfect timing for my validations.

I compiled the whole thing in an article. If you're curious, you can read about it here.

5
Íhor Mé On

I FIGURED IT OUT!

Here's a 100% working solution that works EVERYWHERE with EVERY feature, including even emoji suggestions on iOS and any pasted content. I'm using substring comparison to find actual stuff that changed from onInput to onInput.

Points from which to which text is deleted and from which to which it's inserted are pointed out.

Rating and selecting as an answer is appreciated.

var x = document.getElementById("area"),
    text = x.value,
    event_count = 0



function find_Entered_And_Removed_Substrings(
    previous_string, current_string, pos
) {
    let
        right_pos = pos,
        right_boundary_of_removed =
            previous_string.length -
            (
                current_string.length -
                pos
            ),
        left_max_pos = Math.min(
            pos,
            right_boundary_of_removed
        ),
        left_pos = left_max_pos

    for (
        let x = 0; x < left_max_pos; x++
    ) {
        if (
            previous_string[x] !==
            current_string[x]
        ) {
            left_pos = x
            break
        }
    }


    return {
        left: left_pos,
        right: pos,
        removed_left: left_pos,
        removed_right: right_boundary_of_removed
    }
}

x.oninput =
    (e) => {
        // debugger;
        let
            cur_text = x.value,
            positions =
                find_Entered_And_Removed_Substrings(
                    text, cur_text, Math.max(
                        x.selectionStart, x.selectionEnd
                    )
                )
        console.log(positions)
        let
            entered =
                cur_text.substring(
                    positions.left, positions.right
                ),
            removed =
                text.substring(
                    positions.removed_left, positions.removed_right
                )


        if (
            entered.length >
            0 ||
            removed.length >
            0
        ) {
            document.getElementById("entered")
                .innerHTML +=
                entered

            document.getElementById("removed")
                .innerHTML +=
                removed

            document.getElementById("events")
                .innerHTML =
                event_count++
        }


        text = cur_text
    }
<textarea id="area"></textarea>
<br/>
<pre id="entered"></pre>
<br/>
<div id="events"></div>
<pre id="removed"></pre>

0
Anthony Lee On

I recently implemented a "mentions" feature in the latest version of our Astro AI assisted Email app. Basically you type "@" in our compose web view and you get a list of autocomplete suggestions. We, like most other people, had problems trying to solve this in the Javascript. What eventually worked was a native solution. If you @Override the WebView's onCreateInputConnection() method, you can wrap the super.onCreateInputConnection() result (which is just an InputConnection interface) with a custom class. Then in your wrapper implementation, you can trap input via commitText() or setComposingText() or maybe some other method specific to what you are looking for...like deletes. I don't know if you would get any callbacks on control characters like up and down arrows but maybe this can be a place to start to solve your specific problem.