In extension the callbacks are accumulating and persisting until browser restart - javascript

I am porting a chess game parser to new manifest v3 specification. When clicking context menu on a selected text, it plays visually the selected text as PGN chess game notation.
The problem is that background script on each game execution adds one more chrome.runtime.onMessage listener, and all the listeners persists until browser closed. So, all listeners are invoked and only first game is played each time.
There is the code:
//background.js
function playBoard (info, imgPath, windowAttributes)
{
console.log("play board with: " + info.selectionText);
let gameObject = {
chessObject:
{
gametype : "PGN_OR_FEN_board",
content : info.selectionText,
imgPath : imgPath
}
};
let windowPromise = chrome.windows.create(windowAttributes);
windowPromise.then((value) => {
chrome.runtime.onMessage.addListener(
(request, sender, sendResponse) =>
{
console.log("backg: chrome.runtime.onMessage()> " + gameObject.chessObject.content);
sendResponse(gameObject);
}
);
});
}
The above code is invoked in following context:
//background.js
console.log("enter global>");
var idParser = "parser parent";
var idMiniBoard = "board mini";
var idMediuBoard = "board medium";
chrome.runtime.onInstalled.addListener(
(details)=>
{
console.log("on installing");
idParser = chrome.contextMenus.create({"id":idParser, "title": "Chess Parser", "contexts":["selection"]});
idMiniBoard = chrome.contextMenus.create({"id":idMiniBoard, "title": "Play Small", "contexts":["selection"], "parentId": idRoatta});
idMediuBoard = chrome.contextMenus.create({"id":idMediuBoard, "title": "Play Medium", "contexts":["selection"], "parentId": idRoatta});
chrome.contextMenus.onClicked.addListener(menuItemClick);
console.log("on installed");
}
)
function menuItemClick(clickData, tab)
{
switch (clickData.menuItemId)
{
case idMiniBoard:
playBoard (clickData, "mini18", {url: "board.html", type:"popup", height : 350, width : 350});
return;
case idMediuBoard:
playBoard (clickData, "medium35", {url: "board.html", type:"popup", height: 450, width: 450});
return;
}
}
There is the popup window startup code
document.addEventListener('DOMContentLoaded',
(event) =>
{
chrome.runtime.sendMessage({ chessObject: { gametype : "PGN_OR_FEN_board" } },
(request) => //in fact is response, but is requested response
{
board_doc_main(request.chessObject);
});
}
);
I am trying to apply similar approach as I did for firefox, here Firefox extension, identification of context. Somehow for firefox this works. I am trying to implement like this: Popup open on click from background worker. Then popup window sends message to background worker to receive the required context, including the selection game text.
###################
UPDATE:
As suggested in response from #wOxxOm, this is exactly how I changed now
function playBoard (clickData, imgPath, windowAttributes)
{
let gameObject = {
chessObject:
{
gametype : "PGN_OR_FEN_board",
content : clickData.selectionText,
imgPath : imgPath
}
};
let windowPromise = chrome.windows.create(windowAttributes);
windowPromise.then((wnd) => {
chrome.runtime.onMessage.addListener (
function __playBoardCallback__ (request, sender, sendResponse)
{
if (sender.tab?.id === wnd.tabs[0].id)
{
chrome.runtime.onMessage.removeListener(__playBoardCallback__);
sendResponse(gameObject);
}
}
);
});
}

Unregister the listener immediately when it runs by using a named function:
async function playBoard(info, imgPath, windowAttributes) {
let gameObject = {
// .........
};
const wnd = await chrome.windows.create(windowAttributes);
const tabId = wnd.tabs[0].id;
chrome.runtime.onMessage.addListener(function _(msg, sender, sendResponse) {
if (sender.tab?.id === tabId) {
chrome.runtime.onMessage.removeListener(_);
sendResponse(gameObject);
}
});
}

Related

Unable to persist state of my chrome extension between tab reloads and tab changes

