Pass info,tab through Shortkey/command listener like with a context menu listener

Hey guys,

I’m having troubles using shortkeys/commands.
In my extension I use a context menu, doing this for example:

browser.contextMenus.onClicked.addListener(contextMenuAction);

browser.contextMenus.create({
id: "1 Host Connect",
title: "1 Host Connect",
contexts: ["selection"]
});

function contextMenuAction(info, tab) {
switch (info.menuItemId) {
case "1 Host Connect":
browser.tabs.create({index: 1,url: "xyz://"+info.selectionText.trim()+"/"});
break;
}
}

That works well and as expected. Now I want to achieve the same thing with a shortcut using commands.
So I defined ALT+1 for the above context menu point action. But I’m having troubles making it work, passing the same information as with browser.contextMenus.onClicked.addListener(contextMenuAction), meaning tab,info.

How can I achieve the same result uPreformatted textsing commands? Doing something like this doesn’t work since info and tab are not defined:

function HotkeyContextMenuAction(info, tab) {
browser.tabs.create({index: 1,url: "xyz://"+info.selectionText.trim()+"/"});
}

browser.commands.onCommand.addListener(function(command,info,tab) {
if (command == "1 Host Connect") {
console.log('HOTKEY');
HotkeyContextMenuAction(info,tab);
}
});

Now, I am a total beginner when it comes to extension development, so please bear with me.
Many thanks i advance!!!

Kind regards,
David

You can get the currently focused tab using the browser.tabs API, since that’s likely the tab the shortcut should be executed in. To get the selected text you will have to inject a script that gets the text and returns it to you. browser.tabs.execScript should be fine for that. You’ll also need the activeTab permission to do that easily.

I tried the following:

browser.commands.onCommand.addListener(function(command) {
  if (command == "1 Host Connect") {
     console.log('HOTKEY');
     console.log(command);
     browser.tabs.executeScript({
     code: `console.log('SELECTION: '+window.getSelection().toString());`
      });
  }
});

but the string appears to be empty… :frowning:

So the page debugger shows 'SELECTION: ' but not the text you selected in the top frame of that tab? That would be odd …

1 Like

Exactly!! Output is:

HOTKEY 
1 Host Connect 
SELECTION:

Ok… So it actually does works just not where I need it to work!
If I execute the code via shortcut on a regular html page it works fine, the string is shown.
But I’ll be using the extension with an icinga2 (https://github.com/Icinga/icinga2) webpage. That’s where I was testing it before and there it doesn’t work!! Don’t know why though…
Maybe you guys have an idea???

When I inspect the page element (again, don’t know anything about html whatsoever…) I see:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

When I inspect the text that I’m selecting and trying to get through my extension I see:

Does that information somehow explain why it doesn’t work?

What do you get if you temporarily change the line to

code: `console.log('SELECTION: ' + typeof window.getSelection().toString());`

Then I’ll get:

SELECTION: string

No matter which string I select on the page, result is always the same!

Well, that’s better than null or undefined, but not much help.

I’m guessing because the text is in a child element you need to use selectAllChildren()

eg. maybe

code: `console.log('SELECTION: ' + window.getSelection().selectAllChildren().toString());`

That doesn’t work. Console.log says that it need an argument:

Error: Not enough arguments to Selection.selectAllChildren.

Doc says:

footer = document.getElementById("footer");
window.getSelection().selectAllChildren(footer);

But I really have no idea what to do with document.getElementById

Pretty sure selectAllChildren changes the selection, especially since it says “Previous selection is lost”.

I hinted at this before, but are you making the text selection in the same frame that you call window.getSelection().toString() in? If not, the returned string is expected to be empty.

Also, console.log() acceprs variable arguments and logs them all, you shouldn’t explicitly + them, which casts everything to strings (at least if one of them is a string).

1 Like

Gosh… this was (for me) so much easier with SDK. The extension worked well without having to worry about different frames, code injection and all that.

So let me try to understand this…
A html page can have multiple frames… Top and child frames.
The shortcut and its selection works on a regular html page because it doesn’t have child frames… there is just a top frame!?

However, on my icinga page there are child frames, so making the selection on the same frame (the top frame?) would return an empty string because the actual selection is on a child frame?

How can I access those child frames?
I read that content scripts can be injected into all frames using something like:

"content_scripts": [
    {
      "js": ["my-script.js"],
      "matches": ["https://example.org/"],
      "match_about_blank": true,
      "all_frames": true
    }
  ]

My extension consist of a manifest.json and a background script icinga_addon.js.

{
  "manifest_version": 2,
  "name": "Inc Icinga Shortcuts",
  "version": "1.0",
  "description": "Icinga Shortcuts",
  "permissions": ["contextMenus","activeTab"],
  
  "commands": {
    "1 Host Connect": {
      "suggested_key": {"default": "Alt+1" } ,
      "description": "1 Host Connect"
    }
   },
  
  "background":  {
      "scripts": ["icinga_addon.js"] 
    }
}

How could I implement this?

Many thanks in advance… really appreciate it!!

Yes, that is a pretty precise description for a possible, and given the type of page you are working on I think likely, cause of what you encounter here.

How can I access those child frames?

You want to access the selected text in the background page, right?
Haven’t tested it, but this should work:

const pageSelection = (await Promise.all(
    (await browser.webNavigation.getAllFrames({ tabId, }))
    .map(({ frameId, }) => 
        browser.tabs.executeScript(tabId, { frameId, code: `window.getSelection().toString()`, })
        .catch(_ => '') //in production, avoid weird errors about uninitialized frames and stuff like that, but when debugging empty selection texts again, comment this line
    )
)).reduce((a, b) => a || b, '') || '';

You wouldn’t want static content_scripts in the manifest for this.

1 Like

Thx Niklas,

Couldn’t make it work yet. Debugger always claims that there is a missing ) in line

