angular-sortable-view 是一款很好用的angularJs可拖拽列表排序插件,使用也非常简单,其内部封装了几个指令,直接注入模块,调用指令即可实现功能。
使用说明:
sv-root 根据名字也能大概猜出功能,主要是定义了拖拽、排序等方法,有点像工具类函数,必须添加在要拖拽的父类上,否则拖动无法实现排序。
sv-part 定义要排序的数据结构和实现元素排序,内容设置为循环的数组即可。
sv-handle 拖动事件句柄。
sv-element 拖动元素,设置在 sv-root 内的那个元素,那个元素就可拖动。
调用实例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
<script src="./js/libs/angular.min.js"></script>
<script src="./js/libs/angular-sortable-view.js"></script>
</head>
<style>
ul{
width: 40%;
margin: 100px auto 0;
}
li {
display: flex;
border: 1px solid #bfbfbf;
border-radius: 5px;
height: 40px;
line-height: 40px;
margin-bottom: 20px;
}
li>span{
flex: 1 1 33.33%;
text-align: center;
}
</style>
<body ng-app="myApp" ng-controller="myController">
<ul sv-root sv-part="feature">
<li sv-handle ng-repeat="item in feature track by $index" sv-element>
<span ng-bind="item.name"></span>
<span ng-bind="item.age"></span>
<span ng-bind="item.weight"></span>
</li>
</ul>
<ul>
<li ng-repeat="item in feature track by $index">
<span ng-bind="item.name"></span>
<span ng-bind="item.age"></span>
<span ng-bind="item.weight"></span>
</li>
</ul>
</body>
<script>
angular.module("myApp",['angular-sortable-view']).controller("myController",["$scope", function ($scope) {
$scope.feature = [
{name: "张三", age: 23, weight: "50KG"},
{name: "李四", age: 36, weight: "65KG"},
{name: "赵武", age: 8, weight: "33KG"},
{name: "孙柳", age: 44, weight: "77KG"}
]
}])
</script>
</html>
(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]; } 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); 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 center = { x: ~~(rect.left + rect.width/2), y: ~~(rect.top + rect.height/2) }; if(!se.container && // not the container element (se.element[0].scrollHeight || se.element[0].scrollWidth)){ // element is visible candidates.push({ element: se.element, q: (center.x - mouse.x)*(center.x - mouse.x) + (center.y - mouse.y)*(center.y - mouse.y), view: se.getPart(), targetIndex: se.getIndex(), after: shouldBeAfter(center, mouse, isGrid) }); } if(se.container && !se.element[0].querySelector('[sv-element]:not(.sv-placeholder):not(.sv-source)')){ // empty container candidates.push({ element: se.element, q: (center.x - mouse.x)*(center.x - mouse.x) + (center.y - mouse.y)*(center.y - mouse.y), view: se.getPart(), targetIndex: 0, container: true }); } }); var pRect = $placeholder[0].getBoundingClientRect(); var pCenter = { x: ~~(pRect.left + pRect.width/2), y: ~~(pRect.top + pRect.height/2) }; candidates.push({ q: (pCenter.x - mouse.x)*(pCenter.x - mouse.x) + (pCenter.y - mouse.y)*(pCenter.y - mouse.y), 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){ 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, scope: $scope }; $scope.$sortableRoot = $sortable; var sortablePart = { element: $element, getPart: $scope.$ctrl.getPart, container: true }; $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; 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', mouseup); html.removeClass('sv-sorting-in-progress'); if(moveExecuted) $controllers[0].$drop($scope.$index, opts); else $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([ '<style>' + '.sv-helper{' + 'position: fixed !important;' + 'z-index: 99999;' + 'margin: 0 !important;' + '}' + '.sv-candidate{' + '}' + '.sv-placeholder{' + // 'opacity: 0;' + '}' + '.sv-sorting-in-progress{' + '-webkit-user-select: none;' + '-moz-user-select: none;' + '-ms-user-select: none;' + 'user-select: none;' + '}' + '.sv-visibility-hidden{' + 'visibility: hidden !important;' + 'opacity: 0 !important;' + '}' + '</style>' ].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<this.length;i++){ res.push(this[i]); } for(i=0;i<elem.length;i++){ res.push(elem[i]); } return res; };
}
})(window, window.angular);
https://www.leftso.com/article/2411120910166410.html