Thursday, March 08, 2007

AJAX problem with drag and drop for Internet Explorer using Scriptaculous

I have been having this kind of hair tearing problem with Internet Explorer 6 in the last few days while trying to implement drag and drop within the AJAX framework of Ruby On Rails. The problem is extremely frustrating to track down as it works perfectly with Firefox (a sign of the time, I suppose, IE used to be the benchmark for sane rendering, but alas no more !). Hopefully by detailing it here, no one else will have to go through the same pain I did.

I should point out here that most advertised features work perfectly in both Internet Explorer and Firefox. However, it was a combination of what I did which caused problems in IE. So if you ever get this problem where Internet Explorer throws out this Javascript error "Unspecified error" each time you attempt to move a drag-able item over potential drop sites, then read on.

Ruby On Rails comes integrated with a pretty cool Javascript client library called Scriptaculous, which takes away the pain of cross browser incompatibilities (supposedly, but as I will show here, even this library has its limits when it comes to browser idiosyncrasies). Scriptaculous provides AJAX support, drag and drop, auto-complete, and many other cool features out of the box. I had no problem implementing the basic drag and drop, which worked fine in both Firefox and Internet Explorer. However, I ran into the above mentioned error where Internet Explorer will consistently throw out streams of Javascript "Unspecified error" once a drop has been made. The error itself is of no help as it gives you no clues as to what the problem is. What is it which has completely messed up the DOM in such a way that once the first drop is made, no other items can be dragged around without causing errors ? The Javascript error happens consistently on a block of code in the dragdrop.js file at line 1589, column 7, ( or prototype.js 2326, column 7) which is highlighted bold below.


var Enumerable = {
each: function(iterator) {
var index = 0;
try {
this._each(function(value) {
try {
iterator(value, index++);
} catch (e) {
if (e != $continue) throw e;
}
});
} catch (e) {
if (e != $break) throw e;
}
},


I tried out the Scriptaculous demo shopping cart on Internet Explorer, which works perfectly. This tells me Internet Explorer is not the problem, or indeed the basic boiler plate drag and drop code (just a process of elimination). Well, back to my code. It must be something in my code that causes this. The basic code must work as I am able to drop the draggable item once. So, it must be something to do with the sequence of events, rather than the drag and drop technology itself. I viewed the html before and after the drag and drop action, looks ok. Then I viewed the list of Droppables objects using the JScript debugger, this looked OK too.

Google was of no help. Not sure if it is because the subject is relatively new (Ruby on Rails, AJAX, Scriptaculous etc.). Whatever I could find led me to try out these desperate measures:

* download the latest version of Scriptaculous instead of using the one packaged with Rails
* deploy an application called Browser Object Helper daemon, which allows me to look at all the third party extensions loaded into IE on start up, as these BOH's have been known to cause problems with IE crashing with "Unspecified error"
* re-engineer my pages so that the different fragments get generated together instead of via cascaded Javascript hooks on the :onComplete event

None of the above solutions helped !! Where is Sherlock when you need him ?

Then, on the third day (what is it about the third day ? I always seem to have more luck on the third day !), something dawned on me. I quickly checked out the list of drop sites again in the JScript debugger, and sure enough, the error always happened on the second drop site in the list. A quick drill down gave me an insight into my problem, in that the second drop site has the same name as the first drop site. How is this possible ? pages are generated on the fly, and all the drop sites are guaranteed to have unique id's within the page. Looking through the code again told me the problem, which is to do with Rails's partial rendering mechanism. Rails has a very good built in AJAX engine, which allows you to refresh fragments of the page without regenerating the whole page (this is the technology used extensively at Google Maps). It is this partial rendering which is the root of all problems. Normally, when a page is reloaded, the whole list of drop sites, which is built up from the initial rendering, is started from scratch. In fact, this should work perfectly too, normally, if you do not re-generate the drop sites within the page as part of partial rendering. However, if these drop sites are re-generated (as mine is), then this is when IE behaves differently to Firefox.

Firefox looks like it did the right thing (or maybe not, depending on your point of view), and still honours calls to the DOM element associated with the defunct drop site, specifically the cumulativeOffset() call in dragdrop.js, which traverses up the DOM in order to calculate the overlap between the dragged item and the drop site. It could be because Firefox looks up the DOM element by name and resolves it to the newly created element automatically (cool or what ?).

However, Internet Explorer goes completely crazy on this (or maybe it should, again depending on your point of view) and throws out these JScript errors about the dead drop site still sitting happily in the cached list of Droppables. This list is created by the drag and drop technology, and because we are not reloading the page, is never cleared out properly between AJAX calls.

The solution, once known looks so simple, is to place this Javascript call into the page fragment which is re-built on each AJAX call (but only if you intend to re-generate the drop sites):

<script type="text/javascript">
if (document.all) { Droppables.drops = [] }
</script>

Go, go, gadgets, go !!

The footnote is, however, even though Firefox is more forgiving in this instance (I mean it does the right thing in terms of intended design), in the long run, if your partial rendering is more intensive, it is probably a good idea to clear out the drop sites regardless of the browser to make it more memory efficient and also quicker to iterate through the list of possible drop sites each time an item is dragged around on the screen.

Also, the moral of the story is, Internet Explorer absolutely detests having different DOM elements with the same identifier. Firefox seems to do something internally to cope with this rather impressively. As a client side developer, I know which one I'd rather use. Alas, the majority out there do not write web pages for a living. Also, if technical superiority counts, we'd all be using Betamax tapes, right ?

This article source: http://public.transcraft.co.uk/articles/development/ie_ajax_dragdrop.html

7 comments :

Jason L said...

Thanks a ton; I had the same problem and was pulling my hair out!

Craig Loftus said...

That has solved my problem as well.

I decided to use a method more along the lines of:
* Droppables.Remove
* Regenerate page
* Droppable.Add

Interestingly, I used the same function to add the droppable back as was releasing it in the first place i.e. a recursive function, nasty but it worked.

nazgum said...

big thanks for posting this =)

deeptibaghel said...

Thanks a lot,
my problem was solved in seconds due to this post which otherwise might have taken months.

kalamiez said...

thanks, helped a lot :)

luke said...

This was a very hard to debug problem! Even worse Internet Explorer was crashing on me. Very grateful to you for posting this. I ended up writing a nice function that cleans up the droppables list of anything that shouldn't be there.

works with prototype 1.5, but the iterator could be done with normal js if people aren't using prototype.

function flushDroppables() {
// iterate over droppables list
Droppables.drops.each(function(e){
// test on getHeight to determine
// if element reference still exists
if(e.element.getHeight() == 0) Droppables.remove(e.element);
});
}

Rob Whitener said...

Great posting! This did the trick right off the bat and now I also understand more about IE and the DOM!

Thanks!