2

In my application I have a div whose CSS zoom can be varied.

Unfortunately this messes with the coordinate space. When over the zoomable area 1 page px != 1px any more :)

When it comes to the droppables, it completely breaks the intersection detection.

I'm looking for hints as to where I should start to try to handle this?

I have made an demo here.

Fiddle:

http://jsfiddle.net/crRvp/1/

HTML:

<div id="dse">
    <div id="ds">Drop here!</div>
</div>
<p>
<div id="widget">Drag me!</div>
</p>

CSS:

#dse{
    position:relative;
    width:640px;
    height:360px;
    zoom:0.75;
    -moz-transform: scale(0.75);
    -moz-transform-origin:0 0;
    -o-transform: scale(0.75);
    -o-transform-origin:0 0;
}

#ds{
    position:relative;
    zoom:1;
    width:640px;
    height:360px;
    border: solid 1px black;
    font-size:2em;
}

#widget{
    position:relative;
    width:150px;
    height:50px;
    border: solid 1px black;
}

.droppableActive{
    background-color:red;
}

.droppableHover{
    background-color:green;
}

JS:

$("#widget").draggable({
    opacity     : 0.7,
    scroll      : false,
    revert: function(dropped){
          var wasDropped = dropped && dropped[0].id == "ds";
          if(!wasDropped){$(this).animate({ top: 0, left: 0 }, 'slow');}
          return !wasDropped;
    }
    ,
    stop: function(evt, ui) {

        var canvasLeft = $('#ds').offset().left;
        var canvasTop = $('#ds').offset().top;

        alert('ui.position.left: ' + (ui.position.left) + ' canvasLeft: ' + canvasLeft);
        alert('ui.position.top: ' + (ui.position.top) + ' canvasTop: ' + canvasTop);
    }
});

$("#ds").droppable({
    activeClass: 'droppableActive',
    hoverClass: 'droppableHover',
    accept: "#widget",
    drop: function(evt, ui){
        id = ui.draggable.attr('id');
        alert('id: ' + id);
    }
});
Igor Alemasow
  • 4,484
  • 2
  • 27
  • 24
gibsonneke
  • 21
  • 2

1 Answers1

1

I had the same problem, but only in my case, both: droppable and draggable items were in zoomed container(with zoom css property!).

Draggable easily can be fixed with 'dragFix' fix:

var zoomScale = parseFloat($('.container').css('zoom'));

$('.drag__item').draggable({            
        drag: function (event, ui) {
            var changeLeft = ui.position.left - ui.originalPosition.left;
            var newLeft = ui.originalPosition.left + changeLeft / zoomScale;
            var changeTop = ui.position.top - ui.originalPosition.top;
            var newTop = ui.originalPosition.top + changeTop / zoomScale;

            ui.position.left = newLeft;
            ui.position.top = newTop;
        }
});

To fix droppable we need to edit jquery-ui code. Considering that editing of library source code is not good idea, lets override intersect method. The problem is that x1 and y1 coordinates(top, left of draggable item) are breaked after previous 'dragFix' fix, therefore, intersection detection doesn't work as expected. So we need to normalize x1 and y1 coordinates.

var intersect = $.ui.intersect = (function () {
    function isOverAxis(x, reference, size) {
        return (x >= reference) && (x < (reference + size));
    }

    return function (draggable, droppable, toleranceMode, event) {
        if (!droppable.offset) {
            return false;
        }

        var x1 = draggable.offset.left + draggable.position.left - draggable.originalPosition.left + draggable.margins.left,//here is the fix for scaled container
            y1 = draggable.offset.top + draggable.position.top - draggable.originalPosition.top + draggable.margins.top,//here is the fix for scaled container
            x2 = x1 + draggable.helperProportions.width,
            y2 = y1 + draggable.helperProportions.height,
            l = droppable.offset.left,
            t = droppable.offset.top,
            r = l + droppable.proportions().width,
            b = t + droppable.proportions().height;

        switch (toleranceMode) {
            case 'fit':
                return (l <= x1 && x2 <= r && t <= y1 && y2 <= b);
            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
            case 'pointer':
                return isOverAxis(event.pageY, t, droppable.proportions().height) &&
                    isOverAxis(event.pageX, l, droppable.proportions().width);
            case 'touch':
                return (
                    (y1 >= t && y1 <= b) || // Top edge touching
                    (y2 >= t && y2 <= b) || // Bottom edge touching
                    (y1 < t && y2 > b) // Surrounded vertically
                ) && (
                    (x1 >= l && x1 <= r) || // Left edge touching
                    (x2 >= l && x2 <= r) || // Right edge touching
                    (x1 < l && x2 > r) // Surrounded horizontally
                );
            default:
                return false;
        }
    };
})();

From jquery 1.12 $.ui.intersect function is scoped so that it cannot be directly modified afterwards. It is called in $.ui.ddmanager as a local variable, so even if you modify $.ui.intersect, it won't be used. Customizing it is a bit more complex. You can do it this way, basically you rescope intersect and then redefine drag and drop method on $.ui.ddmanager so that it calls the modified intersect:

from this answer

$.ui.ddmanager.drag = function (draggable, event) {
    // If you have a highly dynamic page, you might try this option. It renders positions
    // every time you move the mouse.
    if (draggable.options.refreshPositions) {
        $.ui.ddmanager.prepareOffsets(draggable, event);
    }

    // Run through all droppables and check their positions based on specific tolerance options
    $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function () {

        if (this.options.disabled || this.greedyChild || !this.visible) {
            return;
        }

        var parentInstance, scope, parent,
            intersects = intersect(draggable, this, this.options.tolerance, event),
            c = !intersects && this.isover ?
                'isout' :
                (intersects && !this.isover ? 'isover' : null);
        if (!c) {
            return;
        }

        if (this.options.greedy) {

            // find droppable parents with same scope
            scope = this.options.scope;
            parent = this.element.parents(':data(ui-droppable)').filter(function () {
                return $(this).droppable('instance').options.scope === scope;
            });

            if (parent.length) {
                parentInstance = $(parent[0]).droppable('instance');
                parentInstance.greedyChild = (c === 'isover');
            }
        }

        // We just moved into a greedy child
        if (parentInstance && c === 'isover') {
            parentInstance.isover = false;
            parentInstance.isout = true;
            parentInstance._out.call(parentInstance, event);
        }

        this[c] = true;
        this[c === 'isout' ? 'isover' : 'isout'] = false;
        this[c === 'isover' ? '_over' : '_out'].call(this, event);

        // We just moved out of a greedy child
        if (parentInstance && c === 'isout') {
            parentInstance.isout = false;
            parentInstance.isover = true;
            parentInstance._over.call(parentInstance, event);
        }
    });

}

$.ui.ddmanager.drop = function (draggable, event) {
    var dropped = false;

    // Create a copy of the droppables in case the list changes during the drop (#9116)
    $.each(($.ui.ddmanager.droppables[draggable.options.scope] || []).slice(), function () {

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

        if (!this.options.disabled && this.visible && this.accept.call(this.element[0],
            (draggable.currentItem || draggable.element))) {
            this.isout = true;
            this.isover = false;
            this._deactivate.call(this, event);
        }

    });
    return dropped;
}

JSFiddle example

Igor Alemasow
  • 4,484
  • 2
  • 27
  • 24