Search code examples
javascripthtmlgreasemonkey

innerHTML won't change in each keydown event


The following code is aimed to change the innerHTML of an element (myNote) to the value of another element (myInput), each time the Return key is pressed:

// ==UserScript==
// @name        Duolingo
// @include     *://duolingo.com/*
// @include     *://www.duolingo.com/*
// ==/UserScript==
setTimeout(()=>{
  let myInput = document.querySelector('._7q434._1qCW5._2fPEB._3_NyK._1Juqt._3WbPm');
  let myNote = document.createElement('div');
  document.body.appendChild(myNote);
  myNote.setAttribute("style", "position: fixed; bottom: 0; right: 0; display: block; width: 400px; height: 400px; background: orange; color: #000");

  myInput.addEventListener('keydown', (k)=>{
    if ( k.keyCode === 13 ) {
      myNote.innerHTML += myInput.value + '<br>';
    }
  });
}, 2000);

Purpose

I run the code in Greasemonkey on duolingo.com (a site for learning mind2mind languages like French), in textual question sessions like "translate this sentence".

The code's purpose is to create a small orange-background box containing the inputs I already tried in Duolingo questions, given that Duolingo doesn't save these.

With the script, I could save them and later use them if I retake a language question as it saves me some time retyping most of a sentence.

Problem

The code fails because the innerHTML is changed only once. If the value changes again and I repress Return, nothing will happen.

Reproduce

Reproduce by using the code in Greasemonkey (or a similar program) on duolingo.com.

Question

Why would the innerHTML be changed only once? Given the addEventListener listens all the time, why it'll work only once?

Using return for the change or adding return false didn't help.

Maybe another approach of adding a new element containing the value each pressing is needed.

Update for Mobius:

Mobius, this is the code I used, which didn't work:

setTimeout(()=>{
    window.myCss =`.note {position: fixed; bottom: 0; right: 0; width: 400px; height: 400px; background: orange}`;
    style = document.createElement("style");
    style.type = "text/css";
    style.styleSheet ? style.styleSheet.cssText = myCss : style.appendChild(document.createTextNode(myCss) );
    head = document.head || document.getElementsByTagName("head")[0];
    head.appendChild(style);

    let note = document.createElement('div');
    note.classList.add('note');
    document.querySelector('body').appendChild(note);

  let savedValue;
    document.addEventListener('keydown', (e)=>{
    let target = e.target;
    if (target.nodeName === 'textarea') {
        savedValue = e.target.value;
        }
    });

    if (k.keyCode === 13) {
        setTimeout(()=>{
            document.querySelector('textarea').value = savedValue;
        }, 100);
    }
}, 2500);

I didn't have any error in console.

I tried to read in the article and I understand that in the original example I babbled instead of captured (and I should capture any handler by going from the html element to my textarea element each time anew and capture it's handler, but I couldn't see how to improve the code from that. Maybe I didn't read good as I'm in a bad mood right now...


Solution

  • The code fails (after removing the typo) because it adds the event listener to a textarea that the webpage overwrites with every new question. When the textarea is overwritten, it destroys the previous event listener that you added. It also orphans the value of myInput in that code.

    Use a smarter event listener on a persistent container or the whole body. And, refresh myInput as needed.

    There are other issues:

    1. ._7q434._1qCW5._2fPEB._3_NyK._1Juqt._3WbPm is an exceptionally brittle selector and will break the next time the website code changes. It also already doesn't work on some pages I saw.

      textarea[data-test] will probably work. Or even textarea might suffice given the rest of the code.

    2. setTimeout is a brittle way to wait for elements and is already causing you grief. Use waitForKeyElements, or MutationObserver or similar instead. But in your case, since you are waiting for user interaction, you probably do not need any other delay mechanism.

    Putting that all together, your userscript would be something like:

    // ==UserScript==
    // @name        Duolingo, answer text recycler
    // @match       *://*.duolingo.com/*
    // @run-at      document-idle
    // ==/UserScript==
    let myNote = document.createElement ('div');
    document.body.appendChild (myNote);
    myNote.setAttribute (
        "style",
        "position: fixed; bottom: 0; right: 0; width: 400px; height: 400px; background: orange; overflow: auto;"
    );
    
    document.addEventListener ('keydown', (zEvent) => {
        if (zEvent.keyCode === 13) {
            if (zEvent.target.nodeName === 'TEXTAREA') {
                let myInput = document.querySelector ('textarea[data-test]');
                if (myInput) {
                    myNote.innerHTML += myInput.value + '<br>';
                }
            }
        }
    } );