Drag. Drop. Lock.

Written by David Walsh on May 6, 2008 · 36 Comments

I've received dozens of emails about my Six Degrees of Kevin Bacon Using MooTools article. The MooTools in my article contained a lot of conditional code to require correct dropping per the game and many people requested that I simplify the process and just explain the drag, drop, lock process. Ask and you shall receive!

Step 1: The XHTML

<!-- holders -->
<div id="droppable-holder">
	<div id="holder1" class="droppable"></div>
	<div id="holder2" class="droppable"></div>
	<div id="holder3" class="droppable"></div>
	<div id="holder4" class="droppable"></div>
	<div id="holder5" class="droppable"></div>
	<div id="holder6" class="droppable"></div>
</div>
<div class="clear"></div>


<!-- images -->
<div id="dragable-holder">
	<div class="dragable" id="dragable1">Drag Me 1</div>
	<div class="dragable" id="dragable2">Drag Me 2</div>
	<div class="dragable" id="dragable3">Drag Me 3</div>
	<div class="dragable" id="dragable4">Drag Me 4</div>
	<div class="dragable" id="dragable5">Drag Me 5</div>
	<div class="dragable" id="dragable6">Drag Me 6</div>
</div><div class="clear"></div>

The XHTML is very basic. You create your dragables, you create your droppables. I gave each an id attribute, but that isn't necessary for the sake of this example.

Step 2: The CSS

