/*
Programmer: Darryl Ballard
Date created: 2011-04-15
Last updated: 2011-04-29

Requires:
	jQuery (1.4+?)
	
*/

// Keep global namespace clean, and allow for the brevity of "$" while depending only on "jQuery"
(function($) {
	/*
		Caught Messages
		
		Adds click-to-dismiss behavior to list items.
		Intended for use on output elements as success/failure messages from the the server.
	*/
	$.fn.gstCaughtMessages = function() {
		return this.each(function() {
			var listItems;
			
			// Get the list items within this
			listItems = $(this).find("li");
			
			// Some hover-classes
			listItems.hover(
				function() { /* Mouse over */
					$(this).addClass("gst-hover");
				},
				function() { /* Mouse out */
					$(this).removeClass("gst-hover");
				}
			);
			
			// Delete the item clicked
			// When the containing element has no children left, delete that element as well
			listItems.click(function() {
				var $parent = $(this.parentNode);
				
				// Delete me
				$(this).remove();
				
				// Remove the list itself once it's empty
				if ($parent.children().length <= 0) {
					$parent.remove();
				}
				
				return false;
			});
		});
	};
	
	/*
		Click Confirmation
		
		Add a "confirm" dialog that, when not canceled, allows the click event to go through.
	*/
	$.fn.gstClickConfirmation = function() {
		//return this.click(function()
		return this.live("click", function() {
			var myTitle = $(this).attr("title");
			var myText;
			
			// No title attribute supplied, try to use the content of the element
			// e.g. a link with text content
			if (!myTitle) {
				myText = $(this).text();
				
				if (myText) {
					myTitle = myText;
				}
			}
			
			// Replace improper punctuation
			if (myTitle.length >= 2) {
				if (myTitle.substr(myTitle.length - 1, 1) == ".") {
					myTitle = myTitle.substr(0, myTitle.length - 1);
				}
			}
			
			// Make it a question (SO ROBUST)
			myTitle = myTitle + "?";
			
			if (!confirm(myTitle)) {
				return false;
			}
		});
	};
	
	/*
		Details
		
		A construct similar to the HTML5 <details> element.
	*/
	$.fn.gstDetails = function() {
		return this.each(function() {
			var summaries = $(this).children(".gst-details-summary");
			
			summaries.click(function() {
				$(this).parent().toggleClass("gst-details-closed");
			});
			
			summaries.find("a").click(function(e) {
				e.preventDefault();
			});
		});
	};
	
	/*
		External Links
		
		Makes links open in a new tab/window.
	*/
	$.fn.gstExternalLinks = function() {
		return this.live("click", function() {
			var relativeHref = "";
			var endURL = "";
			
			var myHref = $(this).attr("href");
			//alert("clicked item attr href = " + myHref);
			
			// See if this link starts with "/" or "http://"
			var isAbsolute = (myHref.indexOf("/") == 0 || myHref.indexOf("http://") == 0);
			//alert("isAbsolute = " + (isAbsolute ? "true" : "false"));
			
			if (isAbsolute) {
				endURL = myHref;
			} else {
				// Every GST site expects a base element with an href attribute.
				var bases = $("base");
				if (bases.length < 1) { throw "Missing the BASE element."; }
				
				relativeHref = bases.attr("href");
				
				endURL = relativeHref + myHref;
			}
			
			var window_name = "gst_new_window_" + (new Date()).getTime();
			//alert("window_name = " + window_name);
			window.open(endURL, window_name);
			
			return false;
		});
	};
	
	/*
		First and Last
		
		Added "gst-first" and "gst-last" to the first and last items of lists, respectively
	*/
	$.fn.gstFirstAndLast = function() {
		return this.each(function() {
			// "this" is a list element (UL or OL)
			//alert("within each: this = " + this);
			
			var kids = $(this).children();
			
			//alert("I am " + this + ", I have " + kids.length + " children.");
			
			kids.first().addClass("gst-first");
			kids.last().addClass("gst-last");
		});
	};
	
	/*
		Gallery - Horizontal
		
		A gallery that slides horizontally between list items
	*/
	$.fn.gstGalleryHorizontal = function(pluginOptions) {
		// Options with defaults
		pluginOptions = $.extend({
			changeAutomatically: false,
			changeInterval: 3000,
			slideChangeDuration: 200,
			startPosition: "first" /* first or middle */
		}, pluginOptions);
		
		return this.each(function() {
			// The gallery element itself
			// Contains the gallery wrap and will contain the
			// navigation controls
			var $gallery = $(this);
			
			// The gallery wrap, contains the list
			var $galleryWrap = $gallery.find(".gst-gallery-wrap");
			
			// The UL or OL child of the gallery wrap
			var $theList = $galleryWrap.children("ul, ol").first();
			
			// The LIs in the gallery list
			var $items = $theList.children();
			
			//alert("I have " + $items.length + " items.");
			
			if ($items.length > 0) {
				// Flag the gallery as script-enhanced
				$gallery.addClass("gst-gallery-active");
				
				// Find the navigation controls
				var $previous = $gallery.find(".gst-gallery-previous");
				var $next = $gallery.find(".gst-gallery-next");
				
				// Figure out how big they are
				var previousWidth = $previous.width();
				var previousHeight = $previous.height();
				var nextWidth = $next.width();
				var nextHeight = $next.height();
				
				// Some static css required to make this work
				//$gallery.css({});
				$gallery.attr('unselectable', 'on').css({
					"-moz-user-select": "none",
					"-khtml-user-select": "none",
					"-webkit-user-select": "none",
					"user-select": "none"
				});
				$galleryWrap.css({
					overflow: "hidden",
					padding: 0,
					position: "relative"
				});
				$theList.css({
					listStyleType: "none",
					margin: 0,
					padding: 0,
					position: "absolute",
					left: 0,
					top: 0
				});
				$items.css({
					float: "left"
				});
				
				var funcRecalculateHeightsAndWidths = function() {
					// Calculate some metrics on the items in the gallery
					var totalItemWidth = 0;
					var largestItemHeight = 0;
					$items.each(function() {
						var $this = $(this);
						totalItemWidth += $this.outerWidth();
						var outerHeight = $this.outerHeight();
						if (outerHeight > largestItemHeight) {
							largestItemHeight = outerHeight;
						}
					});
					
					$galleryWrap.css({
						height: largestItemHeight + "px"
					});
					$theList.css({
						width: totalItemWidth * 2 + "px" /* ample room, most likely */
					});
				};
				
				/*
				// Recalculate the heights and widths after each image loads
				$items.find("img").load(funcRecalculateHeightsAndWidths);
				
				// And once more on window load
				$(window).load(funcRecalculateHeightsAndWidths);
				*/
				
				// Position the list within the gallery wrap
				//var lastPos = $items.last().position();
				//alert(lastPos.left + "\n" + lastPos.top);
				
				var funcUpdateButtons = function() {
					// Are we on the first image
					if (currentIndex == 0) {
						$previous.addClass("gst-gallery-button-disabled");
					} else {
						$previous.removeClass("gst-gallery-button-disabled");
					}
					
					// Are we on the last image
					if (currentIndex == $items.length - 1) {
						$next.addClass("gst-gallery-button-disabled");
					} else {
						$next.removeClass("gst-gallery-button-disabled");
					}
				};
				
				var currentIndex = 0;
				if (pluginOptions.startPosition == "middle") {
					currentIndex = Math.floor(($items.length - 1) / 2);
				}
				
				var funcSetCurrent = function(newIndex) {
					newIndex = 1 * newIndex;
					
					// Only do anything if it's a valid index
					if (newIndex >= 0 && newIndex < $items.length) {
						currentIndex = newIndex;
						
						var $currentItem = $items.eq(newIndex);
						var listOffset = $theList.offset();
						var currentItemOffset = $currentItem.offset();
						var itemWithinListOffset = currentItemOffset.left - listOffset.left;
						var newLeft = $galleryWrap.width() / 2 - $currentItem.width() / 2 - itemWithinListOffset;
						
						//$theList.css("left", newLeft);
						$theList.animate({
							left: newLeft + "px"
						}, pluginOptions.slideChangeDuration);
						
						funcUpdateButtons();
					}
				};
				funcSetCurrent(currentIndex);
				
				// Next & Previous event handlers
				$previous.click(function() {
					//alert("Previous!");
					funcSetCurrent(currentIndex - 1);
					return false;
				});
				$next.click(function() {
					//alert("Next!");
					funcSetCurrent(currentIndex + 1);
					return false;
				});
				
				// Mark this gallery as scripted & active
				$gallery.addClass("gst-gallery-active");
				
				
				// Recalculate the heights and widths after each image loads
				$items.find("img").load(function() {
					funcRecalculateHeightsAndWidths();
					funcSetCurrent(currentIndex);
				});
				
				var handleChangeToNext;
				var funcChangeToNext = function() {
					var nextIndex = (currentIndex + 1) % $items.length;
					
					//alert("nextIndex = " + nextIndex);
					
					funcSetCurrent(nextIndex);
					
					//setTimeout(funcChangeToNext, pluginOptions.changeInterval);
				};
				
				// And once more on window load
				$(window).load(function() {
					funcRecalculateHeightsAndWidths();
					funcSetCurrent(currentIndex);
					
					if (pluginOptions.changeAutomatically) {
						//setTimeout(funcChangeToNext, pluginOptions.changeInterval);
						handleChangeToNext = setInterval(funcChangeToNext, pluginOptions.changeInterval);
					}
				});
				
			}
		});
	};
	
	/*
		Google Maps - Simple multi-location display
		
		Scrape locations out of the content and create a pin for each within
		the map. The map will be zoomed automatically to contain all the pins.
		This will start missing pins if more than ~10 locations are fetched at
		once.
	*/
	$.fn.gstGoogleMapSimple = function() {
		// Process each "map and location(s) wrapper"
		return this.each(function() {
			var theWrapper = this;
			
			// See if there's a map
			var theMap = jQuery(theWrapper).find(".gst-map")
			var elemMap = theMap.get(0);
			
			// Gather the locations we'll be displaying
			var locations = jQuery(theWrapper).find(".gst-map-location");
			
			if (!locations.length || !elemMap || !GBrowserIsCompatible()) {
				//alert("No locations, no map, or browser not compatible.");
				//alert("Locations: " + locations.length + "\nelemMap = " + elemMap);
				if (!locations.length) {
					//alert("No locations found");
				}
				if (!elemMap) {
					//alert("No map element found");
				}
				if (!GBrowserIsCompatible()) {
					//alert("Browser not compatible");
				}
			} else {
				// Unhide the map
				theMap.css("display", "block");
				
				// Create the map (don't forget to setCenter and add controls)
				var map = new GMap2(elemMap);
				
				// Initialize the map, centered on Knoxville, TN
				map.setCenter(new GLatLng(35.868673,-84.207716), 7);
				
				// Add a zoom/pan control
				map.addControl(new GLargeMapControl());
				
				//alert("Found " + locations.length + " locations.");
				
				// Create a bounding region
				var bounds = new GLatLngBounds();
				
				// Add each location
				locations.each(function() {
					var theLocation = this;
					var $theLocation = jQuery(theLocation);
					
					// The location's title
					var locationTitle = $theLocation.find(".gst-map-title").text();
					
					// The location's address
					var address = $theLocation.find(".gst-map-address").text();
					
					// The location's coordinates
					var latitude = $theLocation.find(".gst-map-latitude").text();
					var longitude = $theLocation.find(".gst-map-longitude").text();
					
					var iconPath = $theLocation.find(".gst-map-icon").text();
					
					var id = $theLocation.find(".gst-map-id").text();
					
					//alert("Location: " + locationTitle + " @ " + address);
					
					var funcAddMarkerFromGLatLng = function(point) {
						// Create a marker
						var marker = new GMarker(point);
						
						var marker_html = "<p class=\"title\">" + locationTitle + "</p><p class=\"address\">" + address + "</p>";
						
						// Give it click behavior
						GEvent.addListener(marker, "click", function() {
							this.openInfoWindowHtml(marker_html);
						});
						
						// Add the marker to map
						map.addOverlay(marker);
						if (iconPath && iconPath.length) {
							//marker.setImage("/images/homeicon.png")
							marker.setImage("/" + iconPath);
						}
						
						// Start with the info window open
						//marker.openInfoWindowHtml(marker_html);
						
						// Smart zoom & center
						bounds.extend(point);
						map.setCenter(bounds.getCenter(), Math.min(12, map.getBoundsZoomLevel(bounds) - 0));
					};
					
					if (latitude && longitude) {
						// Create the map pin directly without geocoding
						funcAddMarkerFromGLatLng(new GLatLng(latitude, longitude));
					} else {
						if (address) {
							// Start a geocode request for the address
							var geocoder = new GClientGeocoder();
							geocoder.getLocations(address, function(response) {
								// Retrieve the object
								var place = response.Placemark[0];
								
								// Retrieve the latitude and longitude
								var point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);
								
								// Ajax off the new coordinates
								var ajaxURL = "locations?action=store-gps&id=" + id
									+ "&latitude=" + place.Point.coordinates[1]
									+ "&longitude=" + place.Point.coordinates[0];
								
								//alert(ajaxURL);
								jQuery.ajax(ajaxURL);
								
								funcAddMarkerFromGLatLng(point);
							});
						}
					}
				});
			}
		});
	};
	
	$.fn.gstGoogleMapDelayed = function() {
		// Process each "map and location(s) wrapper"
		return this.each(function() {
			var theWrapper = this;
			
			// See if there's a map
			var theMap = jQuery(theWrapper).find(".gst-map")
			var elemMap = theMap.get(0);
			
			// Gather the locations we'll be displaying
			var locations = jQuery(theWrapper).find(".gst-map-location");
			
			if (!locations.length || !elemMap || !GBrowserIsCompatible()) {
				//alert("No locations, no map, or browser not compatible.");
				//alert("Locations: " + locations.length + "\nelemMap = " + elemMap);
				if (!locations.length) {
					//alert("No locations found");
				}
				if (!elemMap) {
					//alert("No map element found");
				}
				if (!GBrowserIsCompatible()) {
					//alert("Browser not compatible");
				}
			} else {
				// Unhide the map
				theMap.css("display", "block");
				
				// Create the map (don't forget to setCenter and add controls)
				var map = new GMap2(elemMap);
				
				// Initialize the map, centered on Knoxville, TN
				map.setCenter(new GLatLng(35.868673,-84.207716), 7);
				
				// Add a zoom/pan control
				map.addControl(new GLargeMapControl());
				
				//alert("Found " + locations.length + " locations.");
				
				// Create a bounding region
				var bounds = new GLatLngBounds();
				
				// Add each location
				locations.each(function(idx) {
					var theLocation = this;
					var $theLocation = jQuery(theLocation);
					
					setTimeout(function() {
						// The location's title
						var locationTitle = $theLocation.find(".gst-map-title").text();
						
						// The location's address
						var address = $theLocation.find(".gst-map-address").text();
						
						// The location's coordinates
						var latitude = $theLocation.find(".gst-map-latitude").text();
						var longitude = $theLocation.find(".gst-map-longitude").text();
						
						//alert("Location: " + locationTitle + " @ " + address);
						
						var funcAddMarkerFromGLatLng = function(point) {
							// Create a marker
							var marker = new GMarker(point);
							
							var marker_html = "<p class=\"title\">" + locationTitle + "</p><p class=\"address\">" + address + "</p>";
							
							// Give it click behavior
							GEvent.addListener(marker, "click", function() {
								this.openInfoWindowHtml(marker_html);
							});
							
							// Add the marker to map
							map.addOverlay(marker);
							
							
							// Start with the info window open
							//marker.openInfoWindowHtml(marker_html);
							
							// Smart zoom & center
							bounds.extend(point);
							map.setCenter(bounds.getCenter(), Math.min(12, map.getBoundsZoomLevel(bounds) - 1));
						};
						
						if (latitude && longitude) {
							// Create the map pin directly without geocoding
							funcAddMarkerFromGLatLng(new GLatLng(latitude, longitude));
						} else {
							if (address) {
								// Start a geocode request for the address
								var geocoder = new GClientGeocoder();
								geocoder.getLocations(address, function(response) {
									// Retrieve the object
									var place = response.Placemark[0];
									
									// Retrieve the latitude and longitude
									var point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);
									
									funcAddMarkerFromGLatLng(point);
								});
							}
						}
					}, (150*(idx + 1) + 300));
					
				});
			}
		});
	};
	
	/*
		Hierarchical Checkboxes
		
		When a checkbox becomes checked, check all checkboxes within the checkbox context.
		When a checkbox becomes unchecked, uncheck all checkboxes within the checkbox context.
	*/
	$.fn.gstHierarchicalCheckboxes = function() {
		var becomeChecked = function(cbox) {
			// All other checkboxes within the checkbox context
			var cboxes = $(cbox).parents("li").first().find("input[type='checkbox']").not(cbox);
			
			// Set the check state
			cboxes.attr("checked", true);
			
			// Disable
			cboxes.attr("disabled", true);
			
			// Throw a class on the parents
			var cboxparents = cboxes.parent();
			var classHasDisabled = "hierarchical-checkboxes-has-disabled";
			cboxparents.addClass(classHasDisabled);
		};
		var becomeUnchecked = function(cbox) {
			// All other checkboxes within the checkbox context
			var cboxes = $(cbox).parents("li").first().find("input[type='checkbox']").not(cbox);
			
			// Set the check state
			cboxes.attr("checked", false);
			
			// Enable
			cboxes.attr("disabled", false);
			
			// Throw a class on the parents
			var cboxparents = cboxes.parent();
			var classHasDisabled = "hierarchical-checkboxes-has-disabled";
			cboxparents.removeClass(classHasDisabled);
		};
		
		// Process each hierarchical-checkbox list
		return this.each(function() {
			var all_cboxes = $(this).find("input[type='checkbox']");
			
			// Assign click behavior to each checkbox
			all_cboxes.click(function(e) {
				var checkedState = $(this).attr("checked");
				//alert(checkedState);
				
				if (checkedState) {
					becomeChecked(this);
				} else {
					becomeUnchecked(this);
				}
				
			});
			
			all_cboxes.filter("[checked]").each(function() {
				becomeChecked(this);
			});
			
		});
	};
	
	/*
		Slideshow
		
		A simple crossfader, implemented in-house
	*/
	$.fn.gstSlideshow = function(pluginOptions) {
		
		var funcGetRandomElement = function(set) {
			// Choose a random index
			var idx = Math.floor(Math.random() * 2147483647) % set.length;
			
			//alert("randomly chosen idx = " + idx);
			//alert("randomly chosen idx = " + idx + "\nsrc = " + set.get(idx).src);
			
			return $(set.get(idx));
		};
		
		// Options with defaults
		pluginOptions = $.extend({
			fadeDuration: 1000,
			randomize: false,
			slideSelector: "",
			viewDuration: 4000,
			zIndexForTopSlide: 1000
		}, pluginOptions);
		
		// Type coercion
		//pluginOptions.fadeDuration = 1*pluginOptions.fadeDuration;
		//pluginOptions.startRandom = !!pluginOptions.startRandom;
		//pluginOptions.viewDuration = 1*pluginOptions.viewDuration;
		//pluginOptions.zIndexForTopSlide = 1*pluginOptions.zIndexForTopSlide;
		
		return this.each(function() {
			var slides = pluginOptions.slideSelector ? $(this).find(pluginOptions.slideSelector) : $(this).children();
			
			//alert("# of slides: " + slides.length);
			
			// Only activate when there are two or more slides
			if (slides.length >= 2) {
				//alert(pluginOptions + "\nstartRandom = " + pluginOptions.startRandom + "\nsecretOption = " + pluginOptions.secretOption);
				
				// Start at the first slide
				// Assume the first slide is already visible
				var currentSlide = slides.first();
				
				// Prep the current slide
				currentSlide.css({
					display: "block",
					opacity: 1,
					zIndex: (pluginOptions.zIndexForTopSlide - 1)
				});
				
				// Fetch the next slide
				var nextSlide = pluginOptions.randomize ? funcGetRandomElement(slides.not(currentSlide)) : slides.not(currentSlide).first();
				
				// Prep the next slide
				nextSlide.css({
					display: "block",
					opacity: 0,
					zIndex: pluginOptions.zIndexForTopSlide
				});
				
				var funcLoop = function() {
					// nextSlide is now fully faded in
					
					// Hide the current slide
					currentSlide.css({
						display: "none",
						opacity: 0
					});
					
					// Move nextSlide down into currentSlide's zIndex
					nextSlide.css({
						zIndex: (pluginOptions.zIndexForTopSlide - 1)
					});
					
					// nextSlide is now the new currentSlide
					currentSlide = nextSlide;
					
					// Get a new nextSlide
					nextSlide = pluginOptions.randomize ? funcGetRandomElement(slides.not(currentSlide)) : (pluginOptions.slideSelector ? currentSlide.nextAll(pluginOptions.slideSelector).first() : currentSlide.next());
					if (!nextSlide.length) {
						// No next slide available, loop back over (this should never happen with randomize enabled)
						nextSlide = slides.not(currentSlide).first();
					}
					
					// Prep the next slide
					nextSlide.css({
						display: "block",
						opacity: 0,
						zIndex: pluginOptions.zIndexForTopSlide
					});
					
					// Wait, then fade in the next slide
					nextSlide.delay(pluginOptions.viewDuration).animate({
						opacity: 1
					}, pluginOptions.fadeDuration, funcLoop);
				}
				
				// Wait, then start the fade-in loop (fade in the next slide)
				nextSlide.delay(pluginOptions.viewDuration).animate({
					opacity: 1
				}, pluginOptions.fadeDuration, funcLoop);
			}
		});
	};
	
	/*
		Sortable Table
		
		Adds drag-to-reorder functionality to tables with a styleable hover ghost and event hooks.
	*/
	$.fn.gstSortableTable = function(pluginOptions) {
		// Options with defaults
		pluginOptions = $.extend({
			// Fires after the rows have been re-ordered
			afterShunk: false, // function(Element:theTable, Element:removeRow, Element:hoverRow)
			// Fires after having dragged a row far enough to cause row re-ordering, but just before any rows have actually been re-ordered
			beforeShunk: false, // function(Element:theTable, Element:removeRow, Element:hoverRow)
			// Fires after the mouse-up that terminates dragging
			dragFinish: false, // function(Element:theTable, Element:dragRow, Boolean:tableChanged)
			// Fires as soon as dragging begins (not on the mouse-down)
			dragStart: false, // function(Element:theTable, Element:dragRow)
			rowSelector: "", // String: CSS selector for the rows to add sortability to
			zIndexForGhost: 1000
		}, pluginOptions);
		
		// Process each table
		return this.each(function() {
			
			var theTable = this;
			var $theTable = $(this);
			
			var stateMouseIsDown = false; // Bool: Whether the table is between mouse-down and mouse-up
			var stateFirstMouseMove = false; // Bool: Whether the next mouse-move received is the first since the mouse-down
			var stateChanged = false; // Row-swapping happened during a mouse-down
			
			var rowMouseDown = false; // Element: The row that received the mouse-down
			
			var hoverGhost; // Element: The hover ghost container
			
			var cursorDelta; // Object: The distance between the top-left corner of the clicked row and the cursor
			
			var funcGetRows = function() {
				var ret = $theTable.find("tbody tr");
				if (pluginOptions.rowSelector) {
					ret = ret.filter(pluginOptions.rowSelector);
				}
				return ret;
			};
			
			var funcMouseMove = function(event) {
				if (!stateMouseIsDown) {
					alert("Error: Mouse is up but mouse-move event was received.");
					return;
				}
				
				var theTableOffset = $theTable.offset();
				var rowOfInterestOffset = $(rowMouseDown).offset();
				
				//alert("theTableOffset.left = " + theTableOffset.left + "\n theTableOffset.top = " + theTableOffset.top + "\n rowOfInterestOffset.left = " + rowOfInterestOffset.left + "\n rowOfInterestOffset.top = " + rowOfInterestOffset.top);
				
				if (stateFirstMouseMove) {
					stateFirstMouseMove = false;
					
					// Create and attach the hover ghost element
					hoverGhost = document.createElement("div");
					hoverGhost.className = "gst-sortable-table-ghost";
					
					// Style up the hover ghost
					var theTableWidth = $theTable.width();
					$(hoverGhost).css({
						position: "absolute",
						left: -(theTableWidth + 100) + "px",
						top: 0,
						width: theTableWidth + "px",
						height: "1px", // Will be adusted later to show one table row
						overflow: "hidden",
						zIndex: pluginOptions.zIndexForGhost
					});
					
					// Create the table clone
					var tableClone = theTable.cloneNode(true);
					
					// Position the table clone so the row of interest is at the top of the hover ghost
					/*
					$(tableClone).css({
						position: "absolute",
						left: 0,
						top: -(rowOfInterestOffset.top - theTableOffset.top) + "px",
						width: theTableWidth + "px",
						margin: 0
					});
					$(hoverGhost).append(tableClone);
					*/
					$(tableClone).css({
						position: "absolute",
						left: 0,
						top: 0,
						width: theTableWidth + "px",
						margin: 0
					});
					$(hoverGhost).append(tableClone);
					
					//alert("rowOfInterestOffset.top = " + rowOfInterestOffset.top + "\ntheTableOffset.top = " + theTableOffset.top);
					
					// Size the hover ghost such that only the row of interest is visible
					$(hoverGhost).css("height", $(rowMouseDown).outerHeight() + "px");
					
					// Attach the finished hover ghost
					$("body").append(hoverGhost);
					
					var roiCopy = $(tableClone).find("tr.of-interest");
					var roiCopyPos = roiCopy.position();
					//alert(-roiCopyPos.top + "px");
					$(tableClone).css("top", -roiCopyPos.top + "px");
					
					// Event Hook: dragStart
					if (pluginOptions.dragStart) {
						pluginOptions.dragStart(theTable, rowMouseDown);
					}
				}
				
				// Position the hover ghost under the cursor
				$(hoverGhost).css({
					left: theTableOffset.left + "px",
					top: event.pageY - cursorDelta.y + "px"
				});
				
				// Check each row to see if the mouse is over that row
				funcGetRows().each(function() {
					var $this = $(this);
					var rowHeight = $this.height();
					var rowWidth = $this.width();
					var rowOffset = $this.offset();
					
					// How many pixels (vertically) down into the row is the cursor
					var inRowOffsetY = event.pageY - rowOffset.top;
					
					// Not the mouse-down row, and horizontal and vertical match
					if (
						rowMouseDown != this
						&& event.pageX >= rowOffset.left
						&& event.pageX < (rowOffset.left + rowWidth)
						&& event.pageY >= rowOffset.top
						&& event.pageY < (rowOffset.top + rowHeight)
					) {
						// Event Hook: beforeShunk
						if (pluginOptions.beforeShunk) {
							pluginOptions.beforeShunk(theTable, rowMouseDown, this);
						}
						
						// Remove rowMouseDown (the row being dragged) from the table
						rowMouseDown.parentNode.removeChild(rowMouseDown);
						
						// rowMouseDown now exists as a detached DOM node
						
						if (inRowOffsetY <= rowHeight / 2) {
							// The cursor entered from above the row
							// Insert rowMouseDown after the row the cursor is currently hovering over
							$this.after(rowMouseDown);
						} else {
							// The cursor entered from below the row
							// Insert rowMouseDown after the row the cursor is currently hovering over
							$this.before(rowMouseDown);
						}
						
						// Event Hook: afterShunk
						if (pluginOptions.afterShunk) {
							pluginOptions.afterShunk(theTable, rowMouseDown, this);
						}
						
						// There was a table row swap
						stateChanged = true;
						
					}
					
				});
				
			};
			
			// Activate any styles that depend on this table being javascript-enhanced
			$theTable.addClass("gst-sortable-table-scripted");
			
			// Turn off text selection within the table
			/*
			$theTable.attr('unselectable', 'on');
			$theTable.css({
				"-moz-user-select": "none",
				"-khtml-user-select": "none",
				"-webkit-user-select": "none",
				"user-select": "none"
			});
			$theTable.each(function() { 
				this.onselectstart = function() { return false; };
			});
			*/
			if ($.browser.mozilla) {
				$theTable.css({
					"-moz-user-select": "none"
				});
			} else if ($.browser.msie) {
				$theTable.attr('unselectable', 'on');
				$theTable.find("*").attr('unselectable', 'on'); // IE's "unselectable" does not inherit
			} else {
				$theTable.css({
					"-khtml-user-select": "none",
					"-webkit-user-select": "none"
				});
				$theTable.bind("mousedown.disableTextSelect", function() {
					return false;
				});
			}
			$theTable.css({
				"user-select": "none" // never hurts, should override vendor-specific styles
			});
			
			// Assign mouse-down handlers to the rows
			funcGetRows().mousedown(function(event) {
				stateMouseIsDown = true;
				stateFirstMouseMove = true;
				
				// Store which row has been mouse-down'ed
				rowMouseDown = this;
				
				$(rowMouseDown).addClass("of-interest");
				
				// Bind the mouse-move handler to the document
				$(this).parents().last().bind("mousemove", funcMouseMove);
				
				// Calculate the distance between the top-left corner of the row
				// and the cursor
				var rowOffset = $(rowMouseDown).offset();
				cursorDelta = {
					x: event.pageX - rowOffset.left,
					y: event.pageY - rowOffset.top
				};
				
			});
			
			// Assign a mouse-up handler to the document
			$theTable.parents().last().mouseup(function() {
				stateMouseIsDown = false;
				stateFirstMouseMove = false;
				
				$(rowMouseDown).removeClass("of-interest");
				
				// Unbind the mouse-move handler from the document
				$(this).unbind("mousemove", funcMouseMove);
				
				// Destroy the hover ghost
				if (hoverGhost) {
					$(hoverGhost).remove();
					hoverGhost = false;
				}
				
				// Event Hook: dragFinish
				if (pluginOptions.dragFinish) {
					pluginOptions.dragFinish(theTable, rowMouseDown, stateChanged);
				}
				
				// Reset stateChanged
				stateChanged = false;
			});
		});
	};
	
	/*
		Toggleable Definition Lists
		
		Allows the term group(s) of a DL to toggle the display of those terms' definition(s).
	*/
	$.fn.gstToggleable = function() {
		
		// Add "is active" class
		this.addClass("gst-toggleable-scripted");
		
		// On-click handlers on DTs
		this.children("dt").click(function() {
			var terms;
			var definitions;
			
			// Select myself and all adjacent terms (before and after)
			terms = $(this);
			terms = terms.add(terms.prevUntil("dd")).add(terms.nextUntil("dd"));
			
			// Toggle these terms
			terms.toggleClass("gst-toggleable-open");
			
			// Select all the definitions for this term group
			definitions = terms.last().nextUntil("dt");
			
			// Toggle the definitions
			definitions.toggleClass("gst-toggleable-open");
		});
		
		return this;
	};
	
})(jQuery);

