quinta-feira, 8 de janeiro de 2015

jQuery UI Draggable / Resizable with constrain and CSS scale transformation

There are a bunch of questions and solutions to the problems between the interaction of these technologies, these are the better solutions that I found:
  1. http://stackoverflow.com/questions/10212683/jquery-drag-resize-with-css-transform-scale
  2. https://gungfoo.wordpress.com/2013/02/15/jquery-ui-resizabledraggable-with-transform-scale-set/
  3. http://stackoverflow.com/questions/17098464/jquery-ui-draggable-css-transform-causes-jumping
Although they make an improvement, they aren't totally accurate. What they basically do is a calculation of the right position / size that the object must have after the interaction.

You can experiment with the example below, drag the small square to right. Your mouse will be outside the parent div before you hit the edge.

And now, the same example, with the solution presented in [3].

Nice, now the small square moves with the mouse, but it go beyond the parent when dragging or resizing and this was the tricky part to solve. I spend a day trying to figure out how I could solve this, so I'm sharing my solution.

First let's solve the draggable problem. The "bug" (jQuery UI guys don't want to address it, so it's not a bug) occurs because inside jQuery UI it's use absolute event positions.

Think about the parent without the scale, it will be bigger, right? So it's size to jQuery UI is beyond the limit of the scaled down version. What we need to do is inform jQuery UI that our representation is smaller.

I tried in many ways not monkey patch jQuery UI, but these efforts were fruitless. I had to expose the "contaiment" var in the ui parameter passed to callbacks. With this little modification I could use the start and stop callbacks to make jQuery UI work with my scaled containment sizes.

var dragFix, startFix, stopFix;

window.myApp = {
  layout: {
    zoomScale: 1
  },
  draggable: {
    _uiHash: function() {
      return {
        helper: this.helper,
        position: this.position,
        originalPosition: this.originalPosition,
        offset: this.positionAbs,
        containment: this.containment
      };
    }
  }
};

$.ui.draggable.prototype._uiHash = myApp.draggable._uiHash;

startFix = function(event, ui) {
  ui.containment[2] *= myApp.layout.zoomScale;
  return ui.containment[3] *= myApp.layout.zoomScale;
};

stopFix = function(event, ui) {
  ui.containment[2] /= myApp.layout.zoomScale;
  return ui.containment[3] /= myApp.layout.zoomScale;
};

dragFix = function(event, ui) {
  var deltaX, deltaY;
  deltaX = ui.position.left - ui.originalPosition.left;
  deltaY = ui.position.top - ui.originalPosition.top;
  ui.position.left = ui.originalPosition.left + deltaX / myApp.layout.zoomScale;
  return ui.position.top = ui.originalPosition.top + deltaY / myApp.layout.zoomScale;
};

Nice and clean, don't you think? After solving this, I guess that making resizable works would be easy. The inner workings of this can't be very different, right? Wrong, dead wrong. What I think will be solved in 5 minutes, take the hole day.

The resizable code is very different. I expected to see the same algorithms to apply movement constraints and other operations. This way, I had to find a new way to inform jQuery UI about my constraints.

After a day tinkering with resizable code trying to find a solution that need minimal changes to jQuery UI, like with the draggable code, I was unable to find a solution that I liked.

First, I realized that I need to change the methods e, w, n and s in _change to get correct widths and heights according to my zoom scale. This was something that could be done in the "resize" callback, but in this case it's too late in the algorithm, the scaled position is needed by internal methods, before we get a chance to change it.

After this I thought that I had ended, but resizing an element that isn't in the position 0, 0 make it grow beyond the edge of the parent. Digging a bit more, I found that I need some way to change the "woset" and "hoset" calculation, but I don't find anyway to do this without monkey patching the entire method.

The final solution is this:

Maybe you are asking yourself why I'm monkey patching. This is because I'm use Rails and I want to have the benefits of the asset pipeline.

I'm not very proud of it, so I would love to know better ways to accomplish my final result in a simple manner. If you know any, please share!