1

jQuery doesn't work well with CSS "transform: scale()" (however with "transform: translate()" it works fine)

Please take a look at this simple example:

$(document).ready(function() {

  $('#root').dblclick(function() {
    $('#box').position({
      my: 'right bottom',
      at: 'right bottom',
      of: $('#root')
    });
  })

  $('#box').draggable({
    containment: $('#root'),
  });

});
body {
  position: relative;
  margin: 0;
}
#root {
  position: absolute;
  top: 20px;
  left: 20px;
  width: 500px;
  height: 500px;
  border: solid 2px red;
  transform-origin: 0 0 0;
  transform: scale(0.5);
}
#box {
  position: absolute;
  top: 100px;
  left: 50px;
  display: inline-block;
  width: 50px;
  height: 50px;
  background: red;
  border: solid 1px black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>

drag red box :)
<br/>double click in square to position box
<div id="root">
  <div id="box"></div>
</div>

Root node has to be scaled, becouse in my real app I use fullscreen mode and I need to fit content to window resolution. But when I scale parent element, jQuery UI draggable and jQuery position doesn't work properly.

Of course the question is how to make it work properly?

There are many similar question, but I didn't find proper answer.

vals
  • 61,425
  • 11
  • 89
  • 138
l00k
  • 1,525
  • 1
  • 19
  • 29

2 Answers2

3

I adapted this answer by Martii Laine to account for the containment and for the double-click positioning:

$(document).ready(function () {

    var $root = $('#root');
    var $box = $('#box');
  
    var minLeft = parseFloat($root.css("paddingLeft"));
    var minTop = parseFloat($root.css("paddingTop"));
    var maxLeft = minLeft + $root.width() - $box.outerWidth();
    var maxTop = minTop + $root.height() - $box.outerHeight();

    $root.dblclick(function () {
        $box.css({
            left: maxLeft,
            top: maxTop
        });
    })
    
    var zoom = 0.5;
    var click = { x: 0, y: 0 };
  
    $box.draggable({
        start: function (event) {
            click.x = event.clientX;
            click.y = event.clientY;
        },
      
        drag: function (event, ui) {
            var original = ui.originalPosition;
            var left = (event.clientX - click.x + original.left) / zoom;
            var top = (event.clientY - click.y + original.top) / zoom; 
            ui.position = {
                left: Math.max(minLeft, Math.min(maxLeft, left)),
                top: Math.max(minTop, Math.min(maxTop, top))
            };
        }
    });
});
body
{
    position: relative;
    margin: 0;
}

#root
{
    position: absolute;
    top: 20px;
    left: 20px;
    width: 500px;
    height: 500px;
    border: solid 2px red;
    transform-origin: 0 0 0;
    transform: scale(0.5);
}

#box
{
    position: absolute;
    top: 100px;
    left: 50px;
    display: inline-block;
    width: 50px;
    height: 50px;
    background: red;
    border: solid 1px black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>

drag red box :)
<br/>double click in square to position box
<div id="root">
    <div id="box"></div>
</div>

If other alignments are desired when double-clicking in the root div, the code:

$root.dblclick(function () {
    $box.css({
        left: maxLeft,
        top: maxTop
    });
})

can be adapted as follows:

left: minLeft,                  // Left aligned
left: maxLeft,                  // Right aligned
left: (minLeft + maxLeft) / 2,  // Centered (horizontally)

top: minTop,                    // At the top
top: maxTop,                    // At the Bottom
top: (minTop + maxTop) / 2,     // Centered (vertically)
Community
  • 1
  • 1
ConnorsFan
  • 70,558
  • 13
  • 122
  • 146
  • +0.5 for draggable - but it is still imperfect. Your solution for position is not acceptable cuz settings may vary. Thx any way – l00k Oct 23 '16 at 00:22
  • It may be left, center, right, top, bottom. I also need full functionality with collision detection, fliping etc. – l00k Oct 23 '16 at 00:42
  • I added to my answer the `left` and `top` values for the various possible alignements. – ConnorsFan Oct 23 '16 at 01:46
0

I put my content into iframe, and I applied transformation on iframe - it works properly :)

