瀏覽代碼

支持手动排序 https://gitee.com/ghi/dbsyncer/issues/I5JMR9

AE86 2 年之前
父節點
當前提交
2715567b58

+ 20 - 1
dbsyncer-biz/src/main/java/org/dbsyncer/biz/checker/impl/mapping/MappingChecker.java

@@ -27,6 +27,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
 
 /**
  * @author AE86
@@ -153,10 +154,28 @@ public class MappingChecker extends AbstractChecker {
      * 合并关联的映射关系配置
      *
      * @param mapping
+     * @param params
      */
-    public void batchMergeTableGroupConfig(Mapping mapping) {
+    public void batchMergeTableGroupConfig(Mapping mapping, Map<String, String> params) {
         List<TableGroup> groupAll = manager.getTableGroupAll(mapping.getId());
         if (!CollectionUtils.isEmpty(groupAll)) {
+            // 手动排序
+            String[] sortedTableGroupIds = StringUtil.split(params.get("sortedTableGroupIds"), "|");
+            if(null != sortedTableGroupIds){
+                Map<String, TableGroup> tableGroupMap = groupAll.stream().collect(Collectors.toMap(TableGroup::getId, f -> f, (k1, k2) -> k1));
+                groupAll.clear();
+                int size = sortedTableGroupIds.length;
+                int i = size;
+                while (i > 0){
+                    TableGroup g = tableGroupMap.get(sortedTableGroupIds[size - i]);
+                    Assert.notNull(g, "Invalid sorted tableGroup.");
+                    g.setIndex(i);
+                    groupAll.add(g);
+                    i--;
+                }
+            }
+
+            // 合并配置
             for (TableGroup g : groupAll) {
                 tableGroupChecker.mergeConfig(mapping, g);
                 manager.editTableGroup(g);

+ 1 - 1
dbsyncer-biz/src/main/java/org/dbsyncer/biz/impl/MappingServiceImpl.java

@@ -67,7 +67,7 @@ public class MappingServiceImpl extends BaseServiceImpl implements MappingServic
             Mapping model = (Mapping) mappingChecker.checkEditConfigModel(params);
             log(LogType.MappingLog.UPDATE, model);
 
-            mappingChecker.batchMergeTableGroupConfig(model);
+            mappingChecker.batchMergeTableGroupConfig(model, params);
             return manager.editMapping(model);
         }
     }

+ 43 - 25
dbsyncer-biz/src/main/java/org/dbsyncer/biz/impl/TableGroupServiceImpl.java

@@ -13,8 +13,11 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.Assert;
 
-import java.util.*;
-import java.util.stream.Collectors;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.stream.Stream;
 
 /**
@@ -33,26 +36,28 @@ public class TableGroupServiceImpl extends BaseServiceImpl implements TableGroup
         String mappingId = params.get("mappingId");
         assertRunning(manager.getMapping(mappingId));
 
-        // table1, table2
-        String[] sourceTableArray = StringUtil.split(params.get("sourceTable"), "|");
-        String[] targetTableArray = StringUtil.split(params.get("targetTable"), "|");
-        int tableSize = sourceTableArray.length;
-        Assert.isTrue(tableSize == targetTableArray.length, "数据源表和目标源表关系必须为一组");
-
-        String id = null;
-        for (int i = 0; i < tableSize; i++) {
-            params.put("sourceTable", sourceTableArray[i]);
-            params.put("targetTable", targetTableArray[i]);
-            TableGroup model = (TableGroup) tableGroupChecker.checkAddConfigModel(params);
-            log(LogType.TableGroupLog.INSERT, model);
+        synchronized (LOCK){
+            // table1, table2
+            String[] sourceTableArray = StringUtil.split(params.get("sourceTable"), "|");
+            String[] targetTableArray = StringUtil.split(params.get("targetTable"), "|");
+            int tableSize = sourceTableArray.length;
+            Assert.isTrue(tableSize == targetTableArray.length, "数据源表和目标源表关系必须为一组");
+
+            String id = null;
+            for (int i = 0; i < tableSize; i++) {
+                params.put("sourceTable", sourceTableArray[i]);
+                params.put("targetTable", targetTableArray[i]);
+                TableGroup model = (TableGroup) tableGroupChecker.checkAddConfigModel(params);
+                log(LogType.TableGroupLog.INSERT, model);
+                int tableGroupCount = manager.getTableGroupCount(mappingId);
+                model.setIndex(tableGroupCount + 1);
+                id = manager.addTableGroup(model);
+            }
 
-            id = manager.addTableGroup(model);
+            // 合并驱动公共字段
+            mergeMappingColumn(mappingId);
+            return 1 < tableSize ? String.valueOf(tableSize) : id;
         }
-
-        // 合并驱动公共字段
-        mergeMappingColumn(mappingId);
-
-        return 1 < tableSize ? String.valueOf(tableSize) : id;
     }
 
     @Override
@@ -83,6 +88,9 @@ public class TableGroupServiceImpl extends BaseServiceImpl implements TableGroup
 
         // 合并驱动公共字段
         mergeMappingColumn(mappingId);
+
+        // 重置排序
+        resetTableGroupAllIndex(mappingId);
         return true;
     }
 
@@ -95,11 +103,21 @@ public class TableGroupServiceImpl extends BaseServiceImpl implements TableGroup
 
     @Override
     public List<TableGroup> getTableGroupAll(String mappingId) {
-        List<TableGroup> list = manager.getTableGroupAll(mappingId)
-                .stream()
-                .sorted(Comparator.comparing(TableGroup::getUpdateTime).reversed())
-                .collect(Collectors.toList());
-        return list;
+        return manager.getSortedTableGroupAll(mappingId);
+    }
+
+    private void resetTableGroupAllIndex(String mappingId) {
+        synchronized (LOCK) {
+            List<TableGroup> list = manager.getSortedTableGroupAll(mappingId);
+            int size = list.size();
+            int i = size;
+            while (i > 0) {
+                TableGroup g = list.get(size - i);
+                g.setIndex(i);
+                manager.editTableGroup(g);
+                i--;
+            }
+        }
     }
 
     private void mergeMappingColumn(String mappingId) {

+ 5 - 1
dbsyncer-manager/src/main/java/org/dbsyncer/manager/Manager.java

@@ -83,6 +83,10 @@ public interface Manager extends Executor {
 
     List<TableGroup> getTableGroupAll(String mappingId);
 
+    List<TableGroup> getSortedTableGroupAll(String mappingId);
+
+    int getTableGroupCount(String mappingId);
+
     Map<String, String> getCommand(Mapping mapping, TableGroup tableGroup);
 
     long getCount(String connectorId, Map<String, String> command);
@@ -143,4 +147,4 @@ public interface Manager extends Executor {
     String getLibraryPath();
 
     void loadPlugins();
-}
+}

+ 20 - 0
dbsyncer-manager/src/main/java/org/dbsyncer/manager/ManagerFactory.java

@@ -37,8 +37,10 @@ import org.springframework.stereotype.Component;
 import org.springframework.util.Assert;
 
 import java.time.Instant;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * @author AE86
@@ -223,6 +225,24 @@ public class ManagerFactory implements Manager, ApplicationListener<ClosedEvent>
         return tableGroups;
     }
 
+    @Override
+    public List<TableGroup> getSortedTableGroupAll(String mappingId) {
+        List<TableGroup> list = getTableGroupAll(mappingId)
+                .stream()
+                .sorted(Comparator.comparing(TableGroup::getIndex).reversed())
+                .collect(Collectors.toList());
+        return list;
+    }
+
+    @Override
+    public int getTableGroupCount(String mappingId) {
+        TableGroup tableGroup = new TableGroup();
+        tableGroup.setType(ConfigConstant.TABLE_GROUP);
+        tableGroup.setMappingId(mappingId);
+        QueryConfig queryConfig = new QueryConfig<>(tableGroup, GroupStrategyEnum.TABLE);
+        return operationTemplate.queryCount(queryConfig);
+    }
+
     @Override
     public Map<String, String> getCommand(Mapping mapping, TableGroup tableGroup) {
         return parser.getCommand(mapping, tableGroup);

+ 1 - 1
dbsyncer-manager/src/main/java/org/dbsyncer/manager/puller/FullPuller.java

@@ -119,7 +119,7 @@ public class FullPuller extends AbstractPuller implements ApplicationListener<Fu
 
         public FullWorker(Mapping mapping) {
             this.mapping = mapping;
-            this.list = manager.getTableGroupAll(mapping.getId());
+            this.list = manager.getSortedTableGroupAll(mapping.getId());
             Assert.notEmpty(list, "映射关系不能为空");
         }
 

+ 1 - 1
dbsyncer-manager/src/main/java/org/dbsyncer/manager/puller/IncrementPuller.java

@@ -90,7 +90,7 @@ public class IncrementPuller extends AbstractPuller implements ScheduledTaskJob
         logger.info("开始增量同步:{}, {}", metaId, mapping.getName());
         Connector connector = manager.getConnector(mapping.getSourceConnectorId());
         Assert.notNull(connector, "连接器不能为空.");
-        List<TableGroup> list = manager.getTableGroupAll(mappingId);
+        List<TableGroup> list = manager.getSortedTableGroupAll(mappingId);
         Assert.notEmpty(list, "映射关系不能为空.");
         Meta meta = manager.getMeta(metaId);
         Assert.notNull(meta, "Meta不能为空.");

+ 7 - 0
dbsyncer-manager/src/main/java/org/dbsyncer/manager/template/impl/OperationTemplate.java

@@ -66,6 +66,13 @@ public final class OperationTemplate {
         return Collections.EMPTY_LIST;
     }
 
+    public int queryCount(QueryConfig query) {
+        ConfigModel model = query.getConfigModel();
+        String groupId = getGroupId(model, query.getGroupStrategyEnum());
+        Group group = cacheService.get(groupId, Group.class);
+        return null != group ? group.getIndex().size() : 0;
+    }
+
     public <T> T queryObject(Class<T> clazz, String id) {
         if (StringUtil.isBlank(id)) {
             return null;

+ 11 - 0
dbsyncer-parser/src/main/java/org/dbsyncer/parser/model/TableGroup.java

@@ -12,6 +12,9 @@ import java.util.Map;
  */
 public class TableGroup extends AbstractConfigModel {
 
+    // 排序索引
+    private int index;
+
     // 驱动映射关系ID
     private String mappingId;
 
@@ -27,6 +30,14 @@ public class TableGroup extends AbstractConfigModel {
     // 执行命令,例SQL等
     private Map<String, String> command;
 
+    public int getIndex() {
+        return index;
+    }
+
+    public void setIndex(int index) {
+        this.index = index;
+    }
+
     public String getMappingId() {
         return mappingId;
     }

+ 6 - 4
dbsyncer-web/src/main/resources/public/index.html

@@ -16,9 +16,10 @@
     <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/font-awesome.min.css}"/>
     <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/bootstrap/bootstrap.min.css}"/>
     <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/bootstrap-dialog/bootstrap-dialog.min.css}"/>
-    <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/bootstrap-fileinput/fileinput.min.css}"/>
     <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/bootstrap-select/bootstrap-select.min.css}"/>
