ng-sortable
Version:
Angular Library for Drag and Drop, supports Sortable and Draggable.
318 lines (284 loc) • 12.3 kB
JavaScript
/*jshint indent: 2 */
/*global angular: false */
(function () {
'use strict';
var mainModule = angular.module('as.sortable');
/**
* Helper factory for sortable.
*/
mainModule.factory('$helper', ['$document', '$window',
function ($document, $window) {
return {
/**
* Get the height of an element.
*
* @param {Object} element Angular element.
* @returns {String} Height
*/
height: function (element) {
return element[0].getBoundingClientRect().height;
},
/**
* Get the width of an element.
*
* @param {Object} element Angular element.
* @returns {String} Width
*/
width: function (element) {
return element[0].getBoundingClientRect().width;
},
/**
* Get the offset values of an element.
*
* @param {Object} element Angular element.
* @param {Object} [scrollableContainer] Scrollable container object for calculating relative top & left (optional, defaults to Document)
* @returns {Object} Object with properties width, height, top and left
*/
offset: function (element, scrollableContainer) {
var boundingClientRect = element[0].getBoundingClientRect();
if (!scrollableContainer) {
scrollableContainer = $document[0].documentElement;
}
return {
width: boundingClientRect.width || element.prop('offsetWidth'),
height: boundingClientRect.height || element.prop('offsetHeight'),
top: boundingClientRect.top + ($window.pageYOffset || scrollableContainer.scrollTop - scrollableContainer.offsetTop),
left: boundingClientRect.left + ($window.pageXOffset || scrollableContainer.scrollLeft - scrollableContainer.offsetLeft)
};
},
/**
* get the event object for touch.
*
* @param {Object} event the touch event
* @return {Object} the touch event object.
*/
eventObj: function (event) {
var obj = event;
if (event.targetTouches !== undefined) {
obj = event.targetTouches.item(0);
} else if (event.originalEvent !== undefined && event.originalEvent.targetTouches !== undefined) {
obj = event.originalEvent.targetTouches.item(0);
}
return obj;
},
/**
* Checks whether the touch is valid and multiple.
*
* @param event the event object.
* @returns {boolean} true if touch is multiple.
*/
isTouchInvalid: function (event) {
var touchInvalid = false;
if (event.touches !== undefined && event.touches.length > 1) {
touchInvalid = true;
} else if (event.originalEvent !== undefined &&
event.originalEvent.touches !== undefined && event.originalEvent.touches.length > 1) {
touchInvalid = true;
}
return touchInvalid;
},
/**
* Get the start position of the target element according to the provided event properties.
*
* @param {Object} event Event
* @param {Object} target Target element
* @param {Object} [scrollableContainer] (optional) Scrollable container object
* @returns {Object} Object with properties offsetX, offsetY.
*/
positionStarted: function (event, target, scrollableContainer) {
var pos = {};
pos.offsetX = event.pageX - this.offset(target, scrollableContainer).left;
pos.offsetY = event.pageY - this.offset(target, scrollableContainer).top;
pos.startX = pos.lastX = event.pageX;
pos.startY = pos.lastY = event.pageY;
pos.nowX = pos.nowY = pos.distX = pos.distY = pos.dirAx = 0;
pos.dirX = pos.dirY = pos.lastDirX = pos.lastDirY = pos.distAxX = pos.distAxY = 0;
return pos;
},
/**
* Calculates the event position and sets the direction
* properties.
*
* @param pos the current position of the element.
* @param event the move event.
*/
calculatePosition: function (pos, event) {
// mouse position last events
pos.lastX = pos.nowX;
pos.lastY = pos.nowY;
// mouse position this events
pos.nowX = event.pageX;
pos.nowY = event.pageY;
// distance mouse moved between events
pos.distX = pos.nowX - pos.lastX;
pos.distY = pos.nowY - pos.lastY;
// direction mouse was moving
pos.lastDirX = pos.dirX;
pos.lastDirY = pos.dirY;
// direction mouse is now moving (on both axis)
pos.dirX = pos.distX === 0 ? 0 : pos.distX > 0 ? 1 : -1;
pos.dirY = pos.distY === 0 ? 0 : pos.distY > 0 ? 1 : -1;
// axis mouse is now moving on
var newAx = Math.abs(pos.distX) > Math.abs(pos.distY) ? 1 : 0;
// calc distance moved on this axis (and direction)
if (pos.dirAx !== newAx) {
pos.distAxX = 0;
pos.distAxY = 0;
} else {
pos.distAxX += Math.abs(pos.distX);
if (pos.dirX !== 0 && pos.dirX !== pos.lastDirX) {
pos.distAxX = 0;
}
pos.distAxY += Math.abs(pos.distY);
if (pos.dirY !== 0 && pos.dirY !== pos.lastDirY) {
pos.distAxY = 0;
}
}
pos.dirAx = newAx;
},
/**
* Move the position by applying style.
*
* @param event the event object
* @param element - the dom element
* @param pos - current position
* @param container - the bounding container.
* @param containerPositioning - absolute or relative positioning.
* @param {Object} [scrollableContainer] (optional) Scrollable container object
*/
movePosition: function (event, element, pos, container, containerPositioning, scrollableContainer) {
var bounds;
var useRelative = (containerPositioning === 'relative');
element.x = event.pageX - pos.offsetX;
element.y = event.pageY - pos.offsetY;
if (container) {
bounds = this.offset(container, scrollableContainer);
if (useRelative) {
// reduce positioning by bounds
element.x -= bounds.left;
element.y -= bounds.top;
// reset bounds
bounds.left = 0;
bounds.top = 0;
}
if (element.x < bounds.left) {
element.x = bounds.left;
} else if (element.x >= bounds.width + bounds.left - this.offset(element).width) {
element.x = bounds.width + bounds.left - this.offset(element).width;
}
if (element.y < bounds.top) {
element.y = bounds.top;
} else if (element.y >= bounds.height + bounds.top - this.offset(element).height) {
element.y = bounds.height + bounds.top - this.offset(element).height;
}
}
element.css({
'left': element.x + 'px',
'top': element.y + 'px'
});
this.calculatePosition(pos, event);
},
/**
* The drag item info and functions.
* retains the item info before and after move.
* holds source item and target scope.
*
* @param item - the drag item
* @returns {{index: *, parent: *, source: *,
* sourceInfo: {index: *, itemScope: (*|.dragItem.sourceInfo.itemScope|$scope.itemScope|itemScope), sortableScope: *},
* moveTo: moveTo, isSameParent: isSameParent, isOrderChanged: isOrderChanged, eventArgs: eventArgs, apply: apply}}
*/
dragItem: function (item) {
return {
index: item.index(),
parent: item.sortableScope,
source: item,
targetElement: null,
targetElementOffset: null,
sourceInfo: {
index: item.index(),
itemScope: item.itemScope,
sortableScope: item.sortableScope
},
canMove: function(itemPosition, targetElement, targetElementOffset) {
// return true if targetElement has been changed since last call
if (this.targetElement !== targetElement) {
this.targetElement = targetElement;
this.targetElementOffset = targetElementOffset;
return true;
}
// return true if mouse is moving in the last moving direction of targetElement
if (itemPosition.dirX * (targetElementOffset.left - this.targetElementOffset.left) > 0 ||
itemPosition.dirY * (targetElementOffset.top - this.targetElementOffset.top) > 0) {
this.targetElementOffset = targetElementOffset;
return true;
}
// return false otherwise
return false;
},
moveTo: function (parent, index) {
// move the item to a new position
this.parent = parent;
// if the source item is in the same parent, the target index is after the source index and we're not cloning
if (this.isSameParent() && this.source.index() < index && !this.sourceInfo.sortableScope.cloning) {
index = index - 1;
}
this.index = index;
},
isSameParent: function () {
return this.parent.element === this.sourceInfo.sortableScope.element;
},
isOrderChanged: function () {
return this.index !== this.sourceInfo.index;
},
eventArgs: function () {
return {
source: this.sourceInfo,
dest: {
index: this.index,
sortableScope: this.parent
}
};
},
apply: function () {
if (!this.sourceInfo.sortableScope.cloning) {
// if not cloning, remove the item from the source model.
this.sourceInfo.sortableScope.removeItem(this.sourceInfo.index);
// if the dragged item is not already there, insert the item. This avoids ng-repeat dupes error
if (this.parent.options.allowDuplicates || this.parent.modelValue.indexOf(this.source.modelValue) < 0) {
this.parent.insertItem(this.index, this.source.modelValue);
}
} else if (!this.parent.options.clone) { // prevent drop inside sortables that specify options.clone = true
// clone the model value as well
this.parent.insertItem(this.index, angular.copy(this.source.modelValue));
}
}
};
},
/**
* Check the drag is not allowed for the element.
*
* @param element - the element to check
* @returns {boolean} - true if drag is not allowed.
*/
noDrag: function (element) {
return element.attr('no-drag') !== undefined || element.attr('data-no-drag') !== undefined;
},
/**
* Helper function to find the first ancestor with a given selector
* @param el - angular element to start looking at
* @param selector - selector to find the parent
* @returns {Object} - Angular element of the ancestor or body if not found
* @private
*/
findAncestor: function (el, selector) {
el = el[0];
var matches = Element.matches || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector;
while ((el = el.parentElement) && !matches.call(el, selector)) {
}
return el ? angular.element(el) : angular.element(document.body);
}
};
}
]);
}());