Get real position of objects in Javascript with Chrome - javascript

I've been coding a bit of Javascript to place a ducky randomly on this page.
I wanted to make it hide on the side of objects (like the posts), but I ended up having to hardcode quite a bit of it, since I couldn't get a way to properly retrieve the real position of relative objects with Chrome. I read quite a few things about it, and used the recursive offsetParent way, but didn't get any good results.
The last bit of code I tried was this:
var getPost = function (obj) {
var pos = {'x':0,'y':0};
if(obj.offsetParent) {
while(1) {
pos.x += obj.offsetLeft;
pos.y += obj.offsetTop;
if(!obj.offsetParent) {
break;
}
obj = obj.offsetParent;
}
} else if(obj.x) {
pos.x += obj.x;
pos.y += obj.y;
}
return pos;
}
This code doesn't work on Chrome, except on objects with an absolute position (set with CSS).
Is there a good, cross-browser way to achieve this?

I had a case where I was working with mouse positions and objects as well not so long ago because I needed some drag and drop. So these are the two methods I came up with:
/**
* Calculates the mouse x and y position from the mouse move event fired by the document
*
* #param event
* the mouse move event fired by the document
*
* #return the mouse coordinates object with two variables x and y
*/
function mouseCoords(ev) {
var event = ev;
// IE does not pass the event object
if (event == null)
event = window.event;
try {
// normal style
if (event.pageX) {
return {
x : event.pageX,
y : event.pageY
};
}
// IE style
else {
return {
x : event.clientX + document.body.scrollLeft - document.body.clientLeft,
y : event.clientY + document.body.scrollTop - document.body.clientTop
};
}
} catch(ex) {
// IE style
return {
x : event.clientX + document.body.scrollLeft - document.body.clientLeft,
y : event.clientY + document.body.scrollTop - document.body.clientTop
};
}
}
/**
* Calculates an object with the x and y coordinates of the given object
*
* #param object
* the object of which the coordinates to be calculated
*
* #return an object with x and y coordinates
*/
function getObjectPosition(object) {
var left = 0;
var top = 0;
while (object.offsetParent) {
left += object.offsetLeft;
top += object.offsetTop;
object = object.offsetParent;
}
left += object.offsetLeft;
top += object.offsetTop;
return {
x : left,
y : top
};
}
I hope this can help you. This works for me in IE, Firefox and Chrome.

Alright, my problem was somewhere else. This is what I was doing to call the function:
var allPosts = document.getElementsByClassName('post-outer');
for (post in allPosts) {
console.log('Post has position '+getPos(post));
}
You can tell I'm not so used to Javascript's recursive behavior in the for loop, so the following code actually fixes my issue:
var allPosts = document.getElementsByClassName('post-outer');
for (var i=0, len=allPosts.length; i<len; ++i ){
console.log('Post position is '+getPos(allPosts[i]).y);
}
Thanks all for your help :-)

That does not work with absolute positioning because it does not factor in top and left (among other things).
I was going to rip out that part of code from jQuery and post it here, but it is too rooted. So I just have to recommend using jQuery! To do so, just have this in the markup (before any other script tags)...
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
Once you have that referenced, you can get the position of an element so easily...
$(function() {
var pos = $(".className").position();
alert(pos.top + "\n" + pos.left);
});

Related

Efficiently get an element's visible area coordinates

