Wait until js script is loaded on the page - javascript

I'm trying to add js file to the page. How to check if that file is loaded?
javascript ga.js file:
this.event = function (value) {
this.value = value;
this.method = function() {
return "sometext";
};
};
Dart code:
ScriptElement ga = new ScriptElement()
..src = "/ga.js"
..async = true;
querySelector('body').append(ga);
bool exist = context.hasProperty('event');
JsObject event = null;
if (exist) {
print("event exist");
} else {
print("there is no event yet");
}

You can just add an onload event to any script element in HTML.
ga.onload = function(ev) { alert('loaded!') };
Since this is native HTMLScriptElement behaviour it should combine fine with Dart.

The way I found for now it's to use Future.
void checkEvent() {
bool exist = context.hasProperty('event');
JsObject event = null;
if (exist) {
print("event exist");
} else {
print("there is no event yet");
var future = new Future.delayed(const Duration(milliseconds: 10), checkEvent);
}
}
But it's if I now already that 'event' exist in that javascript file. Maybe there is some way to check when javascript loaded in html?

Most browsers with Developer tools like Chrome, Firefox, and even IE have a tab called Network. In these panels/tabs you can search for the file in question. If it has loaded it will be in that view.
As long as the JavaScript recognizes/attempts to load the .js file, the console will usually tell you if it couldn't load the file.
If you're planning on doing something on your page after the file has loaded then you would want to do something more along the lines of what Niels Keurentjes suggested.
More Information:
Chrome's Network Panel
Firefox's Network Panel
Internet Explorer's Network Panel
OnLoad Event

Related

Holy grail for determining whether or not local iframe has loaded

