/*!
 * 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':
					// drag tolerance, x² + y² = distance²
					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");
			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);