StackOverflow is loaded with questions about how to check if an element is really visible in the viewport, but they all seek for a boolean answer. I'm interested in getting the element's actual areas that are visible.
function getVisibleAreas(e) {
...
return rectangleSet;
}
Putting it more formally - the visible areas of elements is the set of (preferably non-overlapping) rectangles in CSS coordinates for which elementFromPoint(x, y) will return the element if the point (x, y) is contained in (at least) one of the rectangles in the set.
The outcome of calling this function on all DOM elements (including iframes) should be a set of non-overlapping area sets which union is the entire viewport area.
My goal is to create some kind of a viewport "dump" data structure, which can efficiently return a single element for a given point in the viewport, and vice versa - for a given element in the dump, it will return the set of visible areas.
(The data structure will be passed to a remote client application, so I will not necessarily have access to the actual document when I need to query the viewport structure).
Implementation requirements:
Obviously, the implementation should consider element's hidden state, z-index, header & footer etc.
I am looking for an implementation that works in all common used browsers, especially mobile - Android's Chrome and iOS's Safari.
Preferably doesn't use external libraries.
Of course, I could be naïve and call elementFromPoint for every discrete point in the viewport, But performance is crucial since I iterate over all of the elements, and will do it quite often.
Please direct me as to how I can achieve this goal.
Disclaimer: I'm pretty noob to web programming concepts, so I might have used wrong technical terms.
Progress:
I came up with an implementation. The algorithm is pretty simple:
Iterate over all elements, and add their vertical / horizontal lines to a coordinates map (if the coordinate is within the viewport).
Call `document.elementFromPoint` for each "rectangle" center position. A rectangle is an area between two consecutive vertical and two consecutive horizontal coordinates in the map from step 1.
This produces a set of areas / rectangles, each pointing to a single element.
The problems with my implementation are:
It is inefficient for complicated pages (can take up to 2-4 minutes for a really big screen and gmail inbox).
It produces a large amount of rectangles per a single element, which makes it inefficient to stringify and send over a network, and also inconvenient to work with (I would want to end up with a set with as few rectangles as possible per element).
As much as I can tell, the elementFromPoint call is the one that takes a lot of time and causes my algorithm to be relatively useless...
Can anyone suggest a better approach?
Here is my implementation:
function AreaPortion(l, t, r, b, currentDoc) {
if (!currentDoc) currentDoc = document;
this._x = l;
this._y = t;
this._r = r;
this._b = b;
this._w = r - l;
this._h = b - t;
center = this.getCenter();
this._elem = currentDoc.elementFromPoint(center[0], center[1]);
}
AreaPortion.prototype = {
getName: function() {
return "[x:" + this._x + ",y:" + this._y + ",w:" + this._w + ",h:" + this._h + "]";
},
getCenter: function() {
return [this._x + (this._w / 2), this._y + (this._h / 2)];
}
}
function getViewport() {
var viewPortWidth;
var viewPortHeight;
// IE6 in standards compliant mode (i.e. with a valid doctype as the first line in the document)
if (
typeof document.documentElement != 'undefined' &&
typeof document.documentElement.clientWidth != 'undefined' &&
document.documentElement.clientWidth != 0) {
viewPortWidth = document.documentElement.clientWidth,
viewPortHeight = document.documentElement.clientHeight
}
// the more standards compliant browsers (mozilla/netscape/opera/IE7) use window.innerWidth and window.innerHeight
else if (typeof window.innerWidth != 'undefined') {
viewPortWidth = window.innerWidth,
viewPortHeight = window.innerHeight
}
// older versions of IE
else {
viewPortWidth = document.getElementsByTagName('body')[0].clientWidth,
viewPortHeight = document.getElementsByTagName('body')[0].clientHeight
}
return [viewPortWidth, viewPortHeight];
}
function getLines() {
var onScreen = [];
var viewPort = getViewport();
// TODO: header & footer
var all = document.getElementsByTagName("*");
var vert = {};
var horz = {};
vert["0"] = 0;
vert["" + viewPort[1]] = viewPort[1];
horz["0"] = 0;
horz["" + viewPort[0]] = viewPort[0];
for (i = 0 ; i < all.length ; i++) {
var e = all[i];
// TODO: Get all client rectangles
var rect = e.getBoundingClientRect();
if (rect.width < 1 && rect.height < 1) continue;
var left = Math.floor(rect.left);
var top = Math.floor(rect.top);
var right = Math.floor(rect.right);
var bottom = Math.floor(rect.bottom);
if (top > 0 && top < viewPort[1]) {
vert["" + top] = top;
}
if (bottom > 0 && bottom < viewPort[1]) {
vert["" + bottom] = bottom;
}
if (right > 0 && right < viewPort[0]) {
horz["" + right] = right;
}
if (left > 0 && left < viewPort[0]) {
horz["" + left] = left;
}
}
hCoords = [];
vCoords = [];
//TODO:
for (var v in vert) {
vCoords.push(vert[v]);
}
for (var h in horz) {
hCoords.push(horz[h]);
}
return [hCoords, vCoords];
}
function getAreaPortions() {
var portions = {}
var lines = getLines();
var hCoords = lines[0];
var vCoords = lines[1];
for (i = 1 ; i < hCoords.length ; i++) {
for (j = 1 ; j < vCoords.length ; j++) {
var portion = new AreaPortion(hCoords[i - 1], vCoords[j - 1], hCoords[i], vCoords[j]);
portions[portion.getName()] = portion;
}
}
return portions;
}
Try
var res = [];
$("body *").each(function (i, el) {
if ((el.getBoundingClientRect().bottom <= window.innerHeight
|| el.getBoundingClientRect().top <= window.innerHeight)
&& el.getBoundingClientRect().right <= window.innerWidth) {
res.push([el.tagName.toLowerCase(), el.getBoundingClientRect()]);
};
});
jsfiddle http://jsfiddle.net/guest271314/ueum30g5/
See Element.getBoundingClientRect()
$.each(new Array(180), function () {
$("body").append(
$("<img>"))
});
$.each(new Array(180), function () {
$("body").append(
$("<img>"))
});
var res = [];
$("body *").each(function (i, el) {
if ((el.getBoundingClientRect().bottom <= window.innerHeight || el.getBoundingClientRect().top <= window.innerHeight)
&& el.getBoundingClientRect().right <= window.innerWidth) {
res.push(
[el.tagName.toLowerCase(),
el.getBoundingClientRect()]);
$(el).css(
"outline", "0.15em solid red");
$("body").append(JSON.stringify(res, null, 4));
console.log(res)
};
});
body {
width : 1000px;
height : 1000px;
}
img {
width : 50px;
height : 50px;
background : navy;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
I don't know if the performance will be sufficient (especially on a mobile device), and the result is not quite a rectangle-set as you requested, but did you consider using a bitmap to store the result?
Note some elements may have 3d css transform (eg. skew, rotate), some elements may have border radius, and some elements may have invisible background - if you want to include these features as well for your "element from pixel" function then a rectangle set can't help you - but the bitmap can accommodate all of the visual features.
The solution to generate the bitmap is rather simple (I imagine... not tested):
Create a Canvas the size of the visible screen.
iterate over all the elements recursively, sorted by z-order, ignore hidden
for each element draw a rectangle in the canvas, the color of the of the rectangle is an identifier of the element (eg. could be incremental counter). If you want you can modify the rectangle based on the visual features of the element (skew, rotate, border radius, etc...)
save the canvas as lossless format, eg png not jpg
send the bitmap as the meta data of elements on screen
To query which element is at point (x,y) you could check the color of the bitmap at pixel (x,y) and the color will tell you what is the element.
If you can jettison IE, here's a simple one:
function getElementVisibleRect(el) {
return new Promise((resolve, reject) => {
el.style.overflow = "hidden";
requestAnimationFrame((timeStamp) => {
var br = el.getBoundingClientRect();
el.style.overflow = "";
resolve(br);
});
});
}
Even then, Promises are easily polyfillable and requestAnimationFrame() works as far back as IE 8. And by 2016, the only thing you should bother to give any poor souls on older IE is a legible experience.

Snap SVG: Dragging group doesn't update elements

JSFiddle here: JSFiddle
When dragging a group of objects, the individual objects' location attributes don't seem to be getting updated. This occurs whether I use the default drag() handler or define my own. Even the group BBox operation doesn't seem to update. Code:
var s = Snap("#svg");
var move = function (dx, dy, posx, posy) {
this.attr({
x: posx,
y: posy
});
//this.transform("t" + dx + "," + dy);
};
var block = s.rect(100, 100, 100, 100);
var circle = s.circle(100, 100, 50);
var group = s.g(block, circle);
//group.drag(move, function () {}, function () {});
group.drag();
//block.drag(move, function () {}, function () {});
//just a way to keep info coming w/o an interminable script
document.addEventListener('mousemove', function (e) {
bbox = block.getBBox();
block_x = block.attr("x");
block_y = block.attr("y");
gbbox = group.getBBox();
console.log("block is at " + block_x + "," + block_y,
" Block Bbbox is at " + bbox.x + "," + bbox.y,
" Group Bbbox is at " + gbbox.x + "," + gbbox.y);
}, false);
If I define only one object (say, a rect) and leave it out of a group, and pass my own "move" function to the call to drag, and include setting the "x" and "y" attributes explicitly, then that works. But if I include the rect in a group, then...I can't figure out how to do it, and I've tried a few ways (see the multiple commented-out lines showing things I've tried). I need to know where the rect sub-group element ends up after the drag, or at least the BBox of the whole group. Neither of these seem to be getting updated -- i.e. the console log I put in shows the same numbers forever, no matter where I move the object(s).
Can anyone help?
JSFiddle here: JSFiddle
I think this is because they are two different things, so they aren't actually interchangable.
The drag handler uses transforms. A transform doesn't affect any other attributes, its just an attribute on an element (in this case the group element).
getBBox will work in its current transform space, note this may be different to the clients (eg if the svg were zoomed in/out). So they are two slightly different methods, that do different things.
Use getBoundingClientRect if you need a bounding box relative to the client window. Use getBBox if you need a bounding box in the elements current coordinate space.
Code is using snap.svg.zpd as well, so zoom is possible. Problem is at onStopMove function. Events are fired when group is moved arround. In group is one circle(this.select('#main-inner-circle')) which does not have predefined location inside group. Im trying to get correct cx and cy of that inner circle after moving group.
self.onMove = function (dx, dy, ev, x, y) {
var clientX, clientY;
var tdx, tdy;
if ((typeof dx == 'object') && (dx.type == 'touchmove')) {
clientX = dx.changedTouches[0].clientX;
clientY = dx.changedTouches[0].clientY;
dx = clientX - this.data('ox');
dy = clientY - this.data('oy');
}
var snapInvMatrix = this.transform().diffMatrix.invert();
snapInvMatrix.e = snapInvMatrix.f = 0;
tdx = snapInvMatrix.x(dx, dy);
tdy = snapInvMatrix.y(dx, dy);
this.transform("t" + [tdx, tdy] + this.data('ot'));
}
self.onStartMove = function (x, y, ev) {
if ((typeof x == 'object') && (x.type == 'touchstart')) {
x.preventDefault();
this.data('ox', x.changedTouches[0].clientX);
this.data('oy', x.changedTouches[0].clientY);
}
this.data('ot', this.transform().local);
if (callbacks.onStartMove) {
callbacks.onStartMove();
}
}
self.onStopMove = function () {
var self = this.select('#main-inner-circle');
this.data('ot', this.transform().local);
//self.data('ot', self.transform().local);
console.log(self.getTransformedBBox());
console.log(this.getBBox());
//console.log($(self.node).offset().left - $(self.node).parent().offset().left);
var bBox = this.getBBox();
//var x = bBox.x + $(self.node).offset().left - $(self.node).parent().offset().left + self.getBBox().width / 2;
//var y = bBox.y + $(self.node).offset().top - $(self.node).parent().offset().top + self.getBBox().height / 2;
model.updateElementCoordinates(index, $(this.node).attr("rel"), { x: self.getTransformedBBox().cx, y: self.getTransformedBBox().cy });
if (callbacks.onStopMove) {
callbacks.onStopMove();
}
}
In order to post this question, I'd created the JSFiddle but left out the crucial snap.svg definitions...
<script src="http://snapsvg.io/assets/js/snap.svg-min.js"></script>
...with that, then indeed the group.getBBox() method actually works. However:
Apparently, using getBBox() is incredibly slow -- much slower than just accessing a "x" attribute of something like I was doing before grouping objects. All I know is that my code slows to a crawl if I use getBBox() (I have a lot of objects on the screen).
Further down in the same post mentioned earier ["Get coordinates of svg group on drag with snap.svg"1 recommended getBoundingClientRect(), which also works fine AND is fast enough! My new, working Fiddle showing all of these methods is here: New JSFiddle.
So, future users: use .node.getBoundingClientRect().

Zoom image to cursor breaks when mouse is moved

This is a followup question to How to zoom to mouse pointer while using my own mousewheel smoothscroll?
I am using css transforms to zoom an image to the mouse pointer. I am also using my own smooth scroll algorithm to interpolate and provide momentum to the mousewheel.
With Bali Balo's help in my previous question I have managed to get 90% of the way there.
You can now zoom the image all the way in to the mouse pointer while still having smooth scrolling as the following JSFiddle illustrates:
http://jsfiddle.net/qGGwx/7/
However, the functionality is broken when the mouse pointer is moved.
To further clarify, If I zoom in one notch on the mousewheel the image is zoomed around the correct position. This behavior continues for every notch I zoom in on the mousewheel, completely as intended. If however, after zooming part way in, I move the mouse to a different position, the functionality breaks and I have to zoom out completely in order to change the zoom position.
The intended behavior is for any changes in mouse position during the zooming process to be correctly reflected in the zoomed image.
The two main functions that control the current behavior are as follows:
self.container.on('mousewheel', function (e, delta) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
if (!self.running) {
self.running = true;
self.animateLoop();
}
self.delta = delta
self.smoothWheel(delta);
return false;
});
This function collects the current position of the mouse at the current scale of the zoomed image.
It then starts my smooth scroll algorithm which results in the next function being called for every interpolation:
zoom: function (scale) {
var self = this;
self.currentLocation.x += ((self.mouseLocation.x - self.currentLocation.x) / self.currentscale);
self.currentLocation.y += ((self.mouseLocation.y - self.currentLocation.y) / self.currentscale);
var compat = ['-moz-', '-webkit-', '-o-', '-ms-', ''];
var newCss = {};
for (var i = compat.length - 1; i; i--) {
newCss[compat[i] + 'transform'] = 'scale(' + scale + ')';
newCss[compat[i] + 'transform-origin'] = self.currentLocation.x + 'px ' + self.currentLocation.y + 'px';
}
self.image.css(newCss);
self.currentscale = scale;
},
This function takes the scale amount (1-10) and applies the css transforms, repositioning the image using transform-origin.
Although this works perfectly for a stationary mouse position chosen when the image is completely zoomed out; as stated above it breaks when the mouse cursor is moved after a partial zoom.
Huge thanks in advance to anyone who can help.
Actually, not too complicated. You just need to separate the mouse location updating logic from the zoom updating logic. Check out my fiddle:
http://jsfiddle.net/qGGwx/41/
All I have done here is add a 'mousemove' listener on the container, and put the self.mouseLocation updating logic in there. Since it is no longer required, I also took out the mouseLocation updating logic from the 'mousewheel' handler. The animation code stays the same, as does the decision of when to start/stop the animation loop.
here's the code:
self.container.on('mousewheel', function (e, delta) {
if (!self.running) {
self.running = true;
self.animateLoop();
}
self.delta = delta
self.smoothWheel(delta);
return false;
});
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
});
Before you check this fiddle out; I should mention:
First of all, within your .zoom() method; you shouldn't divide by currentscale:
self.currentLocation.x += ((self.mouseLocation.x - self.currentLocation.x) / self.currentscale);
self.currentLocation.y += ((self.mouseLocation.y - self.currentLocation.y) / self.currentscale);
because; you already use that factor when calculating the mouseLocation inside the initmousewheel() method like this:
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
So instead; (in the .zoom() method), you should:
self.currentLocation.x += (self.mouseLocation.x - self.currentLocation.x);
self.currentLocation.y += (self.mouseLocation.y - self.currentLocation.y);
But (for example) a += b - a will always produce b so the code above equals to:
self.currentLocation.x = self.mouseLocation.x;
self.currentLocation.y = self.mouseLocation.y;
in short:
self.currentLocation = self.mouseLocation;
Then, it seems you don't even need self.currentLocation. (2 variables for the same value). So why not use mouseLocation variable in the line where you set the transform-origin instead and get rid of currentLocation variable?
newCss[compat[i] + 'transform-origin'] = self.mouseLocation.x + 'px ' + self.mouseLocation.y + 'px';
Secondly, you should include a mousemove event listener within the initmousewheel() method (just like other devs here suggest) but it should update the transform continuously, not just when the user wheels. Otherwise the tip of the pointer will never catch up while you're zooming out on "any" random point.
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
self.zoom(self.currentscale);
});
So; you wouldn't need to calculate this anymore within the mousewheel event handler so, your initmousewheel() method would look like this:
initmousewheel: function () {
var self = this;
self.container.on('mousewheel', function (e, delta) {
if (!self.running) {
self.running = true;
self.animateLoop();
}
self.delta = delta;
self.smoothWheel(delta);
return false;
});
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
self.zoom(self.currentscale); // <--- update transform origin dynamically
});
}
One Issue:
This solution works as expected but with a small issue. When the user moves the mouse in regular or fast speed; the mousemove event seems to miss the final position (tested in Chrome). So the zooming will be a little off the pointer location. Otherwise, when you move the mouse slowly, it gets the exact point. It should be easy to workaround this though.
Other Notes and Suggestions:
You have a duplicate property (prevscale).
I suggest you always use JSLint or JSHint (which is available on
jsFiddle too) to validate your code.
I highly suggest you to use closures (often refered to as Immediately Invoked Function Expression (IIFE)) to avoid the global scope when possible; and hide your internal/private properties and methods.
Add a mousemover method and call it in the init method:
mousemover: function() {
var self = this;
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
self.mouseLocation.x = (e.pageX - offset.left) / self.currentscale;
self.mouseLocation.y = (e.pageY - offset.top) / self.currentscale;
self.zoom(self.currentscale);
});
},
Fiddle: http://jsfiddle.net/powtac/qGGwx/34/
Zoom point is not exactly right because of scaling of an image (0.9 in ratio). In fact mouse are pointing in particular point in container but we scale image. See this fiddle http://jsfiddle.net/qGGwx/99/ I add marker with position equal to transform-origin. As you can see if image size is equal to container size there is no issue. You need this scaling? Maybe you can add second container? In fiddle I also added condition in mousemove
if(self.running && self.currentscale>1 && self.currentscale != self.lastscale) return;
That is preventing from moving image during zooming but also create an issue. You can't change zooming point if zoom is still running.
Extending #jordancpaul's answer I have added a constant mouse_coord_weight which gets multiplied to delta of the mouse coordinates. This is aimed at making the zoom transition less responsive to the change in mouse coordinates. Check it out http://jsfiddle.net/7dWrw/
I have rewritten the onmousemove event hander as:
self.container.on('mousemove', function (e) {
var offset = self.image.offset();
console.log(offset);
var x = (e.pageX - offset.left) / self.currentscale,
y = (e.pageY - offset.top) / self.currentscale;
if(self.running) {
self.mouseLocation.x += (x - self.mouseLocation.x) * self.mouse_coord_weight;
self.mouseLocation.y += (y - self.mouseLocation.y) * self.mouse_coord_weight;
} else {
self.mouseLocation.x = x;
self.mouseLocation.y = y;
}
});

