Skip to content

Instantly share code, notes, and snippets.

@axefrog
Last active December 14, 2015 05:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save axefrog/5037357 to your computer and use it in GitHub Desktop.
Save axefrog/5037357 to your computer and use it in GitHub Desktop.
jQuery plugin that allows any sequence of block elements to become sortable by dragging. Inserts a drag handle element into each item if one is not provided.
<!doctype html>
<html>
<head>
<title>Sortable List</title>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="sortable-list.js"></script>
<script>
$(function() { $('#rows').sortablelist(); });
</script>
<style type="text/css">
.row {
font-family: "Segoe UI";
font-size: 13px;
padding: 5px 10px;
}
.row:nth-child(even) {
background-color: #eee;
}
.drag-handle {
color: #999;
margin-right: 10px;
cursor: move;
}
</style>
</head>
<body>
<div id="rows">
<div class="row">Alpha &mdash; &mdash; &mdash; &mdash; &mdash; &mdash; &mdash;</div>
<div class="row">Bravo &mdash; &mdash; &mdash; &mdash; &mdash; &mdash; &mdash;</div>
<div class="row">Charlie &mdash; &mdash; &mdash; &mdash; &mdash; &mdash; &mdash;</div>
<div class="row">Delta &mdash; &mdash; &mdash; &mdash; &mdash; &mdash; &mdash;</div>
<div class="row">Echo &mdash; &mdash; &mdash; &mdash; &mdash; &mdash; &mdash;</div>
</div>
</body>
</html>
/*
to do:
* DONE tidy up up code
* DONE drag handles shouldn't have to be a direct child of a list item
* DONE IE9, Webkit, Firefox
* heights/positions should be able to adapt to changing inner content (monitor internal structure with setInterval)
* should work with margins, padding and be independent of box-sizing (i.e. whatever styles are used, control should work regardless)
* IE8
* Move items between lists with the same type name
*/
(function ($) {
$.fn.sortablelist = function(options) {
var $body = $('body');
var $window = $(window);
var data = {
mousedown: false,
dragging: false
};
var dragStyle = getDragItemRule();
if(!dragStyle) {
createStyleSheet();
dragStyle = getDragItemRule();
}
function getDragItemRule() {
for(var i = 0; i < document.styleSheets.length; i++) {
var sheet = document.styleSheets[i];
var rules = sheet.rules || sheet.cssRules || [];
for(var j = 0; j < rules.length; j++)
if(rules[j].selectorText == '.sortablelist-item-dragging')
return rules[j].style;
}
}
function createStyleSheet() {
var rules = [
'.sortablelist {\n' +
' display: block !important;\n' +
'}',
'.drag-handle {' +
' -webkit-touch-callout: none;\n' +
' -webkit-user-select: none;\n' +
' -khtml-user-select: none;\n' +
' -moz-user-select: none;\n' +
' -ms-user-select: none;\n' +
' user-select: none;\n' +
'}',
'.sortablelist-whiledragging {\n' +
' -webkit-touch-callout: none;\n' +
' -webkit-user-select: none;\n' +
' -khtml-user-select: none;\n' +
' -moz-user-select: none;\n' +
' -ms-user-select: none;\n' +
' user-select: none;\n' +
' cursor: move !important;\n' +
'}',
'.sortablelist-droptarget {\n' +
' display: block;\n' +
' border: none !important;\n' +
' padding: 0 !important;\n' +
'}',
'.sortablelist-item-dragging {\n' +
' position: absolute;\n' +
' margin-top: 0 !important;\n' +
' margin-bottom: 0 !important;\n' +
' z-index: 50000 !important;\n' +
'}'
];
var sheet = $('<style type="text/css"/>').appendTo('head')[0].sheet;
for(var i = 0; i < rules.length; i++)
sheet.insertRule(rules[i], sheet.cssRules.length);
}
function deselectAll() {
var sel;
if(window.getSelection) {
sel = window.getSelection();
if(sel && sel.removeAllRanges)
sel.removeAllRanges() ;
}
else if(document.selection && document.selection.empty)
document.selection.empty();
}
function debug(data) {
var html = '';
for(var k in data)
html += '<div><b>' + k + '</b>: ' + data[k] + '</div>';
$('#dbg').html(html);
}
//$body.append('<div id="dbg" style="font-family:consolas;font-size:8pt;padding:5px;background-color:white;position:fixed;top:0;right:0;box-shadow:0 0 5px rgba(0,0,0,0.3);"/>');
//setInterval(function() { debug(data); }, 100);
this.data('sortablelist-applied', true);
this.addClass('sortablelist');
this.each(function() {
var $list = $(this);
function findDragHandle($parents) {
var $children = $parents.children().not('.sortablelist');
if(!$children.length)
return $children;
var $found = $children.filter('.drag-handle');
if($found.length)
return $found.first();
return findDragHandle($children);
}
function initListItem() {
var $this = $(this);
if($this.hasClass('sortablelist-item'))
return;
var $item = $this.addClass('sortablelist-item');
var $handle = findDragHandle($item);
if(!$handle.length)
$item.prepend($handle = $('<span class="drag-handle">Move</span>'));
$handle.mousedown(onDragHandleMouseDown);
}
function calculateItemMidpoint($item) {
var y = $item[0].offsetTop;
var h = $item.outerHeight();
return y + h / 2;
}
function isDraggingUpwards() {
var itemTop = data.$dragItem[0].offsetTop;
var targetTop = data.$dropTarget[0].offsetTop;
return data.$dragItem[0].offsetTop <= data.$dropTarget[0].offsetTop;
}
function getPageOffset(element) {
if(element instanceof jQuery) {
if(!element.length)
return 0;
element = element[0];
}
else if(element == null)
return 0;
return element.offsetTop + getPageOffset(element.offsetParent);
}
function repositionDropTargetIfPositionChanged() {
delete data.abc;
var midpoints = [];
function addMidpoint() {
midpoints.push(calculateItemMidpoint($(this)));
}
var up = isDraggingUpwards();
var $items, edgeY;
if(up) {
$items = data.$dropTarget.prevAll(':not(.sortablelist-droptarget)');
$items.each(addMidpoint);
edgeY = data.dragItemTopEdgeY;
for(var i = midpoints.length - 1; i >= 0; i--)
if(edgeY <= midpoints[i]) {
$items.eq(i).before(data.$dropTarget);
data.$dropTarget.after(data.$dragItem);
break;
}
}
else {
$items = data.$dropTarget.nextAll(':not(.sortablelist-item-dragging)');
$items.each(addMidpoint);
edgeY = data.dragItemBottomEdgeY;
for(var i = midpoints.length - 1; i >= 0; i--)
if(edgeY >= midpoints[i]) {
$items.eq(i).after(data.$dropTarget);
data.$dropTarget.after(data.$dragItem);
break;
}
}
}
function setItemDragging($item) {
if(!dragStyle)
dragStyle = getDragItemRule();
if($item) {
data.dragFromIndex = $item.index();
data.parentY = getPageOffset($item.offsetParent());
var t = 0;
function f(k) {
if(!k) return;
t += k.offsetTop;
f(k.offsetParent);
}
f($item[0]);
var $items = $list.children();
var $lastItem = $items.last();
var minDragPageY = getPageOffset($items.first());
var maxDragPageY = getPageOffset($lastItem) + $lastItem.outerHeight() - $item.outerHeight();
data.itemMinY = minDragPageY - data.parentY;
data.itemMaxY = maxDragPageY - data.parentY;
var w = $item.width();
var h = $item.height();
data.$dropTarget = createAndInsertDropTargetFromItem($item);
var x = data.$dropTarget.offsetLeft;
var y = data.$dropTarget.offsetTop;
data.$dragItem = $item;
data.dragging = true;
dragStyle.left = x + 'px';
dragStyle.height = h + 'px';
dragStyle.width = w + 'px';
$item.addClass('sortablelist-item-dragging');
}
else {
data.$dragItem.removeClass('sortablelist-item-dragging');
data.$dropTarget.remove();
var newIndex = data.$dragItem.index();
var $items = data.$dragItem.parent().children();
data.$dropTarget = null;
data.$dragItem = null;
data.dragging = false;
if(data.dragFromIndex != newIndex) {
var e = jQuery.Event('positionsChanged');
var oldIndex = data.dragFromIndex;
e.fromIndex = oldIndex;
e.toIndex = newIndex;
e.changedIndexes = [];
var high = Math.max(newIndex, oldIndex);
var low = Math.min(newIndex, oldIndex);
for(var i = low; i <= high; i++)
e.changedIndexes.push({
element: $items.eq(i),
oldIndex: i == newIndex ? oldIndex : newIndex < oldIndex ? i - 1 : i + 1,
newIndex: i
});
$list.trigger(e);
}
}
}
function updateDragState(mouseY) {
if(!data.dragging)
return;
data.mouseY = mouseY;
var offsetMouseY = mouseY - data.parentY;
var handleMidpoint = calculateItemMidpoint(data.$dragHandle);
var yPos = offsetMouseY - handleMidpoint;
var itemY = Math.min(Math.max(yPos, data.itemMinY), data.itemMaxY);
data.yPos = yPos;
data.itemY = itemY;
data.dragItemTopEdgeY = itemY;
data.dragItemBottomEdgeY = itemY + data.$dragItem.outerHeight() - 1;
dragStyle.top = itemY + 'px';
}
function createAndInsertDropTargetFromItem($item) {
var h = $item.outerHeight();
var w = $item.outerWidth();
var marginTop = $item.css('marginTop');
var marginBottom = $item.css('marginBottom');
var $target = $('<div class="sortablelist-droptarget"/>')
.css('width', w + 'px')
.css('height', h + 'px')
.css('marginTop', marginTop)
.css('marginBottom', marginBottom);
$item.before($target);
if($target.outerHeight() > h)
$target.css('height', $item.innerHeight() + 'px');
if($target.outerWidth() > w)
$target.css('width', $item.innerWidth() + 'px');
return $target;
}
function onDragHandleMouseDown(e) {
deselectAll();
$body.addClass('sortablelist-whiledragging');
data.dragStartY = e.pageY;
data.$dragHandle = $(this);
var $item = data.$dragHandle.closest('.sortablelist-item');
$window.bind('mousemove', onDragListItem)
.bind('mouseup', onEndDragListItem);
data.mousedown = true;
setItemDragging($item);
updateDragState(e.pageY);
}
function onDragListItem(e) {
updateDragState(e.pageY);
repositionDropTargetIfPositionChanged();
}
function onEndDragListItem() {
$body.removeClass('sortablelist-whiledragging');
$window.unbind('mousemove', onDragListItem)
.unbind('mouseup', onEndDragListItem);
data.mousedown = false;
setItemDragging(null);
}
$list.children().each(initListItem);
});
};
})(jQuery);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment