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.

0

2 Answers 2

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)
Sign up to request clarification or add additional context in comments.

3 Comments

+0.5 for draggable - but it is still imperfect. Your solution for position is not acceptable cuz settings may vary. Thx any way
It may be left, center, right, top, bottom. I also need full functionality with collision detection, fliping etc.
I added to my answer the left and top values for the various possible alignements.
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>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.