7

Alright, so I'm changing the color scheme of a site via an extension, it's my first time using content_scripts so yes, I am a complete newbie, feel free to treat me as one.

The problem is tabs.connect it isn't working, I need the tab id or something? Here's what I have so far:

manifest.json:

{
  "manifest_version": 2,

  "name": "ROBLOX Color Scheme",
  "description": "Edit the color scheme of the roblox bar! Note: Not created by roblox.",
  "version": "1.0",

  "permissions": [
    "<all_urls>",
    "tabs"
  ],
  "browser_action": {
    "default_icon": "Icon.png",
    "default_popup": "Popup.html"
  },
  "content_scripts": [
    {
      "matches": ["http://www.roblox.com/*"],
      "js": ["ContentScript.js"]
    }
  ]
}

Popup.html:

<!DOCTYPE html>
<html>
    <head>
        <p>Choose a color:</p>
        <input type="color" id="Color" value="">
        <button type="button" id="Button">Change Color!</button>
    </head>
    <body>
        <script src="Script.js"></script>
    </body>

</html>

Script.js:

function ChangeColor() {
  var TabId;
    chrome.tabs.query({currentWindow: true, active: true}, function(tabArray) {
      TabId = tabArray[0];
    });
  var port = chrome.tabs.connect(TabId, {name: "ColorShare"});
  port.postMessage({Color: document.getElementById("Color").value});
}

document.getElementById('Color').addEventListener("click", ChangeColor);

ContentScript.js:

var Color;
chrome.runtime.onConnect.addListener(function(port) {
  if (port.name == "ColorShare") then {
    port.onMessage.addListener(function(msg) {
      Color = msg.Color;
    });
  }
});

document.getElementsByClassName("header-2014 clearfix")[0].style.backgroundColor = Color;

All help is appreciated, thanks for taking your time to answer my question!

EDIT: Some files have changed now thanks to myself and the help of someone who answers, these now produce no errors, but nothing changes, any help you could possibly give would be great! Here are the current codes:

Script.js:

chrome.tabs.query({currentWindow: true, active: true}, function(tabArray) {
    var TabId = tabArray[0].id;
    var port = chrome.tabs.connect(TabId, {name: "ColorShare"});

    function ChangeColor() {
        port.postMessage({Color: document.getElementById("Color").value});
    }
    document.getElementById('Color').addEventListener("click", ChangeColor);
});

ContentScript.js:

chrome.runtime.onConnect.addListener(function(port) {
    if (port.name == "ColorShare") {
        port.onMessage.addListener(function(msg) {
            document.querySelector("header-2014 clearfix").style.backgroundColor = msg.Color;
        });
    }
});

Edit: This problem was solved. I had to use chrome.storage.sync.set and chrome.storage.sync.get which has full support for content scripts! I'll post the scripts used soon!

warspyking
  • 3,045
  • 4
  • 20
  • 37

5 Answers5

4

I think you’re misunderstanding the idea of a port. A port is used for several messages within a session. But the port is destroyed when the session ends (for example, when the tab is closed). Regardless, you shouldn’t be recreating the port on each click.

One of your comments mentioned refreshing the page, which leads me to think you want this color to persist across page reloads. This makes sense from a user interface perspective, but you really should have mentioned that at the beginning.

As I said, ports get destroyed when the tab is closed (or reloaded). If you want the value to persist, you’ll need a storage mechanism such as chrome.storage (you could also use local storage, but the preceding link gives several reasons why not to).

manifest.json just needs "permissions": [ "activeTab", "storage" ],. You probably also want a page action instead of a browser action (or neither, I’ll get to that).

ContentScript.js:

var myBgColor = false;

chrome.storage.sync.get("myBgColor",function(items) {
    if ( items.myBgColor ) {
        myBgColor = items.myBgColor;
        document.querySelector(".navbar").style.backgroundColor = myBgColor;
    }
});

