├── README.MD ├── demo └── simple.html ├── gulpfile.js ├── index.html ├── package.json └── src ├── draggable.js └── draggable.min.js /README.MD: -------------------------------------------------------------------------------- 1 | # Draggable 2 | 3 | 打造跨平台的轻量级原生拖拽库 4 | 5 | ## Summary 6 | 7 | 自己写的一个拖拽库,基于原生JS实现,无任何依赖,同时还做了IE8的兼容,在IE8的情况下`transform`回退到`position`实现。拖拽的动画通过绑定在`render`函数上的`requestAnimationFrame`实现而不是使用`mousemove`回调。另外库里面还写了很多的注释,方便对源码的解读与交流。如果你觉得这个库写的不错或者有值得学习的经验不妨点下右上角的`star`,谢谢各位。 8 | 9 | ## Install 10 | 11 | 直接从`src`文件夹中引入即可 12 | 13 | ## Usage 14 | 15 |
16 | 17 | 可以直接传入选择器 18 | 19 | new Draggable('#app'); 20 | 21 | 或者传入DOM节点 22 | 23 | var elem=document.querySelector('#app'); 24 | new Draggable(elem); 25 | 26 | 如果需要为多个元素添加拖拽,请循环遍历 27 | 28 | var elem=document.querySelectorAll('.app'); 29 | for(var i=0,len=elem.length;i 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 26 | 27 | 28 |
29 |
点我拖拽
30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var uglify = require('gulp-uglify'); 3 | var rename = require('gulp-rename'); 4 | 5 | gulp.task('script', function() { 6 | gulp.src('src/draggable.js') 7 | .pipe(uglify()) 8 | .pipe(rename('draggable.min.js')) 9 | .pipe(gulp.dest('src')) 10 | }) 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 26 | 27 | 28 |
29 |
点我拖拽
30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draggable", 3 | "version": "1.0.0", 4 | "description": "打造跨平台的轻量级原生拖拽库", 5 | "main": "gulpfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/qiangzi7723/draggable.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/qiangzi7723/draggable/issues" 17 | }, 18 | "homepage": "https://github.com/qiangzi7723/draggable#readme" 19 | } 20 | -------------------------------------------------------------------------------- /src/draggable.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | var Util = function() { 3 | 4 | } 5 | 6 | Util.prototype.checkIsTouch = function() { 7 | return 'ontouchstart' in window || 8 | navigator.maxTouchPoints; 9 | } 10 | 11 | Util.prototype.hackRequestAnimationFrame = function(arguments) { 12 | window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; 13 | // requestAnimationFrame的回退 14 | if (!window.requestAnimationFrame) { 15 | var lastTime = 0; 16 | window.requestAnimationFrame = function(callback) { 17 | var now = new Date().getTime(); 18 | var time = Math.max(16 - now - lastTime, 0); 19 | var id = setTimeout(callback, time); 20 | lastTime = now + time; 21 | return id; 22 | } 23 | } 24 | } 25 | 26 | Util.prototype.hackTransform = function() { 27 | // 兼容transform和WebkitTransform 28 | var docElem = document.documentElement; 29 | this.transformProperty = typeof docElem.style.transform == 'string' ? 30 | 'transform' : 'WebkitTransform'; 31 | } 32 | 33 | Util.prototype.hackEventListener = function() { 34 | if (document.addEventListener) this.eventListener = true; 35 | } 36 | 37 | Util.prototype.hackStyle = function(elem) { 38 | // 兼容IE8的样式获取 39 | if (window.getComputedStyle) { 40 | return window.getComputedStyle(elem); 41 | } else { 42 | return elem.currentStyle; 43 | } 44 | } 45 | 46 | // Draggable 47 | var Draggable = function(elem, options) { 48 | this.options = options || {}; 49 | this.options.elemString = elem; 50 | this.init(); 51 | } 52 | // 为了兼容IE8 只能放弃使用Object.create()实现继承 53 | var proto = Draggable.prototype = new Util(); 54 | 55 | proto.init = function() { 56 | var context = this; 57 | this.setTargetDom(); 58 | this.hackEventListener(); 59 | if (!this.eventListener) this.options.backToPosition = true; 60 | if (this.checkIsTouch()) { 61 | // 说明是手机端 手机端的事件还需要兼容更多手机 62 | this.elem.addEventListener('touchstart', this); 63 | } else { 64 | if (this.eventListener) { 65 | this.elem.addEventListener('mousedown', this); 66 | } else { 67 | this.bindAttach(); 68 | // 为了兼容IE8 优雅的代码全部需要改写 69 | this.elem.attachEvent('onmousedown', this.b_mousedown); 70 | } 71 | } 72 | if (!this.options.cursorCancel) this.elem.style.cursor = 'move'; 73 | } 74 | proto.setTargetDom = function() { 75 | this.elem = this.getDom(this.options.elemString); 76 | this.parentMove = this.getDom(this.options.parentMove); 77 | // 如果参数使用了parentMove接口,那么就使用parentMove作为拖拽的目标元素 78 | this.targetDom = this.parentMove || this.elem; 79 | } 80 | proto.getDom = function(elem) { 81 | if (typeof elem === 'string') { 82 | return document.querySelector(elem); 83 | } else { 84 | return elem; 85 | } 86 | } 87 | proto.bindAttach = function() { 88 | // bindAttach只是为了方便IE8下的事件绑定而写的一个方法 可以省略 89 | var context = this; 90 | var type = ['mousedown', 'mousemove', 'mouseup']; 91 | for (var i = 0, len = type.length; i < len; i++) { 92 | this['b_' + type[i]] = (function(i) { 93 | return function() { 94 | context.event = window.event; 95 | context[type[i]](); 96 | } 97 | }(i)) 98 | } 99 | } 100 | proto.dragDown = function(event) { 101 | this.enable = true; 102 | this.hackTransform(); 103 | this.addClassName(); 104 | this.setIndex(); 105 | this.style = this.hackStyle(this.targetDom); 106 | this.targetPosition = this.getPosition(this.style); 107 | this.startPoint = this.getCoordinate(); 108 | this.movePoint = { 109 | x: 0, 110 | y: 0 111 | }; 112 | this.setPositionProperty(); 113 | this.bindCallBackEvent(); 114 | this.render(); 115 | } 116 | proto.addClassName=function(){ 117 | if (this.options.addClassName) this.elem.className += ' ' + this.options.addClassName; 118 | } 119 | proto.setIndex=function(){ 120 | // this.elem.style.zIndex=2147483647; 121 | } 122 | // 需要把transform上设置的值暂时转换到使用position属性实现 123 | // 这是为了方便拖拽过程中拖拽动画的实现 如果看的不太明白可以先行略过 124 | proto.getPosition = function(style) { 125 | var position = {}; 126 | position.x = style.left == 'auto' ? 0 : parseInt(style.left, 10); 127 | position.y = style.top == 'auto' ? 0 : parseInt(style.top, 10); 128 | // 如果设置了backToPosition属性,那么我们就不需要修改初始的transform属性,直接跳出函数 129 | if (this.options.backToPosition) return position; 130 | position = this.addTransform(position); 131 | return position; 132 | } 133 | proto.addTransform = function(position) { 134 | var transform = this.style[this.transformProperty]; 135 | if (!transform || transform.indexOf('matrix') == '-1') { 136 | // 如果当前元素没有设置transform属性,那么我们可以直接返回position 137 | return position; 138 | } 139 | // 如果是2D的transform,那么translate的值的索引以4开始,否则就是3D,以12开始 140 | var translateIndex = transform.indexOf('matrix3d') == '-1' ? 4 : 12; 141 | var transformArray = transform.split(','); 142 | this.translateX = parseInt(transformArray[translateIndex], 10); 143 | this.translateY = parseInt(transformArray[translateIndex + 1], 10); 144 | position.x += this.translateX; 145 | position.y += this.translateY; 146 | return position; 147 | } 148 | // 获取鼠标的坐标 149 | proto.getCoordinate = function() { 150 | if (this.eventListener) { 151 | return { 152 | // 最后的0是为了避免当 this.event.pageX==0 的时候会取 touches[0] 的值 153 | x: this.event.pageX || (this.event.touches && this.event.touches[0].pageX) || 0, 154 | y: this.event.pageY || (this.event.touches && this.event.touches[0].pageY) || 0 155 | } 156 | } else { 157 | return { 158 | // 兼容IE8的鼠标位置获取 159 | x: this.event.clientX + document.documentElement.scrollLeft, 160 | y: this.event.clientY + document.documentElement.scrollTop 161 | } 162 | } 163 | } 164 | proto.setPositionProperty = function() { 165 | var p = { 166 | fix: true, 167 | absolute: true, 168 | relative: true 169 | }; 170 | if (!p[this.style.position]) { 171 | this.targetDom.style.position = 'relative'; 172 | } 173 | this.targetDom.style.cssText+=';'+'left:'+this.targetPosition.x + 'px;top:'+this.targetPosition.y + 'px;'; 174 | } 175 | // 绑定之后的事件 比如mousemove和mouseup 176 | proto.bindCallBackEvent = function() { 177 | var context = this; 178 | var type = this.event.type; 179 | var handleObj = { 180 | mousedown: ['mousemove', 'mouseup'], 181 | touchstart: ['touchmove', 'touchend'] 182 | } 183 | var handles = handleObj[type]; 184 | this.handles = handles; 185 | // true绑定事件 false解绑事件 186 | this.bindEvent(true); 187 | } 188 | proto.bindEvent = function(isBind) { 189 | var context = this; 190 | var handles = this.handles; 191 | if (this.eventListener) { 192 | var eventListener = isBind ? 'addEventListener' : 'removeEventListener'; 193 | handles.forEach(function(handle) { 194 | window[eventListener](handle, context); 195 | }) 196 | } else { 197 | var eventListener = isBind ? 'attachEvent' : 'detachEvent'; 198 | document[eventListener]('onmousemove', this.b_mousemove); 199 | document[eventListener]('onmouseup', this.b_mouseup); 200 | } 201 | } 202 | proto.render = function() { 203 | var context = this 204 | this.hackRequestAnimationFrame(); 205 | this._render = function() { 206 | if (!context.enable) { 207 | // 通过直接return取消定时器 208 | return; 209 | } 210 | context.setTransform(); 211 | requestAnimationFrame(context._render); 212 | } 213 | requestAnimationFrame(this._render); 214 | } 215 | proto.setTransform = function() { 216 | if (!this.options.backToPosition) { 217 | this.targetDom.style[this.transformProperty] = 'translate3d(' + this.movePoint.x + 'px,' + this.movePoint.y + 'px,' + '0)'; 218 | } else { 219 | var cssString = 'left:' + (this.movePoint.x + this.targetPosition.x) + 'px;top:' + (this.movePoint.y + this.targetPosition.y) + 'px;'; 220 | // cssText会覆盖原样式 所以需要写+ 另外;是为了兼容IE8的cssText不返回; 不加上会出BUG 221 | this.targetDom.style.cssText += ';' + cssString; 222 | } 223 | } 224 | proto.dragMove = function() { 225 | var vector = this.getCoordinate(); 226 | var moveVector = { 227 | x: vector.x - this.startPoint.x, 228 | y: vector.y - this.startPoint.y 229 | } 230 | moveVector = this.setGrid(moveVector); 231 | this.movePoint.x = this.options.axis == 'y' ? 0 : moveVector.x; 232 | this.movePoint.y = this.options.axis == 'x' ? 0 : moveVector.y; 233 | } 234 | proto.setGrid = function(moveVector) { 235 | if (!this.options.grid) return moveVector; 236 | var grid = this.options.grid; 237 | var vector = {}; 238 | vector.x = grid.x ? Math.round(moveVector.x / grid.x) * grid.x : moveVector.x; 239 | vector.y = grid.y ? Math.round(moveVector.y / grid.y) * grid.y : moveVector.y; 240 | return vector; 241 | } 242 | proto.dragUp = function() { 243 | var context = this; 244 | this.enable = false; 245 | this.removeClassName(); 246 | this.bindEvent(false); 247 | this.resetIndex(); 248 | if (this.options.backToPosition) return; 249 | this.resetPosition(); 250 | } 251 | proto.removeClassName=function(){ 252 | if (this.options.addClassName) { 253 | var re = new RegExp("(?:^|\\s)" + this.options.addClassName + "(?:\\s|$)", "g"); 254 | this.elem.className = this.elem.className.replace(re, ''); 255 | } 256 | } 257 | proto.resetIndex=function(){ 258 | // this.elem.style.zIndex=''; 259 | } 260 | proto.resetPosition = function() { 261 | this.endPoint = { 262 | x: this.movePoint.x + this.targetPosition.x, 263 | y: this.movePoint.y + this.targetPosition.y 264 | } 265 | this.targetDom.style.cssText+=';left:'+this.endPoint.x + 'px;top:'+this.endPoint.y + 'px;transform:translate3d(0,0,0)'; 266 | } 267 | 268 | proto.touchstart = function(event) { 269 | this.dragDown(event); 270 | } 271 | proto.mousedown = function() { 272 | this.dragDown(); 273 | } 274 | proto.mousemove = function() { 275 | this.dragMove(); 276 | } 277 | proto.touchmove = function() { 278 | this.dragMove(); 279 | } 280 | proto.mouseup = function() { 281 | this.dragUp(); 282 | } 283 | proto.touchend = function() { 284 | this.dragUp(); 285 | } 286 | // 通过handleEvent绑定事件 287 | proto.handleEvent = function(event) { 288 | this.event = event; 289 | var type = this.event.type; 290 | if (type) this[type](); 291 | } 292 | window.Draggable = Draggable; 293 | }(window)) 294 | -------------------------------------------------------------------------------- /src/draggable.min.js: -------------------------------------------------------------------------------- 1 | !function(t){var e=function(){};e.prototype.checkIsTouch=function(){return"ontouchstart"in t||navigator.maxTouchPoints},e.prototype.hackRequestAnimationFrame=function(arguments){if(t.requestAnimationFrame=t.requestAnimationFrame||t.mozRequestAnimationFrame||t.webkitRequestAnimationFrame||t.msRequestAnimationFrame,!t.requestAnimationFrame){var e=0;t.requestAnimationFrame=function(t){var i=(new Date).getTime(),n=Math.max(16-i-e,0),s=setTimeout(t,n);return e=i+n,s}}},e.prototype.hackTransform=function(){var t=document.documentElement;this.transformProperty="string"==typeof t.style.transform?"transform":"WebkitTransform"},e.prototype.hackEventListener=function(){document.addEventListener&&(this.eventListener=!0)},e.prototype.hackStyle=function(e){return t.getComputedStyle?t.getComputedStyle(e):e.currentStyle};var i=function(t,e){this.options=e||{},this.options.elemString=t,this.init()},n=i.prototype=new e;n.init=function(){this.setTargetDom(),this.hackEventListener(),this.eventListener||(this.options.backToPosition=!0),this.checkIsTouch()?this.elem.addEventListener("touchstart",this):this.eventListener?this.elem.addEventListener("mousedown",this):(this.bindAttach(),this.elem.attachEvent("onmousedown",this.b_mousedown)),this.options.cursorCancel||(this.elem.style.cursor="move")},n.setTargetDom=function(){this.elem=this.getDom(this.options.elemString),this.parentMove=this.getDom(this.options.parentMove),this.targetDom=this.parentMove||this.elem},n.getDom=function(t){return"string"==typeof t?document.querySelector(t):t},n.bindAttach=function(){for(var e=this,i=["mousedown","mousemove","mouseup"],n=0,s=i.length;n