7

UPDATE: I receive so many negatives recently at that it is a really old question(2.5 years). I don't even use jquery any more. So please give it a break!


I have already made some chrome extensions before but this is the first time I need to change something on the website the user is current looking at to respond to button clicks placed in the extension popup.

As I realized just executing some jQuery lines have no effect. What works is a method I found on google developer page sample extension: chrome.tabs.executeScript() but I have no clue why it's necessary.

There must be some basic concept I'm not aware of. Could anyone explain it to me? An can I execute jQuery (which is loaded) too?

Full example:

$('#change_button').on('click', function() {

  //this doesen't work
  $("body").css({backgroundColor: 'blue'});

  //but this line does
  chrome.tabs.executeScript(null, {code:"document.body.style.backgroundColor='red'"});

});

Actually I need jQuery badly to make some more changes in the DOM and respond to them i.e:

if( $(".list,.list-header-name").hasClass("tch"))
{
  $(".list,.list-header-name").addClass("tch");
}
Hexodus
  • 12,361
  • 6
  • 53
  • 72
  • I have contained code sample in my answer, you could call `tabs.executeScript` twice, the first is used to insert `jquery.js` to the current page while the other is used to execute your logic code, is it enough or anything else are you confused? – Haibara Ai Apr 08 '16 at 01:51

3 Answers3

3

Popup page lives in the context of extension, while the DOM in the current web page is another context, they are different processes so they need some ways to communicate, tabs.executeScript is a way to insert code into a page programmatically.

If you heavily depend on jQuery, instead of inserting code directly, you can inject file first then feel free to use the jquery method.

