I have a content script which modifies the DOM based on the current url.
My background script listens to chrome.webNavigation.onHistoryStateUpdated and sends a message to the content script with the current url:
chrome.webNavigation.onHistoryStateUpdated.addListener(onNavigation, { url: ... });
async function onNavigation(details) {
const { url, tabId } = details;
chrome.tabs.sendMessage(tabId, { id: 'my-msg-id', url });
}
Then, based on the url, the content script injects a certain div to the current page:
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
if (message.id === 'my-msg-id' && message.url === '...') {
await injectDiv();
}
...
});
My problem is that sometimes, the same div gets injected to the DOM twice.
This looks like a race condition that happens:
When the page loads slowly.
When the user navigates quickly between urls or goes back and forth between pages.
What I've tried:
Wrapping the injection logic with a guard like this:
async function injectDiv() {
if (document.getElementById('my-div-id')) {
return;
}
const myDiv = document.createElement('div');
myDiv.id = 'my-div-id';
... // Some async code that waits until the container is loaded to the DOM using setInterval
const container = document.querySelector('div.container');
container.appendChild(myDiv);
}
Using throttle to limit the url triggers sent from the background script to the content script:
const throttledNav = throttle(onNavigation, 2000, { leading: true, trailing: true });
chrome.webNavigation.onHistoryStateUpdated.addListener(throttledNav, { url: ... });
It looks like it helped with most cases but I still get a double (or even a triple) duplicate DOM injection of my div from time to time, which makes my extension look very buggy.
What can I do to fix this?
Related
I am trying to inject content script on context menu click in an extension manifest version 3. I need to check if it is already injected or not. If it is not injected , inject the content script. This condition has to be satisfied. Can anyone help me with this?
We can use
ALREADY_INJECTED_FLAG
but this can be checked only in the content script, so this approach will not work as expected.
payload.js(content script)
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)
chrome.storage.local.set({ list_url: list_url, list_ip: list_ip, list_hash: list_hash });
}
chrome.runtime.sendMessage( extract());
background.js
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);
});
// Listen to messages from the payload.js script and create output.
chrome.runtime.onMessage.addListener(async (message) => {
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.length>0 || urls.length>0 || ips.length>0 ){
chrome.windows.create({url: "output.html", type: "popup", height:1000, width:1000});
}
});
}
on my first context menu click I get the output html once. Second time
I click, I get the output html twice likewise.
This behavior is caused by a combination of two factors.
First factor
You're calling chrome.runtime.onMessage.addListener() inside genericOnClick(). So every time the user clicks the context menu item, the code adds a new onMessage listener. That wouldn't be a problem if you passed a named function to chrome.runtime.onMessage.addListener(), because a named function can only be registered once for an event.
function on_message(message, sender, sendResponse) {
console.log("bg.on_message");
sendResponse("from bg");
}
chrome.runtime.onMessage.addListener(on_message);
Second factor
But you're not registering a named function as the onMessage handler. You're registering an anonymous function. Every click on the context menu item creates and registers a new anonymous function. So after the Nth click on the context menu item, there will be N different onMessage handlers, and each one will open a new window.
Solution
Define the onMessage handler as a named function, as shown above.
Call chrome.runtime.onMessage.addListener() outside of a function.
You don't have to do both 1 and 2. Doing either will solve your problem. But I recommend doing both, because it's cleaner.
I need the js script to stop and wait until a certain element appears on the page.
Can this be done with promises?
The fact is that the portal on which the script is used dynamically loads pages without going to the URL, and the necessary part of the script is to be executed on a specific page
Instead of stoping execution, you can move the code into into its own function and execute only after the element appears in DOM.
var check = setInterval("checkElem", 200);
function checkClear() {
clearInterval(check);
}
function checkElem() {
const el1 = document.getElementById("element1");
if (el1 !== null) {
console.log("executing");
yourFunction();
checkClear();
} else {
console.log("element does NOT exist");
}
}
function yourFunction() {
//your executable code
return "some-value";
}
I am developing an addin for Word 2016/365 to grab Word templates and to be able via contentcontrols alter information in header and footer. To begin with there will be only two templates to work with. I'm using ooxml and getting the xmldocument via XMLHttpRequest:
//Function to get ooxmldocument via XMLHttpRequest
function getTemplate(fileName) {
var myOOXMLRequest = new XMLHttpRequest();
var myXML;
myOOXMLRequest.open('GET', fileName, false);
myOOXMLRequest.send();
if (myOOXMLRequest.status === 200) {
myXML = myOOXMLRequest.responseText;
console.log('myXML VariabelData: ', myXML);
return myXML;
}
return "" ;
}
I insert the template via this:
// Insert a 'default' template with logo and contentControllers in header and footer of the xml
function insertDefaultTemplate() {
Word.run(function (context) {
var body = context.document.body;
// Synchronize the document state by executing the queued commands, and return a promise to indicate task completion.
return context.sync().then(function () {
var t = getTemplate('/Content/Templates/BrevmallMD.xml', 'Replace');
body.insertText("Formaterar dokument...", Word.InsertLocation.start);
return context.sync().then(function () {
body.insertOoxml(t, Word.InsertLocation.replace);
return context.sync().then(function () {
showNotification("Standardmallen är införd!", "Välj användaruppgifter.");
});
});
})
}).catch(function (error) {
errorHandler();
})
}
With some other functions I get to insert new info to contentcontrollers (that are in the document from start) via buttons and jQuery.
The problem is when you want to insert the document again. It doesnt load the original document as it is. it seem to get merged in the existing one precent in the addin.
I want to be able to load a new document as it is, not load a document and then it get merged with the existing one. I have seen a lot of example on office devcenter regardin putting in stuff in body and in sections but not to the whole document. How do I do that =) ?
I would like to ask your opinion. I'm building an ajax webpage. My links makes a GET of the URL they link to, pick the div.content and change the content of the actual div.content.
This GET action retrieves HTML code with some code in it. It looks to execute propertly but only when I am not comming from an specific link. I don't see any sense.
I don't know which code may be useful to post here to see the effect, I apolozise if I am pasting to much or too less code.
I have these two function to manage the loading of new script resources in the main layout:
loadScript: function (scriptUrl, callback) {
if (jsArray[scriptUrl]) {
console.log("loadScript already loaded " + scriptUrl);
callback && callback();
} else {
jsArray[scriptUrl] = true;
console.log("loadScript " + scriptUrl);
var body = document.getElementsByTagName("body")[0];
var script = document.createElement('script');
script.type = "text/javascript";
script.src = scriptUrl;
script.onload = callback;
body.appendChild(script); //or something of the likes
}
},
loadScripts: function (scriptsUrl, callback) {
console.log("loadScripts");
if (scriptsUrl.length === 1) {
this.loadScript(scriptsUrl[0], callback);
} else {
var scriptUrl = scriptsUrl[0];
scriptsUrl.shift();
this.loadScript(scriptUrl, function () {
Main.loadScripts(scriptsUrl, callback)
});
}
}
};
All my link with async class are binded to this function:
var loadAsyncUrl = function (url) {
if (main.currentPage === url) {}
main.currentPage = url;
$("div.container .page-content").hide();
$("div.container .loading-link").show();
$.get(url, function (data) {
$("div.container #page-header").html($(data).find("div.container #page-header").html());
$("div.container .breadcrumb").html($(data).find("div.container .breadcrumb").html());
$("div.container .content").html($(data).find("div.container .content").html());
$("div.container .loading-link").hide();
$("div.container .page-content").show();
}, 'html')
.fail(function (e) {
alert("ERROR 404");
console.log(e);
});
};
If I go from the page A to any page (even the page A itself) the loadScripts call that there is at the bottom of div.content is not called. On the other hand, if I go from page B to any page, even A again, the code is executed correctly.
The page A, actually, has got HTML code a bit heavier than the other pages, with all the CSS rules, etc. that consume probably more time to render. May it be the reason? How do you explain if I load again page A coming from page A it is loading propertly?
How would you manage this? I would like that the links point to a complete webpage and not just the partial html I want to load. I want this because if the user decides to open in a new tab, they have the entire section.
As I can see from your code I suspect that maybe in your page A some of this selectors is missing
$("div.container #page-header")
$("div.container .breadcrumb")
$("div.container .content")
try checking the length property of your objects
if ($("selector").length > 0){
//replace html
}
Dammed! Three days looking for it and it was in front of me.
I had more than div.content inside div.container in my page A.
Changed for unique id and working.
sorry
I am trying to display a 'mask' on my client while a file is dynamically generated server side. Seems like the recommend work around for this (since its not ajax) is to use an iframe and listen from the onload or done event to determine when the file has actually shipped to the client from the server.
here is my angular code:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
e.load(function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
});
angular.element('body').append(e);
This works great in Firefox but no luck in Chrome. I have also tried to use the onload function:
e.onload = function() { //unmask here }
But I did not have any luck there either.
Ideas?
Unfortunately it is not possible to use an iframe's onload event in Chrome if the content is an attachment. This answer may provide you with an idea of how you can work around it.
I hate this, but I couldn't find any other way than checking whether it is still loading or not except by checking at intervals.
var timer = setInterval(function () {
iframe = document.getElementById('iframedownload');
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Check if loading is complete
if (iframeDoc.readyState == 'complete' || iframeDoc.readyState == 'interactive') {
loadingOff();
clearInterval(timer);
return;
}
}, 4000);
You can do it in another way:
In the main document:
function iframeLoaded() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
}
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element('body').append(e);
In the iframe document (this is, inside the html of the page referenced by url)
window.onload = function() {
parent.iframeLoaded();
}
This will work if the main page, and the page inside the iframe are in the same domain.
Actually, you can access the parent through:
window.parent
parent
//and, if the parent is the top-level document, and not inside another frame
top
window.top
It's safer to use window.parent since the variables parent and top could be overwritten (usually not intended).
you have to consider 2 points:
1- first of all, if your url has different domain name, it is not possible to do this except when you have access to the other domain to add the Access-Control-Allow-Origin: * header, to fix this go to this link.
2- but if it has the same domain or you have added Access-Control-Allow-Origin: * to the headers of your domain, you can do what you want like this:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element(document.body).append(e);
e[0].contentWindow.onload = function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
};
I have done this in all kinds of browsers.
I had problems with the iframe taking too long to load. The iframe registered as loaded while the request wasn't handled. I came up with the following solution:
JS
Function:
function iframeReloaded(iframe, callback) {
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframeReloaded(iframe[0], function () {
console.log('Reloaded');
})
JQuery
Function:
$.fn.iframeReloaded = function (callback) {
if (!this.is('iframe')) {
throw new Error('The element is not an iFrame, please provide the correct element');
}
let iframe = this[0];
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframe.iframeReloaded(function () {
console.log('Reloaded');
})
I've just noticed that Chrome is not always firing the load event for the main page so this could have an effect on iframes too as they are basically treated the same way.
Use Dev Tools or the Performance api to check if the load event is being fired at all.
I just checked http://ee.co.uk/ and if you open the console and enter window.performance.timing you'll find the entries for domComplete, loadEventStart and loadEventEnd are 0 - at least at this current time:)
Looks like there is a problem with Chrome here - I've checked it on 2 PCs using the latest version 31.0.1650.63.
Update: checked ee again and load event fired but not on subsequent reloads so this is intermittent and may possibly be related to loading errors on their site. But the load event should fire whatever.
This problem has occurred on 5 or 6 sites for me now in the last day since I noticed my own site monitoring occasionally failed. Only just pinpointed the cause to this. I need some beauty sleep then I'll investigate further when I'm more awake.