技术分享

拖拽原理及组件封装

— 刘佳

什么是js拖拽

  • js原生编写可拖动节点的方法
  • 用到的事件:onmousedown、onmousemove、onmouseup
  • 保持鼠标位置和oDiv的相对距离不变

事件触发顺序

鼠标按下 -> 鼠标移动(随鼠标移动) -> 鼠标抬起

oDiv.onmousedown = function(){
    ...//鼠标按下
    oDiv.onmousemove = function(){
        ...//鼠标移动
    };
    oDiv.onmouseup = function(){
        ...//鼠标抬起
    };
};

 一个简单的拖拽例子

按N显示代码

 存在的问题?

 如何修复?

  • Ie下:设置全局捕获setCapture
  • 标准下:阻止默认事件return false

按N显示代码

 总结一下,拖拽的代码

var oDiv = document.getElementById('div1');
oDiv.onmousedown = function(ev) {
    var ev = ev || event; 
    // 1、Event 对象代表事件的状态
    // 2、ie6\7\8下获取不到ev

    var disX = ev.clientX - this.offsetLeft;
    var disY = ev.clientY - this.offsetTop;
    // 获取鼠标点击时在div中的相对位置

    if (oDiv.setCapture) {oDiv.setCapture();}
    // 文字选中bug:ie下,设置全局捕获

    document.onmousemove = function(ev) {
        var ev = ev || event;
        oDiv.style.left = ev.clientX - disX + 'px';
        oDiv.style.top = ev.clientY - disY + 'px';
    };
    document.onmouseup = function() {
        document.onmousemove = null;
        // 获取当前鼠标位置,
        // 减去与div的相对位置得到当前div应该被拖拽的位置

        if (oDiv.releaseCapture) {oDiv.releaseCapture();}
        // 文字选中bug:ie下,释放全局捕获
    };
    return false;
    // 文字选中bug:标准下,阻止事件默认行为
};

拖拽的更多应用

限制范围的拖拽
磁性吸附(边缘吸附、自动吸入)
拖拽排序
封装成函数

拖拽应用1:

限制范围的拖拽 ·实例2

按N显示代码

拖拽应用2:磁性吸附

距离磁性边缘一定距离时,自动拉到吸附位置
边缘吸附 VS 自动吸入

边缘吸附 ·实例3

按N显示代码

自动吸入 ·实例4

同碰撞检测原理

按N显示代码

拖拽应用3:拖拽排序

步骤原理:

▶ 点击节点将其绝对定位脱离文档流
▶ 在其原位置插入一空白元素占位
▶ visibility: hidden;
▶ 根据节点被拖拽的位置,计算其重新排序后的顺序
▶ 将占位空白元素插入到新位置
▶ mouseUp时将节点插入空白元素的位置,将其替换

拖拽排序 ·实例5

仿jQueryUI Sortable

按N显示代码

拖拽应用4:拖拽的封装

面向对象的思想 OOP
组件封装方法

面向对象写法 ·实例6

// 构造函数================================================
var Drag = function(opt){
    this.obj = null; // this:Drag对象,this.obj:元素
    this.disX = 0;
    this.disY = 0;
    this.settings = { //默认参数
        toDown : function(){},
        toMove : function(){},
        toUp : function(){}
    };
    // 如果创建对象的时候就传入了配置参数,执行init()
    opt && this.init(opt); 
};

