<!DOCTYPE html>
<html>

  <head>
    <script data-require="jquery@*" data-semver="2.0.3" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
    <link data-require="jqueryui@*" data-semver="1.10.0" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.0/css/smoothness/jquery-ui-1.10.0.custom.min.css" />
    <script data-require="jqueryui@*" data-semver="1.10.0" src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.0/jquery-ui.js"></script>
    <script data-require="knockout@*" data-semver="3.0.0" src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
    <link rel="stylesheet" href="style.css" />
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.1.0/css/font-awesome.min.css" />
    <script src="knockout-sortable.js"></script>
  </head>

  <body>
    <h2>Multi-select sortable with Knockout.js</h2>
    <p><code>Click</code> to select individual items</p>
    <p><code>Ctrl + Click</code> to select multiple items</p>
    <p><code>Shift + Click</code> to select multiple items</p>
    <div class="list-manager">
      <div class="loading-overlay" data-bind="visible: actingOnThings">
        <span class="fa fa-spin fa-spinner"></span> Doing a thing...
      </div>
      <div class="list">
          <h2>Trash, And Similar Things</h2>
          <ul class="list list-container" id="userContainer_1" data-bind="sortable: { 
            data: userList1, 
            beforeMove: beforeMove,
            afterMove: afterMove,
            options: multiSortableOptions,
            connectToClass: 'list-container' 
          }">
              <li class="item" data-bind="attr: { id: 'sp_'+Id }, text: Name, click: $root.selectProcedure.bind($data, $parent.userList1()), css: { selected: Selected }"></li>
          </ul>
          <button class="selectAll" data-bind="click: selectAllItemsIn.bind(this, userList1)">Select All</button>
          <button class"moveSelected" data-bind="click: moveAllFunction(userList1, userList2)">Move Selected</button>
      </div>
      <div class="list">
          <h2>Actual Things, Like For Real</h2>
          <ul class="list list-container" id="userContainer_2" data-bind="sortable:  { 
            data: userList2, 
            beforeMove: beforeMove,
            afterMove: afterMove,
            options: multiSortableOptions,
            connectToClass: 'list-container' 
          }">
              <li class="item" data-bind="attr: { id: 'sp_'+Id }, text: Name, click: $root.selectProcedure.bind($data, $parent.userList2()), css: { selected: Selected }"></li>
          </ul>
          <button class"moveSelected" data-bind="click: moveAllFunction(userList2, userList1)">Move Selected</button>
          <button class="selectAll" data-bind="click: selectAllItemsIn.bind(this, userList2)">Select All</button>
      </div>
    </div>
    
    <script src="script.js"></script>
  </body>

</html>
var ViewModel = function() {
  var self = this;

  self.userList1 = ko.observableArray([]);

  self.userList2 = ko.observableArray([]);
  
  self.actingOnThings = ko.observable(false);
  
  self.canSort = ko.observable(true);

  for (var i = 0; i < 5000; i++) {
    self.userList2.push({
      Name: 'SP' + i,
      Id: i,
      Selected: ko.observable(false)
    })
  }

  self.multiSortableOptions = {
    revert: 100,
    tolerance: "pointer",
    distance: 15,
    stop: function() {
      if(self.$selected) {
        self.$selected.fadeIn(100);
      }
    },
    helper: function(event, $item) {
      // probably a better way to pass these around than in id attributes, but it works
      var dbId = $item.parent().attr('id').split('_')[1],
        itemId = $item.attr('id').split('_')[1],
        db = myViewModel['userList' + dbId];

      // If you grab an unhighlighted item to drag, then deselect (unhighlight) everything else
      if (!$item.hasClass('selected')) {
        ko.utils.arrayForEach(db(), function(item) {
          //needs to be like this for string coercion
          item.Selected(item.Id == itemId);
        });
      }

      // Create a helper object with all currently selected items
      var $selected = $item.parent().find('.selected');
      var $helper;
      if ($selected.size() > 1) {
        $helper = $('<li class="item selected">You have ' + $selected.size() + ' items selected.</li>');
        $selected.fadeOut(100);
        $selected.removeClass('selected');
      } else {
        $helper = $selected;
      }
      self.$selected = $selected;

      return $helper;
    }
  };

  var moveTheseTo = function(items, from, to, atPosition) {
    self.actingOnThings(true);
    
    var copyFunction = function() {
      var newArgs = [atPosition, 0].concat(items);
  
      ko.utils.arrayForEach(to(), function(item) {
        item.Selected(false);
      });
  
      ko.utils.arrayForEach(items, function(item) {
        from.remove(item);
      });
      
      to.splice.apply(to, newArgs);
      self.actingOnThings(false);
    }
    
    
    if(items.length > 300) {
      setTimeout(copyFunction, 100);
    } else {
      copyFunction();
    }
  };
  
  self.selectAllItemsIn = function(list) {
    ko.utils.arrayForEach(list(), function(item) {
      item.Selected(true);
    });
  };
  
  self.moveAllFunction = function(from, to) {
    return function() {
      var items = ko.utils.arrayFilter(from(), function(item) {
        return item.Selected();
      });
      moveTheseTo(items, from, to, to().length);
    };
  };

  self.beforeMove = function(args, event, ui) {
    if(
      args.sourceParent === args.targetParent 
      && args.targetPosition === args.sourcePosition
    ) {
      self.$selected.fadeIn(100);
      return;
    }
    
    event.cancelDrop = true;
  };

  self.afterMove = function(args, event, ui) {
    
    var items = ko.utils.arrayFilter(args.sourceParent(), function(item) {
      return item.Selected();
    });
    
    moveTheseTo(items, args.sourceParent, args.targetParent, args.targetIndex);
    
    args.item.Selected(true);

  };

  self.selectProcedure = function(array, $data, event) {
    if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {
      $data.Selected(true);
      ko.utils.arrayForEach(array, function(item) {
        if (item !== $data) {
          item.Selected(false);
        }
      });
    } else if (event.shiftKey && !event.ctrlKey && self._lastSelectedIndex > -1) {
      var myIndex = array.indexOf($data);
      if (myIndex > self._lastSelectedIndex) {
        for (var i = self._lastSelectedIndex; i <= myIndex; i++) {
          array[i].Selected(true);
        }
      } else if (myIndex < self._lastSelectedIndex) {
        for (var i = myIndex; i <= self._lastSelectedIndex; i++) {
          array[i].Selected(true);
        }
      }

    } else if (event.ctrlKey && !event.shiftKey) {
      $data.Selected(!$data.Selected());
    }
    self._lastSelectedIndex = array.indexOf($data);
  };

};

