google-docs-utils icon indicating copy to clipboard operation
google-docs-utils copied to clipboard

`typeText` Emoji support?

Open Not-Jayden opened this issue 5 years ago • 3 comments

Hi, first of all just want to say this is a super cool utility you've put together. Nice work!

Unfortunately, the use case I had for this was for being able to type emojis within google docs, which doesn't work due to emojis not actually having a valid keyboard key associated with them.

I know you've alluded to this being a known issue in the readme. Just wondering if you've made any progress or had any ideas on how support for non-keyboard character events might work? I thought maybe you could simulate typing alt-codes but it doesn't seem possible.

Not-Jayden avatar Apr 19 '21 10:04 Not-Jayden

Hm, it looks like at the moment typeText supports only characters that can be represented in a single UTF-16 code unit.

That I mean is that you can use typeText to type any Unicode characters that are <= 0xFFFF (it is all characters from Unicode Basic Multilingual Plane).

For example, consider this emoji - ✅. It is \u2705 in Unicode (UTF-16). And this one - 😀. It is \u1F600. Because in JavaScript a string is a sequence of 16-bit code points, \u1F600 should be represented by surrogate pair - \uD83D\uDE00.

'✅'.length === 1
'😀'.length === 2

So, GoogleDocsUtils.typeText('✅') will type this emoji, same for GoogleDocsUtils.typeText('\u2705'). But GoogleDocsUtils.typeText('😀') or GoogleDocsUtils.typeText('\uD83D\uDE00') will not type anything.

At the moment I'm suggesting you to use only emoji that are <= 0xFFFF, because typeText not supports anything else. I will try to provide support for other characters.

srgykuz avatar Apr 21 '21 14:04 srgykuz

Here is Google Docs code fragments, just for my reference.

By debugging 2628165760-client_js_prod_kix_core__ru.js file I can see that this code fires every time when KeyboardEvent occurs:

line: 114303

x.Ioa = function(a) {
    if (this.F && this.F.ma(a))
        a.preventDefault();
    else {
        var b = ty(), c = Cy(b, new JI(WMa,SBa)), d = a.shiftKey, e = a.keyCode, f = a.charCode, g = new w7c(a), k = Y7c(g), l = g.charCode, m;
        if (!(m = !or || g.keyCode == g.charCode))
            a: if (m = g.keyCode,
            48 <= m && 57 >= m || 96 <= m && 106 >= m || 65 <= m && 90 >= m || (pr || mr) && 0 == m)
                m = !0;
            else
                switch (m) {
                case 32:
                case 43:
                case 63:
                case 64:
                case 107:
                case 109:
                case 110:
                case 111:
                case 186:
                case 59:
                case 189:
                case 187:
                case 61:
                case 188:
                case 190:
                case 191:
                case 192:
                case 222:
                case 219:
                case 220:
                case 221:
                    m = !0;
                    break a;
                default:
                    m = !1
                }
        if (l = (m || 0 == g.keyCode) && 32 <= l) {
            if (!(k = !k) && (k = g.altKey && g.ctrlKey)) {
                a: switch (g.charCode) {
                case 44:
                case 46:
                case 47:
                case 48:
                case 49:
                case 50:
                case 51:
                case 52:
                case 53:
                case 54:
                case 69:
                case 80:
                case 97:
                case 98:
                case 99:
                case 100:
                case 101:
                case 102:
                case 103:
                case 104:
                case 105:
                case 106:
                case 107:
                case 108:
                case 109:
                case 110:
                case 111:
                case 112:
                case 113:
                case 114:
                case 115:
                case 116:
                case 117:
                case 118:
                case 119:
                case 120:
                case 122:
                    k = !0;
                    break a;
                case 34:
                case 43:
                case 58:
                case 60:
                case 65:
                case 66:
                case 67:
                case 68:
                case 71:
                case 72:
                case 73:
                case 74:
                case 75:
                case 76:
                case 77:
                case 78:
                case 79:
                case 82:
                case 83:
                case 84:
                case 86:
                case 88:
                case 90:
                    k = g.shiftKey;
                    break a;
                default:
                    k = !1
                }
                k = !k
            }
            l = k
        }
        l && F8c(this, String.fromCharCode(f), a);
        Y7c(g) || !(!or || wr.$p && wr.Ih("65") || 112 > e || 123 < e) || d && 45 == e || (m6c(this.V),
        a.preventDefault());
        Ey(b, c)
    }
}

This fragment F8c(this, String.fromCharCode(f), a) will call chain of other handlers. Notice String.fromCharCode(f). This code returns an actual input character based on it charCode. For ✅ character charCode will be 9989, for 😀 - 128512. But this method not supports values that are > 0xFFFF. So, String.fromCharCode(128512) will return ("Undefined Character", U+FFFF I suppose). This character will be passed down to other handlers.

By analyzing call stack I can see this code fragment:

line: 71479

rjc.prototype.wc = function(a) {
    a = a.C;
    a = Btb(a);
    var b = new uhc(1)
      , c = new bM(this.getContext(),a);
    QL(this, c);
    whc(vhc(b, c.L), c.J);
    OL(this, kF(dF(c.H + a.length | 0)), !0, null);
    this.F = new sjc(Nva,new njc(c.J,a),b.aa())
}

It accepts result of earlier String.fromCharCode call. Notice a = Btb(a).

By analyzing call stack I can see this code fragment:

line: 29172

function Btb(a) {
    uE();
    return a.replace(atb, "")
}

And by analyzing the code I see this code fragment:

line: 28994

var atb = /[\x00-\b\f-\u001f\ue000-\uf8ff]/g;

It is range of characters that are invalid for typing by Google Docs handlers and shouldn't by passed down to next handlers.

So, in short, what is happening here is:

let input = String.fromCharCode(128512); // `charCode` of 😀
input = input.replace(/[\x00-\b\f-\u001f\ue000-\uf8ff]/g, '');

Google Docs accepts key code of character, handles it with String.fromCharCode, and clears invalid characters. Because String.fromCharCode supports only characters from first Unicode plane, it will never produce valid character for any characters that are greater than 0xFFFF.

I do not know why Google Docs uses String.fromCharCode and String.prototype.charCodeAt for such cases. Probably it is intended. String.fromCodePoint and String.prototype.codePointAt will handle characters that are outside of first Unicode plane (😀, for example). By the way, String.fromCharCode(0xD83D, 0xDE00) is also valid.

srgykuz avatar Apr 21 '21 15:04 srgykuz

I think I should try to simulate insert event, not keyboard event

srgykuz avatar Apr 21 '21 15:04 srgykuz