chrome.runtime.onMessage.addListener(function(request,sender,sendResponse) {
    if ( request.setColor ) {
        document.querySelector(".navbar").style.backgroundColor = request.setColor;
        chrome.storage.sync.set({"myBgColor":request.setColor});
    } else if ( request.getColor ) {
        sendResponse({"myBgColor":myBgColor});
    }
});

I changed the argument to querySelector, since I couldn’t find the elements you were looking for on the index page.

Popup.html:

<!DOCTYPE html>
<html>
<body>
<p>Choose a color:</p>
<input type="color" id="color" value="">
<button type="button" id="button">Change Color!</button>
<script src="Script.js"></script>
</body>
</html>

I’m not sure why you had your inputs in the head of the page.

Script.js (but please rename your file to something more descriptive than Script.js):

document.getElementById('button').addEventListener("click",function() {
    chrome.tabs.query({currentWindow: true, active: true},function(tabArray) {
        chrome.tabs.sendMessage(tabArray[0].id,{"setColor":document.getElementById("color").value});
    });
});

chrome.tabs.query({currentWindow: true, active: true}, function(tabArray) {
    chrome.tabs.sendMessage(tabArray[0].id,{"getColor":1},setCurColor);
});

function setCurColor(response) {
    if ( response.myBgColor ) {
        document.getElementById("color").value = response.myBgColor;
    }
}

We’d like to have ContentScript.js onloading message Script.js if there’s a previously set background color. Unfortunately, Script.js only exists when we’ve clicked on the action icon. So we have Script.js ask ContentScript.js for the current color (if it’s been set).

As Ruwanka Madhushan noticed, your original script was failing (in part) because you were assuming the asynchronous chrome.tabs.query would complete before proceeding to the next line of code. Asynchronous javascript is very powerful, but it gives you the responsibility of not assuming the code has completed. You’ll need to use nested function calls, either anonymously (as with Script.js’s onclick) or by naming a utility function (as with setCurColor). (There are also javascript libraries to help with asynchronous functions, but I don't know them.)

Everything works, but there’s a slight problem: Popup.html closes when it loses focus - in this case, when you click on the color chooser input. The (really bad) work around is to bring up the popup and right click and select “Inspect Element”. This brings up the console for the popup, and prevents the popup from closing while you select the color. Another option may be to embed the color chooser in an iframe within the popup (I don’t know if this is possible).

But since we’re talking about options for your extension, the best alternative may be to use an options page. This would also give much more room to your html. For example, you may want to consider a button to delete localStorage.myBgColor so that you can get the default back. Or other options to customize the site, since I’m hoping you’re not going to all this trouble just to change a color. And it would hide the action icon, since presumably you’re going to set your options and then want to forget about the extension existing.

Community
  • 1
  • 1
Teepeemm
  • 4,331
  • 5
  • 35
  • 58
  • Thanks for setting me on the right track (and the example code) I'll look into the options page link. It looks as though I should restart the code all over, +1 for the explaination, and best answer since it actually helps (not that I'm not thankful for the other 2 answers) – warspyking Sep 04 '14 at 17:40
  • -1 for advocating `localStorage` in content scripts, because `localStorage` in content script are tied to the domain of the page. You're polluting the storage spaec of third party websites with extension data. Use `chrome.storage` instead. The other answers have correctly addressed the issues in the question, and the code at the end of the question should work as expected, unless the extension runtime reloads. Please don't advocate localStorage and preferably explain why the code at the end of the question was not working, then I'll remove the -1. – Rob W Sep 04 '14 at 20:17
  • (and `chrome.runtime.sendMessage` uses Ports under the hood (create port, send message, destroy port); your suggested method is actually *less* efficient than using `chrome.runtime.conntect`) – Rob W Sep 04 '14 at 20:19
  • @RobW `localStorage` has many negatives compared to `chrome.storage`, but the reason you provided ("polluting the storage spaec of third party websites with extension data") is not one of them. This is because Chrome extensions exist in a separate sandbox. – thdoan Jul 28 '16 at 05:20
  • @10basetom Content scripts do not run in a separate sandbox. They share the DOM with the page, including `localStorage`. Only the execution environment is isolated. – Rob W Jul 28 '16 at 07:27
  • @RobW I've been using localStorage in my content scripts for many years since `chrome.storage` wasn't available at the time, and entering `localStorage` in the console returns an empty object. It's the same reason why you cannot access the page's global JS variables from a content script, and inversely why you cannot access a content script's global variables from the console. The only thing that is shared across the domains is the DOM. With this said, you are right in that `chrome.storage` is the way to go and I'm in the process of migrating to that API. – thdoan Jul 28 '16 at 14:15
  • P.S. I think this is where the confusion lies: "They share the DOM with the page, including localStorage". `localStorage` is part of JS, not the DOM. For more info on this topic, I encourage you to read the "Execution environment" section on https://developer.chrome.com/extensions/content_scripts – thdoan Jul 28 '16 at 14:34
  • @10basetom `localStorage` IS part of the DOM. It is tied to the origin of the page, not the extension. If you save a value in localStorage in a content script, then the background/options page cannot use it. The web page in which the content script runs can however access it. Just to make sure that this is indeed correct, I confirmed my claimed behavior in Chromium 51.0.2683.0. – Rob W Jul 28 '16 at 22:29
  • @RobW I also created a dummy extension to test out this claim, and sure enough, you are correct. At first I thought I was going crazy, but after looking at my own extension's code, it turns out I was using `localStorage` on the options page, not in the content script. Apologies for wasting your time setting me straight :). – thdoan Jul 29 '16 at 10:24
2

Alright, what you're attempting to do is do this with message passing. I see how you may have thought long-lived ones was the way to go, but it's not. You'd have to use a storage system, particularly chrome.storage which has full compatibility with content scripts!

Instead of using a browser action, you can simply create an options page, which will save the color to chrome.storage, where content scripts can then retrieve it. The options page is just some simple HTML, but the code could be a bit complicated for someone just starting out.

The options page you'd want would be something like this...

<!DOCTYPE html>
<html>
<head><title>Choose A ROBLOX Color</title></head>
<body>

<h>Color:</h>
<input type="color" id="Color" value="">

<div id="Status"></div>
<button id="Save">Save</button>

<script src="Options.js"></script>
</body>
</html>

Options.js takes the options, and saves it to chrome.storage, by using the options chosen on the above HTML page. It does this using chrome.storage.sync.set

Options.js would look a little like this;

// Saves options to chrome.storage
function save_options() {
  var color = document.getElementById('Color').value;
  chrome.storage.sync.set({
    Color: color
  }, function() {
    // Update status to let user know options were saved.
    var status = document.getElementById('Status');
    status.textContent = 'Options saved.';
    setTimeout(function() {
      status.textContent = '';
    }, 750);
  });
}

// Restores select box and checkbox state using the preferences
// stored in chrome.storage.
function restore_options() {
  // Use default value color = 'red'
  chrome.storage.sync.get({
    Color: 'red'
  }, function(items) {
    document.getElementById('Color').value = items.Color;
  });
}
document.addEventListener('DOMContentLoaded', restore_options);
document.getElementById('Save').addEventListener('click',
    save_options);

Finally, the content script retrieves the data using chrome.storage.sync.get, and then sets the site's bar color.

and the content script would look like this;

chrome.storage.sync.get({
    Color: 'red'
    }, function(items) {
    document.getElementsByClassName("header-2014 clearfix")[0].style.backgroundColor = items.Color;
    }
);

The manifest file should have the permission "storage" and needs to include the "options_page"

If you're not sure what the manifest.json would look like, it'd be like this:

{
  "manifest_version": 2,

  "name": "ROBLOX Color Scheme",
  "description": "Edit the color scheme of the roblox bar! Note: Not created by roblox.",
  "version": "1.0",
   "options_page": "Options.html",

  "permissions": [
    "<all_urls>",
    "storage"
  ],
  "content_scripts": [
    {
        "matches": ["http://www.roblox.com/*"],
        "js": ["ContentScript.js"]
    }
  ]
}
warspyking
  • 3,045
  • 4
  • 20
  • 37
0

Not tested, but I think you should do this:

Script.js:

chrome.tabs.query({currentWindow: true, active: true}, function(tabArray) {
    var TabId = tabArray[0];
    var port = chrome.tabs.connect(TabId, {name: "ColorShare"});

    function ChangeColor() {
        port.postMessage({Color: document.getElementById("Color").value});
    });
    document.getElementById('Color').addEventListener("click", ChangeColor);
}

ContentScript.js:

chrome.runtime.onConnect.addListener(function(port) {
    if (port.name == "ColorShare") {
        port.onMessage.addListener(function(msg) {
            document.querySelector("header-2014 clearfix").style.backgroundColor = msg.Color;
        });
    }
});

And you should use chrome.tabs.sendMessage instead of chrome.tabs.connect

redphx
  • 46
  • 1
  • 3
  • `chrome.tabs.query` invocation is missing `);` – Xan Sep 01 '14 at 12:07
  • Okay that was my fault. I thought I had found an error and removed it (and edited his post) so now I'm stuck with another error to check out :P – warspyking Sep 01 '14 at 23:48
  • Okay I just found out that there WAS indeed an error here, ); is supposed to go on the last } not the one ending ChangeColor. After running that, I got this error; Error in response to tabs.query: Error: Invocation of form tabs.connect(object, object) doesn't match definition tabs.connect(integer tabId, optional object connectInfo) Popup.html:1 – warspyking Sep 02 '14 at 02:50
  • I think it means that tabArray[0] returned an object, not an Id? If so how do we obtain the Id? – warspyking Sep 02 '14 at 02:52
  • Okay, I tried saying var TabId = tabArray[0].id and it stopped producing an error, only problem, it still didn't work, I set the color and refreshed, nothing changed, it was still boring old blue. What could be the problem? – warspyking Sep 02 '14 at 03:49
0

Script.js

function ChangeColor() {
  var tabId;
  chrome.tabs.query({currentWindow: true, active: true}, function(tabArray) {
    tabId = tabArray[0].id;
    chrome.tabs.sendMessage(tabId,{color: document.getElementById("Color").value});
  });
}

document.getElementById('Color').addEventListener("click", ChangeColor);

ContentScript.js

chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
  document.getElementsByClassName("header-2014 clearfix")[0].style.backgroundColor = msg.color;
});

This takes care of your async issues as well as switching to single messages rather than a long-lived connection. Unless you are sending a lot of info back and forth, single messages are probably better than opening a port. You can add some sort of description to the message too, in the form of additional fields, for example:

chrome.tabs.sendMessage(tabId,{type: 'setColor', color: stuff});

You can then check the type in the listener and separate it like that.

BeardFist
  • 8,031
  • 3
  • 35
  • 40
  • I'm using a long-lived connection because whenever I visit that site, it has to "get" the color, and the button has to "set" the color. Is there a better where to do this? – warspyking Aug 31 '14 at 11:16
0

I think for your purpose long lived connection are not needed, just use simple one time request. By the way since you are using long lived connection, I'm going to answer stick to that.

First you have a button with id of Button but you are attaching click event handler for color input document.getElementById('Color').addEventListener("click", ChangeColor); you have to change that here is my code for popup.html.

popup.html

<!DOCTYPE html>
<html>
    <head>
        <p>Choose a color:</p>
        <input type="color" id="ColorVal">
        <button type="button" id="color">Change Color!</button>
    </head>
    <body>
        <script src="script.js"></script>
    </body>

</html>

Change id accordingly in Script.js. Other than that your edited Script.js and ContentScript.js are fine. Your first code fails because of querying tabs take much time. Since tabid is not there port declaration fails. Hope this will help you.

Ruwanka De Silva
  • 3,555
  • 6
  • 35
  • 51