Index: admin/categories.php
===================================================================
--- admin/categories.php	(revision 1531)
+++ admin/categories.php	(revision 1532)
@@ -55,4 +55,17 @@
 }
 
+# Update order
+if (!empty($_POST['categories_order']))
+{
+        $categories = json_decode($_POST['categories_order']);
+
+	foreach ($categories as $category) {
+	        if (!empty($category->item_id)) {
+		        $core->blog->updCategoryPosition($category->item_id, $category->left, $category->right);
+		}
+	}
+	http::redirect('categories.php?reord=1');
+}
+
 # Reset order
 if (!empty($_POST['reset']))
@@ -78,7 +91,16 @@
 /* Display
 -------------------------------------------------------- */
-dcPage::open(__('Categories'),
-	dcPage::jsToolMan()."\n".
-	dcPage::jsLoad('js/_categories.js'),
+$rs = $core->blog->getCategories(array('post_type'=>'post'));
+
+$starting_script = dcPage::jsToolMan()."\n";
+if (!$core->auth->user_prefs->accessibility->nodragdrop
+	&& $core->auth->check('categories',$core->blog->id)
+	&& $rs->count()>1) {
+        $starting_script .= dcPage::jsLoad('js/jquery/jquery-ui.custom.js');
+	$starting_script .= dcPage::jsLoad('js/jquery/jquery.mjs.nestedSortable.js');
+}
+$starting_script .= dcPage::jsLoad('js/_categories.js');
+
+dcPage::open(__('Categories'),$starting_script,
 	dcPage::breadcrumb(
 		array(
@@ -99,6 +121,4 @@
 }
 
-$rs = $core->blog->getCategories(array('post_type'=>'post'));
-
 $categories_combo = array();
 if (!$rs->isEmpty())
@@ -131,5 +151,5 @@
 	while ($rs->fetch())
 	{
-		$attr = 'id="cat'.$rs->cat_id.'"';
+		$attr = 'id="cat_'.$rs->cat_id.'"';
 		if ($rs->nb_total == 0) {
 			$attr .= ' class="deletable"';
@@ -188,4 +208,18 @@
 	'</form>'.
 	'</div>';
+
+	if (!$core->auth->user_prefs->accessibility->nodragdrop
+		&& $core->auth->check('categories',$core->blog->id)
+		&& $rs->count()>1) {
+		echo
+		'<div class="col clear">'.
+		'<form action="categories.php" method="post">'.
+		'<p>'.
+		'<input type="hidden" id="categories_order" name="categories_order" value=""/>'.
+		'<input id="save-set-order" type="submit" value="'.__('Save categories order').'"/>'.
+		$core->formNonce().'</p>'.
+		'</form>'.
+		'</div>';
+	}
 }
 
Index: admin/js/_categories.js
===================================================================
--- admin/js/_categories.js	(revision 1531)
+++ admin/js/_categories.js	(revision 1532)
@@ -1,4 +1,17 @@
-/* TODO: Some nice drag and drop on categories */
 $(function() {
+	if ($.fn['nestedSortable']!==undefined) {
+		$('#categories ul li').css('cursor','move');
+		$('#save-set-order').prop('disabled',true).addClass('disabled');
+		$('#categories ul').nestedSortable({
+			listType: 'ul',
+			items: 'li',
+			placeholder: 'placeholder',
+			update: function() {
+				$('#categories_order').attr('value',JSON.stringify($('#categories ul').nestedSortable('toArray')));
+				$('#save-set-order').prop('disabled',false).removeClass('disabled');
+			}
+		});
+	}
+
 	$('.checkboxes-helpers').each(function() {
 		dotclear.checkboxesHelpers(this);
Index: admin/js/jquery/jquery.mjs.nestedSortable.js
===================================================================
--- admin/js/jquery/jquery.mjs.nestedSortable.js	(revision 1532)
+++ admin/js/jquery/jquery.mjs.nestedSortable.js	(revision 1532)
@@ -0,0 +1,429 @@
+/*
+ * jQuery UI Nested Sortable
+ * v 1.3.5 / 21 jun 2012
+ * http://mjsarfatti.com/code/nestedSortable
+ *
+ * Depends on:
+ *	 jquery.ui.sortable.js 1.8+
+ *
+ * Copyright (c) 2010-2012 Manuele J Sarfatti
+ * Licensed under the MIT License
+ * http://www.opensource.org/licenses/mit-license.php
+ */
+
+(function($) {
+
+	$.widget("mjs.nestedSortable", $.extend({}, $.ui.sortable.prototype, {
+
+		options: {
+			tabSize: 20,
+			disableNesting: 'mjs-nestedSortable-no-nesting',
+			errorClass: 'mjs-nestedSortable-error',
+			doNotClear: false,
+			listType: 'ol',
+			maxLevels: 0,
+			protectRoot: false,
+			rootID: null,
+			rtl: false,
+			isAllowed: function(item, parent) { return true; }
+		},
+
+		_create: function() {
+			this.element.data('sortable', this.element.data('nestedSortable'));
+
+			if (!this.element.is(this.options.listType))
+				throw new Error('nestedSortable: Please check the listType option is set to your actual list type');
+
+			return $.ui.sortable.prototype._create.apply(this, arguments);
+		},
+
+		destroy: function() {
+			this.element
+				.removeData("nestedSortable")
+				.unbind(".nestedSortable");
+			return $.ui.sortable.prototype.destroy.apply(this, arguments);
+		},
+
+		_mouseDrag: function(event) {
+
+			//Compute the helpers position
+			this.position = this._generatePosition(event);
+			this.positionAbs = this._convertPositionTo("absolute");
+
+			if (!this.lastPositionAbs) {
+				this.lastPositionAbs = this.positionAbs;
+			}
+
+			var o = this.options;
+
+			//Do scrolling
+			if(this.options.scroll) {
+				var scrolled = false;
+				if(this.scrollParent[0] != document && this.scrollParent[0].tagName != 'HTML') {
+
+					if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity)
+						this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed;
+					else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity)
+						this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed;
+
+					if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity)
+						this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed;
+					else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity)
+						this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed;
+
+				} else {
+
+					if(event.pageY - $(document).scrollTop() < o.scrollSensitivity)
+						scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
+					else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity)
+						scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
+
+					if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity)
+						scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
+					else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity)
+						scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);
+
+				}
+
+				if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour)
+					$.ui.ddmanager.prepareOffsets(this, event);
+			}
+
+			//Regenerate the absolute position used for position checks
+			this.positionAbs = this._convertPositionTo("absolute");
+
+      // Find the top offset before rearrangement,
+      var previousTopOffset = this.placeholder.offset().top;
+
+			//Set the helper position
+			if(!this.options.axis || this.options.axis != "y") this.helper[0].style.left = this.position.left+'px';
+			if(!this.options.axis || this.options.axis != "x") this.helper[0].style.top = this.position.top+'px';
+
+			//Rearrange
+			for (var i = this.items.length - 1; i >= 0; i--) {
+
+				//Cache variables and intersection, continue if no intersection
+				var item = this.items[i], itemElement = item.item[0], intersection = this._intersectsWithPointer(item);
+				if (!intersection) continue;
+
+				if(itemElement != this.currentItem[0] //cannot intersect with itself
+					&&	this.placeholder[intersection == 1 ? "next" : "prev"]()[0] != itemElement //no useless actions that have been done before
+					&&	!$.contains(this.placeholder[0], itemElement) //no action if the item moved is the parent of the item checked
+					&& (this.options.type == 'semi-dynamic' ? !$.contains(this.element[0], itemElement) : true)
+					//&& itemElement.parentNode == this.placeholder[0].parentNode // only rearrange items within the same container
+				) {
+
+					$(itemElement).mouseenter();
+
+					this.direction = intersection == 1 ? "down" : "up";
+
+					if (this.options.tolerance == "pointer" || this._intersectsWithSides(item)) {
+						$(itemElement).mouseleave();
+						this._rearrange(event, item);
+					} else {
+						break;
+					}
+
+					// Clear emtpy ul's/ol's
+					this._clearEmpty(itemElement);
+
+					this._trigger("change", event, this._uiHash());
+					break;
+				}
+			}
+
+			var parentItem = (this.placeholder[0].parentNode.parentNode &&
+							 $(this.placeholder[0].parentNode.parentNode).closest('.ui-sortable').length)
+				       			? $(this.placeholder[0].parentNode.parentNode)
+				       			: null,
+			    level = this._getLevel(this.placeholder),
+			    childLevels = this._getChildLevels(this.helper);
+
+      // To find the previous sibling in the list, keep backtracking until we hit a valid list item.
+			var previousItem = this.placeholder[0].previousSibling ? $(this.placeholder[0].previousSibling) : null;
+			if (previousItem != null) {
+				while (previousItem[0].nodeName.toLowerCase() != 'li' || previousItem[0] == this.currentItem[0] || previousItem[0] == this.helper[0]) {
+					if (previousItem[0].previousSibling) {
+						previousItem = $(previousItem[0].previousSibling);
+					} else {
+						previousItem = null;
+						break;
+					}
+				}
+			}
+
+      // To find the next sibling in the list, keep stepping forward until we hit a valid list item.
+      var nextItem = this.placeholder[0].nextSibling ? $(this.placeholder[0].nextSibling) : null;
+      if (nextItem != null) {
+        while (nextItem[0].nodeName.toLowerCase() != 'li' || nextItem[0] == this.currentItem[0] || nextItem[0] == this.helper[0]) {
+          if (nextItem[0].nextSibling) {
+            nextItem = $(nextItem[0].nextSibling);
+          } else {
+            nextItem = null;
+            break;
+          }
+        }
+      }
+
+			var newList = document.createElement(o.listType);
+
+			this.beyondMaxLevels = 0;
+			
+			// If the item is moved to the left, send it to its parent's level unless there are siblings below it.
+			if (parentItem != null && nextItem == null &&
+					(o.rtl && (this.positionAbs.left + this.helper.outerWidth() > parentItem.offset().left + parentItem.outerWidth()) ||
+					!o.rtl && (this.positionAbs.left < parentItem.offset().left))) {
+				parentItem.after(this.placeholder[0]);
+				this._clearEmpty(parentItem[0]);
+				this._trigger("change", event, this._uiHash());
+			}
+			// If the item is below a sibling and is moved to the right, make it a child of that sibling.
+			else if (previousItem != null &&
+						(o.rtl && (this.positionAbs.left + this.helper.outerWidth() < previousItem.offset().left + previousItem.outerWidth() - o.tabSize) ||
+						!o.rtl && (this.positionAbs.left > previousItem.offset().left + o.tabSize))) {
+				this._isAllowed(previousItem, level, level+childLevels+1);
+				if (!previousItem.children(o.listType).length) {
+					previousItem[0].appendChild(newList);
+				}
+        // If this item is being moved from the top, add it to the top of the list.
+        if (previousTopOffset && (previousTopOffset <= previousItem.offset().top)) {
+          previousItem.children(o.listType).prepend(this.placeholder);
+        }
+        // Otherwise, add it to the bottom of the list.
+        else {
+				  previousItem.children(o.listType)[0].appendChild(this.placeholder[0]);
+        }
+				this._trigger("change", event, this._uiHash());
+			}
+			else {
+				this._isAllowed(parentItem, level, level+childLevels);
+			}
+
+			//Post events to containers
+			this._contactContainers(event);
+
+			//Interconnect with droppables
+			if($.ui.ddmanager) $.ui.ddmanager.drag(this, event);
+
+			//Call callbacks
+			this._trigger('sort', event, this._uiHash());
+
+			this.lastPositionAbs = this.positionAbs;
+			return false;
+
+		},
+
+		_mouseStop: function(event, noPropagation) {
+
+			// If the item is in a position not allowed, send it back
+			if (this.beyondMaxLevels) {
+
+				this.placeholder.removeClass(this.options.errorClass);
+
+				if (this.domPosition.prev) {
+					$(this.domPosition.prev).after(this.placeholder);
+				} else {
+					$(this.domPosition.parent).prepend(this.placeholder);
+				}
+
+				this._trigger("revert", event, this._uiHash());
+
+			}
+
+			// Clean last empty ul/ol
+			for (var i = this.items.length - 1; i >= 0; i--) {
+				var item = this.items[i].item[0];
+				this._clearEmpty(item);
+			}
+
+			$.ui.sortable.prototype._mouseStop.apply(this, arguments);
+
+		},
+
+		serialize: function(options) {
+
+			var o = $.extend({}, this.options, options),
+				items = this._getItemsAsjQuery(o && o.connected),
+			    str = [];
+
+			$(items).each(function() {
+				var res = ($(o.item || this).attr(o.attribute || 'id') || '')
+						.match(o.expression || (/(.+)[-=_](.+)/)),
+				    pid = ($(o.item || this).parent(o.listType)
+						.parent(o.items)
+						.attr(o.attribute || 'id') || '')
+						.match(o.expression || (/(.+)[-=_](.+)/));
+
+				if (res) {
+					str.push(((o.key || res[1]) + '[' + (o.key && o.expression ? res[1] : res[2]) + ']')
+						+ '='
+						+ (pid ? (o.key && o.expression ? pid[1] : pid[2]) : o.rootID));
+				}
+			});
+
+			if(!str.length && o.key) {
+				str.push(o.key + '=');
+			}
+
+			return str.join('&');
+
+		},
+
+		toHierarchy: function(options) {
+
+			var o = $.extend({}, this.options, options),
+				sDepth = o.startDepthCount || 0,
+			    ret = [];
+
+			$(this.element).children(o.items).each(function () {
+				var level = _recursiveItems(this);
+				ret.push(level);
+			});
+
+			return ret;
+
+			function _recursiveItems(item) {
+				var id = ($(item).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/));
+				if (id) {
+					var currentItem = {"id" : id[2]};
+					if ($(item).children(o.listType).children(o.items).length > 0) {
+						currentItem.children = [];
+						$(item).children(o.listType).children(o.items).each(function() {
+							var level = _recursiveItems(this);
+							currentItem.children.push(level);
+						});
+					}
+					return currentItem;
+				}
+			}
+		},
+
+		toArray: function(options) {
+
+			var o = $.extend({}, this.options, options),
+				sDepth = o.startDepthCount || 0,
+			    ret = [],
+			    left = 2;
+
+			ret.push({
+				"item_id": o.rootID,
+				"parent_id": 'none',
+				"depth": sDepth,
+				"left": '1',
+				"right": ($(o.items, this.element).length + 1) * 2
+			});
+
+			$(this.element).children(o.items).each(function () {
+				left = _recursiveArray(this, sDepth + 1, left);
+			});
+
+			ret = ret.sort(function(a,b){ return (a.left - b.left); });
+
+			return ret;
+
+			function _recursiveArray(item, depth, left) {
+
+				var right = left + 1,
+				    id,
+				    pid;
+
+				if ($(item).children(o.listType).children(o.items).length > 0) {
+					depth ++;
+					$(item).children(o.listType).children(o.items).each(function () {
+						right = _recursiveArray($(this), depth, right);
+					});
+					depth --;
+				}
+
+				id = ($(item).attr(o.attribute || 'id')).match(o.expression || (/(.+)[-=_](.+)/));
+
+				if (depth === sDepth + 1) {
+					pid = o.rootID;
+				} else {
+					var parentItem = ($(item).parent(o.listType)
+											 .parent(o.items)
+											 .attr(o.attribute || 'id'))
+											 .match(o.expression || (/(.+)[-=_](.+)/));
+					pid = parentItem[2];
+				}
+
+				if (id) {
+						ret.push({"item_id": id[2], "parent_id": pid, "depth": depth, "left": left, "right": right});
+				}
+
+				left = right + 1;
+				return left;
+			}
+
+		},
+
+		_clearEmpty: function(item) {
+
+			var emptyList = $(item).children(this.options.listType);
+			if (emptyList.length && !emptyList.children().length && !this.options.doNotClear) {
+				emptyList.remove();
+			}
+
+		},
+
+		_getLevel: function(item) {
+
+			var level = 1;
+
+			if (this.options.listType) {
+				var list = item.closest(this.options.listType);
+				while (list && list.length > 0 && 
+                    	!list.is('.ui-sortable')) {
+					level++;
+					list = list.parent().closest(this.options.listType);
+				}
+			}
+
+			return level;
+		},
+
+		_getChildLevels: function(parent, depth) {
+			var self = this,
+			    o = this.options,
+			    result = 0;
+			depth = depth || 0;
+
+			$(parent).children(o.listType).children(o.items).each(function (index, child) {
+					result = Math.max(self._getChildLevels(child, depth + 1), result);
+			});
+
+			return depth ? result + 1 : result;
+		},
+
+		_isAllowed: function(parentItem, level, levels) {
+			var o = this.options,
+				isRoot = $(this.domPosition.parent).hasClass('ui-sortable') ? true : false,
+				maxLevels = this.placeholder.closest('.ui-sortable').nestedSortable('option', 'maxLevels'); // this takes into account the maxLevels set to the recipient list
+
+			// Is the root protected?
+			// Are we trying to nest under a no-nest?
+			// Are we nesting too deep?
+			if (!o.isAllowed(this.currentItem, parentItem) ||
+				parentItem && parentItem.hasClass(o.disableNesting) ||
+				o.protectRoot && (parentItem == null && !isRoot || isRoot && level > 1)) {
+					this.placeholder.addClass(o.errorClass);
+					if (maxLevels < levels && maxLevels != 0) {
+						this.beyondMaxLevels = levels - maxLevels;
+					} else {
+						this.beyondMaxLevels = 1;
+					}
+			} else {
+				if (maxLevels < levels && maxLevels != 0) {
+					this.placeholder.addClass(o.errorClass);
+					this.beyondMaxLevels = levels - maxLevels;
+				} else {
+					this.placeholder.removeClass(o.errorClass);
+					this.beyondMaxLevels = 0;
+				}
+			}
+		}
+
+	}));
+
+	$.mjs.nestedSortable.prototype.options = $.extend({}, $.ui.sortable.prototype.options, $.mjs.nestedSortable.prototype.options);
+})(jQuery);
Index: admin/style/default.css
===================================================================
--- admin/style/default.css	(revision 1526)
+++ admin/style/default.css	(revision 1532)
@@ -632,4 +632,21 @@
 	font-weight: bold;
 	margin-top: 2em;
+}
+#categories .placeholder {
+        outline: 1px dashed #4183C4;
+        min-height: 2.5em;
+}
+#save-set-order.disabled {
+        text-shadow: none;
+        color: #000;
+        background: #F5F5F5;
+        background: -webkit-gradient(linear, left top, left bottom, from(#DFDFDF), to(#F5F5F5));
+        background: -moz-linear-gradient(center top , #F5F5F5, #DFDFDF) repeat scroll 0 0 transparent;
+        border: 1px solid #CCCCCC;
+}
+#save-set-order.disabled:hover {
+        color: #666;
+        background: #DFDFDF;
+        border: 1px solid #CCCCCC;
 }
 /* ------------------------------------------------------------------ media */