Firstly, I see this question asked a few times but no answers seem satisfactory. What I am looking for is to be able to call a script at anytime and determine whether or not an iframe has loaded - and to not limit the script to require being added to the iframe tag itself in an onload property.
Here's some background: I have been working on an unobtrusive script to try and determine whether or not local iframes in the dom have loaded, this is because one of our clients includes forms on their website in iframes and many of them open in lightboxes - which dynamically add the iframes into the dom at any time. I can attach to the open event of the lightbox, but its hit or miss as to whether I can "catch" the iframe before it has loaded.
Let me explain a little more.
In my testing I've determined that the onload event will only fire once - and only if it is bound before the iframe actually loads. For example: This page should only alert "added to iframe tag" and the listener that is attached afterward does not fire - to me that makes sense. (I'm using the iframe onload property for simple example).
https://jsfiddle.net/g1bkd3u1/2/
<script>
function loaded () {
alert ("added to iframe tag");
$("#test").load(function(){
alert("added after load finished");
});
};
</script>
<iframe onload="loaded()" id="test" src="https://en.wikipedia.org/wiki/HTML_element#Frames"></iframe>
My next approach was to check the document ready state of the iframe which seems to work in almost all of my testing except chrome which reports "complete" - I was expecting "Access Denied" for cross domain request. I'm ok with a cross domain error because I can disregard the iframe since I am only interested in local iframes - firefox reports "unintialized" which I'm ok with because I know I can then attach an onload event.
Please open in Chrome:
https://jsfiddle.net/g1bkd3u1/
<iframe id="test" src="https://en.wikipedia.org/wiki/HTML_element#Frames"></iframe>
<script>
alert($("#test").contents()[0].readyState);
</script>
I've found that if I wait just 100ms - then the iframe seems to report as expected (a cross domain security exception - which is what I want - but I don't want to have to wait an arbitrary length).
https://jsfiddle.net/g1bkd3u1/4/
<iframe id="test" src="https://en.wikipedia.org/wiki/HTML_element#Frames"></iframe>
<script>
setTimeout(function () {
try {
alert($("#test").contents()[0].readyState);
} catch (ignore) {
alert("cross domain request");
}
}, 100);
</script>
My current workaround / solution is to add the onload event handler, then detach the iframe from the dom, then insert it back into the dom in the same place - now the onload event will trigger. Here's an example that waits 3 seconds (hoping thats enough time for the iframe to load) to show that detaching and re-attaching causes the iframe onload event to fire.
https://jsfiddle.net/g1bkd3u1/5/
<iframe id="test" src="https://en.wikipedia.org/wiki/HTML_element#Frames"></iframe>
<script>
setTimeout(function(){
var placeholder = $("<span>");
$("#test").load(function(){
alert("I know the frame has loaded now");
}).after(placeholder).detach().insertAfter(placeholder);
placeholder.detach();
}, 3000);
</script>
While this works it leaves me wondering if there are better more elegant techniques for checking iframe load (unobtrusively)?
Thank you for your time.
Today I actually ran into a bug where my removing and re-inserting of iframes was breaking a wysiwyg editor on a website. So I created the start of a small jQuery plugin to check for iframe readiness. It is not production ready and I have not tested it much, but it should provide a nicer alternative to detaching and re-attaching an iframe - it does use polling if it needs to, but should remove the setInterval when the iframe is ready.
It can be used like:
$("iframe").iready(function() { ... });
https://jsfiddle.net/q0smjkh5/10/
<script>
(function($, document, undefined) {
$.fn["iready"] = function(callback) {
var ifr = this.filter("iframe"),
arg = arguments,
src = this,
clc = null, // collection
lng = 50, // length of time to wait between intervals
ivl = -1, // interval id
chk = function(ifr) {
try {
var cnt = ifr.contents(),
doc = cnt[0],
src = ifr.attr("src"),
url = doc.URL;
switch (doc.readyState) {
case "complete":
if (!src || src === "about:blank") {
// we don't care about empty iframes
ifr.data("ready", "true");
} else if (!url || url === "about:blank") {
// empty document still needs loaded
ifr.data("ready", undefined);
} else {
// not an empty iframe and not an empty src
// should be loaded
ifr.data("ready", true);
}
break;
case "interactive":
ifr.data("ready", "true");
break;
case "loading":
default:
// still loading
break;
}
} catch (ignore) {
// as far as we're concerned the iframe is ready
// since we won't be able to access it cross domain
ifr.data("ready", "true");
}
return ifr.data("ready") === "true";
};
if (ifr.length) {
ifr.each(function() {
if (!$(this).data("ready")) {
// add to collection
clc = (clc) ? clc.add($(this)) : $(this);
}
});
if (clc) {
ivl = setInterval(function() {
var rd = true;
clc.each(function() {
if (!$(this).data("ready")) {
if (!chk($(this))) {
rd = false;
}
}
});
if (rd) {
clearInterval(ivl);
clc = null;
callback.apply(src, arg);
}
}, lng);
} else {
clc = null;
callback.apply(src, arg);
}
} else {
clc = null;
callback.apply(this, arguments);
}
return this;
};
}(jQuery, document));
</script>
The example waits until the window has loaded to dynamically add an iframe to the DOM, it then alerts its document's readyState - which in chrome displays "complete", incorrectly. The iready function should be called after and an attempt to output the document's readyState proves cross domain exception - again this has not been thoroughly tested but works for what I need.
I encountered a similar issue in that I had an iframe and needed to modify its' document once it had finished loading.
IF you know or can control the content of the loaded document in the iFrame, then you could simply check for/add an element that you could check the existence of in order to then update the iframe document.
At least then you know the elements you want to work with are loaded in to the document.
In my case, I called a function, which itself checked for the existence of my known element that would always be found after the elements I needed to update had already been loaded - in the case it was not found, it called itself again through setTimeout().
function updateIframeContents() {
if ($("#theIframe").contents().find('.SaveButton').length > 0) {
// iframe DOM Manipulation
} else {
setTimeout(updateIframeContents, 250);
}
}
updateIframeContents();

Chrome extension: Checking if content script has been injected or not

