diff --git a/build/knockout-sortable.js b/build/knockout-sortable.js
new file mode 100644
index 0000000..51edb6a
--- /dev/null
+++ b/build/knockout-sortable.js
@@ -0,0 +1,496 @@
+// knockout-sortable 1.2.2 | (c) 2021 Ryan Niemeyer |  http://www.opensource.org/licenses/mit-license
+;(function(factory) {
+    if (typeof define === "function" && define.amd) {
+        // AMD anonymous module
+        define(["knockout", "jquery", "jquery-ui/ui/widgets/sortable", "jquery-ui/ui/widgets/draggable", "jquery-ui/ui/widgets/droppable"], factory);
+    } else if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
+        // CommonJS module
+        var ko = require("knockout"),
+            jQuery = require("jquery");
+        require("jquery-ui/ui/widgets/sortable");
+        require("jquery-ui/ui/widgets/draggable");
+        require("jquery-ui/ui/widgets/droppable");
+        factory(ko, jQuery);
+    } else {
+        // No module loader (plain <script> tag) - put directly in global namespace
+        factory(window.ko, window.jQuery);
+    }
+})(function(ko, $) {
+    var ITEMKEY = "ko_sortItem",
+        INDEXKEY = "ko_sourceIndex",
+        LISTKEY = "ko_sortList",
+        PARENTKEY = "ko_parentList",
+        DRAGKEY = "ko_dragItem",
+        unwrap = ko.utils.unwrapObservable,
+        dataGet = ko.utils.domData.get,
+        dataSet = ko.utils.domData.set,
+        version = $.ui && $.ui.version,
+        //1.8.24 included a fix for how events were triggered in nested sortables. indexOf checks will fail if version starts with that value (0 vs. -1)
+        hasNestedSortableFix = version && version.indexOf("1.6.") && version.indexOf("1.7.") && (version.indexOf("1.8.") || version === "1.8.24");
+
+    //internal afterRender that adds meta-data to children
+    var addMetaDataAfterRender = function(elements, data) {
+        ko.utils.arrayForEach(elements, function(element) {
+            if (element.nodeType === 1) {
+                dataSet(element, ITEMKEY, data);
+                dataSet(element, PARENTKEY, dataGet(element.parentNode, LISTKEY));
+            }
+        });
+    };
+
+    //prepare the proper options for the template binding
+    var prepareTemplateOptions = function(valueAccessor, dataName) {
+        var result = {},
+            options = {},
+            actualAfterRender;
+
+        //build our options to pass to the template engine
+        if (ko.utils.peekObservable(valueAccessor()).data) {
+            options = unwrap(valueAccessor() || {});
+            result[dataName] = options.data;
+            if (options.hasOwnProperty("template")) {
+                result.name = options.template;
+            }
+        } else {
+            result[dataName] = valueAccessor();
+        }
+
+        ko.utils.arrayForEach(["afterAdd", "afterRender", "as", "beforeRemove", "includeDestroyed", "templateEngine", "templateOptions", "nodes"], function (option) {
+            if (options.hasOwnProperty(option)) {
+                result[option] = options[option];
+            } else if (ko.bindingHandlers.sortable.hasOwnProperty(option)) {
+                result[option] = ko.bindingHandlers.sortable[option];
+            }
+        });
+
+        //use an afterRender function to add meta-data
+        if (dataName === "foreach") {
+            if (result.afterRender) {
+                //wrap the existing function, if it was passed
+                actualAfterRender = result.afterRender;
+                result.afterRender = function(element, data) {
+                    addMetaDataAfterRender.call(data, element, data);
+                    actualAfterRender.call(data, element, data);
+                };
+            } else {
+                result.afterRender = addMetaDataAfterRender;
+            }
+        }
+
+        //return options to pass to the template binding
+        return result;
+    };
+
+    var updateIndexFromDestroyedItems = function(index, items) {
+        var unwrapped = unwrap(items);
+
+        if (unwrapped) {
+            for (var i = 0; i <= index; i++) {
+                //add one for every destroyed item we find before the targetIndex in the target array
+                if (unwrapped[i] && unwrap(unwrapped[i]._destroy)) {
+                    index++;
+                }
+            }
+        }
+
+        return index;
+    };
+
+    //remove problematic leading/trailing whitespace from templates
+    var stripTemplateWhitespace = function(element, name) {
+        var templateSource,
+            templateElement;
+
+        //process named templates
+        if (name) {
+            templateElement = document.getElementById(name);
+            if (templateElement) {
+                templateSource = new ko.templateSources.domElement(templateElement);
+                templateSource.text($.trim(templateSource.text()));
+            }
+        }
+        else {
+            //remove leading/trailing non-elements from anonymous templates
+            $(element).contents().each(function() {
+                if (this && this.nodeType !== 1) {
+                    element.removeChild(this);
+                }
+            });
+        }
+    };
+
+    //connect items with observableArrays
+    ko.bindingHandlers.sortable = {
+        init: function(element, valueAccessor, allBindingsAccessor, data, context) {
+            var $element = $(element),
+                value = unwrap(valueAccessor()) || {},
+                templateOptions = prepareTemplateOptions(valueAccessor, "foreach"),
+                sortable = {},
+                startActual, updateActual;
+
+            stripTemplateWhitespace(element, templateOptions.name);
+
+            //build a new object that has the global options with overrides from the binding
+            $.extend(true, sortable, ko.bindingHandlers.sortable);
+            if (value.options && sortable.options) {
+                ko.utils.extend(sortable.options, value.options);
+                delete value.options;
+            }
+            ko.utils.extend(sortable, value);
+
+            //if allowDrop is an observable or a function, then execute it in a computed observable
+            if (sortable.connectClass && (ko.isObservable(sortable.allowDrop) || typeof sortable.allowDrop == "function")) {
+                ko.computed({
+                    read: function() {
+                        var value = unwrap(sortable.allowDrop),
+                            shouldAdd = typeof value == "function" ? value.call(this, templateOptions.foreach) : value;
+                        ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, shouldAdd);
+                    },
+                    disposeWhenNodeIsRemoved: element
+                }, this);
+            } else {
+                ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, sortable.allowDrop);
+            }
+
+            //wrap the template binding
+            ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
+
+            //keep a reference to start/update functions that might have been passed in
+            startActual = sortable.options.start;
+            updateActual = sortable.options.update;
+
+            //ensure draggable table row cells maintain their width while dragging (unless a helper is provided)
+            if ( !sortable.options.helper ) {
+                sortable.options.helper = function(e, ui) {
+                    if (ui.is("tr")) {
+                        ui.children().each(function() {
+                            $(this).width($(this).width());
+                        });
+                    }
+                    return ui;
+                };
+            }
+
+            //initialize sortable binding after template binding has rendered in update function
+            var createTimeout = setTimeout(function() {
+                var dragItem;
+                var originalReceive = sortable.options.receive;
+
+                $element.sortable(ko.utils.extend(sortable.options, {
+                    start: function(event, ui) {
+                        //track original index
+                        var el = ui.item[0];
+                        dataSet(el, INDEXKEY, ko.utils.arrayIndexOf(ui.item.parent().children(), el));
+
+                        //make sure that fields have a chance to update model
+                        ui.item.find("input:focus").change();
+                        if (startActual) {
+                            startActual.apply(this, arguments);
+                        }
+                    },
+                    receive: function(event, ui) {
+                        //optionally apply an existing receive handler
+                        if (typeof originalReceive === "function") {
+                            originalReceive.call(this, event, ui);
+                        }
+
+                        dragItem = dataGet(ui.item[0], DRAGKEY);
+                        if (dragItem) {
+                            //copy the model item, if a clone option is provided
+                            if (dragItem.clone) {
+                                dragItem = dragItem.clone();
+                            }
+
+                            //configure a handler to potentially manipulate item before drop
+                            if (sortable.dragged) {
+                                dragItem = sortable.dragged.call(this, dragItem, event, ui) || dragItem;
+                            }
+                        }
+                    },
+                    update: function(event, ui) {
+                        var sourceParent, targetParent, sourceIndex, targetIndex, arg,
+                            el = ui.item[0],
+                            parentEl = ui.item.parent()[0],
+                            item = dataGet(el, ITEMKEY) || dragItem;
+
+                        if (!item) {
+                            $(el).remove();
+                        }
+                        dragItem = null;
+
+                        //make sure that moves only run once, as update fires on multiple containers
+                        if (item && (this === parentEl) || (!hasNestedSortableFix && $.contains(this, parentEl))) {
+                            //identify parents
+                            sourceParent = dataGet(el, PARENTKEY);
+                            sourceIndex = dataGet(el, INDEXKEY);
+                            targetParent = dataGet(el.parentNode, LISTKEY);
+                            targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el);
+
+                            //take destroyed items into consideration
+                            if (!templateOptions.includeDestroyed) {
+                                sourceIndex = updateIndexFromDestroyedItems(sourceIndex, sourceParent);
+                                targetIndex = updateIndexFromDestroyedItems(targetIndex, targetParent);
+                            }
+
+                            //build up args for the callbacks
+                            if (sortable.beforeMove || sortable.afterMove) {
+                                arg = {
+                                    item: item,
+                                    sourceParent: sourceParent,
+                                    sourceParentNode: sourceParent && ui.sender || el.parentNode,
+                                    sourceIndex: sourceIndex,
+                                    targetParent: targetParent,
+                                    targetIndex: targetIndex,
+                                    cancelDrop: false
+                                };
+
+                                //execute the configured callback prior to actually moving items
+                                if (sortable.beforeMove) {
+                                    sortable.beforeMove.call(this, arg, event, ui);
+                                }
+                            }
+
+                            //call cancel on the correct list, so KO can take care of DOM manipulation
+                            if (sourceParent) {
+                                $(sourceParent === targetParent ? this : ui.sender || this).sortable("cancel");
+                            }
+                            //for a draggable item just remove the element
+                            else {
+                                $(el).remove();
+                            }
+
+                            //if beforeMove told us to cancel, then we are done
+                            if (arg && arg.cancelDrop) {
+                                return;
+                            }
+
+                            //if the strategy option is unset or false, employ the order strategy involving removal and insertion of items
+                            if (!sortable.hasOwnProperty("strategyMove") || sortable.strategyMove === false) {
+                                //do the actual move
+                                if (targetIndex >= 0) {
+                                    if (sourceParent) {
+                                        sourceParent.splice(sourceIndex, 1);
+
+                                        //if using deferred updates plugin, force updates
+                                        if (ko.processAllDeferredBindingUpdates) {
+                                            ko.processAllDeferredBindingUpdates();
+                                        }
+
+                                        //if using deferred updates on knockout 3.4, force updates
+                                        if (ko.options && ko.options.deferUpdates) {
+                                            ko.tasks.runEarly();
+                                        }
+                                    }
+
+                                    targetParent.splice(targetIndex, 0, item);
+                                }
+
+                                //rendering is handled by manipulating the observableArray; ignore dropped element
+                                dataSet(el, ITEMKEY, null);
+                            }
+                            else { //employ the strategy of moving items
+                                if (targetIndex >= 0) {
+                                    if (sourceParent) {
+                                        if (sourceParent !== targetParent) {
+                                            // moving from one list to another
+
+                                            sourceParent.splice(sourceIndex, 1);
+                                            targetParent.splice(targetIndex, 0, item);
+
+                                            //rendering is handled by manipulating the observableArray; ignore dropped element
+                                            dataSet(el, ITEMKEY, null);
+                                            ui.item.remove();
+                                        }
+                                        else {
+                                            // moving within same list
+                                            var underlyingList = unwrap(sourceParent);
+
+                                            // notify 'beforeChange' subscribers
+                                            if (sourceParent.valueWillMutate) {
+                                                sourceParent.valueWillMutate();
+                                            }
+
+                                            // move from source index ...
+                                            underlyingList.splice(sourceIndex, 1);
+                                            // ... to target index
+                                            underlyingList.splice(targetIndex, 0, item);
+
+                                            // notify subscribers
+                                            if (sourceParent.valueHasMutated) {
+                                                sourceParent.valueHasMutated();
+                                            }
+                                        }
+                                    }
+                                    else {
+                                        // drop new element from outside
+                                        targetParent.splice(targetIndex, 0, item);
+
+                                        //rendering is handled by manipulating the observableArray; ignore dropped element
+                                        dataSet(el, ITEMKEY, null);
+                                        ui.item.remove();
+                                    }
+                                }
+                            }
+
+                            //if using deferred updates plugin, force updates
+                            if (ko.processAllDeferredBindingUpdates) {
+                                ko.processAllDeferredBindingUpdates();
+                            }
+
+                            //allow binding to accept a function to execute after moving the item
+                            if (sortable.afterMove) {
+                                sortable.afterMove.call(this, arg, event, ui);
+                            }
+                        }
+
+                        if (updateActual) {
+                            updateActual.apply(this, arguments);
+                        }
+                    },
+                    connectWith: sortable.connectClass ? "." + sortable.connectClass : false
+                }));
+
+                //handle enabling/disabling sorting
+                if (sortable.isEnabled !== undefined) {
+                    ko.computed({
+                        read: function() {
+                            $element.sortable(unwrap(sortable.isEnabled) ? "enable" : "disable");
+                        },
+                        disposeWhenNodeIsRemoved: element
+                    });
+                }
+            }, 0);
+
+            //handle disposal
+            ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
+                //only call destroy if sortable has been created
+                if ($element.data("ui-sortable") || $element.data("sortable")) {
+                    $element.sortable("destroy");
+                }
+
+                ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, false);
+
+                //do not create the sortable if the element has been removed from DOM
+                clearTimeout(createTimeout);
+            });
+
+            return { 'controlsDescendantBindings': true };
+        },
+        update: function(element, valueAccessor, allBindingsAccessor, data, context) {
+            var templateOptions = prepareTemplateOptions(valueAccessor, "foreach");
+
+            //attach meta-data
+            dataSet(element, LISTKEY, templateOptions.foreach);
+
+            //call template binding's update with correct options
+            ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
+        },
+        connectClass: 'ko_container',
+        allowDrop: true,
+        afterMove: null,
+        beforeMove: null,
+        options: {}
+    };
+
+    //create a draggable that is appropriate for dropping into a sortable
+    ko.bindingHandlers.draggable = {
+        init: function(element, valueAccessor, allBindingsAccessor, data, context) {
+            var value = unwrap(valueAccessor()) || {},
+                options = value.options || {},
+                draggableOptions = ko.utils.extend({}, ko.bindingHandlers.draggable.options),
+                templateOptions = prepareTemplateOptions(valueAccessor, "data"),
+                connectClass = value.connectClass || ko.bindingHandlers.draggable.connectClass,
+                isEnabled = value.isEnabled !== undefined ? value.isEnabled : ko.bindingHandlers.draggable.isEnabled;
+
+            value = "data" in value ? value.data : value;
+
+            //set meta-data
+            dataSet(element, DRAGKEY, value);
+
+            //override global options with override options passed in
+            ko.utils.extend(draggableOptions, options);
+
+            //setup connection to a sortable
+            draggableOptions.connectToSortable = connectClass ? "." + connectClass : false;
+
+            //initialize draggable
+            $(element).draggable(draggableOptions);
+
+            //handle enabling/disabling sorting
+            if (isEnabled !== undefined) {
+                ko.computed({
+                    read: function() {
+                        $(element).draggable(unwrap(isEnabled) ? "enable" : "disable");
+                    },
+                    disposeWhenNodeIsRemoved: element
+                });
+            }
+
+            //handle disposal
+            ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
+                var $element = $(element);
+                if ($element.data("ui-draggable") || $element.data("draggable")) {
+                    $element.draggable("destroy");
+                }
+            });
+
+            return ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
+        },
+        update: function(element, valueAccessor, allBindingsAccessor, data, context) {
+            var templateOptions = prepareTemplateOptions(valueAccessor, "data");
+
+            return ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
+        },
+        connectClass: ko.bindingHandlers.sortable.connectClass,
+        options: {
+            helper: "clone"
+        }
+    };
+
+    // Simple Droppable Implementation
+    // binding that updates (function or observable)
+    ko.bindingHandlers.droppable = {
+        init: function(element, valueAccessor, allBindingsAccessor, data, context) {
+            var value = unwrap(valueAccessor()) || {},
+                options = value.options || {},
+                droppableOptions = ko.utils.extend({}, ko.bindingHandlers.droppable.options),
+                isEnabled = value.isEnabled !== undefined ? value.isEnabled : ko.bindingHandlers.droppable.isEnabled;
+
+            //override global options with override options passed in
+            ko.utils.extend(droppableOptions, options);
+
+            //get reference to drop method
+            value = "data" in value ? value.data : valueAccessor();
+
+            //set drop method
+            droppableOptions.drop = function(event, ui) {
+                var droppedItem = dataGet(ui.draggable[0], DRAGKEY) || dataGet(ui.draggable[0], ITEMKEY);
+                value(droppedItem);
+            };
+
+            //initialize droppable
+            $(element).droppable(droppableOptions);
+
+            //handle enabling/disabling droppable
+            if (isEnabled !== undefined) {
+                ko.computed({
+                    read: function() {
+                        $(element).droppable(unwrap(isEnabled) ? "enable": "disable");
+                    },
+                    disposeWhenNodeIsRemoved: element
+                });
+            }
+
+            //handle disposal
+            ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
+                var $element = $(element);
+                if ($element.data("ui-droppable") || $element.data("droppable")) {
+                    $element.droppable("destroy");
+                }
+            });
+        },
+        options: {
+            accept: "*"
+        }
+    };
+});
diff --git a/build/knockout-sortable.min.js b/build/knockout-sortable.min.js
new file mode 100644
index 0000000..f3abb32
--- /dev/null
+++ b/build/knockout-sortable.min.js
@@ -0,0 +1,2 @@
+// knockout-sortable 1.2.2 | (c) 2021 Ryan Niemeyer |  http://www.opensource.org/licenses/mit-license
+!function(a){if("function"==typeof define&&define.amd)define(["knockout","jquery","jquery-ui/ui/widgets/sortable","jquery-ui/ui/widgets/draggable","jquery-ui/ui/widgets/droppable"],a);else if("function"==typeof require&&"object"==typeof exports&&"object"==typeof module){var b=require("knockout"),c=require("jquery");require("jquery-ui/ui/widgets/sortable"),require("jquery-ui/ui/widgets/draggable"),require("jquery-ui/ui/widgets/droppable"),a(b,c)}else a(window.ko,window.jQuery)}(function(a,b){var c="ko_sortItem",d="ko_sourceIndex",e="ko_sortList",f="ko_parentList",g="ko_dragItem",h=a.utils.unwrapObservable,i=a.utils.domData.get,j=a.utils.domData.set,k=b.ui&&b.ui.version,l=k&&k.indexOf("1.6.")&&k.indexOf("1.7.")&&(k.indexOf("1.8.")||"1.8.24"===k),m=function(b,d){a.utils.arrayForEach(b,function(a){1===a.nodeType&&(j(a,c,d),j(a,f,i(a.parentNode,e)))})},n=function(b,c){var d,e={},f={};return a.utils.peekObservable(b()).data?(f=h(b()||{}),e[c]=f.data,f.hasOwnProperty("template")&&(e.name=f.template)):e[c]=b(),a.utils.arrayForEach(["afterAdd","afterRender","as","beforeRemove","includeDestroyed","templateEngine","templateOptions","nodes"],function(b){f.hasOwnProperty(b)?e[b]=f[b]:a.bindingHandlers.sortable.hasOwnProperty(b)&&(e[b]=a.bindingHandlers.sortable[b])}),"foreach"===c&&(e.afterRender?(d=e.afterRender,e.afterRender=function(a,b){m.call(b,a,b),d.call(b,a,b)}):e.afterRender=m),e},o=function(a,b){var c=h(b);if(c)for(var d=0;d<=a;d++)c[d]&&h(c[d]._destroy)&&a++;return a},p=function(c,d){var e,f;d?(f=document.getElementById(d),f&&(e=new a.templateSources.domElement(f),e.text(b.trim(e.text())))):b(c).contents().each(function(){this&&1!==this.nodeType&&c.removeChild(this)})};a.bindingHandlers.sortable={init:function(k,m,q,r,s){var t,u,v=b(k),w=h(m())||{},x=n(m,"foreach"),y={};p(k,x.name),b.extend(!0,y,a.bindingHandlers.sortable),w.options&&y.options&&(a.utils.extend(y.options,w.options),delete w.options),a.utils.extend(y,w),y.connectClass&&(a.isObservable(y.allowDrop)||"function"==typeof y.allowDrop)?a.computed({read:function(){var b=h(y.allowDrop),c="function"==typeof b?b.call(this,x.foreach):b;a.utils.toggleDomNodeCssClass(k,y.connectClass,c)},disposeWhenNodeIsRemoved:k},this):a.utils.toggleDomNodeCssClass(k,y.connectClass,y.allowDrop),a.bindingHandlers.template.init(k,function(){return x},q,r,s),t=y.options.start,u=y.options.update,y.options.helper||(y.options.helper=function(a,c){return c.is("tr")&&c.children().each(function(){b(this).width(b(this).width())}),c});var z=setTimeout(function(){var m,n=y.options.receive;v.sortable(a.utils.extend(y.options,{start:function(b,c){var e=c.item[0];j(e,d,a.utils.arrayIndexOf(c.item.parent().children(),e)),c.item.find("input:focus").change(),t&&t.apply(this,arguments)},receive:function(a,b){"function"==typeof n&&n.call(this,a,b),m=i(b.item[0],g),m&&(m.clone&&(m=m.clone()),y.dragged&&(m=y.dragged.call(this,m,a,b)||m))},update:function(g,k){var n,p,q,r,s,t=k.item[0],v=k.item.parent()[0],w=i(t,c)||m;if(w||b(t).remove(),m=null,w&&this===v||!l&&b.contains(this,v)){if(n=i(t,f),q=i(t,d),p=i(t.parentNode,e),r=a.utils.arrayIndexOf(k.item.parent().children(),t),x.includeDestroyed||(q=o(q,n),r=o(r,p)),(y.beforeMove||y.afterMove)&&(s={item:w,sourceParent:n,sourceParentNode:n&&k.sender||t.parentNode,sourceIndex:q,targetParent:p,targetIndex:r,cancelDrop:!1},y.beforeMove&&y.beforeMove.call(this,s,g,k)),n?b(n===p?this:k.sender||this).sortable("cancel"):b(t).remove(),s&&s.cancelDrop)return;if(y.hasOwnProperty("strategyMove")&&y.strategyMove!==!1){if(r>=0)if(n)if(n!==p)n.splice(q,1),p.splice(r,0,w),j(t,c,null),k.item.remove();else{var z=h(n);n.valueWillMutate&&n.valueWillMutate(),z.splice(q,1),z.splice(r,0,w),n.valueHasMutated&&n.valueHasMutated()}else p.splice(r,0,w),j(t,c,null),k.item.remove()}else r>=0&&(n&&(n.splice(q,1),a.processAllDeferredBindingUpdates&&a.processAllDeferredBindingUpdates(),a.options&&a.options.deferUpdates&&a.tasks.runEarly()),p.splice(r,0,w)),j(t,c,null);a.processAllDeferredBindingUpdates&&a.processAllDeferredBindingUpdates(),y.afterMove&&y.afterMove.call(this,s,g,k)}u&&u.apply(this,arguments)},connectWith:!!y.connectClass&&"."+y.connectClass})),void 0!==y.isEnabled&&a.computed({read:function(){v.sortable(h(y.isEnabled)?"enable":"disable")},disposeWhenNodeIsRemoved:k})},0);return a.utils.domNodeDisposal.addDisposeCallback(k,function(){(v.data("ui-sortable")||v.data("sortable"))&&v.sortable("destroy"),a.utils.toggleDomNodeCssClass(k,y.connectClass,!1),clearTimeout(z)}),{controlsDescendantBindings:!0}},update:function(b,c,d,f,g){var h=n(c,"foreach");j(b,e,h.foreach),a.bindingHandlers.template.update(b,function(){return h},d,f,g)},connectClass:"ko_container",allowDrop:!0,afterMove:null,beforeMove:null,options:{}},a.bindingHandlers.draggable={init:function(c,d,e,f,i){var k=h(d())||{},l=k.options||{},m=a.utils.extend({},a.bindingHandlers.draggable.options),o=n(d,"data"),p=k.connectClass||a.bindingHandlers.draggable.connectClass,q=void 0!==k.isEnabled?k.isEnabled:a.bindingHandlers.draggable.isEnabled;return k="data"in k?k.data:k,j(c,g,k),a.utils.extend(m,l),m.connectToSortable=!!p&&"."+p,b(c).draggable(m),void 0!==q&&a.computed({read:function(){b(c).draggable(h(q)?"enable":"disable")},disposeWhenNodeIsRemoved:c}),a.utils.domNodeDisposal.addDisposeCallback(c,function(){var a=b(c);(a.data("ui-draggable")||a.data("draggable"))&&a.draggable("destroy")}),a.bindingHandlers.template.init(c,function(){return o},e,f,i)},update:function(b,c,d,e,f){var g=n(c,"data");return a.bindingHandlers.template.update(b,function(){return g},d,e,f)},connectClass:a.bindingHandlers.sortable.connectClass,options:{helper:"clone"}},a.bindingHandlers.droppable={init:function(d,e,f,j,k){var l=h(e())||{},m=l.options||{},n=a.utils.extend({},a.bindingHandlers.droppable.options),o=void 0!==l.isEnabled?l.isEnabled:a.bindingHandlers.droppable.isEnabled;a.utils.extend(n,m),l="data"in l?l.data:e(),n.drop=function(a,b){var d=i(b.draggable[0],g)||i(b.draggable[0],c);l(d)},b(d).droppable(n),void 0!==o&&a.computed({read:function(){b(d).droppable(h(o)?"enable":"disable")},disposeWhenNodeIsRemoved:d}),a.utils.domNodeDisposal.addDisposeCallback(d,function(){var a=b(d);(a.data("ui-droppable")||a.data("droppable"))&&a.droppable("destroy")})},options:{accept:"*"}}});
\ No newline at end of file
diff --git a/debian/changelog b/debian/changelog
index aebc9ee..60c0f37 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+node-knockout-sortable (1.2.0+git20210818.1.c17d07e-1) UNRELEASED; urgency=low
+
+  * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 04 Apr 2022 22:15:58 -0000
+
 node-knockout-sortable (1.2.0+dfsg-3) unstable; urgency=medium
 
   * Team upload
