6

I'm working on my first JQuery project, and i've hit a bit of a roadblock. I'm trying to allow drag & drop reordering of a set of nested lists (ul). Everything is working with the exception of the positioning. The goal is to vertically center the draggable relative to the cursor (horizontal movement is restricted), so that an li with elements nested inside it can be easily dropped. Here's the relevant JS:

$(function() {
$( ".organizerlink" ).draggable({ axis: "y",
    containment:"#organizer",
    scroll: false ,
    helper: "original",
    revert: "invalid",
    cursorAt: { top: Math.round($(this).outerHeight() / 2)}
});

and HTML:

<ul id="organizer">
<li class="organizerTarget">&nbsp</li>
<li class="organizerlink" id="dbid-1">Page
    <ul><li class="organizerTarget organizerNewParent">&nbsp;</li></ul>
</li>
<li class="organizerTarget">&nbsp</li>
<li class="organizerlink" id="dbid-2">About
    <ul>
        <li class='organizerTarget'>&nbsp;</li>
        <li class='organizerlink' id="dbid-3">Another Page<ul><li class="organizerTarget organizerNewParent">&nbsp;</li></ul></li>
        <li class='organizerTarget'>&nbsp;</li>
        <li class='organizerlink' id="dbid-4">Example<ul><li class="organizerTarget organizerNewParent">&nbsp;</li></ul></li>
    </ul>
</li>
<li class="organizerTarget">&nbsp</li>
<li class="organizerlink" id="dbid-27">Stuff
    <ul><li class="organizerTarget organizerNewParent">&nbsp;</li></ul>
</li>

Some stuff I've already tried:

  • setting cursorAt to $(this).height() - didn't work, i'm guessing height() pulls the css heights in, but they aren't defined explicitly so it jumps to 0
  • setting it to outerHeight() gives me an error "elem.style is undefined" in firebug

I know the outerHeight element exists in jquery, and based on the API doc it would appear that it can be calculated automatically, even if the CSS is undefined, so I'm thinking that is the right way to be going, and perhaps $(this) is just the wrong spot to be looking for it.

Sam
  • 1,504
  • 2
  • 13
  • 19
  • 2
    tolerance:"pointer" under the droppable function has provided me with a decent workaround. – Sam Apr 21 '11 at 23:42

6 Answers6

9

I also wanted to center my draggable objects after I pick them up. My solution:

$(".dragme").draggable({ 
    start: function(event, ui) { 
        $(this).draggable("option", "cursorAt", {
            left: Math.floor(this.clientWidth / 2),
            top: Math.floor(this.clientHeight / 2)
        }); 
    }
});
Jonas Kölker
  • 7,680
  • 3
  • 44
  • 51
Paul
  • 678
  • 10
  • 15
  • 4
    For some reason this works inconsistently for me when put in the start handler (maybe this causes a race condition in the newer versions of jQuery UI?) but works great in the create handler. – mmitchell Feb 25 '13 at 19:39
  • 3
    Yes. This is unreliable - cursorAt must be set normally (before start()). – T4NK3R Oct 17 '13 at 10:35
6

Worked around the initial problem, see the comment to my initial post.

EDIT:

Tolerance option "pointer": The mouse pointer overlaps the other item.

$(".sortable").sortable({
    containment: "parent",
    tolerance: "pointer"
});
Brent
  • 1,378
  • 2
  • 16
  • 30
Sam
  • 1,504
  • 2
  • 13
  • 19
5

This works fine if draggable have helper object. Just put it in start method of draggable as below code.

start: function(event, ui) { 
  $(this).draggable("option", "cursorAt", {
    left: Math.floor(ui.helper.width() / 2),
    top: Math.floor(ui.helper.height() / 2)
  }); 
}
Lalit Kumar Maurya
  • 5,475
  • 2
  • 35
  • 29
5

I just spent a couple of hours smashing my head against the wall to try to reliably make this work, so I thought I'd post the solution :)

Issue: Draggable cannot set cursorAt for 'clone' drags during instantiation for dynamic values (i.e. the center of a draggable proxy of variable size) because ui.helper hasn't been created yet! You can set it inside the start handler but it will not work for the first drag.

Solution: Set the cursorAt inside start, but ALSO manually override the ui.position for the drag event, which occurs continuously. Using a firstRun boolean, you can prevent it from running needlessly. You must set the default cursorAt to the top left for repositioning.

Enjoy!

$jobs.each( function( index, element ) {
    var $el = $(element),
        firstRun = true;

    /*
     * jQuery UI Draggable
     * See @link: http://api.jqueryui.com/draggable/
     *
     * Note that for clone drags, the first drag cannot center the proxy because ui.helper hasn't been created yet
     * so need to set it manually for first drag. Subsequent drags can use the cursorAt property.
     * See my @link: 
     */
    $el.draggable({
        cursorAt : [0, 0],          // Set origin then update dynamically
        drag : function( evt, ui ) { // Fires after start event, and continuously during drag
            if ( firstRun ) {
                // Reset proxy position here since cursorAt cannot be set for 
                ui.position.left -= Math.floor( ui.helper.width()/ 2 );
                ui.position.top -= Math.floor( ui.helper.height()/ 2 );
            };
        },
        helper : 'clone',
        start : function( evt, ui ) { // Fires once
            if ( firstRun ) {
                // Center proxy relative to cursor for future drags
                $(this).draggable( 'option', 'cursorAt', {
                    left : Math.floor( ui.helper.width()/ 2 ),
                    top : Math.floor( ui.helper.height()/ 2 )
                });
            };

            // Set cursor ( 'cursor' option just applies style to <body> )
            ui.helper.css( 'cursor', 'move' );
        },
        stop : function( evt, ui ) { // Fires once
            if ( firstRun ) {
                firstRun = false;
            };
        }
    });
Goz
  • 51
  • 1
  • 3
  • This answer is really really good if you take some time to read through it and apply it to your code. – JREN Mar 04 '16 at 10:43
  • Remove `firstRun` check in drag start if your draggable elements are variable widths. – Emmanuel Mar 02 '18 at 05:40
  • 1
    This firstRun stuff does not work like expected if the droppable is smaller than the draggable! Because the draggable gets the hover focus when the droppable center hovers the draggable but you expect it to hover with the cursor. The perfect solution I found [in this thread](https://stackoverflow.com/a/34830265/3655553) – MaggusK Mar 27 '18 at 14:26
1

In case of multiply draggable objects Goz's answer didn't work for me, so here is my solution without using cursorAt option

var offsetX = null;
var offsetY = null;
$(".item").draggable({
  helper:"clone",
  start:function(e,ui){
    offsetX=null;
    offsetY=null;
  },
  drag:function(e,ui){
    if(offsetX===null){
      offsetX = e.clientX-ui.offset.left;
      offsetY = e.clientY-ui.offset.top;
    }
    ui.position.left += offsetX - Math.floor( ui.helper.outerWidth()/ 2 );
    ui.position.top += offsetY - Math.floor( ui.helper.outerHeight()/ 2 );
            
  }
});
.desk{
  width:300px;
  height:300px;
  border:2px dashed gray;
}

.item {
  padding:8px;
  margin:2px;
  border:1px solid blue;
  background: rgba(0,0,80,.3);
  display:inline-block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<div class="desk">
  <div class="item">Item 1</div>
  <div class="item" style="width:100px;">Item 2</div>
  <div class="item" style="height:50px;">Item 3</div>
</div>
Yuri Gor
  • 1,353
  • 12
  • 26
1

@yuri gor:

Your answer worked for me, but you could have just inited the offset from start:

    var offsetX = null;
    var offsetY = null;
    $(".item").draggable({
      helper:"clone",
      start:function(e,ui){
        offsetX = e.clientX-ui.offset.left;
        offsetY = e.clientY-ui.offset.top;
      },
      drag:function(e,ui){
        ui.position.left += offsetX - Math.floor( ui.helper.outerWidth()/ 2 );
        ui.position.top += offsetY - Math.floor( ui.helper.outerHeight()/ 2 );   
     }
    });
Johan Velthuis
  • 280
  • 3
  • 13
  • Yes, you are right, I don't remember what was I thinking about doing such complicated initialization, maybe it's because of some another code in my app, which is out of the scope of this snippet. – Yuri Gor Apr 17 '19 at 14:01