3

I'm trying to get the selected text from a web page after a hotkey like for example Ctrl+SHIFT+Number. I'm starting with the code from Firefox help.

The manifest.json:

{

  "description": "Native messaging example extension",
  "manifest_version": 2,
  "name": "Native messaging example",
  "version": "1.0",
  "icons": {
    "48": "icons/message.svg"
  },

  "applications": {
    "gecko": {
      "id": "ping_pong@example.org",
      "strict_min_version": "50.0"
    }
  },

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

  "commands": {
  "toggle-feature": {
    "suggested_key": {
      "default": "Ctrl+Shift+Y",
      "linux": "Ctrl+Shift+0"
    },
    "description": "Send a 'toggle-feature' event"
  }
},


  "browser_action": {
    "default_icon": "icons/message.svg"
  },

  "permissions": ["nativeMessaging"]

}

The JavaScript file:

    /*
On startup, connect to the "ping_pong" app.
*/
var port = browser.runtime.connectNative("ping_pong");

/*
Listen for messages from the app.
*/
port.onMessage.addListener((response) => {
  console.log("Received: " + response);
});

/*
On a click on the browser action, send the app a message.
*/
browser.browserAction.onClicked.addListener(() => {
  console.log("Sending:  ping");
  port.postMessage("ping");
});

browser.commands.onCommand.addListener(function(command) {
  if (command == "toggle-feature") {


    console.log("toggling the feature!");

    text1 = window.getSelection();
    console.log(text1);


  }
});

The debugger says:

Selection { anchorNode: null, anchorOffset: 0, focusNode: null, focusOffset: 0, isCollapsed: true, rangeCount: 0, caretBidiLevel: null }

The messaging works, the hotkey works, but I can't get the selected text. Is there another method which I need to use? I tried a lot of code all yesterday, but I didn't find how to do it. Sometimes I have another error from the debugger, but I can never get the selected text. It is a problem of focus? It is crazy!

I read the code from other add-ons. It seems they use that method but maybe it is in a popup window?

I'm on Debian Stretch, and Firefox 56. I tried on 2 computers.

Makyen
  • 31,849
  • 12
  • 86
  • 121
