Background
I'm developing a bookmarklet which has dependencies on 3rd party libraries to be loaded into the DOM before my bookmarklet script can run. The heart of the problem I'm having is this dependency chain that I need to wait for before I can react, and I'm having issues getting it to fly in IE.
I cannot be sure JQuery is even on the page at this stage. It's something I check, and if it isn't there, then I have to create and inject this into the DOM.
The flow of control is something like this:
Load main library (jquery) into the DOM, I've attached an event listener to pick up on when it's actually in the DOM.
var e = document.createElement('script');
e.type = 'text/javascript';
e.src = http:// ... jquerylibrary.js
e.addEventListener('load', callback);
Setup the callback function
var callback = function() {
//my stuff that wants to use jquery
}
This works, and it works well. I can chain jquery, some jquery plugins, then my code, and then I can be sure when my code runs (in real browser land) that it'll run.
The Crux of the problem
When IE > 9 (Which we must support) has a similar function ... attachEvent('onload',callback);
Thing that's similar, but doesn't appear to fire correctly when DOM elements are updated. Looking through MSDN's long list of events, it's not clear if any of those events are what I'm looking for.
What event does IE 7+ fire when the DOM is updated?
I've figured it out after reading some of the source of head.js. This calls for a bit of creative javascript magic.
The events we're interested in are onreadystatechange, and onload. You can attach a custom function to these to trigger update notifications when they insert into the page.
I've abstracted this into a function, where, if you pass it an element and a callback statement, it'll dump the element at the end of the page body and run the callback.
function loadElement(e, callback) {
if ( typeof(callback) === 'function' ) {
e.onreadystatechange = e.onload = function() {
var state = e.readyState;
if ( !callback.done && (!state || /loaded|complete/.test(state)) ) {
callback.done = true;
callback();
}
}
}
document.body.appendChild(e);
}
Though I've posted this answer to my problem, I'm interested in alternative approaches to this :).
Related
I am working on a javascript that sequentially loads a list of other external javascript.
The code I have so far:
function loadJavascript(url){
var js = document.createElement("script");
js.setAttribute("type", "text/javascript");
js.setAttribute("src", url);
if(typeof js!="undefined"){
document.getElementsByTagName("head")[0].appendChild(js)
}
}
loadJavascript("Jquery.js");
loadJavascript("second.js");
loadJavascript("third.js");
The problem I ran into is that sometimes the other js files loads before the Jquery file completes its loading. This gives me some errors.
Is it possible to make it so that the next JS file is only initiated when the previous file is finished loading.
Thanks in advance
Sure there is, but there's entire libraries written around doing this. Stop reinventing the wheel and use something that already works. Try out yepnope.js or if you're using Modernizr it's already available as Modernizr.load
loadJavascript("Jquery.js");
$(function(){
$.getScript('second.js', function(data, textStatus){
$.getScript('third.js', function(data, textStatus){
console.log("loaded");
});
});
}
Also, consider using the Google or Microsoft CDN for the jQuery, it will save you bandwidth and hopefully your visitors will already have it cached.
Actually, it's not necessary to load jquery within a js function. But if you insist, you can callback to make sure other js loaded after jquery.
Still, I recommend you load jquery just before </body> then use $.getScript to load other .js
You could do a check to see if jQuery is loaded, not the best way to do it, but if you really have to wait until jQuery is loaded before loading the other scripts, this is how I would do it, by checking for $ :
loadJavascript("Jquery.js");
T=0;
CheckIfLoaded();
function CheckIfLoaded() {
if (typeof $ == 'undefined') {
if (T <= 3000) {
alert("jQuery not loaded within 3 sec");
} else {
T=T+200;
setTimeout(CheckIfLoaded, 200);
} else {
loadJavascript("second.js");
loadJavascript("third.js");
}
}
In technical terms: Browsers have a funny way of deciding I which order to execute/eval dynamically loaded JS, so after suffering the same pain and checking a lot of posts, libraries, plugins, etc. I came up with this solution, self contained, small, no jquery needed, IE friendly, etc. The code is extensively commented:
lazyLoader = {
load: function (scripts) {
// The queue for the scripts to be loaded
lazyLoader.queue = scripts;
lazyLoader.pendingScripts = [];
// There will always be a script in the document, at least this very same script...
// ...this script will be used to identify available properties, thus assess correct way to proceed
var firstScript = document.scripts[0];
// We will loop thru the scripts on the queue
for (i = 0; i < lazyLoader.queue.length; ++i) {
// Evaluates if the async property is used by the browser
if ('async' in firstScript ) {
// Since src has to be defined after onreadystate change for IE, we organize all "element" steps together...
var element = document.createElement("script");
element.type = "text/javascript"
//... two more line of code than necessary but we add order and clarity
// Define async as false, thus the scripts order will be respected
element.async = false;
element.src = lazyLoader.queue[i];
document.head.appendChild(element);
}
// Somebody who hates developers invented IE, so we deal with it as follows:
// ... In IE<11 script objects (and other objects) have a property called readyState...
// ... check the script object has said property (readyState) ...
// ... if true, Bingo! We have and IE!
else if (firstScript.readyState) {
// How it works: IE will load the script even if not injected to the DOM...
// ... we create an event listener, we then inject the scripts in sequential order
// Create an script element
var element = document.createElement("script");
element.type = "text/javascript"
// Add the scripts from the queue to the pending list in order
lazyLoader.pendingScripts.push(element)
// Set an event listener for the script element
element.onreadystatechange = function() {
var pending;
// When the next script on the pending list has loaded proceed
if (lazyLoader.pendingScripts[0].readyState == "loaded" || lazyLoader.pendingScripts[0].readyState == "complete" ) {
// Remove the script we just loaded from the pending list
pending = lazyLoader.pendingScripts.shift()
// Clear the listener
element.onreadystatechange = null;
// Inject the script to the DOM, we don't use appendChild as it might break on IE
firstScript.parentNode.insertBefore(pending, firstScript);
}
}
// Once we have set the listener we set the script object's src
element.src = lazyLoader.queue[i];
}
}
}
}
Of course you can also use the minified version:
smallLoader={load:function(d){smallLoader.b=d;smallLoader.a=[];var b=document.scripts[0];for(i=0;i<smallLoader.b.length;++i)if("async"in b){var a=document.createElement("script");a.type="text/javascript";a.async=!1;a.src=smallLoader.b[i];document.head.appendChild(a)}else b.readyState&&(a=document.createElement("script"),a.type="text/javascript",smallLoader.a.push(a),a.onreadystatechange=function(){var c;if("loaded"==smallLoader.a[0].readyState||"complete"==smallLoader.a[0].readyState)c=smallLoader.a.shift(),
a.onreadystatechange=null,b.parentNode.insertBefore(c,b)},a.src=smallLoader.b[i])}};
I'm trying to learn to create extensions with Firefox but I'm running into some issues and I hope someone more experienced could give me some tips.
The idea was doing a dummy extension that would access the DOM once the page is loaded, so I hook onto DOMContentLoaded to later iterate over certain element class getElementsByClassName. The problem I noticed is that I get a zero length array as response. I assume this is due to the fact that the page uses asynchronous scripts to load some parts of the content, do when the event is triggered the content is not yet complete.
I found this interesting thread of someone running into a very similar problem: Interacting with DOM for AJAX webpages? and tried to test the answer proposed by someone. Unfortunately, when I run my script I get the following error: "getattribute is not a function"
Just for clarity, I'm using the same snippet from that post (which uses twitter.com for test)
window.addEventListener("DOMNodeInserted",
function(event){
var streamItem = event.target;
if (streamItem == null)
return;
if (streamItem.getAttribute('class') != 'stream-item')
return;
var tweet = streamItem.getElementsByClassName("tweet",streamItem)[0];
var name = tweet.getAttribute("data-screen-name");
if (name.toLowerCase() == 'some-username'.toLowerCase())
{
var icon = tweet.getElementsByTagName("img")[0];
icon.style.display='none';
}
},
false);
except that I'm using gBrowser.addEventListener() instead, which is called whenever I get a callback from window.addEventListener("load") in my extension.
Any idea how to solve this issue? I'm currently using exactly the above script just for testing purposes, as it's an identical case for what I'm trying to achieve.
Felix Kling is correct, you should register your event handler on the content document - right now you are listening for nodes that are being added to the browser (most likely new tabs). Of course that's only possible when the content document is available, e.g. in the DOMContentLoaded event. This also has the advantage that you only slow down the documents that you really want to look at (having a DOMNodeInserted event listener in a document slows down DOM modifications quite a lot). Something like this (untested but should work):
gBrowser.addEventListener("DOMContentLoaded", function(event)
{
var contentDoc = event.target; // That's the document that just loaded
// Check document URL, only add a listener to the document we want
if (contentDoc.URL.indexOf("http://example.com/") != 0)
return;
contentDoc.addEventListener("DOMNodeInserted", function(event)
{
var streamItem = event.target;
if (streamItem == null || streamItem.nodeType != Node.ELEMENT_NODE)
return;
...
});
}, false);
Note the additional check of the node type - there are also text nodes being inserted for example and those don't have the getAttribute method (which is probably causing your error). You only want to look at the element nodes.
I have a page that loads another window on button click. The loaded page has silverlight control on it, so it takes some time to load and get prepared before it can receive javascript calls.
What I need to do is to call a particular method of silverlight object right after the silverlight plugin gets loaded and is ready to interact with me.
Now, if the pop-up page was already opened then the code would be like that:
var slWin = window.open('PopupPage.html', 'WindowName');
var elem = slWin.document.getElementById('slControl');
elem.Content.SlObject.MethodA();
This works when the window is already opened because the control is already loaded and ready. I need to modify this code to handle the situation when the elem need some time to be prepared.
I tried to use jQuery's ready and load methods to add handlers to corresponding events, but with no particular lack. Here's the full snippet:
var slWin = window.open('', 'WindowName');
var elem = slWin.document.getElementById('slControl');
if (elem == null) {
slWin.location.href = 'PopupPage.aspx';
// this branch doesn't work
$(slWin).load(function () {
elem = slWin.document.getElementById('slControl');
elem.Content.SlObject.MethodA();
});
}
else {
// this branch works fine
elem.Content.SlObject.MethodA();
}
How do I solve this issue? I don't mind jQuery solutions.
This error is happening because the Silverlight object is not fully loaded when you are trying to access it.
Try to use the "onload" event of the silverlight object to dectect when it's ready to use. Here you have a link to the MSDN documentation:
http://msdn.microsoft.com/en-us/library/cc838107(v=vs.95).aspx
Hope it helps. :)
I want to use javascript in the url bar to manipulate the rendered html of a given page. Please note that I'm not trying to do something illegal here. Long story short, my university generates a weekly schedule based on your courses. I'd like to use javascript to add a button on the generated schedule page that will allow you to push the schedule to a google calendar. Unfortunately, I can't just go and edit the source itself (obviously), so I figured I would use javascript to edit the page once it has been rendered by my browser. I'm having some trouble calling an external javascript file to parse the rendered html.
As it is, this is what I have:
javascript:{{var e=document.createElement('script');
e.src = http://www.url.of/external/js/file.js';
e.type='text/javascript';
document.getElementsByTagName('head')[0].appendChild(e);}
functionToCall(document.body.innerHTML);}
Which, when pasted into the URL bar, SHOULD add my javascript file to the head and then call my function.
Any help would be greatly appreciated, thanks!
EDIT: Here's a working example if you're interested, thanks everyone!
javascript:(function(){var e=document.createElement('script');
e.src = 'http://www.somewebsite.net/file.js';
e.type='text/javascript';e.onload =function(){functiontocall();};
document.getElementsByTagName('head')[0].appendChild(e);})();
If you need the code to execute once it is loaded you can do one of two things:
Execute functionToCall(document.body.innerHTML); at the bottom of your script (http://www.url.of/external/js/file.js) rather than at the end of your bookmarklet.
Use e.onload = function(){ functionToCall(document.body.innerHTML); }; after e.type='text/javascript' near the end of your JavaScript snippet / bookmarklet, rather than calling functionToCall right after appending e to the document head (since e will most likely not have been loaded and parsed right after appendChild(e) is called.
I see that you've accepted an answer, and that's perfectly valid and great, but I would like to provide a useful tool I made for myself.
It's a bookmarklet generator called zbooks.
(Yes it's my website, no I'm not trying to spam you, there are no ads on that page, I gain nothing from you using it)
It's jQuery enabled and I think it's simple to use (but I built it, so who knows). If you need an extensive explanation of how to use it, let me know so I can make it better. You can even browse over the source if you'd like.
The important part is the business logic that gets jQuery on the page:
//s used for the Script element
var s = document.createElement('script');
//r used for the Ready state
var r = false;
//set the script to the latest version of jQuery
s.setAttribute('src', 'http://code.jquery.com/jquery-latest.min.js');
//set the load/readystate events
s.onload = s.onreadystatechange = function()
{
/**
* LOAD/READYSTATE LOGIC
* execute if the script hasn't been ready yet and:
* - the ready state isn't set
* - the ready state is complete
* - note: readyState == 'loaded' executes before the script gets called so
* we skip this event because it wouldn't have loaded the init event yet.
*/
if ( !r && (!this.readyState || this.readyState == 'complete' ) )
{
//set the ready flag to true to keep the event from initializing again
r = true;
//prevent jQuery conflicts by placing jQuery in the zbooks object
window.zbooks = {'jQuery':jQuery.noConflict()};
//make a new zbook
window.zbooks[n] = new zbooks(c);
}
};
//append the jQuery script to the body
b.appendChild(s);
Can't you use a proper tool like Greasemonkey?
I have a Javascript plugin that searches the DOM for any elements starting with the class name "tracking" and adds a click event listener (or another type of listener, if specified) to that element. The idea is that every time that event occurs on that element, that it runs a Javascript function that sends data to our traffic servers. Here's what the code looks like:
// Once the page is completed loaded
window.mmload(function() {
// Get the container object
obj = document.getElementById(name);
if ( obj.length < 0 )
throw ("The Id passed into the tracker does not exist ("+name+")");
// Find all the elements belonging to the tracking class
var trackingClass = new RegExp( /tracking\[[a-zA-Z0-9\.\-_]+\]/g );
var myElements = getElementsByRegex( trackingClass, obj );
//For each of those elements...
for( var i in myElements ) {
var elm = myElements[i];
var method = elm.className.match( /tracking\[[a-zA-Z0-9\.\-_]+\]/ )[0].split('[')[1].replace(']','').split('.')[2];
method = typeof( method ) == 'undefined' ? 'click' : method;
// Add a click event listener
myElements[i].addEventListener( method, function(e){
// Get the element, the link (if any), and the args of the event
var link = elm.getAttribute('href') == null ? "" : elm.getAttribute('href');
var args = elm.className.match( /tracking\[[a-zA-Z0-9\.\-_]+\]/ )[0].split('[')[1].replace(']','').split('.');
// If a link existed, pause it, for now
if ( link != '' )
e.preventDefault();
// Track the event
eventTracker( args[0], args[1], ( method == 'click' ? 'redirect' : 'default' ), link );
return false;
}, true);
}
});
Right now I've got this chuck of code running once the window has completely loaded (window.mmload() is a function I made for appending window.onload events). However, there maybe times when I need to run this function again because I added new elements to the DOM via Javascript with this class name and I want to track them too.
My initial solution was to run this function using setInterval to check the DOM every few milliseconds or second or whatever makes the most sense. However, I was worried if I took this approach that it might slow down the website, especially since this is running on a mobile website for smartphones. I'm not sure what kind of a performance hit I might take if I'm searching to DOM every so often.
The other approach I had in mind was to simply call the function after adding traceable elements to the DOM. This is probably the most efficient way of handling it. However, the people that I'm working with, granted very smart individuals, are Web Designers who don't often think about nor understand very well code. So the simpler I can make this, the better. That's why I liked the setInterval approach because nothing additional would be required of them. But if it noticeably slows down the site, I might have to take the other approach.
You should consider even delegation.
You just add one event listener to the document root and check the class of the element the event originated from (event.target). If you want to include also clicks from descendants, you'd have to traverse the DOM up form the target and check whether any of the ancestors contains the class.
I see two main advantages:
It works for newly generated elements without any extra steps (so the other developers don't have to do anything special).
It adds only one event handler instead of potentially many, which saves memory.
Disadvantages:
If other event handlers are registered along the path and they prevent the event from bubbling up, you cannot register this event.
A bit more information:
An event handler gets an event object as first argument. This object has several properties, among others, which element the event originated form.
E.g. to get the target element:
var element = event.target || event.srcElement;
This will be a DOM element and you can access the classes via element.className.
So your event listener could look like this (note that IE uses another method to attach event listeners and the event object is not passed but available via window.event):
function handler(event) {
event = event || window.event;
var target = event.target || event.srcElement;
if(target.className.match(/tracking\[[a-zA-Z0-9\.\-_]+\]/g) {
// do your stuff
}
}
if(document.addEventListener) {
document.addEventListener('click', handler, false);
}
else {
document.attachEvent('onclick', handler);
}
But as I said, this would miss events that are prevented from bubbling up. At least in the browsers following the W3C model (so not IE), you can handle the events in the capture phase by setting the last parameter to true:
document.addEventListener('click', handler, true);
If you can live without IE, then there is a change event which you can hook into for the window/document/dom element. Simply hook into the event at the document level, and it'd fire anytime something's changed in the page (stuff inserted, deleted, changed). I believe the event's context contains what got changed, so it should be fairly trivial to find any new trackable elements and attach your spy code to it.
A third option would be to write a method for manipulating the innerHTML of an element. At the end of that method simply call your function that refreshes everything.
example:
var setHtml = function(element, newHtml){
element.innerhtml = newHtml;
yourRefreshFunction();
}
So obviously this requires that you have your web developers user this method to update the dom. And you'll have to do it for anything that is more complicated than simple html edits. But that gives you the idea.
Hope that helps!