For my chrome extension, I want to perform an action when the browserAction popup window closes. I understand that there no built-in events are triggered when this happens. I found this suggestion to open a connection with the background script, and then use the connection's port.onDisconnect event to detect that the popup window is closing.
However, when the popup window closes, I see the following error in the Developer Console for the background script:
(BLESSED_EXTENSION context for glkehflnlfekdijfhacccflbffbjhgbd) extensions::messaging:102: Uncaught TypeError: Cannot read property 'destroy_' of undefined{TypeError: Cannot read property 'destroy_' of undefined
at PortImpl.destroy_ (extensions::messaging:102:37)
at dispatchOnDisconnect (extensions::messaging:322:29)}
The scripts that I use are detailed below.
Can you see where I am going wrong?
manifest.json
{ "manifest_version": 2
, "name": "Detect when popup closes"
, "version": "0.1"
, "browser_action": {
"default_icon": "popup.png"
, "default_popup": "popup.html"
}
, "background": {
"scripts": [
"background.js"
]
}
}
popup.html
<!DOCTYPE html>
<body>
<h1>Test</h1>
<script src="popup.js"></script>
</body>
</html>
popup.js
var port = chrome.runtime.connect()
background.js
chrome.runtime.onConnect.addListener(function (externalPort) {
externalPort.onDisconnect = function () {
try {
var ignoreError = chrome.runtime.lastError
} catch (error) {
console.log("onDisconnect")
}
}
)
For reference, here's the working version of the background.js script:
chrome.runtime.onConnect.addListener(function (externalPort) {
externalPort.onDisconnect.addListener(function () {
console.log("onDisconnect")
// Do stuff that should happen when popup window closes here
})
console.log("onConnect")
})
onDisconnect is not an assignable property.
It's an object that provides addListener method to register a callback:
externalPort.onDisconnect.addListener(function() {
var ignoreError = chrome.runtime.lastError;
console.log("onDisconnect");
});
Related
I am trying to insert youtube videos with the iframe API in to an existing page with the help of a chrome extension content script. But I cannot get the onYouTubeIframeAPIReady to trigger.
manifest.json
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "file://*/*", "*://*/*"],
"js": ["content-script.js"]
}
],
content-script.js
const appEl = document.createElement('div');
appEl.id = 'my-app';
appEl.innerHTML = `<div id="youtube-iframe"></div>`;
const bodyEl = document.querySelector('body');
bodyEl.insertBefore(appEl, bodyEl.firstChild);
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
document.querySelector('body').appendChild(tag);
window.onYouTubeIframeAPIReady = () => {
this.player = new YT.Player('youtube-iframe', {
height: '390',
width: '640',
videoId: 'M7lc1UVf-VE',
events: {
'onReady': onPlayerReady,
}
});
}
function onPlayerReady(event) {
console.log('player ready');
event.target.playVideo();
};
In a chrome-app I was able to make it work with a webview but this does not seem to be available in extensions.
I solved the problem, here is the solution.
I tried all variants of the code injection method but the problem was the the YouTube API script was defining an anonymous function that expected the window as an input argument. So even after following the advice of not loading external scripts (chrome web store might remove your extension) and having a local file that I included with different means I was not able to get the onYouTubeIframeAPIReady to be triggered by the YouTube API script. Only after pasting the script into the same file where I defined onYouTubeIframeAPIReady I was able to see the video. However to organize the code better, so it works with ES6 imports (via Webpack) I did the following steps.
Download the YouTube API script (https://www.youtube.com/iframe_api see https://developers.google.com/youtube/iframe_api_reference) to a local file.
Adopt the script to work as module by changing the the script from
(function(){var g,k=this;function l(a){a=a.split(".");
...
Ub=l("onYouTubePlayerAPIReady");Ub&&Ub();})();
to
export default function(){var g,k=window;function l(a){a=a.split(".")
...
Ub=l("onYouTubePlayerAPIReady");Ub&&Ub();}
This changes the anonymous function call to a function that is exported in a ES6 module style and the this object in the anonymous function is exchanged with the window. I saved it in the file as youtube-iframe-api.js
Now I was able to use the YouTube API in another module with the following code
import youtubeApi from './youtube-iframe-api';
function onPlayerReady(event) {
event.target.playVideo();
},
window.onYouTubeIframeAPIReady = () => {
this.player = new YT.Player('youtube-iframe', {
height: '100',
width: '100',
videoId: 'M7lc1UVf-VE',
events: {
'onReady': onPlayerReady,
}
});
}
youtubeApi();
When executed in Node context (node-main),
setTimeout(function () {
console.log(nw);
}, 20);
throws
nw is not defined
because WebKit context is not ready (right from the start window is unavailable in NW.js <= 0.12, window.nw in NW.js >= 0.13). And
setTimeout(function () {
console.log(nw);
}, 200);
works just fine but setTimeout looks like a hack, setting it to safe delay value may cause undesirable lag.
How can the availability of WebKit context and nw be checked from Node context? Is there a reasonable way, like an event that could be handled?
The following achieves the same thing but does it the other way around.
In your html file:
<body onload="process.mainModule.exports.init()">
In your node-main JS file:
exports.init = function() {
console.log(nw);
}
Here, init function is only called when Webkit context/DOM is available.
You could use pollit :) ...
var pit = require("pollit");
foo = function(data) {
console.log(nw);
};
pit.nw('nw', foo);
I've tested it and it works for me :). This modularizes the solution that I give near the end of this.
The nw object does not exist until webkit is up and running ie the browser
window has been created. This happens after Node starts up which is why you're
getting this error. To use the nw api you either create events that can be
listened to or call global functions the former being preferable. The following code will demonstrate both and should give you a good idea of how Node and WebKit are interfacing with each other.
This example creates a Window, opens devtools and allows you to toggle the
screen. It also displays the mouse location in the console. It also demonstrates how to send events using the DOM ie body.onclick() and attaching events from within Node ie we're going to catch minimize events and write them to the console.
For this to work you need to be using the SDK version of NW. This is my package.json
{
"name": "hello",
"node-main": "index.js",
"main": "index.html",
"window": {
"toolbar": true,
"width": 800,
"height": 600
},
"dependencies" : {
"robotjs" : "*",
"markdown" : "*"
}
}
The two files you need are index.html
<!DOCTYPE html>
<html>
<head>
<script>
var win = nw.Window.get();
global.win = win;
global.console = console;
global.main(nw);
global.mouse();
var markdown = require('markdown').markdown;
document.write(markdown.toHTML("-->Click between the arrows to toggle full screen<---"));
</script>
</head>
<body onclick="global.mouse();">
</body>
</html>
and index.js.
var robot = require("robotjs");
global.mouse = function() {
var mouse = robot.getMousePos();
console.log("Mouse is at x:" + mouse.x + " y:" + mouse.y);
global.win.toggleFullscreen();
}
global.main = function(nw_passed_in) {
global.win.showDevTools();
console.log("Starting main");
console.log(nw_passed_in);
console.log(nw);
global.win.on('minimize', function() {
console.log('n: Window is minimized from Node');
});
}
When running this I used
nwjs --enable-logging --remote-debugging-port=1729 ./
You can then open up the browser using
http://localhost:1729/
for debugging if needed.
If you want to do something as soon as the nw object exists you can poll it. I'd use eventEmitter, if you don't want to use event emitter you can just as easily wrap this in a function and call it recursively. The following will display how many milliseconds it took before the nw object was setup. On my system this ranged between 43 - 48 milliseconds. Using a recursive function was no different. If you add this to the code above you'll see everything logged to the console.
var start = new Date().getTime();
var events = require('events');
var e = new events.EventEmitter();
var stop = 0;
e.on('foo', function() {
if(typeof nw === 'undefined') {
setTimeout(function () {
e.emit('is_nw_defined');
}, 1);
}
else {
if(stop === 0) {
stop = new Date().getTime();
}
setTimeout(function () {
console.log(stop - start);
console.log(nw);
e.emit('is_nw_defined');
}, 2000);
}
});
e.emit('is_nw_defined');
Solution 1:
You can use onload, see reference.
main.js:
var gui = require("nw.gui"),
win = gui.Window.get();
onload = function() {
console.log("loaded");
console.log(win.nw);
};
index.html:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="main.js"></script>
</head>
<body></body>
</html>
package.json:
{
"name": "Freebox",
"main": "index.html"
}
Solution 2:
(To prevent issue, but it is not necessary).
var gui = require("nw.gui"),
win = gui.Window.get();
onload = function() {
console.log("loaded");
var a = function () {
if (!win.nw) return setTimeout(a, 10);
console.log(win.nw);
};
};
The solution I've initially come up looks like
app-node.js
process.once('webkit', () => {
console.log(nw);
});
app.html
<html>
<head>
<script>
global.process.emit('webkit');
</script>
...
I would be glad to know that there is already an event to listen, so cross-platform client scripts could omit NW-related code.
I'm trying to play around with node webkits hotkey example which can be viewed on their Shortcut's page here: https://github.com/nwjs/nw.js/wiki/Shortcut
Here's my code:
test.js
// Load native UI library.
var gui = window.require('nw.gui');
var option = {
key : "Ctrl+Shift+A",
active : function() {
console.log("Global desktop keyboard shortcut: " + this.key + " active.");
},
failed : function(msg) {
// :(, fail to register the |key| or couldn't parse the |key|.
console.log(msg);
}
};
// Create a shortcut with |option|.
var shortcut = new gui.Shortcut(option);
// Register global desktop shortcut, which can work without focus.
gui.App.registerGlobalHotKey(shortcut);
// If register |shortcut| successfully and user struck "Ctrl+Shift+A", |shortcut|
// will get an "active" event.
// You can also add listener to shortcut's active and failed event.
shortcut.on('active', function() {
console.log("Global desktop keyboard shortcut: " + this.key + " active.");
});
shortcut.on('failed', function(msg) {
console.log(msg);
});
// Unregister the global desktop shortcut.
gui.App.unregisterGlobalHotKey(shortcut);
index.html
<!DOCTYPE html>
<html>
<head>
<title>Hello World!</title>
<script>
require("./test.js");
</script>
</head>
<body>
<h1>Hello World!</h1>
We are using node.js <script>document.write(process.version)</script>.
</body>
</html>
package.json
{
"name": "nw-demo",
"main": "index.html",
"dependencies": {
"nw": "^0.12.0"
},
"scripts": {
"start": "nw"
}
}
It breaks at this line on test.js saying undefined is not a function.
var shortcut = new gui.Shortcut(option);
Just remove this line:
gui.App.unregisterGlobalHotKey(shortcut);
In your code you register it, then delete. It working well for me (mac, nwjs 0.12)
As shaynem in (https://github.com/nwjs/nw.js/issues/3263#issuecomment-89679115) points out you should be running at least node-webkit >= 0.10.0
You should make sure your PATH (or the build tool you're using, e.g. nodebob) is not referencing any old leftover versions of node-webkit after you upgrade.
Also if you're on Ubuntu, you might stumble upon an open issue (https://github.com/nwjs/nw.js/issues/3199)
note webkit 0.13.0 has a bug where the the k is lowercase even though the method is documented with an uppercase K. Try:
registerGlobalHotkey(shortcut);
to verify this bug exists do:
var util = require('util');
console.log(util.inspect(nw.App));
I want my plugin to inject so that when i click on the button, it runs a function in the current tab. However, it's giving me a function not found error.. is there some way to do this?
This is my popup.html:
<script>
function start() {
chrome.tabs.executeScript( null,
{ code: "func_in_body()",
allFrames: true }
);
}
start();
</script>
and even though the function is in the page, it gives me an error
The error is:
Uncaught ReferenceError: func_in_body is not defined
(anonymous function)
though one of the buttons' onclick uses it. I'm not sure if there's a scope issue or not.
JavaScript you inject into tabs is executed in a isolated environment and does not have access to the pages JavaScript. You can read more about it in the documentation.
You don't need a popup to execute a function or a script on the current tab. What I did is, I made a background page with:
<html>
<head>
<script>
var iconName = "icon.png";
chrome.browserAction.setIcon({path:iconName});
function onClicked(){
chrome.tabs.executeScript(null, {file: "content_script.js"});
}
chrome.browserAction.onClicked.addListener(onClicked);
</script>
</head>
</html>
the background page has to be defined in the manifest.json
...
"background_page": "background.html",
...
and the last thing just create the content_script.js (or whatever you called it) and enter your code there.
Edit:
Don't forget to add the permissions that your script can be executed on every site
...
"permissions": [
"tabs",
"http://*/*",
"https://*/*"
],
...
I have a lot of code that I only want to run when the user clicks the extension icon. I'd rather not have it run for every tab opened. Thus using the content_scripts entry in the manifest file isn't the best option. However, I haven't been able to see the content scripts show up in the list of scripts in the developer tools when I programatically inject scripts. I'm fine developing for now with content scripts, but at some point I'd like to avoid it.
I run logging all over the place, and perform message passing as well. So I know very well that these scripts are successfully getting injected and running, but they simply fail to show up in the file list.
In code, the following works just dandy (in the manifest):
{
// ...
"content_scripts": [{
"matches": ["<all_urls>"],
"css": ["style/content.css"],
"js": [
"closure/goog/base.js",
"closure/goog/deps.js",
"util.js",
"AddressRE.js",
// ...
"makeRequests.js"
]
}]
}
Performing the following after an onClick does not:
function executeNextScript(tabId, files, callback) {
chrome.tabs.executeScript(tabId, {
file: files.pop()
}, function () {
if (files.length)
executeNextScript(tabId, files, callback);
else
callback();
});
}
function executeScripts(tabId, callback) {
var files = [
"closure/goog/base.js",
"closure/goog/deps.js",
"util.js",
// ...
"makeRequests.js"
];
executeNextScript(tabId, files.reverse(), callback);
}
You can use the debugger JavaScript keyword to set breakpoints in your code.
I add //# sourceURL=myscript.js to any script that is injected and that adds it to the list of sources once it has been injected