// encoding: utf-8 /*! * jquery.event.drag - v 2.2 * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com * Open Source MIT License - http://threedubmedia.com/code/license */ // Created: 2008-06-04 // Updated: 2012-05-21 // REQUIRES: jquery 1.7.x ;(function( $ ){ // add the jquery instance method $.fn.drag = function( str, arg, opts ){ // figure out the event type var type = typeof str == "string" ? str : "", // figure out the event handler... fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null; // fix the event type if ( type.indexOf("drag") !== 0 ) type = "drag"+ type; // were options passed opts = ( str == fn ? arg : opts ) || {}; // trigger or bind event handler return fn ? this.bind( type, opts, fn ) : this.trigger( type ); }; // local refs (increase compression) var $event = $.event, $special = $event.special, // configure the drag special event drag = $special.drag = { // these are the default settings defaults: { which: 1, // mouse button pressed to start drag sequence distance: 0, // distance dragged before dragstart not: ':input', // selector to suppress dragging on target elements handle: null, // selector to match handle target elements relative: false, // true to use "position", false to use "offset" drop: true, // false to suppress drop events, true or selector to allow click: false // false to suppress click events after dragend (no proxy) }, // the key name for stored drag data datakey: "dragdata", // prevent bubbling for better performance noBubble: true, // count bound related events add: function( obj ){ // read the interaction data var data = $.data( this, drag.datakey ), // read any passed options opts = obj.data || {}; // count another realted event data.related += 1; // extend data options bound with this event // don't iterate "opts" in case it is a node $.each( drag.defaults, function( key, def ){ if ( opts[ key ] !== undefined ) data[ key ] = opts[ key ]; }); }, // forget unbound related events remove: function(){ $.data( this, drag.datakey ).related -= 1; }, // configure interaction, capture settings setup: function(){ // check for related events if ( $.data( this, drag.datakey ) ) return; // initialize the drag data with copied defaults var data = $.extend({ related:0 }, drag.defaults ); // store the interaction data $.data( this, drag.datakey, data ); // bind the mousedown event, which starts drag interactions $event.add( this, "touchstart mousedown", drag.init, data ); // prevent image dragging in IE... if ( this.attachEvent ) this.attachEvent("ondragstart", drag.dontstart ); }, // destroy configured interaction teardown: function(){ var data = $.data( this, drag.datakey ) || {}; // check for related events if ( data.related ) return; // remove the stored data $.removeData( this, drag.datakey ); // remove the mousedown event $event.remove( this, "touchstart mousedown", drag.init ); // enable text selection drag.textselect( true ); // un-prevent image dragging in IE... if ( this.detachEvent ) this.detachEvent("ondragstart", drag.dontstart ); }, // initialize the interaction init: function( event ){ // sorry, only one touch at a time if ( drag.touched ) return; // the drag/drop interaction data var dd = event.data, results; // check the which directive if ( event.which != 0 && dd.which > 0 && event.which != dd.which ) return; // check for suppressed selector if ( $( event.target ).is( dd.not ) ) return; // check for handle selector if ( dd.handle && !$( event.target ).closest( dd.handle, event.currentTarget ).length ) return; drag.touched = event.type == 'touchstart' ? this : null; dd.propagates = 1; dd.mousedown = this; dd.interactions = [ drag.interaction( this, dd ) ]; dd.target = event.target; dd.pageX = event.pageX; dd.pageY = event.pageY; dd.dragging = null; // handle draginit event... results = drag.hijack( event, "draginit", dd ); // early cancel if ( !dd.propagates ) return; // flatten the result set results = drag.flatten( results ); // insert new interaction elements if ( results && results.length ){ dd.interactions = []; $.each( results, function(){ dd.interactions.push( drag.interaction( this, dd ) ); }); } // remember how many interactions are propagating dd.propagates = dd.interactions.length; // locate and init the drop targets if ( dd.drop !== false && $special.drop ) $special.drop.handler( event, dd ); // disable text selection drag.textselect( false ); // bind additional events... if ( drag.touched ) $event.add( drag.touched, "touchmove touchend", drag.handler, dd ); else $event.add( document, "mousemove mouseup", drag.handler, dd ); // helps prevent text selection or scrolling if ( !drag.touched || dd.live ) return false; }, // returns an interaction object interaction: function( elem, dd ){ var offset = $( elem )[ dd.relative ? "position" : "offset" ]() || { top:0, left:0 }; return { drag: elem, callback: new drag.callback(), droppable: [], offset: offset }; }, // handle drag-releatd DOM events handler: function( event ){ // read the data before hijacking anything var dd = event.data; // handle various events switch ( event.type ){ // mousemove, check distance, start dragging case !dd.dragging && 'touchmove': event.preventDefault(); case !dd.dragging && 'mousemove': if ( Math.pow( event.pageX-dd.pageX, 2 ) + Math.pow( event.pageY-dd.pageY, 2 ) < Math.pow( dd.distance, 2 ) ) break; // distance tolerance not reached event.target = dd.target; // force target from "mousedown" event (fix distance issue) drag.hijack( event, "dragstart", dd ); // trigger "dragstart" if ( dd.propagates ) // "dragstart" not rejected dd.dragging = true; // activate interaction // mousemove, dragging case 'touchmove': event.preventDefault(); case 'mousemove': if ( dd.dragging ){ // trigger "drag" drag.hijack( event, "drag", dd ); if ( dd.propagates ){ // manage drop events if ( dd.drop !== false && $special.drop ) $special.drop.handler( event, dd ); // "dropstart", "dropend" break; // "drag" not rejected, stop } event.type = "mouseup"; // helps "drop" handler behave } // mouseup, stop dragging case 'touchend': case 'mouseup': default: if ( drag.touched ) $event.remove( drag.touched, "touchmove touchend", drag.handler ); // remove touch events else $event.remove( document, "mousemove mouseup", drag.handler ); // remove page events if ( dd.dragging ){ if ( dd.drop !== false && $special.drop ) $special.drop.handler( event, dd ); // "drop" drag.hijack( event, "dragend", dd ); // trigger "dragend" } drag.textselect( true ); // enable text selection // if suppressing click events... if ( dd.click === false && dd.dragging ) $.data( dd.mousedown, "suppress.click", new Date().getTime() + 5 ); dd.dragging = drag.touched = false; // deactivate element break; } }, // re-use event object for custom events hijack: function( event, type, dd, x, elem ){ // not configured if ( !dd ) return; // remember the original event and type var orig = { event:event.originalEvent, type:event.type }, // is the event drag related or drog related? mode = type.indexOf("drop") ? "drag" : "drop", // iteration vars result, i = x || 0, ia, $elems, callback, len = !isNaN( x ) ? x : dd.interactions.length; // modify the event type event.type = type; // remove the original event event.originalEvent = null; // initialize the results dd.results = []; // handle each interacted element do if ( ia = dd.interactions[ i ] ){ // validate the interaction if ( type !== "dragend" && ia.cancelled ) continue; // set the dragdrop properties on the event object callback = drag.properties( event, dd, ia ); // prepare for more results ia.results = []; // handle each element $( elem || ia[ mode ] || dd.droppable ).each(function( p, subject ){ // identify drag or drop targets individually callback.target = subject; // force propagtion of the custom event event.isPropagationStopped = function(){ return false; }; // handle the event result = subject ? $event.dispatch.call( subject, event, callback ) : null; // stop the drag interaction for this element if ( result === false ){ if ( mode == "drag" ){ ia.cancelled = true; dd.propagates -= 1; } if ( type == "drop" ){ ia[ mode ][p] = null; } } // assign any dropinit elements else if ( type == "dropinit" ) ia.droppable.push( drag.element( result ) || subject ); // accept a returned proxy element if ( type == "dragstart" ) ia.proxy = $( drag.element( result ) || ia.drag )[0]; // remember this result ia.results.push( result ); // forget the event result, for recycling delete event.result; // break on cancelled handler if ( type !== "dropinit" ) return result; }); // flatten the results dd.results[ i ] = drag.flatten( ia.results ); // accept a set of valid drop targets if ( type == "dropinit" ) ia.droppable = drag.flatten( ia.droppable ); // locate drop targets if ( type == "dragstart" && !ia.cancelled ) callback.update(); } while ( ++i < len ) // restore the original event & type event.type = orig.type; event.originalEvent = orig.event; // return all handler results return drag.flatten( dd.results ); }, // extend the callback object with drag/drop properties... properties: function( event, dd, ia ){ var obj = ia.callback; // elements obj.drag = ia.drag; obj.proxy = ia.proxy || ia.drag; // starting mouse position obj.startX = dd.pageX; obj.startY = dd.pageY; // current distance dragged obj.deltaX = event.pageX - dd.pageX; obj.deltaY = event.pageY - dd.pageY; // original element position obj.originalX = ia.offset.left; obj.originalY = ia.offset.top; // adjusted element position obj.offsetX = obj.originalX + obj.deltaX; obj.offsetY = obj.originalY + obj.deltaY; // assign the drop targets information obj.drop = drag.flatten( ( ia.drop || [] ).slice() ); obj.available = drag.flatten( ( ia.droppable || [] ).slice() ); return obj; }, // determine is the argument is an element or jquery instance element: function( arg ){ if ( arg && ( arg.jquery || arg.nodeType == 1 ) ) return arg; }, // flatten nested jquery objects and arrays into a single dimension array flatten: function( arr ){ return $.map( arr, function( member ){ return member && member.jquery ? $.makeArray( member ) : member && member.length ? drag.flatten( member ) : member; }); }, // toggles text selection attributes ON (true) or OFF (false) textselect: function( bool ){ $( document )[ bool ? "unbind" : "bind" ]("selectstart", drag.dontstart ) .css("MozUserSelect", bool ? "" : "none" ); // .attr("unselectable", bool ? "off" : "on" ) document.unselectable = bool ? "off" : "on"; }, // suppress "selectstart" and "ondragstart" events dontstart: function(){ return false; }, // a callback instance contructor callback: function(){} }; // callback methods drag.callback.prototype = { update: function(){ if ( $special.drop && this.available.length ) $.each( this.available, function( i ){ $special.drop.locate( this, i ); }); } }; // patch $.event.$dispatch to allow suppressing clicks var $dispatch = $event.dispatch; $event.dispatch = function( event ){ if ( $.data( this, "suppress."+ event.type ) - new Date().getTime() > 0 ){ $.removeData( this, "suppress."+ event.type ); return; } return $dispatch.apply( this, arguments ); }; // event fix hooks for touch events... var touchHooks = $event.fixHooks.touchstart = $event.fixHooks.touchmove = $event.fixHooks.touchend = $event.fixHooks.touchcancel = { props: "clientX clientY pageX pageY screenX screenY".split( " " ), filter: function( event, orig ) { if ( orig ){ var touched = ( orig.touches && orig.touches[0] ) || ( orig.changedTouches && orig.changedTouches[0] ) || null; // iOS webkit: touchstart, touchmove, touchend if ( touched ) $.each( touchHooks.props, function( i, prop ){ event[ prop ] = touched[ prop ]; }); } return event; } }; // share the same special event configuration with related events... $special.draginit = $special.dragstart = $special.dragend = drag; })( jQuery );