Index: admin/index.php
===================================================================
--- admin/index.php	(revision 3781)
+++ admin/index.php	(revision 3833)
@@ -115,5 +115,5 @@
 if ($core->auth->user_prefs->dashboard->doclinks) {
     if (!empty($__resources['doc'])) {
-        $doc_links = '<div class="box small dc-box"><h3>' . __('Documentation and support') . '</h3><ul>';
+        $doc_links = '<div class="box small dc-box" id="doc-and-support"><h3>' . __('Documentation and support') . '</h3><ul>';
 
         foreach ($__resources['doc'] as $k => $v) {
@@ -150,4 +150,6 @@
 -------------------------------------------------------- */
 dcPage::open(__('Dashboard'),
+    dcPage::jsLoad('js/jquery/jquery-ui.custom.js') .
+    dcPage::jsLoad('js/jquery/jquery.ui.touch-punch.js') .
     dcPage::jsLoad('js/_index.js') .
     $admin_post_behavior .
@@ -244,32 +246,100 @@
 }
 
-# Dashboard items and contents (processed first, as we need to know the result before displaying the icons.)
-$dashboardItems = '';
-foreach ($__dashboard_items as $i) {
-    foreach ($i as $v) {
-        $dashboardItems .= $v;
-    }
-}
-$dashboardContents = '';
-foreach ($__dashboard_contents as $i) {
-    foreach ($i as $v) {
-        $dashboardContents .= $v;
-    }
-}
-
-# Dashboard elements: icons then boxes (items then contents)
-echo '<div id="dashboard-main">';
-
+# Get current main orders
+$main_order = $core->auth->user_prefs->dashboard->main_order;
+$main_order = ($main_order != '' ? explode(',', $main_order) : array());
+
+# Get current boxes orders
+$boxes_order = $core->auth->user_prefs->dashboard->boxes_order;
+$boxes_order = ($boxes_order != '' ? explode(',', $boxes_order) : array());
+
+# Get current boxes items orders
+$boxes_items_order = $core->auth->user_prefs->dashboard->boxes_items_order;
+$boxes_items_order = ($boxes_items_order != '' ? explode(',', $boxes_items_order) : array());
+
+# Get current boxes contents orders
+$boxes_contents_order = $core->auth->user_prefs->dashboard->boxes_contents_order;
+$boxes_contents_order = ($boxes_contents_order != '' ? explode(',', $boxes_contents_order) : array());
+
+$composeItems = function ($list, $blocks, $flat = false) {
+
+    $ret   = array();
+    $items = array();
+
+    if ($flat) {
+        $items = $blocks;
+    } else {
+        foreach ($blocks as $i) {
+            foreach ($i as $v) {
+                $items[] = $v;
+            }
+        }
+    }
+
+    # First loop to find ordered indexes
+    $order = array();
+    $index = 0;
+    foreach ($items as $v) {
+        if (preg_match('/<div.*?id="([^"].*?)".*?>/ms', $v, $match)) {
+            $id       = $match[1];
+            $position = array_search($id, $list, true);
+            if ($position !== false) {
+                $order[$position] = $index;
+            }
+        }
+        $index++;
+    }
+
+    # Second loop to combine ordered items
+    $index = 0;
+    foreach ($items as $v) {
+        $position = array_search($index, $order, true);
+        if ($position !== false) {
+            $ret[$position] = $v;
+        }
+        $index++;
+    }
+    # Reorder items on their position (key)
+    ksort($ret);
+
+    # Third loop to combine unordered items
+    $index = 0;
+    foreach ($items as $v) {
+        $position = array_search($index, $order, true);
+        if ($position === false) {
+            $ret[count($ret)] = $v;
+        }
+        $index++;
+    }
+
+    return join('', $ret);
+};
+
+# Compose dashboard items (doc, …)
+$dashboardItems = $composeItems($boxes_items_order, $__dashboard_items);
+# Compose dashboard contents (plugin's modules)
+$dashboardContents = $composeItems($boxes_contents_order, $__dashboard_contents);
+
+$__dashboard_boxes = array();
+if ($dashboardItems != '') {
+    $__dashboard_boxes[] = '<div class="db-items" id="db-items">' . $dashboardItems . '</div>';
+}
+if ($dashboardContents != '') {
+    $__dashboard_boxes[] = '<div class="db-contents" id="db-contents">' . $dashboardContents . '</div>';
+}
+$dashboardBoxes = $composeItems($boxes_order, $__dashboard_boxes, true);
+
+# Compose main area
+$__dashboard_main = array();
 if (!$core->auth->user_prefs->dashboard->nofavicons) {
     # Dashboard icons
-    echo '<div id="icons">';
+    $dashboardIcons = '<div id="icons">';
     foreach ($__dashboard_icons as $i) {
-        echo
-        '<p><a href="' . $i[1] . '"><img src="' . dc_admin_icon_url($i[2]) . '" alt="" />' .
+        $dashboardIcons .= '<p><a href="' . $i[1] . '"><img src="' . dc_admin_icon_url($i[2]) . '" alt="" />' .
             '<br /><span class="db-icon-title">' . $i[0] . '</span></a></p>';
     }
-    echo '</div>';
-}
-
+    $dashboardIcons .= '</div>';
+    $__dashboard_main[] = $dashboardIcons;
+}
 if ($core->auth->user_prefs->dashboard->quickentry) {
     if ($core->auth->check('usage,contentadmin', $core->blog->id)) {
@@ -279,5 +349,5 @@
         );
 
-        echo
+        $dashboardQuickEntry =
         '<div id="quick">' .
         '<h3>' . __('Quick entry') . sprintf(' &rsaquo; %s', $core->auth->getOption('post_format')) . '</h3>' .
@@ -320,16 +390,15 @@
             '</form>' .
             '</div>';
-    }
-}
-
-if ($dashboardContents != '' || $dashboardItems != '') {
-    echo
-        '<div id="dashboard-boxes">' .
-        ($dashboardItems != '' ? '<div class="db-items">' . $dashboardItems . '</div>' : '') .
-        ($dashboardContents != '' ? '<div class="db-contents">' . $dashboardContents . '</div>' : '') .
-        '</div>';
-}
-
-echo '</div>'; #end dashboard-main
+        $__dashboard_main[] = $dashboardQuickEntry;
+    }
+}
+if ($dashboardBoxes != '') {
+    $__dashboard_main[] = '<div id="dashboard-boxes">' . $dashboardBoxes . '</div>';
+}
+$dashboardMain = $composeItems($main_order, $__dashboard_main, true);
+
+# Dashboard elements
+echo '<div id="dashboard-main">' . $dashboardMain . '</div>';
+
 dcPage::helpBlock('core_dashboard');
 dcPage::close();