EDIT: Previous version of my solution turn out to be buggy. But if someone would like to check it I save the snippet

/*
 * jQuery UI FIX
 * Take focus on window.transformScale
 */

/*
 * Offset fix
 */
(function() {

function getWindow(elem) { return jQuery.isWindow(elem) ? elem : elem.nodeType === 9 && elem.defaultView; }

jQuery.fn.offset = function( options ) {

 // Preserve chaining for setter
 if ( arguments.length ) {
  return options === undefined ?
   this :
   this.each( function( i ) {
    jQuery.offset.setOffset( this, options, i );
   } );
 }

 var docElem, win, rect, doc,
  elem = this[ 0 ];

 if ( !elem ) {
  return;
 }

 // Support: IE <=11 only
 // Running getBoundingClientRect on a
 // disconnected node in IE throws an error
 if ( !elem.getClientRects().length ) {
  return { top: 0, left: 0 };
 }

 var transform = $(document.body).css('transform');
 $(document.body).css('transform', 'none');

 rect = elem.getBoundingClientRect();

 $(document.body).css('transform', transform);

 // Make sure element is not hidden (display: none)
 if ( rect.width || rect.height ) {
  doc = elem.ownerDocument;
  win = getWindow( doc );
  docElem = doc.documentElement;

  return {
   top: rect.top + (win.pageYOffset - docElem.clientTop) / window.transformScale,
   left: rect.left + (win.pageXOffset - docElem.clientLeft) / window.transformScale,
  };
 }

 // Return zeros for disconnected and hidden elements (gh-2310)
 return rect;
};

})();


/*
 * Position fix
 */
