My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Friday, November 24, 2006

My Last DOMContentLoaded Solution

Update 28/11/2006 Thanks to anonymous for its perfect suggest!!!
Update 01/12/2006 Thanks to Stephen Elson for debug
Update 01/13/2007 This is my last version that doesn't use global window.__onContent__ variable
Update 13/02/2007
Sorry guys for my choosed old title, thanks to Pierluigi that explained me what did it mean




function onContent(f){//(C)webreflection.blogspot.com
var a=onContent,b=navigator.userAgent,d=document,w=window,c="onContent",e="addEventListener",o="opera",r="readyState",
s="");
a[c]=(function(o){return function(){a[c]=function(){};for(a=arguments.callee;!a.done;a.done=1)f(o?o():o)}})(a[c]);
if(d[e])d[e]("DOMContentLoaded",a[c],false);
if(/WebKit|Khtml/i.test(b)||(w[o]&&parseInt(w[o].version())<9))(function(){/loaded|complete/.test(d[r])?a[c]():setTimeout(arguments.callee,1)})();
else if(/MSIE/i.test(b))d.write(s);
};



Here is where this "story" began, inside a Dean Edwards post.
After that there was a big list of comments, posts and tests ... for a second post and a better solution !

Dojo adopted that solution ... then came back to old one ... but someone has never stopped to test, to try or to find a "perfect" way to add DOMContentLoaded with every browser and expecially with Internet Explorer.

Mark Wubben and Paul Sowden did it, they've created a complete and portable solution that works with http and https pages too !

Wonderful work guys and ... as first point, thank you very much !


Now, I can show my personal DOMCOntentLoaded solution, explaining them as better as I can.


The first step, the real key of this code, is the source of used script.

It's not void, it's not //0 ... it's exactly this one: //:
and is what I had not to create my tiny, simple and portable solution.

Here is the func, called onContent ...

function onContent(f){//(C)webreflection.blogspot.com
var a,b=navigator.userAgent,d=document,w=window,
c="__onContent__",e="addEventListener",o="opera",r="readyState",
s="<scr".concat("ipt defer src='//:' on",r,"change='if(this.",r,"==\"complete\"){this.parentNode.removeChild(this);",c,"()}'></scr","ipt>");
w[c]=(function(o){return function(){w[c]=function(){};for(a=arguments.callee;!a.done;a.done=1)f(o?o():o)}})(w[c]);
if(d[e])d[e]("DOMContentLoaded",w[c],false);
if(/WebKit|Khtml/i.test(b)||(w[o]&&parseInt(w[o].version())<9))
(function(){/loaded|complete/.test(d[r])?w[c]():setTimeout(arguments.callee,1)})();
else if(/MSIE/i.test(b))d.write(s);
};


... and this is the http test page, while this is the https test page.



Let me explain that unreadable function with a clear commented version :)


