5

I have the following situation where I need to allow a user to select objects from a list and drag/drop them into a certain slot:

enter image description here

The objects can be one to three times the size of a slot. So if a user drags Object 1 to Slot 0, then it only occupies Slot 0 (startSlot = 0 and endSlot = 0). However if a user drags Object 3 to Slot 3, then it occupies Slots 3, 4, and 5 (startSlot = 3 and endSlot = 5).

Once the objects are dropped into the slots, a user can reorder the objects by clicking and dragging the objects up and down in the slots. Objects cannot overlap each other:

enter image description here

I am using Angular, so I'm reading a list of objects from a database and I have a variable for the number of slots. I have attempted a couple of solutions. I believe the use of jQuery and jQueryUI draggable, droppable, and sortable is part of the solution, here is the first fiddle demonstrating drag/drop and sortable:

http://jsfiddle.net/mduvall216/6hfuzvws/4/

The problem with this fiddle is that I need a set number of slots. Once an object is placed in the slots, it replaces 1 to 3 slots depending on the size of the object. The second fiddle below integrates AngularJS:

http://jsfiddle.net/mduvall216/zg5x4b6k/4/

The problem here is that I know I need some type of grid to snap the objects to once dragged from the object list. The result that I'm looking for is a json list of objects in their assigned slots:

[{id:obj1,startSlot:0,endSlot:0},{id:obj3,startSlot:3,endSlot:5}]

I'm also sure the solution would need codf0rmer's Angular Drag-Drop located here:

https://github.com/codef0rmer/angular-dragdrop

But I'm having problems trying to get that integrated into my fiddle to test. This is an interesting challenge I've been spinning on for a while, if anyone can be of assistance it would be greatly appreciated. Thanks for your time.

2
  • Have a look at github.com/angular-ui/ui-sortable , that's part of the popular AngularUI suite of components. They support connected lists, but your item size requirement would probably need some customer handling of the list items, but should be straight forward when using the events. I've used it successfully inside a project before. Unfortunately I do not have time right now to look into your specific requirements. Commented Nov 5, 2015 at 23:42
  • You can also create a custom module using the HTML 5 drag and drop API. Its quite easy to implement. Commented Nov 8, 2015 at 0:32

1 Answer 1

5
+100

I started a basic implementation of your requirements using the HTML5 Drag & Drop API and jQuery. The API is lightweight and doesn't require any 3rd party scripts. The code should be easy to customize. The provided example is only a starting point and by no means production ready and should be optimized and possibly turned into a jQuery plugin module before use. This would increase the re-usability of the module.

Leave any further questions about the code in the comments.

JSFiddle Example without sortable:

JSFiddle with sortable

html:

<ul class="select-list">
    <li class="header">Object List</li>
    <li data-slots="1" class="s1">Object 1</li>
    <li data-slots="2" class="s2">Object 2</li>
    <li data-slots="3" class="s3">Object 3</li>
</ul>
<ul class="drop-list" id="sortable">
    <li>Slot 1</li>
    <li>Slot 2</li>
    <li>Slot 3</li>
    <li>Slot 4</li>
    <li>Slot 5</li>
    <li>Slot 6</li>
    <li>Slot 7</li>
    <li>Slot 8</li>
    <li>Slot 9</li>
    <li>Slot 10</li>
    <li>Slot 11</li>
    <li>Slot 12</li>
    <li>Slot 13</li>
</ul>

javascript without sortable:

