How can I programmatically open the browser action popup, setting its page at runtime?

What I want to accomplish is:

  1. The user selects some text in a Web page.
  2. The user types a key combination.
  3. The browser action popup is opened at a specific URL, based on the previous key combination.

The manifest is like:

...
"commands": {
   "toggle-feature": {
      "suggested_key": {
         "default": "Ctrl+Shift+U"
      },
      "description": "Send a 'toggle-feature' event"
   }
}

Trying to do:

browser.commands.onCommand.addListener(async (command) => {
  if (command == "toggle-feature") {
    const { id: tabId } = (await browser.tabs.query({ active: true, currentWindow: true }))[0];
    const text = (await browser.tabs.executeScript(tabId, { code: 'getSelection()+""' }))[0];
    browser.browserAction.setPopup({ popup: "My URL" + text });
    browser.browserAction.openPopup();
  }
});

results in:

image

while:

browser.commands.onCommand.addListener(async (command) => {
  if (command == "toggle-feature") {
    browser.browserAction.setPopup({ popup: "My URL" });
    browser.browserAction.openPopup();
  }
});

works and opens the popup at the speficied URL. However, I need to set that page at runtime, since I need the selected text to decide the destination URL. How can I accomplish this?

See the docs:

The issue is the async call (tabs.query and tabs.executeScript), since “async” function may take “any” time to resolve, it may be too late for it to be considered as “user action”, imagine the popup being opened 1 minute later :slight_smile:.

So it will work only if you execute it in the current event-loop + micro-tasks (so awaiting already resolved promises should still work).

Workarounds:

  • store the selected text in some storage and let popup load it (or wait for storage change if it wasn’t stored in time)
  • store selected text in some runtime variable and let popup ask the data when it’s loaded (again, watchout for the race condition)
  • wait (somehow) for the popup to be loaded and send it data
  • …try to think of some out of box idea :slight_smile:, I would also like to know about some easy to use, understand, and reason about solution

I tried:

browser.commands.onCommand.addListener((command) => {
   if (command == "toggle-feature") {
      let querying = browser.tabs.query({ active: true, currentWindow: true });
      querying.then(logTabs, onError);
    
      function logTabs(tabs) {
         console.log(tabs);
         const executing = browser.tabs.executeScript(tabs[0].id, {
            code: 'getSelection()+""',
         });

         executing.then(onExecuted, onError);
         function onExecuted(result) {
            browser.browserAction.setPopup({ popup: "My URL" + result[0] });
           browser.browserAction.openPopup();
         }

         function onError(error) {
            console.log(`Error: ${error}`);
         }
      }

     function onError(error) {
        console.log(`Error: ${error}`);
     }
  }
});

but same error

image

appears.

Nope, that’s not gonna work.
The “openPopup” can be called only in the browser.commands.onCommand handler before you call any other async functioction, or more importantly, before you await any async call.

So you will have to first open the popup and then do all the async operations and then somehow get the data to the opened popup.

That’s the point: I don’t know how to do it :sweat_smile:

Start with something that works:

browser.commands.onCommand.addListener(async (command) => {
  if (command == "toggle-feature") {
    browser.browserAction.setPopup({ popup: "My URL" });
    browser.browserAction.openPopup();

    // do all async operations here:
    // then store results with `browser.storage.local.set`
  }
});

This works, because the popup is opened BEFORE any async operation is executed.
Then when you run your async functions, store the results in the storage.
Then in your popup load the data from the storage.

I thought I could just define a command in the manifest to open the browser action popup, and then send a message to the background script to update the popup URL. It seems to work, but the popup shows the previously selected word, so it’s always “a word behind”. This is, of course, due to the runtime setting of the page. It seems setting the URL of an opened popup doesn’t work.

Content script

window.addEventListener("keydown", function (event) { 
   if (event.ctrlKey && event.shiftKey && event.code === "KeyU") {
      browser.runtime.sendMessage({ content: String(window.getSelection()) });
   }
});

Background script

browser.runtime.onMessage.addListener(Notify);
async function Notify(message) {
   await browser.browserAction.setPopup({ popup: "My URL" + message.content });
}

Manifest

...
"commands": {
   "_execute_browser_action": {
      "suggested_key": {
         "default": "Ctrl+Shift+U"
      }
   }
}

That’s some out of box idea :smiley:.
Maybe selectionchange event would be better than keydown.

Or, maybe even better, send message from Popup script to the current tab to get the text selection. Or call the executeScript directly from the Popup.

You are constraining yourself with using the popup: "My URL" + message.content way to pass the info to the popup.

I’ll definitely take your suggestions into account. Also, I found your https://fastaddons.com/ and your profile. I’m really interested in your work in the add-ons world :wink:

1 Like

Trying to use the executeScript approach, the popup page is updated only on the first opening action. Subsequent opening actions don’t update the URL. Also, the executeScript doesn’t correctly shows the currently selected text, just the first one that was previously selected. This is the JavaScript code of the popup (browser action) page:

const querying = browser.tabs.query({ currentWindow: true, active: true });
querying.then(
  function (queryingSuccess) {
    const executeScript = browser.tabs.executeScript(queryingSuccess[0].id, { code: "window.getSelection().toString();" });
    executeScript.then(
      function (executeScriptSuccess) {
        browser.browserAction.setPopup({ popup: "https://..." }); // sets only on the first opening action
      },
      function (executeScriptError) {
        console.log(executeScriptError);
      }
    );
  },
  function (queryingError) {
     console.log(queryingError);
  }
);

This looks almost good! :slight_smile:

But if this code is already running in the popup, and you need the selected text in the popup, then why do you still use browser.browserAction.setPopup to change the popup URL? Note that this will change the URL of the popup when you open it next time, it won’t change current URL.

Also note that popup is actually a “normal web page”, just in a fancy popup container so you can use “location” object to change URL if needed.

And lastly, why did you stopped using “async/await”? It usually makes the code easier to read and understand.