/*! * jquery.event.drop - 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, event.drag 2.2 ;(function($){ // secure $ jQuery alias // Events: drop, dropstart, dropend // add the jquery instance method $.fn.drop = 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("drop") !== 0 ) type = "drop"+ type; // were options passed opts = ( str == fn ? arg : opts ) || {}; // trigger or bind event handler return fn ? this.bind( type, opts, fn ) : this.trigger( type ); }; // DROP MANAGEMENT UTILITY // returns filtered drop target elements, caches their positions $.drop = function( opts ){ opts = opts || {}; // safely set new options... drop.multi = opts.multi === true ? Infinity : opts.multi === false ? 1 : !isNaN( opts.multi ) ? opts.multi : drop.multi; drop.delay = opts.delay || drop.delay; drop.tolerance = $.isFunction( opts.tolerance ) ? opts.tolerance : opts.tolerance === null ? null : drop.tolerance; drop.mode = opts.mode || drop.mode || 'intersect'; }; // local refs (increase compression) var $event = $.event, $special = $event.special, // configure the drop special event drop = $.event.special.drop = { // these are the default settings multi: 1, // allow multiple drop winners per dragged element delay: 20, // async timeout delay mode: 'overlap', // drop tolerance mode // internal cache targets: [], // the key name for stored drop data datakey: "dropdata", // prevent bubbling for better performance noBubble: true, // count bound related events add: function( obj ){ // read the interaction data var data = $.data( this, drop.datakey ); // count another realted event data.related += 1; }, // forget unbound related events remove: function(){ $.data( this, drop.datakey ).related -= 1; }, // configure the interactions setup: function(){ // check for related events if ( $.data( this, drop.datakey ) ) return; // initialize the drop element data var data = { related: 0, active: [], anyactive: 0, winner: 0, location: {} }; // store the drop data on the element $.data( this, drop.datakey, data ); // store the drop target in internal cache drop.targets.push( this ); }, // destroy the configure interaction teardown: function(){ var data = $.data( this, drop.datakey ) || {}; // check for related events if ( data.related ) return; // remove the stored data $.removeData( this, drop.datakey ); // reference the targeted element var element = this; // remove from the internal cache drop.targets = $.grep( drop.targets, function( target ){ return ( target !== element ); }); }, // shared event handler handler: function( event, dd ){ // local vars var results, $targets; // make sure the right data is available if ( !dd ) return; // handle various events switch ( event.type ){ // draginit, from $.event.special.drag case 'mousedown': // DROPINIT >> case 'touchstart': // DROPINIT >> // collect and assign the drop targets $targets = $( drop.targets ); if ( typeof dd.drop == "string" ) $targets = $targets.filter( dd.drop ); // reset drop data winner properties $targets.each(function(){ var data = $.data( this, drop.datakey ); data.active = []; data.anyactive = 0; data.winner = 0; }); // set available target elements dd.droppable = $targets; // activate drop targets for the initial element being dragged $special.drag.hijack( event, "dropinit", dd ); break; // drag, from $.event.special.drag case 'mousemove': // TOLERATE >> case 'touchmove': // TOLERATE >> drop.event = event; // store the mousemove event if ( !drop.timer ) // monitor drop targets drop.tolerate( dd ); break; // dragend, from $.event.special.drag case 'mouseup': // DROP >> DROPEND >> case 'touchend': // DROP >> DROPEND >> drop.timer = clearTimeout( drop.timer ); // delete timer if ( dd.propagates ){ $special.drag.hijack( event, "drop", dd ); $special.drag.hijack( event, "dropend", dd ); } break; } }, // returns the location positions of an element locate: function( elem, index ){ var data = $.data( elem, drop.datakey ), $elem = $( elem ), posi = $elem.offset() || {}, height = $elem.outerHeight(), width = $elem.outerWidth(), location = { elem: elem, width: width, height: height, top: posi.top, left: posi.left, right: posi.left + width, bottom: posi.top + height }; // drag elements might not have dropdata if ( data ){ data.location = location; data.index = index; data.elem = elem; } return location; }, // test the location positions of an element against another OR an X,Y coord contains: function( target, test ){ // target { location } contains test [x,y] or { location } return ( ( test[0] || test.left ) >= target.left && ( test[0] || test.right ) <= target.right && ( test[1] || test.top ) >= target.top && ( test[1] || test.bottom ) <= target.bottom ); }, // stored tolerance modes modes: { // fn scope: "$.event.special.drop" object // target with mouse wins, else target with most overlap wins 'intersect': function( event, proxy, target ){ return this.contains( target, [ event.pageX, event.pageY ] ) ? // check cursor 1e9 : this.modes.overlap.apply( this, arguments ); // check overlap }, // target with most overlap wins 'overlap': function( event, proxy, target ){ // calculate the area of overlap... return Math.max( 0, Math.min( target.bottom, proxy.bottom ) - Math.max( target.top, proxy.top ) ) * Math.max( 0, Math.min( target.right, proxy.right ) - Math.max( target.left, proxy.left ) ); }, // proxy is completely contained within target bounds 'fit': function( event, proxy, target ){ return this.contains( target, proxy ) ? 1 : 0; }, // center of the proxy is contained within target bounds 'middle': function( event, proxy, target ){ return this.contains( target, [ proxy.left + proxy.width * .5, proxy.top + proxy.height * .5 ] ) ? 1 : 0; } }, // sort drop target cache by by winner (dsc), then index (asc) sort: function( a, b ){ return ( b.winner - a.winner ) || ( a.index - b.index ); }, // async, recursive tolerance execution tolerate: function( dd ){ // declare local refs var i, drp, drg, data, arr, len, elem, // interaction iteration variables x = 0, ia, end = dd.interactions.length, // determine the mouse coords xy = [ drop.event.pageX, drop.event.pageY ], // custom or stored tolerance fn tolerance = drop.tolerance || drop.modes[ drop.mode ]; // go through each passed interaction... do if ( ia = dd.interactions[x] ){ // check valid interaction if ( !ia ) return; // initialize or clear the drop data ia.drop = []; // holds the drop elements arr = []; len = ia.droppable.length; // determine the proxy location, if needed if ( tolerance ) drg = drop.locate( ia.proxy ); // reset the loop i = 0; // loop each stored drop target do if ( elem = ia.droppable[i] ){ data = $.data( elem, drop.datakey ); drp = data.location; if ( !drp ) continue; // find a winner: tolerance function is defined, call it data.winner = tolerance ? tolerance.call( drop, drop.event, drg, drp ) // mouse position is always the fallback : drop.contains( drp, xy ) ? 1 : 0; arr.push( data ); } while ( ++i < len ); // loop // sort the drop targets arr.sort( drop.sort ); // reset the loop i = 0; // loop through all of the targets again do if ( data = arr[ i ] ){ // winners... if ( data.winner && ia.drop.length < drop.multi ){ // new winner... dropstart if ( !data.active[x] && !data.anyactive ){ // check to make sure that this is not prevented if ( $special.drag.hijack( drop.event, "dropstart", dd, x, data.elem )[0] !== false ){ data.active[x] = 1; data.anyactive += 1; } // if false, it is not a winner else data.winner = 0; } // if it is still a winner if ( data.winner ) ia.drop.push( data.elem ); } // losers... else if ( data.active[x] && data.anyactive == 1 ){ // former winner... dropend $special.drag.hijack( drop.event, "dropend", dd, x, data.elem ); data.active[x] = 0; data.anyactive -= 1; } } while ( ++i < len ); // loop } while ( ++x < end ) // loop // check if the mouse is still moving or is idle if ( drop.last && xy[0] == drop.last.pageX && xy[1] == drop.last.pageY ) delete drop.timer; // idle, don't recurse else // recurse drop.timer = setTimeout(function(){ drop.tolerate( dd ); }, drop.delay ); // remember event, to compare idleness drop.last = drop.event; } }; // share the same special event configuration with related events... $special.dropinit = $special.dropstart = $special.dropend = drop; })(jQuery); // confine scope