Communicate data from popup to content script injected by popup with executeScript()
Asked Answered
F

1

12

I have a text area and a button in my Chrome extension popup. I want users to input desired text in the text area. Then, once they click the button, it will inject a content script to change the text of the fields on the current page that have <textarea class="comments"> to the text that user entered in the <textarea> in the Chrome extension popup.

My question is, how can I get the text from the <textarea> in my popup.html and pass it from the popup.js to the content script?

This is what I have currently:

popup.html:

<!doctype html>  
<html>  
    <head><title>activity</title></head>  
<body>  
    <button id="clickactivity3">N/A</button> 
    <textarea rows="4" cols="10" id="comments" placeholder="N/A Reason..."></textarea>
    <script src="popup.js"></script> 
</body>
</html>  

popup.js:

function injectTheScript3() {
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
        // query the active tab, which will be only one tab
        //and inject the script in it
        chrome.tabs.executeScript(tabs[0].id, {file: "content_script3.js"});
    });
}

document.getElementById('clickactivity3').addEventListener('click', injectTheScript3);

content_script3:

//returns a node list which is good
var objSelectComments = document.querySelectorAll('.comments'); 

//desired user input how?
function setCommentsValue(objSelectComments,){

    //This will be for loop to iterate among all the text fields on the page, and apply
    //  the text to each instance. 
    for (var i=0; i<objSelectComments.length; i++) {
        objSelectComments[i]= //user's desired text 
    }
Fennec answered 17/11, 2016 at 2:3 Comment(6)
You can not pass DOM elements to or from content scripts. All information which is passed, via all methods, has to be able to be represented as JSON. DOM elements can not be represented as such.Varick
From the rest of your question, it appears that what you are really asking is how do you pass the text that the user has entered from your popup to your content script so the content script can use it to fill the <textarea> contents. Is that really what you are asking? BTW: Your selector would be better as 'textarea.comments'. Currently you are getting everything that has that class, not just the <textarea class="comments">.Varick
Hi Makyen, yes that is exactly what i am trying to do. Pass user text from the popup to fill all instances of the current tab page.Fennec
You may want to check Getting a variable from popup.html, to content script wherein it was suggested that you would need to use Message Passing to pass a variable to the content script. In addition to that, solution given in Chrome Extension Replace word in current textarea might also help.Innominate
@Varick Do you know of a way to do this ?Fennec
Possible duplicate of Pass a parameter to a content script injected using chrome.tabs.executeScript()Varick
V
25

There are three general ways to do this:

  1. Use chrome.storage.local (MDN) to pass the data (set prior to injecting your script).
  2. Inject code prior to your script which sets a variable with the data (see detailed discussion for possible security issue).
  3. Use message passing (MDN) to pass the data after your script is injected.

Use chrome.storage.local (set prior to executing your script)

Using this method maintains the execution paradigm you are using of injecting a script that performs a function and then exits. It also does not have the potential security issue of using a dynamic value to build executing code, which is done in the second option below.

From your popup script:

  1. Store the data using chrome.storage.local.set() (MDN)
  2. In the callback for chrome.storage.local.set(), call tabs.executeScript() (MDN)
var updateTextTo = document.getElementById('comments').value;
chrome.storage.local.set({
    updateTextTo: updateTextTo
}, function () {
    chrome.tabs.executeScript({
        file: "content_script3.js"
    });
});

From your content script:

  1. Read the data from chrome.storage.local.get() (MDN)
  2. Make the changes to the DOM
  3. Invalidate the data in storage.local (e.g. remove the key: chrome.storage.local.remove() (MDN)).
chrome.storage.local.get('updateTextTo', function (items) {
    assignTextToTextareas(items.updateTextTo);
    chrome.storage.local.remove('updateTextTo');
});
function assignTextToTextareas(newText){
    if (typeof newText === 'string') {
        Array.from(document.querySelectorAll('textarea.comments')).forEach(el => {
            el.value = newText;
        });
    }
}

See: Notes 1 & 2.

Inject code prior to your script to set a variable

Prior to executing your script, you can inject some code that sets a variable in the content script context which your primary script can then use:

Security issue:
The following uses "'" + JSON.stringify().replace(/\\/g,'\\\\').replace(/'/g,"\\'") + "'" to encode the data into text which will be proper JSON when interpreted as code, prior to putting it in the code string. The .replace() methods are needed to A) have the text correctly interpreted as a string when used as code, and B) quote any ' which exist in the data. It then uses JSON.parse() to return the data to a string in your content script. While this encoding is not strictly required, it is a good idea as you don't know the content of the value which you are going to send to the content script. This value could easily be something that would corrupt the code you are injecting (i.e. The user may be using ' and/or " in the text they entered). If you do not, in some way, escape the value, there is a security hole which could result in arbitrary code being executed.

From your popup script:

  1. Inject a simple piece of code that sets a variable to contain the data.
  2. In the callback for chrome.tabs.executeScript() (MDN), call tabs.executeScript() to inject your script (Note: tabs.executeScript() will execute scripts in the order in which you call tabs.executeScript(), as long as they have the same value for runAt. Thus, waiting for the callback of the small code is not strictly required).
var updateTextTo = document.getElementById('comments').value;
chrome.tabs.executeScript({
    code: "var newText = JSON.parse('"
          + JSON.stringify(updateTextTo).replace(/\\/g,'\\\\').replace(/'/g,"\\'") + "';"
}, function () {
    chrome.tabs.executeScript({
        file: "content_script3.js"
    });
});

From your content script:

  1. Make the changes to the DOM using the data stored in the variable
if (typeof newText === 'string') {
    Array.from(document.querySelectorAll('textarea.comments')).forEach(el => {
        el.value = newText;
    });
}

See: Notes 1, 2, & 3.

Use message passing (MDN) (send data after content script is injected)

This requires your content script code to install a listener for a message sent by the popup, or perhaps the background script (if the interaction with the UI causes the popup to close). It is a bit more complex.

From your popup script:

  1. Determine the active tab using tabs.query() (MDN).
  2. Call tabs.executeScript() (MDN)
  3. In the callback for tabs.executeScript(), use tabs.sendMessage() (MDN) (which requires knowing the tabId), to send the data as a message.
var updateTextTo = document.getElementById('comments').value;
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
    chrome.tabs.executeScript(tabs[0].id, {
        file: "content_script3.js"
    }, function(){
        chrome.tabs.sendMessage(tabs[0].id,{
            updateTextTo: updateTextTo
        });
    });
});

From your content script:

  1. Add a listener using chrome.runtime.onMessage.addListener() (MDN)
  2. Exit your primary code, leaving the listener active. You could return a success indicator, if you choose.
  3. Upon receiving a message with the data:
    1. Make the changes to the DOM
    2. Remove your runtime.onMessage listener

#3.2 is optional. You could keep your code active waiting for another message, but that would change the paradigm you are using to one where you load your code and it stays resident waiting for messages to initiate actions.

chrome.runtime.onMessage.addListener(assignTextToTextareas);
function assignTextToTextareas(message){
    newText = message.updateTextTo;
    if (typeof newText === 'string') {
        Array.from(document.querySelectorAll('textarea.comments')).forEach(el => {
            el.value = newText;
        });
    }
    chrome.runtime.onMessage.removeListener(assignTextToTextareas);  //optional
}

See: Notes 1 & 2.


Note 1: Using Array.from() is fine if you are not doing it many times and are using a browser version which has it (Chrome >= version 45, Firefox >= 32). In Chrome and Firefox, Array.from() is slow compared to other methods of getting an array from a NodeList. For a faster, more compatible conversion to an Array, you could use the asArray() code in this answer. The second version of asArray() provided in that answer is also more robust.

Note 2: If you are willing to limit your code to Chrome version >= 51 or Firefox version >= 50, Chrome has a forEach() method for NodeLists as of v51. Thus, you don't need to convert to an array. Obviously, you don't need to convert to an Array if you use a different type of loop.

Note 3: While I have previously used this method (injecting a script with the variable value) in my own code, I was reminded that I should have included it here when reading this answer.

Varick answered 17/11, 2016 at 22:20 Comment(1)
Thank you very much for the detailed answer! I admire your expertise, hopefully i can get to your level one day :)Fennec

© 2022 - 2024 — McMap. All rights reserved.