I started a basic implementation of your requirements using the HTML5 and jQuery drag and drop APIs. The API is lightweight and does not require any third-party scripts. The code should be easy to configure. The above example is just a starting point and is by no means ready for production and should be optimized and possibly turned into a jQuery plugin module before use. This will increase the reusability of the module.
Leave additional code questions in the comments.
JSFiddle example without sorting:
JSFiddle with sorting
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 sorting:
(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 adds sorting to it. I have not tested this example, and it cannot work 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));