function onContent(callback){ // (C) webreflection.blogspot.com
// [please note that this code doesn't work]

// private scope variable

var IEStringToWrite = // this is IE dedicated string

"<script defer src='//:' onreadystatechange='
(function(element){

// if readystate is complete
if(element.readyState === "complete") {

// remove the element
element.parentNode.removeChild(element);

// call the global variable
window.__onContent__();
}
})(this);
'></script>";

// the above string is necessary to use onreadystatechange property
// with an undefined page. In this way IE tell us the readyState
// of the current document



// to call callback function IE need a global scope variable
// this variable could call one or more callback
// then if it's already created we need to call the old callback
// then this new callback
window.__onContent__ = (function(oldCallback){

// returns a function that will set every callback fired
// [thanks to Stephen Elson for its suggest]
// to remove multiple callbacks with different
// events and different ways for each browser

return function(){

// set window.__onContent__ as empty function
// required by IE5
window.__onContent__ = function(){};

// verify that callee wasn't fired before
if(!arguments.callee.done) {

// set calle.done 1 as true fired value
arguments.callee.done = 1;

// checks if oldCallback isn't null or undefined
if(oldCallback)
oldCallback(); // call them to preserve the right order

callback(); // call this scope callback function
// (sent calling onContent)
}
}

})(window.__onContent__); // undefined if is the first time we use __onContent__



// __onContent__ is my function to use as callback

// I need to add this function as event

// Opera 9 and FireFox both support DOMContentLoaded as well as
// addEventListener document method
if(document.addEventListener)
document.addEventListener("DOMContentLoaded", __onContent__, false);

// if some browser supports addEventListener but doesn't support DOMContentLoaded
// event I don't need to care about that because this event will never be fired

// at the same time if Safari or KDE one day will support DOMContentLoaded
// I prefere use this dedicated in-core
// event instead of next trick that's quite horrible but works with Safari,
// KDE as Opera 8.5 and lower too

// that's why I don't use an else if but an if ... because the first time
// event will be fired __onContent__
// became an empty function ... then calling them twice is not a problem

if(
// Safari and KDE
/WebKit|Khtml/i.test(navigator.userAgent) ||

// Opera less than 9
(window.opera && parseInt(window.opera.version())<9)
)
// runtime anonymous function
(function(){

// checks if document.readyState is loaded or complete
/loaded|complete/.test(document.readyState) ?

// then call __onContent__ , stopping internal loop
window.__onContent__() :

// or loops itself with the faster timeout
setTimeout(arguments.callee, 1);
})();

// at this point I've setted the DOMContentLoaded event for every browser
// but not for Inernet Explorer.
else if (/MSIE/i.test(navigator.userAgent))

// I can write dedicated string
document.write(IEStringToWrite);
};



My solution doesn't use conditional comments (I hate them !!!) ... and this is the compatibility list:

- FireFox 1 or greater
- Opera 8 or greater (I don't know 7)
- Safari 2 or greater (I don't know 1)
- KDE 3.4 or greater
- Internet Explorer 5 or greater (I don't know IE 5.2 for Mac)

36 comments:

Andrea Giammarchi said...

Ooops, I forgot compatibility list ;)
FireFox 1 or greater
Opera 8 or greater (I don't know 7)
Safari 2 or greater (I don't know 1)
KDE 3.4 or greater
Internet Explorer 5 or greater (I don't know IE 5.2 for Mac)

Mark said...

For reference, progress on implementing support for the DOMContentLoaded event in WebKit can be tracked in bug 5122 over at bugs.webkit.org.

Anonymous said...

I'd seriously suggest finding a better name for this than "Final Solution", a term that has extremely negative connotations.

Andrea Giammarchi said...

I've called this post My Final Solution ... and I will probably use this one for different months and more.

Do You think (who are You ?) that this title should be a problem ?

Then please suggest me a better one :)

kentaromiura said...

can you post the url where the //: hack was find ?
you post 2 link to the home, but I can't find the correct implementation.

Andrea Giammarchi said...

Dean Edwards should write a dedicated official post about this trick.

There's any link to //: hack because it was wrote inside Dojo contributors Mailing List.

Anonymous said...

Thanks for posting this. Have been having a lot of trouble with beating the bad browser into submission to make DOMContentLoad work. Very helpful.

You can save yourself the second loop of going through all of the scripts to cleanup the defered one by adding the following to your IE callback:


window.__onContent__();
element.parentNode.removeChild(element);

Andrea Giammarchi said...

hey anonymous, I'm an idiot and You're damn right !!!

Thank you very much !!!

P.S. You need to remove the element before __onContent__() or recursion doesn't work perfectly .. however I've done some test with updated code and now it seems to be perfect ;)

Stephen Elson said...

Thanks for the nice solution :-)

I think there's a small problem with the callback looping back on itself too many times in Firefox (2.0), this isn't easily seen in your demo.

To see what I mean just change the first onContent call in your demo to an alert rather than innerHTML, you should see the first alert appear twice in FF. As it was innerHTML-ing a div, the fact that this was happening twice wasn't obvious. You could also change it to append to the div to see the same problem.

A simple fix could be the old argument.callee.done trick, e.g.

w[c]=(function(o){
return function(){
if (arguments.callee.done)return;
arguments.callee.done=true;
w[c]=function(){};
f(o?o():o)
}
})(w[c]);

I think that works...

P.S if you want it to provide fallback to the standard window onload event for legacy browsers, you can add the following to the very bottom of your onContent function.

window.onload = function(){return w[c]();};

This will safely be skipped by supported browsers as w[c] has been effectively neutered by this point.

Thanks again!

Andrea Giammarchi said...

Hi Stephen.
Could You pelase show me an example of duplicated callback ?

I usually add or use only one function with every other callback inside them (I prefere, if it's possibile, one callback instead of more than one).

About the onload ... DOMContentLoaded is an event totally different and your sugest should overwrite other onload too.
However this solution should work with every recent browser and with IE 5 too, why do You need an onload alternative and why do You need them inside DOMContentLoaded ?

Andrea Giammarchi said...

Ok Stephen, I did a little change because You was right ( what a stupid error :( )

It seems to work correctly without repeated callbacks.

Thank You again for this report :)

Stephen Elson said...

Hi Andrea,

thanks for posting the update, glad to help.

I suggested the window.onload fallback because I need to support browsers such as IE4, MacIE5, Safari 1, etc.

Having a graceful fallback for these is very useful, though I can understand entirely why you might not want to mix up DOMContentLoaded handlers with onload handlers.

As for overwriting existing onload functions, that's a good point but the chained __onContent__ function allows multiple handlers, and if all such handlers are set through this function it is fairly safe. Maybe not one for the general case however :-)

Cheers,
Steve

Andrea Giammarchi said...

The problem, with unsupported browsers, should be more than one ... for example, if IE for Mac doesn't work (I can't test them) it writes a script that will never be removed.
If safari 1 doesn't work correctly (I can't test them too) it will loop infinitely.

I think that if You need absolutely death browsers such IE4, IE 5.2 for Mac, Safari 1 or these kind of old browsers, You shouldn't use onContent but only onload.
Then You'll be sure that every browser will support correctly Your script .... however, wich kind of recent script could run inside those old, deprecated browsers ?

napyfab said...

Andrea, your last revision of 01/13/2997 doesn't work.. Error on line 3: missing ) after argument list

Andrea Giammarchi said...

sorry napyfab, it's probably an highlighter problem ( I need to update some script of this blog :E )

here you can copy my last version that should work correctly :-)