-    <link rel="stylesheet" th:href="@{/plugins/css/bootstrap-switch/bootstrap-switch.min.css}" type="text/css"/>
+    <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/bootstrap-switch/bootstrap-switch.min.css}"/>
+    <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/bootstrap-table/bootstrap-table-reorder-rows.css}"/>
+    <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/bootstrap-fileinput/fileinput.min.css}"/>
     <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/icheck/all.css}"/>
     <link type="text/css" rel="stylesheet" th:href="@{/plugins/css/loading-plus/loading-plus.css}"/>
     <link type="text/css" rel="stylesheet" th:href="@{/css/common.css}">
@@ -41,13 +42,14 @@
 <!-- 上述2个js文件解决IE8以上bootstrap的兼容性问题 -->
 <script th:src="@{/plugins/js/jquery/jquery-1.11.3.min.js}"></script>
 <script th:src="@{/plugins/js/bootstrap/bootstrap.min.js}"></script>
+<script th:src="@{/plugins/js/tablednd/jquery.tablednd.js}"></script>
 <script th:src="@{/plugins/js/bootstrap-dialog/bootstrap-dialog.min.js}"></script>
 <script th:src="@{/plugins/js/bootstrap-growl/jquery.bootstrap-growl.min.js}"></script>
+<script th:src="@{/plugins/js/bootstrap-select/bootstrap-select.min.js}"></script>
+<script th:src="@{/plugins/js/bootstrap-switch/bootstrap-switch.min.js}"></script>
 <script th:src="@{/plugins/js/bootstrap-fileinput/fileinput.min.js}"></script>
 <script th:src="@{/plugins/js/bootstrap-fileinput/theme.min.js}"></script>
 <script th:src="@{/plugins/js/bootstrap-fileinput/zh.js}"></script>
