jQuery UI draggable and resizable combination


I’ve just taken a look at how a rich interface for creating invitations could be created with jQuery UI’s draggables and resizables.

The goal here is to allow two basic elements to be dragged and resized in a work area to which they are limited, texts and images. They are not to be allowed to be dragged or scaled outside of the work area.

Each element should have two icons attached to it, a dragger which when moved will make the parent text/image element to move with it, and a close button which will remove the element.

Here is a demo of the below code (works only in firefox, probably some stylesheet problem).

Update: I just found a minor bug, I’ll leave it as an exercise to try and find it and fix it :)

Let’s walk the code from top to bottom.

<html>
<head><title>jQuery UI</title>
<link rel="stylesheet" href="style.css" type="text/css" />
<link rel="stylesheet" href="themes/flora/flora.css" type="text/css" />
<link rel="stylesheet" href="themes/flora/flora.resizable.css" type="text/css" />
<script src="jquery-1.2.6.js"></script>
<script src="ui/ui.core.js"></script>
<script src="ui/ui.draggable.js"></script>
<script src="ui/ui.resizable.js"></script>
<script>

We include everything we need, note the flora theme CSS, they’re needed to style the frames of the resizables.

jQuery.fn.stayInBox = function(box){
	var thisPos 		= this.position();
	var boxPos 			= box.position();
	var diff_right 	= (thisPos.left + this.width()) 	- (boxPos.left + box.width());
	var diff_bottom = (thisPos.top 	+ this.height()) 	- (boxPos.top + box.height());
	var diff_left 	= boxPos.left 	- thisPos.left;
	var diff_top 		= boxPos.top 		- thisPos.top;
	
	if(diff_right > 0)
		this.width(this.width() - diff_right);
		
	if(diff_bottom > 0)
		this.height(this.height() - diff_bottom);
		
	if(diff_left > 0)
		this.css("left", thisPos.left + diff_left);
		
	if(diff_top > 0)
		this.css("top", thisPos.top + diff_top);
}

Here we extend jQuery (effectively creating a small plugin) with a function that will constrain the given element to a box (DIV) we pass as the single argument. Note that I didn’t bother with collections here, see the jQuery HTML encode and decode plugin tutorial for more information on how to implement that. I really tried to make containment as an option to resizable work here but I couldn’t get it to work somehow.

Anyway, here we check if the position + width/height is more than the box’s dito, if they are we scale them back to fit within the box. We also check if we are outside to the right and top, if we are we simply move to stay within bounds.

Note the use of jQuery position(), I think that one was originally part of the dimensions plugin but is now integrated as a basic utility in UI. I found documentation on the jQuery homepage during developing this but now that I want to link to it I can’t find it. Well whatever, it simply returns {top: distance from top of parent, left: distance from left of parent}.

var elCount = 0;

function newCommon(tpl_id, sub_tag){
	$("div[id*='el_div_']").css("position", "absolute"); // 1
	
	var newDraggable = $("#"+tpl_id).clone().css("zIndex", elCount + 100).attr("id", "el_div_" + elCount)
								.addClass("resizable ui-resizable").prependTo("#workarea"); // 2
	
	newDraggable.find(".delete").click(function(){
		$(this).parent().remove(); // 3
	});
	
	var dragger = newDraggable.find(".dragger"); // 4
	dragger.mousedown(function(){ newDraggable.draggable({containment: "#workarea"}); });
	dragger.mouseup(function(){ newDraggable.draggable("disable"); });
	elCount++;
	return newDraggable;
}

ElCount is used to keep count of our draggables/resizables. NewCommon() contains all logic that is common to both text objects and image objects.

1.) We begin with setting all other objects’ position types to “absolute”. The reason for doing this is that when we put a new object on stage we don’t want the others to move as a result. As it happens resizable and/or draggable sets this attribute to “relative” all the time and we need to temporarily change it here lest we upset the positions of the other objects.

2.) We clone a DIV template already on stage (and hidden at the bottom of the document). There are two of course which we will get to in a moment, a DIV containing a text area and one containing an image. The tpl_id argument keeps track of which one we are currently working with, sub_tag keeps track of the type of tag the main content object contains, in our case “img” or “textarea“. We then set the z-index so that they don’t occupy the same layer, the new id with the help of elCount to get a unique one, add the flora themes in the form of ui-resizable and finally we add it as the first element in the work area.

3.) The delete button, will remove the element in question via parent() and remove().