Index: admin/js/_index.js
===================================================================
--- admin/js/_index.js	(revision 3820)
+++ admin/js/_index.js	(revision 3833)
@@ -199,6 +199,6 @@
   }
   // Posts
-  var icon_com = $('#dashboard-main #icons p a[href="posts.php"]');
-  if (icon_com.length) {
+  var icon_post = $('#dashboard-main #icons p a[href="posts.php"]');
+  if (icon_post.length) {
     // Icon exists on dashboard
     // First pass
@@ -207,3 +207,52 @@
     dotclear.dbPostsCount_Timer = setInterval(dotclear.dbCommentsPost, 600 * 1000);
   }
+
+  // Dashboard boxes and their children are sortable
+  var set_positions = function(sel, id) {
+    var list = $(sel).sortable("toArray").join();
+    // Save positions (via services) for id
+    var params = {
+      f: 'setDashboardPositions',
+      xd_check: dotclear.nonce,
+      id: id,
+      list: list
+    };
+    $.post('services.php', params, function() {});
+  };
+  // Wait 5 seconds before activating ordering capabilities on dashboard
+  setTimeout(function() {
+    $('#dashboard-main').sortable({
+      cursor: 'move',
+      opacity: 0.5,
+      tolerance: "pointer",
+      update: function() {
+        set_positions(this, 'main_order');
+      }
+    });
+    $('#dashboard-boxes').sortable({
+      cursor: 'move',
+      opacity: 0.5,
+      tolerance: "pointer",
+      update: function() {
+        set_positions(this, 'boxes_order');
+      }
+    });
+    $('#db-items').sortable({
+      cursor: 'move',
+      opacity: 0.5,
+      tolerance: "pointer",
+      update: function() {
+        set_positions(this, 'boxes_items_order');
+      }
+    });
+    $('#db-contents').sortable({
+      cursor: 'move',
+      opacity: 0.5,
+      tolerance: "pointer",
+      update: function() {
+        set_positions(this, 'boxes_contents_order');
+      }
+    });
+  }, 5000);
+
 });
Index: admin/services.php
===================================================================
--- admin/services.php	(revision 3819)
+++ admin/services.php	(revision 3833)
@@ -25,4 +25,5 @@
 $core->rest->addFunction('setSectionFold', array('dcRestMethods', 'setSectionFold'));
 $core->rest->addFunction('getModuleById', array('dcRestMethods', 'getModuleById'));
+$core->rest->addFunction('setDashboardPositions', array('dcRestMethods', 'setDashboardPositions'));
 
 $core->rest->serve();
@@ -566,4 +567,24 @@
     }
 
+    public static function setDashboardPositions($core, $get, $post)
+    {
+        if (empty($post['id'])) {
+            throw new Exception('No zone name');
+        }
+        if (empty($post['list'])) {
+            throw new Exception('No sorted list of id');
+        }
+
+        if ($core->auth->user_prefs->dashboard === null) {
+            $core->auth->user_prefs->addWorkspace('dashboard');
+        }
+
+        $zone  = $post['id'];
+        $order = $post['list'];
+
+        $core->auth->user_prefs->dashboard->put($zone, $order);
+        return true;
+    }
+
     public static function getModuleById($core, $get, $post)
     {
