4

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

Henrik
  • 1,797
  • 4
  • 22
  • 46

2 Answers2

4

Note: this answer only allows sorting one item at a time. For sorting multiple items at a time, see this solution.

I recommend using Ryan Niemeyer's knockout-sortable binding handler (https://github.com/rniemeyer/knockout-sortable), which takes care of a lot of the quirks of dealing with knockout and jQuery's sortable together.

If using knockout-sortable, you could use the beforeMove callback to cancel the drop and add a copy to the second list based on your conditions.

JSFiddle working example, and here's the relevant parts:

HTML

Notice the sortable binding where we pass in an object specifying the data to use for the list, as well as which method to use for the beforeMove callback.

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

JavaScript

And we add this method to the view model to check if the target list already has the item, copies the item if it doesn't, and then cancels the drop event so the original item stays in its original list.

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);
    }
    // Only cancels drop if moving to new list to allow sorting within original list
    if(event.targetParent != event.sourceParent) {
        event.cancelDrop = true;
    }
};
Community
  • 1
  • 1
Charlie
  • 575
  • 2
  • 7
  • Thanks for your input. Is it possible to select multiple items to copy at same time with knockout sortable? – Henrik Apr 10 '14 at 06:47
  • I've started a Bounty, hope for an answer from you. – Henrik Apr 10 '14 at 14:04
  • 1
    Hey @Henrik, I don't have time right now to write up an answer for dragging multiple items, but I did find this related answer that might get you going in the right direction: http://stackoverflow.com/questions/3774755/jquery-sortable-select-and-drag-multiple-list-items/15301704#15301704 – Charlie Apr 10 '14 at 14:37
  • Okey, thank anyways. I've tried using jquery-ui and sortable to implement dragging multiple items, but without success. – Henrik Apr 10 '14 at 14:49
  • If I have time later this week I'll see if I can come up with something. – Charlie Apr 10 '14 at 14:53
  • That would be awesome. Thanks for your help. – Henrik Apr 14 '14 at 13:25
  • It would be very appreciated if you have a little time over to provide me with some info how to fix this. :) Have a nice day! – Henrik Apr 16 '14 at 08:38
  • Ill give you the Bounty as it ends soon. Hope you could help me with this.. Thank you. – Henrik Apr 17 '14 at 13:36
  • Hey Henrik, thanks for the bounty. I added a new answer with a solution that allows dragging multiple items, let me know if you have any questions. – Charlie Apr 17 '14 at 15:57
  • Thanks a lot man. :) Is it possible to do so the copied items always be added to the bottom of the list? – Henrik Apr 21 '14 at 17:21
  • You can use `.push()` instead of `.splice()` on the target array and it'll add the items to the end of the array. Here's an updated fiddle that uses `.push()` when copying: http://jsfiddle.net/QWgRF/717/ – Charlie Apr 21 '14 at 20:55
3

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);
Community
  • 1
  • 1
Charlie
  • 575
  • 2
  • 7