├── .gitignore ├── .jshintrc ├── component.json ├── README.md ├── index.html └── draggabilly.js /.gitignore: -------------------------------------------------------------------------------- 1 | components/ 2 | node_modules/ 3 | build/ 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "devel": true, 4 | "strict": true, 5 | "undef": true, 6 | "unused": true 7 | } 8 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draggabilly", 3 | "main": "./draggabilly.js", 4 | "version": "0.1.1", 5 | "dependencies": { 6 | "classie": "desandro/classie", 7 | "eventEmitter": ">=3", 8 | "eventie": "desandro/eventie", 9 | "get-size": "desandro/get-size#>=0.0.8", 10 | "get-style-property": "desandro/get-style-property" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Draggabilly 2 | 3 |

Make that shiz draggable

4 | 5 | [draggabilly.desandro.com](http://draggabilly.desandro.com) 6 | 7 | Rad because it supports IE8+ and multi-touch. 8 | 9 | ## Install 10 | 11 | Grab a packaged source file: 12 | 13 | + [http://draggabilly.desandro.com/draggabilly.pkgd.min.js](draggabilly.pkgd.min.js) for production 14 | + [http://draggabilly.desandro.com/draggabilly.pkgd.js](draggabilly.pkgd.js) for development 15 | 16 | Or if you're cool with the command line, install with [Bower](http://twitter.github.com/bower). 17 | 18 | ``` bash 19 | bower install draggabilly 20 | ``` 21 | 22 | ## Usage 23 | 24 | ``` js 25 | var elem = document.querySelector('#draggable'); 26 | var draggie = new Draggabilly( elem, { 27 | // options... 28 | }); 29 | ``` 30 | 31 | When dragging, Draggabillly will add the class `.is-dragging` to the element. 32 | 33 | ## Options 34 | 35 | ### containment 36 | 37 | **Type:** _Element_, Selector _String_, or _Boolean_ 38 | 39 | ``` js 40 | containment: '#container' 41 | ``` 42 | 43 | Contains movement to the bounds of the element. If `true`, the container will be the parent element. 44 | 45 | ### handle 46 | 47 | **Type:** Selector _String_ 48 | 49 | ``` js 50 | handle: '.handle' 51 | ``` 52 | 53 | Specifies on what element the drag interaction starts. 54 | 55 | ## Events 56 | 57 | Draggabilly is an Event Emitter. You can bind event listeners to events. 58 | 59 | ``` js 60 | var draggie = new Packery( elem ); 61 | 62 | function onDragMove( event, pointer, instance ) { 63 | console.log( 'dragMove on ' + event.type + 64 | pointer.pageX + ', ' + pointer.pageY + 65 | ' position at ' + instance.position.x + ', ' + instance.position.y ); 66 | } 67 | // bind event listener 68 | draggie.on( 'dragMove', onDragMove ); 69 | // un-bind event listener 70 | draggie.off( 'dragMove', onDragMove ); 71 | // return true to trigger an event listener just once 72 | draggie.on( 'dragMove', function() { 73 | console.log('Draggabilly did move, just once'); 74 | return true; 75 | }); 76 | ``` 77 | 78 | ### dragStart 79 | 80 | ```js 81 | .on( 'dragStart', function( event, pointer, draggieInstance ) { //... 82 | ``` 83 | 84 | + `event` - **Type:** _Event_ - the original `mousedown` or `touchstart` event 85 | + `pointer` - **Type:** _MouseEvent_ or _Touch_ - the event object that has `.pageX` and `.pageY` 86 | + `draggieInstance` - **Type:** _Draggabilly_ - the Draggabilly instance 87 | 88 | ### dragMove 89 | 90 | ```js 91 | .on( 'dragMove', function( event, pointer, draggieInstance ) { //... 92 | ``` 93 | 94 | + `event` - **Type:** _Event_ - the original `mousemove` or `touchmove` event 95 | + `pointer` - **Type:** _MouseEvent_ or _Touch_ - the event object that has `.pageX` and `.pageY` 96 | + `draggieInstance` - **Type:** _Draggabilly_ - the Draggabilly instance 97 | 98 | ### dragEnd 99 | 100 | ```js 101 | .on( 'dragEnd', function( event, pointer, draggieInstance ) { //... 102 | ``` 103 | 104 | + `event` - **Type:** _Event_ - the original `mouseup` or `touchend` event 105 | + `pointer` - **Type:** _MouseEvent_ or _Touch_ - the event object that has `.pageX` and `.pageY` 106 | + `draggieInstance` - **Type:** _Draggabilly_ - the Draggabilly instance 107 | 108 | ## License 109 | 110 | Draggabilly is released under the [MIT License](http://desandro.mit-license.org/). Have at it. 111 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Draggabilly 9 | 10 | 77 | 78 | 79 | 80 | 81 |

Draggabilly

82 | 83 |
84 |
85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 | 93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /draggabilly.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Draggabilly v0.1.1 3 | * Make that shiz draggable 4 | * http://draggabilly.desandro.com 5 | */ 6 | 7 | ( function( window ) { 8 | 9 | 'use strict'; 10 | 11 | // dependencies 12 | var classie = window.classie; 13 | var EventEmitter = window.EventEmitter; 14 | var eventie = window.eventie; 15 | var getStyleProperty = window.getStyleProperty; 16 | var getSize = window.getSize; 17 | 18 | var document = window.document; 19 | 20 | // -------------------------- helpers -------------------------- // 21 | 22 | // extend objects 23 | function extend( a, b ) { 24 | for ( var prop in b ) { 25 | a[ prop ] = b[ prop ]; 26 | } 27 | return a; 28 | } 29 | 30 | // ----- get style ----- // 31 | 32 | var defView = document.defaultView; 33 | 34 | var getStyle = defView && defView.getComputedStyle ? 35 | function( elem ) { 36 | return defView.getComputedStyle( elem, null ); 37 | } : 38 | function( elem ) { 39 | return elem.currentStyle; 40 | }; 41 | 42 | 43 | // http://stackoverflow.com/a/384380/182183 44 | function isElement(o){ 45 | return ( 46 | typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2 47 | o && typeof o === "object" && o.nodeType === 1 && typeof o.nodeName==="string" 48 | ); 49 | } 50 | 51 | // -------------------------- requestAnimationFrame -------------------------- // 52 | 53 | // https://gist.github.com/1866474 54 | 55 | var lastTime = 0; 56 | var prefixes = 'webkit moz ms o'.split(' '); 57 | // get unprefixed rAF and cAF, if present 58 | var requestAnimationFrame = window.requestAnimationFrame; 59 | var cancelAnimationFrame = window.cancelAnimationFrame; 60 | // loop through vendor prefixes and get prefixed rAF and cAF 61 | var prefix; 62 | for( var i = 0; i < prefixes.length; i++ ) { 63 | if ( requestAnimationFrame && cancelAnimationFrame ) { 64 | break; 65 | } 66 | prefix = prefixes[i]; 67 | requestAnimationFrame = requestAnimationFrame || window[ prefix + 'RequestAnimationFrame' ]; 68 | cancelAnimationFrame = cancelAnimationFrame || window[ prefix + 'CancelAnimationFrame' ] || 69 | window[ prefix + 'CancelRequestAnimationFrame' ]; 70 | } 71 | 72 | // fallback to setTimeout and clearTimeout if either request/cancel is not supported 73 | if ( !requestAnimationFrame || !cancelAnimationFrame ) { 74 | requestAnimationFrame = function( callback ) { 75 | var currTime = new Date().getTime(); 76 | var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ); 77 | var id = window.setTimeout( function() { 78 | callback( currTime + timeToCall ); 79 | }, timeToCall ); 80 | lastTime = currTime + timeToCall; 81 | return id; 82 | }; 83 | 84 | cancelAnimationFrame = function( id ) { 85 | window.clearTimeout( id ); 86 | }; 87 | } 88 | 89 | // -------------------------- support -------------------------- // 90 | 91 | var transformProperty = getStyleProperty('transform'); 92 | // TODO fix quick & dirty check for 3D support 93 | var is3d = !!getStyleProperty('perspective'); 94 | 95 | // -------------------------- -------------------------- // 96 | 97 | function Draggabilly( element, options ) { 98 | this.element = element; 99 | 100 | this.options = extend( {}, this.options ); 101 | extend( this.options, options ); 102 | 103 | this._create(); 104 | 105 | } 106 | 107 | // inherit EventEmitter methods 108 | extend( Draggabilly.prototype, EventEmitter.prototype ); 109 | 110 | Draggabilly.prototype.options = { 111 | }; 112 | 113 | Draggabilly.prototype._create = function() { 114 | 115 | // properties 116 | this.position = {}; 117 | this._getPosition(); 118 | 119 | this.startPoint = { x: 0, y: 0 }; 120 | this.dragPoint = { x: 0, y: 0 }; 121 | 122 | this.startPosition = extend( {}, this.position ); 123 | 124 | // set relative positioning 125 | var style = getStyle( this.element ); 126 | if ( style.position !== 'relative' && style.position !== 'absolute' ) { 127 | this.element.style.position = 'relative'; 128 | } 129 | 130 | this.setHandles(); 131 | 132 | }; 133 | 134 | /** 135 | * set this.handles and bind start events to 'em 136 | */ 137 | Draggabilly.prototype.setHandles = function() { 138 | this.handles = this.options.handle ? 139 | this.element.querySelectorAll( this.options.handle ) : [ this.element ]; 140 | 141 | for ( var i=0, len = this.handles.length; i < len; i++ ) { 142 | var handle = this.handles[i]; 143 | // bind pointer start event 144 | // listen for both, for devices like Chrome Pixel 145 | // which has touch and mouse events 146 | eventie.bind( handle, 'mousedown', this ); 147 | eventie.bind( handle, 'touchstart', this ); 148 | } 149 | }; 150 | 151 | 152 | // get left/top position from style 153 | Draggabilly.prototype._getPosition = function() { 154 | // properties 155 | var style = getStyle( this.element ); 156 | 157 | var x = parseInt( style.left, 10 ); 158 | var y = parseInt( style.top, 10 ); 159 | 160 | // clean up 'auto' or other non-integer values 161 | this.position.x = isNaN( x ) ? 0 : x; 162 | this.position.y = isNaN( y ) ? 0 : y; 163 | 164 | this._addTransformPosition( style ); 165 | }; 166 | 167 | // add transform: translate( x, y ) to position 168 | Draggabilly.prototype._addTransformPosition = function( style ) { 169 | if ( !transformProperty ) { 170 | return; 171 | } 172 | var transform = style[ transformProperty ]; 173 | // bail out if value is 'none' 174 | if ( transform.indexOf('matrix') !== 0 ) { 175 | return; 176 | } 177 | // split matrix(1, 0, 0, 1, x, y) 178 | var matrixValues = transform.split(','); 179 | // translate X value is in 12th or 4th position 180 | var xIndex = transform.indexOf('matrix3d') === 0 ? 12 : 4; 181 | var translateX = parseInt( matrixValues[ xIndex ], 10 ); 182 | // translate Y value is in 13th or 5th position 183 | var translateY = parseInt( matrixValues[ xIndex + 1 ], 10 ); 184 | this.position.x += translateX; 185 | this.position.y += translateY; 186 | }; 187 | 188 | // -------------------------- events -------------------------- // 189 | 190 | // trigger handler methods for events 191 | Draggabilly.prototype.handleEvent = function( event ) { 192 | var method = 'on' + event.type; 193 | if ( this[ method ] ) { 194 | this[ method ]( event ); 195 | } 196 | }; 197 | 198 | // returns the touch that we're keeping track of 199 | Draggabilly.prototype.getTouch = function( touches ) { 200 | for ( var i=0, len = touches.length; i < len; i++ ) { 201 | var touch = touches[i]; 202 | if ( touch.identifier === this.pointerIdentifier ) { 203 | return touch; 204 | } 205 | } 206 | }; 207 | 208 | // ----- start event ----- // 209 | 210 | Draggabilly.prototype.onmousedown = function( event ) { 211 | this.dragStart( event, event ); 212 | }; 213 | 214 | Draggabilly.prototype.ontouchstart = function( event ) { 215 | // disregard additional touches 216 | if ( this.pointerIdentifier ) { 217 | return; 218 | } 219 | 220 | this.dragStart( event, event.changedTouches[0] ); 221 | }; 222 | 223 | function setPointerPoint( point, pointer ) { 224 | point.x = pointer.pageX !== undefined ? pointer.pageX : pointer.clientX; 225 | point.y = pointer.pageY !== undefined ? pointer.pageY : pointer.clientY; 226 | } 227 | 228 | /** 229 | * drag start 230 | * @param {Event} event 231 | * @param {Event or Touch} pointer 232 | */ 233 | Draggabilly.prototype.dragStart = function( event, pointer ) { 234 | if ( event.preventDefault ) { 235 | event.preventDefault(); 236 | } else { 237 | event.returnValue = false; 238 | } 239 | 240 | this.pointerIdentifier = pointer.identifier || 1; 241 | 242 | this._getPosition(); 243 | 244 | this.measureContainment(); 245 | 246 | // point where drag began 247 | setPointerPoint( this.startPoint, pointer ); 248 | // position _when_ drag began 249 | this.startPosition.x = this.position.x; 250 | this.startPosition.y = this.position.y; 251 | 252 | // reset left/top style 253 | this.setLeftTop(); 254 | 255 | this.dragPoint.x = 0; 256 | this.dragPoint.y = 0; 257 | 258 | // add events 259 | var binder = event.preventDefault ? window : document; 260 | 261 | var isTouch = event.type === 'touchstart'; 262 | this.pointerMoveEvent = isTouch ? 'touchmove' : 'mousemove'; 263 | this.pointerEndEvent = isTouch ? 'touchend' : 'mouseup'; 264 | 265 | // bind move and and events 266 | eventie.bind( binder, this.pointerMoveEvent, this ); 267 | eventie.bind( binder, this.pointerEndEvent, this ); 268 | 269 | classie.add( this.element, 'is-dragging' ); 270 | 271 | // reset isDragging flag 272 | this.isDragging = true; 273 | 274 | this.emitEvent( 'dragStart', [ event, pointer, this ] ); 275 | 276 | // start animation 277 | this.animate(); 278 | }; 279 | 280 | 281 | Draggabilly.prototype.measureContainment = function() { 282 | var containment = this.options.containment; 283 | if ( !containment ) { 284 | return; 285 | } 286 | 287 | this.size = getSize( this.element ); 288 | var elemRect = this.element.getBoundingClientRect(); 289 | 290 | // use element if element 291 | var container = isElement( containment ) ? containment : 292 | // fallback to querySelector if string 293 | typeof containment === 'string' ? document.querySelector( containment ) : 294 | // otherwise just `true`, use the parent 295 | this.element.parentNode; 296 | 297 | this.containerSize = getSize( container ); 298 | var containerRect = container.getBoundingClientRect(); 299 | 300 | this.relativeStartPosition = { 301 | x: elemRect.left - containerRect.left, 302 | y: elemRect.top - containerRect.top 303 | }; 304 | // console.log( this.relativeStartPosition.x, this.relativeStartPosition.y ); 305 | }; 306 | 307 | // ----- move event ----- // 308 | 309 | Draggabilly.prototype.onmousemove = function( event ) { 310 | this.dragMove( event, event ); 311 | }; 312 | 313 | Draggabilly.prototype.ontouchmove = function( event ) { 314 | var touch = this.getTouch( event.changedTouches ); 315 | if ( touch ) { 316 | this.dragMove( event, touch ); 317 | } 318 | }; 319 | 320 | /** 321 | * drag move 322 | * @param {Event} event 323 | * @param {Event or Touch} pointer 324 | */ 325 | Draggabilly.prototype.dragMove = function( event, pointer ) { 326 | 327 | setPointerPoint( this.dragPoint, pointer ); 328 | this.dragPoint.x -= this.startPoint.x; 329 | this.dragPoint.y -= this.startPoint.y; 330 | 331 | if ( this.options.containment ) { 332 | var relX = this.relativeStartPosition.x; 333 | var relY = this.relativeStartPosition.y; 334 | this.dragPoint.x = Math.max( this.dragPoint.x, -relX ); 335 | this.dragPoint.y = Math.max( this.dragPoint.y, -relY ); 336 | this.dragPoint.x = Math.min( this.dragPoint.x, this.containerSize.width - relX - this.size.width ); 337 | this.dragPoint.y = Math.min( this.dragPoint.y, this.containerSize.height - relY - this.size.height ); 338 | } 339 | 340 | this.position.x = this.startPosition.x + this.dragPoint.x; 341 | this.position.y = this.startPosition.y + this.dragPoint.y; 342 | 343 | this.emitEvent( 'dragMove', [ event, pointer, this ] ); 344 | }; 345 | 346 | 347 | // ----- end event ----- // 348 | 349 | Draggabilly.prototype.onmouseup = function( event ) { 350 | this.dragEnd( event, event ); 351 | }; 352 | 353 | Draggabilly.prototype.ontouchend = function( event ) { 354 | var touch = this.getTouch( event.changedTouches ); 355 | if ( touch ) { 356 | this.dragEnd( event, touch ); 357 | } 358 | }; 359 | 360 | /** 361 | * drag end 362 | * @param {Event} event 363 | * @param {Event or Touch} pointer 364 | */ 365 | Draggabilly.prototype.dragEnd = function( event, pointer ) { 366 | this.isDragging = false; 367 | 368 | delete this.pointerIdentifier; 369 | 370 | // use top left position when complete 371 | if ( transformProperty ) { 372 | this.element.style[ transformProperty ] = ''; 373 | this.setLeftTop(); 374 | } 375 | 376 | // remove events 377 | var binder = event.preventDefault ? window : document; 378 | 379 | eventie.unbind( binder, this.pointerMoveEvent, this ); 380 | eventie.unbind( binder, this.pointerEndEvent, this ); 381 | delete this.pointerMoveEvent; 382 | delete this.pointerEndEvent; 383 | 384 | classie.remove( this.element, 'is-dragging' ); 385 | 386 | this.emitEvent( 'dragEnd', [ event, pointer, this ] ); 387 | 388 | }; 389 | 390 | // -------------------------- animation -------------------------- // 391 | 392 | Draggabilly.prototype.animate = function() { 393 | // only render and animate if dragging 394 | if ( !this.isDragging ) { 395 | return; 396 | } 397 | 398 | this.positionDrag(); 399 | 400 | var _this = this; 401 | requestAnimationFrame( function animateFrame() { 402 | _this.animate(); 403 | }); 404 | 405 | }; 406 | 407 | // transform translate function 408 | var translate = is3d ? 409 | function( x, y ) { 410 | return 'translate3d( ' + x + 'px, ' + y + 'px, 0)'; 411 | } : 412 | function( x, y ) { 413 | return 'translate( ' + x + 'px, ' + y + 'px)'; 414 | }; 415 | 416 | // left/top positioning 417 | Draggabilly.prototype.setLeftTop = function() { 418 | this.element.style.left = this.position.x + 'px'; 419 | this.element.style.top = this.position.y + 'px'; 420 | }; 421 | 422 | Draggabilly.prototype.positionDrag = transformProperty ? 423 | function() { 424 | // position with transform 425 | this.element.style[ transformProperty ] = translate( this.dragPoint.x, this.dragPoint.y ); 426 | } : Draggabilly.prototype.setLeftTop; 427 | 428 | // -------------------------- -------------------------- // 429 | 430 | 431 | // publicize 432 | window.Draggabilly = Draggabilly; 433 | 434 | })( window ); 435 | --------------------------------------------------------------------------------