//
// Copyright Kamil Pękala http://github.com/kamilkp
// angular-sortable-view v0.0.17 2017/12/26
//
;(function(window, angular){
'use strict';
/* jshint eqnull:true */
/* jshint -W041 */
/* jshint -W030 */
var module = angular.module('angular-sortable-view', []);
module.directive('svRoot', [function(){
function shouldBeAfter(elem, pointer, isGrid){
return isGrid ? elem.x - pointer.x < 0 : elem.y - pointer.y < 0;
}
function getSortableElements(key){
return ROOTS_MAP[key];
}
function removeSortableElements(key){
delete ROOTS_MAP[key];
}
function getCoords(rect) {
return [
{ x: rect.left, y: rect.top },
{ x: rect.left + rect.width, y: rect.top },
{ x: rect.left, y: rect.top + rect.height },
{ x: rect.left + rect.width, y: rect.top + rect.height },
{ x: rect.left + rect.width/2, y: rect.top + rect.height/2 },
];
}
function getDistance(point, coords) {
if (
point.x >= coords[0].x && point.x <= coords[1].x &&
point.y >= coords[0].y && point.y <= coords[2].y
) {
return 0;
}
return Math.min.apply(Math, coords.map(function(coord){
return (coord.x - point.x)*(coord.x - point.x) + (coord.y - point.y)*(coord.y - point.y);
}));
}
var sortingInProgress;
var ROOTS_MAP = Object.create(null);
// window.ROOTS_MAP = ROOTS_MAP; // for debug purposes
return {
restrict: 'A',
controller: ['$scope', '$attrs', '$interpolate', '$parse', function($scope, $attrs, $interpolate, $parse){
var mapKey = $interpolate($attrs.svRoot)($scope) || $scope.$id;
if(!ROOTS_MAP[mapKey]) ROOTS_MAP[mapKey] = [];
var that = this;
var candidates; // set of possible destinations
var $placeholder;// placeholder element
var options; // sortable options
var $helper; // helper element - the one thats being dragged around with the mouse pointer
var $original; // original element
var $target; // last best candidate
var isGrid = false;
var onSort = $parse($attrs.svOnSort);
// ----- hack due to https://github.com/angular/angular.js/issues/8044
$attrs.svOnStart = $attrs.$$element[0].attributes['sv-on-start'];
$attrs.svOnStart = $attrs.svOnStart && $attrs.svOnStart.value;
$attrs.svOnStop = $attrs.$$element[0].attributes['sv-on-stop'];
$attrs.svOnStop = $attrs.svOnStop && $attrs.svOnStop.value;
// -------------------------------------------------------------------
var onStart = $parse($attrs.svOnStart);
var onStop = $parse($attrs.svOnStop);
this.sortingInProgress = function(){
return sortingInProgress;
};
if($attrs.svGrid){ // sv-grid determined explicite
isGrid = $attrs.svGrid === "true" ? true : $attrs.svGrid === "false" ? false : null;
if(isGrid === null)
throw 'Invalid value of sv-grid attribute';
}
else{
// check if at least one of the lists have a grid like layout
$scope.$watchCollection(function(){
return getSortableElements(mapKey);
}, function(collection){
isGrid = false;
var array = collection.filter(function(item){
return !item.container;
}).map(function(item){
return {
part: item.getPart().id,
y: item.element[0].getBoundingClientRect().top
};
});
var dict = Object.create(null);
array.forEach(function(item){
if(dict[item.part])
dict[item.part].push(item.y);
else
dict[item.part] = [item.y];
});
Object.keys(dict).forEach(function(key){
dict[key].sort();
dict[key].forEach(function(item, index){
if(index < dict[key].length - 1){
if(item > 0 && item === dict[key][index + 1]){
isGrid = true;
}
}
});
});
});
}
this.$moveUpdate = function(opts, mouse, svElement, svOriginal, svPlaceholder, originatingPart, originatingIndex){
var svRect = svElement[0].getBoundingClientRect();
if(opts.tolerance === 'element')
mouse = {
x: ~~(svRect.left + svRect.width/2),
y: ~~(svRect.top + svRect.height/2)
};
sortingInProgress = true;
candidates = [];
if(!$placeholder){
if(svPlaceholder){ // custom placeholder
$placeholder = svPlaceholder.clone();
$placeholder.removeClass('ng-hide');
}
else{ // default placeholder
$placeholder = svOriginal.clone();
$placeholder.addClass('sv-visibility-hidden');
$placeholder.addClass('sv-placeholder');
$placeholder.css({
'height': svRect.height + 'px',
'width': svRect.width + 'px'
});
}
svOriginal.after($placeholder);
if (!originatingPart.copyMode) {
svOriginal.addClass('ng-hide');
}
// cache options, helper and original element reference
$original = svOriginal;
options = opts;
$helper = svElement;
onStart($scope, {
$helper: {element: $helper},
$part: originatingPart.model(originatingPart.scope),
$index: originatingIndex,
$item: originatingPart.model(originatingPart.scope)[originatingIndex]
});
$scope.$root && $scope.$root.$$phase || $scope.$apply();
}
// ----- move the element
$helper[0].reposition({
x: mouse.x + document.body.scrollLeft - mouse.offset.x*svRect.width,
y: mouse.y + document.body.scrollTop - mouse.offset.y*svRect.height
});
// ------ manage candidates
getSortableElements(mapKey).forEach(function(se, index){
if(opts.containment != null){
// TODO: optimize this since it could be calculated only once when the moving begins
if(
!elementMatchesSelector(se.element, opts.containment) &&
!elementMatchesSelector(se.element, opts.containment + ' *')
) return; // element is not within allowed containment
}
var rect = se.element[0].getBoundingClientRect();
var seCoords = getCoords(rect);
var center = {
x: ~~(rect.left + rect.width/2),
y: ~~(rect.top + rect.height/2)
};
var centerHoriz = {
x: ~~(rect.left + rect.width/2),
y: ~~(rect.top)
};
var centerVert = {
x: ~~(rect.left),
y: ~~(rect.top + rect.height/2)
};
if(
!se.container && // not the container element
(se.element[0].scrollHeight || se.element[0].scrollWidth)
){ // element is visible
var sePart = se.getPart();
candidates.push({
element: se.element,
q: getDistance(mouse, seCoords),
view: sePart,
targetIndex: se.getIndex(),
after: shouldBeAfter(center, mouse, ('isGrid' in sePart) ? sePart.isGrid : isGrid)
});
}
if(
se.container &&
!se.element[0].querySelector('[sv-element]:not(.sv-placeholder):not(.sv-source)') // empty container
){
var c = center;
if (se.centerVariant === 'vertical') {
c = centerVert;
} else if (se.centerVariant === 'horizontal') {
c = centerHoriz;
}
candidates.push({
element: se.element,
q: (c.x - mouse.x)*(c.x - mouse.x) + (c.y - mouse.y)*(c.y - mouse.y),
view: se.getPart(),
targetIndex: 0,
container: true
});
}
});
var pRect = $placeholder[0].getBoundingClientRect();
candidates.push({
q: getDistance(mouse, getCoords(pRect)),
element: $placeholder,
placeholder: true
});
candidates.sort(function(a, b){
return a.q - b.q;
});
candidates.forEach(function(cand, index){
if(index === 0 && !cand.placeholder && !cand.container){
$target = cand;
cand.element.addClass('sv-candidate');
if(cand.after)
cand.element.after($placeholder);
else
insertElementBefore(cand.element, $placeholder);
}
else if(index === 0 && cand.container){
$target = cand;
cand.element.append($placeholder);
}
else
cand.element.removeClass('sv-candidate');
});
};
this.$drop = function(originatingPart, index, options){
if(!$placeholder) return;
if(options.revert && !($target && $target.view && $target.view.noRevert)){
var placeholderRect = $placeholder[0].getBoundingClientRect();
var helperRect = $helper[0].getBoundingClientRect();
var distance = Math.sqrt(
Math.pow(helperRect.top - placeholderRect.top, 2) +
Math.pow(helperRect.left - placeholderRect.left, 2)
);
var duration = +options.revert*distance/200; // constant speed: duration depends on distance
duration = Math.min(duration, +options.revert); // however it's not longer that options.revert
['-webkit-', '-moz-', '-ms-', '-o-', ''].forEach(function(prefix){
if(typeof $helper[0].style[prefix + 'transition'] !== "undefined")
$helper[0].style[prefix + 'transition'] = 'all ' + duration + 'ms ease';
});
setTimeout(afterRevert, duration);
$helper.css({
'top': placeholderRect.top + document.body.scrollTop + 'px',
'left': placeholderRect.left + document.body.scrollLeft + 'px'
});
}
else
afterRevert();
function afterRevert(){
sortingInProgress = false;
$placeholder.remove();
$helper.remove();
$original.removeClass('ng-hide');
candidates = void 0;
$placeholder = void 0;
options = void 0;
$helper = void 0;
$original = void 0;
// sv-on-stop callback
onStop($scope, {
$part: originatingPart.model(originatingPart.scope),
$index: index,
$item: originatingPart.model(originatingPart.scope)[index]
});
if($target){
$target.element.removeClass('sv-candidate');
var spliced = originatingPart.model(originatingPart.scope).splice(index, 1);
var targetIndex = $target.targetIndex;
if($target.view === originatingPart && $target.targetIndex > index)
targetIndex--;
if($target.after)
targetIndex++;
$target.view.model($target.view.scope).splice(targetIndex, 0, spliced[0]);
// sv-on-sort callback
if($target.view !== originatingPart || index !== targetIndex)
onSort($scope, {
$partTo: $target.view.model($target.view.scope),
$partFrom: originatingPart.model(originatingPart.scope),
$item: spliced[0],
$indexTo: targetIndex,
$indexFrom: index
});
}
$target = void 0;
$scope.$root && $scope.$root.$$phase || $scope.$apply();
}
};
this.addToSortableElements = function(se){
getSortableElements(mapKey).push(se);
};
this.removeFromSortableElements = function(se){
var elems = getSortableElements(mapKey);
var index = elems.indexOf(se);
if(index > -1){
elems.splice(index, 1);
if(elems.length === 0)
removeSortableElements(mapKey);
}
};
}]
};
}]);
module.directive('svPart', ['$parse', function($parse){
return {
restrict: 'A',
require: '^svRoot',
controller: ['$scope', function($scope){
$scope.$ctrl = this;
this.getPart = function(){
return $scope.part;
};
this.$drop = function(index, options){
$scope.$sortableRoot.$drop($scope.part, index, options);
};
}],
scope: true,
link: function($scope, $element, $attrs, $sortable){
if(!$attrs.svPart) throw new Error('no model provided');
var model = $parse($attrs.svPart);
if(!model.assign) throw new Error('model not assignable');
$scope.part = {
id: $scope.$id,
element: $element,
model: model,
copyMode: $attrs.svCopy === 'true',
noRevert: $attrs.svNoRevert === 'true',
scope: $scope
};
if ('isGrid' in $attrs) {
$scope.part.isGrid = $attrs.isGrid === 'true';
}
$scope.$sortableRoot = $sortable;
var sortablePart = {
element: $element,
getPart: $scope.$ctrl.getPart,
container: true,
centerVariant: $attrs.svCenter || 'both',
};
$sortable.addToSortableElements(sortablePart);
$scope.$on('$destroy', function(){
$sortable.removeFromSortableElements(sortablePart);
});
}
};
}]);
module.directive('svElement', ['$parse', function($parse){
return {
restrict: 'A',
require: ['^svPart', '^svRoot'],
controller: ['$scope', function($scope){
$scope.$ctrl = this;
}],
link: function($scope, $element, $attrs, $controllers){
var sortableElement = {
element: $element,
getPart: $controllers[0].getPart,
getIndex: function(){
return $scope.$index;
}
};
$controllers[1].addToSortableElements(sortableElement);
$scope.$on('$destroy', function(){
$controllers[1].removeFromSortableElements(sortableElement);
});
var handle = $element;
handle.on('mousedown touchstart', onMousedown);
$scope.$watch('$ctrl.handle', function(customHandle){
if(customHandle){
handle.off('mousedown touchstart', onMousedown);
handle = customHandle;
handle.on('mousedown touchstart', onMousedown);
}
});
var helper;
$scope.$watch('$ctrl.helper', function(customHelper){
if(customHelper){
helper = customHelper;
}
});
var placeholder;
$scope.$watch('$ctrl.placeholder', function(customPlaceholder){
if(customPlaceholder){
placeholder = customPlaceholder;
}
});
var body = angular.element(document.body);
var html = angular.element(document.documentElement);
var moveExecuted;
function onMousedown(e){
touchFix(e);
if($controllers[1].sortingInProgress()) return;
if(e.button != 0 && e.type === 'mousedown') return;
var svHandleDisabledAttr = e.target.attributes['sv-handle-disabled'];
if(svHandleDisabledAttr && svHandleDisabledAttr.value === 'true') return;
moveExecuted = false;
var opts = $parse($attrs.svElement)($scope);
opts = angular.extend({}, {
tolerance: 'pointer',
revert: 200,
containment: 'html'
}, opts);
if(opts.containment){
var containmentRect = closestElement.call($element, opts.containment)[0].getBoundingClientRect();
}
var target = $element;
var clientRect = $element[0].getBoundingClientRect();
var clone;
if(!helper) helper = $controllers[0].helper;
if(!placeholder) placeholder = $controllers[0].placeholder;
if(helper){
clone = helper.clone();
clone.removeClass('ng-hide');
clone.css({
'left': clientRect.left + document.body.scrollLeft + 'px',
'top': clientRect.top + document.body.scrollTop + 'px'
});
target.addClass('sv-visibility-hidden');
}
else{
clone = target.clone();
clone.addClass('sv-helper').css({
'left': clientRect.left + document.body.scrollLeft + 'px',
'top': clientRect.top + document.body.scrollTop + 'px',
'width': clientRect.width + 'px'
});
}
clone[0].reposition = function(coords){
var targetLeft = coords.x;
var targetTop = coords.y;
var helperRect = clone[0].getBoundingClientRect();
var body = document.body;
if(containmentRect){
if(targetTop < containmentRect.top + body.scrollTop) // top boundary
targetTop = containmentRect.top + body.scrollTop;
if(targetTop + helperRect.height > containmentRect.top + body.scrollTop + containmentRect.height) // bottom boundary
targetTop = containmentRect.top + body.scrollTop + containmentRect.height - helperRect.height;
if(targetLeft < containmentRect.left + body.scrollLeft) // left boundary
targetLeft = containmentRect.left + body.scrollLeft;
if(targetLeft + helperRect.width > containmentRect.left + body.scrollLeft + containmentRect.width) // right boundary
targetLeft = containmentRect.left + body.scrollLeft + containmentRect.width - helperRect.width;
}
this.style.left = targetLeft - body.scrollLeft + 'px';
this.style.top = targetTop - body.scrollTop + 'px';
};
var pointerOffset = {
x: (e.clientX - clientRect.left)/clientRect.width,
y: (e.clientY - clientRect.top)/clientRect.height
};
html.addClass('sv-sorting-in-progress');
html.on('mousemove touchmove', onMousemove).on('mouseup touchend touchcancel', function mouseup(e){
html.off('mousemove touchmove', onMousemove);
html.off('mouseup touchend touchcancel', mouseup);
html.removeClass('sv-sorting-in-progress');
if(moveExecuted){
$controllers[0].$drop($scope.$index, opts);
}
$element.removeClass('sv-visibility-hidden');
});
// onMousemove(e);
function onMousemove(e){
touchFix(e);
if(!moveExecuted){
$element.parent().prepend(clone);
moveExecuted = true;
}
$controllers[1].$moveUpdate(opts, {
x: e.clientX,
y: e.clientY,
offset: pointerOffset
}, clone, $element, placeholder, $controllers[0].getPart(), $scope.$index);
}
}
}
};
}]);
module.directive('svHandle', function(){
return {
require: '?^svElement',
link: function($scope, $element, $attrs, $ctrl){
if($ctrl)
$ctrl.handle = $element.add($ctrl.handle); // support multiple handles
}
};
});
module.directive('svHelper', function(){
return {
require: ['?^svPart', '?^svElement'],
link: function($scope, $element, $attrs, $ctrl){
$element.addClass('sv-helper').addClass('ng-hide');
if($ctrl[1])
$ctrl[1].helper = $element;
else if($ctrl[0])
$ctrl[0].helper = $element;
}
};
});
module.directive('svPlaceholder', function(){
return {
require: ['?^svPart', '?^svElement'],
link: function($scope, $element, $attrs, $ctrl){
$element.addClass('sv-placeholder').addClass('ng-hide');
if($ctrl[1])
$ctrl[1].placeholder = $element;
else if($ctrl[0])
$ctrl[0].placeholder = $element;
}
};
});
angular.element(document.head).append([
''
].join(''));
function touchFix(e){
if(!('clientX' in e) && !('clientY' in e)) {
var touches = e.touches || e.originalEvent.touches;
if(touches && touches.length) {
e.clientX = touches[0].clientX;
e.clientY = touches[0].clientY;
}
e.preventDefault();
}
}
function getPreviousSibling(element){
element = element[0];
if(element.previousElementSibling)
return angular.element(element.previousElementSibling);
else{
var sib = element.previousSibling;
while(sib != null && sib.nodeType != 1)
sib = sib.previousSibling;
return angular.element(sib);
}
}
function insertElementBefore(element, newElement){
var prevSibl = getPreviousSibling(element);
if(prevSibl.length > 0){
prevSibl.after(newElement);
}
else{
element.parent().prepend(newElement);
}
}
var dde = document.documentElement,
matchingFunction = dde.matches ? 'matches' :
dde.matchesSelector ? 'matchesSelector' :
dde.webkitMatches ? 'webkitMatches' :
dde.webkitMatchesSelector ? 'webkitMatchesSelector' :
dde.msMatches ? 'msMatches' :
dde.msMatchesSelector ? 'msMatchesSelector' :
dde.mozMatches ? 'mozMatches' :
dde.mozMatchesSelector ? 'mozMatchesSelector' : null;
if(matchingFunction == null)
throw 'This browser doesn\'t support the HTMLElement.matches method';
function elementMatchesSelector(element, selector){
if(element instanceof angular.element) element = element[0];
if(matchingFunction !== null)
return element[matchingFunction](selector);
}
var closestElement = angular.element.prototype.closest || function (selector){
var el = this[0].parentNode;
while(el !== document.documentElement && !el[matchingFunction](selector))
el = el.parentNode;
if(el[matchingFunction](selector))
return angular.element(el);
else
return angular.element();
};
/*
Simple implementation of jQuery's .add method
*/
if(typeof angular.element.prototype.add !== 'function'){
angular.element.prototype.add = function(elem){
var i, res = angular.element();
elem = angular.element(elem);
for(i=0;i