-<script th:src="@{/plugins/js/bootstrap-select/bootstrap-select.min.js}"></script>
-<script th:src="@{/plugins/js/bootstrap-switch/bootstrap-switch.min.js}"></script>
 <script th:src="@{/plugins/js/icheck/icheck.min.js}"></script>
 <script th:src="@{/plugins/js/loading-plus/loading-plus.js}"></script>
 <script th:src="@{/plugins/js/placeholder/jquery.placeholder.js}"></script>

+ 1 - 0
dbsyncer-web/src/main/resources/public/mapping/editTable.html

@@ -63,6 +63,7 @@
                 </tr>
             </tbody>
         </table>
+        <input id="sortedTableGroupIds" name="sortedTableGroupIds" type="hidden" />
     </div>
 </div>
 

+ 13 - 0
dbsyncer-web/src/main/resources/static/js/mapping/edit.js

@@ -96,6 +96,19 @@ function bindMappingTableGroupListClick() {
     $tableGroupList.find("tr").bind('click', function () {
         doLoader('/tableGroup/page/editTableGroup?id=' + $(this).attr("id"));
     });
+
+    // 绑定表格拖拽事件
+    $tableGroupList.tableDnD({
+        onDragClass: "reorder_rows_onDragClass",
+        onDrop: function(table, row) {
+            var newData = [];
+            var $trList = $(table).find("tr");
+            $.each($trList, function () {
+                newData.push($(this).attr('id'));
+            });
+            $("#sortedTableGroupIds").val(newData.join('|'));
+        }
+    });
 }
 
 // 绑定下拉选择事件自动匹配相似表事件

+ 670 - 0
dbsyncer-web/src/main/resources/static/plugins/js/tablednd/jquery.tablednd.js

