基于所选文本的动态扩展上下文菜单

21

我正在尝试基于所选内容创建Chrome上下文菜单条目。

我在Stackoverflow上找到了几个与此有关的问题,在所有这些问题中,答案都是:使用带有“mousedown”侦听器的内容脚本来查看当前选择并创建上下文菜单。

我已经实现了这一点,但它并不总是有效。有时所有日志消息都显示上下文菜单已按照我的要求修改,但出现的上下文菜单没有更新。

基于此,我怀疑这是一种竞争条件:有时Chrome在代码完全运行之前开始渲染上下文菜单。

我尝试添加“contextmenu”和“mouseup”的eventListener。后者会在用户用鼠标选择文本时触发,因此会在上下文菜单出现之前(甚至几秒钟之前)更改上下文菜单。即使使用这种技术,我仍然看到发生相同错误的情况!

这在Chrome 22.0.1229.94(Mac)中经常发生,在Chromium 20.0.1132.47(Linux)中偶尔发生,并且在Windows上尝试了2分钟(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
}

内容脚本.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({});
});

2
我已经在这个问题上设置了悬赏,因为我也遇到了同样的问题(在Mac上)。 - epoch
2个回答

39
contextMenus API 用于定义右键菜单的选项,不需要在打开右键菜单之前调用。因此,请使用 selectionchange 事件来持续更新右键菜单选项,而不是在 contextmenu 事件中创建选项。
以下是一个简单的示例,它只显示所选文本在右键菜单中的内容,以展示选项已经同步更新。
请使用此内容脚本:
document.addEventListener('selectionchange', function() {
    var selection = window.getSelection().toString().trim();
    chrome.runtime.sendMessage({
        request: 'updateContextMenu',
        selection: selection
    });
});

在后台,我们只会创建一次右键菜单项。之后,我们会使用从 chrome.contextMenus.create 获得的 ID 更新右键菜单项。
当选择内容为空时,如果需要,我们将删除右键菜单项。

// 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);
            }
        }
    }
});

为了让这个示例更简单,我假设只有一个上下文菜单条目。如果想要支持更多的条目,请创建一个数组或哈希表来存储ID。
提示:
- 优化 - 为了减少chrome.contextMenus API调用的数量,缓存参数的相关值。然后,使用简单的===比较来检查是否需要创建/更新上下文菜单项。 - 调试 - 所有chrome.contextMenus方法都是异步的。要调试您的代码,请将回调函数传递给.create.remove.update方法。

非常感谢,我会授予奖励,虽然我也很希望您能解释一下为什么之前的代码(在问题中)没有起作用,或者随机工作:)。 - epoch
2
当用户使用鼠标右键时,一个消息会异步地发送到背景页。然后背景页异步地删除所有现有的上下文菜单条目。最后,上下文菜单条目会被创建,也是异步地完成的。你可以想象这些操作需要相对较长的时间。在event.preventDefault()没有触发的情况下,contextmenu/mouseup事件被触发后,上下文菜单就会打开。不久之后,chrome.contextMenus.create方法就会完成。 - Rob W
1
我刚才说的有道理,但如果你想亲自看到它:在 chrome.contextMenus.create 方法之前放置 alert('Hi');。你会看到上下文菜单闪烁:它出现是因为右键单击事件,但隐藏是因为显示了模态对话框(警告)。请记住,alert 是在创建/更新上下文菜单条目之前放置的,因此这证明了 chrome.contextMenu.* 调用太晚了。 - Rob W
@RobW 我也遇到了同样的问题,但是似乎无法解决。我正在根据右键单击的元素以及上述描述的“文本选择”更新ContextMenu项。每次右键单击文本被选中时,都需要更新约30个项目,并且每次调用chrome.contextMenus.update()(项目在加载时创建)。我发现Chrome大约需要3秒钟来更新所有项目,如果我右键单击并查看项目,则最初它们仍然未更新。我认为对于25-30个项目,这会减慢速度。你能帮我降低更新时间吗? - Praveen Tiwari
3
这个答案仍然是最好的吗?似乎更直观的做法是在菜单被绘制时有一个监听器。 - maugch
显示剩余2条评论

3

menus.create() 的 MDN 文档,'title' 参数

你可以在字符串中使用 "%s"。如果你在菜单项中这样做,并且在显示菜单时页面上选择了一些文本,则所选文本将被插入到标题中。

https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/API/menus/create

因此

browser.contextMenus.create({
  id: 'menu-search',
  title: "Search '%s'",  // selected text as %s
  contexts: ['selection'], // show only if selection exist
})

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接