Stefan Van Reeth said...
This comment has been removed by the author.
Stefan Van Reeth said...
This comment has been removed by the author.
Stefan Van Reeth said...

I posted some things before that were way behind current discussions elsewhere, so they got removed. Now that I've catched up on my reading, I still have one question.

What's wrong with src="" opposed to src="//:" for https for the IE solution? Tested it out on my server at home and the annoying popup disappeared. Seems I missed something, because there are many posts written about this here and elsewhere.

On the other hand, maybe nobody tried it because it was too obvious (altough I really can't believe that :)).

Weird thing is it works for http and https, where for example src:"javascript:false" or others did provoce the popup when using https.

Surely src="" can't be a solution that only works on my server. And no, I didn't dabble with server settings at all. I even did a second install without modifications (ApacheFriends bundle) to see if there were any differences in behaviour. Nope, nothing different off course.

Btw, src="" allows also the use of DOM methods to write the script tag, where src="//:" stopped the IE solution from working.

So what do I miss here, or did I really find another solution. Please let me know anyone?

Andrea Giammarchi said...

ehr ... Stefan, You just have the solution, so what are You looking for ?

The src need to be exactly "//:" ... no void, no false ... only that kind of source because the problem is that IE doesn't accept fake sources over https, it wants to know if source is correct or not (and a javascript source or a false/null/undefined value is not a correct source).

What is your problem ? This solution has been adopted by Dojo Toolkit too ... so I suppose it's the best solution, now :-)

Stefan Van Reeth said...

It's just that I implemented DOMContentLoaded without conditional comments and using the DOM to build the script tag. And //: without parentheses doesn't work there. So I tried to find something else that works.

I liked to know if I would run into trouble with the empty src property. Because I can't test on Mac, and switching WIndows/Linux all the time for a few tests is a pain. So therefore I asked the question.

So far I didn't run into trouble with it, but what about Safari? I just thought that a) it could be already been tried out and found to break on certain systems, b) it would be a far faster way to find out any faults.

That's why I can't use the best found solution yet, because I use a different technique ;).