Dragging & Resizing CSS Transformed Elements

If for instance, we set a -vendor-transform: rotate(40deg) css attribute on a rectangle <div>, all the sudden dragging and resizing becomes very weird and flawed.
Here is an example with a simple jQueryUI: http://jsfiddle.net/Ja4dY/1/
You will notice, that if you drag or resize that rectangle when transformed, it will jump up or down and the cursor will not remain in the correct place. In my real code I'm using custom code for resizing and dragging, however I encountered the same problems.
Well, of course the "problem" is that the direction of an Element will change. So left can be right, top gets bottom and something inbetween and the Javascript code still handles each direction as it would be not transformed.
So, the question: How can we compensate transformed / rotated Elements ?
Any good resources / books / blogs are also very welcome.
You can get the current transformation matrix that is applied to an element by using getComputedStyle(). You can use this to transform the current mouse position to its position in transformed space and see whether the click/drag events are within the element boundary and/or corners. Good resources for this:
http://www.useragentman.com/blog/2011/01/07/css3-matrix-transform-for-the-mathematically-challenged/
http://www.eleqtriq.com/2010/05/css-3d-matrix-transformations/
BTW, as you're experiencing, this is non-trivial to code. We had to do it for Sencha Animator, and it was a beast.
The problem is that functions that make elements draggable, wether using jQuery UI or not, relies heavily on the native getBoundingClientRect() function to figure out the position of the element etc.
When applying CSS3 transforms, like rotation, the values of getBoundingClientRect() or the equalent jQuery offset() function used in jQuery UI no longer works as expected, and the position of the mouse pointer gets messed up because the size of the element is suddenly wrong after it has been rotated.
To fix it you need to add some sort of helper function that recalculates the values, and there is a monkey patch available for this that works with jQuery UI's draggable.
It's hard to say anything about how to make the same patch work for custom code, but you'll probably have to integrate it in your custom function somehow, and it will take some coding on your part, and it's even harder to come up with something that works as a helper function out of the box for custom code one has not seen, and be aware that it is rather involved doing these calculations, see the code below :
function monkeyPatch_mouseStart() {
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;
}
}
}
this.helper = this._createHelper(event);
this._cacheHelperProportions();
if($.ui.ddmanager)
$.ui.ddmanager.current = this;
this._cacheMargins();
this.cssPosition = this.helper.css("position");
this.scrollParent = this.helper.scrollParent();
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: {
left: event.pageX - this.offset.left,
top: event.pageY - this.offset.top
},
parent: this._getParentOffset(),
relative: this._getRelativeOffset()
});
this.originalPosition = this.position = this._generatePosition(event);
this.originalPageX = event.pageX;
this.originalPageY = event.pageY;
(o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt));
if(o.containment)
this._setContainment();
if(this._trigger("start", event) === false) {
this._clear();
return false;
}
this._cacheHelperProportions();
if ($.ui.ddmanager && !o.dropBehaviour)
$.ui.ddmanager.prepareOffsets(this, event);
this.helper.addClass("ui-draggable-dragging");
this._mouseDrag(event, true);
if ( $.ui.ddmanager ) $.ui.ddmanager.dragStart(this, event);
return true;
};
}
monkeyPatch_mouseStart();
And here's a FIDDLE showing it working as expected with jQuery UI's draggable and resizeable !
I found this... It's a working example plus info, demo and download link.
jquery-ui-rotation-using-css-transform -> live-demo
He use his own libraries, but if you are interest in the subject, you can read and learn how he get it.
cheers and good luck.
Gmo.-
Btw, the web is in Russian, but with google translate you can manage ;-)
It is not bug in jQuery. Simply it is not supported. If you check jQuery UI source code you will figure out that it doesn't use transformation matrix to calculate difference between transformed object and page.
Your example, and probably every jQ UI drag implementation suffer from this issue cause of 2 methods in JQ UI source code (around 314 line of jquery.ui.draggable.js file v1.8.23 ). Calculated offset do not matter about change in offset since rotation is done over center of element.
You have to calculate what is that change. Here is workaround, quick and dirty. The idea is to check what is difference in bounding box of transformed element.
Check sample here http://jsfiddle.net/mjaric/9Nqrh/
Ignore part with first two rotations, they are just done to minimize lines of code. Third involves translation of coordinate system for calculated difference. It will offset left and top after translation is performed (note it is first in filter).
If you want to avoid first two rotation filters, You could make code using formula for 2D rotation:
x' = x cos f - y sin f
y' = y cos f + x sin f
where f is angle of rotation, but it's not that simple and also includes more lines of code where you have to calculate what is diagonal angle of original bounding box since you need initial angle of top left corner which x and y coords are comparing to x axis (positive part). Then calculate change in x-x' and y-y'. But I'm predicting some issues with sign of change and coding/debugging would take more time then I have right now. Sorry cause of that but I'm sure you can figure out what to do after reading this post.
It looks better if we override the cursorAt:
$("#foo").mousedown(function (e) {
var x = e.pageX - this.offsetLeft;
var y = e.pageY - this.offsetTop;
console.log(x);
$("#foo").draggable("option", "cursorAt", {left: x, top:y});
});
Updated fiddle: http://jsfiddle.net/johnkoer/Ja4dY/8/
​
You said you are not interested with JQuery solutions then,
One solution is;
I recommend you to write your own drag and resize functions. You can
handle resizing and draging on rotated objects to add their top and left with sine and cosine of that degree.
Another solution is;
You can use libraries like Raphael JS to create objects to transform,
drag and resize. Raphael JS uses svg!
For more information about Raphael JS
Yet another solution is;
If you do not want to use library like Raphael JS, you can directly use SVG with JQuery
For more information about SVG
Cannot write more details now, I expand this solutions tomorrow.
Hope these help for now.
This, indeed, seems to be a bug in jQuery. An easy workaround would be: surround the resizable div with a container div. Set the .draggable() to the outer div and .resizable() to the inner div. This seems to be working fine in Chromium running on Ubuntu. See Fiddle.
I've colored the outer div to give you an idea what's happening under the hood.

