September 24, 2006

Scriptaculous Patch

If you are using scriptaculous script and its sortables for drag and drop in your web pages, you may realizied that you can have only an empty space where you will drop your dragging element. Here I will explain how you can have a div with a custom style so that you can a box that will mark where you will drop you element.

Here I am patching scriptaculous script 1.6.4 and I tested final script with FF 1.5 and IE 6. All the changes I will make is in the dragdrop.js that is coming with scriptaculous and I will modify Sortable class in the script.

Lets start with adding two new methods to Sortable class; createGuide and markEmptyPlace methods

createGuide : function (element) {
if(!Sortable._guide) {
Sortable._guide = $('_guide') || document.createElement('DIV');
Sortable._guide.style.position = 'relative';
Sortable._guide.style.width = '1px';
Sortable._guide.style.height = '0px';
Sortable._guide.style.cssFloat = 'left';
Sortable._guide.id = 'guide';

document.getElementsByTagName("body").item(0).appendChild(Sortable._guide);
}


This createGuide method will create a div with 0px height and 1 px width so that it wont be visible. I will insert this div before the real element that we are dragging. After inserting, I will calculate its absolute position and move my marker div (which I will create in markEmptyPlace method) there. Here is a trick that I am copying dragging elements margins which will affect absolute position. markEmptyPlace method looks like this:
markEmptyPlace: function(element) {

if(!Sortable._emptyPlaceMarker) {
Sortable._emptyPlaceMarker = $('emptyPlaceMarker') || document.createElement('DIV');
Element.hide(Sortable._emptyPlaceMarker);
Element.addClassName(Sortable._emptyPlaceMarker, 'emptyPlaceMarker');
Sortable._emptyPlaceMarker.style.position = 'absolute';
document.getElementsByTagName("body").item(0).appendChild(Sortable._emptyPlaceMarker);
}

var pos = Position.cumulativeOffset(Sortable._guide);
Sortable._emptyPlaceMarker.style.left = (pos[0] + 5)+ 'px';
Sortable._emptyPlaceMarker.style.top = (pos[1] + 5) + 'px';

var dim = {};
dim.width = (Element.getDimensions(element).width-5) + 'px';
dim.height = (Element.getDimensions(element).height-5) + 'px';
Sortable._emptyPlaceMarker.setStyle(dim);

var mg = Element.getStyle(element, 'margin');
if(mg && mg != '') {
Sortable._emptyPlaceMarker.setStyle({margin : mg});
} else {
Sortable._emptyPlaceMarker.setStyle({ margin : ''});
}

Element.show(Sortable._emptyPlaceMarker);
},
Here I am calculating marker div's position by using guide div's position. Than I am making the marker div's size equal to the dragging element's size. +5 and -5 in calculations are there so that I have a slightly smaller box. Also it is important that this div is attached to a css class named as 'emptyPlaceMarker' so you need to have such a css class defined. Here is my emptyPlaceMarker css:
.emptyPlaceMarker
{
border-right: red thin dashed;
border-top: red thin dashed;
border-left: red thin dashed;
border-bottom: red thin dashed;
background-color: Transparent;
}
So we have our methods ready for creating necessary divs. Now lets use them. In scriptaculous, there are two methods used while dragging: onHover and onEmptyHover. onHover is called when you are hovering on another draggable element and onEmptyHover is called when you are hovering over an empty place in droppable. These methods are inserting the dragging element into a droppable as a child. So I modified these methods so that I will insert my guide before the dragging element in the child list. Here are the modified onHover and onEmptyHover methods; (red lines are the lines that I added)
 onHover: function(element, dropon, overlap) {
if(Element.isParent(dropon, element)) return;

if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
return;
} else if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering

Sortable.createGuide(element);
dropon.parentNode.insertBefore(element, dropon);
dropon.parentNode.insertBefore(Sortable._guide, element);
Sortable.markEmptyPlace(element);

if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
var nextElement = dropon.nextSibling || null;

if(nextElement != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering

Sortable.createGuide(element);
dropon.parentNode.insertBefore(element, nextElement);
dropon.parentNode.insertBefore(Sortable._guide, element);
Sortable.markEmptyPlace(element);

if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},

onEmptyHover: function(element, dropon, overlap) {
var oldParentNode = element.parentNode;
var droponOptions = Sortable.options(dropon);

if(!Element.isParent(dropon, element)) {
var index;

var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
var child = null;

if(children) {
var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);

for (index = 0; index < children.length; index += 1) {
if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
offset -= Element.offsetSize (children[index], droponOptions.overlap);
} else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
child = index + 1 < children.length ? children[index + 1] : null;
break;
} else {
child = children[index];
break;
}
}
}
Sortable.createGuide(element);
dropon.insertBefore(element, child);
dropon.insertBefore(Sortable._guide, element);
Sortable.markEmptyPlace(element);

