Knockout with jQuery UI Sortable

From http://groups.google.com/group/knockoutjs/browse_thread/thread/4c8f5e42d2713c77/450e409fbf1c2faa

by medmunds

HTML

<script src="http://knockoutjs.com/js/jquery.tmpl.js"></script>
<script src="http://github.com/SteveSanderson/knockout/raw/master/build/output/knockout-latest.js"></script>
<div class="container">
    <ul id="sortable1" class="connectedSortable" data-bind="template: 'tmpProducts', onReceiveItem: makeUnselected"></ul>
    <ul id="sortable2" class="connectedSortable" data-bind="template: 'tmpSelected', onReceiveItem: makeSelected"></ul>
    <div id="jsonResults"></div>
</div>

<script type="text/html" id="tmpProducts">
    {{each(i, product) Products()}}
    <li data-id="${ product.Id }" class="ui-state-default">${product.Name}</li>
    {{/each}}
    <br />
    <a href="#" data-bind="click: save">Show Results</a>
</script>

<script type="text/html" id="tmpSelected">
    {{each(i, product) Selected()}}
    <li data-id="${ product.Id }" class="ui-state-highlight">${product.Name}</li>
    {{/each}}
</script>

CSS

#sortable1, #sortable2  {
    list-style-type: none; 
    margin: 0; 
    padding: 0; 
    float: left; 
    margin-right: 10px; 
    background: #eee; 
    padding: 5px; 
    width: 143px;
}
#sortable1 li, #sortable2 li {
    margin: 5px; 
    padding: 5px; 
    font-size: 1.2em; 
    width: 120px; 
}

JavaScript

$(document).ready(function() {

    $("#sortable1, #sortable2").sortable({
        connectWith: ".connectedSortable",
        dropOnEmpty: true,
        receive: function(event, ui) {}
    }).disableSelection();

    var product = function(data) {
        this.Id = data.Id;
        this.Name = data.Name;
        this.Price = data.Price;
    };

    function moveById(ids, fromArray, toArray) {
        var moved = fromArray.remove(function(p) {
            return ko.utils.arrayIndexOf(ids, p.Id.toString()) >= 0;
        });
        for (var i = 0; i < moved.length; i++)
        toArray.push(moved[i]);
    }

    var viewModel = {
        Products: ko.observableArray([
            new product({
            Id: 1,
            Name: "Windows XP",
            Price: 199.99
        }),
            new product({
            Id: 2,
            Name: "Windows Vista",
            Price: 299.99
        }),
            new product({
            Id: 3,
            Name: "Windows 7",
            Price: 239.99
        })
            ]),
        Selected: ko.observableArray([]),
        save: function() {
            $("#jsonResults").html(ko.toJSON(this.Selected));
        },
        makeSelected: function(ids) {
            moveById(ids, this.Products, this.Selected)
        },
        makeUnselected: function(ids) {
            moveById(ids, this.Selected, this.Products)
        },
    };

    ko.bindingHandlers.onReceiveItem = {
        init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
            var callback = valueAccessor();
            $(element).bind("sortreceive", function(event, ui) {
                var receivedIds = $.map(ui.item, function(item) {
                    return $(item).attr("data-id")
                });
                callback.call(viewModel, receivedIds);
            });
        }
    };
    ko.applyBindings(viewModel);
});