4.) We find the sub-div to be used as a handle for the dragging by the dragger class. On mouse down we start dragging of course with the work area as container (we can’t drag outside of it), on mouse up we stop of course. We finish of by increasing the object count with one and return our newly created draggable/resizable.

var newImage = function(){
	var draggable = newCommon("img_div_tpl", "img");
	
	var subEl = draggable.find("img");
	draggable.width(subEl.width()).height(subEl.height()); // 1
	// 2
	draggable.resizable({
		aspectRatio: true,
		handles: "all",
		minWidth: 150,
		minHeight: 150,
		ghost: true,
		stop: function(){
			$(this).stayInBox($("#workarea"));
			$(this).find("img").width($(this).width()).height($(this).height());
		}
	});
}

So this is the one we call when we want to create an image object, newCommon is the one we just went through above.

1.) We resize the draggable/resizable to fit the resolution of the contained image.

2.) Firing up resizing, we turn on aspectRatio to keep the image’s aspect ratio, the user will probably not want warped images. We set handles all around the resizable to create a kind of border which will be used for resizing. Ghosting will make the object transparent when resizing, ie we are not resizing in real time when this one is turned on. The real resizing will take place when we stop, that’s why we also have our own stop function which will resize the image to be the area of the resizable (the opposite of #1). I tried to make alsoResize work here but couldn’t, that’s why I ended up with the customized on stop logic.

var newText = function(){
	var draggable = newCommon("txt_div_tpl", "textarea");
	draggable.resizable({
		handles: "all",
		minWidth: 160,
		minHeight: 160,
		ghost: true,
		stop: function(){
			$(this).stayInBox($("#workarea"));
			var margin = $(this).find(".dragger").width() * 2;
			$(this).find("textarea").width($(this).width() - margin).height($(this).height() - margin);
		}
	});
}

Similar to newImage() except we now maintain a margin when we scale the contained text area.

Update: The below version also works in IE7:

var newText = function(){
	var draggable = newCommon("txt_div_tpl", "textarea");
	draggable.resizable({
		handles: "all",
		minWidth: 160,
		minHeight: 160,
		ghost: true,
		alsoResize: "#" + draggable.attr("id") + " > textarea",
		stop: function(){
			$(this).stayInBox($("#workarea"));
		}
	});
}

Note the alsoResize option, to which we need to pass a selector to the contained textarea.

$(document).ready(function(){
	$("#new_text").click(newText);
	$("#new_image").click(newImage);
	$("#templates").hide();
});

At last, the ready() logic, not much to say here, note that we hide the templates right away.

</script>
<button id="new_text">New Text Field</button><button id="new_image">New Image</button>
<div id="workarea" class="workspace">
<div id="templates">
	<div id="img_div_tpl" class="img_start">
		<img src="black_reuben.png" class="img_img_start">
		<div class="delete">&nbsp;</div>
		<div class="dragger">&nbsp;</div>
	</div>
	<div id="txt_div_tpl" class="txt_start">
		<textarea class="txt_area_start">jQuery UI, JavaScript, CSS, Ajax, programming, jobs, job, work, Ruby, Rails, PHP, Java, contractor, outsourcing.</textarea>
		<div class="delete">&nbsp;</div>
		<div class="dragger">&nbsp;</div>
	</div>
</div>
</body>
</html>

The templates, buttons and the work area, finally the CSS:

.workspace{
	border:2px black solid; 
	width:500px; 
	height:500px;
}

.txt_area_start{
	width:160px;
	height:160px;
	background: #fff;
	margin: 20px 20px 20px 20px;
	border:0;
	overflow: hidden;
}

.img_img_start{
	margin: 0;
	border:0;
}

.img_start{
	position: absolute;
	top: 0;
	left: 0;
}

.txt_start{
	width:200px;
	height:200px;
	position: absolute;
	top: 0;
	left: 0;
}

.dragger{
	background: url(images/drag.png);
	width:20px; 
	height:20px;
	position: absolute;
	bottom: -15;
	left: -15;
	
}

.resizer{
	background: url(images/zoom.png);
	clear: both;
	width:20px; 
	height:20px;
	position: absolute;
	bottom: 0;
	right: 0;
}

.delete{
	background: url(images/delete.png);
	width:20px; 
	height:20px;
	position: absolute;
	top: -15;
	right: -15;
}

Note how we manage displaying the dragger and delete button on the outside of the parent div by setting the margins, I’ve got no idea if this works in IE or not.

Related Posts

Tags: , , , , ,