/**
@namespace effect
@class Drag
@extends effect.Effect
@description Class for dragging DOM elements.
@constructor
@param {Object} config
@example
<style type="text/css">
.ludo-shim {
border: 15px solid #AAA;
background-color: #DEF;
margin: 5;
opacity: .5;
border-radius: 5px;
}
.draggable{
width:150px;
z-index:1000;
height:150px;
border-radius:5px;
border:1px solid #555;
background-color:#DEF
}
</style>
<div id="draggable" class="draggable">
I am draggable
</div>
<script type="text/javascript">
var d = new ludo.effect.Drag({
useShim:true,
listeners:{
endDrag:function(dragged, dragEffect){
dragEffect.getEl().setStyles({
left : dragEffect.getX(),
top: dragEffect.getY()
});
},
drag:function(pos, dragEffect){
dragEffect.setShimText(dragEffect.getX() + 'x' + dragEffect.getY());
}
}
});
d.add('draggable'); // "draggable" is the id of the div
</script>
*/
ludo.effect.Drag = new Class({
Extends:ludo.effect.Effect,
/**
* Reference to drag handle (Optional). If not set, "el" will be used
* @config handle
* @type Object|String
* @default undefined
*/
handle:undefined,
/**
* Reference to DOM element to be dragged
* @config el
* @type Object|String
* @default undefined
*/
el:undefined,
/**
* Minimum x position
* @config minX
* @type {Number}
* @default undefined
*/
minX:undefined,
/**
* Minimum y position
* @config minY
* @type {Number}
* @default undefined
*/
minY:undefined,
/**
* Maximum x position
* @config maxX
* @type {Number}
* @default undefined
*/
maxX:undefined,
/**
* config y position
* @attribute maxY
* @type {Number}
* @default undefined
*/
maxY:undefined,
/**
* minPos and maxPos can be used instead of minX,maxX,minY and maxY if
* you only accept dragging along x-axis or y-axis
* @config {Number} minPos
* @default undefined
*/
minPos:undefined,
/**
* @config maxPos
* @type {Number}
* @default undefined
*/
maxPos:undefined,
/**
* Accept dragging in these directions
* @config dragX
* @type String
* @default XY
*/
directions:'XY',
/**
* Unit used while dragging
* @config unit, example : "px", "%"
* @default px
*/
unit:'px',
dragProcess:{
active:false
},
coordinatesToDrag:undefined,
/**
* Delay in seconds from mouse down to start drag. If mouse is released within this interval,
* the drag will be cancelled.
* @config delay
* @type {Number}
* @default 0
*/
delay:0,
inDelayMode:false,
els:{},
/**
* True to use dynamically created shim while dragging. When true,
* the original DOM element will not be dragged.
* @config useShim
* @type {Boolean}
* @default false
*/
useShim:false,
/**
* True to automatically hide shim after drag is finished
* @config autohideShim
* @type {Boolean}
* @default true
*/
autoHideShim:true,
/**
CSS classes to add to shim
@config shimCls
@type Array
@default undefined
@example
shimCls:['myShim','myShim-2']
which will results in this shim :
@example
<div class="ludo-shim myShim myShim-2">
*/
shimCls:undefined,
/**
* While dragging, always show dragged element this amount of pixels below mouse cursor.
* @config mouseYOffset
* @type {Number} pixels
* @default undefined
*/
mouseYOffset:undefined,
/**
* While dragging, always show dragged element this amount of pixels right of mouse cursor.
* @config mouseXOffset
* @type {Number} pixels
* @default undefined
*/
mouseXOffset:undefined,
fireEffectEvents:true,
ludoConfig:function (config) {
this.parent(config);
if (config.el !== undefined) {
this.add({
el:config.el,
handle:config.handle
});
}
this.setConfigParams(config, ['useShim','autoHideShim','directions','delay','minX','maxX','minY','maxY',
'minPos','maxPos','unit','shimCls','mouseYOffset','mouseXOffset','fireEffectEvents']);
},
ludoEvents:function () {
this.parent();
this.getEventEl().addEvent(ludo.util.getDragMoveEvent(), this.drag.bind(this));
this.getEventEl().addEvent(ludo.util.getDragEndEvent(), this.endDrag.bind(this));
if (this.useShim) {
this.addEvent('start', this.showShim.bind(this));
if(this.autoHideShim)this.addEvent('end', this.hideShim.bind(this));
}
},
/**
Add draggable object
@method add
@param {effect.DraggableNode|String|HTMLDivElement} node
@return {effect.DraggableNode}
@example
dragObject.add({
el: 'myDiv',
handle : 'myHandle'
});
handle is optional.
@example
dragObject.add('idOfMyDiv');
You can also add custom properties:
@example
dragobject.add({
id: "myReference',
el: 'myDiv',
column: 'city'
});
...
...
dragobject.addEvent('before', beforeDrag);
...
...
function beforeDrag(dragged){
console.log(dragged.el);
console.log(dragged.column);
}
*/
add:function (node) {
node = this.getValidNode(node);
var el = document.id(node.el);
this.setPositioning(el);
var handle = node.handle ? document.id(node.handle) : el;
handle.id = handle.id || 'ludo-' + String.uniqueID();
ludo.dom.addClass(handle, 'ludo-drag');
handle.addEvent(ludo.util.getDragStartEvent(), this.startDrag.bind(this));
handle.setAttribute('forId', node.id);
this.els[node.id] = Object.merge(node, {
el:document.id(el),
handle:handle
});
return this.els[node.id];
},
/**
* Remove node
* @method remove
* @param {String} id
* @return {Boolean} success
*/
remove:function(id){
if(this.els[id]!==undefined){
var el = document.id(this.els[id].handle);
el.removeEvent(ludo.util.getDragStartEvent(), this.startDrag.bind(this));
this.els[id] = undefined;
return true;
}
return false;
},
removeAll:function(){
var keys = Object.keys(this.els);
for(var i=0;i<keys.length;i++){
this.remove(keys[i]);
}
this.els = {};
},
getValidNode:function(node){
if (!this.isElConfigObject(node)) {
node = {
el:document.id(node)
};
}
if(typeof node.el === 'string'){
node.el = document.id(node.el);
}
node.id = node.id || node.el.id || 'ludo-' + String.uniqueID();
if (!node.el.id)node.el.id = node.id;
node.el.setAttribute('forId', node.id);
return node;
},
isElConfigObject:function (config) {
return config.el !== undefined || config.handle !== undefined;
},
setPositioning:function(el){
if (!this.useShim){
el.style.position = 'absolute';
}else{
var pos = el.getStyle('position');
if(!pos || (pos!='relative' && pos!='absolute')){
el.style.position = 'relative';
}
}
},
getById:function(id){
return this.els[id];
},
getIdByEvent:function (e) {
var el = e.target;
if (!el.hasClass('ludo-drag')) {
el = el.getParent('.ludo-drag');
}
return el.getProperty('forId');
},
/**
* Returns reference to dragged object, i.e. object added in constructor or
* by use of add method
* @method getDragged
* @return {Object}
*/
getDragged:function(){
return this.els[this.dragProcess.dragged];
},
/**
* Returns reference to draggable DOM node
* @method getEl
* @return {Object} DOMNode
*/
getEl:function () {
return this.els[this.dragProcess.dragged].el;
},
getShimOrEl:function () {
return this.useShim ? this.getShim() : this.getEl();
},
getSizeOf:function(el){
return el.getSize !== undefined ? el.getSize() : { x: 0, y: 0 };
},
getPositionOf:function(el){
return el.getPosition();
},
setDragCoordinates:function(){
this.coordinatesToDrag = {
x : 'x', y:'y'
};
},
startDrag:function (e) {
var id = this.getIdByEvent(e);
var el = this.getById(id).el;
var size = this.getSizeOf(el);
var pos;
if(this.useShim){
pos = el.getPosition();
}else{
var parent = this.getPositionedParent(el);
pos = parent ? el.getPosition(parent) : this.getPositionOf(el);
}
var x = pos.x;
var y = pos.y;
this.dragProcess = {
active:true,
dragged:id,
currentX:x,
currentY:y,
elX:x,
elY:y,
width:size.x,
height:size.y,
mouseX:e.page.x,
mouseY:e.page.y
};
this.dragProcess.el = this.getShimOrEl();
/**
* Event fired before drag
* @event {effect.DraggableNode}
* @param {Object} object to be dragged
* @param {ludo.effect.Drag} component
* @param {Object} pos(x and y)
*/
this.fireEvent('before', [this.els[id], this, {x:x,y:y}]);
if(!this.isActive()){
return undefined;
}
this.dragProcess.minX = this.getMinX();
this.dragProcess.maxX = this.getMaxX();
this.dragProcess.minY = this.getMinY();
this.dragProcess.maxY = this.getMaxY();
this.dragProcess.dragX = this.canDragAlongX();
this.dragProcess.dragY = this.canDragAlongY();
if (this.delay) {
this.setActiveAfterDelay();
} else {
/**
* Event fired before dragging
* @event start
* @param {effect.DraggableNode} object to be dragged.
* @param {ludo.effect.Drag} component
* @param {Object} pos(x and y)
*/
this.fireEvent('start', [this.els[id], this, {x:x,y:y}]);
if(this.fireEffectEvents)ludo.EffectObject.start();
}
return false;
},
getPositionedParent:function(el){
var parent = el.parentNode;
while(parent){
var pos = parent.getStyle('position');
if (pos === 'relative' || pos === 'absolute')return parent;
parent = parent.getParent();
}
return undefined;
},
/**
Cancel drag. This method is designed to be called from an event handler
attached to the "beforeDrag" event.
@method cancelDrag
@example
// Here, dd is a {{#crossLink "effect.Drag"}}{{/crossLink}} object
dd.addEvent('before', function(draggable, dd, pos){
if(pos.x > 1000 || pos.y > 500){
dd.cancelDrag();
}
});
In this example, dragging will be cancelled when the x position of the mouse
is greater than 1000 or if the y position is greater than 500. Another more
useful example is this:
@example
dd.addEvent('before', function(draggable, dd){
if(!this.isDraggable(draggable)){
dd.cancelDrag()
}
});
Here, we assume that we have an isDraggable method which returns true or false
for whether the given node is draggable or not. "draggable" in this example
is one of the {{#crossLink "effect.DraggableNode"}}{{/crossLink}} objects added
using the {{#crossLink "effect.Drag/add"}}{{/crossLink}} method.
*/
cancelDrag:function () {
this.dragProcess.active = false;
this.dragProcess.el = undefined;
if(this.fireEffectEvents)ludo.EffectObject.end();
},
getShimFor:function (el) {
return el;
},
setActiveAfterDelay:function () {
this.inDelayMode = true;
this.dragProcess.active = false;
this.startIfMouseNotReleased.delay(this.delay * 1000, this);
},
startIfMouseNotReleased:function () {
if (this.inDelayMode) {
this.dragProcess.active = true;
this.inDelayMode = false;
this.fireEvent('start', [this.getDragged(), this, {x:this.getX(),y:this.getY()}]);
ludo.EffectObject.start();
}
},
drag:function (e) {
if (this.dragProcess.active && this.dragProcess.el) {
var pos = {
x:undefined,
y:undefined
};
if (this.dragProcess.dragX) {
pos.x = this.getXDrag(e);
}
if (this.dragProcess.dragY) {
pos.y = this.getYDrag(e);
}
this.move(pos);
/**
* Event fired while dragging. Sends position, example {x:100,y:50}
* and reference to effect.Drag as arguments
* @event drag
* @param {Object} x and y
* @param {effect.Drag} this
*/
this.fireEvent('drag', [pos, this.els[this.dragProcess.dragged], this]);
if (ludo.util.isTabletOrMobile())return false;
}
return undefined;
},
move:function (pos) {
if (pos.x !== undefined) {
this.dragProcess.currentX = pos.x;
this.dragProcess.el.style.left = pos.x + this.unit;
}
if (pos.y !== undefined) {
this.dragProcess.currentY = pos.y;
this.dragProcess.el.style.top = pos.y + this.unit;
}
},
/**
* Return current x pos
* @method getX
* @return {Number} x
*/
getX:function(){
return this.dragProcess.currentX;
},
/**
* Return current y pos
* @method getY
* @return {Number} y
*/
getY:function(){
return this.dragProcess.currentY;
},
getXDrag:function (e) {
var posX;
if(this.mouseXOffset){
posX = e.page.x + this.mouseXOffset;
}else{
posX = e.page.x - this.dragProcess.mouseX + this.dragProcess.elX;
}
if (posX < this.dragProcess.minX) {
posX = this.dragProcess.minX;
}
if (posX > this.dragProcess.maxX) {
posX = this.dragProcess.maxX;
}
return posX;
},
getYDrag:function (e) {
var posY;
if(this.mouseYOffset){
posY = e.page.y + this.mouseYOffset;
}else{
posY = e.page.y - this.dragProcess.mouseY + this.dragProcess.elY;
}
if (posY < this.dragProcess.minY) {
posY = this.dragProcess.minY;
}
if (posY > this.dragProcess.maxY) {
posY = this.dragProcess.maxY;
}
return posY;
},
endDrag:function () {
if (this.dragProcess.active) {
this.cancelDrag();
/**
* Event fired on drag end
* @event end
* @param {effect.DraggableNode} dragged
* @param {ludo.effect.Drag} this
* @param {Object} x and y
*/
this.fireEvent('end', [
this.getDragged(),
this,
{
x:this.getX(),
y:this.getY()
}
]);
}
if (this.inDelayMode)this.inDelayMode = false;
},
/**
* Set new max X pos
* @method setMaxX
* @param {Number} x
*/
setMaxX:function (x) {
this.maxX = x;
},
/**
* Set new min X pos
* @method setMinX
* @param {Number} x
*/
setMinX:function (x) {
this.minX = x;
},
/**
* Set new min Y pos
* @method setMinY
* @param {Number} y
*/
setMinY:function (y) {
this.minY = y;
},
/**
* Set new max Y pos
* @method setMaxY
* @param {Number} y
*/
setMaxY:function (y) {
this.maxY = y;
},
/**
* Set new min pos
* @method setMinPos
* @param {Number} pos
*/
setMinPos:function (pos) {
this.minPos = pos;
},
/**
* Set new max pos
* @method setMaxPos
* @param {Number} pos
*/
setMaxPos:function (pos) {
this.maxPos = pos;
},
getMaxX:function () {
return this.getMaxPos('maxX');
},
getMaxY:function () {
return this.getMaxPos('maxY');
},
getMaxPos:function(key){
var max = this.getConfigProperty(key);
return max !== undefined ? max : this.maxPos !== undefined ? this.maxPos : 100000;
},
getMinX:function () {
var minX = this.getConfigProperty('minX');
return minX !== undefined ? minX : this.minPos;
},
getMinY:function () {
var dragged = this.getDragged();
return dragged && dragged.minY!==undefined ? dragged.minY : this.minY!==undefined ? this.minY : this.minPos;
},
/**
* Return amount dragged in x direction
* @method getDraggedX
* @return {Number} x
*/
getDraggedX:function(){
return this.getX() - this.dragProcess.elX;
},
/**
* Return amount dragged in y direction
* @method getDraggedY
* @return {Number} y
*/
getDraggedY:function(){
return this.getY() - this.dragProcess.elY;
},
canDragAlongX:function () {
return this.getConfigProperty('directions').indexOf('X') >= 0;
},
canDragAlongY:function () {
return this.getConfigProperty('directions').indexOf('Y') >= 0;
},
getConfigProperty:function(property){
var dragged = this.getDragged();
return dragged && dragged[property] !== undefined ? dragged[property] : this[property];
},
/**
* Returns width of dragged element
* @method getHeight
* @return {Number}
*/
getWidth:function () {
return this.dragProcess.width;
},
/**
* Returns height of dragged element
* @method getHeight
* @return {Number}
*/
getHeight:function () {
return this.dragProcess.height;
},
/**
* Returns current left position of dragged
* @method getLeft
* @return {Number}
*/
getLeft:function () {
return this.dragProcess.currentX;
},
/**
* Returns current top/y position of dragged.
* @method getTop
* @return {Number}
*/
getTop:function () {
return this.dragProcess.currentY;
},
/**
* Returns reference to DOM element of shim
* @method getShim
* @return {HTMLDivElement} shim
*/
getShim:function () {
if (this.shim === undefined) {
this.shim = new Element('div');
ludo.dom.addClass(this.shim, 'ludo-shim');
this.shim.setStyles({
position:'absolute',
'z-index':50000,
display:'none'
});
document.body.adopt(this.shim);
if (this.shimCls) {
for (var i = 0; i < this.shimCls.length; i++) {
this.shim.addClass(this.shimCls[i]);
}
}
/**
* Event fired when shim is created
* @event createShim
* @param {HTMLDivElement} shim
*/
this.fireEvent('createShim', this.shim);
}
return this.shim;
},
/**
* Show shim
* @method showShim
*/
showShim:function () {
this.getShim().setStyles({
display:'',
left:this.getShimX(),
top:this.getShimY(),
width:this.getWidth() + this.getShimWidthDiff(),
height:this.getHeight() + this.getShimHeightDiff()
});
this.fireEvent('showShim', [this.getShim(), this]);
},
getShimY:function(){
if(this.mouseYOffset){
return this.dragProcess.mouseY + this.mouseYOffset;
}else{
return this.getTop() + ludo.dom.getMH(this.getEl()) - ludo.dom.getMW(this.shim);
}
},
getShimX:function(){
if(this.mouseXOffset){
return this.dragProcess.mouseX + this.mouseXOffset;
}else{
return this.getLeft() + ludo.dom.getMW(this.getEl()) - ludo.dom.getMW(this.shim);
}
},
getShimWidthDiff:function(){
return ludo.dom.getMW(this.getEl()) - ludo.dom.getMBPW(this.shim);
},
getShimHeightDiff:function(){
return ludo.dom.getMH(this.getEl()) - ludo.dom.getMBPH(this.shim);
},
/**
* Hide shim
* @method hideShim
*/
hideShim:function () {
this.getShim().style.display = 'none';
},
/**
* Set text content of shim
* @method setShimText
* @param {String} text
*/
setShimText:function (text) {
this.getShim().set('html', text);
},
/**
* Fly/Slide dragged element back to it's original position
* @method flyBack
*/
flyBack:function (duration) {
this.fly({
el: this.getShimOrEl(),
duration: duration,
from:{ x: this.getLeft(), y : this.getTop() },
to:{ x: this.getStartX(), y : this.getStartY() },
onComplete : this.flyBackComplete.bind(this)
});
},
/**
* Fly/Slide dragged element to position of shim. This will only
* work when useShim is set to true.
* @method flyToShim
* @param {Number} duration in seconds(default = .2)
*/
flyToShim:function(duration){
this.fly({
el: this.getEl(),
duration: duration,
from:{ x: this.getStartX(), y : this.getStartY() },
to:{ x: this.getLeft(), y : this.getTop() },
onComplete : this.flyToShimComplete.bind(this)
});
},
getStartX:function () {
return this.dragProcess.elX;
},
getStartY:function () {
return this.dragProcess.elY;
},
flyBackComplete:function(){
/**
* Event fired after flyBack animation is complete
* @event flyBack
* @param {effect.Drag} this
* @param {HTMLElement} dom node
*/
this.fireEvent('flyBack', [this, this.getShimOrEl()]);
},
flyToShimComplete:function(){
/**
* Event fired after flyToShim animation is complete
* @event flyBack
* @param {effect.Drag} this
* @param {HTMLElement} dom node
*/
this.fireEvent('flyToShim', [this, this.getEl()]);
},
isActive:function(){
return this.dragProcess.active;
}
});