(function ($, undefined) {
    // document ready function
    $(function () {
        init();

        $('ul.select-list').on({
            'dragstart': dragstart,
                'dragend': dragend
        }, 'li');

        $('ul.drop-list').on({
            'dragenter dragover': dragover,
                'dragleave': dragleave,
                'drop': drop
        }, 'li.dropzone');
    });

    // Initializes the lists
    function init() {
        $('ul.select-list').find('li').not('[class="header"]').each(function () {
            var height = getSlotHeight() * $(this).data('slots');
            $(this).height(height);
        }).attr('draggable', true);

        $('ul.drop-list').find('li').each(function () {
            $(this).height(getSlotHeight());
        }).addClass('dropzone');
    }

    // Get the height of grid slots
    function getSlotHeight() {
        return 20;
    }

    /**
    * Checks whether target is a kompatible dropzone
    * A dropzone needs the dropzone class
    * and needs to have enough consequent slots to drop the object into
    */
    function isDropZone(target, draggedObj) {
        var isDropZone = true;
        var slots = draggedObj.data('slots');
        for (var i = 1; i < slots; i++) {
            target = target.next();
            if (target.size() == 0 || !target.hasClass('dropzone')) {
                isDropZone = false;
                break;
            }
        }
        return isDropZone;
    }    

    /* 
    * The following events are executed in the order the handlers are declared
    * dragstart being first and dragend being last
    */

    // dragstart event handler
    function dragstart(e) {
        e.stopPropagation();
        var dt = e.originalEvent.dataTransfer;
        dt.effectAllowed = 'move';
        dt.setData('text/html', '');
        $('ul.select-list').data({
            draggedObj: $(this)
        });
    }

    // dragover event handler
    function dragover(e) {
        e.preventDefault();
        e.stopPropagation();
        var data = $('ul.select-list').data();
        if (!data.draggedObj || !isDropZone($(this), data.draggedObj)) {
            e.originalEvent.dataTransfer.dropEffect = 'none';
            return;
        }
        e.originalEvent.dataTransfer.dropEffect = 'move';
        var item = $(this).addClass('dragover');
        var slots = data.draggedObj.data('slots');
        for (var i = 1; i < slots; i++) {
            item = item.next('li').addClass('dragover');
        }
        return false;
    }

    // dragleave event handler
    function dragleave(e) {
        e.preventDefault();
        e.stopPropagation();
        var data = $('ul.select-list').data();
        if (!data.draggedObj || !isDropZone($(this), data.draggedObj)) {
            return;
        }
        var item = $(this).removeClass('dragover');
        var slots = data.draggedObj.data('slots');
        for (var i = 1; i < slots; i++) {
            item = item.next('li').removeClass('dragover');
        }
        return false;
    }

    // drop event handler
    function drop(e) {
        e.stopPropagation();
        e.preventDefault();
        var data = $('ul.select-list').data();
        if (data.draggedObj || !isDropZone($(this), data.draggedObj)) {
            data.target = $(this);
            data.draggedObj.trigger('dragend');
        }
        return false;
    }

    // dragend event handler
    function dragend(e) {
        var data = $('ul.select-list').data();
        if (data.draggedObj && data.target && isDropZone(data.target, data.draggedObj)) {
            var item = data.target.hide();
            var slots = data.draggedObj.data('slots');
            for (var i = 1; i < slots; i++) {
                item = item.next('li').hide();
            }
            data.target.before(data.draggedObj);
        }

        data.target = undefined;
        data.draggedObj = undefined;
        $('ul.drop-list').find('li').removeClass('dragover');
    }
}(jQuery));

css:

ul {
     list-style-type: none;
     margin: 0;
     padding: 0;
     float: left;
 }
 li {
     width: 150px;
 }
 li.header {
     height:20px;
     font-weight:bold;
 }
 .select-list li {
     margin-bottom: 10px;
 }
 .drop-list {
     margin-left:20px;
 }
 .drop-list li {
     background-color: #ccc;
     border-left: 1px solid black;
     border-right: 1px solid black;
     border-top: 1px solid black;
 }
 .drop-list li.dragover {
     background-color:#fff;
 }
 .drop-list li:last-child {
     border-bottom: 1px solid black;
 }
 li.s1 {
     background-color: #FFE5E5;
 }
 li.s2 {
     background-color: #C6D4FF;
 }
 li.s3 {
     background-color: #C6FFE3;
 }

Edit: The following script also has sorting added to it. I have not stress tested this example and it may not perform under certain conditions.

