Last active
December 14, 2015 05:40
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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 — — — — — — —</div> | |
<div class="row">Bravo — — — — — — —</div> | |
<div class="row">Charlie — — — — — — —</div> | |
<div class="row">Delta — — — — — — —</div> | |
<div class="row">Echo — — — — — — —</div> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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