如何创建全局热键以在 Firefox (WebExtensions) 中打开“browserAction”弹出窗口?

IT技术 javascript firefox-addon-webextensions
2021-02-25 13:04:19

Chrome 似乎没有打开弹出窗口的 API,但有一个专用系统可以使用热键执行此操作:_execute_browser_action键入commands.

Firefox不支持_execute_browser_actionin的特殊功能commands( 1 )。

我关心的弹出窗口类型是browserAction,不是pageAction

browserAction当按下键盘快捷键/热键组合时,如何打开弹出窗口?

3个回答

在 Firefox 版本 >= 52 中本机可用

此功能将在 Firefox 52 中提供,该版本目前为Firefox 开发人员版(即 Firefox 52.0a2)。如您所知,对于 WebExtensions,您可以使用为_execute_browser_action键提供的对象内的键创建全局热键commands例如:

"commands":{
    "_execute_browser_action": {
        "suggested_key": {
            "default": "Alt+Shift+J"
        }
    }
}

打开一个伪弹出窗口(在旧版本的 Firefox 中填充此功能)

虽然显式功能要到 Firefox 52 才能使用,但您可以在当前版本的 Firefox 中通过定义一个名为"_execute_browser_action". 它看起来与您的普通弹出窗口有点不同,但它会起作用。它将在一个面板中,您可能需要考虑一些相关的样式,这些样式仅在它位于面板中而不是弹出窗口中时才应用。当您的面板打开时,活动选项卡的内容也可能存在一些差异。但是,下面的代码至少说明了在使用chrome.tabs.query(), 或执行查询时browser.tabs.query()如果响应是在真实弹出窗口而不是面板中打开,则响应是预期的。

相同的代码将继续在 Firefox 52+ 上工作。在 Firefox 52+ 上,"_execute_browser_action"直接激活浏览器操作单击或弹出。

因为当您不使用弹出窗口时,主要的是您不为browserAction.onClicked侦听器使用匿名函数这允许commands.onCommand侦听器也调用该功能commands.onCommand是在Firefox 48推出,所以这应该在任何版本,这是48+工作。

除了使用这个 polyfill之外可能会activeTab遇到一些需要权限的问题究竟需要什么,如果有的话,将取决于您的代码。

以下是一个扩展,当您按下键盘快捷键 Alt-Shift-J 时,它会导致通过浏览器操作按钮调用的功能被执行。它将激活该doActionButton()功能,或者,如果定义了一个弹出窗口,它将作为一个面板打开您的弹出窗口,该面板的行为类似于弹出窗口的正常行为,但它并不完美。它从当前为当前活动选项卡定义的弹出文件的名称中获取弹出文件的名称,就像单击browserAction按钮的情况一样。

清单.json

{
    "description": "Polyfill browserAction keyboard shortcut, including popups.",
    "manifest_version": 2,
    "name": "Polyfill browserAction keyboard shortcut",
    "version": "0.1",

    "background": {
        "scripts": [
            "background.js"
        ]
    },

    "browser_action": {
        "default_icon": {
            "32": "myIcon.png"
        },
        "default_title": "Open popup",
        "default_popup": "popup.html"
    },
    
    "commands": {
        "_execute_browser_action": {
            "suggested_key": {
                "default": "Alt+Shift+J"
            }
        }
    }
}

背景.js

chrome.browserAction.onClicked.addListener(doActionButton);

function doActionButton(tab){
    console.log('Action Button clicked. Tab:',tab);
}

chrome.commands.onCommand.addListener(function(command) {
    //Polyfill the Browser Action button
    if(command === '_execute_browser_action') {
        chrome.tabs.query({active:true,currentWindow:true},function(tabs){
            //Get the popup for the current tab
            chrome.browserAction.getPopup({tabId:tabs[0].id},function(popupFile){
                if(popupFile){
                    openPopup(tabs[0],popupFile);
                } else {
                    //There is no popup defined, so we do what is supposed to be done for
                    //  the browserAction button.
                    doActionButton(tabs[0]);
                }
            });
        });
        return;
    } //else
});

//popupWindowId can be true, false, or the popup's window Id.
var popupWindowId = false;
var lastFocusedWin;
var lastActiveTab;
function openPopup(tab,popupFile){
    chrome.windows.getLastFocused(function(win){
        lastFocusedWin=win;
        if(popupWindowId === false){
            //This prevents user from pressing the button quickly multiple times in a row.
            popupWindowId = true;
            lastActiveTab = tab;
            chrome.windows.create({ 
                url: popupFile, 
                type: 'popup',
            },function(win){
                popupWindowId = win.id;
                //Poll for the view of the window ID. Poll every 50ms for a
                //  maximum of 20 times (1 second). Then do a second set of polling to
                //  accommodate slower machines.
                //  Testing on a single moderately fast machine indicated the view 
                //  was available after, at most, the second 50ms delay.
                waitForWindowId(popupWindowId,50,20,actOnPopupViewFound,do2ndWaitForWinId);
            });
            return;
        }else if(typeof popupWindowId === 'number'){
            //The window is open, and the user pressed the hotkey combo.
            //  Close the window (as happens for a browserAction popup).
            closePopup();
        }
    });
}

