/*jshint camelcase:true, plusplus:true, forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, browser:true, devel:true, maxerr:100, white:false, onevar:false */
/*global jQuery:true $:true */
/* jQuery Simulate Drag-n-Drop Plugin 1.3.0
* http://github.com/j-ulrich/jquery-simulate-ext
*
* Copyright (c) 2014 Jochen Ulrich
* Licensed under the MIT license (MIT-LICENSE.txt).
*/
;(function($, undefined) {
"use strict";
/* Overwrite the $.fn.simulate function to reduce the jQuery set to the first element for the
* drag-n-drop interactions.
*/
$.fn.simulate = function( type, options ) {
switch (type) {
case "drag":
case "drop":
case "drag-n-drop":
var ele = this.first();
new $.simulate( ele[0], type, options);
return ele;
default:
return this.each(function() {
new $.simulate( this, type, options );
});
}
};
var now = Date.now || function() { return new Date().getTime(); };
var rdocument = /\[object (?:HTML)?Document\]/;
/**
* Tests whether an object is an (HTML) document object.
* @param {DOM Element} elem - the object/element to be tested
* @returns {Boolean} true if elem is an (HTML) document object.
* @private
* @author julrich
* @since 1.1
*/
function isDocument( elem ) {
return rdocument.test(Object.prototype.toString.call($(elem)[0]));
}
/**
* Selects the first match from an array.
* @param {Array} array - Array of objects to be be tested
* @param {Function} check - Callback function that accepts one argument (which will be one element
* from the array) and returns a boolean.
* @returns {Boolean|null} the first element in array for which check returns true.
* If none of the elements in array passes check, null is returned.
* @private
* @author julrich
* @since 1.1
*/
function selectFirstMatch(array, check) {
var i;
if ($.isFunction(check)) {
for (i=0; i < array.length; i+=1) {
if (check(array[i])) {
return array[i];
}
}
return null;
}
else {
for (i=0; i < array.length; i+=1) {
if (array[i]) {
return array[i];
}
}
return null;
}
}
// Based on the findCenter function from jquery.simulate.js
/**
* Calculates the position of the center of an DOM element.
* @param {DOM Element} elem - the element whose center should be calculated.
* @returns {Object} an object with the properties x and y
* representing the position of the center of elem in page relative coordinates
* (i.e. independent of any scrolling).
* @private
* @author julrich
* @since 1.0
*/
function findCenter( elem ) {
var offset,
$elem = $( elem );
if ( isDocument($elem[0]) ) {
offset = {left: 0, top: 0};
}
else {
offset = $elem.offset();
}
return {
x: offset.left + $elem.outerWidth() / 2,
y: offset.top + $elem.outerHeight() / 2
};
}
/**
* Converts page relative coordinates into client relative coordinates.
* @param {Numeric|Object} x - Either the x coordinate of the page relative coordinates or
* an object with the properties pageX and pageY representing page
* relative coordinates.
* @param {Numeric} [y] - If x is numeric (i.e. the x coordinate of page relative coordinates),
* then this is the y coordinate. If x is an object, this parameter is skipped.
* @param {DOM Document} [docRel] - Optional DOM document object used to calculate the client relative
* coordinates. The page relative coordinates are interpreted as being relative to that document and
* the scroll position of that document is used to calculate the client relative coordinates.
* By default, document is used.
* @returns {Object} an object representing the client relative coordinates corresponding to the
* given page relative coordinates. The object either provides the properties x and
* y when x and y were given as arguments, or clientX
* and clientY when the parameter x was given as an object (see above).
* @private
* @author julrich
* @since 1.0
*/
function pageToClientPos(x, y, docRel) {
var $document;
if ( isDocument(y) ) {
$document = $(y);
} else {
$document = $(docRel || document);
}
if (typeof x === "number" && typeof y === "number") {
return {
x: x - $document.scrollLeft(),
y: y - $document.scrollTop()
};
}
else if (typeof x === "object" && x.pageX && x.pageY) {
return {
clientX: x.pageX - $document.scrollLeft(),
clientY: x.pageY - $document.scrollTop()
};
}
}
/**
* Browser-independent implementation of document.elementFromPoint().
*
* When run for the first time on a scrolled page, this function performs a check on how
* document.elementFromPoint() is implemented in the current browser. It stores
* the results in two static variables so that the check can be skipped for successive calls.
*
* @param {Numeric|Object} x - Either the x coordinate of client relative coordinates or an object
* with the properties x and y representing client relative coordinates.
* @param {Numeric} [y] - If x is numeric (i.e. the x coordinate of client relative coordinates),
* this is the y coordinate. If x is an object, this parameter is skipped.
* @param {DOM Document} [docRel] - Optional DOM document object
* @returns {DOM Element|Null}
* @private
* @author Nicolas Zeh (Basic idea), julrich
* @see http://www.zehnet.de/2010/11/19/document-elementfrompoint-a-jquery-solution/
* @since 1.0
*/
function elementAtPosition(x, y, docRel) {
var doc;
if ( isDocument(y) ) {
doc = y;
} else {
doc = docRel || document;
}
if(!doc.elementFromPoint) {
return null;
}
var clientX = x, clientY = y;
if (typeof x === "object" && (x.clientX || x.clientY)) {
clientX = x.clientX || 0 ;
clientY = x.clientY || 0;
}
if(elementAtPosition.prototype.check)
{
var sl, ele;
if ((sl = $(doc).scrollTop()) >0)
{
ele = doc.elementFromPoint(0, sl + $(window).height() -1);
if ( ele !== null && ele.tagName.toUpperCase() === 'HTML' ) { ele = null; }
elementAtPosition.prototype.nativeUsesRelative = ( ele === null );
}
else if((sl = $(doc).scrollLeft()) >0)
{
ele = doc.elementFromPoint(sl + $(window).width() -1, 0);
if ( ele !== null && ele.tagName.toUpperCase() === 'HTML' ) { ele = null; }
elementAtPosition.prototype.nativeUsesRelative = ( ele === null );
}
elementAtPosition.prototype.check = (sl<=0); // Check was not meaningful because we were at scroll position 0
}
if(!elementAtPosition.prototype.nativeUsesRelative)
{
clientX += $(doc).scrollLeft();
clientY += $(doc).scrollTop();
}
return doc.elementFromPoint(clientX,clientY);
}
// Default values for the check variables
elementAtPosition.prototype.check = true;
elementAtPosition.prototype.nativeUsesRelative = true;
/**
* Informs the rest of the world that the drag is finished.
* @param {DOM Element} ele - The element which was dragged.
* @param {Object} [options] - The drag options.
* @fires simulate-drag
* @private
* @author julrich
* @since 1.0
*/
function dragFinished(ele, options) {
var opts = options || {};
$(ele).trigger({type: "simulate-drag"});
if ($.isFunction(opts.callback)) {
opts.callback.apply(ele);
}
}
/**
* Generates a series of mousemove events for a drag.
* @param {Object} self - The simulate object.
* @param {DOM Element} ele - The element which is dragged.
* @param {Object} start - The start coordinates of the drag, represented using the properties
* x and y.
* @param {Object} drag - The distance to be dragged, represented using the properties dx
* and dy.
* @param {Object} options - The drag options. Must have the property interpolation
* containing the interpolation options (stepWidth, stepCount, etc.).
* @requires eventTarget
* @requires now()
* @private
* @author julrich
* @since 1.0
*/
function interpolatedEvents(self, ele, start, drag, options) {
var targetDoc = selectFirstMatch([ele, ele.ownerDocument], isDocument) || document,
interpolOptions = options.interpolation,
dragDistance = Math.sqrt(Math.pow(drag.dx,2) + Math.pow(drag.dy,2)), // sqrt(a^2 + b^2)
stepWidth, stepCount, stepVector;
if (interpolOptions.stepWidth) {
stepWidth = parseInt(interpolOptions.stepWidth, 10);
stepCount = Math.floor(dragDistance / stepWidth)-1;
var stepScale = stepWidth / dragDistance;
stepVector = {x: drag.dx*stepScale, y: drag.dy*stepScale };
}
else {
stepCount = parseInt(interpolOptions.stepCount, 10);
stepWidth = dragDistance / (stepCount+1);
stepVector = {x: drag.dx/(stepCount+1), y: drag.dy/(stepCount+1)};
}
var coords = $.extend({},start);
/**
* Calculates the effective coordinates for one mousemove event and fires the event.
* @requires eventTarget
* @requires targetDoc
* @requires coords
* @requires stepVector
* @requires interpolOptions
* @fires mousemove
* @inner
* @author julrich
* @since 1.0
*/
function interpolationStep() {
coords.x += stepVector.x;
coords.y += stepVector.y;
var effectiveCoords = {pageX: coords.x, pageY: coords.y};
if (interpolOptions.shaky && (interpolOptions.shaky === true || !isNaN(parseInt(interpolOptions.shaky,10)) )) {
var amplitude = (interpolOptions.shaky === true)? 1 : parseInt(interpolOptions.shaky,10);
effectiveCoords.pageX += Math.floor(Math.random()*(2*amplitude+1)-amplitude);
effectiveCoords.pageY += Math.floor(Math.random()*(2*amplitude+1)-amplitude);
}
var clientCoord = pageToClientPos(effectiveCoords, targetDoc),
eventTarget = elementAtPosition(clientCoord, targetDoc) || ele;
self.simulateEvent( eventTarget, "mousemove", {pageX: Math.round(effectiveCoords.pageX), pageY: Math.round(effectiveCoords.pageY)});
}
var lastTime;
/**
* Performs one interpolation step (i.e. cares about firing the event) and then sleeps for
* stepDelay milliseconds.
* @requires lastTime
* @requires stepDelay
* @requires step
* @requires ele
* @requires eventTarget
* @reuiqre targetDoc
* @requires start
* @requires drag
* @requires now()
* @inner
* @author julrich
* @since 1.0
*/
function stepAndSleep() {
var timeElapsed = now() - lastTime; // Work-around for Firefox & IE "bug": setTimeout can fire before the timeout
if (timeElapsed >= stepDelay) {
if (step < stepCount) {
interpolationStep();
step += 1;
lastTime = now();
setTimeout(stepAndSleep, stepDelay);
}
else {
var pageCoord = {pageX: Math.round(start.x+drag.dx), pageY: Math.round(start.y+drag.dy)},
clientCoord = pageToClientPos(pageCoord, targetDoc),
eventTarget = elementAtPosition(clientCoord, targetDoc) || ele;
self.simulateEvent( eventTarget, "mousemove", pageCoord);
dragFinished(ele, options);
}
}
else {
setTimeout(stepAndSleep, stepDelay - timeElapsed);
}
}
if ( (!interpolOptions.stepDelay && !interpolOptions.duration) || ((interpolOptions.stepDelay <= 0) && (interpolOptions.duration <= 0)) ) {
// Trigger as fast as possible
for (var i=0; i < stepCount; i+=1) {
interpolationStep();
}
var pageCoord = {pageX: Math.round(start.x+drag.dx), pageY: Math.round(start.y+drag.dy)},
clientCoord = pageToClientPos(pageCoord, targetDoc),
eventTarget = elementAtPosition(clientCoord, targetDoc) || ele;
self.simulateEvent( eventTarget, "mousemove", pageCoord);
dragFinished(ele, options);
}
else {
var stepDelay = parseInt(interpolOptions.stepDelay,10) || Math.ceil(parseInt(interpolOptions.duration,10) / (stepCount+1));
var step = 0;
lastTime = now();
setTimeout(stepAndSleep, stepDelay);
}
}
/**
* @returns {Object|undefined} an object containing information about the currently active drag
* or undefined when there is no active drag.
* The returned object contains the following properties:
*
dragElement: the dragged elementdragStart: object with the properties x and y
* representing the page relative start coordinates of the dragdragDistance: object with the properties x and y
* representing the distance of the drag in x and y direction