├── .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 |
86 |
87 |
88 |
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 |
--------------------------------------------------------------------------------