I'm working on a portal/dashboard type interface which has panels/widgets that can be freely dragged around the dashboard space as long as they don't overlay any other panels. New panels can be added to the dashboard via a menu containing all available panels, and when a menu item is clicked, the panel is placed into the dashboard. The panels currently occupying the dashboard space are all represented in an object like this:
{
'panel_1': { top: 0, left: 0, width: 300, height: 350 },
'panel_2': { top: 0, left: 370, width: 200, height: 275 },
'panel_3': { top: 370, left: 0, width: 275, height: 400 },
...
}
My question is, what is a valid algorithm that would, when the user clicks one in the menu, correctly place a new panel (of a given width and height) in unoccupied space that is closest to a left and top (x and y) value of 0, 0, without overlapping any of the existing panels?
I think, simple bruteforce algorithm will fit you. As I remember, fit rectangle solve another problem
Iterate over your dashboard axis to find out, whether you can place your rectangle, until X < rectangle.widh + dashboard.width, same for Y.
Foreach X,Y on dashboard iterate over every panel to find whether they overlap. You can apply some optimization, to decrease amount of iteration. If panel overlap rectangle, you can increase X or Y(which is in nested loop) not by 1, but by width or height of panel.
In most cases, you will not make dashboard.width*dashboard.height*panel.count iteration. With some optimization, it will find best fit rather quick
I know this is an old question but if anyone wants a proof of concept then it looks like this:
function findSpace(width, height) {
var $ul = $('.snap-layout>ul');
var widthOfContainer = $ul.width();
var heightOfContainer = $ul.height();
var $lis = $ul.children('.setup-widget'); // The li is on the page and we dont want it to collide with itself
for (var y = 0; y < heightOfContainer - height + 1; y++) {
var heightOfShortestInRow = 1;
for (var x = 0; x < widthOfContainer - width + 1; x++) {
//console.log(x + '/' + y);
var pos = { 'left': x, 'top': y };
var $collider = $(isOverlapping($lis, pos, width, height));
if ($collider.length == 0) {
// Found a space
return pos;
}
var colliderPos = $collider.position();
// We have collided with something, there is no point testing the points within this widget so lets skip them
var newX = colliderPos.left + $collider.width() - 1; // -1 to account for the ++ in the for loop
x = newX > x ? newX : x; // Make sure that we are not some how going backwards and looping forever
var colliderBottom = colliderPos.top + $collider.height();
if (heightOfShortestInRow == 1 || colliderBottom - y < heightOfShortestInRow) {
heightOfShortestInRow = colliderBottom - y; // This isn't actually the height its just the distance from y to the bottom of the widget, y is normally at the top of the widget tho
}
}
y += heightOfShortestInRow - 1;
}
//TODO: Add the widget to the bottom
}
function isOverlapping($obsticles, tAxis, width, height) {
var t_x, t_y;
if (typeof (width) == 'undefined') {
// Existing element passed in
var $target = $(tAxis);
tAxis = $target.position();
t_x = [tAxis.left, tAxis.left + $target.outerWidth()];
t_y = [tAxis.top, tAxis.top + $target.outerHeight()];
} else {
// Coordinates and dimensions passed in
t_x = [tAxis.left, tAxis.left + width];
t_y = [tAxis.top, tAxis.top + height];
}
var overlap = false;
$obsticles.each(function () {
var $this = $(this);
var thisPos = $this.position();
var i_x = [thisPos.left, thisPos.left + $this.outerWidth()]
var i_y = [thisPos.top, thisPos.top + $this.outerHeight()];
if (t_x[0] < i_x[1] && t_x[1] > i_x[0] &&
t_y[0] < i_y[1] && t_y[1] > i_y[0]) {
overlap = this;
return false;
}
});
return overlap;
}
Related
I'm working on my Javascript project.
In this project I have to create some animations.
In this specific case, I have to make a ball bounce up and down.
The code below works great just from the top to the bottom, but not viceversa.
var div = document.getElementById('container-ball');
function createBall(event)
{
var x = event.clientX;
var y = event.clientY;
var newBall = document.createElement('div');
newBall.style.position = "absolute"
newBall.style.width = '15px';
newBall.style.height = '15px';
var bx = newBall.style.left = x + 'px';
var by = newBall.style.top = y + 'px';
newBall.style.borderRadius = '10px';
newBall.style.backgroundColor = 'white';
var incrementPos = 0;
var id = setInterval(bounce, 5);
function bounce()
{
incrementPos++;
by = newBall.style.top = incrementPos + y + "px";
if(by == 650 + "px")
{
clearInterval(id)
var id2 = setInterval(function bounceUp()
{
incrementPosYMax -= 650
by = newBall.style.bottom = by + "px" - incrementPosYMax
}, 5)
}`/*Function that make the ball bounce down and up(but when it came at 650 px it stopped )*/ì
} /*End of the set interval */`
div.appendChild(newBall);
}
div.addEventListener("click", createBall);
This down below is the HTML CODE
<html>
<head>
<link rel= "stylesheet" href="style/style.css">
</head>
<body>
<div id ="container-ball">
</div>
<script src="js/main.js"></script>
</body>
Working example (comments see below):
const areaHeight = 150; // it is 650px in the original question
var div = document.getElementById('container-ball');
function createBall(event) {
var x = event.clientX;
var y = event.clientY;
var newBall = document.createElement('div');
newBall.className = 'ball';
var bx = newBall.style.left = x + 'px';
var by = newBall.style.top = y + 'px';
var incrementPos = 0;
var id = setInterval(bounce, 5);
let direction = 1; // 1 = down, -1 = up
function bounce() {
incrementPos += direction;
by = newBall.style.top = incrementPos + y + "px";
if (by == areaHeight + "px" || by == y + 'px') {
direction = -direction;
}
}
div.appendChild(newBall);
}
#container-ball {
width: 300px;
height: 157px;
background: gray;
}
#container-ball .ball {
position: absolute;
width: 15px;
height: 15px;
border-radius: 10px;
background-color: white;
border: 1px solid #333;
box-sizing: border-box;
}
<div id="container-ball" onclick="createBall(event)"></div>
Click on the grey box
Now, the explanation.
I've moved ball's styles to CSS - this is easier to control in the future. So when I have a class for ball, I can write in my code: newBall.className = 'ball';
I removed incrementPosYMax because I do not really understand if you need it
I understand your 'bounce' as bounce, so my ball just fall to the floor and then return to the original position. I do not really understand if you mean that (please, comment if it is wrong).
Your program is quite small, so I do not see the need for another setInterval, so all the animation in my example is inside only one setInterval
I've added new variable direction to control the direction for the ball (1 = down, -1 = up)
I do not like the parts with by == areaHeight + "px", but I keep them for you, because you use it in your code.
This code have some bugs, that you (or me if you ask) can fix. I just need to understand that my approach is correct
How the direction works:
Take a look at this line by = newBall.style.top = incrementPos + y + "px"; here you set new "y" coordinate for the ball as sum of 'original' "y" coordinate (in y) and offset (the distance that ball moved over the time) in incrementPos. So, if you increase incrementPos, then the ball's position will be lower (because "zero" in browser is at the top left corner, bigger "y" means lower the element).
Before my change, in your code you changed the offset with this line: incrementPos++; (means you increase incrementPos by 1 on every bounce step).
To move to another direction, you need to subtract 1 on every bounce step.
To reflect the "direction" of that move, I've added direction variable (so 1 means move down, and -1 means move up)
Now the offset is changed by: incrementPos += direction; (so I add this direction, not always 1)
Sometimes we need to change the "direction" with this code: direction = -direction;
The "levels" where we need to change direction is checked by this code: if (by == areaHeight + "px" || by == y + 'px') - here we check bottom (areaHeight) and top (y - it is where user clicks the mouse)
I adapted this code to create a large div which scrolls horizontally inside a smaller div, depending on the position of the mouse.
You can see my example here.. http://thetally.efinancialnews.com/tallyassets/20years/index.html
What I am trying to achieve is for the inner (yellow) div to stop at a maximum of left:0px, in other words the far left of the yellow div will become stuck to the far left of the outer div if you go that far.
I tried to implement this with an 'if else' statement, however as this piece of code gets run every 30th of a second it creates a strange result, which I can't find a solution for. I'm sure its very simple but its stumped me
You can see my code here...
var x=0,
rate=0,
maxspeed=10;
var backdrop = $('.container');
var years = $('.events');
$('.direction', backdrop).mousemove(function(e){
var $this = $(this);
var left = $this.is('.left');
if (left){
var w = $this.width();
rate = (w - e.pageX - $(this).offset().left + 1)/w;
} else {
var w = $this.width();
rate = -(e.pageX - $(this).offset().left + 1)/w;
}
});
backdrop.hover(function(){
var scroller = setInterval( moveBackdrop, 30 );
$(this).data('scroller', scroller);
},
function(){
var scroller = $(this).data('scroller');
clearInterval( scroller );
});
function moveBackdrop(){
if ( parseInt(years.css("left"), 10) <= 0 ) {
x += maxspeed * rate;
var newpos = x+'px';
years.css('left',newpos);
} else {
years.css('left','0');
}
}
The code in question is right here at the end^
Is this what you were trying to do?
function moveBackdrop(){
if ( parseInt(years.css("left"), 10) <= 0 && rate < 0 ) {
// Means that the element is already at left: 0 or less,
// and we are trying to move it even more left with rate being negative.
// So keep the element at left: 0
years.css('left','0');
} else {
x += maxspeed * rate;
var newpos = x+'px';
years.css('left',newpos);
}
}
Extra note for future: parseInt uses base 10 by default :) so parseInt("20px") will equal 20
Final Edit: Ah there is an even better way to do it.
function moveBackdrop(){
x += maxspeed * rate;
if( x < 0 ) x = 0; // If left less than 0, fix it at 0
var newpos = x+'px';
years.css('left',newpos);
}
I'm attempting to output on a page multiple 'labels' over an image using absolute positioned divs. Each of these divs has a unique number and are placed according to an x and y position on the map (these are percentage based so the image may be scaled).
As some of these labels may overlap, I need a way to either stop them from overlapping, or to essentially 'bump' them off eachother so they no longer overlap. (At this point, it doesn't matter if they are not in their correct position as long as they are near enough as there is a separate 'Pin' view).
They need to stay within the confines of their container and not overlap with eachother.
HTML:
<div id="labelzone">
<div class="label" style="left:0%;top:8%">001</div>
<div class="label" style="left:0%;top:11%">002</div>
<div class="label" style="left:1%;top:10%">003</div>
</div>
CSS:
#labelzone{
float:left;
width:500px;
height:500px;
border: 1px solid black;
position: relative;
}
.label{
position:absolute;
border:1px solid black;
background-color:white;
}
Jsfiddle: https://jsfiddle.net/79cco1oy/
There's a simple example of what I have as an output, these pins could be placed anywhere and there is no limit to how many is on the page, however there shouldn't be any occasion where there are too many to fit in the area.
I'm toying around with doing some form of collision detection and currently attempting to figure out an algorithm of some sort to get them to no longer overlap, and ensure they also don't overlap another item.
My solution is a bit more object oriented.
One object (LabelPool) will contain labels and will be in charge of storing and accomodating them so that they don't collide. You can customize the x/y values that you want to add/substract of the Label's positions in order to avoid their collision. The other object (Label) defines a Label and has some convenient methods. The collision algorithm that I used in LabelPool was taken from this post
var Label = function ($el) {
var position = $el.position(),
width = $el.outerWidth(true),
height = $el.outerHeight(true);
this.getRect = function () {
return {
x: position.left,
y: position.top,
width: width,
height: height
};
};
this.modifyPos = function (modX, modY) {
position.top += modY;
position.left += modX;
updatePos();
};
function updatePos() {
$el.css({
top: position.top,
left: position.left
});
}
};
var LabelPool = function () {
var labelPool = [];
function collides(a, b) {
return !(((a.y + a.height) < (b.y)) || (a.y > (b.y + b.height)) || ((a.x + a.width) < b.x) || (a.x > (b.x + b.width)));
}
function overlaps(label) {
var a = label.getRect();
return labelPool.some(function (other) {
return collides(a, other.getRect());
});
}
this.accomodate = function (label) {
while (labelPool.length && overlaps(label)) {
label.modifyPos(0, 1);// You can modify these values as you please.
}
labelPool.push(label);
};
};
var labelPool = new LabelPool;
$(".label").each(function (_, el) {
labelPool.accomodate(new Label($(el)));
});
Here's the fiddle.
Hope it helps.
Using js and jquery, you can find a basic collision engine based on left/top abs position and size of the label.
https://jsfiddle.net/Marcassin/79cco1oy/6/
Every time you want to add a Label, you check if the positionning is overlaping any existing div, in this case, you translate the new Label to position. This operation may not be the most beautiful you can find, there can be a long process time in case of lots of labels.
$(document).ready (function () {
addLabel (0, 8);
addLabel (0, 11);
addLabel (1, 10);
addLabel (2, 7);
});
function addLabel (newLeft, newTop)
{
var newLab = document.createElement ("div");
newLab.className = "label";
$(newLab).css({"left": newLeft+"%", "top": newTop + "%"});
var labels = $("#labelzone > div");
newLab.innerHTML = "00" + (labels.length + 1); // manage 0s
$("#labelzone").append (newLab);
var isCollision = false;
var cpt = 1;
do
{
isCollision = false;
$(labels).each (function () {
if (! isCollision && collision (this, newLab))
isCollision = true;
});
if (isCollision)
$(newLab).css({"left": (newLeft + cpt++) + "%",
"top": (newTop + cpt++) + "%"});
} while (isCollision);
}
function isInside (pt, div)
{
var x = parseInt($(div).css("left"));
var y = parseInt($(div).css("top"));
var w = $(div).width () + borderWidth;
var h = $(div).height ();
if (pt[0] >= x && pt[0] <= x + w &&
pt[1] >= y && pt[1] <= y + h)
return true;
return false;
}
function collision (div1, div2)
{
var x = parseInt($(div1).css("left"));
var y = parseInt($(div1).css("top"));
var w = $(div1).width () + borderWidth;
var h = $(div1).height ();
var pos = [x, y];
if (isInside (pos, div2))
return true;
pos = [x + w, y];
if (isInside (pos, div2))
return true;
pos = [x + w, y + h];
if (isInside (pos, div2))
return true;
pos = [x, y + h];
if (isInside (pos, div2))
return true;
return false;
}
Here's another implementation of collision detection close to what you asked for. The two main goals being:
move vertically more than horizontally (because boxes are wider than tall)
stay within a reasonable range from the origin
Here goes:
function yCollision($elem) {
var $result = null;
$('.label').each(function() {
var $candidate = $(this);
if (!$candidate.is($elem) &&
$candidate.position().top <= $elem.position().top + $elem.outerHeight() &&
$candidate.position().top + $candidate.outerHeight() >= $elem.position().top) {
$result = $candidate;
console.log("BUMP Y");
}
});
return $result;
}
function xCollision($elem) {
var $result = null;
$('.label').each(function() {
$candidate = $(this);
if (!$candidate.is($elem) &&
yCollision($elem) &&
yCollision($elem).is($candidate) &&
$candidate.position().left <= $elem.position().left + $elem.outerWidth() &&
$candidate.position().left + $candidate.outerWidth() >= $elem.position().left) {
$result = $candidate;
console.log("BUMP X");
}
});
return $result;
}
function fuzzyMoveY($elem, direction) {
var newTop = $elem.position().top + $elem.outerHeight() / 4 * direction;
// stay in the canvas - top border
newTop = (newTop < 0 ? 0 : newTop);
// stay in the canvas - bottom border
newTop = (newTop + $elem.outerHeight() > $("#labelzone").outerHeight() ? $("#labelzone").outerHeight() - $elem.outerHeight() : newTop);
// stay close to our origin
newTop = (Math.abs(newTop - $elem.attr("data-origin-top")) > $elem.outerHeight() ? $elem.attr("data-origin-top") : newTop);
$elem.css({'top': newTop});
}
function fuzzyMoveX($elem, direction) {
var newLeft = $elem.position().left + $elem.outerWidth() / 4 * direction;
// stay in the canvas - left border
newLeft = (newLeft < 0 ? 0 : newLeft);
// stay in the canvas - right border
newLeft = (newLeft + $elem.outerWidth() > $("#labelzone").outerWidth() ? $("#labelzone").outerWidth() - $elem.outerWidth() : newLeft);
// stay close to our origin
newLeft = (Math.abs(newLeft - $elem.attr("data-origin-left")) > $elem.outerWidth() ? $elem.attr("data-origin-left") : newLeft);
$elem.css({'left': newLeft});
}
function bumpY($above, $below) {
if ($above.position().top > $below.position().top) {
$buff = $above;
$above = $below;
$below = $buff;
}
fuzzyMoveY($above, -1);
fuzzyMoveY($below, 1);
}
function bumpX($left, $right) {
if ($left.position().left > $right.position().left) {
$buff = $right;
$right = $left;
$left = $buff;
}
fuzzyMoveX($left, 1);
fuzzyMoveX($right, -1);
}
$('.label').each(function() {
$(this).attr('data-origin-left', $(this).position().left);
$(this).attr('data-origin-top', $(this).position().top);
});
var yShallPass = true;
var loopCount = 0;
while (yShallPass && loopCount < 10) {
yShallPass = false;
$('.label').each(function() {
var $this = $(this);
$collider = yCollision($this);
if ($collider) {
bumpY($this, $collider);
yShallPass = true;
}
});
loopCount++;
}
console.log("y loops", loopCount);
var xShallPass = true;
var loopCount = 0;
while (xShallPass && loopCount < 10) {
xShallPass = false;
$('.label').each(function() {
var $this = $(this);
$collider = xCollision($this);
if ($collider) {
bumpX($this, $collider);
xShallPass = true;
}
});
loopCount++;
}
console.log("x loops", loopCount);
This is not production code obviously but please report back if it helps.
I'm trying to draw a bunch of divs inside of a container in such a way that they are randomly positioned every time the page is loaded and that there is no overlapping. I seem to be stuck in an infinite loop and without the sources loading in Chrome before crashing the page, I can't debug properly.
Here's my code:
$(document).ready(function() {
var filledAreas = new Array();
var minX = 50;
var maxX = $("#banner").width() - 150;
var minY = 50;
var maxY = $("#banner").height() - 150;
var loops = 0;
var loopsMax = 100;
for (var i = 0; i < 15; i++) {
var color = '#' + Math.round(0xffffff * Math.random()).toString(16);
$newDiv = $("<div class='bubble-outer'></div>").css({
'width': '100px',
'height': '100px',
'background-color': color,
});
var randX = 0;
var randY = 0;
var area;
do {
randX = Math.floor(Math.random() * (maxX - minX + 1)) + minX;
randY = Math.floor(Math.random() * (maxY - minY + 1)) + minY;
var area = {
'left': randX,
'top': randY,
'right': randX + 100,
'bottom': randY + 100,
};
} while (loops < loopsMax && checkOverlap(area))
filledAreas.push(area)
$newDiv.css({
'position': 'absolute',
'left': randX + 'px',
'top': randY + 'px',
'display': 'none'
}).delay(1000).appendTo("#banner").fadeIn(300);
}
console.log("Loops: " + loops);
function checkOverlap(area) {
for (var i = 0; i < filledAreas.length; i++) {
var check = filledAreas[i];
if (area.right < check.left && area.bottom < check.top &&
check.right < area.left && check.bottom < area.top) {
loops++;
continue;
} else {
return true;
}
return false;
}
}
});
Since you used my algorithm, might as well post it as an answer!
As with many things, it is easier to cheat than to do exactly what you want to do.
Doing it the "proper" way would involve detecting collisions, which gets very tricky very fast.
However, here's a nice way to cheat at it:
Divide the area into a grid. Each grid square must be at least as big as your biggest element for this to work correctly.
For each element, pick a grid square. If that grid square hasn't been taken yet, place your element somewhere inside that grid square at random.
And... that's it! Now you are guaranteed to never have any collisions, and it looks pretty random because it is :)
As an experiment, I created a few div's and rotated them using CSS3.
.items {
position: absolute;
cursor: pointer;
background: #FFC400;
-moz-box-shadow: 0px 0px 2px #E39900;
-webkit-box-shadow: 1px 1px 2px #E39900;
box-shadow: 0px 0px 2px #E39900;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
}
I then randomly styled them and made them draggable via jQuery.
$('.items').each(function() {
$(this).css({
top: (80 * Math.random()) + '%',
left: (80 * Math.random()) + '%',
width: (100 + 200 * Math.random()) + 'px',
height: (10 + 10 * Math.random()) + 'px',
'-moz-transform': 'rotate(' + (180 * Math.random()) + 'deg)',
'-o-transform': 'rotate(' + (180 * Math.random()) + 'deg)',
'-webkit-transform': 'rotate(' + (180 * Math.random()) + 'deg)',
});
});
$('.items').draggable();
The dragging works, but I am noticing a sudden jump while dragging the div's only in webkit browsers, while everything is fine in Firefox.
If I remove the position: absolute style, the 'jumping' is even worse. I thought there was maybe a difference in the transform origin between webkit and gecko, but they are both at the centre of the element by default.
I have searched around already, but only came up with results about scrollbars or sortable lists.
Here is a working demo of my problem. Try to view it in both Safari/Chrome and Firefox. http://jsbin.com/ucehu/
Is this a bug within webkit or how the browsers render webkit?
I draw a image to indicate the offset after rotate on different browsers as #David Wick's answer.
Here's the code to fix if you don't want patch or modify jquery.ui.draggable.js
$(document).ready(function () {
var recoupLeft, recoupTop;
$('#box').draggable({
start: function (event, ui) {
var left = parseInt($(this).css('left'),10);
left = isNaN(left) ? 0 : left;
var top = parseInt($(this).css('top'),10);
top = isNaN(top) ? 0 : top;
recoupLeft = left - ui.position.left;
recoupTop = top - ui.position.top;
},
drag: function (event, ui) {
ui.position.left += recoupLeft;
ui.position.top += recoupTop;
}
});
});
or you can see the demo
This is a result of draggable's reliance on the jquery offset() function and offset()'s use of the native js function getBoundingClientRect(). Ultimately this is an issue with the jquery core not compensating for the inconsistencies associated with getBoundingClientRect(). Firefox's version of getBoundingClientRect() ignores the css3 transforms (rotation) whereas chrome/safari (webkit) don't.
here is an illustration of the issue.
A hacky workaround:
replace following in jquery.ui.draggable.js
//The element's absolute position on the page minus margins
this.offset = this.positionAbs = this.element.offset();
with
//The element's absolute position on the page minus margins
this.offset = this.positionAbs = { top: this.element[0].offsetTop,
left: this.element[0].offsetLeft };
and finally a monkeypatched version of your jsbin.
David Wick is right about the general direction above, but computing the right coordinates is way more involved than that. Here's a more accurate monkey patch, based on MIT licensed Firebug code, which should work in far more situations where you have a complex DOM:
Instead replace:
//The element's absolute position on the page minus margins
this.offset = this.positionAbs = this.element.offset();
with the less hacky (be sure to get the whole thing; you'll need to scroll):
//The element's absolute position on the page minus margins
this.offset = this.positionAbs = getViewOffset(this.element[0]);
function getViewOffset(node) {
var x = 0, y = 0, win = node.ownerDocument.defaultView || window;
if (node) addOffset(node);
return { left: x, top: y };
function getStyle(node) {
return node.currentStyle || // IE
win.getComputedStyle(node, '');
}
function addOffset(node) {
var p = node.offsetParent, style, X, Y;
x += parseInt(node.offsetLeft, 10) || 0;
y += parseInt(node.offsetTop, 10) || 0;
if (p) {
x -= parseInt(p.scrollLeft, 10) || 0;
y -= parseInt(p.scrollTop, 10) || 0;
if (p.nodeType == 1) {
var parentStyle = getStyle(p)
, localName = p.localName
, parent = node.parentNode;
if (parentStyle.position != 'static') {
x += parseInt(parentStyle.borderLeftWidth, 10) || 0;
y += parseInt(parentStyle.borderTopWidth, 10) || 0;
if (localName == 'TABLE') {
x += parseInt(parentStyle.paddingLeft, 10) || 0;
y += parseInt(parentStyle.paddingTop, 10) || 0;
}
else if (localName == 'BODY') {
style = getStyle(node);
x += parseInt(style.marginLeft, 10) || 0;
y += parseInt(style.marginTop, 10) || 0;
}
}
else if (localName == 'BODY') {
x += parseInt(parentStyle.borderLeftWidth, 10) || 0;
y += parseInt(parentStyle.borderTopWidth, 10) || 0;
}
while (p != parent) {
x -= parseInt(parent.scrollLeft, 10) || 0;
y -= parseInt(parent.scrollTop, 10) || 0;
parent = parent.parentNode;
}
addOffset(p);
}
}
else {
if (node.localName == 'BODY') {
style = getStyle(node);
x += parseInt(style.borderLeftWidth, 10) || 0;
y += parseInt(style.borderTopWidth, 10) || 0;
var htmlStyle = getStyle(node.parentNode);
x -= parseInt(htmlStyle.paddingLeft, 10) || 0;
y -= parseInt(htmlStyle.paddingTop, 10) || 0;
}
if ((X = node.scrollLeft)) x += parseInt(X, 10) || 0;
if ((Y = node.scrollTop)) y += parseInt(Y, 10) || 0;
}
}
}
It's a shame the DOM doesn't expose these calculations natively.
#ecmanaut: Great solution. Thanks for your efforts. To assist others I turned your solution into a monkey-patch. Copy below code to a file. Include the file after loading jquery-ui.js as follows:
<script src="javascripts/jquery/jquery.js"></script>
<script src="javascripts/jquery/jquery-ui.js"></script>
<!-- the file containing the monkey-patch to draggable -->
<script src="javascripts/jquery/patch_draggable.js"></script>
Here's the code to copy/paste into patch_draggable.js:
function monkeyPatch_mouseStart() {
// don't really need this, but in case I did, I could store it and chain
var oldFn = $.ui.draggable.prototype._mouseStart ;
$.ui.draggable.prototype._mouseStart = function(event) {
var o = this.options;
function getViewOffset(node) {
var x = 0, y = 0, win = node.ownerDocument.defaultView || window;
if (node) addOffset(node);
return { left: x, top: y };
function getStyle(node) {
return node.currentStyle || // IE
win.getComputedStyle(node, '');
}
function addOffset(node) {
var p = node.offsetParent, style, X, Y;
x += parseInt(node.offsetLeft, 10) || 0;
y += parseInt(node.offsetTop, 10) || 0;
if (p) {
x -= parseInt(p.scrollLeft, 10) || 0;
y -= parseInt(p.scrollTop, 10) || 0;
if (p.nodeType == 1) {
var parentStyle = getStyle(p)
, localName = p.localName
, parent = node.parentNode;
if (parentStyle.position != 'static') {
x += parseInt(parentStyle.borderLeftWidth, 10) || 0;
y += parseInt(parentStyle.borderTopWidth, 10) || 0;
if (localName == 'TABLE') {
x += parseInt(parentStyle.paddingLeft, 10) || 0;
y += parseInt(parentStyle.paddingTop, 10) || 0;
}
else if (localName == 'BODY') {
style = getStyle(node);
x += parseInt(style.marginLeft, 10) || 0;
y += parseInt(style.marginTop, 10) || 0;
}
}
else if (localName == 'BODY') {
x += parseInt(parentStyle.borderLeftWidth, 10) || 0;
y += parseInt(parentStyle.borderTopWidth, 10) || 0;
}
while (p != parent) {
x -= parseInt(parent.scrollLeft, 10) || 0;
y -= parseInt(parent.scrollTop, 10) || 0;
parent = parent.parentNode;
}
addOffset(p);
}
}
else {
if (node.localName == 'BODY') {
style = getStyle(node);
x += parseInt(style.borderLeftWidth, 10) || 0;
y += parseInt(style.borderTopWidth, 10) || 0;
var htmlStyle = getStyle(node.parentNode);
x -= parseInt(htmlStyle.paddingLeft, 10) || 0;
y -= parseInt(htmlStyle.paddingTop, 10) || 0;
}
if ((X = node.scrollLeft)) x += parseInt(X, 10) || 0;
if ((Y = node.scrollTop)) y += parseInt(Y, 10) || 0;
}
}
}
//Create and append the visible helper
this.helper = this._createHelper(event);
//Cache the helper size
this._cacheHelperProportions();
//If ddmanager is used for droppables, set the global draggable
if($.ui.ddmanager)
$.ui.ddmanager.current = this;
/*
* - Position generation -
* This block generates everything position related - it's the core of draggables.
*/
//Cache the margins of the original element
this._cacheMargins();
//Store the helper's css position
this.cssPosition = this.helper.css("position");
this.scrollParent = this.helper.scrollParent();
//The element's absolute position on the page minus margins
this.offset = this.positionAbs = getViewOffset(this.element[0]);
this.offset = {
top: this.offset.top - this.margins.top,
left: this.offset.left - this.margins.left
};
$.extend(this.offset, {
click: { //Where the click happened, relative to the element
left: event.pageX - this.offset.left,
top: event.pageY - this.offset.top
},
parent: this._getParentOffset(),
relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper
});
//Generate the original position
this.originalPosition = this.position = this._generatePosition(event);
this.originalPageX = event.pageX;
this.originalPageY = event.pageY;
//Adjust the mouse offset relative to the helper if 'cursorAt' is supplied
(o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
//Set a containment if given in the options
if(o.containment)
this._setContainment();
//Trigger event + callbacks
if(this._trigger("start", event) === false) {
this._clear();
return false;
}
//Recache the helper size
this._cacheHelperProportions();
//Prepare the droppable offsets
if ($.ui.ddmanager && !o.dropBehaviour)
$.ui.ddmanager.prepareOffsets(this, event);
this.helper.addClass("ui-draggable-dragging");
//JWL: Hier vindt de jump plaats
this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position
//If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003)
if ( $.ui.ddmanager ) $.ui.ddmanager.dragStart(this, event);
return true;
};
}
monkeyPatch_mouseStart();
I prefer this workaround as it preserves the original handler
It removes the transform then restores it
$(document).ready(function(){
// backup original handler
var _mouseStart = $.ui.draggable.prototype._mouseStart;
$.ui.draggable.prototype._mouseStart = function(event) {
//remove the transform
var transform = this.element.css('transform');
this.element.css('transform', 'none');
// call original handler
var result = _mouseStart.call(this, event);
//restore the transform
this.element.css('transform', transform);
return result;
};
});
demo (started from #Liao San-Kai jsbin)
the answer of David Wick was very helpful... thanks...
here i coded the same workaround for the resizeable, because it has the same problem:
search for the following in jquery.ui.resizable.js
var o = this.options, iniPos = this.element.position(), el = this.element;
and replace with:
var o = this.options, iniPos = {top:this.element[0].offsetTop,left:this.element[0].offsetLeft}, el = this.element;
I used a lot of the solutions to get dragging working correctly. BUT, it still reacted wrong to a dropzone (like it wasn't rotated). The Solution really is to use a parent container that is positioned relative.
This saved me soooo much time.
<div id="drawarea">
<div class="rect-container h">
<div class="rect"></div>
</div>
</div>
.rect-container {
position:relative;
}
Full Solution here (it's not from me):
http://jsfiddle.net/Sp6qa/2/
Also I researched a lot. And its just like this, jQuery doesn't have any plans to change that current behavior in the future. All submitted tickets about that topic were closed. So just start out with having parentcontainers that are positioned relative. It works like a charm and should be futureproof.
You have to set the parent container of the draggable element to "position: relative".