// 方法================================================
Drag.prototype = {
    // init() 初始化
    init: function(opt) {
        if (!opt) { return false;}
        var This = this; // 保存this
        // 判断传入的是对象还是id名,获取对象
        this.obj = 'string'==(typeof opt.id) ? document.getElementById(opt.id) : opt.id;
        // 调用extend() 将配置的参数传到对象中保存
        this.extend( this.settings , opt ); 
        this.obj.onmousedown = function(ev){
            var ev = ev || window.event;
            // 执行fnDown()
            This.fnDown(ev);
            if (oDiv.setCapture) {oDiv.setCapture();}
            document.onmousemove = function(ev){
                var ev = ev || window.event;
                // 执行fnDown()
                This.fnMove(ev);
            };
            document.onmouseup = function(){
                // 执行fnUp()
                This.fnUp();
                if (oDiv.releaseCapture) {oDiv.releaseCapture();}
            };
            return false;
        };
    },
    // fnDown() 鼠标按下方法
    fnDown: function(ev) { 
        this.obj.style.zIndex = this.setZ(); // 设置层级
        this.disX = ev.clientX - this.obj.offsetLeft;
        this.disY = ev.clientY - this.obj.offsetTop;
        this.settings.toDown(this.obj); // 执行自定义的toDown()
    },
    // fnMove() 鼠标移动方法
    fnMove: function(ev) {
        this.obj.style.left = ev.clientX - this.disX + 'px';
        this.obj.style.top = ev.clientY - this.disY + 'px';
        this.settings.toMove(this.obj); // 执行自定义的toMove()
    },
    // fnUp() 鼠标抬起方法
    fnUp: function() {
        document.onmousemove = null;
        document.onmouseup = null;
        this.settings.toUp(this.obj); // 执行自定义的toUp()
    },
    // extend() 传参
    extend: function(obj1,obj2) {
        for(var attr in obj2){ 
            obj1[attr] = obj2[attr];
        }
    },
    // setZ() 其他div层级
    setZ: function(){
        var otherDiv = this.obj.parentNode.getElementsByTagName('div');
        var n = 0;
        for(var i=0; i < otherDiv.length; i++){ 
            var dn = parseInt(otherDiv[i].style.zIndex);
            n = ( dn > n ? dn : n ); // if ( dn > n) { n = dn;} 
        }
        return n+1;
    }
}

// 调用================================================
var aDiv = document.getElementsByTagName('div');
var d1 = new Drag({ // div1
    id : aDiv[0], // 传入对象
    toDown : function(obj){
        obj.style.opacity = '0.6';
        obj.innerHTML = parseInt(obj.innerHTML) +1;
    },
    toUp : function(obj){
        obj.style.opacity = '1';
    }
});
var d2 = new Drag({ // div2
    id : 'div2', // 直接穿入对象id
    toDown : function(obj){
        obj.style.backgroundColor = '#E03E5D';
        obj.style.opacity = '0.6';
        obj.innerHTML = parseInt(obj.innerHTML) -1;
    },
    toUp : function(obj){
        obj.style.opacity = '1';
    }
});
var d3 = new Drag({ // div3
    id : aDiv[2],
    toDown : function(obj){
        obj.style.backgroundColor = '#333';
        obj.style.opacity = '0.6';
        obj.innerHTML = '我是黑色';
        obj.style.fontSize = '20px';
    },
    toUp : function(obj){
        obj.innerHTML = '';
        obj.style.opacity = '1';
    }
});
var d4 = new Drag({ // div4
    id : aDiv[3],
    toDown : function(obj){
        obj.style.backgroundColor = '#3E96E0';
        obj.style.opacity = '0.8';
        obj.innerHTML = '我是方形';
        obj.style.fontSize = '20px';
        obj.style.borderRadius = '0';
    },
    toMove : function(obj){
        document.body.style.background = '#333';
    },
    toUp : function(obj){
        obj.innerHTML = '';
        obj.style.opacity = '1';
    }
});

组件封装的写法 ·实例7

// 构造函数================================================
var Drag = function(opt){
    this.obj = null; // this:Drag对象,this.obj:元素
    this.disX = 0;
    this.disY = 0;
    this.rangeId = null; // 范围
    this.hasRange = false;
    this.rangeJson = {};
    this.settings = { //默认参数
        toDown : function(){},
        toMove : function(){},
        toUp : function(){}
    };
    opt && this.init(opt); 
};