Sortable.options(oldParentNode).onChange(element);
droponOptions.onChange(element);
}
},

As a final touch, I updated the unmark method so that after dragging ends, I will hide the marker and remove the guide from droppables child list:
unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker);
if(Sortable._guide && Sortable._guide.parentNode){
Sortable._guide.parentNode.removeChild(Sortable._guide);
}
if(Sortable._emptyPlaceMarker) Element.hide(Sortable._emptyPlaceMarker);

},
After making these changes to your dragdrop.js and have a 'emptyPlaceMarker' css class defined, you will be able to have a box that will mark where you will drop your dragging element.

Tankut Koray

37 comments:

Thomas said...

Hi. I want to thank u too much for your work, i had no time for developing it and were waiting for my app to be published to modify the last ver of script.aculo.us.
I want u to note also that there is a mistake in your red text marking on this article, in the first block on the onhover modification u marked in red a line that was already in the code and not the one that inserts the guide.
And also that that mods also works in 1.6.4.

Tankut Koray said...

Thanks Thomas,
I corrected the red line in the article.

Anonymous said...

Peculiar, I your onEmptyHover is different in many ways to the one packaged in 1.6.4.

The one you have posted, doesnt seem to be liked by browsers. :-(

Tankut Koray said...

yes you are right about my onEmptyHover is wrong because it is somehow scrambled while pasting my html into the blog.

sorry about that and it is should be alright now.

Phil Crosby said...

Can you fix the display in your blog? This post looks really useful, but the code runs right off the edge of your page, and the page is not scrollable. The width of your blog is set to only a few hundred pixels and the code is much wider than that with font sizes in Firefox turned up.

Tankut Koray said...

Hi Phil,

I tried to edit my blog but when I did it scrambled the omEmptyHover method again.

So I will try to put the file somewhere and post the link here.

Meanwhile, you can have the original script and add the red lines and methods I have here.(my methods are not oversized hopefully)

Tankut

Anonymous said...

Any chance you can provide a final packaged .js file?

Tankut Koray said...

Hi,

Finally with the help of my friend Sinan, here is the link to final .js file:

http://sinan.ussakli.net/script/script/dragdrop.js

And here is the example:

http://sinan.ussakli.net/script/

Tankut

Tankut Koray said...

Hi again,

Due to a bug in the script, instead of copying the element's margin to Sortable_guide, now I am copying to Sortable._emptyPlaceMarker.

Example will be updated soon,

Tankut

andreas said...

Very nice, and very useful :)
Thanks a lot for this! :)

Just one question: Is there a way to get the emptyPlaceMarker-div lower than the moving div?

Tankut Koray said...

thanks andreas:)

But I couldnt understand your question, what do you mean by lower?

can it be the z-index your are talkin about?

andreas said...

Yeah, that's what I mean. :)

Somehow, the place holder layer gets on top of the moving layer.

Tankut Koray said...

Hi andreas,

Can you try to give a lower z-index in the css for the placeholder? And in which browser you are trying this?

Tankut

bbouillot@gmail.com said...

hi, thanx a lot for this ! great work !

John H. said...

Nicely done. Hopefully scriptaculous will officially adopt this feature soon.

In the meantime do you have an update for version 1.6.5 (assuming the class change)?

Probably the only thing that you might want to consider changing is the hard-coded reference of emptyPlaceMarker (make it a definable property instead), but this is a minor squabble.

Serkan Yerşen said...

Hi Tankut, i really liked your work. i'm currently developing the jotform.com, i would like to use your patch in our application. we were considering about it but we had no time to modify the code, thanks to your code we can surely add that feature to jotform's new release.
keep up the good work.

Tankut Koray said...

To John: First of all, sorry for my late reply. I am busy with other things. I didnt updated the patch according to 1.6.5. But I am planning to look at 1.6.5 and update in a few weeks. I will post it here.

To serkan: Thanks a lot. Hope you will be happy about the patch. If you need any help integrating the patch, let me know.

Anonymous said...

hmm hi :) First of all congratulations on developing a feature that everybody needs so much. Any chance to polish it up so that it works with the latest version of scriptaculous and just provide us a new .js file? :)

Tankut Koray said...

Finally I patched 1.6.5. Please find in the blog.

Zhou said...

Thank you for your effort!
It's really useful and should be included in scriptaculous

Zoram said...

Did anyone ever figure out how to make the place marker show up under the draggable object? I have tried to change the z-index on both of the elements and it still shows up on top in IE.

Tankut Koray said...

Hi everybody,

Look for the newer posts for patches for new versions.

Tankut

Sandy said...

Hi Tankut,

