Search code examples
javascriptaudiopopcornjs

how to remove event listener from this during callback


I have a page with some javascript for listening to some stories which are in a series of audio files. I use the popcorn library to add footnotes to particular timestamps which highlights the words in the text on the page as the audio plays. I have a javascript object AudioPlayer (instance in this example is called ap) which contains many audio elements and many popcorn instances as well. The first time an audio element has 'loadedmetadata', if I have timestamps for that item, I want to create the popcorn footnotes. But, I don't want to run this function more than once for a given audio item. That is, people may click on an item and reload it, but I don't want to recreate the timestamps. For that reason, I wrote callback code like this to run when I get timestamps for a given item:

// try to load any timestamps for this file
this.loadTS = function(ann) { 
  var xhr = new XMLHttpRequest();
  xhr.open("GET", window.location.protocol+"//"+
                  window.location.hostname+"/sitename/timestamps/"+
                  window.location.pathname.substring(
                  window.location.pathname.lastIndexOf('/')+1)+".json",
            true);
  xhr.onreadystatechange=function(){
    if(xhr.readyState==4 && xhr.status==200){
      console.log(xhr.responseText);
      this.timestamps = JSON.parse(xhr.responseText);
      for(var idx in this.timestamps){

        var stampX = function(){ 
          // this is an audio element, get it's index to 
          // do the stamping  
          var x = window.ap.audios.indexOf(this);

          // need to remove this listner so it doesn't fire again!          
          this.removeEventListener('loadedmetadata',stampX); // <- fail :(
          // window.ap.audios[x]
          //  .removeEventListener('loadedmetadata',stampX);
          // ^^ this failed too :( :(

          // stamp away!
          window.ap.stampItem(window.ap.winIGTs[x], 
            window.ap.timestamps[x], window.ap.audios[x],
              window.ap.popcorns[x]);

        };

        this.audios[idx].addEventListener('loadedmetadata', stampX);

        if(ann) 
          this.textIGTs[idx].setAttribute("class","igt existstamps");
      }
    } else console.log(xhr.status);
  }.bind(this);
  xhr.send();
}

But I found in testing this code that 'stampX' is getting called more again if the audio element gets reloaded, so my only conclusion is that the removeEventListener is somehow not getting the same reference as the addEventListener below.

I'm finding this difficult to debug. I can't pass a variable into stampX because need a function reference for the event listener (not a function call).

Anyway, I'm having difficulty finding the proper way to write this such that I can remove the eventListener upon the first time stampX is called.


Solution

  • Looks like stampX is recreated every time because you declare it inside the onreadystatechange function.

    This means every addEventListener you do gets a different callback function.

    What you need to do is seperate it's logic, move it outside, higher on the lexical scope, for exampl where you declare the xhr object or even outside the loadTS deceleration. This way both the addEventListener and the removeEventListener will point to the same function.