.clear			{ clear:both; }
#dragable-holder	{ margin:20px 0; }
.dragable		{ position:relative; cursor:move; width:100px; height:70px; border:1px dotted #ccc; float:left; margin:0 20px 0 0; padding:30px 0 0 0; text-align:center; }
#droppable-holder	{  }
.droppable		{ border:2px solid #ccc; width:100px; height:100px; float:left; margin:0 20px 0 0; }
.mo				{ background:#eee; }
.locked			{ cursor:default; background:#fffea1; }
.locked-border	{ border:1px solid #fc0; }

None of the CSS is required, per say, but you always want to make things look good.

Step 3: The MooTools JavaScript

//when the dom is ready...
window.addEvent('domready',function() {
	
	document.ondragstart = function () { return false; }; //IE drag hack
	
	//for every dragable image...
	$$('.dragable').each(function(drag) {
		
		//make it dragable, and set the destination divs
		new Drag.Move(drag, { 
			droppables: '.droppable',
			onDrop: function(el,droppable) {
				if(droppable.get('rel') != 'filled')
				{
					//inject into current parent
					el.inject(droppable).addClass('locked');
					el.setStyles({'left':0,'top':0,'position':'relative','margin':0}); //hack -- wtf?
					droppable.set('rel','filled');
					this.detach();
				}
			},
			onEnter: function(el,droppable) {
				//colors!
				droppable.addClass('mo');
			},
			onLeave: function(el,droppable) {
				droppable.removeClass('mo');
			}
		});
		
		drag.setStyles({ 'top':0, 'left':0, 'margin-right':'20px' });
		
	});
});

The dragging and dropping is very self-explanatory. The part to note is the drop event. This is where we inject the dragable into the droppable and detach the element from the list of dragable elements.

Once I removed all of the "Six Degrees" specific code, the Moo to drag, drop, and lock is super easy!

Comments

  1. Pretty cool stuff! Thanks!

  2. Great example – the one critique is that if you drop multiple boxes on the same container it messes up, but this is an example item, so understandably people should code for this too when implementing.

  3. @Paul: Awesome catch! I didn’t notice that. Hopefully I’ve have some time to hit that this weekend.

  4. @Paul: I’ve updated the post to disallow dropping more than one DIV in a droppable.

  5. Is it anyway possible to lock them entirely so that if you call refresh the
    page the boxes will stay where you droped them ?

  6. @Tim: Definitely. I would recommend using cookie functionality to do this. You can see a great example here:

    http://davidwalsh.name/save-text-size-preference-mootools-12

  7. @David: But with a cookie it will only be each visitor who will be able to see the change they do … It need to be only one person who make the changes and then everybody else who enter the site should see the change. So I was thing abit about mysgl php something .. ? Have you seen it in use somewhere maybe ?

  8. @Tim: I get you now. Yep, you’ll make an Ajax call that records the dragable element and which droppable it fell into. I don’t have a specific example now, but maybe in the future.

  9. Hi David

    Excellent tutorial, its proved to be very useful so far. I was wondering if there is a way to make a ‘dragable’ snap on to a ‘droppable’ without locking? So even after it snaps on, it can be dragged out of the drop zone. Thanks

  10. @Vivek: Here’s how you prevent the lock:

    onDrop: function(el,droppable) {
    //inject into current parent
    el.addClass(‘locked’);
    el.setStyles({‘left’:0,’top’:0,’position’:'relative’,'margin’:0}); //hack
    }
    },

  11. Awesome david, your blog has been tremendously helpful. However, I was wondering if there a way to undo the inject operation. For example, if I have two droppable R1 and R2 and two dragables G1 and G2; first I insert G1 into R1 (works perfectly with your code update), then I take G1 out of R1 (all good) and then try to put G2 into R1 (G2 appears below R1 instead of inside R1) . I think because of the ‘inject’ it is still inside the parent.

    So I was wondering is there a way to ‘eject’ an element from a parent?

    Thanks again.

  12. @Vivek: Interesting question. The problem of G2 appearing below R1 could have something to do with the following line:

    drag.setStyles({ ‘top’:0, ‘left’:0, ‘margin-right’:’20px’ });

    I’m not certain though. Could you post a sample URL?

    “Eject” is a very interesting idea. It would be cool to extend Drag.Move to allow “ejecting” of elements.

  13. Hey David

    So the quick fix I’ve got so far, is to re-inject the dragable into the dragable holder with the hack:

    onDrop: function(el,droppable) {
    if(droppable)
    {
    if(droppable.get(‘rel’) != ‘filled’)
    {
    el.inject(droppable);
    el.addClass(‘locked’);
    el.setStyles({‘left’:0,’top’:0,’position’:'relative’,'margin’:0}); //hack — wtf?
    }
    else
    { //filled

    }
    }
    else
    {//not a drop zone
    el.inject(‘dragable-holder’);
    el.removeClass(‘locked’);
    el.setStyles({‘left’:0,’top’:0,’position’:'relative’,'margin’:0}); //hack — wtf?
    }
    },

    This seems to work fine for now, but I still need to test it out.

  14. sorry abou the double post (damn IE)..

  15. christleo July 13, 2008

    figuring out for hours, google and found your side!

    document.ondragstart = function () { return false; }; //IE drag hack

    this code save me!

    THANKS!

  16. Great example.

    I’ve been scouring docs.mootools.com and google to find a way to ghost the draggable element, or have it snap back to it’s position to no avail.

    I’m basically going for the old Drag.Cart from mootools 1.11 (http://demos111.mootools.net/Drag.Cart). Any where you know to point me?

  17. Ryan

    I am also looking for a solution to find a way to ghost the draggable element, or have it snap back to it’s position to no avail. Any one out there has quick example or URL? – saves me lot of time.

  18. hey david, thanks for this:

    el.setStyles({‘left’:0,’top’:0,’position’:'relative’,'margin’:0}); //hack — wtf?

    damn bill gates!

  19. SoLoGHoST September 4, 2008

    Hello, I think this drag and drop script is perfect for a sidebar gadget i am working on.

    I have a few problems with it, perhaps someone here can help…?

    First, trying to set it so that the dragable divs are automatically inserted into the holders upon startup. I’ve tried different approaches for this, however, unsuccessful…

    Second, my dragable divs contain images, and I am having the darnest time trying to get the images to position themselves correctly. Can anyone help with this? The holder divs position themselves perfectly, however the dragable divs do not. I’ve set the left and top parameters of each image, and the div itself using styles, still can’t get it to position properly.

    Can someone please help me?

  20. @David

    //this is where we’ll save the initial spot of the image
    var position = drag.getPosition();

    Where does “position” ever actually get used? I don’t see that the variable is ever referenced again.

  21. @AppBeacon: Good catch. Must have been a small snippet left from the testing process. Removed.

  22. hey guys is there a way to save the locations in a databse?

    like when your finisht placing the squares you press save?

  23. @Mike : When you drag and drop, the holder gets marked as rel=”filled” . Just create a save button, set an event on the save button to scan for all elements with ref=”filled”. Then, use child selector to get the id of the dragged element inside each droppable. Then, serialize that and submit via AJAX to a web page that saves the values in the db.

  24. wow…lol you lost me..is there any version i can download?i need to see it to understand..i guess im so used to using php and mysql..

  25. @mike

    You could also drop an xhr request into the onDrop function. I use this code, not sure if it fits perfectly into Mr. Walsh’s here, but I’m sure you’ll get the idea:

    onDrop: function(el,droppable) {
    var req = new Request.HTML({
    method: ‘get’,
    url: ‘some_script.php’,
    data: { ‘droppable’ : droppable.get(‘rel’), ‘draggable’ : el.get(‘rel’) },
    update: $$(‘billing_items_content’),
    }).send();
    }

    I use different names in my data variables, but you can send information about the droppable and draggable elements to the server script.

  26. i still dont see were i would put this code?and why function(el,droppable)

  27. @mike

    Sounds like you need to go through the mootorial, or at least get a bit more familiar with mootools.

    I said to put in the onDrop funciton, scroll up to David’s code and you’ll see an “onDrop” function. That’s where :)

    el = the element being dragged

    droppable = the element receiving

    You’d probably need those to give the database anything worthwhile.

    I knew nothing about javascript less than a year ago but with mootools I’m able to do all sorts of crap I once thought impossible. It’s just too easy to pick up.

  28. it’s all good thxt i will play arround with it!

  29. Hi David! How can I get the position of the dragable object? I need them to put into a database… Can you help me?
    By the way: your site is perfect =)

    Thanks :D

  30. vanquybn January 2, 2010

    can you help me convert this file to mootools 1.2.x
    please help me. thanks!.

    var src_collap_1 = tmplurl + “/images/icon-min.gif”;
    var src_collap_2 = tmplurl + “/images/icon-max.gif”;
    new Asset.images ([src_collap_1, src_collap_2]);
    function testfunc () {
    alert(‘fdsf’);
    }
    var JADDModules = new Class({

    options: {
    handles: false,
    containers: false,
    onStart: Class.empty,
    onComplete: Class.empty,
    ghost: true,
    snap: 3,
    title: ‘h3′,
    src_collap_1:”,
    src_collap_2:”,

    onDragStart: function(element, ghost){
    ghost.setStyles({‘opacity’:0.7, ‘z-index’:100});
    element.getChildren().setStyles({‘opacity’:0.3, ‘z-index’:1});
    element.addClass(‘moving’);
    },
    onDragComplete: function(element, ghost){
    element.getChildren().setStyle(‘opacity’, 1);
    element.removeClass(‘moving’);
    ghost.remove();
    this.trash.remove();
    }
    },

    initialize: function(lists, options){
    //console.log(encodeURIComponent(“h%E1%BB%93ng%20c%C3%B4ng”));
    this.setOptions(options);
    this.lists = $$(lists);
    this.lists.sort(function(a,b){return a.getCoordinates().left – b.getCoordinates().left;});
    this.elements = [];
    this.handles = [];

    /* Get cookies */
    this.hc = ”;
    if(Cookie.get(‘ja-ordercolumn’)){
    this.hc = Cookie.get(‘ja-ordercolumn’).split(‘,’);
    if(this.hc!=”){
    this.hc.each(function(cc,k){
    this.hc[k] = this.hc[k].split(“_”);
    },this);
    }
    }

    this.lists.each(function(list){
    var elements = list.getChildren();
    elements.each(function(el,i){
    el._p = list;
    if(this.options.title){
    el._h = $E(this.options.title, el);
    if (!el._h) return;
    tmp = el._h.getParent();
    el._h.remove();
    tmp.innerHTML = “”+tmp.innerHTML+”";
    el._innerdiv = tmp.getFirst();
    el._h.injectTop (tmp);
    //el._h._el = el;

    el._pos = i;

    if(this.hc){
    //element property: position, container, …
    this.hc.each(function(val){
    if (val[1] == el._h.innerHTML.trim().substr(0,20)) {
    el._p = $(val[0]);
    el._pos = parseInt(val[2]);
    el._h.className = val[3];
    }
    },this);
    }

    if(el._h.className==”hide”){
    src_collap = this.options.src_collap_1;
    }
    else{
    el._h.className = ‘show’;
    src_collap = this.options.src_collap_2;
    }

    //create handler for moveable and collapsible hotspot
    divmd = new Element(‘span’,{‘class’:'ja-mdtool’});
    divmd.inject(el._h);
    chdl = new Element(‘img’,{‘src’:src_collap});
    chdl.setStyle(‘cursor’,'pointer’);
    chdl.inject (divmd);
    el._h._chdl = chdl;

    this.handles.push(el._h);
    }
    },this);

    this.elements.merge(elements);

    this.lists.setStyle(‘visibility’,'visible’);

    },this);

    this.elements.each (function (el){

    el.remove();
    p = $(el._p);
    if(!p) return ;
    tmp = p.getChildren().length > el._pos ? p.getChildren()[el._pos]:null;
    if (tmp) {
    if (tmp._pos > el._pos) el.injectBefore(tmp);
    else el.injectAfter(tmp);
    }
    else el.inject (p);

    });

    //this.elements = $$(list);
    this.handles = (this.options.handles) ? $$(this.options.handles) : (this.handles.length?this.handles:this.elements);
    //this.handles.setStyle(‘cursor’, ‘move’);
    this.bound = {
    ‘start’: [],
    ‘moveGhost’: this.moveGhost.bindWithEvent(this)
    };
    for (var i = 0, l = this.handles.length; i < l; i++){
    this.bound.start[i] = this.start.bindWithEvent(this, this.elements[i]);
    }

    this.attach();
    this.collap();

    if (this.options.initialize) this.options.initialize.call(this);
    this.bound.move = this.move.bindWithEvent(this);
    this.bound.end = this.end.bind(this);

    if (window.opera) window.addEvent("unload", this.saveCookies.bind(this));
    else window.addEvent("beforeunload", this.saveCookies.bind(this));

    },

    collap: function(){
    this.lists.each(function(list){
    var elements = list.getChildren();
    elements.each(function(el,i){
    /* For collap */

    el.elmain = $E('.ja-mod-content',el);
    if(!el._h) return;

    el.maxH = el.elmain.getStyle('height').toInt();

    el.elmain.setStyle ('overflow','hidden');

    el._h._chdl.addEvent('mousedown', function(e){
    e = new Event(e).stop();
    });
    el._h._chdl.addEvent('click', function(e){
    e = new Event(e).stop();
    el.toggle();
    });

    el.toggle = function(){
    if (el._h.className=='hide'){
    el.show();
    }
    else el.hide();
    }

    el.show = function() {
    el._h.className='show';
    el._h._chdl.src = src_collap_2;
    new Fx.Style(el.elmain,'height',{onComplete:el.toggleStatuss}).start(el.elmain.offsetHeight,el.elmain.scrollHeight);
    }
    el.hide = function() {
    el._h.className='hide';
    el._h._chdl.src = src_collap_1;
    new Fx.Style(el.elmain,'height',{onComplete:el.toggleStatuss}).start(el.elmain.offsetHeight,0);
    }
    el.toggleStatuss = function () {
    el._status=(el._status=='hide')?'show':'hide';
    }

    if(el._h.className=='hide') {
    el.hide();
    }
    else{
    el.show();
    }

    }, this);
    }, this);
    },

    attach: function(){
    this.handles.each(function(handle, i){
    //handle.addEvent('mousedown', this.bound.start[i]);
    handle.addEvent('mousedown', this.bound.start[i]);
    handle.setStyle('cursor','move');
    }, this);
    },

    detach: function(){
    this.handles.each(function(handle, i){
    handle.removeEvent('mousedown', this.bound.start[i]);
    }, this);
    },

    start: function(event, el){
    this.active = el;
    //this.coordinates = this.list.getCoordinates();
    if (this.options.ghost){
    this.previous = 0;
    var position = el.getPosition();
    this.offsetX = event.page.x – position.x;
    this.offsetY = event.page.y – position.y;
    this.trash = new Element('div').inject(document.body);

    this.ghost = el.clone().inject(this.trash).setStyles({
    'position': 'absolute',
    'left': event.page.x – this.offsetX,
    'top': event.page.y – this.offsetY,
    'background':'url(../images/box-br.gif) no-repeat bottom right #444444',
    'width': el.offsetWidth
    });

    document.addListener('mousemove', this.bound.moveGhost);
    this.fireEvent('onDragStart', [el, this.ghost]);
    }
    document.addListener('mousemove', this.bound.move);
    document.addListener('mouseup', this.bound.end);
    this.fireEvent('onStart', el);
    event.stop();
    },

    moveGhost: function(event){
    this.ghost.setStyles({'left': event.page.x-this.offsetX,
    'top': event.page.y-this.offsetY
    });
    event.stop();
    },

    move: function(event){
    var cor = this.active.getCoordinates();
    if(cor.left < event.page.x && event.page.x cor.top && event.page.y list.getCoordinates().left) clist = list;
    }, this);

    if(clist == this.active._p) {
    var now = event.page.y;
    this.previous = this.previous || now;
    var up = ((this.previous – now) > 0);
    var prev = this.active.getPrevious();
    var next = this.active.getNext();
    if (prev && up && now next._h.getCoordinates().top) this.active.injectAfter(next);
    this.previous = now;
    }else{
    var now = event.page.y;

    //Get correct position
    var els = clist.getChildren();
    if(els.length) {
    var cel = els[0];
    els.each(function(el, idx){
    if (now > el._h.getCoordinates().bottom)
    {
    if(idx < els.length – 1) cel = els[idx+1];
    else cel = null;
    }
    },this);

    if(cel) this.active.injectBefore(cel);
    else this.active.inject(clist);
    } else {
    this.active.inject(clist);
    }
    this.active._p = clist;
    this.previous = now;
    }

    },

    serialize: function(converter){
    return this.list.getChildren().map(converter || function(el){
    return this.elements.indexOf(el);
    }, this);
    },

    end: function(){
    this.previous = null;
    document.removeListener('mousemove', this.bound.move);
    document.removeListener('mouseup', this.bound.end);
    if (this.options.ghost){
    document.removeListener('mousemove', this.bound.moveGhost);
    this.fireEvent('onDragComplete', [this.active, this.ghost]);
    }
    this.fireEvent('onComplete', this.active);
    },

    saveCookies: function() {
    if (Cookie.get("ja-ordercolumn") == '-') {
    Cookie.set("ja-ordercolumn", '',{path:'/'});
    return;
    }
    c = '';
    this.lists.each(function(list){
    var elements = list.getChildren();
    if (!elements){
    return;
    }
    elements.each(function(el,i){
    c += el._p.id + "_" + el._h.innerHTML.trim().substr(0,20) + "_" + i + "_" + el._h.className+",";
    },this);
    },this);
    if (this.hc) {
    this.hc.each(function(value, k){
    if (!c.test ('_' + value[1] + '_')) {
    c += value[0] + "_" + value[1] + "_" + value[2] + "_" + value[3]+",";
    }
    },this);
    }
    c = c.substr(0, (c.length-1));
    Cookie.set("ja-ordercolumn", c, {duration: 365,path:'/'});
    }
    });

    document.write('.ja-movable-container{visibility: hidden;}’);
    window.addEvent(‘load’, function(){
    new JADDModules($$(“.ja-movable-container”), {src_collap_1:src_collap_1, src_collap_2:src_collap_2});
    });

    JADDModules.implement(new Events, new Options);

    JAResizer = new Class({
    initialize: function(els, options){
    this.options = Object.extend({
    min: 100,
    max: 0
    }, options || {});
    $$(els).each(function(el){
    el.onmouseover = function(){
    this.addClass(‘ja-colresizehover’);
    };

    resizemouseout = function(){
    //console.log(‘call mouse out event for ‘ + this);
    this.removeClass(‘ja-colresizehover’);
    }
    el.onmouseout = resizemouseout;

    var prev = el.getPrevious();
    var next = el.getNext();
    prev.makeResizableNew ({handle: el, modifiers:{y:false}, limit:{width:[100]}});
    next.makeResizableNew ({handle: el, modifiers:{y:false}, dir: -1, limit:{width:[100]}});
    var eld = el.makeDraggable({modifiers:{y:false}});

    eld.addEvent(‘onStart’, function (el) {
    //console.log(‘Remove mouse out for ‘ + eld.element);
    el.onmouseout = null;
    this._next = el.getNext();
    this._prev = el.getPrevious();
    this._w = this._prev.getStyle(‘width’).toFloat() + this._next.getStyle(‘width’).toFloat();
    });

    eld.addEvent(‘onComplete’, function (el) {
    el.onmouseout = resizemouseout;

    var w1 = this._prev.offsetWidth;
    var w2 = this._next.offsetWidth;
    var w1p = w1/(w1+w2)*this._w;
    var w2p = w2/(w1+w2)*this._w;
    var elwp = el.offsetLeft * this._w / (w1+w2);
    this._prev.setStyle(‘width’, w1p + ‘%’);
    this._next.setStyle(‘width’, w2p + ‘%’);
    el.setStyle (‘left’, elwp + ‘%’);

    el = el.getCoordinates(this.options.overflown);
    var now = this.mouse.now;
    if (!(now.x > el.left && now.x < el.right && now.y el.top)) resizemouseout.call(this.element);

    });
    }.bind( this));
    }
    });

    Drag.Resize = Drag.Base.extend({

    options: {
    dir: 1
    },

    initialize: function(el, options){
    this.setOptions(options);
    this.parent(el);
    },

    start: function(event){
    this.fireEvent(‘onBeforeStart’, this.element);
    this.mouse.start = event.page;
    var limit = this.options.limit;
    this.limit = {‘x’: [], ‘y’: []};
    for (var z in this.options.modifiers){
    if (!this.options.modifiers[z]) continue;
    this.value.now[z] = this.element.getCoordinates()[this.options.modifiers[z]].toInt();
    this.mouse.pos[z] = event.page[z] – this.value.now[z]*this.options.dir;
    if (limit && limit[z]){
    for (var i = 0; i this.limit[z][1])){
    this.value.now[z] = this.limit[z][1];
    this.out = true;
    } else if ($chk(this.limit[z][0]) && (this.value.now[z] < this.limit[z][0])){
    this.value.now[z] = this.limit[z][0];
    this.out = true;
    }
    }
    if (this.options.grid[z]) this.value.now[z] -= (this.value.now[z] % this.options.grid[z]);
    this.element.setStyle(this.options.modifiers[z], this.value.now[z] + this.options.unit);
    }
    this.fireEvent('onDrag', this.element);
    event.stop();
    }

    });

    /*
    Class: Element
    Custom class to allow all of its methods to be used with any DOM element via the dollar function .
    */

    Element.extend({

    /*
    Property: makeDraggable
    Makes an element draggable with the supplied options.

    Arguments:
    options – see and for acceptable options.
    */

    makeResizableNew: function(options){
    return new Drag.Resize(this, $merge({modifiers: {x: ‘width’, y: ‘height’}}, options));
    }

    });

    This works in mootools 1.12, but not work in 1.2.x, please help me

  31. LoL vanquybn why don’t you post your whole website and David can sort out all your errors.

    By the way for those wanting the ghost drag thing check out this site

    http://www.monkeyphysics.com/mootools/script/1/dragghost

  32. Hi David,

    Excellent example. In my use case i have the elements which are draggable as well as droppable. so i am having the same class for all the elements.

    When we drag an element from top to bottom in forward direction, it’s working fine. In the OnEnter method of Drag.Move, we can find out both draggable element and the entered droppable element.

    But the reverse is not happening. Means when we drag any element in reverse direction that is from bottom to top, in the onEnter method i am getting the same element for both draggable and droppable.

    i am using css position relative for the div’s.

    Can someone help me how to handle this.

    Thanks
    Visu

  33. Will this work with jQuery?

  34. I just wanted to say thanks for the IE hack in the code above!


    document.ondragstart = function () { return false; }; //IE drag hack

    I wanted to make the draggables have an anchor element only so that the user would be able to use TAB to step to the draggable for keyboard accessibility.


    That works fine in FF but not in IE7. Applying the above hack solves the problem nicely!

    Thanks!

    /Arvind

  35. GREAT Site! You are one of the first sites I read every morning I get going.

    Quick Question: Can you force an item to be in a droppable location on page load … if for instance I wanted to load the selections a person made previously via a recordset … and drop their selections where they ended up. Is there a way to do that?

    Thanks!

    - Patrick

Be Heard

Tip: Wrap your code in <pre> tags or link to a GitHub Gist!

Use Code Editor
Older
Implementing Basic and Fancy Show/Hide in MooTools 1.2
Newer
Flashy FAQs Using MooTools Sliders