Your code is works very smoothly on firefox. But in IE, somehow it can't get the position right. The empty div keeps on displaying outside the container.
-->
var pos = Position.cumulativeOffset(Sortable._guide);
[the Pos value stays the same no matter where Element is hovering]

Could you tell me what i am doing wrong? I copied all your code , made some modifications -> the empty div displayed in the wrong place when Element is hovering over the first droppable on the next row.

var pos = Position.cumulativeOffset(Sortable._guide);
var pos2 = Position.cumulativeOffset(element);

var diff = parseInt(pos[0])-parseInt(pos2[0]);


if(diff >300){
debug("diff>>>");
var nextEle = element.nextSibling || null;
pos = Position.cumulativeOffset(dropon);

if($('image_list')){
Sortable._emptyPlaceMarker.style.left = (pos[0] - 90)+ 'px';
Sortable._emptyPlaceMarker.style.top = (pos[1] ) + 'px';
}else{
Sortable._emptyPlaceMarker.style.left = (pos[0] - 150)+ 'px';
Sortable._emptyPlaceMarker.style.top = (pos[1]) + 'px';
}
}else{
debug("diff<<<<");
if($('image_list')){
Sortable._emptyPlaceMarker.style.left = (pos[0] + 5)+ 'px';
Sortable._emptyPlaceMarker.style.top = (pos[1]) + 'px';
}else{
Sortable._emptyPlaceMarker.style.left = (pos[0] + 5)+ 'px';
Sortable._emptyPlaceMarker.style.top = (pos[1]+ 2) + 'px';
}
}

Tankut Koray said...

Hi Sandy,

Can you send me an example page?

Tankut

Sandy said...

I dont have the code running on any servers.. but the idea is ~ I have a list of sortable divs, float:left. Where only 5 divs are displayed on each row.
d1 d2 d3 d4 d5
d6 d7 d8 d9 d10

So when i hover d10 over d6, the empty div displays:
d1 d2 d3 d4 d5 dX
d6 d7 d8 d9
But what i want is
d1 d2 d3 d4 dX
d5 d7 d8 d9


Thankx for replying ^^

Sandy said...

Also, i was wondering if you noticed that running Scriptaculous drag drop in IE is much slower (like snap effect) but in FF dragging is smooth.

Is there anything i can do to fix this problem?

Tankut Koray said...

Hi Everybody,

Please find the new patch in my last post:

http://tanrikut.blogspot.com/2007/06/scriptaculous-171-beta-3-repatched.html

To Sandy: Sorry for my late reply, but can you try the last version and provide some feedback. Also please note that having ghosting makes the patch work weird(marking the wrong element).

retschgi said...

Hi. Thank you for this extension! You made my day!

Gold Guide for World of Warcraft said...

good post :)

Iab said...

Excellent Work! One thing I noticed is that the emptyPlaceMarker sometimes appears in places where the draggable won't actually fit. So on occasion even if you drop it on a emptyPlaceMarker the draggable gets returned to its original spot. To see this, you can change your example just a little bit.
Add three new children to Layer1. Then change Layer1 to have a smaller width, say 730px. Now if you try to drag Content 5 next to Content 4, the emptyPlaceMarker appears, but on drop the draggable is pushed back to its original postion. Anyway to fix this? Thanks again!

Anonymous said...

Your blog keeps getting better and better! Your older articles are not as good as newer ones you have a lot more creativity and originality now. Keep it up!
And according to this article, I totally agree with your opinion, but only this time! :)

Anonymous said...

Hiya everyone, I just registered on this lovely community forum and desired to say howdy! Have a fantastic day!

Anonymous said...

if you guys requisite to pique [url=http://www.generic4you.com]viagra[/url] online you can do it at www.generic4you.com, the most trusted viagra dispensary other of generic drugs.
you can prize drugs like [url=http://www.generic4you.com/Sildenafil_Citrate_Viagra-p2.html]viagra[/url], [url=http://www.generic4you.com/Tadalafil-p1.html]cialis[/url], [url=http://www.generic4you.com/VardenafilLevitra-p3.html]levitra[/url] and more at www.rxpillsmd.net, the pre-eminent [url=http://www.rxpillsmd.net]viagra[/url] informant on the web. well another great [url=http://www.i-buy-viagra.com]viagra[/url] pharmacy you can find at www.i-buy-viagra.com

Anonymous said...

All men speculation, but not equally. Those who day-dream by means of twilight in the dusty recesses of their minds, wake in the heyday to espy that it was vanity: but the dreamers of the time are menacing men, for the duration of they may dissimulation on their dreams with unblocked eyes, to create them possible.

Anonymous said...

The prestige of spacious men should many times be leisurely on the means they secure utilized to into it.

Anonymous said...

The distinction of extensive men should always be slow on the means they from used to into it.

Anonymous said...

Locale an exemplar is not the sheer means of influencing another, it is the however means.