7

I am trying to create multiple jquery droppables next to eachother where some parts might overlap, in these cases I want the one that is on top (z-index wise) to be greedy.

I have tried setting the greedy: true option in the droppable, but this does not seem to help. I have also tried to return false on the drop event and used event.stopPropagation();.

Here is a jsfiddle based on the demo page of jquery.

Is there any way to stop the drop event from propagating if there is another droppable triggering it, preferably the one that has the highest z-index?

jeffreydev
  • 1,722
  • 13
  • 31

5 Answers5

7

Use document.elementFromPoint to check the element directly under the cursor. If it’s not your droppable, don’t do anything. Add the following to the top of your drop: handler:

var droppable = $(this);
ui.helper.css({pointerEvents: 'none'});
var onto = document.elementFromPoint(event.clientX, event.clientY);
ui.helper.css({pointerEvents: ''});
if(!droppable.is(onto) && droppable.has(onto).length === 0) {
    return;
}

Disabling pointer events on the helper is necessary for document.elementFromPoint to ignore the thing your dragging and only checking underneath. I’ve updated your jsFiddle for a quick demonstration. Note that the highlight still affects both elements. You should be able to change that by not letting jQuery UI do the highlighting but instead write it yourself on the draggables drag: event. I have, however, found this to be unusable in practice as this check (and disabling pointer events) is quite slow and may also cause flickering.

Raphael Schweikert
  • 18,244
  • 6
  • 55
  • 75
  • nice solution! looked for about 3 hours of different approaches with z-indexes and so on, but this is just simple and effective! – con Jun 19 '13 at 09:19
2

You need a function to check if the element is the highest visible element. Something is the highest visible when

  1. The item is later in the dom list
  2. It has a higher z-index set

The below function can be used to see if the current element (either in over method or drop method in the droppable setting) is in fact the highest visible element.

function isHighestVisible(cEl) {
    cEl = $(this); //use this if you want to use it in the over method for draggable, else just pass it to the method

    //get all active droppable elements, based on the classes you provided
    var $list = $('.ui-widget-header.ui-state-active');
    var listCount = $list.length;
    var HighestEl = null;
    var HighestZI = -1;


    //one element is always highest.
    if (listCount<=1) {
        console.log(cEl.attr('id'));
        return true;
    }

    //check each element
    $list.each(function(i) {
        var $el = $(this);

        var id = $el.attr('id');

        //try to parse the z-index. If its 'auto', return 0.

        var zi = isNaN(parseInt($el.css('z-index'))) ? 0 : parseInt($el.css('z-index'));

        if (zi>0) {
            //z-index is set, use it.
            //I add the listCount to prevent errors when a low z-index is used (eg z-index=1) and there are multiple elements.
            //Adding the listCount assures that elements with a z-index are always later in the list then elements without it.
            zi = zi + listCount;
        } else {
            //z-index is not set, check for z-index on parents
            $el.parents().each(function() {
                $par = $(this);
                var ziParent = isNaN(parseInt($par.css('z-index'))) ? 0 : parseInt($par.css('z-index'));
                if (ziParent>0) {
                    zi = ziParent;
                    return false;
                }
            });
        }

        //add the current index of the element to the zi
        zi += i;

        //if the calculated z-index is highest, change the highest element.
        if (HighestEl==null || HighestZI<zi) {
            HighestEl=$el;
            HighestZI = zi;
        }
    });

    //if the highest found active element, return true;
    if (HighestEl.attr('id')==cEl.attr('id')) {
        console.log(cEl.attr('id'));
        return true;
    }

    //if all else fails, return false
    return false;
}
Hugo Delsing
  • 13,803
  • 5
  • 45
  • 72
  • 1
    This looks very promising. I didn’t yet have a chance to try it out but the principle is sound. However, please note, that valid z-indices can also be negative and that every positioned element (any value other than `static`) opens up its own stacking context which means you would have to traverse the parents even if a valid z-index is found. – Raphael Schweikert Nov 27 '12 at 17:41
  • Thanks for the upvote and the comment. I did forget the negative z-index options. I didnt even know about the second part you mentioned. I rarely use z-index multiple times on a single page, guess i never encountered it. – Hugo Delsing Nov 27 '12 at 22:05
