Search code examples
javascriptgoogle-chrometampermonkeyuserscripts

Change page javascript code (TamperMonkey) to send keyboard strokes to the parent DOM


I have a page that uses the TradingView widget, which you can type inside it any key on the keyboard to trigger a symbol search. I used Chrome's debugger to breakpoint and know exactly which line of code does that. I want the key event to be bubbled up to the general DOM to be handled by my own custom function.

How to do that? Any sample code would be appreciated. Here is the code snippet from TradingView ('t' is the key event):

            a || (o.handle = a = function(t) {
                return s === Te || t && Te.event.triggered === t.type ? s : Te.event.dispatch.apply(a.elem, arguments)
            }

So instead of the return statement I want to send that key event to my own function. The full TradingView code where this code is: https://static.bitmex.com/assets/tradingView/static/tv-chart.min-6ce28e05fd34b9cf06a4e63b29980a72.js


Solution

  • Rather than altering the page code (which is possible, but can be tedious), consider changing your own listener to run in the capturing phase, rather than the bubbling phase. Eg, instead of

    document.body.addEventListener('keypress', fn);
    

    do

    document.body.addEventListener('keypress', fn, true);
    

    and it'll run before the event has bubbled down to the inner element.

    If the page's listener on the inner element needs to be complete before your fn runs, you can call fn after a small setTimeout:

    document.body.addEventListener('keypress', () => setTimeout(fn), true);
    

    If you also want to prevent the widget's code from running, make sure to call stopPropagation in your capturing listener so the event doesn't capture down to the widget:

    const fn = (event) => {
      event.stopPropagation();
      // rest of code
    };
    

    If the container you're sending the key event to is inside an iframe, the parent window won't see the event at all, due to security issues.

    On most pages, you'd be able to communicate the event up to the parent by using postMessage, eg:

    // ==UserScript==
    // @name             0 New Userscript
    // @include          https://www.bitmex.com/app/trade/XBTUSD
    // @include          https://static.bitmex.com/chartEmbed*
    // @grant            GM_getValue
    // @run-at document-start
    // ==/UserScript==
    
    if (window.location.host === 'www.bitmex.com') {
      const mainFn = () => {
        console.log('b');
      };
      // We're on the top level
      const keydown = (e) => {
        e.stopPropagation();
        if (!e.altKey && e.key === 'b') {
          mainFn();
        }
      };
      window.addEventListener('keydown', keydown, true);
      window.addEventListener('message', (e) => {
        if (e.origin === 'https://static.bitmex.com' && e.data === 'keydown inside iframe') {
          mainFn();
        }
      });
    } else {
      // We're in the iframe
      // When a keypress is detected, message the parent window
      const keydown = (e) => {
        console.log('iframe kd');
        e.stopPropagation();
        if (!e.altKey && e.key === 'b') {
          window.top.postMessage('keydown inside iframe', '*');
        }
      };
      window.addEventListener('click', keydown, true);
    }
    

    The @run-at document-start is needed because the iframe has a CSP which forbids userscripts without it from running.

    But unfortunately, there's another problem: in this particular case, the iframe window also overwrites EventTarget.prototype.addEventListener on pageload, before the userscript has a chance to run. While you could usually use // @run-at document-start to ensure you save a reference to a variable before a page script overwrites it, this isn't possible in an iframe; Chrome userscripts in iframes cannot run at the very start of pageload.

    Without a reference to EventTarget.addEventListener (or EventTarget.prototype.onclick or EventTarget.prototype.onkeydown setters, etc), there's no way to access the browser-level API that registers the user's actions.

    I don't think what you're looking for is possible in Tampermonkey.