I am trying to create an extension that will have a side panel. This side panel will have buttons that will perform actions based on the host page state.
I followed this example to inject the side panel and I am able to wire up a button onClick listener. However, I am unable to access the global js variable. In developer console, in the scope of the host page I am able to see the variable (name of variable - config) that I am after. but when I which to the context of the sidepanel (popup.html) I get the following error -
VM523:1 Uncaught ReferenceError: config is not defined. It seems like popup.html also runs in a separate thread.
How can I access the global js variable for the onClick handler of my button?
My code:
manifest.json
{
"manifest_version": 2,
"name": "Hello World",
"description": "This extension to test html injection",
"version": "1.0",
"content_scripts": [{
"run_at": "document_end",
"matches": [
"https://*/*",
"http://*/*"
],
"js": ["content-script.js"]
}],
"browser_action": {
"default_icon": "icon.png"
},
"background": {
"scripts":["background.js"]
},
"permissions": [
"activeTab"
],
"web_accessible_resources": [
"popup.html",
"popup.js"
]
}
background.js
chrome.browserAction.onClicked.addListener(function(){
chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
chrome.tabs.sendMessage(tabs[0].id,"toggle");
})
});
content-script.js
chrome.runtime.onMessage.addListener(function(msg, sender){
if(msg == "toggle"){
toggle();
}
})
var iframe = document.createElement('iframe');
iframe.style.background = "green";
iframe.style.height = "100%";
iframe.style.width = "0px";
iframe.style.position = "fixed";
iframe.style.top = "0px";
iframe.style.right = "0px";
iframe.style.zIndex = "9000000000000000000";
iframe.frameBorder = "none";
iframe.src = chrome.extension.getURL("popup.html")
document.body.appendChild(iframe);
function toggle(){
if(iframe.style.width == "0px"){
iframe.style.width="400px";
}
else{
iframe.style.width="0px";
}
}
popup.html
<head>
<script src="popup.js"> </script>
</head>
<body>
<h1>Hello World</h1>
<button name="toggle" id="toggle" >on</button>
</body>
popup.js
document.addEventListener('DOMContentLoaded', function() {
document.getElementById("toggle").addEventListener("click", handler);
});
function handler() {
console.log("Hello");
console.log(config);
}
In JavaScript, variables can be accessed from another file using the <script> tags or the import or export statement. The script tag is mainly used when we want to access variable of a JavaScript file in an HTML file. This works well for client-side scripting as well as for server-side scripting.
A JavaScript global variable is declared outside the function or declared with window object. It can be accessed from any function.
Since content scripts run in an "isolated world" the JS variables of the page cannot be directly accessed from an extension, you need to run code in page's main world.
WARNING! DOM element cannot be extracted as an element so just send its innerHTML
or another attribute. Only JSON-compatible data types can be extracted (string, number, boolean, null, and arrays/objects of these types), no circular references.
This is the entire code in your extension popup/background script:
async function getPageVar(name, tabId) {
const [{result}] = await chrome.scripting.executeScript({
func: name => window[name],
args: [name],
target: {
tabId: tabId ??
(await chrome.tabs.query({active: true, currentWindow: true}))[0].id
},
world: 'MAIN',
});
return result;
}
Usage:
(async () => {
const v = await getPageVar('foo');
console.log(v);
})();
See also how to open correct devtools console.
We'll extract the variable and send it into the content script via DOM messaging. Then the content script can relay the message to the extension script in iframe or popup/background pages.
ManifestV3 for Chrome 94 or older needs two separate files
content script:
const evtToPage = chrome.runtime.id;
const evtFromPage = chrome.runtime.id + '-response';
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg === 'getConfig') {
// DOM messaging is synchronous so we don't need `return true` in onMessage
addEventListener(evtFromPage, e => {
sendResponse(JSON.parse(e.detail));
}, {once: true});
dispatchEvent(new Event(evtToPage));
}
});
// Run the script in page context and pass event names
const script = document.createElement('script');
script.src = chrome.runtime.getURL('page-context.js');
script.dataset.args = JSON.stringify({evtToPage, evtFromPage});
document.documentElement.appendChild(script);
page-context.js should be exposed in manifest.json's web_accessible_resources
, example.
// This script runs in page context and registers a listener.
// Note that the page may override/hook things like addEventListener...
(() => {
const el = document.currentScript;
const {evtToPage, evtFromPage} = JSON.parse(el.dataset.args);
el.remove();
addEventListener(evtToPage, () => {
dispatchEvent(new CustomEvent(evtFromPage, {
// stringifying strips nontranferable things like functions or DOM elements
detail: JSON.stringify(window.config),
}));
});
})();
ManifestV2 content script:
const evtToPage = chrome.runtime.id;
const evtFromPage = chrome.runtime.id + '-response';
// this creates a script element with the function's code and passes event names
const script = document.createElement('script');
script.textContent = `(${inPageContext})("${evtToPage}", "${evtFromPage}")`;
document.documentElement.appendChild(script);
script.remove();
// this function runs in page context and registers a listener
function inPageContext(listenTo, respondWith) {
addEventListener(listenTo, () => {
dispatchEvent(new CustomEvent(respondWith, {
detail: window.config,
}));
});
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg === 'getConfig') {
// DOM messaging is synchronous so we don't need `return true` in onMessage
addEventListener(evtFromPage, e => sendResponse(e.detail), {once: true});
dispatchEvent(new Event(evtToPage));
}
});
usage example for extension iframe script in the same tab:
function handler() {
chrome.tabs.getCurrent(tab => {
chrome.tabs.sendMessage(tab.id, 'getConfig', config => {
console.log(config);
// do something with config
});
});
}
usage example for popup script or background script:
function handler() {
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
chrome.tabs.sendMessage(tabs[0].id, 'getConfig', config => {
console.log(config);
// do something with config
});
});
}
So, basically:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With