I'm developing a Chrome extension. Instead of using manifest.json to match content script for all URLs, I lazily inject the content script by calling chrome.tabs.executeScript when user do click the extension icon.
What I'm trying is to avoid executing the script more than once. So I have following code in my content script:
if (!window.ALREADY_INJECTED_FLAG) {
window.ALREADY_INJECTED_FLAG = true
init() // <- All side effects go here
}
Question #1, is this safe enough to naively call chrome.tabs.executeScript every time the extension icon got clicked? In other words, is this idempotent?
Question #2, is there a similar method for chrome.tabs.insertCSS?
It seems impossible to check the content script inject status in the backgroud script since it can not access the DOM of web page. I've tried a ping/pong method for checking any content script instance is alive. But this introduces an overhead and complexity of designing the ping-timeout.
Question #3, any better method for background script to check the inject status of content script, so I can just prevent calling chrome.tabs.executeScript every time when user clicked the icon?
Thanks in advance!
is this safe enough to naively call chrome.tabs.executeScript every
time the extension icon got clicked? In other words, is this
idempotent?
Yes, unless your content script modifies the page's DOM AND the extension is reloaded (either by reloading it via the settings page, via an update, etc.). In this scenario, your old content script will no longer run in the extension's context, so it cannot use extension APIs, nor communicate directly with your extension.
is there a similar method for chrome.tabs.insertCSS?
No, there is no kind of inclusion guard for chrome.tabs.insertCSS. But inserting the same stylesheet again does not change the appearance of the page because all rules have the same CSS specificity, and the last stylesheet takes precedence in this case. But if the stylesheet is tightly coupled with your extension, then you can simply inject the script using executeScript, check whether it was injected for the first time, and if so, insert the stylesheet (see below for an example).
any better method for background script to check the inject status of
content script, so I can just prevent calling
chrome.tabs.executeScript every time when user clicked the icon?
You could send a message to the tab (chrome.tabs.sendMessage), and if you don't get a reply, assume that there was no content script in the tab and insert the content script.
Code sample for 2
In your popup / background script:
chrome.tabs.executeScript(tabId, {
file: 'contentscript.js',
}, function(results) {
if (chrome.runtime.lastError || !results || !results.length) {
return; // Permission error, tab closed, etc.
}
if (results[0] !== true) {
// Not already inserted before, do your thing, e.g. add your CSS:
chrome.tabs.insertCSS(tabId, { file: 'yourstylesheet.css' });
}
});
With contentScript.js you have two solutions:
Using windows directly: not recommended, cause everyone can change that variables and Is there a spec that the id of elements should be made global variable?
Using chrome.storage API: That you can share with other windows the state of the contentScript ( you can see as downside, which is not downside at all, is that you need to request permissions on the Manifest.json. But this is ok, because is the proper way to go.
Option 1: contentscript.js:
// Wrapping in a function to not leak/modify variables if the script
// was already inserted before.
(function() {
if (window.hasRun === true)
return true; // Will ultimately be passed back to executeScript
window.hasRun = true;
// rest of code ...
// No return value here, so the return value is "undefined" (without quotes).
})(); // <-- Invoke function. The return value is passed back to executeScript
Note, it's important to check window.hasRun for the value explicitly (true in the example above), otherwise it can be an auto-created global variable for a DOM element with id="hasRun" attribute, see Is there a spec that the id of elements should be made global variable?
Option 2: contentscript.js (using chrome.storage.sync you could use chrome.storage.local as well)
// Wrapping in a function to not leak/modify variables if the script
// was already inserted before.
(chrome.storage.sync.get(['hasRun'], (hasRun)=>{
const updatedHasRun = checkHasRun(hasRun); // returns boolean
chrome.storage.sync.set({'hasRun': updatedHasRun});
))()
function checkHasRun(hasRun) {
if (hasRun === true)
return true; // Will ultimately be passed back to executeScript
hasRun = true;
// rest of code ...
// No return value here, so the return value is "undefined" (without quotes).
}; // <-- Invoke function. The return value is passed back to executeScript
Rob W's option 3 worked great for me. Basically the background script pings the content script and if there's no response it will add all the necessary files. I only do this when a tab is activated to avoid complications of having to add to every single open tab in the background:
background.js
chrome.tabs.onActivated.addListener(function(activeInfo){
tabId = activeInfo.tabId
chrome.tabs.sendMessage(tabId, {text: "are_you_there_content_script?"}, function(msg) {
msg = msg || {};
if (msg.status != 'yes') {
chrome.tabs.insertCSS(tabId, {file: "css/mystyle.css"});
chrome.tabs.executeScript(tabId, {file: "js/content.js"});
}
});
});
content.js
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.text === 'are_you_there_content_script?') {
sendResponse({status: "yes"});
}
});
Just a side note to the great answer from Rob.
I've found the Chrome extension from Pocket is using a similar method. In their dynamic injected script:
if (window.thePKT_BM)
window.thePKT_BM.save();
else {
var PKT_BM_OVERLAY = function(a) {
// ... tons of code
},
$(document).ready(function() {
if (!window.thePKT_BM) {
var a = new PKT_BM;
window.thePKT_BM = a,
a.init()
}
window.thePKT_BM.save()
}
)
}
For MV3 Chrome extension, I use this code, no chrome.runtime.lastError "leaking" as well:
In Background/Extension page (Popup for example)
private async injectIfNotAsync(tabId: number) {
let injected = false;
try {
injected = await new Promise((r, rej) => {
chrome.tabs.sendMessage(tabId, { op: "confirm" }, (res: boolean) => {
const err = chrome.runtime.lastError;
if (err) {
rej(err);
}
r(res);
});
});
} catch {
injected = false;
}
if (injected) { return tabId; }
await chrome.scripting.executeScript({
target: {
tabId
},
files: ["/js/InjectScript.js"]
});
return tabId;
}
NOTE that currently in Chrome/Edge 96, chrome.tabs.sendMessage does NOT return a Promise that waits for sendResponse although the documentation says so.
In content script:
const extId = chrome.runtime.id;
class InjectionScript{
init() {
chrome.runtime.onMessage.addListener((...params) => this.onMessage(...params));
}
onMessage(msg: any, sender: ChrSender, sendRes: SendRes) {
if (sender.id != extId || !msg?.op) { return; }
switch (msg.op) {
case "confirm":
console.debug("Already injected");
return void sendRes(true);
// Other ops
default:
console.error("Unknown OP: " + msg.op);
}
}
}
new InjectionScript().init();
What it does:
When user opens the extension popup for example, attempt to ask the current tab to "confirm".
If the script isn't injected yet, no response would be found and chrome.runtime.lastError would have value, rejecting the promise.
If the script was already injected, a true response would result in the background script not performing it again.