diff --git a/debian/patches/gruntfile.patch b/debian/patches/gruntfile.patch
index 208198a..fd4dfc8 100644
--- a/debian/patches/gruntfile.patch
+++ b/debian/patches/gruntfile.patch
@@ -1,7 +1,9 @@
 Disable grunt tasks using wrappers not packaged for Debian
---- a/Gruntfile.js
-+++ b/Gruntfile.js
-@@ -39,7 +39,7 @@
+Index: node-knockout-sortable/Gruntfile.js
+===================================================================
+--- node-knockout-sortable.orig/Gruntfile.js
++++ node-knockout-sortable/Gruntfile.js
+@@ -39,7 +39,7 @@ module.exports = function(grunt) {
                      nospawn: true
                  }
              }
@@ -10,7 +12,7 @@ Disable grunt tasks using wrappers not packaged for Debian
          jasmine : {
              src : 'src/*.js',
              options : {
-@@ -51,16 +51,16 @@
+@@ -51,16 +51,16 @@ module.exports = function(grunt) {
                      report: 'reports/coverage'
                  }
              }
diff --git a/package.json b/package.json
index 3850155..92d256d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "knockout-sortable",
-  "version": "1.2.0",
+  "version": "1.2.2",
   "author": "Ryan Niemeyer",
   "homepage": "https://github.com/rniemeyer/knockout-sortable",
   "description": "A Knockout.js binding to connect observableArrays with jQuery UI sortable functionality",
diff --git a/spec/knockout-sortable.spec.js b/spec/knockout-sortable.spec.js
index ec7bb24..1701d0d 100644
--- a/spec/knockout-sortable.spec.js
+++ b/spec/knockout-sortable.spec.js
@@ -958,7 +958,8 @@ describe("knockout-sortable", function(){
                 expect(options.root.draggable("option", "connectToSortable")).toEqual("." + defaults.connectClass);
             });
 
-            it("should be disposed on node", function() {
+            it("should be disposed on node removal", function() {
+                expect(options.root.hasClass("ui-draggable")).toBeTruthy();
                 ko.removeNode(options.root[0]);
                 expect(options.root.hasClass("ui-draggable")).toBeFalsy();
             });
@@ -1165,6 +1166,12 @@ describe("knockout-sortable", function(){
             it("it should have droppable instance", function() {
                 expect(options.root.droppable("instance")).toBeTruthy();
             });
+
+            it("should be disposed on node removal", function() {
+                expect(options.root.hasClass("ui-droppable")).toBeTruthy();
+                ko.removeNode(options.root[0]);
+                expect(options.root.hasClass("ui-droppable")).toBeFalsy();
+            });
         });
         describe("when dropping to an observable", function() {
             beforeEach(function() {
diff --git a/src/knockout-sortable.js b/src/knockout-sortable.js
index 6f63caa..e3c5a45 100644
--- a/src/knockout-sortable.js
+++ b/src/knockout-sortable.js
@@ -427,7 +427,10 @@
 
             //handle disposal
             ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
-                $(element).draggable("destroy");
+                var $element = $(element);
+                if ($element.data("ui-draggable") || $element.data("draggable")) {
+                    $element.draggable("destroy");
+                }
             });
 
             return ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context);
@@ -479,7 +482,10 @@
 
             //handle disposal
             ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
-                $(element).droppable("destroy");
+                var $element = $(element);
+                if ($element.data("ui-droppable") || $element.data("droppable")) {
+                    $element.droppable("destroy");
+                }
             });
         },
         options: {