function closePopup(){
    if(typeof popupWindowId === 'number'){
        chrome.windows.remove(popupWindowId,function(){
            popupWindowId = false;
        });
    }
}

chrome.windows.onRemoved.addListener(function(winId){
    if(popupWindowId === winId){
        popupWindowId = false;
    }
});

chrome.windows.onFocusChanged.addListener(function(winId){
    //If the focus is no longer the popup, then close the popup.
    if(typeof popupWindowId === 'number'){
        if(popupWindowId !== winId){
            closePopup();
        }
    } else if(popupWindowId){
    }
});

function actOnPopupViewFound(view){
    //Make tabs.query act as if the panel is a popup.
    if(typeof view.chrome === 'object'){
        view.chrome.tabs.query = fakeTabsQuery;
    }
    if(typeof view.browser === 'object'){
        view.browser.tabs.query = fakeTabsQuery;
    }
    view.document.addEventListener('DOMContentLoaded',function(ev){
        let boundRec = view.document.body.getBoundingClientRect();
        updatePopupWindow({
            width:boundRec.width + 20,
            height:boundRec.height + 40
        });
    });
    updatePopupWindow({});
}

function updatePopupWindow(opt){
    let width,height;
    if(opt){
        width =typeof opt.width  === 'number'?opt.width :400;
        height=typeof opt.height === 'number'?opt.height:300;
    }
    //By the time we get here it is too late to find the window for which we
    //  are trying to open the popup.
    let left = lastFocusedWin.left + lastFocusedWin.width - (width +40);
    let top = lastFocusedWin.top + 85; //Just a value that works in the default case.
    let updateInfo = {
        width:width,
        height:height,
        top:top,
        left:left
    };
    chrome.windows.update(popupWindowId,updateInfo);
}

function waitForWindowId(id,delay,maxTries,foundCallback,notFoundCallback) {
    if(maxTries--<=0){
        if(typeof notFoundCallback === 'function'){
            notFoundCallback(id,foundCallback);
        }
        return;
    }
    let views = chrome.extension.getViews({windowId:id});
    if(views.length > 0){
        if(typeof foundCallback === 'function'){
            foundCallback(views[0]);
        }
    } else {
        setTimeout(waitForWindowId,delay,id,delay,maxTries,foundCallback,notFoundCallback);
    }
}

function do2ndWaitForWinId(winId,foundCallback){
    //Poll for the view of the window ID. Poll every 500ms for a
    //  maximum of 40 times (20 seconds). 
    waitForWindowId(winId,500,40,foundCallback,windowViewNotFound);
}

function windowViewNotFound(winId,foundCallback){
    //Did not find the view for the window. Do what you want here.
    //  Currently fail quietly.
}

function fakeTabsQuery(options,callback){
    //This fakes the response of chrome.tabs.query and browser.tabs.query, which in
    //  a browser action popup returns the tab that is active in the window which
    //  was the current window when the popup was opened. We need to emulate this
    //  in the popup as panel.
    //The popup is also stripped from responses if the response contains multiple
    //  tabs.
    let origCallback = callback;
    function stripPopupWinFromResponse(tabs){
        return tabs.filter(tab=>{
            return tab.windowId !== popupWindowId;
        });
    }
    function stripPopupWinFromResponseIfMultiple(tabs){
        if(tabs.length>1){
            return stripPopupWinFromResponse(tabs);
        }else{
            return tabs;
        }
    }
    function callbackWithStrippedTabs(tabs){
        origCallback(stripPopupWinFromResponseIfMultiple(tabs));
    }
    if(options.currentWindow || options.lastFocusedWindow){
        //Make the query use the window which was active prior to the panel being
        //  opened.
        delete options.currentWindow;
        delete options.lastFocusedWindow;
        options.windowId = lastActiveTab.windowId;
    }
    if(typeof callback === 'function') {
        callback = callbackWithStrippedTabs;
        chrome.tabs.query.apply(this,arguments);
        return;
    }else{
        return browser.tabs.query.apply(this,arguments)
                                 .then(stripPopupWinFromResponseIfMultiple);
    }
}

WebExtensions 仍在开发中:

WebExtensions API 仍在开发中。每个版本的 Firefox 都在改进。目前,您可能最好使用Firefox Developer EditionFirefox Nightly(对于_execute_browser_action来开发和测试您的 WebExtension 附加组件您还应该仔细注意您希望使用的功能需要什么版本的 Firefox。此信息包含在 MDN 文档页面的“浏览器兼容性”部分中。


这个问题中代码的某些部分是从我的其他各种答案中复制/修改的。

支持_exectue_browser_action正在进行中:https : //bugzilla.mozilla.org/show_bug.cgi?id=1246034

同时,我很确定这是不可能的。

_exectue_browser_action, _execute_page_action, 已_execute_sidebar_action实现:特殊快捷方式