Naftali
Naftali

Reputation: 146302

Only drop to element that is seen

I have the following:

I am trying to set it up so that when you drag the item, it only gets dropped to the div element which you can see, and is not covered up.

So I used this js:

$(".draggable").draggable({
    helper: "clone"
})
$("#bottom, .draggable").droppable({
    drop: function(event, ui) {
        var $this = $(this),
            $dragged = $(ui.draggable);
        $this.append($dragged.clone());
    },
    hoverClass: "dragHover"
})​

But it drops the element in both places even though only one of the drop zones is not visible!

How do I fix it so that this does not happen?

Fiddle: http://jsfiddle.net/maniator/Wp4LU/


Extra Info to recreate the page without the fiddle:

HTML:

  <div id="top">
    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>

    <div class="draggable">
      Lorem ipsum dolor sit amet
    </div>
  </div>

  <div id="bottom"></div>

CSS:

.draggable {
    border: 1px solid green;
    background: white;
    padding: 5px;
}

.dragHover{
    background: blue;
}

#top {
    height: 500px;
    overflow-y: scroll;
}
#bottom {
    height: 150px;
    overflow-y: scroll;
    border: red solid 4px;
}

Upvotes: 7

Views: 1388

Answers (4)

TecHunter
TecHunter

Reputation: 6131

If you just want to drop on element you can see you could change your selector :

$(".draggable:visible").draggable({
    helper: "clone"
});
$("#bottom, .draggable:visible").droppable({
    drop: function(event, ui) {
        var $this = $(this),
            $dragged = $(ui.draggable);
        $this.append($dragged.clone());
    },
    hoverClass: "dragHover"
})​;

Or when you hide an element change its' draggable class by something else.

Upvotes: 0

xdazz
xdazz

Reputation: 160833

Try setting with accept function. The working demo.

$("#bottom, .draggable").droppable({
    drop: function(event, ui) {
        var $this = $(this),
            $dragged = $(ui.draggable);
        $this.append($dragged.clone());
    },
    accept: function () {
        var $this = $(this), divTop= $("#top");
        if ($this.is(".draggable")) {
          return $this.offset().top < divTop.offset().top + divTop.height() ;
        }
        return true;
    },
    hoverClass: "dragHover"
})​;​

Upvotes: 2

mgibsonbr
mgibsonbr

Reputation: 22007

According to the sources (jquery.ui.droppable.js), the drop operation will search for every eligible droppable and apply the drop function to every one that intersects with it:

drop: function(draggable, event) {

    var dropped = false;
    $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() {

        if(!this.options) return;
        if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance))
            dropped = this._drop.call(this, event) || dropped;

(Old versions had the last "OR" condition reversed, so it would only apply to a single droppable. Try your fiddle using jQuery 1.5.2 / jQuery UI 1.8.9, and see that it only drops to one element, albeit the "wrong" one...)

And every tolerance mode currently implemented in the $.ui.intersect function only take into account the (x,y) coordinates:

switch (toleranceMode) {
    case 'fit':
        return (l <= x1 && x2 <= r
            && t <= y1 && y2 <= b);
        break;
    case 'intersect':
        return (l < x1 + (draggable.helperProportions.width / 2) // Right Half
            && x2 - (draggable.helperProportions.width / 2) < r // Left Half
            && t < y1 + (draggable.helperProportions.height / 2) // Bottom Half
            && y2 - (draggable.helperProportions.height / 2) < b ); // Top Half
        break;
    ...

So, unless someone adds a z-index aware tolerance mode, your only option is to work around the issue somehow. I'd suggest first adding every droppable candidate to a set and, when it's time to drop, select only the one that is "closest" to the screen:

$("#bottom, .draggable").droppable({
    over: function(event, ui) {
        if ( !ui.draggable.data("drop-candidates") )
            ui.draggable.data("drop-candidates",[]);
        ui.draggable.data("drop-candidates").push(this);
    },
    out: function(event, ui) {
        var that = this,
            candidates = ui.draggable.data("drop-candidates") || [];
        ui.draggable.data("drop-candidates", $.grep(candidates, function(e) {
            return e != that;
        });
    },
    drop: function(event, ui) {
        var $this = $(this),
            $dragged = $(ui.draggable);
        var candidates = $.data("drop-candidates").sort(closestToScreen);
        if ( candidates[0] == this )
            $this.append($dragged.clone());
    },
    hoverClass: "dragHover"
})​

Now, implementing the closestToScreen comparator is the tricky part. The W3C CSS Specification describes how rendering engines should sort elements to paint, but I wasn't able to find so far an easy way to access this information. I also asked this question here at SO, maybe someone will find a good way.


P.S. If modifying the jQuery UI source is an option, you could try implementing a z-index aware tolerance mode using document.getElementFromPoint, as this answer to said question suggested:

var x1 = (draggable.positionAbs || draggable.position.absolute).left, x2 = x1 + draggable.helperProportions.width,
    y1 = (draggable.positionAbs || draggable.position.absolute).top, y2 = y1 + draggable.helperProportions.height;
switch (toleranceMode) {
    ...
    case 'z-index-aware':
        return document.elementFromPoint(x1,y1) == droppable;
        break;

(that would ensure only the element right below the upper-left corner of the draggable would be considered "good enough" as a drop target - not ideal, but better than what we have so far; a similar solution could be adapted to use the mouse pointer coordinates instead)

And, no, you can't use this method with the workaround presented before: at the moment the drop happens, the drag helper is the element closest to the screen... (Edit: d'oh! It wouldn't work if implemented as a tolerance mode either, for the same reason...)

Upvotes: 1

krasu
krasu

Reputation: 2037

If I got you right - this one should solve your problem - http://jsfiddle.net/Wp4LU/60/ Also you could write custom accept function - http://jqueryui.com/demos/droppable/#option-accept

Code:

var draggableList = $('#top');

$(".draggable").draggable({
    helper: "clone"
});
$("#bottom, .draggable").droppable({
    drop: function(event, ui) {
        var $this = $(this),
            $dragged = $(ui.draggable);

        if ($this.hasClass("draggable")) {
            if ($this.position().top >= draggableList.height() || 
                $this.position().top + $this.outerHeight() >= 
                draggableList.height()) 
                return;
        }

        $this.append($dragged.clone());
    },
    hoverClass: "dragHover"
})​;​

Upvotes: 2

Related Questions