I am trying to create a pretty simple chrome extension that enables me to navigate the browser back and forward using horizontal scrolling. I want to be able to enable and disable the extension; however I want the state to persist when the tab changes (whether the active tab is a new url or a new tab is launched).
Here is my current code that partially works for only the active tab:
manifest.json:
{
"name": "SwipeNav",
"action": {},
"manifest_version": 3,
"version": "0.1",
"description": "Navigates browser forward and back with 2 finger swiping",
"permissions": [
"activeTab",
"scripting",
"tabs",
"alarms"
],
"background": {
"service_worker": "background.js"
}
}
background.js
function twoFingerBack() {
console.log("Enabling 2 finger scrolling");
let timer;
window.addEventListener("wheel", function(event) {
clearTimeout(timer);
if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
timer = setTimeout(function() {
if (event.deltaX > 0) {
history.back();
} else if (event.deltaX < 0) {
history.forward();
}
}, 200);
}
});
}
function removeExt() {
console.log("Disabling 2 finger scrolling");
window.removeEventListener("wheel", removeExt)
}
let enabled = true;
chrome.action.onClicked.addListener((tab) => {
enabled = !enabled;
if (enabled) {
if (!tab.url.includes("chrome://")) {
chrome.scripting.executeScript({
target: { tabId: tab.id },
function: twoFingerBack
});
}
} else {
if (!tab.url.includes("chrome://")) {
chrome.scripting.executeScript({
target: { tabId: tab.id },
function: removeExt
});
}
}
});
I have read that I can use chrome.tabs.onActivated however I could not get this to work at all. Not sure if that is the right path.
Any help is appreciated...thanks!
Save the state in storage. Since localStorage is not available in a service worker you can use chrome.storage.local.
Register/unregister the content script, so it runs automatically next time the tab is loaded.
Process all tabs using executeScript to apply the changes to the currently loaded pages.
Use a global function for the wheel listener and use its name in removeEventListener.
// manifest.json
Add "storage" inside "permissions"
Add "host_permissions": ["<all_urls>"]
// background.js
chrome.action.onClicked.addListener(async (tab) => {
let {enabled} = await chrome.storage.local.get('enabled');
enabled = !enabled;
chrome.storage.local.set({enabled});
await chrome.scripting.unregisterContentScripts({ids: ['foo']}).catch(() => {});
if (enabled) {
chrome.scripting.registerContentScripts([{
id: 'foo',
js: ['content.js'],
matches: ['<all_urls>'],
runAt: 'document_start',
}]);
}
const execOpts = enabled ? {files: ['content.js']} : {func: removeExt};
const tabs = (await chrome.tabs.query({}))
.sort(t => t.active ? -1 : 0); // processing the active tab(s) first
for (const {id} of tabs) {
chrome.scripting.executeScript({target: {tabId: id}, ...execOpts})
.catch(() => {});
}
});
function removeExt() {
console.log('Disabling');
// onWheel is the name of the global function used in content.js
if (typeof onWheel === 'function') removeEventListener('wheel', onWheel);
}
// content.js
console.log('Enabling');
if (typeof onWheel !== 'function') (() => {
let timer;
window.onWheel = e => {
clearTimeout(timer);
const x = e.deltaX;
if (x && Math.abs(x) > Math.abs(e.deltaY)) {
timer = setTimeout(() => x > 0 ? history.back() : history.forward(), 200);
}
};
addEventListener('wheel', onWheel);
})();

Context menu not reponding for first click

