/*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: * * @public * @author julrich * @since 1.0 */ $.simulate.activeDrag = function() { if (!$.simulate._activeDrag) { return undefined; } return $.extend(true,{},$.simulate._activeDrag); }; $.extend( $.simulate.prototype, /** * @lends $.simulate.prototype */ { /** * Simulates a drag. * * @see https://github.com/j-ulrich/jquery-simulate-ext/blob/master/doc/drag-n-drop.md * @public * @author julrich * @since 1.0 */ simulateDrag: function() { var self = this, ele = self.target, options = $.extend({ dx: 0, dy: 0, dragTarget: undefined, clickToDrag: false, eventProps: {}, interpolation: { stepWidth: 0, stepCount: 0, stepDelay: 0, duration: 0, shaky: false }, callback: undefined }, this.options); var start, continueDrag = ($.simulate._activeDrag && $.simulate._activeDrag.dragElement === ele); if (continueDrag) { start = $.simulate._activeDrag.dragStart; } else { start = findCenter( ele ); } var x = Math.round( start.x ), y = Math.round( start.y ), coord = { pageX: x, pageY: y }, dx, dy; if (options.dragTarget) { var end = findCenter(options.dragTarget); dx = Math.round(end.x - start.x); dy = Math.round(end.y - start.y); } else { dx = options.dx || 0; dy = options.dy || 0; } if (continueDrag) { // We just continue to move the dragged element $.simulate._activeDrag.dragDistance.x += dx; $.simulate._activeDrag.dragDistance.y += dy; coord = { pageX: Math.round(x + $.simulate._activeDrag.dragDistance.x) , pageY: Math.round(y + $.simulate._activeDrag.dragDistance.y) }; } else { if ($.simulate._activeDrag) { // Drop before starting a new drag $($.simulate._activeDrag.dragElement).simulate( "drop" ); } // We start a new drag $.extend(options.eventProps, coord); self.simulateEvent( ele, "mousedown", options.eventProps ); if (options.clickToDrag === true) { self.simulateEvent( ele, "mouseup", options.eventProps ); self.simulateEvent( ele, "click", options.eventProps ); } $(ele).add(ele.ownerDocument).one('mouseup', function() { $.simulate._activeDrag = undefined; }); $.extend($.simulate, { _activeDrag: { dragElement: ele, dragStart: { x: x, y: y }, dragDistance: { x: dx, y: dy } } }); coord = { pageX: Math.round(x + dx), pageY: Math.round(y + dy) }; } if (dx !== 0 || dy !== 0) { if ( options.interpolation && (options.interpolation.stepCount || options.interpolation.stepWidth) ) { interpolatedEvents(self, ele, {x: x, y: y}, {dx: dx, dy: dy}, options); } else { var targetDoc = selectFirstMatch([ele, ele.ownerDocument], isDocument) || document, clientCoord = pageToClientPos(coord, targetDoc), eventTarget = elementAtPosition(clientCoord, targetDoc) || ele; $.extend(options.eventProps, coord); self.simulateEvent( eventTarget, "mousemove", options.eventProps ); dragFinished(ele, options); } } else { dragFinished(ele, options); } }, /** * Simulates a drop. * * @see https://github.com/j-ulrich/jquery-simulate-ext/blob/master/doc/drag-n-drop.md * @public * @author julrich * @since 1.0 */ simulateDrop: function() { var self = this, ele = this.target, activeDrag = $.simulate._activeDrag, options = $.extend({ clickToDrop: false, eventProps: {}, callback: undefined }, self.options), moveBeforeDrop = true, center = findCenter( ele ), x = Math.round( center.x ), y = Math.round( center.y ), coord = { pageX: x, pageY: y }, targetDoc = ( (activeDrag)? selectFirstMatch([activeDrag.dragElement, activeDrag.dragElement.ownerDocument], isDocument) : selectFirstMatch([ele, ele.ownerDocument], isDocument) ) || document, clientCoord = pageToClientPos(coord, targetDoc), eventTarget = elementAtPosition(clientCoord, targetDoc); if (activeDrag && (activeDrag.dragElement === ele || isDocument(ele))) { // We already moved the mouse during the drag so we just simulate the drop on the end position x = Math.round(activeDrag.dragStart.x + activeDrag.dragDistance.x); y = Math.round(activeDrag.dragStart.y + activeDrag.dragDistance.y); coord = { pageX: x, pageY: y }; clientCoord = pageToClientPos(coord, targetDoc); eventTarget = elementAtPosition(clientCoord, targetDoc); moveBeforeDrop = false; } if (!eventTarget) { eventTarget = (activeDrag)? activeDrag.dragElement : ele; } $.extend(options.eventProps, coord); if (moveBeforeDrop === true) { // Else we assume the drop should happen on target, so we move there self.simulateEvent( eventTarget, "mousemove", options.eventProps ); } if (options.clickToDrop) { self.simulateEvent( eventTarget, "mousedown", options.eventProps ); } this.simulateEvent( eventTarget, "mouseup", options.eventProps ); if (options.clickToDrop) { self.simulateEvent( eventTarget, "click", options.eventProps ); } $.simulate._activeDrag = undefined; $(eventTarget).trigger({type: "simulate-drop"}); if ($.isFunction(options.callback)) { options.callback.apply(eventTarget); } }, /** * Simulates a drag followed by drop. * * @see https://github.com/j-ulrich/jquery-simulate-ext/blob/master/doc/drag-n-drop.md * @public * @author julrich * @since 1.0 */ simulateDragNDrop: function() { var self = this, ele = this.target, options = $.extend({ dragTarget: undefined, dropTarget: undefined }, self.options), // If there is a dragTarget or dx/dy, then we drag there and simulate an independent drop on dropTarget or ele dropEle = ((options.dragTarget || options.dx || options.dy)? options.dropTarget : ele) || ele; /* dx = (options.dropTarget)? 0 : (options.dx || 0), dy = (options.dropTarget)? 0 : (options.dy || 0), dragDistance = { dx: dx, dy: dy }; $.extend(options, dragDistance); */ $(ele).simulate( "drag", $.extend({},options,{ // If there is no dragTarget, no dx and no dy, we drag onto the dropTarget directly dragTarget: options.dragTarget || ((options.dx || options.dy)?undefined:options.dropTarget), callback: function() { $(dropEle).simulate( "drop", options ); } })); } }); }(jQuery));