Skip to content

Instantly share code, notes, and snippets.

@amritk

amritk/dragdrop.js

Last active Apr 19, 2017
Embed
What would you like to do?
Trying to get this dragdrop from this codrops article working on the latest version of dragabilly
/**
* dragdrop.js
* http://www.codrops.com
*
* Licensed under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
*
* Copyright 2014, Codrops
* http://www.codrops.com
*
* From:
* http://tympanus.net/codrops/2014/11/11/drag-and-drop-interaction-ideas/
*
* Check #todo for code that I just commented for now
*
*/
;( function() {
'use strict';
/*************************************************************/
/******************* Some helper functions *******************/
/*************************************************************/
var body = document.body, docElem = window.document.documentElement,
transEndEventNames = { 'WebkitTransition': 'webkitTransitionEnd', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'msTransition': 'MSTransitionEnd', 'transition': 'transitionend' },
transEndEventName = transEndEventNames[ Modernizr.prefixed( 'transition' ) ],
support = { transitions : Modernizr.csstransitions };
// https://remysharp.com/2010/07/21/throttling-function-calls
function throttle(fn, threshhold, scope) {
threshhold || (threshhold = 250);
var last,
deferTimer;
return function () {
var context = scope || this;
var now = +new Date,
args = arguments;
if (last && now < last + threshhold) {
// hold on to it
clearTimeout(deferTimer);
deferTimer = setTimeout(function () {
last = now;
fn.apply(context, args);
}, threshhold);
} else {
last = now;
fn.apply(context, args);
}
};
}
// from http://responsejs.com/labs/dimensions/
function getViewportW() {
var client = docElem['clientWidth'], inner = window['innerWidth'];
return client < inner ? inner : client;
}
function getViewportH() {
var client = docElem['clientHeight'], inner = window['innerHeight'];
return client < inner ? inner : client;
}
function scrollX() { return window.pageXOffset || docElem.scrollLeft; }
function scrollY() { return window.pageYOffset || docElem.scrollTop; }
// gets the offset of an element relative to the document
function getOffset( el ) {
var offset = el.getBoundingClientRect();
return { top : offset.top + scrollY(), left : offset.left + scrollX() }
}
function setTransformStyle( el, tval ) { el.style.transform = tval; }
function onEndTransition( el, callback ) {
var onEndCallbackFn = function( ev ) {
if( support.transitions ) {
this.removeEventListener( transEndEventName, onEndCallbackFn );
}
if( callback && typeof callback === 'function' ) { callback.call(); }
};
if( support.transitions ) {
el.addEventListener( transEndEventName, onEndCallbackFn );
}
else {
onEndCallbackFn();
}
}
function extend( a, b ) {
for( var key in b ) {
if( b.hasOwnProperty( key ) ) {
a[key] = b[key];
}
}
return a;
}
/*************************************************************/
/************************ Drag & Drop ************************/
/*************************************************************/
var is3d = !!getStyleProperty( 'perspective' )
/***************/
/** Droppable **/
/***************/
function Droppable( droppableEl, options ) {
this.el = droppableEl;
this.options = extend( {}, this.options );
extend( this.options, options );
}
Droppable.prototype.options = {
onDrop : function(instance, draggableEl) { return false; }
}
// based on http://stackoverflow.com/a/2752387 : checks if the droppable element is ready to collect the draggable: the draggable element must intersect the droppable in half of its width or height.
Droppable.prototype.isDroppable = function( draggableEl ) {
var offset1 = getOffset( draggableEl ), width1 = draggableEl.offsetWidth, height1 = draggableEl.offsetHeight,
offset2 = getOffset( this.el ), width2 = this.el.offsetWidth, height2 = this.el.offsetHeight;
return !(offset2.left > offset1.left + width1 - width1/2 ||
offset2.left + width2 < offset1.left + width1/2 ||
offset2.top > offset1.top + height1 - height1/2 ||
offset2.top + height2 < offset1.top + height1/2 );
}
// highlight the droppable if it's ready to collect the draggable
Droppable.prototype.highlight = function( draggableEl ) {
if( this.isDroppable( draggableEl ) )
classie.add( this.el, 'highlight' );
else
classie.remove( this.el, 'highlight' );
}
// accepts a draggable element...
Droppable.prototype.collect = function( draggableEl ) {
// remove highlight class from droppable element
classie.remove( this.el, 'highlight' );
this.options.onDrop( this, draggableEl );
}
/***************/
/** Draggable **/
/***************/
function Draggable( draggableEl, droppables, options ) {
this.el = draggableEl;
this.options = extend( {}, this.options );
extend( this.options, options );
this.droppables = droppables;
this.scrollableEl = this.options.scrollable === window ? window : document.querySelector( this.options.scrollable );
this.scrollIncrement = 0;
if( this.options.helper ) {
this.offset = { left : getOffset( this.el ).left, top : getOffset( this.el ).top };
}
this.draggie = new Draggabilly( this.el, this.options.draggabilly );
this.initEvents();
}
Draggable.prototype.options = {
// auto scroll when dragging
scroll: false,
// element to scroll
scrollable : window,
// scroll speed (px)
scrollSpeed : 20,
// minimum distance to the scrollable element edges to trigger the scroll
scrollSensitivity : 10,
// draggabilly options
draggabilly : {},
// if the item should animate back to its original position
animBack : true,
// clone the draggable and insert it on the same position while dragging the original one
helper : false,
// callbacks
onStart : function() { return false; },
onDrag : function() { return false; },
onEnd : function(wasDropped) { return false; }
}
Draggable.prototype.initEvents = function() {
this.draggie.on( 'dragStart', this.onDragStart.bind(this) );
this.draggie.on( 'dragMove', throttle( this.onDragMove.bind(this), 5 ) );
this.draggie.on( 'dragEnd', this.onDragEnd.bind(this) );
}
Draggable.prototype.onDragStart = function( event, pointer ) {
//callback
this.options.onStart();
// save left & top
this.position = { left : 0, top : 0 };
// this.displacement = { left : 0, top : 0 };
// create helper
if( this.options.helper ) {
this.createHelper( pointer.target );
}
// add class is-active to the draggable element (control the draggable z-index)
classie.add( pointer.target, 'is-active' );
// highlight droppable elements if draggables intersect
this.highlightDroppables();
}
Draggable.prototype.onDragMove = function( event, pointer, moveVector ) {
//callback
this.options.onDrag();
// Keep track of displacement
// this.displacement = { left : moveVector.x, top : moveVector.y };
// scroll page if at viewport's edge
if( this.options.scroll ) {
this.scrollPage( pointer.target );
}
// highlight droppable elements if draggables intersect
this.highlightDroppables();
}
Draggable.prototype.onDragEnd = function( event, pointer ) {
// if( this.options.helper ) {
// instance.element.style.left = instance.position.x + this.position.left + 'px';
// instance.element.style.top =instance.position.y + this.position.top + 'px';
// }
if( this.options.scroll ) {
// reset this.scrollIncrement
this.scrollIncrement = 0;
}
// if the draggable && droppable elements intersect then "drop" and move back the draggable
var dropped = false;
for( var i = 0, len = this.droppables.length; i < len; ++i ) {
var droppableEl = this.droppables[i];
if( droppableEl.isDroppable( pointer.target ) ) {
dropped = true;
droppableEl.collect( pointer.target );
}
}
//callback
this.options.onEnd( dropped );
if( dropped ) {
// add class is-dropped to draggable ( controls how the draggable appears again at its original position)
classie.add( pointer.target, 'is-dropped' );
// after a timeout remove that class to trigger the transition
setTimeout( function() {
classie.add( pointer.target, 'is-complete' );
onEndTransition( pointer.target, function() {
classie.remove( pointer.target, 'is-complete' );
classie.remove( pointer.target, 'is-dropped' );
} );
}, 25 );
}
// move back the draggable element (with or without a transition)
this.moveBack( !dropped );
}
Draggable.prototype.highlightDroppables = function( el ) {
for( var i = 0, len = this.droppables.length; i < len; ++i ) {
this.droppables[i].highlight( this.el );
}
}
Draggable.prototype.createHelper = function() {
// clone the original item (same position)
var clone = this.el.cloneNode( true );
// because the original element started the dragging, we need to remove the is-dragging class
classie.remove( clone, 'is-dragging' );
this.el.parentNode.replaceChild( clone, this.el );
// initialize Draggabilly on the clone..
var draggable = new Draggable( clone, this.droppables, this.options );
// the original item will be absolute on the page - need to set correct position values..
classie.add( this.el, 'helper' );
this.el.style.left = draggable.offset.left + 'px';
this.el.style.top = draggable.offset.top + 'px';
// save new left/top
this.position.left = draggable.offset.left;
this.position.top = draggable.offset.top;
body.appendChild( this.el );
}
// move back the draggable to its original position
Draggable.prototype.moveBack = function( withAnimation ) {
var anim = this.options.animBack && withAnimation;
// add animate class (where the transition is defined)
if( anim ) {
classie.add( this.el, 'animate' );
}
// reset translation value
setTransformStyle( this.el, is3d ? 'translate3d(0,0,0)' : 'translate(0,0)' );
// apply original left/top
this.el.style.left = this.position.left + 'px';
this.el.style.top = this.position.top + 'px';
// remove class animate (transition) and is-active to the draggable element (z-index)
var callbackFn = function() {
if( anim ) {
classie.remove( this.el, 'animate' );
}
classie.remove( this.el, 'is-active' );
if( this.options.helper ) {
body.removeChild( this.el );
}
}.bind( this );
if( anim ) {
onEndTransition( this.el, callbackFn );
}
else {
callbackFn();
}
}
// check if element is outside of the viewport. TODO: check also for right and left sides
Draggable.prototype.outViewport = function() {
var scrollSensitivity = Math.abs( this.options.scrollSensitivity ) || 0,
scrolled = scrollY(),
viewed = scrolled + getViewportH(),
elT = getOffset( this.el ).top,
elHalfPos = elT + this.el.offsetHeight/2,
belowViewport = elT + this.el.offsetHeight + scrollSensitivity > viewed,
aboveViewport = elT - scrollSensitivity < scrolled;
if( belowViewport ) this.scrolldir = 'down';
else if( aboveViewport ) this.scrolldir = 'up';
return belowViewport || aboveViewport;
}
// force the scroll on the page when dragging..
Draggable.prototype.scrollPage = function() {
// check if draggable is "outside" of the viewport
if( this.outViewport( this.el ) ) {
this.doScroll();
}
else {
// reset this.scrollIncrement
this.scrollIncrement = 0;
}
}
// Destroy draggabilly
Draggable.prototype.destroy = function() {
this.draggie.destroy();
}
// just considering scroll up and down
// this.scrollIncrement is used to accelerate the scrolling. But mainly it's used to avoid the draggable flickering due to the throttle when dragging
// todo: scroll right and left
// todo: draggabilly multi touch scroll: see https://github.com/desandro/draggabilly/issues/1
Draggable.prototype.doScroll = function() {
var speed = this.options.scrollSpeed || 20;
this.scrollIncrement++;
var val = this.scrollIncrement < speed ? this.scrollIncrement : speed;
this.scrollableEl === window ?
this.scrollableEl.scrollBy( 0, this.scrolldir === 'up' ? val * -1 : val ) :
this.scrollableEl.scrollTop += this.scrolldir === 'up' ? val * -1 : val;
}
window.Droppable = Droppable;
window.Draggable = Draggable;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment