I've built an AddIn, a chess game visualiser, which worked many years on desktop browsers Firefox, Chrome, Opera, Safari and IE. The AddIn runs when invoked from context menu and parses the selected text from the current page. The selected text is expected to be Chess Notation Format, which can be FEN or PGN.
It is the second time when the addin becomes incompatible with Firefox when FF version changes, and I have to do architectural changes. The new firefox architecture is meant to be compatible with Chrome, but it is only partially compatible, so I can't fully reuse the code from chrome. I also used info from
Communicate data from popup to content script injected by popup with executeScript()
and Communicate data from popup to content script injected by popup with executeScript()
Here are my questions:
See the code of background.js, browser.runtime.sendMessage is invoked before the invocation browser.runtime.onMessage.addListener from pop.js. I don't understand how to send the message from background.js after the window opened by browser.windows.create is fully created, all scripts ended execution and document fully loaded. Also I can't do it on invocation of document.addEventListener('DOMContentLoaded', when I call browser.runtime.onMessage.addListener it generates the error Error: sendRemoveListener on closed conduit b212f9bfb4f0394efe56168a583c91346caf2d00#temporary-addon.412316861125 and probably is not executed at all. Maybe there are special events I can handle, I don't see much documentation on that. In Chrome the approach mostly similar, but it works pretty well.
I still can catch the message sent from background.js in some unusable scenario: if I opened two windows, yet good for debugging purposes. The problem is that inside the handler from pop.js the document.innerHTML is undefined no matter that I am trying to do. I can't handle the content of the window from extension api handlers. I think the document is fully loaded because it is handled by the first of the two opened windows.
Note, I can send the selection text to newly opened popup via invocation URL parameter. It works, but I think it is a workaround. I use it on Opera and Safari, it also works on Firefox, but I am thinking getting rid of this where it is possible.
There is the scenario, simplified:
background.js:
{//Firefox create context menus
var id = chrome.contextMenus.create({"title": "Chess game", "contexts":["selection", "page"]});
var childId = chrome.contextMenus.create
({
"title": "Play Small","contexts":["selection", "page"],
"parentId": id,
"onclick": (info, tab) => { playBoard (info, tab, "mini18"); }
});
}
function onCreated(windowInfo, request)
{
browser.windows.get(windowInfo.id).then
(
(wnd) => { browser.runtime.sendMessage(request); }
);
}
function playBoard (info, tab, imagePath)
{
let creating = browser.windows.create({ type:"detached_panel", url:"pop.html", width:250, height:100 });
creating.then
(
(inf) =>
{onCreated
(
inf,
{
chessObject:
{
gametype : "PGN_OR_FEN_board",
content : selection,
imgPath : info.selectionText
}
}
)
}
);
}
pop.js:
browser.runtime.onMessage.addListener
(
(request) =>
{
console.log("popup chrome listen to game: " + request.chessObject.content);
console.log("popup chrome listen to game: document inner html: " + document.innerHTML);
}
);
pop.html:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>hello world</title>
<script language="javascript" src="pop.js"></script>
</head>
<body>
hello popup
</body>
</html>
manifest.json:
{
"manifest_version": 2,
"name": "Roatta Waaayyyy!!!",
"version": "1.0.0.4",
"description": "Chess AddIn for Firefox",
"icons": { "32": "icons/roatta_32_32.jpg" },
"permissions": [ "contextMenus" ],
"background": { "scripts": ["background.js"] }
}
For reference here is the full source code on GIT, the one I try to implement now is Firefox65 which is meant to replace the previous Firefox. The basic version is the one for Chrome, initially it was the one for Firefox.
As suggested by #wOxxOm I applied the inverse message pattern, and it works.
Now the background.js register a listener and waits to be called, then it removes the listener:
{//Firefox
console.log("Creating context menus");
var id = chrome.contextMenus.create({"title": "Chess game", "contexts":["selection", "page"]});
var childId = chrome.contextMenus.create
({
"title": "Play Small","contexts":["selection", "page"],
"parentId": id,
"onclick": (info, tab) => { playBoard (info, tab, "mini18"); }
});
}
function playBoard (info, tab, imagePath)
{
var createData =
{
type : "detached_panel",
url : "pop.html",
width : 250,
height: 100
};
let requestData =
{
chessObject:
{
gametype : "PGN_OR_FEN_board",
content : info.selectionText,
imgPath : imagePath
}
};
try
{
console.log ("Play Board: " + imagePath);
console.log ("Selection: " + info.selectionText);
let creating = browser.windows.create(createData);
function onGameDataExchange(request)
{
browser.runtime.onMessage.removeListener(onGameDataExchange);
return Promise.resolve(requestData);
}
browser.runtime.onMessage.addListener (onGameDataExchange);
console.log ("opened pop.html");
}
catch (err)
{
console.log ("Error: main background playBoard() " + err);
}
}
The pop.js is the one requesting the message:
document.addEventListener('DOMContentLoaded',
function (event)
{
browser.runtime.sendMessage ( { chessObject: { gametype : "PGN_OR_FEN_board" } } )
.then
(
(request) =>
{
console.log("on DOMContentLoaded message: " + request.chessObject.gametype);
console.log("on DOMContentLoaded message: " + request.chessObject.content);
console.log("on DOMContentLoaded message: " + request.chessObject.imgPath);
}
);
console.log("Popup DOMContentLoaded send message end");
}
);
Related
I have a function in the context.js which loads a panel and sends a message to panel.js at the last. The panel.js function updates the ui on receiving that msg. But it is not working for the first click i.e. it just loads normal ui, not the one that is expected that is updated one after the msg is received. while debugging it works fine.
manifest.json
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_scripts": [{
"all_frames": false,
"matches": ["<all_urls>"],
"js":["context.js"]
}],
"permissions": ["activeTab","<all_urls>", "storage","tabs"],
"web_accessible_resources":
"panel.html",
"panel.js"
]
context.js - code
fillUI (){
var iframeNode = document.createElement('iframe');
iframeNode.id = "panel"
iframeNode.style.height = "100%";
iframeNode.style.width = "400px";
iframeNode.style.position = "fixed";
iframeNode.style.top = "0px";
iframeNode.style.left = "0px";
iframeNode.style.zIndex = "9000000000000000000";
iframeNode.frameBorder = "none";
iframeNode.src = chrome.extension.getURL("panel.html")
document.body.appendChild(iframeNode);
var dataForUI = "some string data"
chrome.runtime.sendMessage({action: "update UI", results: dataForUI},
(response)=> {
console.log(response.message)
})
}
}
panel.js - code
var handleRequest = function(request, sender, cb) {
console.log(request.results)
if (request.action === 'update Not UI') {
//do something
} else if (request.action === 'update UI') {
document.getElementById("displayContent").value = request.results
}
};
chrome.runtime.onMessage.addListener(handleRequest);
background.js
chrome.runtime.onMessage.addListener((request,sender,sendResponse) => {
chrome.tabs.sendMessage(sender.tab.id,request,function(response){
console.log(response)`
});
});
panel.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="panel.css" />
</head>
<body>
<textarea id="displayContent" rows="10" cols="40"></textarea>
</body>
</html>
Any suggestions on what I am doing wrong or what can I do instead?
An iframe with a real URL loads asynchronously so its code runs after the embedding code finishes - hence, your message is sent too early and is lost. The URL in your case points to an extension resource so it's a real URL. For reference, a synchronously loading iframe would have a dummy URL e.g. no src at all (or an empty string) or it would be something like about:blank or javascript:/*some code here*/, possibly srcdoc as well.
Solution 1: send a message in iframe's onload event
Possible disadvantage: all extension frames in all tabs will receive it, including the background script and any other open extension pages such the popup, options, if they also have an onMessage listener.
iframeNode.onload = () => {
chrome.runtime.sendMessage('foo', res => { console.log(res); });
};
document.body.appendChild(iframeNode);
Solution 2: let iframe send a message to its embedder
Possible disadvantage: wrong data may be sent in case you add several such extension frames in one tab and for example the 2nd one loads earlier than the 1st one due to a bug or an optimization in the browser - in this case you may have to use direct DOM messaging (solution 3).
iframe script (panel.js):
chrome.tabs.getCurrent(ownTab => {
chrome.tabs.sendMessage(ownTab.id, 'getData', data => {
console.log('frame got data');
// process data here
});
});
content script (context.js):
document.body.appendChild(iframeNode);
chrome.runtime.onMessage.addListener(
function onMessage(msg, sender, sendResponse) {
if (msg === 'getData') {
chrome.runtime.onMessage.removeListener(onMessage)
sendResponse({ action: 'update UI', results: 'foo' });
}
});
Solution 3: direct messaging via postMessage
Use in case of multiple extension frames in one tab.
Disadvantage: no way to tell if the message was forged by the page or by another extension's content script.
The iframe script declares a one-time listener for message event:
window.addEventListener('message', function onMessage(e) {
if (typeof e.data === 'string' && e.data.startsWith(chrome.runtime.id)) {
window.removeEventListener('message', onMessage);
const data = JSON.parse(e.data.slice(chrome.runtime.id.length));
// process data here
}
});
Then, additionally, use one of the following:
if content script is the initiator
iframeNode.onload = () => {
iframeNode.contentWindow.postMessage(
chrome.runtime.id + JSON.stringify({foo: 'data'}), '*');
};
document.body.appendChild(iframeNode);
if iframe is the initiator
iframe script:
parent.postMessage('getData', '*');
content script:
document.body.appendChild(iframeNode);
window.addEventListener('message', function onMessage(e) {
if (e.source === iframeNode) {
window.removeEventListener('message', onMessage);
e.source.postMessage(chrome.runtime.id + JSON.stringify({foo: 'data'}), '*');
}
});
one possible way that worked for me is by using functionality in setTimeout() method.
in context.js
setTimeout(() => {
chrome.runtime.sendMessage({action: "update UI", results: dataForUI},
(response)=> {
console.log(response.message)
}
)
}, 100);
But I am not sure if this is the best way.
I'm writing Chrome devtools plugin.
devtools.js
chrome.devtools.panels.create("Override Debug",
null,
"panel.html",
(panel) => {
panel.onShown.addListener(addDebugger);
panel.onHidden.addListener(destroyDebugger);
}
);
So when user navigates to Override Debug tab in devtools, I'm initializing debugger.
When user navigating away from my devtool, I'm discarding debugger - Which is working great
Now the issue comes, when user navigates to Override Debug and closed devtools completely, debugger still alive making page un responsive.
I tried to see events in panel object chrome.devtools objects also tried entire github to see if we have such event. No luck.
Is there any event that monitors devtools close event to discard the debugger I attached?
Update 1
Tried to attach Inspector.enable command as follows but no luck. It never got Inspector.detached message
function addDebugger() {
chrome.tabs.getSelected(null, function(target) {
debuggee = { tabId: target.id };
chrome.debugger.attach(debuggee, "1.2", () => {
chrome.debugger.sendCommand(debuggee, "Network.setRequestInterception", { patterns: [{ urlPattern: '*' }] });
chrome.debugger.sendCommand(debuggee, "Inspector.enable");
});
chrome.debugger.onEvent.addListener((source, method, params) => {
if (source.tabId === target.id) {
if (method === "Network.requestIntercepted") {
// Do many things
} else if (method === "Inspector.detached") {
destroyDebugger();
}
}
})
});
}
I'm building a chrome extension , I need to create a function to detect the URL changes for example youtube.com to youtube.com/watch?v={some video id}
I kept looking around for a way to do that on Content.js but apparently it's not possible and i need to use background.js in that particular case
First I've implanted this part
chrome.webNavigation.((EVENT)).addListener(function() {
alert("worked");
});
to test if chrome can detect the URL changes correctly , then i will need to connect that to content.js to Ajax the URL changes and return JSON object from the server
I saw on this page Chrome Extension - webNavigation multiple Events that i can use
Events
onBeforeNavigate
onCommitted
onDOMContentLoaded
onCompleted
onErrorOccurred
onCreatedNavigationTarget
onReferenceFragmentUpdated
onTabReplaced
onHistoryStateUpdated
sadly none of them works properly as they the Alert pops Multiple times ( sometimes 1-4 times ) when i visit ( reload or visit a single page )
I don't know what i should do or if there is any other way to handle a problem like this , I'm fairly new to chrome extension and JS.
background.js
chrome.storage.sync.get( "extensionSwitch", function(data){
if( data[ "extensionSwitch" ] == undefined ){
chrome.storage.sync.set( { "extensionSwitch" : "true" }, function() { } );
}
});
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
fetch(request.input, request.init).then(function(response) {
return response.text().then(function(text) {
sendResponse([{
body: text,
status: response.status,
statusText: response.statusText,
}, null]);
});
}, function(error) {
sendResponse([null, error]);
});
return true;
});
chrome.webNavigation.onHistoryStateUpdated.addListener(function() {
alert("worked");
});
I'm trying to write my first WebExtension for Firefox 60.0.1 Quantum, and ask for your help.
I'm try to briefly describe my Firefox WebExtension.
Step 1. In the browser panel, press the extention button, - opens my
Extension Window.
Step 2. In this Extention Window there is a HTML-Page (file
window.html) with a text field called "Active Tab URL" (id="url").
Step 3. When switching/updating Tabs in main browser window, this
text field should contain URL of the Active/Opened Tab.
My Questions:
How can I pass (from background.js) a any value to the text field (id="url") of my Extension Window?
How can I get the URL of the active Tab, to transfer it to my Extension Window?
I searched the information on the site: https://developer.mozilla.org/en-US/Add-ons/WebExtensions
but unfortunately I did not understand how it can be done.
Thank you for your help and ideas!
manifest.json
{
"description": "Adds browser action icon to toolbar to open packaged web page.",
"manifest_version": 2,
"name": "Extension Window",
"version": "1.0",
"permissions": ["tabs", "contextMenus", "clipboardWrite", "clipboardRead"],
"icons": {"48": "icons/icon-48.png"},
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "icons/icon-32.png"
}
}
background.js
function openMyWindow() {
var createWindow = {
type: "detached_panel",
url: "window.html",
titlePreface: "Extension Window",
width: 350,
height: 950
};
let windowId;
browser.windows.create(createWindow).then((data) => {
windowId = data.id;});
/*
1. How can I pass a any value to the text field (id="url") of my Extension Window?
2. How can I get the URL of the active Tab, to transfer it to my Extension Window?
*/
}
/*
Add openMyWindow() as a listener to clicks on the browser action.
*/
browser.browserAction.onClicked.addListener(openMyWindow);
window.html
<html>
<head>
<title>Extension Window</title>
<meta charset="utf-8">
</head>
<body>
<h1>My Extention Window</h1>
<p>Active Tab URL: <input id="url" type="text"></p>
</body>
</html>
Interesting question, let's get to the solution.
In background.js, you keep the URL of the current (interesting) tab in memory. Since the extension window is also a tab, we filter that out in setUrlIfDesired. In onCreated we filter about:blank and about:newtab, since those two are used by Firefox to indicate that the tab has not finished loading yet.
As soon as the URL changes to something interesting, we use sendMessageToBrowserPopup to notify the extension screen that it needs updating.
// Keep URL changes in a variable
let urlOfCurrentTab = null;
// Keep the id of the created window
let windowId = -1;
browser.tabs.onUpdated.addListener((tabId, changeInfo, tabInfo) => {
if(changeInfo.url) {
// Here anything is allowed, even about:blank and about:newtab
setUrlIfDesired("updated", changeInfo.url, tabInfo.active);
}
});
browser.tabs.onCreated.addListener((tabInfo) => {
if(tabInfo.url && tabInfo.active === true){
if(tabInfo.url !== "about:blank" && tabInfo.url !== "about:newtab"){
setUrlIfDesired("created", tabInfo.url, tabInfo.active);
}
}
});
browser.tabs.onActivated.addListener((activeInfo) => {
if(activeInfo.tabId !== -1 && activeInfo.tabId !== null){
browser.tabs.get(activeInfo.tabId).then((tabInfo) => {
setUrlIfDesired("activated", tabInfo.url, tabInfo.active);
});
}
});
function setUrlIfDesired(type, url, active){
console.log(type + ": " + url + " is " + active);
if(url.indexOf("moz-extension") === -1 && url.indexOf("window.html") === -1){
urlOfCurrentTab = url;
console.log("urlOfCurrentTab is now " + urlOfCurrentTab);
sendMessageToBrowserPopup("getCurrentUrlResult", urlOfCurrentTab);
}
}
function openMyWindow(tab) {
browser.windows.create({
type: "detached_panel",
url: "window.html",
titlePreface: "Extension Window",
width: 350,
height: 950
}).then((data) => {
windowId = data.id;
// The window needs time to create, it gets 1000 milliseconds
setTimeout(function(){
setUrlIfDesired("init", tab.url, true); // initialise the current URL
}, 1000);
});
}
// Track whether we need to clear windowId because the window was closed
browser.windows.onRemoved.addListener((removedWindowId) => {
if(removedWindowId === windowId){
windowId = -1;
}
});
/* Add openMyWindow() as a listener to clicks on the browser action. */
browser.browserAction.onClicked.addListener(openMyWindow);
function getCurrentUrl(){
// You would think this would work, but it doesn't. This is because the extension window is the one that is active!
/*browser.tabs.query({active: true, currentWindow: true}).then((tabs) => {
let tab = tabs[0];
sendMessageToBrowserPopup("getCurrentUrlResult", tab.url);
});*/
// So we use the urlOfCurrentTab variable that we kept in memory instead.
sendMessageToBrowserPopup("getCurrentUrlResult", urlOfCurrentTab);
}
// See also https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/sendMessage
function sendMessageToBrowserPopup(action, data){
// Only send messages to the window if it exists
if(windowId != -1){
console.log("sending " + data);
browser.runtime.sendMessage({"action": action, "data": data});
}
}
In order for this to work, we need to edit window.html:
<html>
<head>
<title>Extension Window</title>
<meta charset="utf-8">
</head>
<body>
<h1>My Extension Window</h1>
<p>Active Tab URL: <input id="url" type="text"></p>
<script src="window.js" type="text/javascript"></script>
</body>
</html>
In window.js (a new file), we subscribe to the event which was emitted by background.js:
browser.runtime.onMessage.addListener(function(message) {
switch (message.action) {
case "getCurrentUrlResult":
updateTextbox(message.data);
break;
default:
break;
}
});
function updateTextbox(urlOfCurrentTab){
// Update the value in the text box to a new value
document.getElementById("url").value = urlOfCurrentTab;
}
Your manifest.json was good. Don't forget to remove the console.log statements when you understand the code / go to production.
If you have further questions, feel free to ask a new question on StackOverflow.
My project is a Chrome extension that will do the following.
Push the extension icon.
Popup will appear (from popup.html)
5 buttons will be in the popup.
When you click one of the four buttons, one javascript code will be executed.
close popup window.
So depending on the answer of this post over here
Detect a button click in the browser_action form of a Google Chrome Extension
(big ups to Michael for his enormous help)
This example is only for one button. Created it with only one of my javascript code and works perfect.
But when it comes to put all of the 5 buttons i 've tried to make this kind of coding but it didnt work at all (im new at javascript code so dont hate)
Here are the codes
MANIFEST.JSON
{
"background": {
"scripts": [ "background.js" ]
},
"browser_action": {
"default_icon": "img/icon.png",
"default_title": "TITLE",
"default_popup": "popup.html"
},
"icons": {
"128": "img/icon_128.png",
"19": "img/icon19.png",
"38": "img/icon38.png",
"48": "img/icon_48_2.png"
},
"manifest_version": 2,
"name": " NAME",
"description": " DESCR ",
"permissions": [ "activeTab" ],
"version": "2.0"
}
POPUP.HTML
<html>
<head>
<script src="popup.js"></script>
<style type="text/css" media="screen">
body { min-width:250px; text-align: center; }
#click-me-l { font-size: 20px; }
#click-me-f { font-size: 20px; }
</style>
</head>
<body>
<button id='click-me-l'>Click1</button>
<button id='click-me-f'>Click2</button>
</body>
</html>
POPUP.JS
function clickHandler(e) {
chrome.extension.sendMessage({directive: "popup-click-l"}, function(response) {
this.close(); // close the popup when the background finishes processing request
});
}
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('click-me-l').addEventListener('click', clickHandler);
})
function clickHandler(e) {
chrome.extension.sendMessage({directive: "popup-click-f"}, function(response) {
this.close(); // close the popup when the background finishes processing request
});
}
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('click-me-f').addEventListener('click', clickHandler);
})
BACKGROUND.JS
chrome.extension.onMessage.addListener(
function(request, sender, sendResponse) {
switch (request.directive) {
case 1 "popup-click-l":
// execute the content script
chrome.tabs.executeScript(null, { // defaults to the current tab
file: "script1.js", // script to inject into page and run in sandbox
allFrames: true // This injects script into iframes in the page and doesn't work before 4.0.266.0.
});
case 2 "popup-click-f":
// execute the content script
chrome.tabs.executeScript(null, { // defaults to the current tab
file: "script2.js", // script to inject into page and run in sandbox
allFrames: true // This injects script into iframes in the page and doesn't work before 4.0.266.0.
});
sendResponse({}); // sending back empty response to sender
break;
default:
// helps debug when request directive doesn't match
alert("Unmatched request of '" + request + "' from script to background.js from " + sender);
}
}
);
So the codes in the link are working PERFECT for only 1 button.
in this example i am trying to make it work for 2 buttons but i cant find what im doing wrong. If anyone has any idea i would appreciate it.
Thanks a lot for your time!!!
(UPDATE 2. Updated codes for 2 buttons but not working.)
You’re defining clickHandler twice, so only the second one counts. One fix would be:
function clickHandler(e) {
chrome.extension.sendMessage({"directive": e.target.id}, function(response) {
this.close(); // close the popup when the background finishes processing request
});
}
In general, you’re repeating yourself too much. You could combine your DOMContentLoaded events into one:
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('click-me-l').addEventListener('click', clickHandler);
document.getElementById('click-me-f').addEventListener('click', clickHandler);
})
but even better would be to put all the buttons into an array, so that popup.js is now:
function clickHandler(e) {
chrome.extension.sendMessage({"directive": e.target.id}, function(response) {
this.close(); // close the popup when the background finishes processing request
});
}
document.addEventListener('DOMContentLoaded', function () {
var buttons = document.getElementsByTagName("button");
for ( var i = 0 ; i < buttons.length ; i++ ) {
buttons[i].addEventListener('click',clickHandler);
}
})
(And I’d recommend button { font-size: 20px; } in your style instead of five separate ids.)
Finally, your switch statement is buggy. Once you start a case, you’ll keep going until you get to a break, so that case "popup-click-l" hits both cases. You could have a separate executeScript for each case, but even better would be to assign to fileName based on the case, and have a single injection at the end. Or best of all would be to have a javascript object define which files go with which ids, so that background.js is now:
chrome.extension.onMessage.addListener(
function(request, sender, sendResponse) {
var injected = {
"click-me-l": "script1.js",
"click-me-f": "script2.js"
};
chrome.tabs.executeScript(null, {
"file": injected[request.directive],
"allFrames": true
});
sendResponse({});
}
);
Fundamentally, this comes back to a point I made in a comment: browser extensions are a bad way to learn javascript, because you’re learning two separate things at the same time. Your difficulties with switch, {}, and generally following the code is a javascript problem. Not seeing when the console tells you about syntax errors is more of a browser extension problem. And your biggest problem is that you’re not seeing which error is which.