Draggable, droppable, and sortable list

http://www.knockmeout.net/2011/05/dragging-dropping-and-sorting-with.html

by unklefolk

HTML

<script src="http://rniemeyer.github.com/KnockMeOut/Scripts/jquery.tmpl.js"></script>
<script src="http://rniemeyer.github.com/KnockMeOut/Scripts/knockout-latest.debug.js"></script>
<div id="main">
   
    <h2>Tasks</h2>
    <ul class="container topLevelContainer" data-bind="template: { name: 'taskTmpl', foreach: normalPriorityTasks, templateOptions: { parentList: normalPriorityTasks} }, sortableList: normalPriorityTasks"></ul>
    
    <a href="#" data-bind="click: addTask">Add Task</a>
    <hr />
    
    <script id="taskTmpl" type="text/html">
        <li class="item" data-bind="sortableItem: { item: $data, parentList: $item.parentList }" >
            <a href="#" data-bind="text: name, click: function() { viewModel.selectTask($data); }, visible: $data !== viewModel.selectedTask()"></a>
            <input data-bind="value: name, visibleAndSelect: $data === viewModel.selectedTask(), event: { blur: function() { viewModel.selectTask(''); } }" />
{{if childTasks != null}}
            <ul class="container"  data-bind="template: { name: 'taskTmpl', foreach: childTasks, templateOptions: { parentList: childTasks} }, sortableList: childTasks"></ul>  
            {{/if}}     
        </li>
    </script>
    
</div>    

<script id="resultTmpl" type="text/html">
    <li data-bind="text: name">
  {{if childTasks != null}}
    <ul data-bind="template: { name: 'resultTmpl', foreach: childTasks}"></ul>
   {{/if}}  
    </li>
</script>
        
<div id="results">
    <h2>Tasks</h2> 
  
    <ul data-bind="template: { name: 'resultTmpl', foreach: normalPriorityTasks }"></ul>
     
</div>

CSS

body { font-family: arial; }
h2 { font-weight: bold; }
ul, li {  padding: 5px; margin: 5px; border: black 1px solid; }
p, a { font-size: .8em; }
ul { padding-bottom: 10px; }
.container { }
.topLevelContainer {  width: 125px; min-height: 50px; background-color: #AAA;}
.item { background-color: #DDD; cursor: move; }
.item input { width: 100px; }
#main { float: left; }
#results { margin-left: 175px; width: 150px; }

JavaScript

function Task(name, childTasks) {
    this.name = ko.observable(name);
    this.childTasks = ko.observableArray(childTasks);
}
var viewModel = {
    normalPriorityTasks: ko.observableArray([
        new Task("Fix fence", [new Task("Child A"), new Task("Child B"), new Task("Child C")]),
        new Task("Walk dog"),
        new Task("Read book")
        ]),
    selectedTask: ko.observable(),
    selectTask: function(task) {
        this.selectedTask(task);
    },
    addTask: function() {
        var task = new Task("new");
        this.selectedTask(task);
        this.normalPriorityTasks.push(task);
    },
    trash: []
};

//connect items with observableArrays
ko.bindingHandlers.sortableList = {
    init: function(element, valueAccessor, allBindingsAccessor, context) {
        $(element).data("sortList", valueAccessor()); //attach meta-data
        $(element).sortable({
            update: function(event, ui) {
                var item = ui.item.data("sortItem");
                if (item) {
                    //identify parents
                    var originalParent = ui.item.data("parentList");
                    var newParent = ui.item.parent().data("sortList");
                    //figure out its new position
                    var position = ko.utils.arrayIndexOf(ui.item.parent().children(), ui.item[0]);
                    if (position >= 0) {
                        originalParent.remove(item);
                        newParent.splice(position, 0, item);
                    }
                }
            },
            connectWith: '.container'
        });
    }
};

//attach meta-data
ko.bindingHandlers.sortableItem = {
    init: function(element, valueAccessor) {
        var options = valueAccessor();
        $(element).data("sortItem", options.item);
        $(element).data("parentList", options.parentList);
    }
};

//control visibility, give element focus, and select the contents (in order)
ko.bindingHandlers.visibleAndSelect = {
    update:...