Here is the solution I came up with, purely with JS and JQuery, with no external libraries required and without using JQueryUI Sortables.
HTML:
<div class="list_container">
<div class="list_item">One</div>
<div class="list_item">Two</div>
<div class="list_item">Three</div>
<div class="list_item">Four</div>
<div class="list_item">Five</div>
<div class="list_item">Six</div>
</div>
where list_container
holds the individual list_item
elements. Is it the latter of the two which can be moved around to create your sorted list. You can put just about anything you'd like within list_item
and it'll still work just fine.
CSS:
.list_container {
position: relative;
}
.list_item {
position: absolute;
z-index: 0;
left: 0px;
}
.list_item.selected {
z-index: 1000;
}
Please visit this fiddle for the full list of CSS rules (only necessary ones are shown above).
JavaScript:
I'll go through this bit-by-bit and then show the full code at the bottom.
First off, I defined an array that matches up index numbers with their written counterparts
var classes = new Array("one", "two", "three", ...);
This is used to create classes dynamically (upon page load). These classes are used to order the list. You are only required to populate this array with as many items as you will have in your list. This is the one downfall of the code I have written and am unsure of how to overcome this issue (would be VERY tedious to enter in the elements for a list of hundreds of items, or more!)
Next, a few other variables:
var margin = 2; // Space desired between each list item
var $el; // Used to hold the ID of the element that has been selected
var oldPos = 0; // The position of the selected element BEFORE animation
var newPos = 0; // The position of the selected element AFTER animation (also current position)
var dragging = false; // Whether or not an item is being moved
var numElements = $('.list_container > div').length;
// selectionHeight is the height of each list element (assuming all the same height)
// It includes the div height, the height of top and bottom borders, and the desired margin
var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;
var classInfo = ''; // classInfo will be populated with the information that is used to dynamically create classes upon page load
When page loads, go through each list_item
and assign it a class according to its initial position in the list. Also add to classInfo
the location of the TOP of the list item.
$('.list_container .list_item').each(function (index) {
$(this).addClass(classes[index]);
classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
});
Now, using classInfo
that was created above, dynamically write the classes to the page.
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = classInfo;
document.getElementsByTagName('head')[0].appendChild(style);
The above bit of code will write the required classes into the HTML of the page. If you view the source of the page, you can see the classes in the head of the page.
Now for the ordering part. First, mousedown
$('.list_item').mousedown(function (ev) {
$el = $(this);
oldPos = $el.index() + 1;
newPos = oldPos;
dragging = true;
startY = ev.clientY; // Gets the current mouse position
startT = parseInt($el.css('top')); // Gets the current position of the TOP of the item
$el.addClass('selected'); // Adding class brings it to top (z-index) and changes color of list item
});
Next, the mousemove
and mouseup
functions are tied together
$(window).mousemove(function (ev) { // Use $(window) so mouse can leave parent div and still work
if (dragging) {
$el.attr('class', 'list_item') // Remove the numbered class (.one, .two, etc)
$el.addClass('selected'); // Add this class back for aesthetics
// ----- calculate new top
var newTop = startT + (ev.clientY - startY);
$el.css('cursor', 'pointer');
// ------
//------ stay in parent
var maxTop = $el.parent().height() - $el.height();
newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
$el.css('top', newTop);
//------
newPos = getPos(newTop, selectionHeight); // Determine what the current position of the selected list item is
// If the position of the list item has changed, move the position's current element out of the way and reassign oldPos to newPos
if (oldPos != newPos) {
moveThings(oldPos, newPos, selectionHeight);
oldPos = newPos;
}
}
}).mouseup(function () {
dragging = false; // User is no longer dragging
$el.removeClass('selected'); // Element is no longer selected
setNewClass($el, newPos); // Set the new class of the moved list item
$el.css('top', (newPos - 1) * selectionHeight); // Position the moved element where it belongs. Otherwise it'll come to rest where you release it, not in its correct position.
});
Finally, the three functions getPos
, moveThings
and setNewClass
are as follows:
function getPos(a, b) { // a == newTop, b == selectionHeight
return Math.round( (a/b) + 1 );
}
getPos
works by finding out which region the selected element is currently in. If newTop is less than .5b, then it is in region 1. If between .5b and 1.5b, then it is region 2. If between 1.5b and 2.5b, then in region 3. And so on. Write out a few cases on a piece of paper and it'll make sense what is happening.
function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
var first = classes[b - 1]; // What is the current class of the item that will be moved
var $newEl = $('.list_container .' + first); // ID of element that will be moved
if (a < b) { // oldPos less than newPos
var second = classes[b - 2]; // The new class of the moved element will be one less
var newTop = parseInt($newEl.css('top')) - c; // Top of element will move up
} else { // oldPos more than newPos
var second = classes[b]; // The new class of the moved element will be one more
var newTop = parseInt($newEl.css('top')) + c; // Top of element will move down
}
// The following line of code is required, otherwise the following animation
// will animate of from top=0px to the new position (opposed to from top=currentPosition)
// Try taking it out and seeing
$newEl.css('top', parseInt($newEl.css('top')));
$newEl.removeClass(first); // Remove the current numbered class of element to move
// Move element and remove the added style tags (or future animations will get buggy)
$newEl.animate({top: newTop}, 300, function () {
$newEl.removeAttr('style');
});
$newEl.addClass(second); // Add the new numbered class
return false; // Cleans up animations
}
The function above is what does the actual animation part and moves the list items around to accommodate the selected list item.
function setNewClass(e, a) { // e == selected element, a == newPos
// Remove 'selected' class, then add back the 'list_item' class and the new numbered class
e.attr('class', 'list_item').addClass(classes[a-1]);
}
** All JavaScript together: **
var classes = new Array("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeem", "eighteen", "nineteen", "twenty", "twentyone", "twentytwo", "twentythree", "twentyfour");
$(document).ready(function () {
var margin = 2;
var $el;
var oldPos = 0;
var newPos = 0;
var dragging = false;
var selectionHeight = $('.list_container .list_item').height() + parseInt($('.list_container .list_item').css("border-bottom-width")) + parseInt($('.list_container .list_item').css("border-top-width")) + margin;
var classInfo = '';
$('.list_container .list_item').each(function (index) {
$(this).addClass(classes[index]);
classInfo += '.' + classes[index] + ' {top: ' + index * selectionHeight + 'px;}\n';
});
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = classInfo;
document.getElementsByTagName('head')[0].appendChild(style);
$('.list_item').mousedown(function (ev) {
$el = $(this);
oldPos = $el.index() + 1;
newPos = oldPos;
dragging = true;
startY = ev.clientY;
startT = parseInt($el.css('top'));
$el.addClass('selected');
});
$(window).mousemove(function (ev) {
if (dragging) {
$el.attr('class', 'list_item')
$el.addClass('selected');
// ----- calculate new top
var newTop = startT + (ev.clientY - startY);
$el.css('cursor', 'pointer');
// ------
//------ stay in parent
var maxTop = $el.parent().height() - $el.height();
newTop = newTop < 0 ? 0 : newTop > maxTop ? maxTop : newTop;
$el.css('top', newTop);
//------
newPos = getPos(newTop, selectionHeight);
if (oldPos != newPos) {
moveThings(oldPos, newPos, selectionHeight);
oldPos = newPos;
}
}
}).mouseup(function () {
dragging = false;
$el.removeClass('selected');
setNewClass($el, newPos);
$el.css('top', (newPos - 1) * selectionHeight);
});
});
function getPos(a, b) { // a == topPos, b == selectionHeight
return Math.round((a / b) + 1);
}
function moveThings(a, b, c) { // a == oldPos, b == newPos, c == selectedHeight
var first = classes[b - 1];
var $newEl = $('.list_container .' + first);
if (a < b) { // oldPos less than newPos
var second = classes[b - 2];
var newTop = parseInt($newEl.css('top')) - c;
} else { // oldPos more than newPos
var second = classes[b];
var newTop = parseInt($newEl.css('top')) + c;
}
$newEl.css('top', parseInt($newEl.css('top')));
$newEl.removeClass(first);
$newEl.animate({
top: newTop
}, 300, function () {
$newEl.removeAttr('style');
});
$newEl.addClass(second);
return false; // Cleans up animations
}
function setNewClass(e, a) { // e == selected element, a == newPos
e.attr('class', 'list_item').addClass(classes[a - 1]);
}