Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery Drag/Resize with CSS Transform Scale

I am applying a CSS transform (and the browser specific -webkit, -o etc):

transform: matrix(0.5 , 0 , 0, 0.5, 0 , 0);

to a div then using jQuery's draggable() and resizable() plugins on children of said div.

The problem I had was that when dragging or resizing the child elements, the alteration jQuery made were out of "sync" with the mouse by a factor equal to the scale applied.

I found a solution on stackoverflow (though I stupidly did not bookmark it and now cant find it....) that suggested patching the plugins, and it worked wonderfully. It went along these line:

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;      //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      //PATCH CODE     this.offset = this.positionAbs = getViewOffset(this.element[0]);     //END      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     if(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");     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) $.ui.ddmanager.dragStart(this, event);      return true;   }  }  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;     }   } } var isNumber = function(value) {   return !isNaN(parseInt(value, 10)); }; 

I have made my own changes such as (you can see on the 6-7 lines the multiplication of the movement by a "scale factor"):

 $.ui.draggable.prototype._generatePosition = function(event) {     var o = this.options, scroll = this.cssPosition == 'absolute' && !(this.scrollParent[0] != document && $.ui.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName);     var pageX = event.pageX;     var pageY = event.pageY;     //PATCH CODE     if($(this.element[0]).hasClass('item')){         pageY = this.originalPageY + ((pageY - this.originalPageY)*(1/$.viewbox.foreground.scale));         pageX = this.originalPageX + ((pageX - this.originalPageX)*(1/$.viewbox.foreground.scale));     }     //END     /*      * - Position constraining -      * Constrain the position to a mix of grid, containment.      */      if(this.originalPosition) { //If we are not dragging yet, we won't check for options        if(this.containment) {         if(event.pageX - this.offset.click.left < this.containment[0]) pageX = this.containment[0] + this.offset.click.left;         if(event.pageY - this.offset.click.top < this.containment[1]) pageY = this.containment[1] + this.offset.click.top;         if(event.pageX - this.offset.click.left > this.containment[2]) pageX = this.containment[2] + this.offset.click.left;         if(event.pageY - this.offset.click.top > this.containment[3]) pageY = this.containment[3] + this.offset.click.top;       }        if(o.grid) {         var top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1];         pageY = this.containment ? (!(top - this.offset.click.top < this.containment[1] || top - this.offset.click.top > this.containment[3]) ? top : (!(top - this.offset.click.top < this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top;          var left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0];         pageX = this.containment ? (!(left - this.offset.click.left < this.containment[0] || left - this.offset.click.left > this.containment[2]) ? left : (!(left - this.offset.click.left < this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left;       }     }     return {       top: (         pageY                               // The absolute mouse position         - this.offset.click.top                         // Click offset (relative to the element)         - this.offset.relative.top                        // Only for relative positioned nodes: Relative offset from element to offset parent         - this.offset.parent.top                        // The offsetParent's offset without borders (offset + border)         + ($.browser.safari && $.browser.version < 526 && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ))       ),       left: (         pageX                               // The absolute mouse position         - this.offset.click.left                        // Click offset (relative to the element)         - this.offset.relative.left                       // Only for relative positioned nodes: Relative offset from element to offset parent         - this.offset.parent.left                       // The offsetParent's offset without borders (offset + border)         + ($.browser.safari && $.browser.version < 526 && this.cssPosition == 'fixed' ? 0 : ( this.cssPosition == 'fixed' ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ))       )     };    } 

So big thank you to whoever suggested that.

So, my question! Has anyone come across a nice way to have draggable/resizable events inside a scaled element that doesn't require patching jQuery? I have googled, and this was the best solution I could find. Does anyone know of alternative to jquery that perhaps works under these conditions, with CSS transforms?

Many thanks for any responses.

like image 817
paullth Avatar asked Apr 18 '12 15:04

paullth


2 Answers

It's been a while since this question was asked. I have found (actually created) an answer. All it requires is setting callback handlers. No editing jquery-ui needed!

Note: zoomScale in this example is a global variable and the transform is set using animate (aided by jquery.transform.js) like so:

target.animate({     transform: 'scale(' + zoomScale + ')' }); 

Take a look at this:

transform scale() fix for resizable:

$(this).resizable({     minWidth: -(contentElem.width()) * 10,  // these need to be large and negative     minHeight: -(contentElem.height()) * 10, // so we can shrink our resizable while scaled     resize: function(event, ui) {          var changeWidth = ui.size.width - ui.originalSize.width; // find change in width         var newWidth = ui.originalSize.width + changeWidth / zoomScale; // adjust new width by our zoomScale          var changeHeight = ui.size.height - ui.originalSize.height; // find change in height         var newHeight = ui.originalSize.height + changeHeight / zoomScale; // adjust new height by our zoomScale          ui.size.width = newWidth;         ui.size.height = newHeight;      } }); 

transform scale() fix for draggable:

$(this).draggable({     handle: '.drag-handle',     start: function(event, ui) {         ui.position.left = 0;         ui.position.top = 0;     },     drag: function(event, ui) {          var changeLeft = ui.position.left - ui.originalPosition.left; // find change in left         var newLeft = ui.originalPosition.left + changeLeft / (( zoomScale)); // adjust new left by our zoomScale          var changeTop = ui.position.top - ui.originalPosition.top; // find change in top         var newTop = ui.originalPosition.top + changeTop / zoomScale; // adjust new top by our zoomScale          ui.position.left = newLeft;         ui.position.top = newTop;      } }); 

Let me know if you find any problems or further improvements on this. :)

Reference: jQuery-UI resizable/draggable with transform: scale() set

like image 170
Gung Foo Avatar answered Sep 25 '22 02:09

Gung Foo


I was trying the transform scale() fix for resizable posted by gungfoo on a element displayed at 10% of its actual size and the method didin't work. The cursor still moved away from the element during resizing.

I changed the last two lines of the resizeFix method to directly update the element's width and height and this solved my issue.

function resizeFix(event, ui) {      var changeWidth = ui.size.width - ui.originalSize.width; // find change in width     var newWidth = ui.originalSize.width + changeWidth / zoomScale; // adjust new width by our zoomScale      var changeHeight = ui.size.height - ui.originalSize.height; // find change in height     var newHeight = ui.originalSize.height + changeHeight / zoomScale; // adjust new height by our zoomScale      ui.originalElement.width(newWidth);     ui.originalElement.height(newHeight); } 
like image 42
Malcolm Wax Avatar answered Sep 25 '22 02:09

Malcolm Wax