jQuery Sortable - Select and Drag Multiple List Items

JqueryJquery Ui

Jquery Problem Overview


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!

Jquery Solutions


Solution 1 - Jquery

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

Solution 2 - Jquery

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 });
            }
        });

});

Solution 3 - Jquery

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>

Solution 4 - Jquery

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

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

$('ul.sortable').multisortable();

Solution 5 - Jquery

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);
		},

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiondanspantsView Question on Stackoverflow
Solution 1 - JqueryAaron BlenkushView Answer on Stackoverflow
Solution 2 - JqueryMottieView Answer on Stackoverflow
Solution 3 - JqueryShanimalView Answer on Stackoverflow
Solution 4 - JqueryneochiefView Answer on Stackoverflow
Solution 5 - JquerybbeView Answer on Stackoverflow