chrome.webNavigation.onCompleted - Event firing multiple times - javascript

I load plagin for Inject code to page, the manifest code:
{
"name": "any",
"version": "1.0",
"permissions": [
"webNavigation",
"*://*/*"
],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"manifest_version": 2
}
And the background.js:
chrome.webNavigation.onCompleted.addListener(function(details) {
chrome.tabs.executeScript(details.tabId, {
code: 'console.log("A")'
});
});
The problem is the event firing a few times after page load, I want the event just one time. What is my mistake?
I would appreciate any help on.

chrome.webNavigation.onCompleted is invoked even when the navigation occurs in a subframe. One way to capture it only once is to implement your code with a condition for frame id. frame id = 0 corresponds to the parent frame. Your code would look like :
chrome.webNavigation.onCompleted.addListener(function(tab) {
if(tab.frameId==0){
//logic
}
});
detailed documentation available here : https://developers.chrome.com/extensions/webNavigation

The best practice is to use named functions, register them when ready and, on the performance point of view, only for the requested URLs (if possible).
in the next example the listener is getting removed before registered plus only if the filter contains URLs:
if (filter.url.length < 1) {
resolve(dictionary);
return;
}
if (chrome.webNavigation.onBeforeNavigate.hasListener(onBeforeNavigate))
chrome.webNavigation.onBeforeNavigate.removeListener(onBeforeNavigate);
chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate, filter);
and the named should follow this pattern:
const onBeforeNavigate = (details) => {
if (details.frameId > 0)
return;
...
}
see more info here:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webNavigation/onCompleted

Related

Why does my browser extension only works the first time it's added or when it's reloaded?

