Dragging & Resizing CSS Transformed Elements

10,933

Solution 1

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.

Solution 2

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 !

Solution 3

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

Solution 4

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.

Solution 5

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/

Share:
10,933

Related videos on Youtube

jAndy
Author by

jAndy

Frontend Engineer Public CV: http://careers.stackoverflow.com/jandy Contact: [email protected] Playground: CodePen Remembering that I'll be dead soon, is the most important tool I've ever encountered to help me make the big choices in Life. Because almost everything - all external expectations, all pride, all fear of embarrassment or failure, these things just fall away in the face of death, leaving only what is truly important. Remembering that you are going to die, is the best way I know to avoid the trap of thinking you have something to lose. You are already naked, there is no reason not to follow your heart. -- Steve Jobs

Updated on June 06, 2022

Comments

  • jAndy
    jAndy almost 2 years

    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.

    • John Koerner
      John Koerner almost 12 years
      It looks like you are not alone on this. Here is a jquery bug: bugs.jqueryui.com/ticket/6844
    • jAndy
      jAndy almost 12 years
      @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.
    • Roko C. Buljan
      Roko C. Buljan over 11 years
      (offtopic) Good news: Using jQuery 1.8.0+ you don't need any more the vendor's prefixes jsfiddle.net/Ja4dY/112
  • jAndy
    jAndy almost 12 years
    problem there is, it doesn't really solve anything. For the dragging it looks better, but we still don't really "know" the x and y positions for the corners for instance. The Resizing is also still broken.
  • Ben Hull
    Ben Hull over 11 years
    This demo suffers from the same problem as the original - it doesn't account for the fact that the drag handles rotate along with the object: Try spinning the globe in the demo 180deg, then dragging the left handle to the left...
  • gmo
    gmo over 11 years
    I tested in Chrome and Firefox (both in win) and the demo work's perfect. You can rotate, drag and resize whitout breaks problems.
  • jAndy
    jAndy over 11 years
    Those links are pretty useful nonetheless. I think I can do it with that help and information.
  • Ben Hull
    Ben Hull over 11 years
    Definitely broken in Safari/Chrome under OSX.
  • jAndy
    jAndy over 11 years
    solutions like yours are "ok" if you just want to somewhat workaround the weird looking dragging. After all, its still an illusion. The only real solution lays within the math and the transformation matrices. You can't fix resizing with an approach like that.
  • jAndy
    jAndy over 11 years
    I guess the author from that demo has it somewhat close. I'll have a look at it, but it still needs some optimizations.
  • Josh Deese
    Josh Deese over 11 years
    this works great for draggable, but it is not working for resizable for me. I'm using chrome and resizing still makes it jump when you start resizing. I'm having the same problem in my project, am I missing something?