0

If i had this issue i would use jQuery's .addclass

.droppablewidgetforce {
z-index: 1000 !important; 
}

make a class as such so the layer stays on top no matter what.. this should fix the issue.

  • Maybe I misunderstand your answer, but the problem is not in the z-index. The problem is that the event is triggered on both the `#droppable3` element aswell as on the `droppable3-inner` element while it should only be triggered on the `droppable3-inner` while dragging it on both elements. I'm not sure how setting the droppables z-index to 1000 would help me with that. – jeffreydev Oct 04 '12 at 09:44
  • take out droppable3 from the selector list and make new function with that and desired parameters – Michael O'Brien Oct 04 '12 at 09:55
  • This only places the droppable above the draggable, which is not what I desire to do at all, perhaps you can show me in a jsfiddle what you mean? – jeffreydev Oct 04 '12 at 11:07
0

The greedy options is just for nested elements that have a droppable parent.

In your code the 2 droppable elements are siblings so the greedy option will not work:

<div id="droppable3" class="ui-widget-header">
    <p>Outer droppable</p>
</div>
<div id="droppable3-inner" class="ui-widget-header">
    <p>Inner droppable (greedy)</p>
</div>

Here is a messy workaround if you still want to use siblings instead of parents and children.

Live code example.

function normalDraggedOver() {
    $( this ).addClass( "ui-state-highlight" )
             .find( "> p" )
             .html( "Droppeeeed!" );
}

var options = {
    activeClass: "ui-state-hover",
    hoverClass: "ui-state-active",
    greedy: true,
    drop: normalDraggedOver
};

var options2 = {
    activeClass: "ui-state-hover",
    hoverClass: "ui-state-active",
    disabledClass: "ui-state-disabled",
    hoverStateClass: "ui-state-hover",
    greedy: true,
    greedyWithSibligs: true,
    drop: normalDraggedOver,
    over: function () {
    /* if we have previous siblings of the inner element we handle them as parents */
        if(options2.greedyWithSibligs && $(this).prev().length > 0) {       
            $(this).siblings().slice(0, $(this).index())
            .removeClass(options2.hoverClass)
            .droppable('disable')
            .removeClass(options2.disabledClass)
            .removeClass(options2.hoverStateClass);
        }
    },
    out: function () {
    /* all the siblings of the inner element that were handled as parents act as one*/
        if(options2.greedyWithSibligs && $(this).prev().length > 0) {
            $(this).siblings().slice(0, $(this).index())
            .droppable('enable')
            .addClass(options2.hoverClass);
            console.log($(this).attr('id'));
        }
    }
};
AlexandruSerban
  • 312
  • 2
  • 9
  • Well, I’m still looking for an even more general solution: the droppables should not have to be siblings. I just want absolutely-positioned droppables to respect the `z-index` so when dropping onto an element that visually lies in front of another (that is also droppable), the drop should only register on the front element. – Raphael Schweikert Nov 27 '12 at 13:04
  • The problem with z-index is that it will always have the value 'auto' if you didn't set it yourself. – AlexandruSerban Nov 27 '12 at 13:13
0

If just want it to look the same you can set the #droppable3 to position: relative and set the child #droppable3-inner to position: absolute.

HTML:

<div id="droppable3" class="ui-widget-header">
    <p>Outer droppable</p>
</div>
<div id="droppable3-inner" class="ui-widget-header">
    <p>Inner droppable (greedy)</p>
</div>

Here is the live example

AlexandruSerban
  • 312
  • 2
  • 9
  • This example would require for me to actually move the droppable3-inner element into the droppable3 element, which I would like to avoid. – jeffreydev Jan 15 '13 at 13:47