59

I have a design where I have a list of "available boxes", users take boxes by dragging them from the "available boxes" list to their "My Boxes" list.

Users more often than not take multiple boxes at a time (max 20), once they have finished with the boxes they drag them back to the "available boxes" list to return them.

jQuery sortable allows me to drag one box at a time which from a user perspective is undesirable. I've been unable to come up with a simple solution to the issue.

I may have to come up with a different UI method entirely, but first does anyone have any suggestions on how this might be accomplished?

Thanks!

1

5 Answers 5

77

Working Solution

tl;dr: Refer to this Fiddle for a working answer.


I looked everywhere for a solution to the issue of dragging multiple selected items from a sortable into a connected sortable, and these answers were the best I could find.

However...

The accepted answer is buggy, and @Shanimal's answer is close, but not quite complete. I took @Shanimal's code and built on it.

I fixed:

I added:

  • Proper Ctrl + click (or Cmd + click if on a mac) support for selecting multiple items. Clicking without the Ctrl key held down will cause that item selected, and other items in the same list to be deselected. This is the same click behavior as the jQuery UI Selectable() widget, the difference being that Selectable() has a marquee on mousedrag.

Fiddle

HTML:

<ul>
    <li>One</li>
    <li>Two</li>
    <li>Three</li>
</ul>
<ul>
    <li>Four</li>
    <li>Five</li>
    <li>Six</li>
</ul>

JavaScript (with jQuery and jQuery UI):

$("ul").on('click', 'li', function (e) {
    if (e.ctrlKey || e.metaKey) {
        $(this).toggleClass("selected");
    } else {
        $(this).addClass("selected").siblings().removeClass('selected');
    }
}).sortable({
    connectWith: "ul",
    delay: 150, //Needed to prevent accidental drag when trying to select
    revert: 0,
    helper: function (e, item) {
        var helper = $('<li/>');
        if (!item.hasClass('selected')) {
            item.addClass('selected').siblings().removeClass('selected');
        }
        var elements = item.parent().children('.selected').clone();
        item.data('multidrag', elements).siblings('.selected').remove();
        return helper.append(elements);
    },
    stop: function (e, info) {
        info.item.after(info.item.data('multidrag')).remove();
    }

});

NOTE:

Since I posted this, I implemented something simmilar - connecting draggable list items to a sortable, with multi-select capability. It is set up almost exactly the same, since jQuery UI widgets are so similar. One UI tip is to make sure you have the delay parameter set for the draggables or selectables, so you can click to select multiple items without initiating a drag. Then you construct a helper that looks like all the selected elements put together (make a new element, clone the selected items, and append them), but make sure to leave the original item intact (otherwise it screws up the functionality - I cannot say exactly why, but it involves a lot of frustrating DOM Exceptions).

I also added Shift + Click functionality, so that it functions more like native desktop applications. I might have to start a blog so I can expound on this in greater detail :-)

19
  • It is better... but bug that Ryan mentioned is still reproducable... i dragged 2 elements out... then back in becuase I decided i didnt want to move them and unless I place them in the same postion as they were the DOM 3 exception occurs Commented Mar 8, 2013 at 19:35
  • 1
    @RoopakVenkatakrishnan: After a revamp it's fixed now. Commented Mar 8, 2013 at 22:53
  • 2
    @Mrigesh, I looked at the Fiddle for this answer and remembered how I access the dragged elements in the stop callback. Rather than explain it here, I made an update to the Fiddle that has comments so you can see how to do it. (Hint: it uses the jQuery.data() method to attach the elements to the dragged item that Sortable passes between its callbacks. You have to attach the selected items to item when the sort is initiated. I did it on the helper callback.) jsfiddle.net/hQnWG/614 Commented Jun 17, 2013 at 16:29
  • 1
    @AaronBlenkush thanks for such an effort. Hats off for making it happen. I will try this solution you gave and let you know :) Commented Jun 18, 2013 at 11:48
  • 1
    Definitely the best solution for users who have elements like <select> nested inside their list items. Worked perfectly for me (and I tried other libraries which were very frustrating and had issues) Good job dude, appreciate it Commented Aug 5, 2016 at 1:26
49

I don't have this working using sortable, but I did using draggable & droppable. I don't know if I covered all the functionality you need, but it should be a good start (demo here):

HTML

<div class="demo">
    <p>Available Boxes (click to select multiple boxes)</p>    
    <ul id="draggable">
        <li>Box #1</li>
        <li>Box #2</li>
        <li>Box #3</li>
        <li>Box #4</li>
    </ul>

    <p>My Boxes</p>
    <ul id="droppable">
    </ul>

</div>

Script

$(document).ready(function(){

    var selectedClass = 'ui-state-highlight',
        clickDelay = 600,     // click time (milliseconds)
        lastClick, diffClick; // timestamps

    $("#draggable li")
        // Script to deferentiate a click from a mousedown for drag event
        .bind('mousedown mouseup', function(e){
            if (e.type=="mousedown") {
                lastClick = e.timeStamp; // get mousedown time
            } else {
                diffClick = e.timeStamp - lastClick;
                if ( diffClick < clickDelay ) {
                    // add selected class to group draggable objects
                    $(this).toggleClass(selectedClass);
                }
            }
        })
        .draggable({
            revertDuration: 10, // grouped items animate separately, so leave this number low
            containment: '.demo',
            start: function(e, ui) {
                ui.helper.addClass(selectedClass);
            },
            stop: function(e, ui) {
                // reset group positions
                $('.' + selectedClass).css({ top:0, left:0 });
            },
            drag: function(e, ui) {
                // set selected group position to main dragged object
                // this works because the position is relative to the starting position
                $('.' + selectedClass).css({
                    top : ui.position.top,
                    left: ui.position.left
                });
            }
        });

    $("#droppable, #draggable")
        .sortable()
        .droppable({
            drop: function(e, ui) {
                $('.' + selectedClass)
                 .appendTo($(this))
                 .add(ui.draggable) // ui.draggable is appended by the script, so add it after
                 .removeClass(selectedClass)
                 .css({ top:0, left:0 });
            }
        });

});
1
  • It's a bit random, but dragging an item without selecting it doesn't execute the drag. Selecting two items and dragging them results in only one element dragged to the target. Mostly, some elements remain in the source area instead of being dragged to the target. Chromium 18, Linux 64 Bit.
    – Alp
    Commented Jul 2, 2012 at 16:30
21

JSFiddle: http://jsfiddle.net/hQnWG/

<style>
    ul {border:1px solid Black;width:200px;height:200px;display:inline-block;vertical-align:top}
    li {background-color:Azure;border-bottom:1px dotted Gray}   
    li.selected {background-color:GoldenRod}
</style>
<h1>Click items to select them</h1>
<ul>
    <li>One</li>
    <li>Two<li>
    <li>Three</li>
</ul><ul>
    <li>Four</li>
    <li>Five<li>
    <li>Six</li>
</ul>
<script>
    $("li").click(function(){
        $(this).toggleClass("selected");
    })
    $("ul").sortable({
        connectWith: "ul",
        start:function(e,info){
            // info.item.siblings(".selected").appendTo(info.item);
            info.item.siblings(".selected").not(".ui-sortable-placeholder").appendTo(info.item);

        },
        stop:function(e,info){
            info.item.after(info.item.find("li"))
        }
    })
</script>
9
  • 3
    Thank you! This is super simple and just what I was looking for. One thing I noticed in your JSFiddle example though was that there was a weird bug where if you selected an item then dropped it around the center it would disappear.
    – Ryan
    Commented Aug 24, 2012 at 15:44
  • I figured out jquery-sortable adds an extra li element when you are moving as a placeholder for where the item was. This is getting collected when you call info.item.siblings(".selected"). You may have to add .not(".ui-sortable-placeholder") to that as well to exclude that list item. This seems to fix the bug for me.
    – Ryan
    Commented Aug 24, 2012 at 16:01
  • 2
    This code worked great for me as is. I just added a line to the stop function to make it remove the .selected class from all selected items.
    – supertrue
    Commented Sep 13, 2012 at 3:45
  • 1
    @Shanimal Chrome v25.0.1364.152 m The problem occurs only if you select more than one and drop it on the same list you selected them from and not at the exact same place you picked them from (or sometimes between both lists). select 2 and start dragging and drop immediately Commented Mar 8, 2013 at 20:46
  • 1
    @Shanimal: I believe the issue was with the sibling elements appended inside the dragged element. I've fixed the issue by using jQuery.data() to store the siblings, and then use that after the drop to reconstruct them in the receiving list. The drag 'helper' is a new blank <li/> with the moved elements appended. jsfiddle.net/hQnWG/480 Commented Mar 8, 2013 at 22:55
8

There's a jQuery UI plugin for that: https://github.com/shvetsgroup/jquery.multisortable

jsFiddle: http://jsfiddle.net/neochief/KWeMM/

$('ul.sortable').multisortable();
1
  • Does it support connectWith? How about draggable items?
    – seebiscuit
    Commented Oct 6, 2014 at 18:08
1

Aaron Blenkush's solution has a major fault: removing and adding items to the sortable list breaks structure; refresh can help, but if other functions process the listing, a trigger for all of them is needed to refresh and it all becomes overly complex.

After analysing some solutions at stackoverflow, I've summarized mine in the following:

Do not use helper - use start function, cause it already has ui.item, which is the helper by default.

    start: function(event, ui){
        // only essential functionality below

        // get your own dragged items, which do not include ui.item;
        // the example shows my custom select which selects the elements
        // with ".selected" class
        var dragged = ui.item.siblings(arr["nested_item"]).children('.tRow.tSelected').parent(arr["nested_item"]);

        // clone the dragged items
        var dragged_cloned = dragged.clone();

        // add special class for easier pick-up at update part
        dragged_cloned.each(function(){$(this).addClass('drag_clone');});

        // record dragged items as data to the ui.item
        ui.item.data('dragged', dragged);

        // hide dragged from the main list
        dragged.hide();

        // attached cloned items to the ui.item - which is also ui.helper
        dragged_cloned.appendTo(ui.item);
        },
  1. On the update part:

    update: function(event, ui){
        // only essential functionality below
    
        // attach dragged items after the ui.item and show them
        ui.item.after(ui.item.data("dragged").show());
    
        // remove cloned items
        ui.item.children(".drag_clone").remove();
        },
    

Stop function may need some copy of the update functionality, but is likely to be separate from update, 'cause if no change - do not submit anything to the server.

To add: preserving order of dragged items.

Not the answer you're looking for? Browse other questions tagged or ask your own question.