Using classes to observe a page - javascript

I am writing a script that needs to detect elements added to a Web page, for example events rendered in a calendar (div tags). I don't care about elements that are removed. There should be at most 20-30 such elements on the page.
My idea - short and easy code - is to use a specific class ("myName") to brand elements already in the page. At regular intervals I would poll the page:
// Get all divs in the calendar:
var allDivsCount=myCalendar.querySelectorAll("div").length;
// Get already branded divs
var oldDivsCount=myCalendar.querySelectorAll("div.myName").length;
if (allDivsCount > oldDivsCount) {
// brand the new divs and do stuff
}
Is this a good practice, or is there a better way to do it? Are there libraries that already have such logic implemented?
I am trying to avoid DOMNodeInserted as some browsers don't support it and it is deprecated (due to performance issues, from what I've read).

I know that you're against DOMNodeInserted but I'll added it anyways for options (in case you're not supporting older version of IE).
I blogged about this a while back but it seems like the same solution still applies today (but like I said, depending on the browsers you currently support).
Example:
$(document.body).on('DOMNodeInserted', function (e) {
if (e.currentTarget.toString() === 'HTMLBodyElement') {
console.log(e);
}
});
This triggers any changes within the body not just the body itself.
$('#context').append($('<div />')); // triggers the event above
If you can intercept a DOM change (an AJAX call for instance) and create a global callback function, that would be ideal instead of the setInterval option.
var globalCallback = function() {
/** Do something when the DOM changes */
};
/** Global AJAX event to watch for all AJAX complete within the body */
$('body').ajaxComplete(globalCallback);
This is just one example (AJAX callbacks) of course.

Related

editing a Div that is around an iframe via jquery from within said iframe

I'm sure this sounds a little odd, but here's the background...
We utilize a company that loads their chat program, so we can support our customers, into our page. This is done via javascript and jquery, and creates a structure like this:
<div id="myid" style="...; right: 0px;..."><div><iframe></iframe></div></div>
There's a WHOLE lot more to that, but those are the important parts. Now the tool allows us to put custom scripting, which will be placed in the iframe. My goal is to just remove the "right: 0px", which I have done via the below code, but I don't want to put that code on every page that this tool integrates with. I would like to load it into the tool, and have it run when the iframe and divs are created.
working code on parent:
$(document).ready(function() {
function checkPos() {
$('#myId').each(function() {
var oldstyle = $('#myId').attr('style');
var newstyle = oldstyle.replace(' right: 0px;','');
$('#myId').attr('style', newstyle);
});
setTimeout(checkPos, 100);
};
$(document).ready(function() {
setTimeout(checkPos, 100);
});
});
Once placed in the code include method they provide, I have trouble having it wait until the div tag actually has the "right: 0px;" in its style tag. the only thing I need to run is the three lines in the $('#myId').each(function()
Basically, I need help with having the script in the iframe target the div that the iframe is nested in.
Assuming that whatever tool your using actually lets you pass in a custom script to the content rendered in the iframe (seems fishy to me), a better way of modifying the style in jquery is to use the css function:
$('#myId').css('right', '0px');
Notice I removed the $.each function as well. You are targeting an element with an id, so there isn't any need to iterate.
Edit:
Anyways, back to the problem of delaying execution to when the target, #myId, actually exists. If they are really injecting your javascript into their page (again, seems fishy), then attaching the above code to the $(document).ready() event should do the trick, as long as this listener is attached to their document.
If all else fails, try to use the waitUntilExists plugin, here:
Source:
https://gist.github.com/buu700/4200601
Relevant question:
How to wait until an element exists?

shadowbox stops working after jquery function call

I have a shadowbox script. When I load the page everything works fine, but when I call this jquery load function and then try to trigger the shadowbox by clicking on the image, the large image opens in new window instead.
Here's the code:
<link href="CSS/main.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="shadowbox-3.0.3/shadowbox.js"></script>
<script type="text/javascript">
Shadowbox.init();
</script>
<p id="compas"></p>
Any idea why this is happening?
EDIT
So, we finally get the bottom of this. 15 hours after first commenting on this issue, and at least 50 iterations later, we finally have identified what the problem is and how to fix it.
It actually struck me suddenly when I was creating local aaa.html and bbb.html on my server. That was when it hit me that the element nodes for the content that was being replaced was being removed altogether from the DOM when $.load() runs the callback function. So, once the #menu-home content elements were replaced, they were removed from the DOM and no longer had Shadowbox applied to them.
Once I figured this out, it was just a matter of a single web search and I found:
Nabble-Shadowbox - Reinit Shadowbox
Specifically, the response from mjijackson. What he describes is how to "restart" (reinitialize) Shadowbox using:
Shadowbox.clearCache();
Shadowbox.setup();
So once the #menu-home content was reloaded, what needs to happen is the Shadowbox cache needs to be cleared (essentially, shutting it down on the page), then the Shadowbox.setup() is run, which will detect the elements all over again. You don't run the Shadowbox.init() method again either.
I noticed that you had tried to copy/paste the Shadowbox.setup() in after the $.load(), at least sequentially in the code. However, this wasn't going to work, due to the cache clearing that needs to happen first, and primarily because the .clearCache() and .setup() functions need to be run after the $.load() completes (finishes and runs any callbacks). Those two functions need to be run in the $.load() callback handler; otherwise, you're running it's immediately, but the $.load() is asynchronous and will complete at some later time.
I'm going to go over some other changes I made, just so you understand what, why and wherefore.
Note, I'm not sure if you're familiar with <base>, but the following is at the top of the HEAD element:
<base href="http://62.162.170.125/"/>
This just let's me use the resource files on your computer. You'll not want to use this on your actual site more than likely. If you copy/paste, make sure and remove this line.
<div id="menu">
<ul>
<li><a id="menu-home" href="index.html" rel="http://jfcoder.com/test/homecontent.html">Home</a></li>
<li><a id="menu-services" href="services.html" rel="http://jfcoder.com/test/servicescontent.html">Services</a></li>
<li><a id="menu-tour" href="tour.html" rel="http://jfcoder.com/test/tourcontent.html">Tour</a></li>
<li><a id="menulogin" href="login.html">Login</a></li>
</ul>
</div>
Here, you'll notice I have a relative url in the HREF attribute, and a link to some pages on my server. The reason for the links to my server is that I couldn't access your aaa.html and bbb.html files through AJAX due to cross-site scripting limitations. The links to my website should be removed as well.
Now, the reason I'm using the rel attribute here is that I want allow for the links by way of the href attribute to continue to work in case the JS doesn't function correctly or there's some other error. If you have separate files, one for full HTML document and another for just the fragments, this is what you'll want to do. If you can serve both the full document AND the content-only from the linked file, then you probably don't need the rel attribute, but you'll need to manage the request so the server knows how to respond (full document or just the content part).
var boxInitialize = function(){
try {
if (!Shadowbox.initialized) {
Shadowbox.init();
Shadowbox.initialized = true;
} else {
Shadowbox.clearCache();
Shadowbox.setup();
}
} catch(e) {
try {
Shadowbox.init();
} catch(e) {};
}
};
All I've done here is create a central location for the initialization/setup requests. Fairly straightforward. Note, I added the Shadowbox.initialized property so I could keep track of if the Shadowbox.init() had run, which can only be run once. However, keeping it all in one spot is a good idea if possible.
I also created a variable function which can be called either as a regular function:
boxInitialize();
Or as a function reference:
window.onload = boxInitialize; // Note, no () at the end, which execute the function
You'll probably notice I removed the $() and replaced them with jQuery() instead. This can turn into a real nightmare if you end up with an environment with multiple frameworks and libraries competing for $(), so it's best to avoid it. This actually just bit me real good the other day.
Since we have a closure scope within the .ready() callback, we can take advantage of that to save several "private" variables for ow use at different times in the scripts execution.
var $ = jQuery,
$content = jQuery("#content"), // This is "caching" the jQuery selected result
view = '',
detectcachedview = '',
$fragment,
s = Object.prototype.toString,
init;
Note the , at the end of all but the last line. See how I "imported" the $ by making it equal to the jQuery variable, which means you could actually use it in that#.
var loadCallback = function(response, status, xhr){
if (init != '' && s.call(init) == '[object Function]') {
boxInitialize();
}
if (xhr.success()
&& view != ''
&& typeof view == 'string'
&& view.length > 1) {
$fragment = $content.clone(true, true);
cacheContent(view, $fragment);
}
};
This runs when the $.load() completes the process of the AJAX request. Note, the content returned in the request has already been placed on the DOM by the time this runs. Note as well that we're storing the actual cached content in the $content.data(), which should never be removed from the page; only the content underneath it.
var cacheContent = function(key, $data){
if (typeof key == 'string'
&& key.length > 1
&& $data instanceof jQuery) {
$content.data(key, $data.html());
$content.data(detectcachedview, true);
}
};
cacheContent() is one a method you may not want; essentially, if it was already loaded on a previous request, then it will be cached and then directly retrieved instead of initiating another $.load() to get the content from the server. You may not want to do this; if so, just comment out the second if block in the menuLoadContent() function.
var setContent = function(html){
$content.empty().html(html);
if (init != '' && s.call(init) == '[object Function]') {
boxInitialize();
}
};
What this does is first empty the $content element of it's contents/elements, then add the specified string-based markup that we saved earlier by getting the $content.html(). This is what we'll re-add when possible; you can see once the different links have been clicked and loaded, reclicking to get that to redisplay is really quick. Also, if it's the same request as currently loaded, it also will skip running the code altogether.
(We use $content like because it is a reference to a variable containing a jQuery element. I am doing this because it's in a closure-scope, which means it doesn't show up in the global scope, but will be available for things like event handlers.
Look for the inline comments in the code.
var menuLoadContent = function(){
// This is where I cancel the request; we're going to show the same thing
// again, so why not just cancel?
if (view == this.id || !this.rel) {
return false;
}
// I use this in setContent() and loadCallback() functions to detect if
// the Shadowbox needs to be cleared and re-setup. This and code below
// resolve the issue you were having with the compass functionality.
init = this.id == 'menu-home' ? boxInitialize : '';
view = this.id;
detectcachedview = "__" + view;
// This is what blocks the superfluous $.load() calls for content that's
// already been cached.
if ($content.data(detectcachedview) === true) {
setContent($content.data(view));
return false;
}
// Now I have this in two different spots; there's also one up in
// loadCallback(). Why? Because I want to cache the content that
// loaded on the initial page view, so if you try to go back to
// it, you'll just pickup what was sent with the full document.
// Also note I'm cloning $content, and then get it's .html()
// in cacheContent().
$fragment = $content.clone(true, true);
cacheContent(view, $fragment);
// See how I use the loadCallback as a function reference, and omit
// the () so it's not called immediately?
$content.load(this.rel, loadCallback);
// These return false's in this function block the link from navigating
// to it's href URL.
return false;
};
Now, I select the relevant menu items differently. You don't need a separate $.click() declaration for each element; instead, I select the #menu a[rel], which will get each a element in the menu that has a rel="not empty rel attribute". Again, note how I use menuLoadContent here as a function reference.
jQuery("#menu a[rel]").click(menuLoadContent);
Then, at the very bottom, I run the boxInitialize(); to setup Shadowbox.
Let me know if you have any questions.
I think I might be getting to the bottom of this. I think the flaw is the way you're handling the $.load() of the new content when clicking a menu item, coupled with an uncaught exception I saw having to do with an iframe:
Uncaught exception: Unknown player iframe
This Nabble-Shadowbox forum thread deals with this error. I'm actually not getting that anymore, however I think it came up with I clicked on the tour menu item.
Now, what you're doing to load the content for the menu items really doesn't make any sense. You're requesting an entire HTML document, and then selecting just an element with a class="content". The only benefit I can see for doing this is that the page never reloads, but you need to take another approach to how to get and display the data that doesn't involve downloading the entire page through AJAX and then trying to get jQuery to parse out just the part you want.
I believe handling the content loading this way is the root cause of your problem, hence the $.load() toggling of menu views breaks your page in unexpected ways.
Question: Why don't you just link to the actual page and skip all the $.load() fanciness? Speed-wise, it won't make that much of an impact, if any at all. It just doesn't make sense to use AJAX like this, when you could just link them to the same content without issue.
There are two alternatives that would allow you to prevent roundtrip page reloads:
Setup your AJAX calls to only request the .content portion of the markup if you have the ?contentonly=true flag in the URL, not the entire HTML document. This is how it's traditionally done, and is usually relative simple to do if you have a scripting environment.
$(".content").load('index.html?contentonly=true');
Then your server responds only with the content view requested.
Serve all of the content views within the same HTML document, then show as appropriate:
var $content = $('.content');
$content.find('.content-view').hide();
$content.find('#services-content').show();
It doesn't look like you have a whole lot of content to provide, so the initial page load probably won't have that much of an impact with this particular approach. You might have to look into how to preload images, but that's a very well known technique with many quality scripts and tutorials out there.
Either one of these techniques could use the #! (hashbang) technique to load content, although I believe there are some issues with this for search engines. However, here is a link to a simple technique I put together some time ago:
http://jfcoder.com/test/hash.html
Also, this is just a tip, but don't refer to your "content" element with a class, ie, .content. There should only be one content-displaying element in the markup, right? There's not more than one? Use an id="content"; that's what ID attributes are for, to reference a single element. classes are meant to group elements by some characteristic they share, so above when I .hide() the inline content views (see #2), I look for all of the class="content-view" elements, which are all similar (they contain content view markup). But the $content variable should refer to $('#content');. This is descriptive of what the elements are.
This worked for us, we made a site that used vertical tabs and called in the pages with our shadowbox images using jQuery.load
Just give all of your anchor tags the class="sbox" and paste this script in the header.
<script>
Shadowbox.init({
skipSetup:true,
});
$(document).ready(function() {
Shadowbox.setup($('.sbox'));//set up links with class of sbox
$('a.sbox').live('click',function(e){
Shadowbox.open(this);
//Stops loading link
e.preventDefault();
});
});
</script>
Note: we had to put the .sbox class on all our rel="shadowbox" anchors as well as the on the anchor for the tab that called the .load
Thanks to this guy-> http://www.webmuse.co.uk/blog/shadowbox-ajax-and-other-generated-content-with-jquery-and-javascript/
Well, based on Shem's answer, this is my solution.
Every click on specific class, setup and open shadowbox with elements from same class:
jQuery('.sb-gallery a').live('click',function(e){
Shadowbox.setup(jQuery('.sb-gallery a'));
Shadowbox.open(this);
//Stops loading link
e.preventDefault();
});
Thanks to all

How to hide all text, which are smaller than certain pixel and bring them back later using Javascript?

I'm working on a project, which in some cases requires to hide all small text (eg. less than 12px), and on some other event, bring them back. It's not a website, but something happening on the webkit browser. I don't have control over the content on the page (developed by the third party developers), but have control to modify it. I know I can loop through all tag elements and check font sizes and hide them if smaller than 12px, but it's not only inefficient, but the text can be changed to be shown again, say after an ajax call, which is "prohibited". Other solution would be to run that loop every couple seconds, but it's an expensive process.
Other task is to show small text on some other event, which is not too difficult to implement by just using simple custom class.
You can run the code on page-load, and then when any AJAX call completes using jQuery's Global AJAX Event Handlers: http://api.jquery.com/category/ajax/global-ajax-event-handlers/
$(function () {
function findSmallText($root, state) {
if (typeof $root == 'undefined') {
$root = $(document);
}
if (typeof state == 'undefined') {
state = 'none';
}
$.each($root.find('p, div, span, font, button, a'), function () {
if ($(this).css('font-size').replace(/(px|pt|em)/gi, '') <= 12) {
$(this).css('display', state);
}
});
}
//run the function when the DOM is ready
findSmallText();
//also run the function when any AJAX request returns successfully
$(document).ajaxSuccess(findSmallText);
});
You can pass the findSmallText function two arguments:
$root: (jQuery object) the root element to start looking for small text, limit this as much as possible to increase performance (so unnecessary elements don't have to be scanned).
state: (string) the display property to add to the elements with small text, you can use block to show the elements.
if the HTML structure doesnt change (no extra containers added thru AJAX) simply analyze the page onLoad (kinda like what Jasper suggests) but instead of re-running the analysis after each AJAX call you add a new class - let's call it .HideMeInCertainCases for the fun of it. That way you can hide / show everything you want with a simple selector whenever you want.
So instead of this line: $(this).css('display', state); use $(this).addClass('HideMeInCertainCases');
When the event you were talking about occurs you can then toggle the display state with this selector $("HideMeInCertainCases").toggleClass("hideMe"). Changing the display-attribute directly might break your layout as the nodes containing text might have different displays to begin with (block, inline, inline-block...). Of course .hideMe { display:none; } should be somewhere in your stylesheet. If you want the layout to stay the same and only hide the content use visibility instead of display

Does browser rendering and JavaScript execution happen simultaneously?

For example, if I have this:
$('#button').click(function() {
$.get('/question', function(data) {
$('#question').html(data);
$('#question').dialog( ... );
});
return false;
});
Will the user see the question content for a brief moment before the dialog is shown?
Note: Normally I'd just hide the #question manually, but there's actually a step in between html() and dialog() with another jQuery plugin where the content must not be 'hidden'.
Short Answer
Yes, it's possible that the user will see the question content for a brief moment before the dialog is shown.
The Fix
To guarantee you won't momentarily see the contents of #question before displaying the dialog, absolutely position #question offscreen before displaying it. After that, call the jQuery plugin that requires #question to be displayed. Finally, hide #question and restore its position.
CSS
#question
{
display: none;
}
JavaScript
$('#button').click(function() {
$.get('/question', function(data) {
var question = $('#question');
question.html(data);
var position = question.css('position');
var top = question.css('top');
var left = question.css('left');
question.css({ position: 'absolute', top: -1000, left: -1000 }).show();
//whatever you need to do with #question while it's not hidden
question.hide().css({ position: position, top: top, left: left });
question.dialog( ... );
});
return false;
});
The browser will render the DOM up until that call, at which point it will stop and parse/execute your js. This is why it's considered best practice to put all script tags at the bottom of a page (so that the browser can render enough of the DOM so your visitors aren't stuck staring at a blank white screen).
Using
$(document).ready();
can alleviate this to an extent, but if you're truly concerned with when it is added to the DOM, make sure your code is added at the very bottom of your HTML's body tag.
References:
http://developer.yahoo.com/blogs/ydn/posts/2007/07/high_performanc_5/
In your case absolute not, because you are using a framework. It works like this:
1) Script code is requested from external files as the page progressively loads. An HTML parser has to parse the script tags before there is any awareness of a script request. This code executes when called, but it is fed into the JavaScript interpreter the moment it is available to the JavaScript interpreter.
2) Script code resident directly in the page is fed into the interpreter as the HTML code is parsed by an HTML parser and a script tag is encountered. Code inside functions executes when called, with one exception. Otherwise code executes immediately upon interpretation. The one exception is when a function block is immediately followed by "()" which indicates immediate invocation.
3) Most code that executes initially executes from function calls made with the "onload" event. The onload event occurs when the static DOM is fully available from the HTML parser and when all asset requests from the initial static HTML are requested. In some edge cases with older browsers it is possible for conflicting conditions to occur that create a race condition in the page that prevents the onload event from ever firing or after an extraordinary delay.
4) You are using jQuery, so you are at a severe disadvantage with regards to speed of availability. jQuery code is JavaScript code, so it has to enter the JavaScript interpreter exactly like any other JavaScript. All the prior points from this post must be observed before any jQuery code can execute.
5) When I perform A/B testing I need code to execute as absolutely as early as possible and as fast as possible to minimize flicker on the page, so frameworks are definitely not an option. In this case I follow these steps:
5a) I find the first id attribute directly after the DOM node(s) that I need to access.
5b) I write a function to test for the availability of this node. If the node is available then the areas above it are available, so I am know I am solid. Consider the following example:
var test = function () {
var a = document.getElementById("first_node_lower_than_I_need");
if (a !== null && typeof a === "object") {
//my code here, because I know my target area is available
} else {
setTimeout(test, 100);
}
};
setTimeout(test, 100);
5c) Notice in the sample code above that I call my function with a setTimout delay to give the DOM a fighting chance. If the function executes early that is okay because I am calling it recursively with a delay to give the DOM some extra time to load. If you set your delay to 50ms or lower you are increasing execution time in IE8 and lower because of numerous unnecessary calls for the function. I recommend keeping the delay at 100ms for an ideal balance cross browser, but if you really want rapid execution in new browsers then set the first delay to 50ms, this is the one outside the function, and keep the other at 100ms.
5d) Minimize your use of innerHTML property with the above method, or be very familiar with the targeted page to know when it is okay to use innerHTML. The problem with innerHTML is that it changes the page output without reporting those changes back to the parsed DOM in memory, which normally is an irrelevant disconnect. However, in this case it is certainly relevant because of how fast and early your injected code can execute. This is a problem because other code that executes later, such as with the onload event or jQuery's ready event, will either overwrite your changes or will not be able to find their respected DOM load and simply drop their execution all together. This is particularly an important concern if you are targeted a very high level node in the DOM tree, so for your safety be very specific when selecting nodes to use innerHTML or just use DOM methods. This is a bit more complicated in that you cannot use a DOM method only solution because you cannot change text nodes with the nodeValue method cross-browser as this is not supported in IE7.
If you need to execute JavaScript code before completion of DOM parsing then do not use a JavaScript framework. Write using plain regular JavaScript code and optimize the hell out of it. Otherwise you will always have flicker and the larger of the static HTML download the longer and more noticeable that flicker will be. Additionally, jQuery code tends be far slower to execute than regular optimized JavaScript due to its reliance on CSS like selectors. If your injected jQuery code is required to perform a large task on a very large static HTML document then it is unlikely to complete execution in IE7 without timing out.
This is the reason initializing any DOM-related activity should be done/triggered from within $(document).ready() .
So if you put you $.get statement inside of doc ready, you can ensure that all the elements in the HTML have been rendered and are ready to be interacted with via JS.
$(document).ready(function () {
$.get('/question', function(data) {
$('#question').html(data);
$('#question').dialog( ... );
});
});