(function ($, undefined) {
    // document ready function
    $(function () {
        init();

        $('ul.select-list,ul.drop-list').on({
            'dragstart': dragstart,
                'dragend': dragend
        }, 'li.object').on('dragenter dragover', listDragover);

        $('ul.drop-list').on({
            'dragenter dragover': dragover,
                'dragleave': dragleave,
                'drop': drop
        }, 'li.dropzone');
    });

    // Initializes the lists
    function init() {
        $('ul.select-list').find('li').not('[class="header"]').each(function () {
            var height = getSlotHeight() * $(this).data('slots');
            $(this).height(height);
        }).attr('draggable', true).addClass('object');

        $('ul.drop-list').find('li').each(function () {
            $(this).height(getSlotHeight());
        }).addClass('dropzone');
    }

    // Get the height of the grid
    function getSlotHeight() {
        return 20;
    }

    /**
     * Checks whether target is a kompatible dropzone
     * A dropzone needs the dropzone class
     * and needs to have enough consequent slots to drop the object into
     */
    function isDropZone(target, draggedObj) {
        var isDropZone = true;
        var slots = draggedObj.data('slots');
        for (var i = 1; i < slots; i++) {
            target = target.next('li');
            if (target.size() == 0 || !target.hasClass('dropzone')) {
                if (!target.is(draggedObj)) {
                    isDropZone = false;
                    break;
                } else {
                    i--;
                }
            }
        }
        return isDropZone;
    }

    // dragstart event handler
    function dragstart(e) {
        e.stopPropagation();
        var dt = e.originalEvent.dataTransfer;
        dt.effectAllowed = 'move';
        dt.setData('text/html', ''); 
        $('ul.select-list').data({
            draggedObj: $(this)
        });        
    }

    // dragover list event handler
    function listDragover(e) {
        e.preventDefault();
        e.stopPropagation();
        e.originalEvent.dataTransfer.dropEffect = 'none';
        var data = $('ul.select-list').data();
        if (data.draggedObj) {
            var item = data.draggedObj;
            item.hide();
            if (data.draggedObj.closest('ul').is('ul.drop-list')) {
                var slots = item.data('slots');
                for (var i = 0; i < slots; i++) {
                    item = item.next('li').show();
                }
            }
        }
        return false;
    }

    // dragover event handler
    function dragover(e) {
        e.preventDefault();
        e.stopPropagation();
        var data = $('ul.select-list').data();
        if (!data.draggedObj || !isDropZone($(this), data.draggedObj)) {
            e.originalEvent.dataTransfer.dropEffect = 'none';
            return;
        }
        e.originalEvent.dataTransfer.dropEffect = 'move';
        var item = $(this).addClass('dragover');
        var slots = data.draggedObj.data('slots');
        for (var i = 1; i < slots; i++) {
            item = item.next('li');
            if (!item.is(data.draggedObj)) {
                item.addClass('dragover');
            } else {
                i--;
            }
        }
        return false;
    }

    // dragleave event handler
    function dragleave(e) {
        e.preventDefault();
        e.stopPropagation();
        var data = $('ul.select-list').data();
        if (!data.draggedObj || !isDropZone($(this), data.draggedObj)) {
            return;
        }
        var item = $(this).removeClass('dragover');
        var slots = data.draggedObj.data('slots');
        for (var i = 1; i < slots; i++) {
            item = item.next('li');
            if (!item.is(data.draggedObj)) {
                item.removeClass('dragover');
            } else {
                i--;
            }
        }
        return false;
    }

    // drop event handler
    function drop(e) {
        e.stopPropagation();
        e.preventDefault();
        var data = $('ul.select-list').data();
        if (data.draggedObj || !isDropZone($(this), data.draggedObj)) {
            data.target = $(this);
            data.draggedObj.trigger('dragend');
        }
        return false;
    }

    // dragend event handler
    function dragend(e) {
        var data = $('ul.select-list').data();
        var target = data.target;
        if (data.draggedObj && !target && data.draggedObj.closest('ul').is('ul.drop-list')) {
            target = data.draggedObj.next('li');
        }
        if (data.draggedObj && target && isDropZone(target, data.draggedObj)) {
            data.draggedObj = data.draggedObj.insertBefore(target);
            var item = target.hide();
            var slots = data.draggedObj.data('slots');
            for (var i = 1; i < slots; i++) {
                item = item.next('li').hide();
            }
        }
        if (data.draggedObj) {
            data.draggedObj.show();
        }
        data.target = undefined;
        data.draggedObj = undefined;
        $('ul.drop-list').find('li').removeClass('dragover');
    }
}(jQuery));
Sign up to request clarification or add additional context in comments.

1 Comment

this is great, just what I needed to get over the 'hump' and provide a solution to our customer. The fiddle is perfect, thanks again for your help.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.