Search code examples
javascriptgoogle-chrome-extensiontabs

Changing tab index of a Chrome extension's Options page causes error


I am working on a Chrome extension intended to be active on Gmail pages. The extension also has an options page, where the user can modify the features of the extension. So far so good. But the standard behavior for the options page, when created, is to appear in the last (rightmost) tab in the current window. I instead wanted that tab to appear next to the Gmail tab. With any other programmatically created tab, the index property of the tab (governing its place relative to other tabs in a window) can be set immediately using the chrome.tabs API. But the options page tab is not created that way; instead (if you are using an event listener on the main page, as I am) a message has to be sent to activate the chrome.runtime.openOptions() command in a service worker page. I haven't found any way to specify the tab index for the options page directly in conjunction with that command.

Instead, I have resorted to changing the index of the options page tab immediately after it is created, like this:

content.js

$(document).on("click","#gmx-options-item",function(ev){ // when clicking the UI element for showing the options page
  chrome.runtime.sendMessage({command:"getTabIndex"},function(reply){ // get main tab index
    let optTabIdx = reply.tabIdx + 1; // determine value of intended options page tab index
    chrome.storage.local.set({optTabIdx:optTabIdx}); // store that value
    chrome.runtime.sendMessage({command:"openOptions"}); // now create the options page
  });
});

background.js

var replyObject = {};
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    switch(request.command){

      case "getTabIndex":  // called by content.js to retrieve main tab index
        chrome.tabs.query({active: true, currentWindow: true, highlighted: true}, function (tabs) {
          let currTabIdx = tabs[0].index;
          replyObject = {tabIdx : currTabIdx}
          sendResponse(replyObject);
        });
      break;

      case "openOptions": // called by content.js to create options page
        chrome.runtime.openOptionsPage();
      break;

      case "setOptionTabIndex": // called by options.js to adjust tab index of options page
        chrome.tabs.query({active: true,lastFocusedWindow:true},function(tab){
          let id = tab[0].id;
          chrome.tabs.move(id,{index:request.optTabIdx});
        });    
      break;
    };
    return true;
});

options.js

chrome.storage.local.get({ // read values from storage
 //<other default values>,
    optTabIdx:0
  },function(stored){
    chrome.runtime.sendMessage({command:"setOptionTabIndex",optTabIdx:stored.optTabIdx}) // send command to background.js to change tab index to stored value (stored.optTabIdx)
});

Now, this does work, although it will cause the newly created options page tab to briefly slide across the window, something I could do without. However, it also generates an error, which appears after a few minutes:

Unchecked runtime.lastError: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received

I have tried to read up on this error here at SO, but I haven't found anything that helps in this case. My suspicion is that the movement of the tab causes the "message channel" to close. Is there a way to avoid this error? It does not seem to cause any malfunctioning to my extension, but an error is an error.


Solution

  • Don't use query + the active tab because the user or another extension may have switched it.

    Use sender of the message and open the options URL explicitly in the desired position.

    // content

    $(document).on('click', '#gmx-options-item', () => {
      chrome.runtime.sendMessage({command: 'openOptions'});
    });
    

    // background

    chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
      if (msg.command === 'openOptions') openOptions(sender.tab);
    });
    
    async function openOptions(tab) {
      const url = 'chrome://extensions/?options=' + chrome.runtime.id;
      const [optTab] = await chrome.tabs.query({url});
      if (optTab) {
        return chrome.tabs.update({active: true});
      }
      if (!tab) {
        [tab] = await chrome.tabs.query({active: true, currentWindow: true});
      }
      return chrome.tabs.create({
        index: tab.index + 1,
        openerTabId: tab.id,
        windowId: tab.windowId,
        url,
      });
    }