问题
Aim
- to snap elements together or reject snapping depending on positions
- to change the element appearance depending on snap position
Sources already consulted
Jquery UI – draggable 'snap' event
Retrieving the "snapped to" element using jQuery UI draggable with snap enabled
jQuery UI draggable options snaptolerance
Visualisation Imagine a div with a connector - if I connect it to the left bottle, I need the connector to point right, if to the right bottle, the connector needs to point left.
Such a connector cannot be snapped to the top or bottom of the bottle so a top or bottom snap needs to be rejected in this case - Other cases may be vertical snapping only and again others may snap on vertical or horizontal. The most likely is either horizontal OR vertical


Example code DEMO
snapped: function(event, ui) {
var snapper = ui.snapElement.attr("id"),snapperPos = ui.snapElement.position(),
snappee = ui.helper.attr("id"), snappeePos = ui.helper.position(),
snapping = ui.snapping,
collisionTolerance = 3;
if (Math.abs(snapperPos.left-snappeePos.left)<=collisionTolerance) {
if (snapperPos.top > snappeePos.top) ui.helper.html(showPos(snapper,"top",snapperPos,snappeePos));
else ui.helper.html(showPos(snapper,"bottom",snapperPos,snappeePos));
}
else if (Math.abs(snapperPos.top-snappeePos.top)<=collisionTolerance) {
if (snapperPos.left > snappeePos.left) ui.helper.html(showPos(snapper,"left",snapperPos,snappeePos));
else ui.helper.html(showPos(snapper,"right",snapperPos,snappeePos));
}
else ui.helper.html(showPos(snapper,"corner",snapperPos,snappeePos));
},
Questions
- How do I reliably detect I have really snapped to the exact left, right, top or bottom? - to see what I mean, snap on a bias and the boxes are snapped but the script says corner
- How do I reject a snap/mark a position non-snappable?
I need to change the image in the dragged div to point in the correct direction when it gets near a snap position so it would be great if you had a suggestion to the math that would show this. As you can see, the 3 pixels where not enough - I am considering half the width or height of the dragged element. The math is important.
回答1:
Update: In light of the comments below, this calls for a more generic solution than the one in my original answer.
As discussed in the comments, the snapped
event is not triggered again on drag if the snapped element remains the same, even when the edge the helper element is snapped to changes (that was by design in my original implementation).
To overcome this limitation, edge detection has to be performed in the drag
handler, and additional snapped
events have to be triggered if the edge changes.
We can start with a utility function that computes the relative position of an element to another. The function returns human-readable strings like "top left"
or "bottom right"
. I chose to use half the element's width and height as the tolerance for corners in the code below, this can of course be modified if it is not well-suited to the shape of your draggable elements:
function computeRelativePosition($element, $reference)
{
var result = [],
vtol = $element.outerHeight() / 2,
htol = $element.outerWidth() / 2,
pos = $element.position(),
bounds = $reference.position();
$.extend(pos, {
bottom: pos.top + $element.outerHeight() - 1,
right: pos.left + $element.outerWidth() - 1
});
$.extend(bounds, {
bottom: bounds.top + $reference.outerHeight() - 1,
right: bounds.left + $reference.outerWidth() - 1
});
if (pos.top + vtol <= bounds.top) {
result.push("top");
} else if (pos.bottom - vtol >= bounds.bottom) {
result.push("bottom");
}
if (pos.left + htol <= bounds.left) {
result.push("left");
} else if (pos.right - htol >= bounds.right) {
result.push("right");
}
return result.join(" ");
}
From there, we have to use that function in our drag
handler and trigger a snapped
event if the relative position of the helper to the snapped element has changed since the last time the handler ran:
drag: function(event, ui) {
var draggable = $(this).data("draggable"); // "ui-draggable" with jQuery UI 1.10+
$.each(draggable.snapElements, function(index, element) {
ui = $.extend({}, ui, {
snapElement: $(element.item),
snapping: element.snapping
});
if (element.snapping) {
var at = computeRelativePosition(ui.helper, ui.snapElement);
if (!element.snappingKnown || at != element.at) {
element.snappingKnown = true;
element.at = ui.at = at;
draggable._trigger("snapped", event, ui);
}
} else if (element.snappingKnown) {
element.snappingKnown = false;
draggable._trigger("snapped", event, ui);
}
});
}
And finally, we can update your snapped
handler into:
snapped: function(event, ui) {
var snapper = ui.snapElement.attr("id"),
snapperPos = ui.snapElement.position(),
snappee = ui.helper.attr("id"),
snappeePos = ui.helper.position(),
snapping = ui.snapping;
if (snapping) {
ui.helper.html(showPos(snapper, ui.at, snapperPos, snappeePos));
}
}
This solution gives consistently accurate results in my tests. You will find an updated fiddle here.
Original answer follows:
It looks like you are not taking the helper element's width and height into account in your computations. The edge detection code should look like:
if (Math.abs(snapperPos.left - snappeePos.left) <= collisionTolerance) {
if (snapperPos.top + ui.snapElement.outerHeight() > snappeePos.top) {
ui.helper.html(showPos(snapper, "top", snapperPos, snappeePos));
} else {
ui.helper.html(showPos(snapper, "bottom", snapperPos, snappeePos));
}
} else if (Math.abs(snapperPos.top - snappeePos.top) <= collisionTolerance) {
if (snapperPos.left + ui.snapElement.outerWidth() > snappeePos.left) {
ui.helper.html(showPos(snapper, "left", snapperPos, snappeePos));
} else {
ui.helper.html(showPos(snapper, "right", snapperPos, snappeePos));
}
} else {
ui.helper.html(showPos(snapper, "corner", snapperPos, snappeePos));
}
The code above gives me better results, provided collisionTolerance
is set to 50
and the code only runs if snapping
is true
. You can test it in this fiddle I created from your example.
That said, I think your problem can be simplified quite a bit. Since you are only interested in horizontal edge detection, you only have to compare the left position of the helper with the left position of the snapped element. If the former is less than the latter, the helper is snapped to the left. Otherwise it is snapped to the right.
I modified the fiddle in my answer to the original question to implement this (because I am more familiar with it than with your example). The result is:
snapped: function(event, ui) {
ui.snapElement.toggleClass("ui-state-highlight ui-state-default");
var status = ui.snapElement.find(".status");
if (ui.snapping) {
if (ui.helper.position().left < ui.snapElement.position().left) {
status.text("left");
} else {
status.text("right");
}
} else {
status.text("");
}
}
You can test it here.
Regarding your second question, I'm afraid the draggable widget does not support partial (horizontal only) snapping (yet). However, maybe you can just ignore vertical snapping and keep switching the helper's image depending on its horizontal position. Apart from the "jump" that occurs when helper gets close to the top and bottom edges of an element, the code should still work fine.
来源:https://stackoverflow.com/questions/15588413/detecting-draggable-snapper-relative-position-to-the-element-it-snaps-to