Preserve toggle between different tabs - javascript

I'm working on a Google Chrome Extension that you're supposed to be able to turn on and off using the symbol in the extension toolbar. This toggle should be universal, meaning that no matter where you turn it on or off, the current state is preserved everywhere. Doesn't matter in what tab or window you were, the status is shared everywhere.
Let's just say for our example, it's supposed to write "I'm on!" in the console when it's turned on and you press the A key. If it's turned off and you press A, it will say "I'm off!".
manifest.json:
{
"name": "Test Extension",
"version": "1.0",
"manifest_version": 2,
"description": "Just a test for Stack Overflow.",
"browser_action": {
"default_icon": "images/icon.png"
},
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
background.js:
var extensionMode = true;
chrome.browserAction.onClicked.addListener(function(tab) {
extensionMode = !extensionMode;
if(extensionMode) {
chrome.browserAction.setIcon({
path : "images/icon.png"
});
} else {
chrome.browserAction.setIcon({
path : "images/icon_disabled.png"
});
}
let msg = {
extensionMode: extensionMode
}
chrome.tabs.sendMessage(tab.id, msg);
});
content.js:
var extensionMode = true;
chrome.runtime.onMessage.addListener(gotMessage);
function gotMessage(message, sender, sendResponse) {
extensionMode = message.extensionMode;
}
onkeydown = onkeyup = function(e){
if(extensionMode) {
if(event.keyCode == 65) { // A
console.log("I'm on!");
}
} else {
if(event.keyCode == 65) { // A
console.log("I'm off!");
}
}
}
The above code works when you stay in the tab you are, but not when you switch... the icon will stay disabled, but extensionMode actually reverses back to true.
What am I doing wrong here? Is this the wrong approach for what I'm trying to do?

chrome.tabs.sendMessage targets just one tab so you would need to get a list of all tabs using chrome.tabs.query and send the message to each one:
const ignoreRuntimeError = () => chrome.runtime.lastError;
chrome.tabs.query({}, tabs => {
tabs.forEach(tab => chrome.tabs.sendMessage(tab.id, msg, ignoreRuntimeError));
});
You would also need to query the state in content scripts on tabs navigated/opened later:
chrome.runtime.sendMessage('getState', state => extensionMode = state);
For which the background script should have a listener:
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg === 'getState') {
sendResponse(extensionMode);
}
});
A more efficient approach for you to consider:
use a nonpersistent event page and store the state in chrome.storage.local
run the content scripts only when enabled using chrome.tabs.executeScript and register/unregister the DOM listeners when toggled

Related

Why does my Chrome extension only load on refresh? But attempts to run in background

My Chrome Extension is only working when I refresh the website I have it matched to.
If I navigate the website, it will not successfully load, however, I do see the content-script being re-run in the console. It seems to fail because it's not finding the HTML elements I'm looking for. On refresh, it can find those HTML elements, and works fine.
I've been trying a few things like chrome.tabs.onUpdated.addListener and MutationObserver but couldn't figure it out. Most likely because my JS skills are fairly limited.
Link to the extension documents:
https://www.dropbox.com/s/x31uvkdpdcnhchz/chrome-ext-stack-example.zip?dl=0
How can I get the content-script to find the HTML elements as I navigate without always having to refresh?
Any thoughts on what I'm screwing up?
Thank you!
manifest.json
{
"manifest_version": 3,
"name": "Test",
"description": "Example for StackOverflow",
"version": "0.0.1",
"host_permissions": ["<all_urls>"],
"permissions": ["storage", "activeTab", "scripting", "tabs", "webNavigation"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://www.zillow.com/homedetails/*"],
"js": ["ballpark.js"],
"css": ["main.css"]
}
],
"web_accessible_resources": [
{
"resources": ["/images/*"],
"matches": ["<all_urls>"]
}
]
}
background.js
function injectScript(tabId) {
chrome.scripting.executeScript(
{
target: {tabId: tabId},
files: ['ballpark.js'],
}
);
}
// adds a listener to tab change
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// check for a URL in the changeInfo parameter (url is only added when it is changed)
if (changeInfo.url) {
// calls the inject function
injectScript(tabId);
}
});
ballpark.js
var offerPrice;
// setTimeout(startWidget, 2000);
startWidget()
function startWidget() {
if(location.href.match(/homedetails/g)) {
console.log("YES Zillow Home Details URL");
getAskingPrice();
insertWidget();
} else {
console.log("NO Zillow Home Details URL");
}
}
// Get Price from Zillow
function getAskingPrice() {
var askingPrice = document.querySelector('[data-testid="price"] span');
if(askingPrice !== null) {
offerPrice = parseFloat(askingPrice.innerText.replace(/\$|,/g, ''));
console.log(offerPrice + " Offer Price");
} else {
console.log("Null Asking Price");
}
}
// Find Zillow widget to insert the extension widget
function insertWidget() {
const select_div_for_bpd = document.querySelector('div.Spacer-c11n-8-65-2__sc-17suqs2-0');
if(select_div_for_bpd !== null) {
const ballpark_container = document.createElement("div");
ballpark_container.setAttribute("id", "ballpark-container");
select_div_for_bpd.appendChild(ballpark_container);
ballpark_container.innerHTML = `
<div class="ballpark-roi-container">
<div><h1>${offerPrice}</h1> Offer Price</div>
</div>
`;
} else {
console.log("Cannot insert ballpark widget");
}
}
for getAskingPrice and insertWidget you need to wait from the DOM to be ready first.
try this
function startWidget() {
if(location.href.match(/homedetails/g)) {
console.log("YES Zillow Home Details URL");
const fn = () => {
getAskingPrice();
insertWidget();
}
document.addEventListener('DOMContentLoaded', fn, false);
} else {
console.log("NO Zillow Home Details URL");
}
}
You should wait until the document completely loaded before you interact with the page.
You could implement a function which will return a promise if the document has loaded. Your function could look something like this.
function getCompletedReadyState()
{
return new Promise((resolve) =>
{
if (document.readyState === 'complete')
{
resolve('completed');
}
document.onreadystatechange = () =>
{
if (document.readyState === 'complete')
{
resolve('completed');
}
};
});
}
Then you could call the method at the end of your content script like so
...
(async () => {
await getCompletedReadyState();
startWidget()
})();
You can read more about document.readyState here
I added the following to the top of my content-script file and now my extension will keep looking for a specific class as I navigate the SPA website.
const observer = new MutationObserver(function() {
if (document.getElementsByClassName('name-of-class-you-want')[0]) {
// add what you want to do here...
}
})
const target = document.querySelector('body');
const config = { childList: true };
observer.observe(target, config);

How to share a unique tab ID between content and background scripts without an asynchronous delay?

I have built a chrome extension and I'm getting hit by a race condition that I need help with.
If you see the answer chrome extension: sharing an object between content scripts and background script it tells us that you cannot share a variable between content and background scripts.
My goal is to generate a unique ID per-browser tab and then share it in between the content.js and the background.js. Then I need to use this value in a content injected javascript as explained in this answer: In Chrome extensions, can you force some javascript to be injected before everything?
The only way I have been able to figure out how to do this is by doing the following async code then I just use the tab ID as the unique ID:
content.js
await pingBackground();
async function pingBackground() {
var info;
await new Promise(function (resolve, reject) {
chrome.runtime.sendMessage({ type: 1 }, function (response) {
if (response !== undefined) {
info = response;
resolve();
}
else {
reject();
}
});
});
console.log("Id is " + info.id);
}
background.js
chrome.runtime.onMessage.addListener(messageHandler);
function messageHandler(message, sender, reply) {
switch (message.type) {
case 1:
reply({ 'id': sender['tab'].id, 'active': true });
break;
}
}
manifest.json
{
"name": "oogi",
"version": "0.1",
"manifest_version": 2,
"background": {
"scripts": [
"common.js",
"background.js"
],
"persistent": true
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content.js"],
"run_at": "document_start"
}
],
"permissions": [
"contentSettings",
"webRequest",
"webRequestBlocking",
"*://*/*"
]
}
But the problem with this is by the time I get the tab ID from background js, the script's content has already been loaded.
Is there some way to make it so this variable can be asynchronously shared between background.js and content.js? Or is this simply impossible?
Can I switch it around and have background.js load a variable from content.js asynchronously?
UPDATE:
A terrible hack which works is to do this in the foreground of the content.js:
var sleepScript = document.createElement('script');
var sleepCode = `function sleep (ms) {
var start = new Date().getTime();
while (new Date() < start + ms) {}
return 0;
}
sleep(500);`;
sleepScript.textContent = sleepCode;
(document.head || document.documentElement).appendChild(sleepScript);
This will force the page to wait for a bit giving the time to query the background before running the inline dom.
It works but that's awful.
This question was already answered previously, although it is hard to tell that this is the same issue at first glance.
https://stackoverflow.com/a/45105934
The answer is pretty descriptive so give it a read.
Here is the script changes that make it work:
// background.js
function addSeedCookie(details) {
details.responseHeaders.push({
name: "Set-Cookie",
value: `tab_id=${details.tabId}; Max-Age=2`
});
return {
responseHeaders: details.responseHeaders
};
}
chrome.webRequest.onHeadersReceived.addListener(
addSeedCookie, {urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]
);
// inject.js
function getCookie(cookie) { // https://stackoverflow.com/a/19971550/934239
return document.cookie.split(';').reduce(function(prev, c) {
var arr = c.split('=');
return (arr[0].trim() === cookie) ? arr[1] : prev;
}, undefined);
}
var tabId = getCookie("tab_id");

How to update tab with Background.js and THEN execute Content Script?

I would like for my Chrome extension to run like this:
User clicks on the icon and that brings up the hard-coded URL, "www.example.com". It does not open a new tab but instead updates the window. I would then like to execute the content script and be able to alert "working" on the updated page.
Here is what i've got so far:
Background.js
chrome.browserAction.onClicked.addListener(function(activeTab)
{
chrome.tabs.query({'active': true, 'lastFocusedWindow': true}, function
(tabs)
{
chrome.tabs.update({
url: "http://www.example.com/"
});
});
function test()
{
chrome.tabs.executeScript(null, {file: "myscript.js"});
}
chrome.tabs.onUpdated.addListener(function(tabid, changeinfo, tab) {
var url = tab.url;
if (url !== undefined && changeinfo.status == "complete") {
test();
}
});
});
Content script
alert('working');
The result is odd. When I click on the icon, it brings up example.com fine, however sometimes the alert works and sometimes it doesn't. Even weirder, it works more often if I double click but if I click it a bunch of times, the alerts add up and i then get many alerts all at once (I just want one).
content.js can set a global variable so you can check it to skip reinjection.
if (window[chrome.runtime.id]) {
alert('Oi! Reinjected.');
} else {
window[chrome.runtime.id] = true;
alert('Oi!');
}
// do something
Depending on what you do in the content script, you can add a message listener which will process requests from your background page instead of re-running the entire code.
background.js checks if the active tab is already navigated to the site of interest (or navigates to it) and injects the content script (comment out inject(); to skip reinjection).
The check itself is simple: inject a content script code that checks that global variable. This code runs in the same context as other content scripts for a given page (the isolated world).
const SITE_URL = 'http://www.example.com';
chrome.browserAction.onClicked.addListener(tab => {
if (tab.url === new URL(SITE_URL).href) {
checkIfInjected(tab).then(tab => {
console.log('already injected in %d, reinjecting anyway', tab.id);
inject(tab);
}).catch(inject);
} else {
updateTabAndWaitForStart(tab.id, {url: SITE_URL})
.then(inject);
}
});
function checkIfInjected(tab) {
return runContentScript(tab.id, {
code: 'window[chrome.runtime.id]',
}).then(results => {
resolve(results[0] ? tab : Promise.reject(tab));
});
}
function inject(tab) {
return runContentScript(tab.id, {
file: 'content.js',
runAt: 'document_end',
allFrames: false,
});
}
function runContentScript(tabId, options) {
return new Promise(resolve => {
chrome.tabs.executeScript(tabId, options, resolve);
});
}
// onUpdated listener waits for our tab to get an URL, detaches on success
function updateTabAndWaitForStart(tabId, options) {
return new Promise(resolve => {
chrome.tabs.update(tabId, options, newTab => {
chrome.tabs.onUpdated.addListener(
function onUpdated(updatedId, info, updatedTab) {
if (updatedId === newTab.id && info.url) {
chrome.tabs.onUpdated.removeListener(onUpdated);
resolve(updatedTab);
}
}
);
});
});
}
manifest.json should contain permissions for the site you navigate to and for the active tab.
Of course, a larger permission like <all_urls> would include these, but the advantage of an exact list is that the installation warning in the extensions web store will show only the site.
* in the site URL permission is optional (works the same as per API design).
The final / is mandatory (it's a path).
{
"name": "test",
"version": "0.0.1",
"manifest_version": 2,
"description": ".............",
"background": {
"scripts": ["background.js"],
"persistent": false
},
"browser_action": {
"default_title": "click me"
},
"permissions": ["activeTab", "http://www.example.com/*"]
}

Developing debugger in chrome

I am working on developing an extension (debugger) for a Javascript library. Initial results are fine and I have come across one bug (not sure if its a bug). Problem is "When a code is running multiple tabs open (for each, dev-tools is also open) and The dev-tools receive messages from all the open tabs, which should not be the case. How to prevent it from happening? any suggestions would be great help. Let me know if I need to put code related to any other files.
//Part of Manifest.json
{
"manifest_version": 2,
"name": "debugger",
"version": "1.0",
"description": "some",
"author": "some",
"devtools_page": "devtools.html",
"background": {
"scripts": [
"background.js"
]
},
"permissions": [
"tabs",
"storage",
"<all_urls>"
]
}
// content-script.js
chrome.extension.sendMessage(message, function (message) {
console.log("message sent");
});
chrome.extension.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.action == 'debug') {
console.log("Message: " + msg.content);
}
});
// Background.js
chrome.extension.onConnect.addListener(function (port) {
var extensionListener = function (message, sender, sendResponse) {
if (message.destination == "panel") {
port.postMessage(message);
// chrome.tabs.sendMessage(sender.tab.id, message, sendResponse);
}
});
// panel.js
var port = chrome.extension.connect({
name: "my-extension"
});
port.onMessage.addListener(function (message) {
// do something with message
});
In your case object sender from the background message listener contains property tab (with tab.id), so you can recognise which tab sent this message.
Then you can get current tab using:
chrome.tabs.query({active: true, currentWindow: true}, function(foundTabs){
//foundTabs[0].id //current tab id
});
And that means you can filter messages from the current tab only.

Chrome extension background message listener fires up twice

I got an chrome extension that works like this:
Background every few seconds checks site x for changes, if they occur background opens new tab with page Y and does some page automation (fills survey in ajax - based page). Page automation is done by content script. Content script needs to ask Background for permission to start working. It does that by sending message and waiting for reply. When content script finishes it's job it sends message to Background with information about success or not. When failure tab should be updated with new link, when succes - tab should be closed. The problem is, sometimes - not always, bacground get's messages twice. Once - when tab fails it's job, and second time just after tab is updated with new link. I wish i could know why...
Here is script:
Background.js listener creation
function create_listerner(){
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
switch(request.greeting){
case "site_opened":
if (sender.tab.id == tabId) {
sendResponse({
farewell : "go"
});
} else {
sendResponse({
farewell : "stop"
});
}
break;
case "imDoneWithSuccess":
//update tab logic
break;
case "imDoneWithFail":
//close tab logic
actNgo(url,arr);
break;
}
});
}
Listener call is made once, when user starts script from context menu: background.js
chrome.contextMenus.create({
title : "Start",
contexts : ["browser_action"],
onclick : function () {
create_listerner();
}
});
tab update looks simple: background.js
function actNgo(url,arr){
chrome.storage.local.set({
array : arr,
ndz : 1
}, function () {
chrome.tabs.update(
tabId,
{
"url" : url,
"active" : false
}, function (tab) {
console.log("tabId "+tabId);
});
});
}
content script is injected into website, site doesn't reload as is ajax based, and even if - messages cannot be send without proper conditions met
content.js
function SR(st) {
var status ;
if(st){
status = "imDoneWithSuccess";
} else {
status = "imDoneWithFail";
}
chrome.runtime.sendMessage({
greeting : status
}, function (response) {
});
}
content script should work only on tabs that bacground.js opens
function askForPermission() {
chrome.runtime.sendMessage({
greeting : "site_opened"
}, function (response) {
if (response.farewell == 'go') {
start();
} else {
console.log("bad tab");
}
});
}
again - there is no possibility for them to fire up by themselves.
Log file looks like this:
Background: tab created;
content script: askForPermission(); <-sends message asking for permission to start
Bacground: go <- allows.
content script: (content script logs, working as intended)
Background listener: imDoneWithFail;
Background update tab;
background listener: imDoneWithFail; <- shouldn't happen, doesn't look like it conten't script even started to work as it didn't ask for permission. Looks like listener is fired up twice...
Background update tab;
content script: askForPermission(); <-sends message asking for permission to start
Bacground: go <- allows.
edit: manifest
{
"name": "Test script",
"version": "0.0.78",
"manifest_version": 2,
"background": {
"scripts": ["src/background/background.js"]
},
"icons": {
"19": "icons/icon19.png"
},
"default_locale": "en",
"options_ui": {
"page": "src/options/index.html",
"chrome_style": true
},
"browser_action": {
"default_icon": "icons/icon19.png",
"default_title": "Test",
"default_popup": "src/browser_action/browser_action.html"
},
"permissions": [
"http://localhost/survey/*",
"storage",
"contextMenus",
"tabs",
"webRequest"
],
"content_scripts": [
{
"matches": [
"http://localhost/survey/*",
"https://localhost/survey/*"
],
"js": [
"js/jquery/jquery.min.js",
"src/inject/content.js"
]
}
]
}
Before someone ask - I tried to use long-live connections, but i had same issue. Why does it happen?
Ok i found a answer, the problem is with function changing url of tab.
function actNgo(url,arr){
chrome.storage.local.set({
array : arr,
ndz : 1
}, function () {
chrome.tabs.update(
tabId,
{
"url" : url,
"active" : false
}, function (tab) {
console.log("tabId "+tabId);
});
});
}
Function, when given same url as current didn't refreshed tab, and content script couldn't fire up as new... Still don't know why listener fired up messages twice, but since my change it has stopped.
I don't know if this fix will help anyone, but i fixed this by first changing tab url to blank, and then to new one.
chrome.tabs.update(
tabId,
{
"url" : "about:blank",
"active" : false
}, function (tab) {
console.log("bcgr: aktualizujStroneIObstaw: pusta: tabId "+tabId);
chrome.tabs.update(
tabId,
{
"url" : url_beta,
"active" : false
}, function (tab) {
console.log("bcgr: aktualizujStroneIObstaw: wlasciwa: tabId "+tabId);
}
);

Categories