ce6999
  • 57
  • 4
  • I suggest you read the [Anatomy of a WebExtension](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Anatomy_of_a_WebExtension) page (perhaps work through reading the pages linked from there). It has overall architecture information which should help your understanding of how things are generally organized/done. You need to use a [content script](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Content_scripts) to interact with a web page (e.g. manipulate the DOM, listen to clicks on the page, get the current selection, etc.). – Makyen Oct 15 '17 at 08:10
  • However, what you are doing to get the selection (even if you had correctly placed the code in a content script) is not sufficient, You will need to use the longer, more complex code contained in this answer to: [Get the Highlighted/Selected text](https://stackoverflow.com/a/5379408). That's the `getSelectionText()` which is in the snippet, not the first version. – Makyen Oct 15 '17 at 08:15
  • Nothing works, I tried yesterday these function. And I retry now, but the same error. I desactivate all add-ons. You give me a link for content-script but I need to add this ? I don't understand – ce6999 Oct 15 '17 at 08:37
  • You *must* have a content script. You should read both links I provided, which should explain the architecture (i.e. that you *must* have a content script). You should use [`chrome.tabs.executeScript()`](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/executeScript) to inject the content script when you need it. You can, of course, use `browser.tabs.executeScript()`, but my preference is to use the `chrome.` namespace functions, as that maintains compatibility with Chrome. – Makyen Oct 15 '17 at 09:05
  • Ok, it works but only with permission I give of web site I want. How can I do for work on any website ? (I'm develop with files) – ce6999 Oct 15 '17 at 09:47

2 Answers2

3

To get the selected text you must use a content script. Given that you are initiating getting the selected text from a hotkey defined with a manifest.json commands, you're best off using tabs.executeScript() to inject the needed code when the user presses the hotkey.

The following adapts the code you have in the question to do only the portion which is defining the hotkey and adds getting the selection (based on the code in Get the Highlighted/Selected text) using tabs.executeScript() to inject into all frames in the activeTab.

It is possible for the user to have made a selection in each existing iframe. You will need to determine how you want to handle that. The code below gets the selection from each iframe. However, it currently discards all but the last selection found (the first result is the main frame). You may want to notify the user when they have selections in multiple frames. Note that Chrome does not permit selecting text in multiple frames, but Firefox does.

The following code is tested in both Firefox and Chrome.

manifest.json:

{
    "description": "Get selected text upon hotkey",
    "manifest_version": 2,
    "name": "Hotkey: get selected text",
    "version": "1.0",
    "icons": {
        "48": "icon.png"
    },
    "background": {
        "scripts": ["background.js"]
    },
    "commands": {
        "get-selected-text": {
            "suggested_key": {
                "default": "Ctrl+Shift+Y",
                "linux": "Ctrl+Shift+0"
            },
            "description": "Get the selected text from the active tab."
        }
    },
    "permissions": [
        "activeTab"
    ]
}

background.js:

chrome.commands.onCommand.addListener(function (command) {
    if (command == "get-selected-text") {
        chrome.tabs.executeScript({
            code: '(' + getSelectionText.toString() + ')()',
            //We should inject into all frames, because the user could have made their
            // selection within any frame, or in multiple frames.
            allFrames: true,
            matchAboutBlank: true
        }, function (results) {
            selectedText = results.reduce(function (sum, value) {
                //This checks all the results from the different frames to get the one
                //  which actually had a selection.
                if (value) {
                    if (sum) {
                        //You will need to decide how you want to handle it when the user
                        //  has things selected in more than one frame. This case is
                        //  definitely possible (easy to demonstrate).
                        console.log('Selections have been made in multiple frames:');
                        console.log('Had:', sum, '::  found additional:', value);
                    }
                    // Currently, we just discard what was obtained first (which will be
                    // the main frame).  You may want to concatenate the strings, but
                    // then you need to determine which comes first.  Reasonably, that
                    // means determining where the iframe is located on the page with
                    // respect to any other selection the user has made.  You may want
                    // to just inform the user that they need to make only one
                    // selection.
                    return value;
                }
                return sum;
            }, '');
            console.log('selectedText:', selectedText);
        })
    }
});

//The following code to get the selection is from an answer to "Get the
//  Highlighted/Selected text" on Stack Overflow, available at:
//  https://stackoverflow.com/a/5379408
//  The answer is copyright 2011-2017 by Tim Down and Makyen. It is
//  licensed under CC BY-SA 3.0, available at
//  https://creativecommons.org/licenses/by-sa/3.0/
function getSelectionText() {
    var text = "";
    var activeEl = document.activeElement;
    var activeElTagName = activeEl ? activeEl.tagName.toLowerCase() : null;
    if (
        (activeElTagName == "textarea") || (activeElTagName == "input" &&
        /^(?:text|search|password|tel|url)$/i.test(activeEl.type)) &&
        (typeof activeEl.selectionStart == "number")
    ) {
        text = activeEl.value.slice(activeEl.selectionStart, activeEl.selectionEnd);
    } else if (window.getSelection) {
        text = window.getSelection().toString();
    }
    return text;
}
Makyen
  • 31,849
  • 12
  • 86
  • 121
-1

I found a solution:

I select the text from any webpage, and I have the text in the background.js and after I can do what I want with the text. In my specific case, I use an external program (in python) to receive the selected text.

Manifest.json

{

  "description": "Native messaging + Hotkey + content-script messaging",
  "manifest_version": 2,
  "name": "getSelectedTextFromHotkey",
  "version": "1.0",
  "icons": {
    "48": "icons/message.svg"
  },

  "applications": {
    "gecko": {
      "id": "gettext@example.org",
      "strict_min_version": "50.0"
    }
  },

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

  "commands": {
  "toggle-feature": {
    "suggested_key": {
      "default": "Ctrl+Shift+4",
      "linux": "Ctrl+Shift+5"
    },
    "description": "Send the selected text"
  }
},


  "browser_action": {
    "default_icon": "icons/message.svg"
  },

 "content_scripts": [
  {
    "matches": ["<all_urls>"],
    "js": ["content-script.js"]
  }

  ],

  "permissions": [ "<all_urls>","nativeMessaging","webRequest"]

}

Background.js

    var port = browser.runtime.connectNative("gettext");

browser.runtime.onConnect.addListener(connected);

port.onMessage.addListener((response) => {
  console.log("Received: " + response);
});

function onExecuted(result) {
  console.log(`We executed`);
}

function onError(error) {
  console.log(`Error: ${error}`);
}

browser.commands.onCommand.addListener(function(command) {
  if (command == "toggle-feature") {
    console.log("toggling the feature!");
    var executing = browser.tabs.executeScript({ file: "/content-script.js",  allFrames: false });
    executing.then(onExecuted, onError);
  }
});

var portFromCS;

function connected(p) {
  portFromCS = p;
  portFromCS.onMessage.addListener(function(m) {
  console.log("message selected:")
  console.log(m);
  console.log("Sending:  ping");
  port.postMessage("ping");
  });
}

content-script.js

// content-script.js

var selectedText = getSelection().toString();

var myPort = browser.runtime.connect({name:"port-from-cs"});

myPort.postMessage(selectedText);

gettext.json

{
  "name": "gettext",
  "description": "Native messaging + Hotkey + content-script messaging",
  "path": "/home/marie/web-ext/gettext.py",
  "type": "stdio",
  "allowed_extensions": [ "gettext@example.org" ]
}

gettext.py

#!/usr/bin/python -u
# Note that running python with the `-u` flag is required on Windows,
# in order to ensure that stdin and stdout are opened in binary, rather
# than text, mode.

import sys, json, struct

# Read a message from stdin and decode it.
def getMessage():
  rawLength = sys.stdin.read(4)
  if len(rawLength) == 0:
      sys.exit(0)
  messageLength = struct.unpack('@I', rawLength)[0]
  message = sys.stdin.read(messageLength)
  return json.loads(message)

# Encode a message for transmission, given its content.
def encodeMessage(messageContent):
  encodedContent = json.dumps(messageContent)
  encodedLength = struct.pack('@I', len(encodedContent))
  return {'length': encodedLength, 'content': encodedContent}

# Send an encoded message to stdout.
def sendMessage(encodedMessage):
  sys.stdout.write(encodedMessage['length'])
  sys.stdout.write(encodedMessage['content'])
  sys.stdout.flush()
# BE CAREFUL, NEVER USE THE CONSOLE in the loop ! it stops the connection!!!
while True: 
  receivedMessage = getMessage()
  if (receivedMessage == "ping"):
    sendMessage(encodeMessage("pong"))

It seems to work well on Firefox.

ce6999
  • 57
  • 4