Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sortable - Drag-and-drop items from a list to another (copying)

I want to be able to select multiple items and copy them from a list to the otherone (bidirectional) by using drag-and-drop and only drop if it doesnt exists already...

Some help would be appreciated alot.

EDIT: Regarding to Charlie's post and fiddle, how can i make it possible to select multiple items to drag and drop to the other list? As it is now, it only permit 1 item at same time.

HTML:

<div class="list">
    <h2>Stored procedures In DB 1</h2>
    <ul class="list" data-bind="sortable: { data: storedProceduresInDB1, beforeMove: checkAndCopy }">
        <li class="item" data-bind="text: Name"></li>
    </ul>
</div><br>

<div class="list">
    <h2>Stored procedures In DB 2</h2>
    <ul class="list" data-bind="sortable: { data: storedProceduresInDB2, beforeMove: checkAndCopy }">
        <li class="item" data-bind="text: Name"></li>
    </ul>
</div>

Code:

var ViewModel = function () {
    var self = this;
   self.storedProceduresInDB1 = ko.observableArray([
        { Name: 'SP1', Id: 1 },
        { Name: 'SP2', Id: 2 },
        { Name: 'SP3', Id: 3 }
    ]);
    self.storedProceduresInDB2 = ko.observableArray([
        { Name: 'SP3', Id: 3 },
        { Name: 'SP4', Id: 4 },
        { Name: 'SP5', Id: 5 }
    ]);
    self.selectedStoredProcedureInDB1 = ko.observable();
    self.selectedStoredProcedureInDB2 = ko.observable();
    self.selectStoredProcedureInDB1 = function (sp) {
        self.selectedStoredProcedureInDB1(sp);
    };
    self.selectStoredProcedureInDB2 = function (sp) {
        self.selectedStoredProcedureInDB2(sp);
    };
    self.checkAndCopy = function(event) {
        var targetHasItem = ko.utils.arrayFilter(event.targetParent(), function(item) {
            return item.Id == event.item.Id;
        }).length;
        if(!targetHasItem) {
            event.targetParent.splice(event.targetIndex, 0, event.item);
        }
        if(event.targetParent != event.sourceParent) {
            event.cancelDrop = true;
        }
    };
};

ko.applyBindings(new ViewModel());

Charlie's JSFiddle

like image 809
Henrik Avatar asked Mar 20 '23 23:03

Henrik


1 Answers

Here's a solution that allows sorting multiple items at a time. Based on this related answer but modified to use knockout.js and knockout-sortable.

JSFiddle (tested in Chrome): http://jsfiddle.net/QWgRF/715/

HTML

Added IDs to the sortables and list items so that the jQuery sortable code could determine which items were being selected.

Added an options property which is passed to the underlying jQuery Sortable plugin.

Added a click handler to each list item for selection and multi-selection.

<div class="list">
    <h2>Stored procedures In DB 1</h2>
    <ul class="list" id="sortableForDB_1" data-bind="sortable: {
      data: storedProceduresInDB1,
      beforeMove: checkAndCopy,
      options: multiSortableOptions }">
        <li class="item" data-bind="attr: { id: 'sp_'+Id }, text: Name,
          click: $root.selectProcedure.bind($data, $parent.storedProceduresInDB1()),
          css: { selected: Selected }">
        </li>
    </ul>
</div>
<div class="list">
    <h2>Stored procedures In DB 2</h2>
    <ul class="list" id="sortableForDB_2" data-bind="sortable:  {
      data: storedProceduresInDB2,
      beforeMove: checkAndCopy,
      options: multiSortableOptions }">
        <li class="item" data-bind="attr: { id: 'sp_'+Id }, text: Name,
          click: $root.selectProcedure.bind($data, $parent.storedProceduresInDB2()),
          css: { selected: Selected }">
        </li>
    </ul>
</div>

JavaScript

Sortable options. Delay 150 to allow the click handler to function for selecting. Revert 0 since the animations would look strange with how we're messing with the list and helper.

The stop function resets the observableArrays which causes the list html to be refreshed.

The helper function grabs all selected items and adds them into a new helper tag to allow for the visual of dragging multiple items around.

var multiSortableOptions =  {
    delay: 150,
    revert: 0,
    stop: function(event, ui) {
        // Force lists to refresh all items
        var db1 = myViewModel.storedProceduresInDB1,
            db2 = myViewModel.storedProceduresInDB2,
            temp1 = db1(),
            temp2 = db2();
        ui.item.remove();
        db1([]);
        db1(temp1);
        db2([]);
        db2(temp2);
    },
    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['storedProceduresInDB'+dbId];

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

        // Create a helper object with all currently selected items
        var $selected = $item.parent().find('.selected');
        var $helper = $('<li class="helper"/>');
        $helper.append($selected.clone());
        $selected.not($item).remove();
        return $helper;
    }
};

View Model. Added a Selected observable property to each stored procedure. Added a selectProcedure method to update the new Selected property based on clicks and ctrl+clicks.

Also heavily modified the checkAndCopy method. The inline comments I think explain how it works pretty well.

var ViewModel = function () {
    var self = this;
    self.storedProceduresInDB1 = ko.observableArray([
        { Name: 'SP1', Id: 1, Selected: ko.observable(false) },
        { Name: 'SP2', Id: 2, Selected: ko.observable(false) },
        { Name: 'SP3', Id: 3, Selected: ko.observable(false) }
    ]);
    self.storedProceduresInDB2 = ko.observableArray([
        { Name: 'SP3', Id: 3, Selected: ko.observable(false) },
        { Name: 'SP4', Id: 4, Selected: ko.observable(false) },
        { Name: 'SP5', Id: 5, Selected: ko.observable(false) }
    ]);
    self.checkAndCopy = function(event) {
        var items;
        if(event.targetParent !== event.sourceParent) {
            // Get all items that don't yet exist in the target
            items = ko.utils.arrayFilter(event.sourceParent(), function(item) {
                return item.Selected() && !ko.utils.arrayFirst(event.targetParent(), function(targetItem) {
                    return targetItem.Id == item.Id;
                });
            });
            // Clone the items (otherwise the Selected observable is shared by the item in both lists)
            items = ko.utils.arrayMap(items, function(item) {
                var clone = ko.toJS(item);
                clone.Selected = ko.observable(true);

                // While we're here, let's deselect the source items so it looks cleaner
                item.Selected(false);

                return clone;
            });
            // Deselect everything in the target list now so when we splice only the new items are selected
            ko.utils.arrayForEach(event.targetParent(), function(item) {
                item.Selected(false);
            });
        } else {
            // Moving items within list, grab all selected items from list
            items = event.sourceParent.remove(function(item) {
                return item.Selected();
            });
        }

        // splice items in at target index
        items = items.reverse();
        for(var i=0; i<items.length; i++) {
            event.targetParent.splice(event.targetIndex, 0, items[i]);
        }

        // always cancel drop now, since we're manually rearranging everything
        event.cancelDrop = true;
    };
    self.selectProcedure = function(array, $data, event) {
        if(!event.ctrlKey && !event.metaKey) {
            $data.Selected(true);
            ko.utils.arrayForEach(array, function(item) {
                if(item !== $data) {
                    item.Selected(false);
                }
            });
        } else {
            $data.Selected(!$data.Selected());
        }
    };

};

Added a global reference to our View Model so the jQuery code can interact with it.

myViewModel = new ViewModel();

ko.applyBindings(myViewModel);
like image 174
Charlie Avatar answered Apr 24 '23 20:04

Charlie