hytzgroup / blog

write & read & though
0 stars 0 forks source link

拖拽原理 #5

Open hytzgroup opened 5 years ago

hytzgroup commented 5 years ago

拖拽原理

综述

​ 拖拽是前端的一个基本技能,拖拽涉及到前端的许多基础知识,比如:元素的尺寸、偏移量计算、scroll、事件绑定等。并且拖拽是前端组件的一个基础功能。本文旨在学习拖拽的基本原理。

基本原理

  1. 给被拖拽元素添加mousedown事件,记录鼠标距离布局视口的坐标event.pageX、event.pageY。获取被拖拽元素的距离布局视口的offsetTop、offsetLeft。计算鼠标的点击点距离被拖拽元素的左上角的偏移量dx、dy。

    dx = event.pageX - offsetLeft; 
    dy = event.pageY - offsetTop;
  2. 在mousedown事件内部,给document对象添加mousemove事件。记录鼠标距离布局视口的坐标,计算元素的left、top值。

    left = event.pageX - dx;
    top  = event.pageY - dy;
  3. 在mousedown事件回调函数内,document对象添加mouseup事件。移除在document对象上注册的mousemove和mouseup事件

实例一

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>drag原理</title>
    <style type="text/css">
        .drag{
            width: 200px;
            height: 200px;
            position: absolute;
            left: 100px;
            top: 100px;
            background-color: rebeccapurple;
        }
    </style>
</head>
<body>
    <div id="drag" class="drag"></div>
    <script type="text/javascript">
        window.onload = function(){
            var $elm = document.getElementById('drag');
            var dx = 0, dy = 0;
            $elm.onmousedown = function(ev){
                dx = ev.pageX - $elm.offsetLeft;
                dy = ev.pageY - $elm.offsetTop;
                document.onmousemove = function(ev){
                    var left = ev.pageX - dx;
                    var top  = ev.pageY - dy;
                    $elm.style.left = left+ 'px';
                    $elm.style.top  = top + 'px';
                }
                document.onmouseup = function(){
                    document.onmousemove = null;
                    document.onmouseup = null;
                }
            }
        }
    </script>
</body>
</html>

实例二

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>drag原理</title>
    <style type="text/css">
        .drag{
            width: 200px;
            height: 200px;
            position: absolute;
            left: 100px;
            top: 100px;
            background-color: rebeccapurple;
        }
    </style>
</head>
<body>
    <div id="drag" class="drag"></div>
    <script type="text/javascript">
        window.onload = function(){
            var $elm = document.getElementById('drag');
            var startPageX = 0; 
            var startPageY = 0;
            var startLeft = $elm.offsetLeft;
            var startTop = $elm.offsetTop;
            var left = 0;
            var top = 0;
            $elm.onmousedown = function(ev){
                startLeft = $elm.offsetLeft;
                startTop = $elm.offsetTop;
                startPageX = ev.pageX;
                startPageY = ev.pageY;
                document.onmousemove = function(mev){
                    left = mev.pageX - startPageX + startLeft;
                    top  = mev.pageY - startPageY + startTop;
                    $elm.style.left = left+ 'px';
                    $elm.style.top  = top + 'px';
                }
                document.onmouseup = function(uev){
                    document.onmousemove = null;
                    document.onmouseup = null;
                }
            }
        }
    </script>
</body>
</html>

从实例中可以得出以下结论:

  1. event.pageX(Y)、$elm.offsetLeft(Top)都是相对于布局视口的偏移。
  2. 给document对象注册mousemove事件可以防止被拖拽元素被拖出浏览器窗口,松开鼠标被拖拽元素依然跟随鼠标移动。
  3. 移除鼠标的mousemove和mouseup事件,每次按下鼠标都需要重新注册,提高性能。
hytzgroup commented 4 years ago
/**
 * jquery easy-ui draggable
 * jquery offset方法参考的原点是文档布局的原点,与是否有滚动条无关
 * 1.目前元素在body中,绝对定位,目标元素的坐标是相对body的
 * 2.目标元素在父容器中,父容器已经定位,目标元素的坐标也是相对body的
 */