// 方法================================================
Drag.prototype = {
    init: function(opt) {
        if (!opt) { return false;}
        // 处理obj指向
        this.obj = 'string'==(typeof opt.id) ? document.getElementById(opt.id) : opt.id;
        if (opt.rangeId){ // 处理range
            this.rangeId = ('string'==(typeof opt.rangeId) ? document.getElementById(opt.rangeId) : opt.rangeId) ;
            this.hasRange = true;
            this.rangeJson = this.setRange();
            this.obj.style.left = this.rangeJson.left + 'px';
            this.obj.style.top = this.rangeJson.top + 'px';
        }
        this.extend( this.settings , opt );
        this.run();
    },
    run: function(){
        var This = this; 
        this.obj.onmousedown = function(ev){
            This.fnDown( ev||window.event ); 
            document.onmousemove = function(ev){ This.fnMove( ev||window.event ); };
            document.onmouseup = function(){ This.fnUp(); };
            return false;
        };
    },
    fnDown: function(ev) { 
        this.obj.style.zIndex = this.setZindex(); // 设置层级
        this.disX = ev.clientX - this.obj.offsetLeft;
        this.disY = ev.clientY - this.obj.offsetTop;
        if (this.setCapture) {this.setCapture();}
        this.settings.toDown(this.obj); // toDown()
    },
    fnMove: function(ev) {
        var L = ev.clientX - this.disX;
        var T = ev.clientY - this.disY;
        if (this.rangeJson) { // 判断范围
            if ( L < this.rangeJson.left) { L = this.rangeJson.left; } 
            else if (L > this.rangeJson.right) { L = this.rangeJson.right; }
            if ( T < this.rangeJson.top ) { T = this.rangeJson.top; } 
            else if ( T > this.rangeJson.bottom) { T = this.rangeJson.bottom; }
        }
        this.obj.style.left = L + 'px';
        this.obj.style.top = T + 'px';
        this.settings.toMove(this.obj); // toMove()
    },
    fnUp: function() {
        document.onmousemove = null;
        document.onmouseup = null;
        this.settings.toUp(this.obj); // toUp()
        if (this.releaseCapture) {this.releaseCapture();}
    },
    extend: function(obj1,obj2) {
        for(var attr in obj2){ obj1[attr] = obj2[attr]; }
    },
    setZindex: function(){
        var otherDiv = this.obj.parentNode.getElementsByTagName('div');
        var n = 0;
        for(var i=0; i n ? dn : n );
        }
        return n+1;
    },
    setRange: function(){
        return {
            left : this.posLeft(this.rangeId),  
            right : this.posLeft(this.rangeId) + this.rangeId.offsetWidth - this.obj.offsetWidth,
            top : this.posTop(this.rangeId),
            bottom : this.posTop(this.rangeId) + this.rangeId.offsetHeight - this.obj.offsetHeight
        };
    },
    posLeft: function (obj){ // 获取绝对位置left
        var iLeft = 0;
        while(obj){
            iLeft += obj.offsetLeft;
            obj = obj.offsetParent;
            if(obj && obj!=document.body && obj!=document.documentElement){
                iLeft += this.getStyle(obj, 'borderLeftWidth');
            }
        }
        return iLeft;
    },
    posTop: function (obj){ // 获取绝对位置top
        var iTop = 0;
        while(obj){
            iTop += obj.offsetTop;
            obj = obj.offsetParent;
            if(obj && obj!=document.body && obj!=document.documentElement){
                iTop += this.getStyle(obj, 'borderTopWidth');
            }
        }
        return iTop;
    },
    getStyle: function (obj,attr){
        if(obj.currentStyle){
            return parseFloat( obj.currentStyle[attr]) || 0;
        }
        return parseFloat( getComputedStyle(obj)[attr]) || 0;
    }
}


// 调用================================================
window.onload = function(){
    // div1
    var d1 = new Drag({ 
        id : 'div1', // 传入对象
        toDown : function(obj){
            obj.style.opacity = '0.6';
            obj.innerHTML = parseInt(obj.innerHTML) +1;
        },
        toUp : function(obj){
            obj.style.opacity = '1';
        }
    });
    // div2
    var d2= new Drag({ 
        id : 'div2', // 传入对象
        rangeId : 'box', // 限制范围填写id,否则不用填
        toDown : function(obj){
            obj.style.opacity = '0.6';
        },
        toUp : function(obj){
            obj.style.opacity = '1';
        }
    });
};

谢谢收看!