I'm trying to create a simple extension for Chrome and Firefox which just gets some content from the DOM of certain pages and adds other content.
But the issue is that it only works the first time I load it as a temporary Add-on for testing on both Chrome and Firefox, or when I hit the extension's reload button on about:debugging on Firefox.
My manifest.json only contains the following information:
{
"manifest_version": 2,
"name": "ft_blackhole",
"version": "0.1",
"description": "Shows how many days you have left before you get absorbed by the Blackhole.",
"icons": {
"48": "icons/blackhole.png"
},
"content_scripts": [
{
"matches": [
"https://profile.intra.42.fr/",
"https://profile.intra.42.fr/users/*",
"https://profile.intra.42.fr/users/*/"
],
"js": [
"ft_blackhole.js"
]
}
],
"browser_specific_settings": {
"gecko": {
"id": "ft_blackhole-0.1#intra.42.fr"
}
}
}
I use almost the same exact manifest file (same exact matches) for another extension that works and depends on the same URLs. That extension works fine. The difference between it and this one is that while they both insert content in the DOM, my older extension does it after it fetches data from an API, but this one just gets some text from the current page's DOM:
ft_blackhole.js:
console.log("Hello World");
let blackholeDiv = document
.getElementById("bh")
.getElementsByClassName("emote-bh")[0];
let daysLeft = blackholeDiv.getAttribute("data-original-title");
let daysNum = daysLeft.split(" ")[0];
let status = (() => {
if (daysNum <= 14)
return {cat: "😿", color: "#D8636F"};
else if (daysNum <= 42)
return {cat: "🙀", color: "#F0AD4E"};
else
return {cat: "😸", color: "#5CB85C"};
})();
let daysLeftDiv = document.createElement("div");
daysLeftDiv.innerText = daysLeft + ' ' + status["cat"];
daysLeftDiv.style.color = status["color"];
daysLeftDiv.style.fontSize = "0.7em";
daysLeftDiv.style.fontWeight = "400";
blackholeDiv
.children[1]
.appendChild(daysLeftDiv);
I searched all over the internet, I couldn't understand what is causing it to run only the first time the extension is installed, but then when I refresh the page, it doesn't add anything to the page anymore, until I reload the extension again from about:debugging.
I hope someone could help.
Edit:
I also noticed that it also works after a hard reload of the page on Chrome, but not after a normal reload.
Edit 2:
When I console.log() something in the content script, it works all the time, always logs when I reload the page (normal reload), but the other code for DOM manipulation doesn't...
Edit 3:
I have uploaded a static copy of the profile page if anyone wants to take a look at the HTML content: https://haddi.me/intra-example/intra.html
Edit 4:
It seems the issue is caused after everything is loaded in the DOM, by some javascript that runs after that and makes changes to it, I used Mutation Observer on the target node to log those changes, and there were indeed some few ones, what I can't figure out is how am I supposed to run my code only after that last change (Which is their modification of span#bh-date's style attribute)? This the code I added:
const blackholeDiv = document
.getElementById("bh")
.getElementsByClassName("emote-bh")[0];
// Callback function to execute when mutations are observed
• const callback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed: ', mutation);
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified: ', mutation);
}
}
};
const config = {attributes: true, childList: true, subtree: true};
const observer = new MutationObserver(callback);
observer.observe(blackholeDiv, config);
And these are the logs:
Screenshot from Chrome's Devtools Console
In your manifest.json file, I do not see a "run_at", to make sure the DOM is ready and then immediately inject the script. Use the "document_end" string in your code.
Source:
https://developer.chrome.com/docs/extensions/mv3/content_scripts/#run_time
manifest.json
{
"name": "Test Extenstion",
"description": "Content test!",
"version": "1.0",
"manifest_version": 2,
"content_scripts": [
{
"matches": ["https://haddi.me/*"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"permissions": ["activeTab"]
}
Tested in Google Chrome web browser version 99.0.4844.83.

How to Access / Read any Text / Tag Value from the Webpage using Chrome Extension

I am writing my First Chrome Plugin and I just want to get some text present on the current webpage and show it as a alert when i click the Extension. Lets say I am using any any webpage on www.google.com after some Search query, Google shows something like "About 1,21,00,00,000 results (0.39 seconds) " . I want to show this Text as an alert when i execute my plugin. This is what i am doing.
here is the manifest.json that i am using
{
"manifest_version": 2,
"name": "Getting started example",
"description": "This extension shows a Google Image search result for the current page",
"version": "1.0",
"background": {
"persistent": false,
"scripts": ["background.js"]
},
"content_scripts": [{
"matches": ["*://*.google.com/*"],
"js": ["content.js"]
}],
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"permissions": [
"activeTab"
]
}
Here is my popup.js
document.addEventListener('DOMContentLoaded', function() {
document.getElementById("checkPage").addEventListener("click", handler);
});`
function handler() {
var a = document.getElementById("resultStats");
alert(a.innerText); // or alert(a.innerHTML);
}
Here is my content.js
// Listen for messages
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
// If the received message has the expected format...
if (msg.text === 'report_back') {
// Call the specified callback, passing
// the web-page's DOM content as argument
sendResponse(document.all[0].outerHTML);
}
});
Here is my background.js
var urlRegex = /^https?:\/\/(?:[^./?#]+\.)?google\.com/;
// A function to use as callback
function doStuffWithDom(domContent) {
console.log('I received the following DOM content:\n' + domContent);
}
// When the browser-action button is clicked...
chrome.browserAction.onClicked.addListener(function (tab) {
// ...check the URL of the active tab against our pattern and...
if (urlRegex.test(tab.url)) {
// ...if it matches, send a message specifying a callback too
chrome.tabs.sendMessage(tab.id, {text: 'report_back'}, doStuffWithDom);
}
});
1) run content scrip after document ready ** check "run_at"
"content_scripts": [{
"run_at": "document_idle",
"matches"["*://*.google.com/*"],
"js": ["content.js"]
}],
2) on click of extension make another js to run( popup js). The popup js has access to the ( open page document)
// A function to use as callback
function doStuffWithDom() {
//console.log('I received the following DOM content:\n' + domContent);
//get tabID when browser action clicked # tabId = tab.id
chrome.tabs.executeScript(tabId, {file: "js/popup.js"});
}
3) In popup JS simple you can set alert
var a = document.getElementById("resultStats");
alert(a.innerText); // or alert(a.innerHTML);
Just remove "default_popup" part in manifest.json, as you have listened to chrome.browserAction.onClicked event in background page. They can't live at the same time.

Running a function in chrome extension on double click/select of a word

I am trying to fire a notification whenever I double click on a word/select it. Found several examples online but I still cannot get my example working:
Manifest:
{
"name": "hh",
"description": "hh!",
"manifest_version": 2,
"version": "0.0.0.1",
"content_scripts": [
{
"matches": [ "http://*/*", "https://*/*" ],
"js": [ "background.js" ],
"all_frames": true,
"run_at": "document_end"
}
],
"permissions": ["storage", "notifications"],
"icons": { "128": "neoprice.png" }
}
background.js
var listener = function(evt) {
var selection = window.getSelection();
if (selection.rangeCount > 0) {
displayPrice();
var range = selection.getRangeAt(0);
var text = range.cloneContents().textContent;
console.log(text);
}
};
document.addEventListener('dblclick', listener);
function displayPrice(){
chrome.notifications.create(getNotificationId(), {
title: "message.data.name",
iconUrl: 'hh.png',
type: 'basic',
message: "message.data.prompt"
}, function() {});
}
// Returns a new notification ID used in the notification.
function getNotificationId() {
var id = Math.floor(Math.random() * 9007199254740992) + 1;
return id.toString();
}
I was earlier adding the following but I saw people weren't using it, so I removed it
"app": {
"background": {
"scripts": ["background.js", "assets/jquery.min.js"]
}
},
What I am trying to achieve: Whenever they go to ANY page on selecting a word, it fires the function. Later, I wish to use this for a specific page. :)
Tried: How to keep the eventlistener real time for chrome extensions?
Chrome extension double click on a word
https://github.com/max99x/inline-search-chrome-ext
Both don't really work as I want them too. :(
Solution
It seems you are confused with background page and content script. Your background.js is a content script in fact, though its name is "background". While chrome.notifications api can be only called in background page, trying commenting displayPrice function will make your code work.
Next step
Take a look at above tutorials, wdblclick event triggers, use Message Passing to communicate with background page and call chrome.notications api in background page.
What's more
The following code is used in chrome apps rather than chrome extension.
"app": {
"background": {
"scripts": ["background.js", "assets/jquery.min.js"]
}
},

Dynamic loading of content script (chrome extension)

I have an chrome extension, with 2 content script injected by manifest and one background script.
{
"manifest_version": 2,
"name": "Test",
"permissions": [
"tabs", "<all_urls>", "activeTab", "storage"
],
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": [
"content/autofill/lib_generic.js",
"content/autofill/lib.js"],
"run_at": "document_end"
}
],
"web_accessible_resources": [
"content/specific_scripts/*"
],
"background": {
"scripts": ["background.js"],
"persistent": false
}
}
lib_generic.js contains one function named apply_forms(...) (its description is not important). The function is called from lib.js file. But this procedure doesn't work with several pages, so for each such page a I have a special script - also with only one function named apply_forms(...).
I have a function, which takes current domain as input and returns name of desired specific script or false if generic should be used.
There is too many files and it's logic is more complicated, so I can't just list all (url, script) pairs in "content_scripts" directive (I also don't want to inject all specific files as content script).
I've tried something like this in background (note that it's only for demonstration):
var url = ""; //url of current tab
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if(changeInfo.status == "complete") {
var filename = getSpecificFilename(url);
chrome.tabs.executeScript(tabId, {file: filename}, function() {
//script injected
});
}
});
NOTE: getSpecificFilename(...) will always return a name
But I get Unchecked runtime.lastError while running tabs.executeScript: Cannot access a chrome:// URL on the 5th line.
Can anyone help me with this? Is it the good way to "override` function definition dynamically, or should I go different way (which one, then).
Thanks.
This means, probably, that you're getting an onUpdated event on an extension/internals page (popup? options page? detached dev tools?).
One option is to filter by URL:
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if(changeInfo.status == "complete") {
if(!tab.url.match(/^http/)) { return; } // Wrong scheme
var filename = getSpecificFilename(url);
chrome.tabs.executeScript(tabId, {file: filename}, function() {
//script injected
});
}
});
Another (and probably better) option is to make your content script request this injection:
// content script
chrome.runtime.sendMessage({injectSpecific : true}, function(response) {
// Script injected, we can proceed
if(response.done) { apply_forms(/*...*/); }
else { /* error handling */ }
});
// background script
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if(message.injectSpecific){
var filename = getSpecificFilename(sender.url);
chrome.tabs.executeScript(sender.tab.id, {file: filename}, function() {
sendResponse({ done: true });
});
return true; // Required for async sendResponse()
}
});
This way you know that a content script is injected and initiated this.