@@ -0,0 +1,670 @@
+/**
+ * TableDnD plug-in for JQuery, allows you to drag and drop table rows
+ * You can set up various options to control how the system will work
+ * Copyright (c) Denis Howlett <denish@isocra.com>
+ * Licensed like jQuery, see http://docs.jquery.com/License.
+ *
+ * Configuration options:
+ *
+ * onDragStyle
+ *     This is the style that is assigned to the row during drag. There are limitations to the styles that can be
+ *     associated with a row (such as you can't assign a border--well you can, but it won't be
+ *     displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as
+ *     a map (as used in the jQuery css(...) function).
+ * onDropStyle
+ *     This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations
+ *     to what you can do. Also this replaces the original style, so again consider using onDragClass which
+ *     is simply added and then removed on drop.
+ * onDragClass
+ *     This class is added for the duration of the drag and then removed when the row is dropped. It is more
+ *     flexible than using onDragStyle since it can be inherited by the row cells and other content. The default
+ *     is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your
+ *     stylesheet.
+ * onDrop
+ *     Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table
+ *     and the row that was dropped. You can work out the new order of the rows by using
+ *     table.rows.
+ * onDragStart
+ *     Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the
+ *     table and the row which the user has started to drag.
+ * onAllowDrop
+ *     Pass a function that will be called as a row is over another row. If the function returns true, allow
+ *     dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under
+ *     the cursor. It returns a boolean: true allows the drop, false doesn't allow it.
+ * scrollAmount
+ *     This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the
+ *     window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2,
+ *     FF3 beta
+ * dragHandle
+ *     This is a jQuery mach string for one or more cells in each row that is draggable. If you
+ *     specify this, then you are responsible for setting cursor: move in the CSS and only these cells
+ *     will have the drag behaviour. If you do not specify a dragHandle, then you get the old behaviour where
+ *     the whole row is draggable.
+ *
+ * Other ways to control behaviour:
+ *
+ * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows
+ * that you don't want to be draggable.
+ *
+ * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form
+ * <tableID>[]=<rowID1>&<tableID>[]=<rowID2> so that you can send this back to the server. The table must have
+ * an ID as must all the rows.
+ *
+ * Other methods:
+ *
+ * $("...").tableDnDUpdate()
+ * Will update all the matching tables, that is it will reapply the mousedown method to the rows (or handle cells).
+ * This is useful if you have updated the table rows using Ajax and you want to make the table draggable again.
+ * The table maintains the original configuration (so you don't have to specify it again).
+ *
+ * $("...").tableDnDSerialize()
+ * Will serialize and return the serialized string as above, but for each of the matching tables--so it can be
+ * called from anywhere and isn't dependent on the currentTable being set up correctly before calling
+ *
+ * Known problems:
+ * - Auto-scoll has some problems with IE7  (it scrolls even when it shouldn't), work-around: set scrollAmount to 0
+ *
+ * Version 0.2: 2008-02-20 First public version
+ * Version 0.3: 2008-02-07 Added onDragStart option
+ *                         Made the scroll amount configurable (default is 5 as before)
+ * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes
+ *                         Added onAllowDrop to control dropping
+ *                         Fixed a bug which meant that you couldn't set the scroll amount in both directions
+ *                         Added serialize method
+ * Version 0.5: 2008-05-16 Changed so that if you specify a dragHandle class it doesn't make the whole row
+ *                         draggable
+ *                         Improved the serialize method to use a default (and settable) regular expression.
+ *                         Added tableDnDupate() and tableDnDSerialize() to be called when you are outside the table
+ * Version 0.6: 2011-12-02 Added support for touch devices
+ * Version 0.7  2012-04-09 Now works with jQuery 1.7 and supports touch, tidied up tabs and spaces
+ */
+!function ($, window, document, undefined) {
+// Determine if this is a touch device
+    var hasTouch   = 'ontouchstart' in document.documentElement,
+        startEvent = hasTouch ? 'touchstart' : 'mousedown',
+        moveEvent  = hasTouch ? 'touchmove'  : 'mousemove',
+        endEvent   = hasTouch ? 'touchend'   : 'mouseup';
+
+// If we're on a touch device, then wire up the events
+// see http://stackoverflow.com/a/8456194/1316086
+    hasTouch
+    && $.each("touchstart touchmove touchend".split(" "), function(i, name) {
+        $.event.fixHooks[name] = $.event.mouseHooks;
+    });
+
+
+    $(document).ready(function () {
+        function parseStyle(css) {
+            var objMap = {},
+                parts = css.match(/([^;:]+)/g) || [];
+            while (parts.length)
+                objMap[parts.shift()] = parts.shift().trim();
+
+            return objMap;
+        }
+        $('table').each(function () {
+            if ($(this).data('table') == 'dnd') {
+
+                $(this).tableDnD({
+                    onDragStyle: $(this).data('ondragstyle') && parseStyle($(this).data('ondragstyle')) || null,
+                    onDropStyle: $(this).data('ondropstyle') && parseStyle($(this).data('ondropstyle')) || null,
+                    onDragClass: $(this).data('ondragclass') == undefined && "tDnD_whileDrag" || $(this).data('ondragclass'),
+                    onDrop: $(this).data('ondrop') && new Function('table', 'row', $(this).data('ondrop')), // 'return eval("'+$(this).data('ondrop')+'");') || null,
+                    onDragStart: $(this).data('ondragstart') && new Function('table', 'row' ,$(this).data('ondragstart')), // 'return eval("'+$(this).data('ondragstart')+'");') || null,
+                    scrollAmount: $(this).data('scrollamount') || 5,
+                    sensitivity: $(this).data('sensitivity') || 10,
+                    hierarchyLevel: $(this).data('hierarchylevel') || 0,
+                    indentArtifact: $(this).data('indentartifact') || '<div class="indent">&nbsp;</div>',
+                    autoWidthAdjust: $(this).data('autowidthadjust') || true,
+                    autoCleanRelations: $(this).data('autocleanrelations') || true,
+                    jsonPretifySeparator: $(this).data('jsonpretifyseparator') || '\t',
+                    serializeRegexp: $(this).data('serializeregexp') && new RegExp($(this).data('serializeregexp')) || /[^\-]*$/,
+                    serializeParamName: $(this).data('serializeparamname') || false,
+                    dragHandle: $(this).data('draghandle') || null
+                });
+            }
+
+
+        });
+    });
+
+    jQuery.tableDnD = {
+        /** Keep hold of the current table being dragged */
+        currentTable: null,
+        /** Keep hold of the current drag object if any */
+        dragObject: null,
+        /** The current mouse offset */
+        mouseOffset: null,
+        /** Remember the old value of X and Y so that we don't do too much processing */
+        oldX: 0,
+        oldY: 0,
+
+        /** Actually build the structure */
+        build: function(options) {
+            // Set up the defaults if any
+
+            this.each(function() {
+                // This is bound to each matching table, set up the defaults and override with user options
+                this.tableDnDConfig = $.extend({
+                    onDragStyle: null,
+                    onDropStyle: null,
+                    // Add in the default class for whileDragging
+                    onDragClass: "tDnD_whileDrag",
+                    onDrop: null,
+                    onDragStart: null,
+                    scrollAmount: 5,
+                    /** Sensitivity setting will throttle the trigger rate for movement detection */
+                    sensitivity: 10,
+                    /** Hierarchy level to support parent child. 0 switches this functionality off */
+                    hierarchyLevel: 0,
+                    /** The html artifact to prepend the first cell with as indentation */
+                    indentArtifact: '<div class="indent">&nbsp;</div>',
+                    /** Automatically adjust width of first cell */
+                    autoWidthAdjust: true,
+                    /** Automatic clean-up to ensure relationship integrity */
+                    autoCleanRelations: true,
+                    /** Specify a number (4) as number of spaces or any indent string for JSON.stringify */
+                    jsonPretifySeparator: '\t',
+                    /** The regular expression to use to trim row IDs */
+                    serializeRegexp: /[^\-]*$/,
+                    /** If you want to specify another parameter name instead of the table ID */
+                    serializeParamName: false,
+                    /** If you give the name of a class here, then only Cells with this class will be draggable */
+                    dragHandle: null
+                }, options || {});
+
+                // Now make the rows draggable
+                $.tableDnD.makeDraggable(this);
+                // Prepare hierarchy support
+                this.tableDnDConfig.hierarchyLevel
+                && $.tableDnD.makeIndented(this);
+            });
+
+            // Don't break the chain
+            return this;
+        },
+        makeIndented: function (table) {
+            var config = table.tableDnDConfig,
+                rows = table.rows,
+                firstCell = $(rows).first().find('td:first')[0],
+                indentLevel = 0,
+                cellWidth = 0,
+                longestCell,
+                tableStyle;
+
+            if ($(table).hasClass('indtd'))
+                return null;
+
+            tableStyle = $(table).addClass('indtd').attr('style');
+            $(table).css({whiteSpace: "nowrap"});
+
+            for (var w = 0; w < rows.length; w++) {
+                if (cellWidth < $(rows[w]).find('td:first').text().length) {
+                    cellWidth = $(rows[w]).find('td:first').text().length;
+                    longestCell = w;
+                }
+            }
+            $(firstCell).css({width: 'auto'});
+            for (w = 0; w < config.hierarchyLevel; w++)
+                $(rows[longestCell]).find('td:first').prepend(config.indentArtifact);
+            firstCell && $(firstCell).css({width: firstCell.offsetWidth});
+            tableStyle && $(table).css(tableStyle);
+
+            for (w = 0; w < config.hierarchyLevel; w++)
+                $(rows[longestCell]).find('td:first').children(':first').remove();
+
+            config.hierarchyLevel
+            && $(rows).each(function () {
+                indentLevel = $(this).data('level') || 0;
+                indentLevel <= config.hierarchyLevel
+                && $(this).data('level', indentLevel)
+                || $(this).data('level', 0);
+                for (var i = 0; i < $(this).data('level'); i++)
+                    $(this).find('td:first').prepend(config.indentArtifact);
+            });
+
+            return this;
+        },
+        /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */
+        makeDraggable: function(table) {
+            var config = table.tableDnDConfig;
+
+            config.dragHandle
+            // We only need to add the event to the specified cells
+            && $(config.dragHandle, table).each(function() {
+                // The cell is bound to "this"
+                $(this).bind(startEvent, function(e) {
+                    $.tableDnD.initialiseDrag($(this).parents('tr')[0], table, this, e, config);
+                    return false;
+                });
+            })
+            // For backwards compatibility, we add the event to the whole row
+            // get all the rows as a wrapped set
+            || $(table.rows).each(function() {
+                // Iterate through each row, the row is bound to "this"
+                if (! $(this).hasClass("nodrag")) {
+                    $(this).bind(startEvent, function(e) {
+                        if (e.target.tagName == "TD") {
+                            $.tableDnD.initialiseDrag(this, table, this, e, config);
+                            return false;
+                        }
+                    }).css("cursor", "move"); // Store the tableDnD object
+                }
+            });
+        },
+        currentOrder: function() {
+            var rows = this.currentTable.rows;
+            return $.map(rows, function (val) {
+                return ($(val).data('level') + val.id).replace(/\s/g, '');
+            }).join('');
+        },
+        initialiseDrag: function(dragObject, table, target, e, config) {
+            this.dragObject    = dragObject;
+            this.currentTable  = table;
+            this.mouseOffset   = this.getMouseOffset(target, e);
+            this.originalOrder = this.currentOrder();
+
+            // Now we need to capture the mouse up and mouse move event
+            // We can use bind so that we don't interfere with other event handlers
+            $(document)
+                .bind(moveEvent, this.mousemove)
+                .bind(endEvent, this.mouseup);
+
+            // Call the onDragStart method if there is one
+            config.onDragStart
+            && config.onDragStart(table, target);
+        },
+        updateTables: function() {
+            this.each(function() {
+                // this is now bound to each matching table
+                if (this.tableDnDConfig)
+                    $.tableDnD.makeDraggable(this);
+            });
+        },
+        /** Get the mouse coordinates from the event (allowing for browser differences) */
+        mouseCoords: function(e) {
+            if (hasTouch)
+                return {
+                    x: event.changedTouches[0].clientX,
+                    y: event.changedTouches[0].clientY
+                };
+
+            if(e.pageX || e.pageY)
+                return {
+                    x: e.pageX,
+                    y: e.pageY
+                };
+
+            return {
+                x: e.clientX + document.body.scrollLeft - document.body.clientLeft,
+                y: e.clientY + document.body.scrollTop  - document.body.clientTop
+            };
+        },
+        /** Given a target element and a mouse eent, get the mouse offset from that element.
+         To do this we need the element's position and the mouse position */
+        getMouseOffset: function(target, e) {
+            var mousePos,
+                docPos;
+
+            e = e || window.event;
+
+            docPos    = this.getPosition(target);
+            mousePos  = this.mouseCoords(e);
+
+            return {
+                x: mousePos.x - docPos.x,
+                y: mousePos.y - docPos.y
+            };
+        },
+        /** Get the position of an element by going up the DOM tree and adding up all the offsets */
+        getPosition: function(element) {
+            var left = 0,
+                top  = 0;
+
+            // Safari fix -- thanks to Luis Chato for this!
+            // Safari 2 doesn't correctly grab the offsetTop of a table row
+            // this is detailed here:
+            // http://jacob.peargrove.com/blog/2006/technical/table-row-offsettop-bug-in-safari/
+            // the solution is likewise noted there, grab the offset of a table cell in the row - the firstChild.
+            // note that firefox will return a text node as a first child, so designing a more thorough
+            // solution may need to take that into account, for now this seems to work in firefox, safari, ie
+            if (element.offsetHeight == 0)
+                element = element.firstChild; // a table cell
+
+            while (element.offsetParent) {
+                left   += element.offsetLeft;
+                top    += element.offsetTop;
+                element = element.offsetParent;
+            }
+
+            left += element.offsetLeft;
+            top  += element.offsetTop;
+
+            return {
+                x: left,
+                y: top
+            };
+        },
+        autoScroll: function (mousePos) {
+            var config       = this.currentTable.tableDnDConfig,
+                yOffset      = window.pageYOffset,
+                windowHeight = window.innerHeight
+                    ? window.innerHeight
+                    : document.documentElement.clientHeight
+                        ? document.documentElement.clientHeight
+                        : document.body.clientHeight;
+
+            // Windows version
+            // yOffset=document.body.scrollTop;
+            if (document.all)
+                if (typeof document.compatMode != 'undefined'
+                    && document.compatMode != 'BackCompat')
+                    yOffset = document.documentElement.scrollTop;
+                else if (typeof document.body != 'undefined')
+                    yOffset = document.body.scrollTop;
+
+            mousePos.y - yOffset < config.scrollAmount
+            && window.scrollBy(0, - config.scrollAmount)
+            || windowHeight - (mousePos.y - yOffset) < config.scrollAmount
+            && window.scrollBy(0, config.scrollAmount);
+
+        },
+        moveVerticle: function (moving, currentRow) {
+
+            if (0 != moving.vertical
+                // If we're over a row then move the dragged row to there so that the user sees the
+                // effect dynamically
+                && currentRow
+                && this.dragObject != currentRow
+                && this.dragObject.parentNode == currentRow.parentNode)
+                0 > moving.vertical
+                && this.dragObject.parentNode.insertBefore(this.dragObject, currentRow.nextSibling)
+                || 0 < moving.vertical
+                && this.dragObject.parentNode.insertBefore(this.dragObject, currentRow);
+
+        },
+        moveHorizontal: function (moving, currentRow) {
+            var config       = this.currentTable.tableDnDConfig,
+                currentLevel;
+
+            if (!config.hierarchyLevel
+                || 0 == moving.horizontal
+                // We only care if moving left or right on the current row
+                || !currentRow
+                || this.dragObject != currentRow)
+                return null;
+
+            currentLevel = $(currentRow).data('level');
+
+            0 < moving.horizontal
+            && currentLevel > 0
+            && $(currentRow).find('td:first').children(':first').remove()
+            && $(currentRow).data('level', --currentLevel);
+
+            0 > moving.horizontal
+            && currentLevel < config.hierarchyLevel
+            && $(currentRow).prev().data('level') >= currentLevel
+            && $(currentRow).children(':first').prepend(config.indentArtifact)
+            && $(currentRow).data('level', ++currentLevel);
+
+        },
+        mousemove: function(e) {
+            var dragObj      = $($.tableDnD.dragObject),
+                config       = $.tableDnD.currentTable.tableDnDConfig,
+                currentRow,
+                mousePos,
+                moving,
+                x,
+                y;
+
+            e && e.preventDefault();
+
+            if (!$.tableDnD.dragObject)
+                return false;
+
+            // prevent touch device screen scrolling
+            e.type == 'touchmove'
+            && event.preventDefault(); // TODO verify this is event and not really e
+
+            // update the style to show we're dragging
+            config.onDragClass
+            && dragObj.addClass(config.onDragClass)
+            || dragObj.css(config.onDragStyle);
+
+            mousePos = $.tableDnD.mouseCoords(e);
+            x = mousePos.x - $.tableDnD.mouseOffset.x;
+            y = mousePos.y - $.tableDnD.mouseOffset.y;
+
+            // auto scroll the window
+            $.tableDnD.autoScroll(mousePos);
+
+            currentRow = $.tableDnD.findDropTargetRow(dragObj, y);
+            moving = $.tableDnD.findDragDirection(x, y);
+
+            $.tableDnD.moveVerticle(moving, currentRow);
+            $.tableDnD.moveHorizontal(moving, currentRow);
+
+            return false;
+        },
+        findDragDirection: function (x,y) {
+            var sensitivity = this.currentTable.tableDnDConfig.sensitivity,
+                oldX        = this.oldX,
+                oldY        = this.oldY,
+                xMin        = oldX - sensitivity,
+                xMax        = oldX + sensitivity,
+                yMin        = oldY - sensitivity,
+                yMax        = oldY + sensitivity,
+                moving      = {
+                    horizontal: x >= xMin && x <= xMax ? 0 : x > oldX ? -1 : 1,
+                    vertical  : y >= yMin && y <= yMax ? 0 : y > oldY ? -1 : 1
+                };
+
+            // update the old value
+            if (moving.horizontal != 0)
+                this.oldX    = x;
+            if (moving.vertical   != 0)
+                this.oldY    = y;
+
+            return moving;
+        },
+        /** We're only worried about the y position really, because we can only move rows up and down */
+        findDropTargetRow: function(draggedRow, y) {
+            var rowHeight = 0,
+                rows      = this.currentTable.rows,
+                config    = this.currentTable.tableDnDConfig,
+                rowY      = 0,
+                row       = null;
+
+            for (var i = 0; i < rows.length; i++) {
+                row       = rows[i];
+                rowY      = this.getPosition(row).y;
+                rowHeight = parseInt(row.offsetHeight) / 2;
+                if (row.offsetHeight == 0) {
+                    rowY      = this.getPosition(row.firstChild).y;
+                    rowHeight = parseInt(row.firstChild.offsetHeight) / 2;
+                }
+                // Because we always have to insert before, we need to offset the height a bit
+                if (y > (rowY - rowHeight) && y < (rowY + rowHeight))
+                // that's the row we're over
+                // If it's the same as the current row, ignore it
+                    if (draggedRow.is(row)
+                        || (config.onAllowDrop
+                            && !config.onAllowDrop(draggedRow, row))
+                        // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic)
+                        || $(row).hasClass("nodrop"))
+                        return null;
+                    else
+                        return row;
+            }
+            return null;
+        },
+        processMouseup: function() {
+            var config      = this.currentTable.tableDnDConfig,
+                droppedRow  = this.dragObject,
+                parentLevel = 0,
+                myLevel     = 0;
+
+            if (!this.currentTable || !droppedRow)
+                return null;
+
+            // Unbind the event handlers
+            $(document)
+                .unbind(moveEvent, this.mousemove)
+                .unbind(endEvent,  this.mouseup);
+
+            config.hierarchyLevel
+            && config.autoCleanRelations
+            && $(this.currentTable.rows).first().find('td:first').children().each(function () {
+                myLevel = $(this).parents('tr:first').data('level');
+                myLevel
+                && $(this).parents('tr:first').data('level', --myLevel)
+                && $(this).remove();
+            })
+            && config.hierarchyLevel > 1
+            && $(this.currentTable.rows).each(function () {
+                myLevel = $(this).data('level');
+                if (myLevel > 1) {
+                    parentLevel = $(this).prev().data('level');
+                    while (myLevel > parentLevel + 1) {
+                        $(this).find('td:first').children(':first').remove();
+                        $(this).data('level', --myLevel);
+                    }
+                }
+            });
+
+            // If we have a dragObject, then we need to release it,
+            // The row will already have been moved to the right place so we just reset stuff
+            config.onDragClass
+            && $(droppedRow).removeClass(config.onDragClass)
+            || $(droppedRow).css(config.onDropStyle);
+
+            this.dragObject = null;
+            // Call the onDrop method if there is one
+            config.onDrop
+            && this.originalOrder != this.currentOrder()
+            && $(droppedRow).hide().fadeIn('fast')
+            && config.onDrop(this.currentTable, droppedRow);
+
+            this.currentTable = null; // let go of the table too
+        },
+        mouseup: function(e) {
+            e && e.preventDefault();
+            $.tableDnD.processMouseup();
+            return false;
+        },
+        jsonize: function(pretify) {
+            var table = this.currentTable;
+            if (pretify)
+                return JSON.stringify(
+                    this.tableData(table),
+                    null,
+                    table.tableDnDConfig.jsonPretifySeparator
+                );
+            return JSON.stringify(this.tableData(table));
+        },
+        serialize: function() {
+            return $.param(this.tableData(this.currentTable));
+        },
+        serializeTable: function(table) {
+            var result = "";
+            var paramName = table.tableDnDConfig.serializeParamName || table.id;
+            var rows = table.rows;
+            for (var i=0; i<rows.length; i++) {
+                if (result.length > 0) result += "&";
+                var rowId = rows[i].id;
+                if (rowId && table.tableDnDConfig && table.tableDnDConfig.serializeRegexp) {
+                    rowId = rowId.match(table.tableDnDConfig.serializeRegexp)[0];
+                    result += paramName + '[]=' + rowId;
+                }
+            }
+            return result;
+        },
+        serializeTables: function() {
+            var result = [];
+            $('table').each(function() {
+                this.id && result.push($.param(this.tableData(this)));
+            });
+            return result.join('&');
+        },
+        tableData: function (table) {
+            var config = table.tableDnDConfig,
+                previousIDs  = [],
+                currentLevel = 0,
+                indentLevel  = 0,
+                rowID        = null,
+                data         = {},
+                getSerializeRegexp,
+                paramName,
+                currentID,
+                rows;
+
+            if (!table)
+                table = this.currentTable;
+            if (!table || !table.id || !table.rows || !table.rows.length)
+                return {error: { code: 500, message: "Not a valid table, no serializable unique id provided."}};
+
+            rows      = config.autoCleanRelations
+                && table.rows
+                || $.makeArray(table.rows);
+            paramName = config.serializeParamName || table.id;
+            currentID = paramName;
+
+            getSerializeRegexp = function (rowId) {
+                if (rowId && config && config.serializeRegexp)
+                    return rowId.match(config.serializeRegexp)[0];
+                return rowId;
+            };
+
+            data[currentID] = [];
+            !config.autoCleanRelations
+            && $(rows[0]).data('level')
+            && rows.unshift({id: 'undefined'});
+
+
+
+            for (var i=0; i < rows.length; i++) {
+                if (config.hierarchyLevel) {
+                    indentLevel = $(rows[i]).data('level') || 0;
+                    if (indentLevel == 0) {
+                        currentID   = paramName;
+                        previousIDs = [];
+                    }
+                    else if (indentLevel > currentLevel) {
+                        previousIDs.push([currentID, currentLevel]);
+                        currentID = getSerializeRegexp(rows[i-1].id);
+                    }
+                    else if (indentLevel < currentLevel) {
+                        for (var h = 0; h < previousIDs.length; h++) {
+                            if (previousIDs[h][1] == indentLevel)
+                                currentID         = previousIDs[h][0];
+                            if (previousIDs[h][1] >= currentLevel)
+                                previousIDs[h][1] = 0;
+                        }
+                    }
+                    currentLevel = indentLevel;
+
+                    if (!$.isArray(data[currentID]))
+                        data[currentID] = [];
+                    rowID = getSerializeRegexp(rows[i].id);
+                    rowID && data[currentID].push(rowID);
+                }
+                else {
+                    rowID = getSerializeRegexp(rows[i].id);
+                    rowID && data[currentID].push(rowID);
+                }
+            }
+            return data;
+        }
+    };
+
+    jQuery.fn.extend(
+        {
+            tableDnD             : $.tableDnD.build,
+            tableDnDUpdate       : $.tableDnD.updateTables,
+            tableDnDSerialize    : $.proxy($.tableDnD.serialize, $.tableDnD),
+            tableDnDSerializeAll : $.tableDnD.serializeTables,
+            tableDnDData         : $.proxy($.tableDnD.tableData, $.tableDnD)
+        }
+    );
+
+}(jQuery, window, window.document);