Andrea Giammarchi said...

Stefan ... where can You read only one single char of conditional comment inside my solution ?

Where do You read that Safari has problems with my soultion ???

Maybe You've not see the function as well as You've wrote every comment (2 of them deleted ...)

As title: this is my final, DOMContentLoaded, solution ... bye!

Stefan Van Reeth said...

lol, i give up.

Seems that only Andrea's solutions can get commented here.

Never said anything about YOUR solution man. But wanted to know if there could be any gotcha's when implementing it otherwise with src empty. I can't read the whole net myself.

Why? Because I could use the DOM in MY implemetation. Because //: don't work there. Read it all above.

But instead of getting some answers, I get flamed for supposedly attacking Andrea's Ultimate blah blah blah. Where do you read that then? I didn't loose a word about Andrea's thing. I just asked if there were any known issues with my preposition. What's wrong with that? To top it off, I get commented for not reading well?

Well, if I can't get a serious answer, I might as well stay out. Sorry to waste my time.

Andrea Giammarchi said...

Dear Stefan ... if everyone before You has used document.write instead of DOM for Internet Explorer ... don't You think that maybe DOM is not usable to add that kind of script with IE browsers ?

I don't know what is the problem ...

You: I implemented DOMContentLoaded without conditional comments and using the DOM to build the script tag

Me: this solution doesn't use condictional comments

You: I could use the DOM in MY implemetation

Me: ... now ... why everyone want its implementation of the same, just solved, thing ... and why do You think that We are so un-skilled because we've chosed document.write instead of a simple DOM script node ?

Starting from Dean Edwards we've tested different solutions from different months ... and the problem is that IE doesn't accept that kind of script node ... then You must use document.write to create that "strange" script tag.


Sorry to waste my time ... of course, if You want to rewrite every solution and You want to re-test every problem to find a solution You're right: You waste your time.

Regards

Pierluigi said...

Update 01/13/2997 [...]

Cavolo, Andrea! Non sapevo scrivessi per Star Trek ;)))))))))

Salutissimi.
Pierluigi (da 'Edit').

Andrea Giammarchi said...

I don't understand what do You mean :E ... however, ciao Pierluigi ^_^

Pierluigi said...

Give a look at the year you wrote...
2997 !!!!

Andrea Giammarchi said...

ooops ... fixed, thank You :D

Anonymous said...

alla fine qual'e l' ultimissima versione?

Andrea Giammarchi said...

Last solution should be this one:
http://www.3site.eu/jstests/onContent/onContent.js

Bye :)

Anonymous said...

As already said in other places the document.write trick is not for all situations, for example it will fail when there are no images in the page, it will fail when the page size is bigger than the images it contains, it will fail (not break but loose it's real scope) if you use this trick in a big js library where the library is bigger than the page itself.

In all those cases this trick will miss the bus and the "onload" will fire before of it, making it obsolete.

Please review your tests.

Andrea Giammarchi said...

Please review your tests.
Please show us your test cases

Dao said...

hey, I've started a similar approach, namely whenDOMReady.

ND said...

Dear Andrea,

Can I copy this code under the terms of the MIT license?
This is the license under which Dean Edwards released his code.

Thanks for the solution.

Cheers,

ND

Andrea Giammarchi said...

Dear ND, of course you can but I think you should better view last jQuery implementation that uses the most common and tested solution for this kind of problem.

Cheers,
Andrea

Wilsont said...

Hi Andrea,

I have a very old web project which is using your code (http://www.3site.eu/jstests/onContent/final.html)

and I found there is console exception

Uncaught TypeError: Object function (){a[c]=function(){};for(a=arguments.callee;!a.done;a.done=1)f(o?o():o)} has no method 'onContent'


Any hints to fix it? Thanks!

Andrea Giammarchi said...

yes, forget whatever you are using in there and put DOM4 before any script that trusts DOMContentLoaded ;-)