Script Running Multiple Times on Content Editable & Iframe

I am experiencing odd behavior on an extension.

It generates a context menu, and then fires depending on what menu is clicked.

If I use this in a text box, it will run once, returning “am I looping” to the console log once, but if I do this in a content editable html element it runs 3 times.

I have stripped down the content script to the minimum parts and run it, and I still get it running 3 times for a content editable html element.

Is this a bug, or am I missing something?

(function(global) {

    var clickedElement = null;

    document.removeEventListener("mousedown", function(event) {
        //right click
        if (event.button == 2) {
            clickedElement = event.target;
        }
    }, true);

    browser.runtime.onMessage.removeListener(function(commandString, sendResponse) {
        CommandParse(commandString);
    }, true);

    document.addEventListener("mousedown", function(event) {
        //right click
        if (event.button == 2) {
            clickedElement = event.target;
        }
    }, true);

    browser.runtime.onMessage.addListener(function(commandString, sendResponse) {
        CommandParse(commandString);
    });
	

async function CommandParse(argString) {
	console.log("am I looping");

	}

}) (this);

For starters, that removeListener will not work that way. You’d want to pass CommandParse directly to addListener/removeListener. Plus it wouldn’t work either way, since it’s removing a function that’s not even added yet?
Same for removeEventListener btw.

If you’re using the menus API using https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/getTargetElement may be simpler.

Further, I’d make sure that you’re only injecting this script in the frame you want it to run in and only inject it once per page load.

Lastly, I’m not sure mousedown even goes for context menu clicks, but typically you’d use the contextmenu event when you want to deal with things in relation to a context menu.

So the main reason why you’d see this thrice is because you’re injecting/running the script multiple times, thus having multiple listeners.

A quick question: Why would this happen with a content editable element, but not a text box or an input box?

It only runs once in the latter case.

Depends on the setup. Could be because there’s iframes for the contenteditable element (typical for wysiwyg editors) etc.

So the script is injecting for each nested iframe then?

If you tell it to, yes.

OK, I’m unclear on the functionality of https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/getTargetElement .

Right now, I am generating the menus (100+ items) from a JSON file using browser.menus.create in the background script, and then using browser.menus.onClicked.addListener(info, tab, defaultMenu) to communicate this to the content script.

Would I need to completely rewrite my menu generation, or just change the listener section?

getTargetElement can be used in the handler by sending the target element ID to a content script (or using it directly like shown in the example on MDN) and then you can use the method to turn the target element ID into an actual DOM node.

Since, getElementByID does not work on the pages with rich text elements embedded in an iframe, how would I get the target element ID?

This is unrelated to the DOM ID of the element. You get the ID in the click context info: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/OnClickData (look for targetElementId).

OK, let me walk through this to see if I understand properly.

In my background script I create a hundred or so menus using browser.menus.create().

I am sending a message to the content menu using:

browser.menus.onClicked.addListener((info, tab, defaultMenu) => {
/* data manipulations creating clickArg omitted */
  browser.tabs.sendMessage(tab.id, clickArg); 
    });

What I want to do is this:

browser.menus.onClicked.addListener((info, tab, defaultMenu) => {
let wotElement = info.targetElementId; //the frame ID
    /* data manipulations creating clickArg omitted */
      browser.tabs.sendMessage(tab.id, clickArg, wotElement); 
    });

And if I do this, the script will only be injected into the iframe and not the whole page.

Is that an accurate description of its operation?

OK, that seems to work, it generates a long integer number.

It is unclear to me how I am supposed to pass the ID via sendmessage, and what if anything, I have to do ensure that the listener will apply it to only 1 frame.

The browser.tabs.sendMessage documentagion notes that options are an object, and frameId is an integer, but I am unclear on how to structure this syntactically.

How you’d send the message:

browser.tabs.sendMessage(tab.id, {
  clickArg,
  wotElement
}, {
  frameId: idOfTargetFrame
});

listener in content script:

browser.runtime.onMessage.addListener((message, sender) => {
  const element = browser.menusgetTargetElement(message.wotElement);
  element.doStuffWith(message.clickArg);
});

OK, I have it in place, and it is still going to all the frames.

Sending from background script:

browser.menus.onClicked.addListener((info, tab, defaultMenu) => {
  let identifyFrame = info.targetElementId; //the active frame ID for nested frames
  console.log(identifyFrame); //Shows integer value
// clickArg stuff snipped for clarity
        browser.tabs.sendMessage(tab.id, 
    {
	clickArg, 
	"frameId": identifyFrame
	}
    ); 
    });

Receiving on content script:

browser.runtime.onMessage.addListener(function(commandString) {
   CommandParse(commandString.clickArg), commandString.frameId});  

Still running in all three iframes.

The frameId is in a separate object from the actual payload. Also, the frameId is not the targetElementId.

Edit: and your onMessage listener also doesn’t seem to actually use the “frameId” (which is actually the targetElementId).

If i place frameId object in the sendMessage like this:

browser.tabs.sendMessage(tab.id, 
	{clickArg, "frameId": identifyFrame}, 
	{
		'frameId': identifyFrame
	}
);

I get the following: Error: Could not establish connection. Receiving end does not exist. error.

As I had mentioned, the targetElementId is not the frameId. Those are separate things. To find the correct frame, use the frameId from the click info.
image
(from https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/OnClickData )

I understand what you are saying, that I should send a frameId object, and the listener should take that object from the message, and then apply only to that frame.

What I am unclear on is the syntax of the send and listen, and neither the docs, nor the examples that I have looked at, give me any insight into what the syntax should be.

Also, info.targetElementId and info.iframeId give different values, though both are integers, so I am unclear on what I should be doing from a syntax/variable perspective.

You should use the two values as different things in your sendMessage call. The targetElementId would correspond to wotElement in my example code, and the frameId to idOfTargetFrame.

It’s sendMessage that does the filtering based on the frame ID, not the listener.

OK, I get it now. sorry about being a bit dense.

Thank you.

So basically, the send message (ignoring the logic stuff that is irrelevant to the messaging) is:

browser.menus.onClicked.addListener((info, tab, defaultMenu) => {
/* various stuff to generate clickArg ignored */
    browser.tabs.sendMessage(tab.id, clickArg, {'frameId': info.frameId})
});  

The listener is fine as it was originally shown, since the sender determines the frame:

browser.runtime.onMessage.addListener(function(commandString) {
	CommandParse(commandString)}); 

Posted here for posterity.