(function() {

var cachedScrollbarWidth,
 max = Math.max,
 abs = Math.abs,
 rhorizontal = /left|center|right/,
 rvertical = /top|center|bottom/,
 roffset = /[\+\-]\d+(\.[\d]+)?%?/,
 rposition = /^\w+/,
 rpercent = /%$/,
 _position = $.fn.position;

function getOffsets( offsets, width, height ) {
 return [
  parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
  parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
 ];
}

function parseCss( element, property ) {
 return parseInt( $.css( element, property ), 10 ) || 0;
}

function getDimensions( elem ) {
 var raw = elem[ 0 ];
 if ( raw.nodeType === 9 ) {
  return {
   width: elem.width() / window.transformScale,
   height: elem.height() / window.transformScale,
   offset: { top: 0, left: 0 }
  };
 }
 if ( $.isWindow( raw ) ) {
  return {
   width: elem.width() / window.transformScale,
   height: elem.height() / window.transformScale,
   offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
  };
 }
 if ( raw.preventDefault ) {
  return {
   width: 0,
   height: 0,
   offset: { top: raw.pageY, left: raw.pageX }
  };
 }
 return {
  width: elem.outerWidth() / window.transformScale,
  height: elem.outerHeight() / window.transformScale,
  offset: elem.offset()
 };
}

jQuery.fn.position = function( options ) {
 if ( !options || !options.of ) {
  return _position.apply( this, arguments );
 }

 // Make a copy, we don't want to modify arguments
 options = $.extend( {}, options );

 var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
  target = $( options.of ),
  within = $.position.getWithinInfo( options.within ),
  scrollInfo = $.position.getScrollInfo( within ),
  collision = ( options.collision || "flip" ).split( " " ),
  offsets = {};

 dimensions = getDimensions( target );
 if ( target[ 0 ].preventDefault ) {

  // Force left top to allow flipping
  options.at = "left top";
 }
 targetWidth = dimensions.width;
 targetHeight = dimensions.height;
 targetOffset = dimensions.offset;

 // Clone to reuse original targetOffset later
 basePosition = $.extend( {}, targetOffset );

 // Force my and at to have valid horizontal and vertical positions
 // if a value is missing or invalid, it will be converted to center
 $.each( [ "my", "at" ], function() {
  var pos = ( options[ this ] || "" ).split( " " ),
   horizontalOffset,
   verticalOffset;

  if ( pos.length === 1 ) {
   pos = rhorizontal.test( pos[ 0 ] ) ?
    pos.concat( [ "center" ] ) :
    rvertical.test( pos[ 0 ] ) ?
     [ "center" ].concat( pos ) :
     [ "center", "center" ];
  }
  pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
  pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";

  // Calculate offsets
  horizontalOffset = roffset.exec( pos[ 0 ] );
  verticalOffset = roffset.exec( pos[ 1 ] );
  offsets[ this ] = [
   horizontalOffset ? horizontalOffset[ 0 ] : 0,
   verticalOffset ? verticalOffset[ 0 ] : 0
  ];

  // Reduce to just the positions without the offsets
  options[ this ] = [
   rposition.exec( pos[ 0 ] )[ 0 ],
   rposition.exec( pos[ 1 ] )[ 0 ]
  ];
 } );

 // Normalize collision option
 if ( collision.length === 1 ) {
  collision[ 1 ] = collision[ 0 ];
 }

 if ( options.at[ 0 ] === "right" ) {
  basePosition.left += targetWidth;
 } else if ( options.at[ 0 ] === "center" ) {
  basePosition.left += targetWidth / 2;
 }

 if ( options.at[ 1 ] === "bottom" ) {
  basePosition.top += targetHeight;
 } else if ( options.at[ 1 ] === "center" ) {
  basePosition.top += targetHeight / 2;
 }

 atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
 basePosition.left += atOffset[ 0 ];
 basePosition.top += atOffset[ 1 ];

 return this.each( function() {
  var collisionPosition, using,
   elem = $( this ),
   elemWidth = elem.outerWidth() / window.transformScale,
   elemHeight = elem.outerHeight() / window.transformScale,
   marginLeft = parseCss( this, "marginLeft" ),
   marginTop = parseCss( this, "marginTop" ),
   collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) +
    scrollInfo.width,
   collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) +
    scrollInfo.height,
   position = $.extend( {}, basePosition ),
   myOffset = getOffsets( offsets.my, elem.outerWidth() / window.transformScale, elem.outerHeight() / window.transformScale );

  if ( options.my[ 0 ] === "right" ) {
   position.left -= elemWidth;
  } else if ( options.my[ 0 ] === "center" ) {
   position.left -= elemWidth / 2;
  }

  if ( options.my[ 1 ] === "bottom" ) {
   position.top -= elemHeight;
  } else if ( options.my[ 1 ] === "center" ) {
   position.top -= elemHeight / 2;
  }

  position.left += myOffset[ 0 ];
  position.top += myOffset[ 1 ];

  collisionPosition = {
   marginLeft: marginLeft,
   marginTop: marginTop
  };

  $.each( [ "left", "top" ], function( i, dir ) {
   if ( jQuery.ui.position[ collision[ i ] ] ) {
    jQuery.ui.position[ collision[ i ] ][ dir ]( position, {
     targetWidth: targetWidth,
     targetHeight: targetHeight,
     elemWidth: elemWidth,
     elemHeight: elemHeight,
     collisionPosition: collisionPosition,
     collisionWidth: collisionWidth,
     collisionHeight: collisionHeight,
     offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
     my: options.my,
     at: options.at,
     within: within,
     elem: elem
    } );
   }
  } );

  if ( options.using ) {

   // Adds feedback as second argument to using callback, if present
   using = function( props ) {
    var left = targetOffset.left - position.left,
     right = left + targetWidth - elemWidth,
     top = targetOffset.top - position.top,
     bottom = top + targetHeight - elemHeight,
     feedback = {
      target: {
       element: target,
       left: targetOffset.left,
       top: targetOffset.top,
       width: targetWidth,
       height: targetHeight
      },
      element: {
       element: elem,
       left: position.left,
       top: position.top,
       width: elemWidth,
       height: elemHeight
      },
      horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
      vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
     };
    if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
     feedback.horizontal = "center";
    }
    if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
     feedback.vertical = "middle";
    }
    if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
     feedback.important = "horizontal";
    } else {
     feedback.important = "vertical";
    }
    options.using.call( this, props, feedback );
   };
  }

  elem.offset( $.extend( position, { using: using } ) );
 } );
};

})();