myViewModel = new ViewModel();

ko.applyBindings(myViewModel);

.item {
    cursor: pointer;
    padding-left: 10px;
}

.item.selected {
    background-color:#1e91fc;
    color: #fff;
    cursor: move;
}

div.list {
    display: inline-block;
    vertical-align: top;
    width: 300px;
    max-width: 350px;
}

div.list ul.list {
    max-height: 400px;
    height: 400px;
    overflow-y: scroll;
}

.list {
    padding: 0;
    background-color: #fafafa;
    border: 1px solid #CCC;
    list-style-type: none;
    margin: 0;
}

.list-manager {
  position: relative;
  display: table-cell;
}

.loading-overlay {
  position: absolute;
  line-height: 400px;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color:#fafafa;
  filter:alpha(opacity=90);
  opacity:0.9;
  -moz-opacity:0.9;
  z-index:100;

  text-align:center;
  vertical-align:middle;
}
;(function(factory) {
    if (typeof define === "function" && define.amd) {
        // AMD anonymous module
        define(["knockout", "jquery", "jquery.ui.sortable"], factory);
    } else {
        // No module loader (plain <script> tag) - put directly in global namespace
        factory(window.ko, 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;

    //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 = unwrap(valueAccessor()) || {},
            actualAfterRender;

        //build our options to pass to the template engine
        if (options.data) {
            result[dataName] = options.data;
            result.name = options.template;
        } else {
            result[dataName] = valueAccessor();
        }

        ko.utils.arrayForEach(["afterAdd", "afterRender", "as", "beforeRemove", "includeDestroyed", "templateEngine", "templateOptions"], function (option) {
            result[option] = options[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;
    };

    //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;

            //remove leading/trailing non-elements from anonymous templates
            $element.contents().each(function() {
                if (this && this.nodeType !== 1) {
                    element.removeChild(this);
                }
            });

            //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;

            //initialize sortable binding after template binding has rendered in update function
            var createTimeout = setTimeout(function() {
                var dragItem;
                $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) {
                        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;

                        dragItem = null;

                        //make sure that moves only run once, as update fires on multiple containers
                        if (item && (this === parentEl || $.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);
                            }

                            if (sortable.beforeMove || sortable.afterMove) {
                                arg = {
                                    item: item,
                                    sourceParent: sourceParent,
                                    sourceParentNode: sourceParent && ui.sender || el.parentNode,
                                    sourceIndex: sourceIndex,
                                    targetParent: targetParent,
                                    targetIndex: targetIndex,
                                    cancelDrop: false
                                };
                            }

                            if (sortable.beforeMove) {
                                sortable.beforeMove.call(this, arg, event, ui);
                                if (arg.cancelDrop) {
                                    //call cancel on the correct list
                                    if (arg.sourceParent) {
                                        $(arg.sourceParent === arg.targetParent ? this : ui.sender).sortable('cancel');
                                    }
                                    //for a draggable item just remove the element
                                    else {
                                        $(el).remove();
                                    }

                                    return;
                                }
                            }

                            if (targetIndex >= 0) {
                                if (sourceParent) {
                                    sourceParent.splice(sourceIndex, 1);

                                    //if using deferred updates plugin, force updates
                                    if (ko.processAllDeferredBindingUpdates) {
                                        ko.processAllDeferredBindingUpdates();
                                    }
                                }

                                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");
                }

                //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 = 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
                });
            }

            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"
        }
    };

});