chrome.tabs.executeScript(null, {file: "jquery.js"}, function() {
    chrome.tabs.executeScript(null, {code:"$("body").css({backgroundColor: 'blue'})");

Besides Programming injection, you can also inject your content scripts by specifying matches field in manifest.json. In this way, when user clicks button in popup page, you can use Message Passing to communicate between extension process and content scripts, then use content scripts to manipulate the current page.

Haibara Ai
  • 10,703
  • 2
  • 31
  • 47
2

The Javascript you are running in your Chrome extension is run either in the background page or some popup page. It is NOT the page in your browser where you were when running the extension. That is why you need to executeScript inside a specific tab.

To visualize this better, right click on the button of your extension, and select Inspect Popup. The same for a background page, go to chrome://extensions to your (presumably) unpacked extension and click on background page, and you have developer tools to see what is going on.

In order to package some resources in your chrome extension so they can be used by direct access from web pages later on, you can use Web Accessible Resources. Basically you declare the files that you want accessible by the web pages, then you can load them in or inject them using a URL of the form "chrome-extension://[PACKAGE ID]/[PATH]". In order to use this mechanism and make the packaged jQuery library available to web pages, we add:

"web_accessible_resources": [
    "jquery-2.2.2.min.js"
]

in manifest.json.

In order to have access to web pages from the extension, you need to add permissions for it:

"permissions" : [
    "tabs",
    [...]
    "<all_urls>",
    "file://*/*",
    "http://*/*",
    "https://*/*"
],

also in manifest.json. <all_urls> is a special keyword not something I added there and it is a placeholder for any type of URL, regardless of schema. If you want only http and https URLs, just use the last two only.

Here is a function that loads jQuery in the page of a certain tab, assigning it the name ex$ so that it doesn't conflict with other libraries or versions of jQuery.

function initJqueryOnWebPage(tab, callback) {
    // see if already injected
    chrome.tabs.executeScript(tab.id,{ code: '!!window.ex$'},function(installed) {
        // then return
        if (installed[0]) return;
        // load script from extension (the url is chrome.extension.getUrl('jquery-2.2.2.min.js') )
        chrome.tabs.executeScript(tab.id,{ file: 'jquery-2.2.2.min.js' },function() {
            // make sure we get no conflicts
            // and return true to the callback
            chrome.tabs.executeScript(tab.id,{ code: 'window.ex$=jQuery.noConflict(true);true'},callback);
        });
    });
}

This function is loading jQuery from the extension to the web page, then executes a callback when it's installed. From then one can use the same chrome.tabs.executeScript(tab.id,{ code/file: to run scripts using jQuery on the remote page.

Since it uses the executeScript method with the file option, it doesn't need the web_accessible_resources declaration for the script

From a nice comment below, I learned about execution environment. executeScript runs in an "isolated world" so that it doesn't have access to global variables and whatever gets executed doesn't create something that can be accessed by the web page. So the entire complicated function above is overkill. You can still use this system if you do not want to interact with code on the page, but just with its DOM, and be unconcerned with any conflicts in code

But the extension does have access to the document of the tab. Here is a function that uses the web_accessible_resources mechanism described at first to create a script tag in the web page that then loads the jQuery library that is packaged in the extension:

// executeScript's a function rather than a string
function remex(tabId, func,callback) {
    chrome.tabs.executeScript(tabId,{ code: '('+func.toString()+')()' },callback);
}

function initJqueryOnWebPage(tab, callback) {
    // the function to be executed on the loaded web page
    function f() {
        // return if jQuery is already loaded as ex$
        if (window.ex$) return;
        // create a script element to load jQuery
        var script=document.createElement('script');
        // create a second script tag after loading jQuery, to avoid conflicts
        script.onload=function() {
            var noc=document.createElement('script');
            // this should display in the web page's console
            noc.innerHTML='window.ex$=jQuery.noConflict(true); console.log(\'jQuery is available as \',window.ex$);';
            document.body.appendChild(noc);
        };
        // use extension.getURL to get at the packed script
        script.src=chrome.extension.getURL('jquery-2.2.2.min.js');
        document.body.appendChild(script);
    };
    // execute the content of the function f
    remex(tab.id,f,function() { 
       console.log('jQuery injected'); // this should log in the background/popup page console
       if (typeof(callback)=='function') callback();
    });
}

It is cumbersome to send scripts to executeScripts, that is why I used the remex function above. Note that there is a difference between remex, which executes a function in the 'isolated world' environment and the code executed by the tab when a script tag is appended to the document. In both cases, though, what actually gets executed is strings, so be careful not to try to carry objects or functions across contexts.

Two consecutive remex calls will be executed in their own context, so even if you load jQuery in one, you won't have it available in the next. One can use the same system as in initJqueryOnWebPage, though, meaning adding the code to a script on the page. Here is a bit of code, use it just like remex:

function windowContextRemex(tabId,func,callback) {
    var code=JSON.stringify(func.toString());
    var code='var script = document.createElement("script");'+
        'script.innerHTML = "('+code.substr(1,code.length-2)+')();";'+
        'document.body.appendChild(script)';
    chrome.tabs.executeScript(tabId, {
        code : code
    }, function () {
        if (callback)
            return callback.apply(this, arguments);
    });
}

So here I have demonstrated:

  • executing code in an isolated context that has only access to the DOM of a page opened in a tab
  • executing packaged scripts in the same context as above
  • injecting scripts that run in the context of the page

What is missing:

  • a nice way of chaining code so that you don't have to create the script element manually
  • loading CSS files through insertCSS (akin to executeScript with a file option, but that load directly in the page
Community
  • 1
  • 1
Siderite Zackwehdex
  • 6,293
  • 3
  • 30
  • 46
  • Thank you for explaining me the reason why. But how then can I manipulate the website itself in a better way and using jQuery. I have no background page btw. – Hexodus Apr 04 '16 at 16:12
  • 1
    Ok, If the tab page would be the page that is opened in the current browser tab and the popup page is the page you get when you click on the extension button and pops up at you, in this scenario, I would imagine the popup page uses tabs.executeScript in order to load jQuery on the tab page, if not already there, then run whatever changes you need using jQuery. In this case you should be careful not to cause conflicts by the libraries you introduce or to use code that is not compatible with the jQuery version from the tab site, if you choose not to replace it. – Siderite Zackwehdex Apr 04 '16 at 17:19
  • I've updated my reply with a feature of Chrome extensions that allows you to access extension resources from web pages. – Siderite Zackwehdex Apr 08 '16 at 14:51
  • 1
    I think you're mixing up creating a `script` tag (requires `web_accessible_resource` and `noConflict`) with `executeScript` (always available and in the isolated world). And what are `x` and `callback` at the end? – Teepeemm Apr 08 '16 at 17:16
  • Sorry about the x, it was a previous refactoring to replace `chrome.tabs.executeScript(tab.id,{ code: ...`. The callback is the function passed as a parameter to initJqueryOnWebPage. It's what gets executed when jQuery has been successfully installed on the web page. – Siderite Zackwehdex Apr 08 '16 at 17:50
  • Thank you very much for that informative updates - this is the best answer to my question so you really deserve the reward ;) – Hexodus Apr 12 '16 at 12:14
  • @Siderite Downvoting, as I agree with Teepeeemm's comment. While you do a good job explaining some things, you mash together concepts in a pretty bad way that's not good for others. Due to [isolated context](https://developer.chrome.com/extensions/content_scripts#execution-environment), there will never be any conflict with copies of jQuery from other sources. [`web_accessible_resources`](https://developer.chrome.com/extensions/manifest/web_accessible_resources) does not matter for `executeScript` (see _"Content scripts themselves do not need to be whitelisted"_). – Xan Apr 21 '16 at 11:09
  • You are correct. I have updated the text of the answer with more complete research. – Siderite Zackwehdex Apr 21 '16 at 12:30
1

The thing you're missing is that the popup's javascript only affects the popup's DOM. To affect the tab's DOM, you need to use a content script (either from the manifest, or by injecting it).

Let's suppose you have this as popup.html:

<!doctype html>
<html>
<head>
<title>popup html</title>
<style>
body { padding: 3em; }
button { width: 10em; margin: 1ex; }
</style>
</head>
 <body>
  <p>
   <button id="onlyPopup">Color the popup</button>
   <button id="affectWebpage">Color the webpage</button>
   <button id="jqWebpage">Inject jQuery</button>
  </p>
  <script src="jquery.min.js"></script>
  <script src="popup.js"></script>
 </body>
</html>

And this as popup.js:

$('#onlyPopup').click(function() {
    $("body").css('backgroundColor','blue');
});

$("#affectWebpage").click(function() {
    chrome.tabs.executeScript(null, {code:"document.body.style.backgroundColor='red'"});
});

$("#jqWebpage").click(function() {
    chrome.tabs.executeScript(null, {file:"jquery.min.js"}, callback);
});

function callback() {
    chrome.tabs.executeScript(null, {code:"$('body').css('backgroundColor','yellow');"});
}

So your original "doesn't work" code actually does work, just not the way you expect. It colors the background of the popup, because that's where the code is executing. To execute a jQuery code in the tab, you need to first inject jQuery and then run your code (and the only permissio this needs is activeTab). The temptation would be

$("#jqWebpage").click(function() { // don't do this
    chrome.tabs.executeScript(null, {file:"jquery.min.js"});
    chrome.tabs.executeScript(null, {code:"$('body').css('backgroundColor','yellow');"});
});

But this won't work because of asynchronicity. These two lines will run before the file is fully injected. This means that the second line of code can (and probably will) run before jQuery is fully loaded, with unknown consequences. Instead, you need to use the callback parameter to execute the script you want after jQuery has finished loading.

Teepeemm
  • 4,331
  • 5
  • 35
  • 58
  • Thank you for the well made explanation but I needed a working solution which was provided by someone else. What a pitty that I cannot split the reward. Thanks anyways. – Hexodus Apr 12 '16 at 12:20