Firefox for Android Extension: How to trigger event on every page load?

I am trying to write a Firefox extension for Android that will fire an event every time the web page changes. It is monitoring which URLs are being loaded (all URLs) and the contents of the page loaded (via DOM inspection). My problem is that the window load event using the code below only gets loaded when a tab is opened, if you navigate away from the page, no events get fired.
How do I hook into every page load event for any URL?
This code is the entire contents of bootstrap.js:
Components.utils.import("resource://gre/modules/Services.jsm");
var windowListener = {
onOpenWindow: function(aWindow) {
console.log('vipro.openWindow');
let domWindow = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowInternal || Components.interfaces.nsIDOMWindow);
domWindow.addEventListener("UIReady", function onLoad() {
domWindow.removeEventListener("UIReady", onLoad, false);
console.log('vipro.openWindow.loaded');
// ** ONLY EVER FIRED ONCE ** //
try {
var browser = Services.wm.getMostRecentWindow("navigator:browser");
} catch(e) {
console.log('vipro.openWindow.error.' + e.toString());
}
console.log('vipro.openWindow.loaded.DONE');
});
console.log('vipro.openWindow.DONE');
},
onCloseWindow: function(aWindow) {},
onWindowTitleChange: function(aWindow, aTitle) {},
};
function startup(data, reason) {
console.log('vipro.startup');
try {
let wm = Components.classes["#mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
wm.addListener(windowListener);
} catch(e) {
console.log('vipro.startup.error.' + e.toString());
}
console.log('vipro.startup.DONE');
}
function shutdown() {
console.log('vipro.shutdown');
}
function install(aData, aReason) {}
function uninstall(aData, aReason) {}
I'm not fussed that the code above only works for new tabs and not existing (I've intentionally kept it simple), it's just the fact that when I navigate away from the initial new tab page, I don't get the opportunity to hook into the other pages.
Found a slightly different approach on this page:
https://developer.mozilla.org/en-US/Add-ons/SDK/Tutorials/Mobile_development
Using the layout that cfx provides and not using the bootstrap... bootstrap i am able to achieve what i need using page-mod.

