Event Listeners Added Multiple Times

I have an addon which does the following:

  1. Clicking on the toolbar button displays a popup, in the WebExtension meaning.
  2. The popup consists of an HTML document which links to a JavaScript a.js file.
  3. The JavaScript calls code in another JavaScript file, b.js, referenced in manifest.json as a content script.
  4. Among other things, b.js adds an event listener to a web page DOM element.

This is my first WebExtension, so I’m stumbling in the dark here. As far as I understand, the first JavaScript file a.js has access only to the popup DOM, while the second file b.js has access to the web page.

The problem is I think that a new instance is created whenever b.js is run. This affects adding and removing event listeners:

  • I find that it sometimes adds an event listener more than once. This isn’t supposed to be possible, so I guess that even though it has the same name as before, the actual function is regarded as a new one.
  • I cannot reliably remove the event listener. Again I presume that it things I’m trying to remove a different event listener.

Now I have to say that the whole EventListener thing is a pain. While it’s sweet that you can have more than one attached to an element, JavaScript has no provision for tracking them, so you can’t even test whether one has already been attached, or remove them if you lose track of the original function.

The question:

  • Am I correct in assuming that a new instance of the script is created every time?
  • Is it possible to ensure that it doesn’t happen within a particular tab?

Thanks

How do you “call[…] code in another JavaScript file” that is “a content script”?
Do you include it in your popup.html like this <script src="b.js"></script>?
If so, then you are loading a copy of that code into your popup, which has nothing to do with the ones that are loaded as content scripts. To call functions in content scripts, you have to use messaging.

If this didn’t help, I’d suggest you link your code (or at least the relevant parts of it) here.

Thanks for your reply.

I do the following:

manifest.json:

"content_scripts": [
    {    "matches": ["<all_urls>"],
         "js": ["/do-more-stuff.js"]
    }
]

popup.html

…
<script type="text/javascript" src="do-stuff.js"></script>

do-stuff.js

var request={…};
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
    chrome.tabs.sendMessage(tabs[0].id, request,null,null);
});

do-more-stuff.js

var inited;
if(!inited) {
    chrome.runtime.onMessage.addListener(doMoreStuff);
}
function init() {
    //    preliminary one-time-processing, including some DOM tweaking
    inited=true;
}
function doMoreStuff(request, sender, sendResponse) {
    if(!inited) init(request);
    //    further processing
}

The nonsense with the init variable above is the only way I could think of to ensure that init was called only once.

Thanks

So what you are want to do is to run code in the web page of the currently active tab when the user clicks a button or something, but you don’t want your code to run more than once per site?

If so, a static content script is not your best option. I’d rather suggest programmatic injection of the “/do-more-stuff.js” when it’s actually needed:

popup.js

chrome.runtime.sendMessage('inject');

background.js

const ports = new Set;
chrome.runtime.onConnect.addListener(
    port => ports.add(port) === port.onDisconnect.addListener(() => ports.delete(port))
);

chrome.runtime.onMessage.addListener(m => m === 'inject' && injectToCurrent);

function injectToCurrent() {
    chrome.tabs.query({active: true, currentWindow: true}, ([ tab, ]) => tab && injectToTab(tab.id));
}

function injectToTab(tabId) {
    if (Array.from(ports).some(_=>_.sender.tab.id === tabId)) { return; } // already injected
    chrome.tabs.executeScript(tabId, { file: "/do-more-stuff.js", ... });
}

do-more-stuff.js

chrome.runtime.connect({ name: 'tab', });
init(); // should only ever be executed once

And remove the “content_scripts” key from the manifest.json.

I hope this does the job. I’m going to bed now.

Thanks for this. I’ll have a good look at this and see how I can make it work for me.

Good Night!

I am also new here but what I noticed is that the point 1) and 2) are weird. If 1) is page script than it cannot call any function in 2) content script.

I think about Mozilla addons like this:

  • background script is like a “core” which controls whole addon. It is like server who says what to do.
  • page scripts which run in the popup are like a “client”. So these things are completely separated, they communicate through messages. - Similar it is with the Content Scripts. They are also completely separated so you cannot call any function from page script (popup.html -> popup.js). You need to send a message from either the Content Script or from the Page Script to the Background Script (“the core”). There you have a listener which will receive your message and then a function from the Background Script is called. This one cab do some stuff and then it sends another message to the “client” - either to the Content Script or to the Page Script. So you have 3 different functions, 3 different namespaces (they can have same names, but they are different).

These basics are needed to prevent problems.

Not quite, actually. There are only two kinds of processes(*) where your extension code runs:

  1. the Extension process and
  2. the Content processes
  • The (invisible) background page and any tabs/popups/panels/… that have a page of your extension running in its main_frame are all executed in the same extension process (1) and can directly communicate with each other (you can get references to each others global objects (“window”) through chrome.runtime.getBackgroundPage() and chrome.extension.getViews({ ...})).
    I’d always use massages anyway to avoid bugs and have a clearer structure.

  • All web pages are loaded in individual content processes (2). You can inject content scripts into these processes. To communicate with these, you have to use messages.
    Also note that, even though in the same process, your content script won’t be loaded into the same global context as the page script, it gets a completely different JavaScript wrapper of the DOM (and everything else), but on the other hand has access to the chrome.* APIs (which page scripts don’t).

* In Chrome these “processes” are (almost) always actually different native processes, in Firefox they are usually not (yet), but have to be treated as such.

As a site note: There are no such things as “namespaces” in JavaScript, you only have (global) scopes and objects.

I know I was not exact, but I tried to simplify this as possible and write it as short as possible. For me as a beginner, when I see long complicated definitions so the result is just a confusion in my head. So for me the analogy to client - server is enough.