/*
 * Draggable fix
 */
(function() {

jQuery.ui.draggable.prototype._refreshOffsets = function( event ) {
 this.offset = {
  top: this.positionAbs.top - this.margins.top,
  left: this.positionAbs.left - this.margins.left,
  scroll: false,
  parent: this._getParentOffset(),
  relative: this._getRelativeOffset()
 };

 this.offset.click = {
  left: event.pageX / window.transformScale - this.offset.left,
  top: event.pageY / window.transformScale - this.offset.top
 };
};

jQuery.ui.draggable.prototype._generatePosition = function( event, constrainPosition ) {

 var containment, co, top, left,
  o = this.options,
  scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ),
  pageX = event.pageX / window.transformScale,
  pageY = event.pageY / window.transformScale;

 // Cache the scroll
 if ( !scrollIsRootNode || !this.offset.scroll ) {
  this.offset.scroll = {
   top: this.scrollParent.scrollTop(),
   left: this.scrollParent.scrollLeft()
  };
 }

 /*
  * - Position constraining -
  * Constrain the position to a mix of grid, containment.
  */

 // If we are not dragging yet, we won't check for options
 if ( constrainPosition ) {
  if ( this.containment ) {
   if ( this.relativeContainer ) {
    co = this.relativeContainer.offset();
    containment = [
     this.containment[ 0 ] + co.left,
     this.containment[ 1 ] + co.top,
     this.containment[ 2 ] + co.left,
     this.containment[ 3 ] + co.top
    ];
   } else {
    containment = this.containment;
   }

   var width = 0;
   var height = 0;
   if(window.transformScale != 1)
   {
    var width = this.helper.outerWidth();
    var height = this.helper.outerHeight();
   }

   if ( pageX - this.offset.click.left < containment[ 0 ] ) {
    pageX = containment[ 0 ] + this.offset.click.left;
   }
   if ( pageY - this.offset.click.top < containment[ 1 ] ) {
    pageY = containment[ 1 ] + this.offset.click.top;
   }
   if ( pageX - this.offset.click.left + width > containment[ 2 ] ) {
    pageX = containment[ 2 ] + this.offset.click.left - width;
   }
   if ( pageY - this.offset.click.top + height > containment[ 3 ] ) {
    pageY = containment[ 3 ] + this.offset.click.top - height;
   }
  }

  if ( o.grid ) {

   //Check for grid elements set to 0 to prevent divide by 0 error causing invalid
   // argument errors in IE (see ticket #6950)
   top = o.grid[ 1 ] ? this.originalPageY + Math.round( ( pageY -
    this.originalPageY ) / o.grid[ 1 ] ) * o.grid[ 1 ] : this.originalPageY;
   pageY = containment ? ( ( top - this.offset.click.top >= containment[ 1 ] ||
    top - this.offset.click.top > containment[ 3 ] ) ?
     top :
     ( ( top - this.offset.click.top >= containment[ 1 ] ) ?
      top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : top;

   left = o.grid[ 0 ] ? this.originalPageX +
    Math.round( ( pageX - this.originalPageX ) / o.grid[ 0 ] ) * o.grid[ 0 ] :
    this.originalPageX;
   pageX = containment ? ( ( left - this.offset.click.left >= containment[ 0 ] ||
    left - this.offset.click.left > containment[ 2 ] ) ?
     left :
     ( ( left - this.offset.click.left >= containment[ 0 ] ) ?
      left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : left;
  }

  if ( o.axis === "y" ) {
   pageX = this.originalPageX;
  }

  if ( o.axis === "x" ) {
   pageY = this.originalPageY;
  }
 }

 return {
  top: (

   // The absolute mouse position
   pageY -

   // Click offset (relative to the element)
   this.offset.click.top -

   // Only for relative positioned nodes: Relative offset from element to offset parent
   this.offset.relative.top -

   // The offsetParent's offset without borders (offset + border)
   this.offset.parent.top +
   ( this.cssPosition === "fixed" ?
    -this.offset.scroll.top :
    ( scrollIsRootNode ? 0 : this.offset.scroll.top ) )
  ),
  left: (

   // The absolute mouse position
   pageX -

   // Click offset (relative to the element)
   this.offset.click.left -

   // Only for relative positioned nodes: Relative offset from element to offset parent
   this.offset.relative.left -

   // The offsetParent's offset without borders (offset + border)
   this.offset.parent.left +
   ( this.cssPosition === "fixed" ?
    -this.offset.scroll.left :
    ( scrollIsRootNode ? 0 : this.offset.scroll.left ) )
  )
 };

};

jQuery.ui.draggable.prototype._mouseStart = function( event ) {

 var o = this.options;

 //Create and append the visible helper
 this.helper = this._createHelper( event );

 this._addClass( this.helper, "ui-draggable-dragging" );

 //Cache the helper size
 this._cacheHelperProportions();

 //If ddmanager is used for droppables, set the global draggable
 if ( jQuery.ui.ddmanager ) {
  jQuery.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( true );
 this.offsetParent = this.helper.offsetParent();
 this.hasFixedAncestor = this.helper.parents().filter( function() {
   return $( this ).css( "position" ) === "fixed";
  } ).length > 0;

 //The element's absolute position on the page minus margins
 this.positionAbs = this.element.offset();
 this._refreshOffsets( event );

 //Generate the original position
 this.originalPosition = this.position = this._generatePosition( event, false );
 this.originalPageX = event.pageX / window.transformScale;
 this.originalPageY = event.pageY / window.transformScale;

 //Adjust the mouse offset relative to the helper if "cursorAt" is supplied
 ( o.cursorAt && this._adjustOffsetFromHelper( o.cursorAt ) );

 //Set a containment if given in the options
 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 ( jQuery.ui.ddmanager && !o.dropBehaviour ) {
  jQuery.ui.ddmanager.prepareOffsets( this, event );
 }

 // Execute the drag once - this causes the helper not to be visible before getting its
 // correct position
 this._mouseDrag( event, true );

 // If the ddmanager is used for droppables, inform the manager that dragging has started
 // (see #5003)
 if ( jQuery.ui.ddmanager ) {
  jQuery.ui.ddmanager.dragStart( this, event );
 }

 return true;

};

jQuery.ui.draggable.prototype._mouseDrag = function( event, noPropagation ) {

 // reset any necessary cached properties (see #5009)
 if ( this.hasFixedAncestor ) {
  this.offset.parent = this._getParentOffset();
 }

 //Compute the helpers position
 this.position = this._generatePosition( event, true );
 this.positionAbs = this._convertPositionTo( "absolute" );

 //Call plugins and callbacks and use the resulting position if something is returned
 if ( !noPropagation ) {
  var ui = this._uiHash();
  if ( this._trigger( "drag", event, ui ) === false ) {
   this._mouseUp( new $.Event( "mouseup", event ) );
   return false;
  }
  this.position = ui.position;
 }

 this.helper[ 0 ].style.left = this.position.left + "px";
 this.helper[ 0 ].style.top = this.position.top + "px";

 if ( jQuery.ui.ddmanager ) {
  jQuery.ui.ddmanager.drag( this, event );
 }

 return false;
};

})();
body {
 position: relative;
 margin:  0;
 transform-origin: 0 0 0;
}

#root {
 position: fixed;
 top:  20px;
 left:  20px;

 width:  500px;
 height:  500px;

 border:  solid 2px green;

}

#box {
 position: absolute;
 top:  100px;
 left:  50px;

 display: inline-block;
 width:  50px;
 height:  50px;

 background: yellow;
 border:  solid 2px black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script>
/*
 * EXAMPLE CODE
 */
// this variable is required to make extension work
window.transformScale = 0.5;

$(document).ready(function() {
  $(document.body).attr('style', 'transform: scale('+ window.transformScale +')');

  $('#root').dblclick(function() {
    $('#box').position({
      my: 'right bottom',
      at: 'right bottom',
      of: $('#root')
    });
  })

  $('#box').draggable({
    containment: $('#root'),
  });

});
</script>

<div id="root">
  <div id="box"></div>
</div>
l00k
  • 1,525
  • 1
  • 19
  • 29