45
votes

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.

7
It looks like you are not alone on this. Here is a jquery bug: bugs.jqueryui.com/ticket/6844John Koerner
@JohnKoerner: indeed. But I'm not particulary interested in a jQuery(UI) solution. So far the links which were provided by MichaelMullany were pretty helpful.jAndy
(offtopic) Good news: Using jQuery 1.8.0+ you don't need any more the vendor's prefixes jsfiddle.net/Ja4dY/112Roko C. Buljan

7 Answers

15
votes

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.

7
votes

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 !

5
votes

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 ;-)

3
votes

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.

1
votes

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/

0
votes

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.

0
votes

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.