AngularJs可拖拽排序列表

本文介绍了一款名为angular-sortable-view的AngularJS插件,该插件提供了一种简便的方法来实现列表项的拖拽排序功能。通过几个简单的指令如sv-root、sv-part、sv-handle和sv-element,开发者可以轻松地将拖拽排序功能集成到自己的项目中。

       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>


angular-sortable-view.js




(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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值