Access element in recently loaded XUL document

I'm developing a Firefox extension and need to do the following:
load a page
get an element from this page
modify the attributes from this element
The code I would like to work looks like this:
gBrowser.loadURI("chrome://myExtension/content/myPage.xul");
let button = content.document.getElementById("myExtension-theButton");
button.setAttribute("oncommand", "myFunction(withParams)");
But when I run this, button is null. (Maybe loadURI returns too early and the document isn't fully loaded, yet.)
add to that gBrowser:
gBrowser.addEventListener('DOMContentLoaded', dofunc, false);
function dofunc(e) {
var win = event.originalTarget.defaultView;
var doc = win.document;
if (doc.location == 'chrome://myExtension/content/myPage.xul') {
let button = content.document.getElementById("myExtension-theButton");
button.setAttribute("oncommand", "myFunction(withParams)");
gBrowser.removeEventListener('DOMCOntentLoaded', dofunc, false);
}
}

How to inject a JavaScript function to all web page using Firefox extension

I am developing a Firefox addon. What I want to do is to inject a custom JavaScript function.
i.e.
function foo() {..}
So all the pages can call the foo without define it first.
I have look from other answer such as: http://groups.google.com/group/greasemonkey-users/browse_thread/thread/3d82a2e7322c3fce
But it requires modification on the web page. What if perhaps I want to inject the function foo into Google.com? Is it possible to do so?
I can do it with a userscript, but I want to use the extension approach if possible.
The first thing I thought when reading your question was "this looks like a scam". What are you trying to achieve?
Anyway, here's a Jetpack (Add-on builder) add-on that injects a script in every page loaded:
main.js:
const self = require("self"),
page_mod = require("page-mod");
exports.main = function() {
page_mod.PageMod({
include: "*",
contentScriptWhen: "ready",
contentScriptFile: self.data.url("inject.js")
});
};
inject.js:
unsafeWindow.foo = function() {
alert('hi');
}
unsafeWindow.foo();
What if you make a simple href with javascript function on the page.
Like bookmarklets work.
Here is a sample code :
function(scriptUrl) {
var newScript = document.createElement('script');
// the Math.random() part is for avoiding the cache
newScript.src = scriptUrl + '?dummy=' + Math.random();
// append the new script to the dom
document.body.appendChild(newScript);
// execute your newly available function
window.foo();
}('[url of your online script]')
To use it, put your script's url.
It must be only one line of code, url formated, but for code readability I've formated it.
I've never developed a Firefox extension, but for javascript injection that's how I would roll.
Hope it helped.
You can use Sandbox
// Define DOMContentLoaded event listener in the overlay.js
document.getElementById("appcontent").addEventListener("DOMContentLoaded", function(evt) {
if (!evt.originalTarget instanceof HTMLDocument) {
return;
}
var view = evt.originalTarget.defaultView;
if (!view) {
return;
}
var sandbox = new Components.utils.Sandbox(view);
sandbox.unsafeWindow = view.window.wrappedJSObject;
sandbox.window = view.window;
sandbox.document = sandbox.window.document;
sandbox.__proto__ = sandbox.window;
// Eval your JS in the sandbox
Components.utils.evalInSandbox("function foo() {..}", sandbox);
}, false);

Categories