how to find coordinates of an HTML button or image, cross browser?

There was a thread on this in comp.lang.javascript recently where
victory was announced but no code was posted:
On an HTML page how do you find the lower left corner coordinates of an element (image or button, say)
reliably across browsers and page styles? The method advocated in "Ajax in Action" (copy I have) doesn't seem to work in IE under some circumstances. To make the problem easier, let's assume we can set the global document style to be "traditional" or "transitional" or whatever.
Please provide code or a pointer to code please (a complete function that works on all browsers) -- don't just say "that's easy" and blather about what traversing the DOM -- if I want to read that kind of thing I'll go back to comp.lang.javascript. Please scold me if this is a repeat and point me to the solution -- I did try to find it.
In my experience, the only sure-fire way to get stuff like this to work is using JQuery (don't be afraid, it's just an external script file you have to include). Then you can use a statement like
$('#element').position()
or
$('#element').offset()
to get the current coordinates, which works excellently across any and all browsers I've encountered so far.
I found this Solution from the web... This Totally Solved my Problem.
Please check this link for the origin.
http://www.quirksmode.org/js/findpos.html
/** This script finds the real position,
* so if you resize the page and run the script again,
* it points to the correct new position of the element.
*/
function findPos(obj){
var curleft = 0;
var curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return {X:curleft,Y:curtop};
}
}
Works Perfectly in Firefox, IE8, Opera (Hope in others too)
Thanks to those who share their knowledge...
Regards,
ADynaMic
I have been using this, works for both IE and Firefox.
var Target = document.getElementById('SomeID');
var Pos = findPos(Target);
AnotherObj = document.getElementById('AnotherID');
AnotherObj .style.top = Pos[1] + "px";
AnotherObj .style.left = Pos[0] + "px";
//------------------------------------
function findPos(obj) {
//----------------------------------------
var curleft = curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return [curleft,curtop];
}
}
In jQuery:
var jq = $('#yourElement');
var position = jq.offset();
alert('x: ' + position.left + ', y: ' + position.top);
var bottomLeftPixelPosition =
{ left: position.left, top: position.top + jq.height() - 1; };
Try this:
var elm = document.getElementById('foo');
var point = {x:0,y:elm.offsetHeight}; // Change to y:0 to get the top-left
while (elm)
{
// This will get you the position relative to the absolute container,
// which is what you need for positioning an element within it
if (elm.style.position == 'absolute')
break;
point.x += elm.offsetLeft;
point.y += elm.offsetTop;
elm = elm.offsetParent;
}

Categories