Создание динамического контекстного меню в Chrome Extension не выполняется

Я пытаюсь создать записи в контекстном меню Chrome на основе выбранного. Я нашел несколько вопросов об этом в Stackoverflow, и для всех из них ответ: используйте контент script с слушателем "mousedown", который смотрит на текущий выбор и создает контекстное меню.

Я реализовал это, но это не всегда работает. Иногда все сообщения журнала говорят, что контекстное меню было изменено так, как я хотел, но появившееся контекстное меню не обновляется.

Основываясь на этом, я подозревал, что это состояние гонки: иногда хром начинает визуализировать контекстное меню до того, как код будет работать полностью.

Я попытался добавить eventListener в "contextmenu" и "mouseup". Более поздние триггеры, когда пользователь выбирает текст с помощью мыши, поэтому он меняет контекстное меню намного до его появления (даже секунд). Даже с этой техникой я все еще вижу ту же ошибку!

Это происходит очень часто в Chrome 22.0.1229.94 (Mac), иногда в Chromium 20.0.1132.47 (linux), и это произошло не через 2 минуты при попытке Windows (Chrome 22.0.1229.94).

Что происходит точно? Как я могу это исправить? Есть ли другой способ обхода?


Вот упрощенная версия моего кода (не так просто, потому что я сохраняю сообщения журнала):

manifest.json:

{
  "name": "Test",
  "version": "0.1",
  "permissions": ["contextMenus"],
  "content_scripts": [{
    "matches": ["http://*/*", "https://*/*"],
    "js": ["content_script.js"]
  }],
  "background": {
    "scripts": ["background.js"]
  },
  "manifest_version": 2
}

content_script.js

function loadContextMenu() {
  var selection = window.getSelection().toString().trim();
  chrome.extension.sendMessage({request: 'loadContextMenu', selection: selection}, function (response) {
    console.log('sendMessage callback');
  });
}

document.addEventListener('mousedown', function(event){
  if (event.button == 2) {
    loadContextMenu();
  }
}, true);

background.js

function SelectionType(str) {
  if (str.match("^[0-9]+$"))
    return "number";
  else if (str.match("^[a-z]+$"))
    return "lowercase string";
  else
    return "other";
}

chrome.extension.onMessage.addListener(function(msg, sender, sendResponse) {
  console.log("msg.request = " + msg.request);
  if (msg.request == "loadContextMenu") {
    var type = SelectionType(msg.selection);
    console.log("selection = " + msg.selection + ", type = " + type);
    if (type == "number" || type == "lowercase string") {
      console.log("Creating context menu with title = " + type);
      chrome.contextMenus.removeAll(function() {
        console.log("contextMenus.removeAll callback");
        chrome.contextMenus.create(
            {"title": type,
             "contexts": ["selection"],
             "onclick": function(info, tab) {alert(1);}},
            function() {
                console.log("ContextMenu.create callback! Error? " + chrome.extension.lastError);});
      });
    } else {
      console.log("Removing context menu")
      chrome.contextMenus.removeAll(function() {
          console.log("contextMenus.removeAll callback");
      });
    }
    console.log("handling message 'loadContextMenu' done.");
  }
  sendResponse({});
});

Ответы

Ответ 1

API contextMenus используется для определения записей в контекстном меню. Его не нужно вызывать прямо перед открытием контекстного меню. Таким образом, вместо создания записей в событии contextmenu используйте событие selectionchange, чтобы постоянно обновлять запись contextmenu.

Я покажу простой пример, который просто отображает выделенный текст в записи контекстного меню, чтобы показать, что записи синхронизированы хорошо.

Используйте этот контент script:

document.addEventListener('selectionchange', function() {
    var selection = window.getSelection().toString().trim();
    chrome.runtime.sendMessage({
        request: 'updateContextMenu',
        selection: selection
    });
});

На заднем плане мы собираемся создать запись contextmenu только один раз. После этого мы обновляем элемент contextmenu (используя идентификатор, который мы получаем из chrome.contextMenus.create).
Когда выбор пуст, мы удаляем запись контекстного меню, если это необходимо.

// ID to manage the context menu entry
var cmid;
var cm_clickHandler = function(clickData, tab) {
    alert('Selected ' + clickData.selectionText + ' in ' + tab.url);
};

chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
    if (msg.request === 'updateContextMenu') {
        var type = msg.selection;
        if (type == '') {
            // Remove the context menu entry
            if (cmid != null) {
                chrome.contextMenus.remove(cmid);
                cmid = null; // Invalidate entry now to avoid race conditions
            } // else: No contextmenu ID, so nothing to remove
        } else { // Add/update context menu entry
            var options = {
                title: type,
                contexts: ['selection'],
                onclick: cm_clickHandler
            };
            if (cmid != null) {
                chrome.contextMenus.update(cmid, options);
            } else {
                // Create new menu, and remember the ID
                cmid = chrome.contextMenus.create(options);
            }
        }
    }
});

Чтобы этот пример был прост, я предположил, что есть только одна запись в контекстном меню. Если вы хотите поддерживать больше записей, создайте массив или хеш для хранения идентификаторов.

Советы

  • Оптимизация. Чтобы уменьшить количество вызовов API chrome.contextMenus, кешируйте соответствующие значения параметров. Затем используйте простое сравнение ===, чтобы проверить, нужно ли создавать/обновлять элемент contextMenu.
  • Отладка. Все методы chrome.contextMenus являются асинхронными. Чтобы отладить ваш код, передайте функцию обратного вызова .create, .remove или .update.