Event handling issues

I have a couple of, what may end up being for this forum, overly-novice questions regarding unobtrusive event handling.
As I understand it, a properly set-up document would look something like this:
<html>
<head>
<title>Title</title>
<script src="jsfile.js" type="text/javascript></script>
</head>
<body>
//Body content, like some form elements in my case
</body>
</html>
Jsfile.js would look something like this:
function a() {
//code;
}
function b()...
window.addEventListener('load', a, false);
document.getElementById("id").addEventListener('click', b, false);
document.myForm.typeSel.addEventListener('change', c, false);
//or to use better browser-compatible code...
function addEvent(obj,evt,fn) {
if (obj.addEventListener)
obj.addEventListener(evt,fn,false);
else if (obj.attachEvent)
obj.attachEvent('on'+evt,fn);
}
addEvent(window, 'load', a);
addEvent(document.getElementById('id'), 'click', b);
addEvent(document.myForm.typeSel, 'change', c);
As I understand it, while in the head the browser will load this JavaScript code, adding each of those event handlers to their respective elements. HOWEVER... While the window handler is added properly, none of the others are. But if placed within a function, the (for instance) getElementById method of accessing an element works just fine, and the event handler is added. So I could conceivably make a loadEvents() function which is called via window onload, which contains all of the addEvent() functions for the other document elements for which I need event handlers. But as I understand the whole thing, I shouldn't have to do this.
In addition, if I were to stick the addEvent code within the body along with the element it addresses, such as:
<input type="checkbox" id="test" />
<script type="text/javascript>
document.getElementById("test").onclick = func;
</script>
...then it works fine. But of course it also violates the whole reason for removing inline event handlers!
So question being: In order to use *element*.addEventListener('click',func,false), addEvent(*element*,'click',func), or even *element*.onclick = func - how can I successfully reference an element at the end of a script file in the head, without having to stick it in another function? Why does getElementById and other such methods not work outside of a function in the head?
Or, is there some flaw in my underlying understanding?
Putting <script> in the <head> used to be the wisdom. But nowadays, with heavy ajax pages, <script> is more and more often but in the body, as far down below as possible. The idea is that the loading and parsing of the <script> keeps the rest of the page from loading, so the user will be looking at a blank page. By making sure the body is loaded as fast as possible, you give the user something to look at. See YAHOO best practices for a great explanation on that issue: http://developer.yahoo.com/performance/rules.html
Now, regardless of that issue, the code as you set it up now, can't work - at least, not when the elements you attempt to attach the handlers to aren't created yet. For example, in this line:
document.getElementById("id").addEventListener('click', b, false);
you will get a runtime error if the element with id="id" is inside the body. Now, if you put the <script> in the body, way below, after the content (including the lement with id="id", it will just work, since the script is executed after the html code for those elements is parsed and added to the DOM.
If you do want to have the script in the head, then you can do so, but you'll need to synchronize the adding of the event handlers with the rendering of the page content. You could do this by adding them all inside the document or window load handler. So, if you'd write:
//cross browser add event handler
function addEventHandler(obj,evt,fn) {
if (obj.addEventListener) {
obj.addEventListener(evt,fn,false);
} else if (obj.attachEvent) {
obj.attachEvent('on'+evt,fn);
}
}
addEventHandler(document, 'load', function(){
//set up all handlers after loading the document
addEventHandler(document.getElementById('id'), 'click', b);
addEventHandler(document.myForm.typeSel, 'change', c);
});
it does work.
The reason why window.addEventListener works while document.getEle...().addEventListener does not is simple: window object exists when you're executing that code while element with id="abc" is still not loaded.
When your browser downloads page's sources the source code is parsed and executed as soon as possible. So if you place script in head element - on the very beginning of the source - it's executed before some <div id="abc">...</div> is even downloaded.
So I think now you know why
<div id="test">Blah</div>
<script type="text/javascript">document.getElementById("test").style.color = "red";</script>
works, while this:
<script type="text/javascript">document.getElementById("test").style.color = "red";</script>
<div id="test">Blah</div>
doesn't.
You can handle that problem in many ways. The most popular are:
putting scripts at the end of document (right before </body>)
using events to delay execution of scripts
The first way should be clear right now, but personally I prefer last one (even if it's a little bit worse).
So how to deal with events? When browser finally download and parse whole source the DOMContentLoaded event is executed. This event means that the source is ready, and you can manipulate DOM using JavaScript.
window.addEventListener("DOMContentLoaded", function() {
//here you can safely use document.getElementById("...") etc.
}, false);
Unfortunately not every browser support DOMContentLoaded event, but as always... Google is the anwser. But it's not the end of bad news. As you noticed addEventListener isn't well supported by IE. Well... this browser really makes life difficult and you'll have to hack one more thing... Yes... once again - Google. But it's IE so it's not all. Normal browsers (like Opera or Firefox) supports W3C Event Model while IE supports its own - so once again - Google for cross-browser solution.
addEventListener might seems now the worst way to attach events but in fact it's the best one. It let you easly add or remove many listeners for single event on single element.
PS. I noticed that you consider of using Load event to execute your scripts. Don't do that. Load event is execute too late. You have to wait till every image or file is loaded. You should use DOMContentLoaded event. ;)
EDIT:
I've forgotten... dealing with cross-browser event model is much easier when you're using some framework like very popular jQuery. But it's good to know how the browsers work.
are you familiar with jQuery?
its a javascript library featuring some really awesome tools.
for instance if you want to have some js action done just after your page if fully loaded and all DOM elements are created (to avoid those annoying exceptions) you can simply use the ready() method.
also i see you want to attach click \ change events jQuery takes care of this too :) and you don't have to worry about all those cross-browser issues.
take a look at jQuery selectors to make your life easier when attempting to fetch an element.
well thats it, just give it a shot, its has a very intuitive API and a good documentation.

Categories

Resources