Sorting with jQuery sortable

Update (2010-08): There is now a newer tutorial on jQuery UI’s sortable, using a much simpler GUI than in this tutorial.

I recently did a rich interface for Dive Admin to handle various configuration settings using jQuery and jQuery UI to be able to drag stuff around to control sorting. There are three possibilities:

1.) It’s possible to change headlines for various fields, in the image below we have the settings for a customer table. Clicking in the light blue area will transform it into a text field. When the enter key is pressed the new headline is saved to the database for that field.


2.) It’s possible to turn a field on or off completely by pressing the image resembling an eye.

3.) It’s possible to drag each field to change the ordering, saving the ordering is not automatic however, it has to be saved by pressing a save button that is outside the area captured in the image.

interface.gif

Anyway, if we for instance hide the Area field, drag the First Name field to the top and change Certification to Cert. Then those changes will be reflected in the add customer form, the Area field won’t show, the First Name field will be on top and Certification will now display as Cert. I think you get the picture.

Let’s walk through the code from top to bottom (the HTML is rendered with the Smarty templating engine):

{config_load file="$lang" section="Conf"}
{include file="head.tpl"}
<SCRIPT src="js/jquery.js"></SCRIPT>
<script src="js/jquery.dimensions.js"></script>
<script src="js/ui.mouse.js"></script>
<script src="js/ui.draggable.js"></script>
<script src="js/ui.droppable.js"></script>
<script src="js/ui.draggable.ext.js"></script>
<script src="js/ui.droppable.ext.js"></script>
<script src="js/ui.sortable.js"></script>

We need quite a lot of dependencies. Note that I had to comment out line 183 in ui.sortable.js due to buggy behavior. The line in question tries to remove something that is not there in the first place, at least in this implementation. Note that this problem has probably been fixed in more recent releases of the ui library.

<SCRIPT type="text/javascript">

var saved_msg = "{#saved_msg#}";

{literal}

function transClick(){
	var inner_html = $(this).html();
	var cur_id = $(this).attr('id');
	$(this).html('<input type="text" id="'+cur_id+'-input" value="'+inner_html+'" class="standard_box"/>');
}

Saved_msg is simply conversion of a Smarty config variable for use with the javascript, it has to be done outside the {literal}{/literal} escaping. The escaping is necessary because otherwise Smarty would break on all curly brackets. The transClick function is responsible for transforming each headline into a textfield and putting said headline inside the textfield’s value.

function fixInput(){
	$(this).unbind("click");
	$(this).find("input").keydown(function(event){
		if(event.keyCode == 13){
			var cur_id = $(this).attr('id').split('-').shift();
			var cur_trans = $(this).val();
			$container = $("#"+cur_id).html( cur_trans );
			$container.toggle(transClick, fixInput);
			var url = 'route.php?c=conf&f=updateTranslation';
			$.post(url, {id: cur_id, translation: cur_trans});
		}
	});
}

This function is the main driver behind the whole edit in place logic. Let’s step through each line, first we disable clicking the active area as this would screw up things if the user chooses to change marker position by clicking somewhere in the text field. Next we fetch the input field, yes we are not dealing with the field itself initially but the parent div that contains it. We check which key has been pressed, if it is the enter key (num 13) then we have to save the new headline to the database and change back to be the original text only state. We do this by first getting the id of the row in the configuration table in the database and store it in the variable cur_id. Next we fetch the new headline (translation).

Since we are inside the text field logically at the moment we have to be able to access the parent container somehow, there might be a much better way of doing this than the above way. Anyway what we are doing is setting the new inner html to the new translation/headline while at the same time getting an instance of the new state (div with only text now). Then we reapply the toggle function again so the whole process can be repeated.

Finally we call the Ajax to actually update the database, we need just the row id and the new headline.

function vizCommon(cur_el, cur_viz){
	var cur_id = cur_el.attr('id').split('-').shift();
	var url = 'route.php?c=conf&f=setVisibility';
	$.post(url, {id: cur_id, viz: cur_viz});
}

function makeVisible(){
	$(this).removeClass('invisible_eye').addClass('visible_eye');
	vizCommon($(this), 1);
}

function makeInvisible(){
	$(this).removeClass('visible_eye').addClass('invisible_eye');
	vizCommon($(this), 0);
}

The above functions are handling the visible/not visible logic. If we for instance click an eye that is not x’ed we will call makeInvisible() which will change classes to switch the appearance of the eye and then the vizCommon function which is responsible for getting the cur_id which is the id of the row in the database and then set the value with an Ajax call.

function sortUpdate(){
	var dragEls = $(".drag_box400");
	var els = '';
	jQuery.each(dragEls, function (){
		var cur_id = $(this).attr('id').split('-').shift();
		els += cur_id+" ";
	});
	var url = 'route.php?c=conf&f=setPositions';
	$.post(url, {ids: els});
}

As can be expected this function is responsible for saving the sorting :). We get all the elements that can be dragged and initialize an empty string which will contain all elements ids. Next we loop through all sortable elements and fetch their respective ids that they have in the database. No serialization is needed, we simply do a delimited string, in this case the delimiter is a white space. Finally we dispatch the string to a PHP function that will split it and update each row’s position with the help of the ids.

$(document).ready(function(){
	$(".translation").toggle(transClick, fixInput);
	$(".visible_eye").toggle(makeInvisible, makeVisible);
	$(".invisible_eye").toggle(makeVisible, makeInvisible);
	
	$("#conf_list").sortable({
		smooth: false,
	});
	
	$("#pos_button").click(function(){
		sortUpdate();
		$("#save_msg").html(saved_msg);
	});
	
});

Finally the ready() function that sets everything up. We connect the edit in place logic and the invisible/visible stuff, note that we need to keep track of which eyes are x’ed or not initially so that we start off on the right page.

Next we initialize the sorting logic, I set smooth to false, having it true made the whole interface very slow, even on my kick ass machine. Lastly we setup the save button to call the sortUpdate function that was explained above and also to display a saved confirmation message.

The HTML/Smarty:

<table>
	<tr>
		<td>
			<div>
				<ul id="conf_list">
					{foreach from=$entries item=entry}
						<li id="{$entry.id}-cont" class="drag_box400">
							<div id="{$entry.id}" class="translation">{$entry.translation}</div>
							<div class="conf_drag">&nbsp;</div>
							{if $entry.visible eq 1}
								<div id="{$entry.id}-viz" class="visible_eye">&nbsp;</div>
							{else}
								<div id="{$entry.id}-viz" class="invisible_eye">&nbsp;</div>
							{/if}
						</li>
					{/foreach}
				</ul>
			</div>
		</td>
	</tr>
	<tr>
		<td align="center">
			<br/>
			<div id="save_msg"></div>
			<br/>
			<input id="pos_button" type="button" value="{#save#}" class="standard_button">
		</td>
	</tr>
</table>

It will loop through all config rows that were fetched by the PHP script that was responsible for the initial display of the page. Note that the jQuery code in this tutorial could probably be made much smaller and niftier, I’m still just starting out with this library :).

Related Posts

Tags: , , , ,