278 lines
8.2 KiB
JavaScript
278 lines
8.2 KiB
JavaScript
/**
|
|
* drag-shift
|
|
* Copyright (c) 2014 Vishal Kumar
|
|
* Licensed under the MIT License.
|
|
*/
|
|
'use strict';
|
|
(function (factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD. Register as an anonymous module.
|
|
define(['jquery'], factory);
|
|
} else {
|
|
// Browser globals
|
|
factory(jQuery);
|
|
}
|
|
}(function ($) {
|
|
var IS_TOUCH_DEVICE = ('ontouchstart' in document.documentElement);
|
|
/**
|
|
* mouse move threshold (in px) until drag action starts
|
|
* @type {Number}
|
|
*/
|
|
var DRAG_THRESHOLD = 5;
|
|
/**
|
|
* to generate event namespace
|
|
* @type {Number}
|
|
*/
|
|
var counter = 0;
|
|
|
|
/**
|
|
* Javascript events for touch device/desktop
|
|
* @return {Object}
|
|
*/
|
|
var dragEvents = (function () {
|
|
if (IS_TOUCH_DEVICE) {
|
|
return {
|
|
START: 'touchstart',
|
|
MOVE: 'touchmove',
|
|
END: 'touchend'
|
|
};
|
|
}
|
|
else {
|
|
return {
|
|
START: 'mousedown',
|
|
MOVE: 'mousemove',
|
|
END: 'mouseup'
|
|
};
|
|
}
|
|
}());
|
|
|
|
$.fn.arrangeable = function(options) {
|
|
var dragging = false;
|
|
var $clone;
|
|
var dragElement;
|
|
var originalClientX, originalClientY; // client(X|Y) position before drag starts
|
|
var $elements; // list of elements to shift between
|
|
var touchDown = false;
|
|
var leftOffset, topOffset;
|
|
var eventNamespace;
|
|
|
|
if (typeof options === "string") {
|
|
// check if want to destroy drag-arrange
|
|
if (options === 'destroy') {
|
|
if (this.eq(0).data('drag-arrange-destroy')) {
|
|
this.eq(0).data('drag-arrange-destroy')();
|
|
}
|
|
|
|
return this;
|
|
}
|
|
}
|
|
|
|
options = $.extend({
|
|
"dragEndEvent": "drag.end.arrangeable"
|
|
}, options);
|
|
|
|
var dragEndEvent = options["dragEndEvent"];
|
|
|
|
$elements = this;
|
|
eventNamespace = getEventNamespace();
|
|
|
|
|
|
this.each(function() {
|
|
// bindings to trigger drag on element
|
|
var dragSelector = options.dragSelector;
|
|
var self = this;
|
|
var $this = $(this);
|
|
|
|
if (dragSelector) {
|
|
$this.on(dragEvents.START + eventNamespace, dragSelector, dragStartHandler);
|
|
} else {
|
|
$this.on(dragEvents.START + eventNamespace, dragStartHandler);
|
|
}
|
|
|
|
function dragStartHandler(e) {
|
|
// a mouse down/touchstart event, but still drag doesn't start till threshold reaches
|
|
// stopPropagation is compulsory, otherwise touchmove fires only once (android < 4 issue)
|
|
e.stopPropagation();
|
|
touchDown = true;
|
|
originalClientX = e.clientX || e.originalEvent.touches[0].clientX;
|
|
originalClientY = e.clientY || e.originalEvent.touches[0].clientY;
|
|
dragElement = self;
|
|
}
|
|
});
|
|
|
|
// bind mouse-move/touchmove on document
|
|
// (as it is not compulsory that event will trigger on dragging element)
|
|
$(document).on(dragEvents.MOVE + eventNamespace, dragMoveHandler)
|
|
.on(dragEvents.END + eventNamespace, dragEndHandler);
|
|
|
|
function dragMoveHandler(e) {
|
|
if (!touchDown) { return; }
|
|
var $dragElement = $(dragElement);
|
|
var dragDistanceX = (e.clientX || e.originalEvent.touches[0].clientX) - originalClientX;
|
|
var dragDistanceY = (e.clientY || e.originalEvent.touches[0].clientY) - originalClientY;
|
|
|
|
if (dragging) {
|
|
e.stopPropagation();
|
|
|
|
$clone.css({
|
|
left: leftOffset + dragDistanceX,
|
|
top: topOffset + dragDistanceY
|
|
});
|
|
|
|
shiftHoveredElement($clone, $dragElement, $elements);
|
|
|
|
// check for drag threshold (drag has not started yet)
|
|
} else if (Math.abs(dragDistanceX) > DRAG_THRESHOLD ||
|
|
Math.abs(dragDistanceY) > DRAG_THRESHOLD) {
|
|
$clone = clone($dragElement);
|
|
|
|
// initialize left offset and top offset
|
|
// will be used in successive calls of this function
|
|
leftOffset = dragElement.offsetLeft - parseInt($dragElement.css('margin-left')) -
|
|
parseInt($dragElement.css('padding-left'));
|
|
topOffset = dragElement.offsetTop - parseInt($dragElement.css('margin-top')) -
|
|
parseInt($dragElement.css('padding-top'));
|
|
|
|
// put cloned element just above the dragged element
|
|
// and move it instead of original element
|
|
$clone.css({
|
|
left: leftOffset,
|
|
top: topOffset
|
|
});
|
|
$dragElement.parent().append($clone);
|
|
|
|
// hide original dragged element
|
|
$dragElement.css('visibility', 'hidden');
|
|
|
|
dragging = true;
|
|
}
|
|
}
|
|
|
|
function dragEndHandler(e) {
|
|
if (dragging) {
|
|
// remove the cloned dragged element and
|
|
// show original element back
|
|
e.stopPropagation();
|
|
dragging = false;
|
|
$clone.remove();
|
|
dragElement.style.visibility = 'visible';
|
|
$(dragElement).parent().trigger(dragEndEvent, [$(dragElement)]);
|
|
}
|
|
|
|
touchDown = false;
|
|
}
|
|
|
|
function destroy() {
|
|
$elements.each(function() {
|
|
// bindings to trigger drag on element
|
|
var dragSelector = options.dragSelector;
|
|
var $this = $(this);
|
|
|
|
if (dragSelector) {
|
|
$this.off(dragEvents.START + eventNamespace, dragSelector);
|
|
} else {
|
|
$this.off(dragEvents.START + eventNamespace);
|
|
}
|
|
});
|
|
|
|
$(document).off(dragEvents.MOVE + eventNamespace)
|
|
.off(dragEvents.END + eventNamespace);
|
|
|
|
// remove data
|
|
$elements.eq(0).data('drag-arrange-destroy', null);
|
|
|
|
// clear variables
|
|
$elements = null;
|
|
dragMoveHandler = null;
|
|
dragEndHandler = null;
|
|
}
|
|
|
|
this.eq(0).data('drag-arrange-destroy', destroy);
|
|
};
|
|
|
|
function clone($element) {
|
|
var $clone = $element.clone();
|
|
|
|
$clone.css({
|
|
position: 'absolute',
|
|
width: $element.width(),
|
|
height: $element.height(),
|
|
'z-index': 100000 // very high value to prevent it to hide below other element(s)
|
|
});
|
|
|
|
return $clone;
|
|
}
|
|
|
|
/**
|
|
* find the element on which the dragged element is hovering
|
|
* @return {DOM Object} hovered element
|
|
*/
|
|
function getHoveredElement($clone, $dragElement, $movableElements) {
|
|
var cloneOffset = $clone.offset();
|
|
var cloneWidth = $clone.width();
|
|
var cloneHeight = $clone.height();
|
|
var cloneLeftPosition = cloneOffset.left;
|
|
var cloneRightPosition = cloneOffset.left + cloneWidth;
|
|
var cloneTopPosition = cloneOffset.top;
|
|
var cloneBottomPosition = cloneOffset.top + cloneHeight;
|
|
var $currentElement;
|
|
var horizontalMidPosition, verticalMidPosition;
|
|
var offset, overlappingX, overlappingY, inRange;
|
|
|
|
for (var i = 0; i < $movableElements.length; i++) {
|
|
$currentElement = $movableElements.eq(i);
|
|
|
|
if ($currentElement[0] === $dragElement[0]) { continue; }
|
|
|
|
offset = $currentElement.offset();
|
|
|
|
// current element width and draggable element(clone) width or height can be different
|
|
horizontalMidPosition = offset.left + 0.5 * $currentElement.width();
|
|
verticalMidPosition = offset.top + 0.5 * $currentElement.height();
|
|
|
|
// check if this element position is overlapping with dragged element
|
|
overlappingX = (horizontalMidPosition < cloneRightPosition) &&
|
|
(horizontalMidPosition > cloneLeftPosition);
|
|
|
|
overlappingY = (verticalMidPosition < cloneBottomPosition) &&
|
|
(verticalMidPosition > cloneTopPosition);
|
|
|
|
inRange = overlappingX && overlappingY;
|
|
|
|
if (inRange) {
|
|
return $currentElement[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
function shiftHoveredElement($clone, $dragElement, $movableElements) {
|
|
var hoveredElement = getHoveredElement($clone, $dragElement, $movableElements);
|
|
|
|
if (hoveredElement !== $dragElement[0]) {
|
|
// shift all other elements to make space for the dragged element
|
|
var hoveredElementIndex = $movableElements.index(hoveredElement);
|
|
var dragElementIndex = $movableElements.index($dragElement);
|
|
if (hoveredElementIndex < dragElementIndex) {
|
|
$(hoveredElement).before($dragElement);
|
|
} else {
|
|
$(hoveredElement).after($dragElement);
|
|
}
|
|
|
|
// since elements order have changed, need to change order in jQuery Object too
|
|
shiftElementPosition($movableElements, dragElementIndex, hoveredElementIndex);
|
|
}
|
|
}
|
|
|
|
function shiftElementPosition(arr, fromIndex, toIndex) {
|
|
var temp = arr.splice(fromIndex, 1)[0];
|
|
return arr.splice(toIndex, 0, temp);
|
|
}
|
|
|
|
function getEventNamespace() {
|
|
counter += 1;
|
|
|
|
return '.drag-arrange-' + counter;
|
|
}
|
|
|
|
}));
|