jQuery Sortable - Select and Drag Multiple List Items
Asked Answered
C

5

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!

Comportment answered 23/9, 2010 at 1:16 Comment(1)
See my answer for a working solution. The accepted answer and the answer by @Shanimal are good starts, but they had some bugs that needed to be worked out.Cheryle
A
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 });
            }
        });

});
Abbate answered 23/9, 2010 at 14:20 Comment(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.Chasechaser
C
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 :-)

Cheryle answered 8/3, 2013 at 19:24 Comment(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 occursRemontant
@RoopakVenkatakrishnan: After a revamp it's fixed now.Cheryle
Hi Aaron,Your solution works perfectly.But in some senarios i need to only allow user to select all the the list(all the <li>) and put into other <ul>.Cookhouse
@Gautam: I'm not sure what you mean; you can Ctrl+click to select all the list items at the same time. I've since used this in an application and I added Ctrl + A for select all and Shift+click to select a range of items. The Shift+click is a bit trickier, but Ctrl+A works as long as you know which list has "focus". Just listen for Ctrl+A, then add the selected class to each item.Cheryle
@aaron Awesome stuff; but I have a function that handles some sync on other DOM area after the drag completion i.e. thru stop event. And at that time I need the recently dragged item's objects. I haven't been able to get that dragged stack. Can u help me on this?Gumption
@Mrigesh: Have you checked out the API documentation for jQuery UI for Sortable and Draggable? In particular, look at sortable's event callbacks. I'd recommend creating a Fiddle and exploring the values of the event and ui objects by printing to the Console (console.dir(event);, for example). I rarely memorize this stuff - whenever I need to "remember" I just take a couple minutes to what I described above and see exactly what's happening, if the API docs don't tell me right away.Cheryle
@AaronBlenkush that app segment where this scenario exists is real bizzare. Fiddling it is real hard stuff. Abt api, i checked all but couldnt get a collection of multiple items just dragged in.Gumption
@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/614Cheryle
@AaronBlenkush thanks for such an effort. Hats off for making it happen. I will try this solution you gave and let you know :)Gumption
This solution leads me to a few errors (because some items are removed from the list I guess). I have found it was more safe to hide elements, and remove them in the stop functionFante
@Fante That makes sense. It's all smoke and mirrors anyway; whatever achieves the end result of a (reliable) user experience! If you feel so inclined, post your improvement as separate answer for posterity :-)Cheryle
If we have selected multiple items and then decide not drop them (reason could be incorrect selection),then one should be able to do press ecaspe during drag to revert items back to tehir original position. any ideas how to do it ?Cymry
@Cymry It's outside of the scope of this question. You should research it and open a new question if you can't figure it out. Be sure to include what you have tried. forum.jquery.com/topic/how-to-cancel-drag-while-draggingCheryle
@AaronBlenkush your solution works perfect but i want one modification to have clone elements rather to remove them from source list. can u help me how i can achieve that ? your code remove elements from source list.Hanoi
One problem with this is that if i simply drag non-consecutive items and leave them in same container , upon reverting back the order of items will be changed...Kazbek
@TJ: This is Stack Overflow, not open source code repo. The answer is meant to be a starting point, not a final solution ;-)Cheryle
@AaronBlenkush well, so are the other answers.. you were saying accepted answer is buggy, other answers are not quite complete etc, hence i pointed out - this one too.Kazbek
Nice work, Aaron. I was wondering what a clean entry point may be, without having to muck with the internals too much, and it seems you've found it. I'm implementing your code for a connected draggable. As you may have encountered, connectToSortable takes the draggable helper and passes it along to the sortable. Therefore, the elements that are stored in the helper of the draggable are then lost by the time we get to the sortable drop function. How did you get around this?Allowable
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 itGraphitize
A
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 });
            }
        });

});
Abbate answered 23/9, 2010 at 14:20 Comment(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.Chasechaser
V
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>
Verlinevermeer answered 17/7, 2012 at 18:5 Comment(9)
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.Sweeps
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.Sweeps
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.Peptidase
@Sweeps I know im opening an old question up... But I've been trying to get rid of the bug u described above... and even the not doesnt seem to help me.. maybe new jquery version... could you tell me what exactly you did?Remontant
@Peptidase did you try this?Verlinevermeer
What browser are you using? I can't reproduce the problem. codepen.io/anon/pen/lcofVerlinevermeer
@Verlinevermeer 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 immediatelyRemontant
I think its trying to append it to its selected self. Let me see if I can fix itVerlinevermeer
@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/480Cheryle
C
8

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

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

$('ul.sortable').multisortable();
Calesta answered 16/9, 2013 at 17:43 Comment(1)
Does it support connectWith? How about draggable items?Allowable
E
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.

Eadith answered 2/10, 2017 at 17:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.