const pageSelection = (await Promise.all(

Don’t understand why yet. Will check it next thing tomorrow morning though…

The error message isn’t that helpful. You have to putthe code in an async function:

browser.commands.onCommand.addListener(async command => { switch (command) {
    case '1 Host Connect': {
        const { id: windowId, } = (await browser.windows.getLastFocused());
        const { id: tabId, } = (await (browser.tabs.query({ active: true, windowId, })))[0];
        const pageSelection = (await Promise.all(
            (await browser.webNavigation.getAllFrames({ tabId, }))
            .map(({ frameId, }) => 
                browser.tabs.executeScript(tabId, { frameId, code: `window.getSelection().toString()`, })
                .catch(_ => '') // in production, avoid weird errors about uninitialized frames and stuff like that, but when debugging empty selection texts again, comment this line
            )
        )).reduce((a, b) => a || b, '') || '';
        // use `pageSelection`
    } break;
    // other commands
} });

Again, net tested, but it is syntactically correct and should work.

1 Like

Hi Niklas thank you once again!! I’m not getting an syntax errors anymore… but everything after const pageSelection doesn’t seem to fire…
If I simply implement a
console.log('End host connect');
for
// use pageSelection
I’ll get nothing in the console… noerror either. Any idea?

Huh. Not really. I’d throw a bunch of console.logs in there and see what happens:

code with logging
browser.commands.onCommand.addListener(async command => { switch (command) {
    case '1 Host Connect': {
        const { id: windowId, } = (await browser.windows.getLastFocused());
        console.log(`windowId`, windowId);
        const { id: tabId, } = (await (browser.tabs.query({ active: true, windowId, })))[0];
        console.log(`tabId`, tabId);
        const pageSelection = (await Promise.all(
            (await browser.webNavigation.getAllFrames({ tabId, }))
            .map(async ({ frameId, }) => { try {
                console.log(`Checking frame ${frameId} in tab ${tabId} ...`);
                const selected = (await browser.tabs.executeScript(tabId, { frameId, code: `window.getSelection().toString()`, }));
                console.log(`Selection in frame ${frameId} in tab ${tabId}:`, selection);
                return selection;
            } catch (_) { console.error(_); return ''; } }) // in production, avoid weird errors about uninitialized frames and stuff like that, but when debugging empty selection texts again, comment this line
        )).reduce((a, b) => a || b, '') || '';
        console.info(`Selection in some frame in the currently active tab ${tabId}`: pageSelection);
        // use `pageSelection`
    } break;
    // other commands
} });
1 Like

It’s really odd…
Implementing your code I only get:

windowId 5 icinga_addon.js:672:9
tabId 2 icinga_addon.js:674:9

Thought this is some weird Icinga thing again, but even on a regular html page I can’t get more output.