zoukankan      html  css  js  c++  java
  • 重新发明轮子之Draggable Elements

    虽说js框架到处都是, 都封装了很多实用的功能,能快速的让我们实现如动画,元素拖拽等功能, 不过由于好奇心的驱使, 有时想一探究竟, 看看一些功能是如何实现的, 当然我们可以研究js库的源码, 也可以自己去发明轮子试试看, 其过程还是挺有趣的...下面我就来实现下页面元素的拖拽功能

     
     Handle

    现在就开始着手实现, 让我们从最顶层的方法讲起, 它用于初始化一个drag object, 方法的声明如下

    function DragObject(cfg)
    

    这里的cfg我们用一个对象来传入, 有点像Extjs里配置属性

       var dragObj = new DragObject({
           el: 'exampleB',
           attachEl: 'exampleBHandle',
           lowerBound: new Position(0, 0),  //position代表一个点,有属性x,y下面会详细讲到
           upperBound: new Position(500, 500),
           startCallback: ..., // 开始拖拽时触发的回调 这里均省略了
           moveCallback: ...,  // 拖拽过程中触发的回调
           endCallback: ...,   // 拖拽结束触发的回调
           attachLater: ...    // 是否立刻启动拖拽事件的监听 
       });
    

    配置参数中el可以是具体元素的id, 也可以直接是个dom对象 attachEl就是例子中的handle元素, 通过拖拽它来拖拽元素, lowerBound和upperBound是用于限定拖拽范围的, 都是Position对象, 关于这个对象的封装和作用我们下面会分析到,不急哈: ), 如果没有传入的话, 拖拽的范围就没有限制. startCallback, moveCallback, endCallback都是些回调函数, attachLater为true或者false. 如果不是很明白看了下面的分析, 我想你肯定很快会清楚的..

    下面就来写Position, 代码如下:

       function Position(x, y) {
         this.X = x;
         thix.Y = y;
       }
       Position.prototype = {
    	constructor: Position,
    		
    	add : function(val) {
    		var newPos = new Position(this.X, this.Y);
    		if (val) {
    			newPos.X += val.X;
    			newPos.Y += val.Y;
    		}
    		return newPos;
    	},
    
    	subtract : function(val) {
    		var newPos = new Position(this.X, this.Y);
    		if (val) {
    			newPos.X -= val.X;
    			newPos.Y -= val.Y;
    		}
    		return newPos;
    	},
    
    	min : function(val) {
    		var newPos = new Position(this.X, this.Y);
    		if (val) {
    			newPos.X = this.X > val.X ? val.X : this.X;
    			newPos.Y = this.Y > val.Y ? val.Y : this.Y;
    			return newPos;
    		}
    		return newPos;
    	},
    
    	max : function(val) {
    		var newPos = new Position(this.X, this.Y);
    		if (val) {
    			newPos.X = this.X < val.X ? val.X : this.X;
    			newPos.Y = this.Y < val.Y ? val.Y : this.Y;
    			return newPos;
    		}
    		return newPos;
    	},
    
    	bound : function(lower, upper) {
    		var newPos = this.max(lower);
    		return newPos.min(upper);
    	},
    
    	check : function() {
    		var newPos = new Position(this.X, this.Y);
    		if (isNaN(newPos.X))
    			newPos.X = 0;
    		if (isNaN(newPos.Y))
    			newPos.Y = 0;
    		return newPos;
    	},
    	
    	apply : function(el) {
    		if(typeof el == 'string')
    			el = document.getElementById(el);
    		if(!el) return;
    		el.style.left = this.X + 'px';
    		el.style.top = this.Y + 'px';
    	}
        };
    

    一个坐标点的简单封装, 它保存两个值: x, y坐标. 我们能够通过add和substract方法跟别的坐标点进行+运算和-运算, 返回一个计算过的新坐标点. min和max函数顾名思义用于跟其他坐标点进行比较,并返回其中较小和教大的值.bound方法返回一个在限定范围内的坐标点. check方法用于确保属性x, y的值是数字类型的, 否则会置0. 最后apply方法就是把属性x,y作用于元素style.left和top上. 接着我把剩下的大部分代码拿出来, 再一点一点看:

     function DragObject(cfg) {
    	var el = cfg.el,
    	    attachEl = cfg.attachEl,
    	    lowerBound = cfg.lowerBound,
    	    upperBound = cfg.upperBound,
    	    startCallback = cfg.startCallback,
    	    moveCallback = cfg.moveCallback,
    	    endCallback = cfg.endCallback,
    	    attachLater = cfg.attachLater;
    	
    	if(typeof el == 'string')
    		el = document.getElementById(el);
    	if(!el) return;
    	
    	if(lowerBound != undefined && upperBound != undefined) {
    		var tempPos = lowerBound.min(upperBound);
    		upperBound = lowerBound.max(upperBound);
    		lowerBound = tempPos;
    	}
    	
    	var cursorStartPos,
    	    elementStartPos,
    	    dragging = false,
    	    listening = false,
    	    disposed = false;
    	
    	function dragStart(eventObj) {
    		if(dragging || !listening || disposed) return;
    		dragging = true;
    		
    		if(startCallback)
    			startCallback(eventObj, el);
    		
    		cursorStartPos = absoluteCursorPosition(eventObj);
    		
    		elementStartPos = new Position(parseInt(getStyle(el, 'left')), parseInt(getStyle(el, 'top')));
    		
    		elementStartPos = elementStartPos.check();
    		
    		hookEvent(document, 'mousemove', dragGo);
    		hookEvent(document, 'mouseup', dragStopHook);
    		
    		return cancelEvent(eventObj);
    	}
    	
    	function dragGo(e) {
    		if(!dragging || disposed) return;
    		var newPos = absoluteCursorPosition(e);
    		newPos = newPos.add(elementStartPos)
    					   .subtract(cursorStartPos)
    					   .bound(lowerBound, upperBound);
    		newPos.apply(el);
    		if(moveCallback)
    			moveCallback(newPos, el);
    		
    		return cancelEvent(e);
    	}
    	
    	function dragStopHook(e) {
    		dragStop();
    		return cancelEvent(e);
    	}
    	
    	function dragStop() {
    		if(!dragging || disposed) return;
    		unhookEvent(document, 'mousemove', dragGo);
    		unhookEvent(document, 'mouseup', dragStopHook);
    		cursorStartPos = null;
    		elementStartPos = null;
    		if(endCallback)
    			endCallback(el);
    		dragging = false;
    	}
    	
    	this.startListening = function() {
    		if(listening || disposed) return;
    		
    		listening = true;
    		hookEvent(attachEl, 'mousedown', dragStart);
    	};
    	
    	this.stopListening = function(stopCurrentDragging) {
    		if(!listening || disposed)
    			return;
    		
    		unhookEvent(attachEl, 'mousedown', dragStart);
    		listening = false;
    		
    		if(stopCurrentDragging && dragging)
    			dragStop();
    	};
    	
    	this.dispose = function() {
    	    if(disposed) return;
    	    this.stopListening(true);
    	    el = null;
    	    attachEl = null;
    	    lowerBound = null;
    	    upperBound = null;
    	    startCallback = null;
    	    moveCallback = null;
    	    endCallback = null;
    	    disposed = true;
    	};
    	
    	this.isDragging = function() {
    		return dragging;
    	};
    	
    	this.isListening = function() {
    		return listening;
    	};
    	
    	this.isDisposed = function() {
    		return disposed;
    	};
    	
    	if(typeof attachEl == 'string')
    		attachEl = document.getElementById(attachEl);
    	// 如果没有配置, 或者没找到该Dom对象, 则用el
            if(!attachEl) attachEl = el;
    	
    	if(!attachLater)
    		this.startListening();
    }
    

    其中一些未给出方法, 在往下分析的过程中, 会一一给出....

    我们先通过cfg来使el和attachEl指向实际的Dom对象, 如果attachEl没配置或者没找到对应元素则用el替代. 我们同时设置了一些在拖拽中要用到的变量. cursorStartPos用于保存鼠标按下开始拖拽时鼠标的坐标点. elementStartPos用于保存元素开始拖拽时的起始点. dragging, listening, disposed是一些状态变量. listening: 指drag object是否正在监听拖拽开始事件. dragging: 元素是否正在被拖拽. disposed: drag object被清理, 不能再被拖拽了.

    在代码的最后, 我们看到如果attachLater不为true, 那么就调用startListening, 这是一个 public方法定义在drag object中, 让我们看下它的实现

      this.startListening = function() {
            if(listening || disposed) return;
    		
    	listening = true;
    	hookEvent(attachEl, 'mousedown', dragStart);
      };
    

    前两行就是做个判断, 如果已经开始对拖拽事件进行监听或者清理过了, 就什么都不做直接return. 否则把listening状态设为true, 表示我们开始监听啦, 把dragStart函数关联到attachEl的mousedown事件上. 这里碰到个hookEvent函数, 我们来看看它的样子:

       function hookEvent(el, eventName, callback) {
    	if(typeof el == 'string')
    		el = document.getElementById(el);
    	if(!el) return;
    	if(el.addEventListener)
    		el.addEventListener(eventName, callback, false);
    	else if (el.attachEvent)
    		el.attachEvent('on' + eventName, callback);
       }
    

    其实也没什么, 就是对元素事件的监听做了个跨浏览器的封装, 同样的unhookEvent方法如下

       function unhookEvent(el, eventName, callback) {
    	if(typeof el == 'string')
    		el = document.getElementById(el);
    	if(!el) return;
    	if(el.removeEventListener)
    		el.removeEventListener(eventName, callback, false);
    	else if(el.detachEvent) 
    		el.detachEvent('on' + eventName, callback);
    }
    

    接着我们来看看dragStart函数的实现, 它是drag object的一个私有函数

      function dragStart(eventObj) {
    		if(dragging || !listening || disposed) return;
    		dragging = true;
    		
    		if(startCallback)
    			startCallback(eventObj, el);
    		
    		cursorStartPos = absoluteCursorPosition(eventObj);
    		
    		elementStartPos = new Position(parseInt(getStyle(el, 'left')), parseInt(getStyle(el, 'top')));
    		
    		elementStartPos = elementStartPos.check();
    		
    		hookEvent(document, 'mousemove', dragGo);
    		hookEvent(document, 'mouseup', dragStopHook);
    		
    		return cancelEvent(eventObj);
    	}
    

    attachEl所指的dom对象捕获到mousedown事件后调用此函数. 首先我们先确定drag object在一个适合拖拽的状态, 如果拖拽正在进行, 或者没有在监听拖拽事件, 再或者已经处理完"后事"了, 那就什么都不做. 如果一切ok, 我们把 dragging状态设为true, 然后"开工了", 如果startCallback定义了, 那我们就调用下它, 以mousedown event和el为参数. 接着我们定位鼠标的绝对位置, 保存到cursorStartPos中. 然后拿到拖拽元素当前的top, left,封装成Position对象保存到elementStartPos中. 保险起见我们检查下elementStartPos中属性是否合法. 再看两个hookEvent的调用, 一个是mousemove事件, 表示正在dragging,调用dragGo函数. 一个是mouseup事件, 代表拖拽的结束, 调用dragStopHook函数.可能你会问,为什么事件绑定在document上, 而不是要拖拽的元素上,比如我们这里的el或者attachEl.因为考虑到直接将事件绑定到元素上,可能由于浏览器的一些延时会影响效果,所以直接把事件绑定到document上. 如果实在不是很理解, 或许影响也不大: P.... 看最后一句话中的cancelEvent(eventObj)

     function cancelEvent(e) {
    	e = e ? e : window.event;
    	if(e.stopPropagation)
    		e.stopPropagation();
    	if(e.preventDefault)
    		e.preventDefault();
    	e.cancelBubble = true;
    	e.returnValue = false;
    	return false;
    }
    

    用于停止冒泡, 阻止默认事件, 可以理解为安全考虑....在dragStart中有些方法需要介绍下,先来 看看absoluteCursorPosition, 再看下getStyle

      function absoluteCursorPosition(e) {
    	e = e ? e : window.event;
    	var x = e.clientX + (document.documentElement || document.body).scrollLeft;
    	var y = e.clientY + (document.documentElement || document.body).scrollTop;
    	return new Position(x, y);
      } 
    

    此方法就只是用于获得鼠标在浏览器中的绝对位置, 把滚动条考虑进去就行了

     function getStyle(el, property) {
    	if(typeof el == 'string')
    		el = document.getElementById(el);
    	if(!el || !property) return;
    	var value = el.style[property];
    	if(!value) {
    		if(document.defaultView && document.defaultView.getComputedStyle) {
    			var css = document.defaultView.getComputedStyle(el, null);
    			value = css ? css.getPropertyValue(property) : null;
    		} else if (el.currentStyle) {
    			value = el.currentStyle[property];
    		}
    	}
    	return value == 'auto' ? '' : value;
    }
    

    getStyle方法用于获取元素的css属性值, 这样不管你样式是写成内联形式还是定义在css中, 我们都能拿到正确的值, 当然我们这里只要获取元素的top, left属性即可..下面真正处理拖拽工作的方法dragGo

      function dragGo(e) {
    		if(!dragging || disposed) return;
    		var newPos = absoluteCursorPosition(e);
    		newPos = newPos.add(elementStartPos)
    					   .subtract(cursorStartPos)
    					   .bound(lowerBound, upperBound);
    		newPos.apply(el);
    		if(moveCallback)
    			moveCallback(newPos, el);
    		
    		return cancelEvent(e);
    	}
    

    这个方法并不复杂, 像其他的方法一样, 我们先查看下状态如何, 如果没有在拖拽中或者已经清理了, 那么什么都不做. 如果一切顺利, 我们利用鼠标当前位置, 元素初始位置, 鼠标初始位置, 和限定范围(如果配置upperBound, lowerBound的话)来计算出一个结果点, 通过apply方法我们把计算的坐标赋给元素style.top和style.left, 让拖拽元素确定其位置. 如果配置了moveCallback, 那么就调用下, 最后来个cancelEvent...这里的新坐标运算,类似于jquery的操作, 因为Position对象的每个方法都返回了一个Position对像...dragStart里还有个方法dragStopHook

            function dragStopHook(e) {
    		dragStop();
    		return cancelEvent(e);
    	}
    	
    	function dragStop() {
    		if(!dragging || disposed) return;
    		unhookEvent(document, 'mousemove', dragGo);
    		unhookEvent(document, 'mouseup', dragStopHook);
    		cursorStartPos = null;
    		elementStartPos = null;
    		if(endCallback)
    			endCallback(el);
    		dragging = false;
    	}
    

    关键看下dragStop方法, 同样先判断下状态, 一切ok的话, 我们移除事件的绑定mousemove和mouseup, 并把 cursorStartPos和elementStartPos的值释放掉, 一次拖拽结束啦..如果配置了endCallback那就调用下, 最后把dragging状态设置为false......最后给出会用到的public方法

      this.stopListening = function(stopCurrentDragging) {
    		if(!listening || disposed)
    			return;
    		
    		unhookEvent(attachEl, 'mousedown', dragStart);
    		listening = false;
    		
    		if(stopCurrentDragging && dragging)
    			dragStop();
      };
      
      this.dispose = function() {
    	   if(disposed) return;
    	   this.stopListening(true);
    	    el = null;
    	    attachEl = null;
    	    lowerBound = null;
    	    upperBound = null;
    	    startCallback = null;
    	    moveCallback = null;
    	    endCallback = null;
    	    disposed = true;
    	};
    	
    	this.isDragging = function() {
    		return dragging;
    	};
    	
    	this.isListening = function() {
    		return listening;
    	};
    	
    	this.isDisposed = function() {
    		return disposed;
    	};
    

    stopListening移除监听拖拽的mousedown事件, 把监听状态listening设置为false, 这里有个参数stopCurrentDragging见名知意. dispose方法用于些处理工作, 如果你不想让drag object能被拖拽,那么调用一下dispose就可以了, 至于下面的三个小方法isDragging, isListening, isDisposed一看便知, 返回相关的状态. 最后给个源码的下拉链接 下载点我 欢迎园友留言, 交流!

  • 相关阅读:
    django基础之ORM基础知识
    Centos7 搭建sonarQube
    centos7安装部署SVN
    centos7.5 SVN 搭建
    centos 7 部署 zookeeper
    centos7 发送邮件
    Centos7安装配置Gitlab-CE
    openldap 双主模式部署
    K8s一键安装
    ELK实战部署
  • 原文地址:https://www.cnblogs.com/AndyWithPassion/p/js_drag.html
Copyright © 2011-2022 走看看