Getting the div-class id on clicking using jquery

Bear with me please if this sounds too simple.
I am writing a chrome extension that once the user right clicks on an object in the page, the context menu appears and there he has an option to get the ID the of clicked div-class + rate it.
I've been working on this for a few days and managed to add the extra buttons to the context menu. Once any of those buttons is clicked, the page's source is logged. Could have taken me less time to do that but it's my first time writing in javascript and I got stuck along the way.
Anyways I don't want the whole html code of the page, but the clicked div-class. I after googling it turns out that the best way to do this is by using jquery's "html" function instead of pure javascript. Problem is that it doesn't work for me.
Here is my code:
BackgroundPage (everything seems to be working fine here)
function genericOnClick(tab)
{
console.log(tab.pageUrl);
chrome.tabs.getSelected(null, function(tab)
{
chrome.tabs.sendMessage(tab.id, {action: "getSource"}, function(source) {
console.log(source);
});
});
}
// Create a parent item and two children.
var parent1 = chrome.contextMenus.create({"title": "Rank Trigger", "contexts":["all"]});
var child1 = chrome.contextMenus.create
(
{"title": "Rank 1", "contexts":["all"], "parentId": parent1, "onclick": genericOnClick}
);
var child2 = chrome.contextMenus.create
(
{"title": "Rank 2", "contexts":["all"], "parentId": parent1, "onclick": genericOnClick}
);
var child3 = chrome.contextMenus.create
(
{"title": "Rank 3", "contexts":["all"], "parentId": parent1, "onclick": genericOnClick}
);
ContentPage: (problem here)
chrome.extension.onMessage.addListener(function(request, sender, callback)
{
if (request.action == "getSource")
{
// callback(document.documentElement.outerHTML); //here i can get the page's whole source, I only want the clicked div class
callback($('div class).html(); <--- something wrong here
}
});
Manifest file:
{
"name": "x",
"description": "x",
"version": "0.1",
"permissions": ["contextMenus", "tabs"],
"content_scripts":
[
{
"matches": ["http://*/*","https://*/*"],
"js": ["jquery.js", "content.js"],
"run_at": "document_end"
}
],
"background":
{
"scripts": ["background.js"]
},
"manifest_version": 2
}
Any ideas? Again I know that my questions seems simple but I am self teaching myself all this, and for some reason I finding web-programming a little harder than application programming.
Much thanks.
EDIT: Just for clarification the html source I want is this:
<div class="story reviewed-product-story" data-story-id="488481648"....</div>.
I want this div class, so that I can parse it to get the data-story-id. If there is a way to get the ID without getting the div class first then that would also work (and maybe even preferable)
EDIT: Thanks to Adeneo, I can now get the ID of the first div-class in the page's source, but not the clicked one. A step forward on what I had but not exactly what I need.
Maybe I have to get the source of clicked div before using $('.story').data('story-id'). Any suggestions?
I'm guessing it's supposed to be:
chrome.extension.onMessage.addListener(function(request, sender, callback) {
console.log('message listener');
if (request.action == "getSource") {
callback( $('.story').data('story-id') );
}
});

Categories

Resources