Search code examples
javascriptgoogle-chrome-extensiongmaildom-events

How can I detect keyboard events in Gmail


I am writing a browser extension that needs to attach handlers to the keyup and keydown events on all pages. I can get it working pretty well with the following content script code.

document.addEventListener("keydown",keyDown, true);      
document.addEventListener("keyup", keyUp, true);

I can't get this to work in Gmail though. Specifically I can't get it to work when composing the body of an new email. It will work everywhere else I have tested. I think the problem is because Gmail is calling stopPropagation on all keyboard events but it is difficult to debug their minimized code. I thought that setting the 3rd parameter to true would cause the event to be captured during the CAPTURE_PHASE but this isn't working.

How can I capture keyup and keydown events while composing a new body in Gmail with a Google Chrome content script?

Edit:

I've ensured that my content scripts are being injected into all iframes of the DOM by adding "all_frames": true, to my manifest. I have even tried using the following code:

document.addEventListener("DOMNodeInserted", function (event) {
     if(event.type === "DOMNodeInserted") {
        if(event.srcElement.nodeName === "IFRAME") {
        console.log(event.srcElement.nodeName + " iframe detected");
        event.srcElement.addEventListener("keydown", function(kevent) {
            document.dispatchEvent(kevent);
            }, true);
        event.srcElement.addEventListener("keyup", function(kevent) {
            document.dispatchEvent(kevent);
            }, true);
        
    }
}
},true);

This still doesn't fix the issue with Gmail.


Solution

  • Your code doesn't work because event.srcElement refers to the <iframe> element, not its content. To access its content document, you have to wait for the frame to be loaded (onload or polling), then use frame.contentDocument to access the frame.

    Starting from Chrome 37.0.1995.0, you can also use the match_about_blank (with all_frames) to insert a content script in the about:blank frame that captures the event and sends it to the parent content script.

    Here is an example of an implementation for the original idea (using polling):

    The relevant parts of manifest.json:

      "content_scripts": [{
          "matches": ["*://mail.google.com/*"],
          "js": ["contentscript.js"],
          "run_at": "document_end"
      }],
    

    contentscript.js

    function keyDown(e) {console.log(e.which);}; // Test
    function keyUp(e) {console.log(e.keyCode);}; // Test
    (function checkForNewIframe(doc) {
        if (!doc) return; // document does not exist. Cya
    
        // Note: It is important to use "true", to bind events to the capturing
        // phase. If omitted or set to false, the event listener will be bound
        // to the bubbling phase, where the event is not visible any more when
        // Gmail calls event.stopPropagation().
        // Calling addEventListener with the same arguments multiple times bind
        // the listener only once, so we don't have to set a guard for that.
        doc.addEventListener('keydown', keyDown, true);
        doc.addEventListener('keyup', keyUp, true);
        doc.hasSeenDocument = true;
        for (var i = 0, contentDocument; i<frames.length; i++) {
            try {
                contentDocument = iframes[i].document;
            } catch (e) {
                continue; // Same-origin policy violation?
            }
            if (contentDocument && !contentDocument.hasSeenDocument) {
                // Add poller to the new iframe
                checkForNewIframe(iframes[i].contentDocument);
            }
        }
        setTimeout(checkForNewIframe, 250, doc; // <-- delay of 1/4 second
    })(document); // Initiate recursive function for the document.
    

    Note that I used polling instead of DOM mutation events, because the latter heavily reduces performance.