I am building an extension in which content script is injected on context menu click. This functionality is working. Now the problem I am facing is that, the first click on context menu is not working.
Now the problem I am facing is that, the first click on context menu is not working. Is this a bug?.
background.js
on_message = async(message, sender, sendResponse) => {
console.log("bg.on_message");
sendResponse("from bg");
chrome.storage.local.get("list_url", function (data) {
if (typeof data.list_url != "undefined") {
urls = data.list_url
}
});
chrome.storage.local.get("list_ip", function (data) {
if (typeof data.list_ip != "undefined") {
ips = data.list_ip
}
});
chrome.storage.local.get("list_hash", function (data) {
if (typeof data.list_hash != "undefined") {
hashes = data.list_hash;
}
});
if (hashes){
hash_report = await createHashReport(hashes)
hash_table = await createHashTable(hash_report)
await chrome.storage.local.set({
"scanHash": true,
"hash_table": hash_table
}, () => {});
}
if (ips){
ip_report = await createIpReport(ips)
ip_table = await createIpTable(ip_report)
await chrome.storage.local.set({
"scanIp": true,
"ip_table": ip_table
}, () => {});
}
if (urls){
url_report = await createUrlReport(urls)
url_table = await createUrlTable(url_report)
await chrome.storage.local.set({
"scanUrl": true,
"url_table": url_table
}, () => {});
}
if ( hashes.length>0 || urls.length>0 || ips.length>0 ){
chrome.windows.create({url: "output.html", type: "popup", height:1000, width:1000});
}
}
chrome.runtime.onMessage.addListener(on_message);
genericOnClick = async () => {
// Inject the payload.js script into the current tab after the backdround has loaded
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
chrome.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ["payload.js"]
},() => chrome.runtime.lastError);
});
}
// create context menu
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'scrapper',
"title": "Scrapper",
"contexts": ["all"]
});
});
chrome.contextMenus.onClicked.addListener(genericOnClick);
payload.js
function extract() {
htmlInnerText = document.documentElement.innerText;
url_exp = /[-a-zA-Z0-9#:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9#:%_\+.~#?&//=]*)?/gi;
regex = new RegExp(url_exp)
list_url = htmlInnerText.match(url_exp)
ip_exp = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/;
list_ip = htmlInnerText.match(ip_exp)
hash_exp = /\b[A-Fa-f0-9]{32}\b|\b[A-Fa-f0-9]{40}\b|\b[A-Fa-f0-9]{64}\b/g
list_hash = htmlInnerText.match(hash_exp)
await chrome.storage.local.set({ list_url: removeEmails(removeDuplicates(list_url)), list_ip: removeDuplicates(list_ip), list_hash: removeDuplicates(list_hash) });
}
chrome.runtime.sendMessage( extract());
Assuming that "removeEmails" and "removeDuplicates" are synchronous functions (only you know this), the chrome.storage.local.set method instead returns a promise. Basically you send a message to the background hoping that it can read the updated values of list_url, list_ip and list_hash, when instead it has those available before the storage.local.set (so it won't find anything on the first round).
I thought you'd already figured it out yourself with my clue.
Add await keyword before each chrome.storage.local.set

Can't click buttons in the title bar (Electron App)

I try to build a simple Text Editor using Electron.
At the moment I want to add a custom title bar which doesn't really work as the buttons are not clickable...
I added an onclick tag to the buttons.
main.js:
//-- variables --\\
const { BrowserWindow, app, Menu, dialog, ipcMain } = require("electron")
let window
let filePath = null //currently opened file
let openFiles = [] //opened files
//-- startup --\\
app.whenReady().then(function()
{
createWindow()
createMenu()
})
//-- functions --\\
function createWindow()
{
window = new BrowserWindow
({
width: 1000,
height: 600,
frame: false,
icon: "./assets/icon.png",
darkTheme: true,
autoHideMenuBar: true,
webPreferences:
{
nodeIntegration: true,
contextIsolation: false
}
})
//window.maximize()
window.loadFile("./frontend/index.html")
window.webContents.openDevTools()
}
function createMenu()
{
Menu.setApplicationMenu(Menu.buildFromTemplate
([
{
label: "File",
submenu:
[
{
label: "Save",
click: saveFile,
accelerator: "CmdOrCtrl+S"
},
{
label: "Open",
click: openFile,
accelerator: "CmdOrCtrl+O"
},
{
label: "New",
click: newFile,
accelerator: "CmdOrCtrl+N"
}
]
}
]))
}
async function promptFilePathOpen()
{
await dialog.showOpenDialog
({ properties: ["openFile"] }).then(function(res)
{
if(res.canceled) return
filePath = res.filePaths[0]
})
}
async function promptFilePathSave()
{
await dialog.showSaveDialog().then(function(res)
{
if(res.canceled) return
filePath = res.filePath
})
}
async function openFile()
{
await promptFilePathOpen()
//openFiles.push(filePath)
window.webContents.send("crd-openFile", filePath)
}
async function saveFile()
{
if(filePath == null || undefined) await promptFilePathSave()
window.webContents.send("crd-saveFile", filePath)
}
async function newFile()
{
filePath = null
await promptFilePathSave()
window.webContents.send("crd-resetEditor")
window.webContents.send("crd-saveFile", filePath)
}
//-- listeners --\\
app.on("window-all-closed", function()
{
if(process.platform != "darwin") app.quit()
})
ipcMain.on("crd-minimizeWindow", function() //does not get called by the renderer
{
//coming soon
})
ipcMain.on("crd-toggleWindowSize", function() //does not get called by the renderer
{
//coming soon
})
ipcMain.on("crd-closeWindow", function() //does not get called by the renderer
{
console.log("quit")
})
renderer/script.js:
//-- imports --\\
const { ipcRenderer } = require("electron")
const fs = require("fs")
const editorElem = document.querySelector("textarea.editor")
//-- functions --\\
function minimizeWindow() // does not get called by the button (index.html)
{
ipcRenderer.send("crd-minimizeWindow")
}
function toggleWindowSize() // does not get called by the button (index.html)
{
ipcRenderer.send("crd-toggleWindowSize")
}
function closeWindow() // does not get called by the button (index.html)
{
ipcRenderer.send("crd-closeWindow")
}
//-- listeners --\\
ipcRenderer.on("crd-openFile", function(e, path)
{
editorElem.value = fs.readFileSync(path, "utf-8")
})
ipcRenderer.on("crd-saveFile", function(e, path)
{
fs.writeFile(path, editorElem.value , function(res, err)
{
if(err) throw err
})
})
ipcRenderer.on("crd-resetEditor", function()
{
editorElem.value = ""
})
The entire code is avalable on my GitHub, because I do not want the whole question consisting of code :)
Two issues here:
You defined functions like closeWindow, but you didn't actually add an event listener for them. You mention onclick but I can't see that in your code. So the first step would be to add document.querySelector('.closeWindow').addEventListener('click', closeWindow) .
You made the whole title bar draggable, including the buttons. That means that the role of the buttons is also a draggable area, so when you click them, you start the drag operation instead of sending a click event. The solution is therefore to make sure the button area does not have the -webkit-app-region: drag style but only the area left to them has. This will probably require you to redesign the HTML layout for the title bar a bit, since this won't work well with the whole thing being a grid.
For more details, see this tutorial.

Google Chrome extension to close notification after user click

The Chrome extension works fine. My problem is that the notification closes in 7 seconds. I want for the user click to close the notification.
function engine(){
var latestId;
var ids;
var messages = [];
var newmessage = [];
$.get('http://localhost/site/json.php',function (data){
var json = $.parseJSON(data);
messages = json;
ids = json[0].id;
if(latestId == ids){
//no update
}else if(latestId === undefined){
var run = {
type: "basic",
title: "Title",
message: "Some message",
iconUrl : "icon.png",
};
chrome.notifications.create(run);
}
});
}
First create your notification and give it a notificationID parameter to close it later.
var notification = {
type:"basic",
title:"News From Us!",
message:"Google Chrome Updated to v50!",
iconUrl:"assets/images/icon.png"
};
chrome.notifications.create("notfyId",notification);
On notification click you can close notification using its id (which is "notfyId")
function forwardNotfy(){
chrome.notifications.clear("notfyId");
window.open(url); //optional
}
chrome.notifications.onClicked.addListener(forwardNotfy);
Now, when you click your notification it'll close.
This feature is currently only implemented in the beta channel, and not in the latest version of chrome. See here for details.
When it is implemented, you will be able to use requireInteraction : True like:
var run = {
type: "basic",
title: "Title",
message: "Some message",
iconUrl : "icon.png",
requireInteraction : True,
}
to implement this.
You can use notification.close();:
setTimeout(function() {
notification.close();
}, 2000);
Demo:
// request permission on page load
document.addEventListener('DOMContentLoaded', function () {
if (Notification.permission !== "granted")
Notification.requestPermission();
});
function notifyMe() {
if (!Notification) {
alert('Desktop notifications not available in your browser. Try Chromium.');
return;
}
if (Notification.permission !== "granted")
Notification.requestPermission();
else {
var notification = new Notification('Notification title', {
icon: 'http://cdn.sstatic.net/stackexchange/img/logos/so/so-icon.png',
body: "Hey there! You've been notified!x",
});
notification.onclick = function () {
window.open("http://stackoverflow.com/a/13328397/1269037");
};
setTimeout(function() {
notification.close();
}, 2000);
}
}
<button onclick="notifyMe()">
Notify me!
</button>
JSBin Demo
notification.close() is used to close any notification.
For more information please see the below code-
To create the notification:
var notification = new Notification('OnlineOfferPrice', {
icon: 'icon.png',
body: "Test Message",
});
If you want to perform operation on notification click please use the below code. After your business logic it will automatically close-
notification.onclick = function () {
//write your business logic here.
notification.close();
};
If notification is not clicked and you want to close it automatically after 6 seconds-
setTimeout(function() { notification.close(); }, 6000);

Firefox Extension trouble using port.emit and port.on

I can't seem to get port.emit working correctly with my Firefox extension. From the init() function in popup.js the messages are correctly sent to main.js using addon.port.emit. Once they've been sent, the giveStorage message is correctly received in popup.js. However, this only works correctly when the original message is sent in the init function.
When I try sending the messages using using the jQuery change listener, the logs "Storage has been set." and "Sending storage to popup.js" come through, so popup.js is just not receiving it, but I have no idea why not. Only messages are logged correctly when ran from the init function.
If anyone has any ideas, or if you need any more information, please let me know and I'll see what I can do. Any help is greatly appreciated!
main.js
panel.port.on("setStorage", function (text) {
console.log("Storage has been set.");
ss.storage[text[0]] = text[1];
})
panel.port.on("getStorage", function (text) {
console.log("Sending storage to popup.js");
panel.port.emit("giveStorage", [text, ss.storage[text]])
})
popup.js
function init(){
$(".panel").hide();
addon.port.emit("getStorage", "username");
addon.port.emit("getStorage", "volume");
setInterval(function(){following();}, 60000);
}
addon.port.on("giveStorage", function (text) {
console.log("Message received from main.js");
if (text[1] !== null) {
if (text[0] === "username") {
username = text[1];
$('#menuFollowing').click();
}
else if (text[0] === "volume"){
volume = text[1];
$("#volume").val(volume);
$('#volumeValue').empty();
$('#volumeValue').append('Volume: ' + volume);
}
}
})
$('#volume').change(function(){
volume = $('#volume').val();
addon.port.emit("setStorage", ["volume", volume]);
addon.port.emit("getStorage", "volume");
});
Complete main.js
var { ToggleButton } = require('sdk/ui/button/toggle');
var panels = require("sdk/panel");
var self = require("sdk/self");
var ss = require("sdk/simple-storage");
var notifications = require("sdk/notifications");
var panel = panels.Panel({
width: 500,
height: 500,
contentURL: self.data.url("popup.html"),
onHide: handleHide,
});
var button = ToggleButton({
id: "hitbox-plus",
label: "hitbox Plus",
icon: {
"16": "./icon16.png",
"48": "./icon48.png",
},
onChange: handleChange,
});
function handleChange(state) {
panel.contentURL = self.data.url("popup.html");
if (state.checked) {
panel.show({
position: button
});
}
}
function handleHide() {
button.state('window', {checked: false});
}
panel.port.on("getStorage", function (text) {
console.log("Sending storage to popup.js");
panel.port.emit("giveStorage", [text, ss.storage[text]])
})
panel.port.on("setStorage", function (text) {
console.log("Storage has been set.");
ss.storage[text[0]] = text[1];
})

Categories

Resources