(function(){
    function drag(e){
        var data = $.data(e.data.target, 'draggable');
        var dragData = e.data;
        // var left = scrollLeft + offsetLeft + Dx(e.pageX - e.pageX缓存)
        var left = dragData.startLeft + e.pageX - dragData.startX;
        var top = dragData.startTop + e.pageY - dragData.startY;
        if(e.data.parent != document.body){
            left += $(e.data.parent).scrollLeft();
            top += $(e.data.parent).scrollTop();
        }

        dragData.left = left;
        dragData.top = top;
    }

    function applyDrag(e){
        var data = $.data(e.data.target, 'draggable');
        var opts = data.options;
        var proxy = data.proxy;
        if(!proxy){
            proxy = $(e.data.target);
        }
        proxy.css({left:e.data.left,top:e.data.top});
        $('body').css('cursor',opts.cursor);
    }

    function onMouseDown(e){
        if(!$.fn.draggable.isDragging){
            return false;
        }
        var data = $.data(e.data.target,'dragable');
        var proxy = data.proxy;
        if(!proxy){
            proxy = $(e.data.target);
        }
        proxy.css('position','absolute');
        drag(e);
        applyDrag(e);
        options.onStartDrag.call(e.data.target,e)
        return false;
    }

    function onMouseMove(e){
        if(!$.fn.draggable.isDragging){
            return false;
        }
        var data = $.data(e.data.target, 'draggable');
        drag(e);
        if(data.options.onDrag.call(e.data.target, e) != false){
            applyDrag(e);
        }
        return false;
    }

    function onMouseUp(e){
        if(!$.fn.draggable.isDragging){
            clearTimer();
            return false;
        }
        onMouseMove(e);
        var data = $.data(e.data.target, 'draggable');
        var opts = data.options;
        opts.onEndDrag.call(e.data.target, e);
        $(e.data.target).css({
            position: e.data.startPosition,
            left: e.data.left,
            top: e.data.top
        })
        clearTimer();
        return false;
    }

    function clearTimer(){
        if($.fn.draggable.timer){
            clearTimeout($.fn.draggable.timer);
            $.fn.draggable.timer = undefined;
        }
        $(document).unbind('.draggable');
        $.fn.draggable.isDragging = false;
        setTimeout(function(){ 
            $('body').css('cursor', '') 
        }, 100);
    }

    $.fn.draggable = function(options,param){
        if(typeof options == 'string'){
            return $.fn.draggable.methods[options](this,param)
        }

        return this.each(function(){
            var opts;
            var state = $.data(this, 'draggable');
            if(state){
                state.handle.unbind('.draggable');
                opts = $.extend(state.options, options);
            }else{
                opts = $.extend({},$.fn.draggable.defaults, options || {});
            }

            if(opts.disabled == true){
                $(this).css('cursor', 'default');
                return;
            }
            // 被拖拽的对象
            var handle = null;
            if(opts.handle == null){
                handle = $(this);
            }else{
                handle = typeof opts.handle == 'string' 
                         ? $(opts.handle, this)
                         : handle;
            }
            $.data(this, 'draggable', {
                options: opts,
                handle: handle
            })
            if(opts.disabled){
                $(this).css('cursor', '');
                return;
            }
            handle.unbind('.draggable')
                  .bind('mousemove.draggable',{target: this},function(e){
                        if($.fn.draggable.isDragging){
                            return;
                        }
                        var options = $.data(e.data.target, 'draggable').options;
                        // 鼠标的位置在handler的内部
                        if(checkArea(e)){
                            $(this).css('cursor', options.cursor);
                        }else{
                            $(this).css('cursor','');
                        }
                  })
                  .bind('mouseleave.draggable',{target: this},function(e){
                        $(this).css('cursor', '');
                  })
                  .bind('mousedown.draggable',{target: this},function(e){
                        // 是否在元素内部
                        if(checkArea(e) == false){
                            return;
                        }
                        $(this).css('cursor','');
                        // handler相对于父元素的偏移
                        var position = $(e.data.target).position();
                        // handler相对于文档视口的偏移(不受滚动条的影响)
                        var offset = $(e.data.target).offset();
                        var data = {
                            startPosition: $(e.data.target).css('position'), // hanlder的原始positon属性
                            startLeft: position.left,
                            startTop: position.top,
                            left: position.left,
                            top: position.top,
                            startX: e.pageX, // 鼠标相对于文档视口的偏移
                            startY: e.pageY,
                            width: $(e.data.target).outerWidth(),
                            height: $(e.data.target).outerHeight(),
                            offsetWidth: e.pageX - offset.left,
                            offsetHeight: e.pageY = offset.top,
                            target: e.data.target,
                            parent: $(e.data.target).parent()[0]
                        }
                        $.extend(e.data,data);
                        var options = $.data(e.data.target, 'draggable').options;
                        if(options.onBeforeDrag.call(e.data.target,e) == false){
                            return;
                        }
                        // 给document绑定mousedown、mousemove、mouseup
                        // 按住鼠标不送手,会触发mousemove、mouseup
                        $(document).bind('mousedown.draggable', e.data, onMouseDown);
                        $(document).bind('mousemove.draggable', e.data, onMouseMove);
                        $(document).bind('mouseup.draggable', e.data, onMouseUp);
                        // 手动触发mousedown的回调函数
                        $.fn.draggable.timer = setTimeout(function(){
                            $.fn.draggable.isDragging = true;
                            onMouseDown(e);
                        },options.delay);
                        return false;
                  })

            function checkArea(e){ 
                // 布局视口的作为原点
                var opts = $.data(e.data.target, 'draggable');
                var handle = opts.handle;
                var offset = $(handle).offset();
                var outerWidth = $(handle).outerWidth();
                var outerHeight = $(handle).outerHeight();
                var top = e.pageY - offset.top;
                var right = offset.left + outerWidth - e.pageX;
                var bottom = offset.top + outerHeight - e.pageY;
                var left = e.pageX - offset.left;
                return Math.min(top, right, bottom, left) > opts.options.edge;
            }

        })
    }

    $.fn.draggable.methods = {
        options: function(jq){
            return $.data(jq[0], 'draggable').options;
        },
        proxy: function(jq){
            return $.data(jq[0], 'draggable').proxy;
        },
        enable: function(jq){
            return jq.each(function(){
                $(this).draggable({disabled:false})
            })
        },
        disable: function(){
            return jq.each(function(){
                $(this).draggable({disabled:true})
            })
        }
    };

    $.fn.draggable.defaults = {
        proxy: null,
        revert: false,
        cursor: 'move',
        deltaX: null,
        deltaY: null,
        handle: null,
        disabled: false,
        edge: 0,
        axis: null,
        delay: 100,
        onBeforeDrag: function(e){},
        onStartDrag: function(e){},
        onDrag: function(e){},
        onEndDrag: function(e){},
        onStopDrag: function(){}
    };
    $.fn.draggable.isDragging = false;

})(jQuery)