| « In-the-Brain of Erik Doernenburg: Why Agile Teams Fail | The Social Impact of the Web: Society, Government and the Internet » |
Scriptaculous Draggable and Z-index
05/06/07
Scriptaculous Draggable and Z-index
I was building a prototype today using the Scriptaculous Draggable object. It was a fairly simple idea, a set of 'items' along the top of the page which could be dragged onto a selection of timeslots below, the idea being that by dragging the item to the timeslot you are 'booking' the item for that timeslot. Obviously in the real application this would have to link back to a database and do all sorts of complex stuff, but for now I just wanted to mockup how the UI would work.
I started off aiming to get the basic functionality working and not worrying too much about design. The Scriptaculous stuff is easy to use, I set up my items and timeslots as div elements and gave them a class name, then in my initialisation function used getElementsByClassName to get an array of elements to which I could apply the behaviour:
var els = $('available').getElementsByClassName('cat');
for (var i=0; i<els.length; i++) {
new Draggable(els[i],{revert: true});
}
The droppable bit was a bit more complicated as I added event handler functions for onHover and onDrop:
var els = $('times').getElementsByClassName('slot');
for (var i=0; i<els.length; i++) {
Droppables.add(els[i],{accept: 'cat', onDrop: catchDrop, onHover: showCatch } );
}
My catchDrop handler inserts an item into the booking slot, or increments the number of that item if one or more is already booked, though initially I just popped up an alert to confirm everything worked as expected. The showCatch handler changes the background colour of the element when a draggable is moved over it. I tried using Effect.Highlight initially, but it didn't interact well with the drag and drop stuff - if you tried to add an item to the bottom booking slot, all of the slots above became highlighted as well. So I settled for changing the background colour and used a brute force approach of removing the background colour from all the other elements before applying to the current one. Here is what I'd got so far.
With the basic functionality working I set about styling the page with CSS. I envisaged a 'toolbar' of items across the top with an 'Outlook style' collection of timeslots below. Since, in the actual application, there could be any number of items, I made the toolbar a fixed height and width and set it to scroll along the x axis:
#available {
width: 100%;
height: 64px;
overflow-x: scroll;
overflow-y: hidden;
}
This had an unfortunate side effect, however. Now when I dragged my items down onto the booking slots they were hidden behind the slots, and this looked a bit stupid. I did some experimenting with z-index for the various elements involved, but didn't have much joy - I got it sort of working in IE but that 'solution' hid all the booking slots in Firefox, so I resorted to Google. The first thing I learned was that Scriptaculous sets the z-index of the draggable element to 1000 - so clearly that shouldn't be the problem. Next, I wondered if the float: left I'd applied to the draggable elements somehow interfered with the the z-index, which led me to an article, with an excellent example, on the Mozilla developer site. This was useful information, and clearly floating does have some effect but it looked like it wasn't what was causing my problem. Finally I came across a CSS-d wiki article on z-index stacking context. Although I'm sure I haven't fully understood the article yet, this seemed to be the problem - the draggable elements were now in a different stacking context to the droppable elements, and this is why my z-index experiments weren't working. This got me to the root of the problem - if it worked fine before any CSS was involved, then something in my CSS was messing with the stacking context. Commenting the rules out one by one soon revealed the culprit - overflow-x creates a new stacking context (or at least, has an equivalent effect) in Firefox, whereas it seems IE's broken z-index handling was what allowed me to end up with something which worked there.
Rather than delve even deeper into the mysteries of z-index I chickened out and did the page without the overflow, it's only a mockup after all (which is also why it's only been tested in Firefox and IE7, before anyone starts complaining
)
Trackback address for this post
Trackback URL (right click and copy shortcut/link location)
11 comments
1) After "startDrag: function(event) {", add "this.element.style.position = 'absolute'; "
2) After "finishDrag: function(event, success) {", add "this.element.style.position = 'static'; "
Thanks for your article.
Rob
I tested in FF3 Beta 5
new Draggable(asanaId,{revert:true
,onStart: function(drgObj,mouseEvent){
drgObj.element.style.position='absolute'
}
,onEnd: function(drgObj,mouseEvent) {
drgObj.element.style.position='relative'
}
});
,onStart: function(drgObj,mouseEvent){
drgObj.element.setStyle({'position':'absolute',width:'30%'})
drgObj.offset=[0,0]
}
$$('#available .cat').each(function(el) {
new Draggable(el,{revert: true});
});
etc.
After a long time of searching I gave up on searching a solution. I had searched every single site about the subject I could find with google. Then I thought of perhaps the solution lies within my CSS code. I placed my stylesheet back piece by piece until I found when the trouble started.
I came to the conclusion that position: relative; creates the problem when you use overflow: auto; and try to drag something to a other element than the container which has overflow: auto; on it.
That was the solution in my case. Maybe it will help other people who also got this problem too.
Hi
I couldn't get any of these suggestions to work but finally worked out this solution. Thought I'd post it here as this page ranks high in Google.
I added the following event handlers to the Draggable declaration (in my code, not draggable.js).
onStart: function(drgObj,mouseEvent) {
if (Prototype.Browser.IE) {
var element = drgObj.element;
element.parentNode.removeChild(element);
$(document.body).appendChild(element);
element.setStyle({
width: '230px'
});
}
},
onEnd: function(drgObj,mouseEvent) {
if (Prototype.Browser.IE) {
drgObj.element.setStyle({
width: 'auto'
});
}
}
It bypasses IE's weird stacking order by moving the draggable element to the root node while it is being dragged (what isn't shown here is the event handler that is called when the element is dropped and inserts the draggable into the droparea but this didn't require any changes).
The width also has to be set (in my code at least) to stop the draggable element spanning the whole page.
I hope this helps someone.
Michael
Rob
http://www.iterativedesigns.com/2007/11/9/dragdropextra-js

Early access to Hello! HTML5 and CSS3 available now
Early access to HTML5 in Action available now

