﻿/*
 * jQuery UI Autocomplete @VERSION
 *
 * Copyright (c) 2007, 2008 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 * 
 * http://docs.jquery.com/UI/Autocomplete
 *
 * Depends:
 *	ui.core.js
 */
(function($) {

$.widget("ui.autocomplete", {

	_init: function() {
		// TODO move these to instance properties?
		$.extend(this.options, {
			delay: this.options.delay != undefined ? this.options.delay : (this.options.url? this.options.ajaxDelay : this.options.localDelay),
			max: this.options.max != undefined ? this.options.max : (this.options.scroll? this.options.scrollMax : this.options.noScrollMax),
			highlight: this.options.highlight || function(value) { return value; }, // if highlight is set to false, replace it with a do-nothing function
			formatMatch: this.options.formatMatch || this.options.formatItem // if the formatMatch option is not specified, then use formatItem for backwards compatibility
		});

		var input = this.element[0],
			options = this.options,
			// Create $ object for input element
			$input = $(input).attr("autocomplete", "off").addClass(options.inputClass),
			KEY = $.ui.keyCode,
			previousValue = "",
			cache = $.ui.autocomplete.cache(options),
			hasFocus = 0,
			config = {
				mouseDownOnSelect: false
			},
			timeout,
			blockSubmit,
			lastKeyPressCode,
			select = $.ui.autocomplete.select(options, input, selectCurrent, config);

		if(options.result) {
			$input.bind('result.autocomplete', options.result);
		}

		// prevent form submit in opera when selecting with return key
		$.browser.opera && $(input.form).bind("submit.autocomplete", function() {
			if (blockSubmit) {
				blockSubmit = false;
				return false;
			}
		});

		// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
		$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
			// track last key pressed
			lastKeyPressCode = event.keyCode;
			switch(event.keyCode) {

				case KEY.UP:
					event.preventDefault();
					if ( select.visible() ) {
						select.prev();
					} else {
						onChange(0, true);
					}
					break;

				case KEY.DOWN:
					event.preventDefault();
					if ( select.visible() ) {
						select.next();
					} else {
						onChange(0, true);
					}
					break;

				case KEY.PAGE_UP:
					event.preventDefault();
					if ( select.visible() ) {
						select.pageUp();
					} else {
						onChange(0, true);
					}
					break;

				case KEY.PAGE_DOWN:
					event.preventDefault();
					if ( select.visible() ) {
						select.pageDown();
					} else {
						onChange(0, true);
					}
					break;

				// matches also semicolon
				case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
				case KEY.TAB:
				case KEY.ENTER:
					if( selectCurrent() ) {
						// stop default to prevent a form submit, Opera needs special handling
						event.preventDefault();
						blockSubmit = true;
						return false;
					}
					break;

				case KEY.ESCAPE:
					select.hide();
					break;

				default:
					clearTimeout(timeout);
					timeout = setTimeout(onChange, options.delay);
					break;
			}
		})
		.bind('focus.autocomplete', function(){
			// track whether the field has focus, we shouldn't process any
			// results if the field no longer has focus
			hasFocus++;
		})
		.bind('blur.autocomplete', function() {
			hasFocus = 0;
			if (!config.mouseDownOnSelect) {
				hideResults();
			}
		})
		.bind('click.autocomplete', function() {
			// show select when clicking in a focused field
			if ( hasFocus++ > 1 && !select.visible() ) {
				onChange(0, true);
			}
		}).bind("search.autocomplete", function() {
			// TODO why not just specifying both arguments?
			var fn = (arguments.length > 1) ? arguments[1] : null;
			function findValueCallback(q, data) {
				var result;
				if( data && data.length ) {
					for (var i=0; i < data.length; i++) {
						if( data[i].result.toLowerCase() == q.toLowerCase() ) {
							result = data[i];
							break;
						}
					}
				}
				if( typeof fn == "function" ) fn(result);
				else $input.trigger("result.autocomplete", result && [result.data, result.value]);
			}
			$.each(trimWords($input.val()), function(i, value) {
				request(value, findValueCallback, findValueCallback);
			});
		})
		.bind("flushCache.autocomplete", function() {
			cache.flush();
		})
		.bind("setOptions.autocomplete", function() {
			$.extend(options, arguments[1]);
			// if we've updated the data, repopulate
			if ( "data" in arguments[1] )
				cache.populate();
		})
		.bind("unautocomplete", function() {
			select.unbind();
			//$input.unbind(); << this would unbind all events. Bad news if we've had another plugin applied to this element. AAP 03.12.09
			$(input).unbind(".autocomplete");
			$(input.form).unbind(".autocomplete");
		});

		// Private methods
		function selectCurrent() {
			var selected = select.selected();
			if( !selected ) return false;

			var v = selected.result;
			previousValue = v;

			if ( options.multiple ) {
				var words = trimWords($input.val());
				if ( words.length > 1 ) {
					v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
				}
				v += options.multipleSeparator;
			}

			$input.val(v);
			hideResultsNow();
			$input.trigger("result.autocomplete", [selected.data, selected.value]);
			return true;
		};

		function onChange(crap, skipPrevCheck) {
			if( lastKeyPressCode == KEY.DELETE ) {
				select.hide();
				return;
			}

			var currentValue = $input.val();

			if ( !skipPrevCheck && currentValue == previousValue )
				return;

			previousValue = currentValue;

			currentValue = lastWord(currentValue);
			if ( currentValue.length >= options.minChars) {
				$input.addClass(options.loadingClass);
				if (!options.matchCase)
					currentValue = currentValue.toLowerCase();
				request(currentValue, receiveData, hideResultsNow);
			} else {
				stopLoading();
				select.hide();
			}
		};

		function trimWords(value) {
			if ( !value ) {
				return [""];
			}
			if ( !options.multiple ) {
				return [value];
			}
			var words = value.split( options.multipleSeparator );
			var result = [];
			$.each(words, function(i, value) {
				if ( $.trim(value) )
					result[i] = $.trim(value);
			});
			return result;
		};

		function lastWord(value) {
			var words = trimWords(value);
			return words[words.length - 1];
		};

		// fills in the input box w/the first match (assumed to be the best match)
		// q: the term entered
		// sValue: the first matching result
		function autoFill(q, sValue){
			// autofill in the complete box w/the first match as long as the user hasn't entered in more data
			// if the last user key pressed was backspace, don't autofill
			if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != $.ui.keyCode.BACKSPACE ) {
				// fill in the value (keep the case the user has typed)
				$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
				// select the portion of the value not typed by the user (so the next character will erase)
				$.ui.autocomplete.selection(input, previousValue.length, previousValue.length + sValue.length);
			}
		};

		function hideResults() {
			clearTimeout(timeout);
			timeout = setTimeout(hideResultsNow, 200);
		};

		function hideResultsNow() {
			var wasVisible = select.visible();
			select.hide();
			clearTimeout(timeout);
			stopLoading();
			if (options.mustMatch) {
				// call search and run callback
				$input.autocomplete("search", function (result){
						// if no value found, clear the input box
						if( !result ) {
							if (options.multiple) {
								var words = trimWords($input.val()).slice(0, -1);
								$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
							}
							else
								$input.val( "" );
						}
					}
				);
			}
			if (wasVisible)
				// position cursor at end of input field
				$.ui.autocomplete.selection(input, input.value.length, input.value.length);
		};

		function receiveData(q, data) {
			if ( data && data.length && hasFocus ) {
				stopLoading();
				select.display(data, q);
				autoFill(q, data[0].value);
				select.show();
			} else {
				hideResultsNow();
			}
		};

		function request(term, success, failure) {
			if (!options.matchCase)
				term = term.toLowerCase();
			var data = cache.load(term);
			// recieve the cached data
			if (data && data.length) {
				success(term, data);
			} // if an AJAX url has been supplied, try loading the data now
			else if( (typeof options.url == "string") && (options.url.length > 0) ){

				var extraParams = {
					timestamp: +new Date()
				};
				$.each(options.extraParams, function(key, param) {
					extraParams[key] = typeof param == "function" ? param(term) : param;
				});

				$.ajax({
					// try to leverage ajaxQueue plugin to abort previous requests
					mode: "abort",
					// limit abortion to this input
					port: "autocomplete" + input.name,
					dataType: options.dataType,
					url: options.url,
					data: $.extend({
						q: lastWord(term),
						limit: options.max
					}, extraParams),
					success: function(data) {
						var parsed = options.parse && options.parse(data) || parse(data);
						cache.add(term, parsed);
						success(term, parsed);
					}
				});
			}

			else if (options.source && typeof options.source == 'function') {
				var resultData = options.source(term);
				var parsed = (options.parse) ? options.parse(resultData) : resultData;

				cache.add(term, parsed);
				success(term, parsed);
			} else {
				// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
				select.emptyList();
				failure(term);
			}
		};

		function parse(data) {
			var parsed = [];
			var rows = data.split("\n");
			for (var i=0; i < rows.length; i++) {
				var row = $.trim(rows[i]);
				if (row) {
					row = row.split("|");
					parsed[parsed.length] = {
						data: row,
						value: row[0],
						result: options.formatResult && options.formatResult(row, row[0]) || row[0]
					};
				}
			}
			return parsed;
		};

		function stopLoading() {
			$input.removeClass(options.loadingClass);
		};

	}, //End _init
	
	_propagate: function(n, event) {
		$.ui.plugin.call(this, n, [event, this.ui()]);
		return this.element.triggerHandler(n == 'autocomplete' ? n : 'autocomplete'+n, [event, this.ui()], this.options[n]);
	},

	// Public methods
	ui: function(event) {
		return {
			options: this.options,
			element: this.element
		};
	},
	result: function(handler) {
		return this.element.bind("result.autocomplete", handler);
	},
	search: function(handler) {
		return this.element.trigger("search.autocomplete", [handler]);
	},
	flushCache: function() {
		return this.element.trigger("flushCache.autocomplete");
	},
	setData: function(key, value){
		return this.element.trigger("setOptions.autocomplete", [{ key: value }]);
	},
	destroy: function() {
		this.element
			.removeAttr('disabled')
			.removeClass('ui-autocomplete-input');
		return this.element.trigger("unautocomplete");
	},
	enable: function() {
		this.element
			.removeAttr('disabled')
			.removeClass('ui-autocomplete-disabled');
		this.disabled = false;
	},
	disable: function() {
		this.element
			.attr('disabled', true)
			.addClass('ui-autocomplete-disabled');
		this.disabled = true;
	}
});

$.extend($.ui.autocomplete, {
	defaults: {
		inputClass: "ui-autocomplete-input",
		resultsClass: "ui-widget ui-widget-content ui-autocomplete-results",
		loadingClass: "ui-autocomplete-loading",
		minChars: 1,
		ajaxDelay: 400,
		localDelay: 10,
		matchCase: false,
		matchSubset: true,
		matchContains: false,
		cacheLength: 10,
		scrollMax: 150,
		noScrollMax: 10,
		mustMatch: false,
		extraParams: {},
		selectFirst: true,
		formatItem: function(row) { return row[0]; },
		formatMatch: null,
		autoFill: false,
		width: 0,
		multiple: false,
		multipleSeparator: ", ",
		highlight: function(value, term) {
			return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
		},
		scroll: true,
		scrollHeight: 180
	}
});

$.ui.autocomplete.cache = function(options) {

	var data = {};
	var length = 0;

	function matchSubset(s, sub) {
		if (!options.matchCase)
			s = s.toLowerCase();
		var i = s.indexOf(sub);
		if (i == -1) return false;
		return i == 0 || options.matchContains;
	};

	function add(q, value) {
		if (length > options.cacheLength){
			flush();
		}
		if (!data[q]){ 
			length++;
		}
		data[q] = value;
	}

	function populate(){
		if( !options.data ) return false;
		// track the matches
		var stMatchSets = {},
			nullData = 0;

		// no url was specified, we need to adjust the cache length to make sure it fits the local data store
		if( !options.url ) options.cacheLength = 1;

		// track all options for minChars = 0
		stMatchSets[""] = [];

		// loop through the array and create a lookup structure
		for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
			var rawValue = options.data[i];
			// if rawValue is a string, make an array otherwise just reference the array
			rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;

			var value = options.formatMatch(rawValue, i+1, options.data.length);
			if ( value === false )
				continue;

			var firstChar = value.charAt(0).toLowerCase();
			// if no lookup array for this character exists, look it up now
			if( !stMatchSets[firstChar] )
				stMatchSets[firstChar] = [];

			// if the match is a string
			var row = {
				value: value,
				data: rawValue,
				result: options.formatResult && options.formatResult(rawValue) || value
			};

			// push the current match into the set list
			stMatchSets[firstChar].push(row);

			// keep track of minChars zero items
			if ( nullData++ < options.max ) {
				stMatchSets[""].push(row);
			}
		};

		// add the data items to the cache
		$.each(stMatchSets, function(i, value) {
			// increase the cache size
			options.cacheLength++;
			// add to the cache
			add(i, value);
		});
	}

	// populate any existing data
	setTimeout(populate, 25);

	function flush(){
		data = {};
		length = 0;
	}

	return {
		flush: flush,
		add: add,
		populate: populate,
		load: function(q) {
			if (!options.cacheLength || !length)
				return null;
			/* 
			 * if dealing w/local data and matchContains than we must make sure
			 * to loop through all the data collections looking for matches
			 */
			if( !options.url && options.matchContains ){
				// track all matches
				var csub = [];
				// loop through all the data grids for matches
				for( var k in data ){
					// don't search through the stMatchSets[""] (minChars: 0) cache
					// this prevents duplicates
					if( k.length > 0 ){
						var c = data[k];
						$.each(c, function(i, x) {
							// if we've got a match, add it to the array
							if (matchSubset(x.value, q)) {
								csub.push(x);
							}
						});
					}
				}
				return csub;
			} else 
			// if the exact item exists, use it
			if (data[q]){
				return data[q];
			} else
			if (options.matchSubset) {
				for (var i = q.length - 1; i >= options.minChars; i--) {
					var c = data[q.substr(0, i)];
					if (c) {
						var csub = [];
						$.each(c, function(i, x) {
							if (matchSubset(x.value, q)) {
								csub[csub.length] = x;
							}
						});
						return csub;
					}
				}
			}
			return null;
		}
	};
};

$.ui.autocomplete.select = function (options, input, select, config) {
	var CLASSES = {
		//ACTIVE: "ui-state-active" << From the Css Framework Docs : .ui-state-active: Class to be applied on mousedown to clickable button-like elements.
		//Since this isnt a button element, but a hybrid 'menu item', use a custom class.
		//See: http://jqueryui.pbwiki.com/Menu 2- Visual Design
		DEFAULT: 'ui-autocomplete-state-default',
		ACTIVE: 'ui-autocomplete-state-active'
	};

	var listItems,
		active = -1,
		data,
		term = "",
		needsInit = true,
		element,
		list;

	// Create results
	function init() {
		if (!needsInit) return;
		element = $("<div/>")
		.hide()
		.addClass(options.resultsClass)
		//.css("position", "absolute") set this up in the css in case users want to do something wonky with the positioning.
		.appendTo(document.body);

		list = $("<ul/>").appendTo(element).mouseover( function(event) {
			var e = target(event);
			if(e.nodeName && e.nodeName.toUpperCase() == 'LI') {
				active = $("li", list).removeClass(CLASSES.ACTIVE).index(e);
				$(e).addClass(CLASSES.ACTIVE);
			}
		}).click(function(event) {
			$(target(event)).addClass(CLASSES.ACTIVE);
			select();
			// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
			input.focus();
			return false;
		}).mousedown(function() {
			config.mouseDownOnSelect = true;
		}).mouseup(function() {
			config.mouseDownOnSelect = false;
		});

		if( options.width > 0 )
			element.css("width", options.width);

		needsInit = false;
	} 

	function target(event) {
		var element = event.target;
		while(element && element.tagName != "LI")
			element = element.parentNode;
		// more fun with IE, sometimes event.target is empty, just ignore it then
		if(!element)
			return [];
		return element;
	}

	function moveSelect(step) {
		listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
		movePosition(step);
		var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
		if(options.scroll) {
			var offset = 0;
			listItems.slice(0, active).each(function() {
				offset += this.offsetHeight;
			});
			if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
				list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
			} else if(offset < list.scrollTop()) {
				list.scrollTop(offset);
			}
		}
	};

	function movePosition(step) {
		active += step;
		if (active < 0) {
			active = listItems.size() - 1;
		} else if (active >= listItems.size()) {
			active = 0;
		}
	}

	function limitNumberOfItems(available) {
		return options.max && options.max < available
			? options.max
			: available;
	}

	function fillList() {
		list.empty();
		var max = limitNumberOfItems(data.length);
		for (var i=0; i < max; i++) {
			if (!data[i])
				continue;
			var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
			if ( formatted === false )
				continue;
			var li = $("<li/>")
				.html( options.highlight(formatted, term) )
				.addClass(i%2 == 0 ? "ui-autocomplete-even" : "ui-autocomplete-odd")
				.addClass(CLASSES.DEFAULT)
				.appendTo(list)[0];
			$.data(li, "ui-autocomplete-data", data[i]);
		}
		listItems = list.find("li");
		if ( options.selectFirst ) {
			listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
			active = 0;
		}
		// apply bgiframe if available
		if ( $.fn.bgiframe )
			list.bgiframe();
	}

	return {
		display: function(d, q) {
			init();
			data = d;
			term = q;
			fillList();
		},
		next: function() {
			moveSelect(1);
		},
		prev: function() {
			moveSelect(-1);
		},
		pageUp: function() {
			if (active != 0 && active - 8 < 0) {
				moveSelect( -active );
			} else {
				moveSelect(-8);
			}
		},
		pageDown: function() {
			if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
				moveSelect( listItems.size() - 1 - active );
			} else {
				moveSelect(8);
			}
		},
		hide: function() {
			element && element.hide();
			listItems && listItems.removeClass(CLASSES.ACTIVE)
			active = -1;
			$(input).triggerHandler("autocompletehide", [{}, { options: options }], options["hide"]);
		},
		visible : function() {
			return element && element.is(":visible");
		},
		current: function() {
			return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
		},
		show: function() {
			var offset = $(input).offset();
			element.css({
				width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
				top: offset.top + input.offsetHeight,
				left: offset.left
			}).show();

			if(options.scroll) {
				list.scrollTop(0);
				list.css({
					maxHeight: options.scrollHeight,
					overflow: 'auto'
				});

				if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
					var listHeight = 0;
					listItems.each(function() {
						listHeight += this.offsetHeight;
					});
					var scrollbarsVisible = listHeight > options.scrollHeight;
					list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
					if (!scrollbarsVisible) {
						// IE doesn't recalculate width when scrollbar disappears
						listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
					}
				}

			}

			$(input).triggerHandler("autocompleteshow", [{}, { options: options }], options["show"]);

		},
		selected: function() {
			var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
			return selected && selected.length && $.data(selected[0], "ui-autocomplete-data");
		},
		emptyList: function (){
			list && list.empty();
		},
		unbind: function() {
			element && element.remove();
		}
	};
};

$.ui.autocomplete.selection = function(field, start, end) {
	if( field.createTextRange ){
		var selRange = field.createTextRange();
		selRange.collapse(true);
		selRange.moveStart("character", start);
		selRange.moveEnd("character", end);
		selRange.select();
	} else if( field.setSelectionRange ){
		field.setSelectionRange(start, end);
	} else {
		if( field.selectionStart ){
			field.selectionStart = start;
			field.selectionEnd = end;
		}
	}
	field.focus();
};

})(jQuery);


//// jQuery Extensions ////
( function( $ ) {
  $.template = function( str ) {
    return new $.template.instance( str );
  };
  $.template.instance = function( str ) {
    this.text = str;
  };
  
  $.extend( $.template.instance.prototype, {
    expand: function( replacements ) {
      var o_str = this.text;
      $.each( replacements, function( key, val ) {
        o_str = o_str.replace( new RegExp( "#{" + key + "}", "g" ), val );
      });
      return o_str;
    }
  });
})( jQuery );
//// Templates ////
var TEMPLATES = {
  recipe_grab: $.template( '\
<form id="#{form_id}" action="#{href}" method="POST" style="display: none;">\n\
<input type="hidden" name="authenticity_token" value="#{token}" />\n\
</form>\n\
'),
  tag_edit_form: $.template( '\
<form id="edit_tags_#{id}" class="edit_tags" name="#{id}">\n\
<input name="tag_class" type="hidden" value="#{tag_class}" />\n\
<input type="text" id="edit_tag_field_#{id}" name="tags" size="35" value="#{tags}" />\n\
<input type="submit" class="submit" value="Save" />\n\
<a class="tag_edit_complete">Cancel</a>\n\
</form>\n\
'),
  noise_comment_form: $.template( '\
<div class="table_row_plain_spaced">\n\
<div id="noise_comment_error_#{id}" class="errorExplanation" style="display: none;">\n\
<h2>Sorry, comment was too short.</h2>\n\
</div>\n\
<form id="noise_comment_#{id}" class="noise_comment" name="#{id}">\n\
<p>\n\
<textarea rows="3" onfocus="field_focus( this, \'Write your message or reply here\' );" name="noise[message]" cols="40">Write your message or reply here</textarea><br />\n\
</p>\n\
<input type="submit" class="submit" value="Add comment" />\n\
| <a class="noise_comment_cancel" href="#">Cancel</a>\n\
<input name="noise[recipe_id]" type="hidden" value="#{id}" />\n\
</form>\n\
</div>\n\
'),
  search_tag_add: $.template( '\
<a title="Remove \'#{tag_name}\' tag from search" id="#{id}" class="search_tag #{tag_type}" name="#{escaped_name}">\n\
<b>#{tag_name}</b> ×\n\
</a>\n\
'),
  tag_custom: $.template( '\
    <a class="#{class_name}" :title="Add \'#{tag_name}\' tag to search" href="#">#{tag_name}</a>\n\
')
};
var LINKS = {
  follow: $.template( "/users/#{id}/follow" ),
  stop_following: $.template( "/users/#{id}/stop_following" )
}
//// On load ////
$( document ).ready( function() {
  //// General form functions - Send ////
  $( "a.send_show" ).live( "click", function() {
    send_show( this );
    return false;
  });
  
  send_form_bind( "form#new_invitation" );
  //// Friends ////
  $( "a.follow" ).live( "click", function() {
    friend_action( this, "follow" );
    return false;
  });
  $( "a.stop_following" ).live( "click", function() {
    friend_action( this, "stop_following", { method_delete: true } );
    return false;
  });
  //// Images ////
  $( "a.image_clear" ).live( "click", function() {
    image_clear( this );
    return false;
  });
  $( "a.image_delete" ).live( "click", function() {
    image_delete( this );
    return false;
  });
  //// Noise - Comment ////
  $( "a.add_comment" ).live( "click", function() {
    noise_comment( this );
    return false;
  });
  $( "a.noise_comment_cancel" ).live( "click", function() {
    noise_comment_complete( this );
    return false;
  });
  //// Recipe - Sort ////
  $( "a.sort_by" ).live ( "click", function () {
    recipes_sort_by( this );
    return false;
  });
  //// Recipe - Delete ////
  $( "a.recipe_delete" ).live( "click", function() {
    recipe_delete( this );
    return false;
  });
  //// Recipe - Format ////
  $( "textarea.preview_req" ).live( "keyup", function() {
    resize_textarea( this );
    format_preview( this );
  });
  $( "textarea.preview_req" ).live( "click", function() {
    resize_textarea( this );
    format_preview( this );
    return false;
  });
  $( "textarea.preview_req" ).each( function() {
    resize_textarea( this );
    format_preview( this );
  });
  $( "textarea.preview_req_hide" ).each( function() {
    format_preview( this, true );
  });
  //// Recipe - Grab ////
  $( "a.recipe_grab" ).live( "click", function() {
    recipe_grab( this );
    return false;
  });
  //// Recipe - Rate ////
  $( "a.star_rate" ).live( "click", function() {
    star_rate( this );
    return false;
  });
  //// Recipe - Tag edit ////
  $( "a.edit_tags" ).live( "click", function() {
    tag_edit( this );
    return false;
  });
  
  $( "a.tag_edit_complete" ).live( "click", function() {
    tag_edit_complete( this );
    return false;
  });
  //// Search ////
  $( "#search" ).live( "keyup", function() {
    search_run();
  });
  $( "a.tag:not(.search_tag)" ).live( "click", function() {
    search_tag_add( this, true );
    search_run();
    return false;
  });
  $( "a.search_tag" ).live( "click", function() {
    search_tag_remove( this );
    search_run();
    return false;
  });
  
  $( "a.clear_search" ).live( "click", function() {
    search_clear();
    return false;
  });
  //// User ////
  $( "a.reset_password" ).live( "click", function() {
    user_password_reset( this );
    return false;
  });
});
//// Auto-complete ////
function tag_completion( tag_list ) {
  $( document ).ready( function() {
    $( "#search" ).autocomplete({
      data: tag_array,
      max: 4,
      multiple: true,
      multipleSeparator: " ",
      scroll: false
    });
    $( "#search" ).autocomplete( "result", function() {
      search_run();
    });
  });
}
//// Friends ////
function friend_action( dom_elem, action, options ) {
  var jq_elem = $( dom_elem );
  var jq_div = jq_elem.parent().parent().parent();
  var title = jq_elem.attr( "title" );
  var id = jq_elem.attr( "name" );
  var url = LINKS[ action ].expand({ id: id });
  var data = { authenticity_token: TOKEN };
  
  if( typeof( options ) != "undefined" && options[ "method_delete" ]) {
    data[ "_method" ] = "delete";
  }
  $.ajax({
    type: "post",
    url: url,
    data: data,
    beforeSend: function() {
      jq_div.empty();
      jq_div.append( "<p>" + $( "#search_progress_holder" ).clone().html() + "</p>" );
    },
    error: function( response, code ) {
      $( "#flash_error" ).html( response.responseText );
      $( "#error_notification" ).show();
      jq_div.empty();
      jq_div.append( "<p>" + response.responseText + "</p>" );
    },
    success: function( response, code ) {
      jq_div.empty();
      jq_div.append( "<p>" + response + "</p>" );
      highlight( jq_div );
    },
    dataType: "html"
  });
}
//// General form functions ////
function field_focus( element, hint ) {
  var elem = $( element );
  if( escape( elem.val() ).replace( /%0D/g, "" ) == escape( hint )) {
    elem.val( "" );
  }
}
function field_blur( element, hint ) {
  var elem = $( element );
  if( escape( elem.val() ).replace( /%0D/g, "" ) == "" ) {
    elem.val( hint );
  }
}
function hide_notifications() {
  $( "#error_notification" ).hide();
  $( "#feedback_notification" ).hide();
}
function highlight( elem_id, colour ) {
  colour = colour || "#ffc20e";
  $( elem_id ).effect( "highlight", { color: colour }, 3000 );
}
function disable_during_submit( dom_elem ) {
  var jq_elem = $( dom_elem );
  jq_elem.children().attr( "disabled", "disabled" );
  jq_elem.fadeTo( "slow", 0.4 );
}
function enable_on_error_after_submit( dom_elem ) {
  var jq_elem = $( dom_elem );
  jq_elem.children().removeAttr( "disabled" );
  jq_elem.fadeTo( "slow", 1.0 );
}
//// General form functions - Send ////
function send_show( dom_elem ) {
  var jq_elem = $( dom_elem );
  var id = "#" + jq_elem.attr( "name" );
  slide_toggle( id );
  $( id + " input:visible:first" ).focus();
}
function send_form_bind( dom_elem ) {
  var jq_elem = $( dom_elem );
  jq_elem.bind( "submit", function() {
    send_form_submit( this );
    return false;
  });
}
function send_form_submit( dom_elem ) {
  var jq_elem = $( dom_elem );
  var jq_div = jq_elem.parent();
  var url = jq_elem.attr( "action" );
  var dom_id = jq_elem.attr( "id" );
  
  $.ajax({
    type: "post",
    url: url,
    data: jq_elem.serialize(),
    beforeSend: function() {
      disable_during_submit( jq_elem );
    },
    error: function( response, code ) {
      jq_div.empty();
      jq_div.replaceWith( response.responseText );
      send_form_bind( "#" + dom_id );
    },
    success: function( response, code ) {
      $( "#flash_notice" ).html( response );
      $( "#feedback_notification" ).show();
      jq_div.empty();
      jq_div.append( "<p>" + response + "</p>" );
      slide_toggle( jq_div );
    },
    dataType: "html"
  });
}
function slide_toggle( dom_elem ) {
  var jq_elem = $( dom_elem );
  
  if( jq_elem.is( ":visible" )) {
    jq_elem.slideUp( 200 );
  }
  else {
    jq_elem.slideDown( 400 );
  }
}
//// Images ////
function image_clear( dom_elem ) {
  var jq_elem = $( dom_elem );
  
  jq_elem.prev().val( "" );
}
function image_delete( dom_elem ) {
  var jq_elem = $( dom_elem );
  jq_elem.siblings( ".should_destroy" ).val( 1 );
  jq_elem.parent().hide();
  
  // after marking old image for deletion & hiding it - display a new upload dialog to load an image
  $( "#image_upload_form" ).show();
}
//// Noise - Comment ////
function noise_comment( dom_elem ) {
  var jq_elem = $( dom_elem );
  var id = jq_elem.attr( "name" );
  
  jq_elem.hide();
  jq_elem.after( TEMPLATES[ "noise_comment_form" ].expand({ id: id }));
  var jq_form = jq_elem.next().find( "form" );
  jq_form.find( ":input:first" ).focus();
  jq_form.bind( "submit", function() {
    noise_comment_save( this );
    return false;
  });
}
function noise_comment_save( dom_elem ) {
  var jq_elem = $( dom_elem );
  var jq_cancel = jq_elem.children( "a.noise_comment_cancel" )
  var id = jq_elem.attr( "name" );
  var jq_elem_error = $( "#noise_comment_error_" + id );
  var url = "/noises";
  $.ajax({
    type: "post",
    url: url,
    data: jq_elem.serialize() + "&authenticity_token=" + TOKEN,
    beforeSend: function() {
      disable_during_submit( jq_elem );
      jq_cancel.hide();
    },
    error: function( response, code ) {
      jq_elem_error.children( "h2" ).text( response.responseText );
      jq_elem_error.show();
      enable_on_error_after_submit( jq_elem );
      jq_cancel.show();
    },
    success: function( response, code ) {
      var elem_new = jq_elem.parent().prev().before( response ).prev();
      highlight( elem_new.children( ".table_noise" ));
      noise_comment_complete( jq_cancel );
    },
    dataType: "html"
  });
}
function noise_comment_complete( dom_elem ) {
  var jq_elem = $( dom_elem );
  var jq_div = jq_elem.parent().parent();
  var jq_link = jq_div.prev( "a.add_comment" );
  jq_div.remove();
  jq_link.show();
}
//// Print view ////
function page_view( new_style ) {
  set_style_sheet( new_style );
  if( new_style == "print" ) {
    window.print();
  }
}
function set_style_sheet( new_style ) {
  for ( i = 0; i < document.styleSheets.length; i++ ) {
    document.styleSheets[ i ].disabled = true;
    if ( document.styleSheets[ i ].title == new_style ) {
      document.styleSheets[ i ].disabled = false;
    }
  }
}
//// Recipes ////
//// Recipes - Sort By ////
function recipes_sort_by( dom_elem ) {
  var jq_elem = $( dom_elem );
  var jq_div_before = $( "#recipe_list_index" );
  var url = "/recipes/sort_by/" + jq_elem.attr( "name" );
  $.ajax({
    type: "post",
    url: url,
    data: ({
      authenticity_token: TOKEN
    }),
    beforeSend: function() {
      var jq_progress = $( "#search_progress_holder" ).clone();
      jq_div_before.before( "<div id='progress_bar' class='table_row'><p>" + jq_progress.html() + "</p></div>" );
      hide_notifications();
    },
    complete: function( response, code ) {
      $( "#progress_bar" ).remove();
      jq_div_before.empty();
      jq_div_before.append( response.responseText );
    },
    dataType: "html"
  });
}
//// Recipes - Delete ////
function recipe_delete( dom_elem ) {
  var jq_elem = $( dom_elem );
  var recipe_page = ( $( ".recipe" ).length > 0 );
  var jq_div = ( recipe_page ? $( ".recipe:first" ) : jq_elem.parent().parent().parent() );
  var title = jq_elem.attr( "title" );
  var id = jq_elem.attr( "name" );
  var url = "/recipes/" + id;
  $.ajax({
    type: "post",
    url: url,
    data: ({
      _method: "delete",
      authenticity_token: TOKEN
    }),
    beforeSend: function() {
      var is_deleting = confirm( title + "?" );
      if( is_deleting ) {
        jq_div.empty();
        var jq_progress = $( "#search_progress_holder" ).clone().wrap( "<p></p>" );
        if( recipe_page ) {
          jq_progress.wrap( "<div class='table_row'></div>" );
        }
        jq_div.append( jq_progress.html() );
        hide_notifications();
      }
      return is_deleting;
    },
    complete: function( response, code ) {
      jq_div.empty();
      var html_response = "<p>" + response.responseText + "</p>";
      var jq_highlight = jq_div;
      if( recipe_page ) {
        html_response = "<div class='table_row'>" + html_response + "</div>";
      }
      jq_div.append( html_response );
      if( recipe_page ) {
        jq_highlight = jq_div.children();
      }
      highlight( jq_highlight );
    },
    dataType: "html"
  });
}
//// Recipes - Format ////
function format_preview( dom_raw, hide_source ) {
  hide_source = hide_source || false;
  var jq_raw = $( dom_raw );
  var prefix = jq_raw.attr( "id" ).replace( "_raw", "" );
  var jq_preview = $( "#" + prefix + "_preview" );
  var src_type = prefix;
  var src_txt = "";
  var c_txt = "";
  
  // get content depending type of the raw element: paragraph (p) or textbox
  src_txt = jq_raw.val() || jq_raw.html();
  
  if( src_type == "ingredients" || src_type == "steps" ) {
    c_txt = format_list( src_txt, src_type );
  }
  else {
    c_txt = format_default( src_txt, src_type );
  }
  
  jq_preview.empty();
  jq_preview.append( c_txt );
  if( hide_source ) {
    jq_raw.hide();
   }
}
function format_list( src_txt, h1_class ) {
  list_type = h1_class == "steps" ? "ol" : "ul";
  h1_class = h1_class || "ingredients";
  
  var c_txt = src_txt;
  if( typeof( c_txt ) == "undefined" ) {
      return "";
  }
  //remove empty whitespace at the start of lines
  c_txt = c_txt.replace( /^\s+/mg, "" );
  //remove empty lines \s already picks this up?
  c_txt = c_txt.replace( /[\r\n]{1,2}/mg, "\n" );
  //remove bullets [-~#*•]
  c_txt = c_txt.replace( /^\s*[-~#\*•]{1,3}\s/mg, "" );
  //remove numbers 1. 2)
  c_txt = c_txt.replace( /^\d+\s?[-~=\)\.\*•]\s([^\d])/mg, "$1" );
  //make everything a list item (including headings)
  c_txt = c_txt.replace( /^([^\n]+)/mg, "<li>$1</li>\n" );
  //convert the h1. lines to html headings
  c_txt = c_txt.replace( /^<li>h1\.\s*(.*)<\/li>/mg, "<h1 class='" + h1_class + "'>$1</h1>" );
  //add list tags around each list item section
  c_txt = c_txt.replace( /(<\/h1>)(\n+<li)/g, "$1\n<" + list_type + ">$2" );
  c_txt = c_txt.replace( /(^<li)/g, "<" + list_type + ">\n$1" );
  c_txt = c_txt.replace( /(<\/li>)(\n+<h1)/g, "$1\n</" + list_type + ">$2" );
  c_txt = c_txt.replace( /(<\/li>$)/g, "$1\n  </" + list_type + ">" );
  return c_txt;
}
  
function format_default( src_txt, h1_class ) {
  h1_class = h1_class || "ingredients";
  var c_txt = src_txt;
  //convert the h1. lines to html headings
  c_txt = c_txt.replace( /^\s*h1\.\s*(.*)$/mg, "<h1 class='" + h1_class + "'>$1</h1>" );
  c_txt = c_txt.replace( /^\n/mg, "" );
  c_txt = c_txt.replace( /"\b(.+?)\b":([^\s]+)/g, '<a rel="nofollow" href="$2">$1</a>' );
  c_txt = c_txt.replace( /([^">])(http:\/\/[^\s]+\b)/g, '$1<a rel="nofollow" href="$2">$2</a>' );
  c_txt = c_txt.replace( /\n/mg, "<br />" );
  
  return c_txt;
}
function resize_textarea( dom_textarea ) {
  var jq_textarea = $( dom_textarea );
  var max_lines = 50;
  var extra_lines = 1;
  var wrapped_extra_ratio = 1.1;
  var extra_ratio_limit = 3;
  // estimate number of lines needed
  var needed_lines = 0;
  var lines = jq_textarea.val().split( "\n" );
  for( var i = 0; i < lines.length; ++i ) {
    estimated_wrapped_lines = Math.ceil(( lines[i].length + 1 ) / jq_textarea.attr( "cols" ));
    needed_lines += estimated_wrapped_lines > extra_ratio_limit ? estimated_wrapped_lines * wrapped_extra_ratio : estimated_wrapped_lines;
  }
  
  jq_textarea.attr( "rows", Math.min( max_lines, needed_lines + extra_lines ));
}
//// Recipes - Grab ////
function recipe_grab( dom_elem ) {
  var jq_elem = $( dom_elem );
  var id = jq_elem.attr( "name" );
  var url = "/recipes/" + id + "/grab";
  var form_id = "recipe_grab_form_" + id;
  jq_elem.after( TEMPLATES[ "recipe_grab" ].expand({
    form_id: form_id,
    href: url,
    token: TOKEN
  }));
  $( "#" + form_id ).trigger( "submit" );
}
//// Recipes - Rate ////
function star_rate( dom_elem ) {
  var jq_elem = $( dom_elem );
  var jq_star_span = jq_elem.parent().parent();
  var jq_star_div = jq_star_span.parent();
  var id = jq_star_span.attr( "name" );
  var url = "/recipes/" + id + "/rate";
  $.ajax({
    type: "post",
    url: url,
    data: ({
      rating: $.trim( jq_elem.text() ),
      authenticity_token: TOKEN
    }),
    beforeSend: function() {
      jq_star_div.empty();
      jq_star_div.append( $( "#search_progress_holder" ).clone().html() );
    },
    complete: function( response, code ) {
      jq_star_div.empty();
      jq_star_div.append( response.responseText );
    },
    dataType: "html"
  });
}
//// Recipes - Tag edit ////
function tag_edit( dom_elem ) {
  var jq_elem = $( dom_elem );
  var jq_tag_span = jq_elem.parent();
  var id = jq_tag_span.attr( "name" );
  var jq_tag_section = $( "#tag_list_" + id );
  var tags = new Array();
  
  jq_tag_span.children( ".tag" ).each( function( ix, item ) {
    tags.push( $.trim( $( item ).text() ));
  });
  
  jq_tag_span.hide();
  jq_tag_span.after( TEMPLATES[ "tag_edit_form" ].expand( { id: id, tag_class: jq_tag_section.attr( "class" ), tags: tags.join( " " ).replace( /"/g, "&quot;" )}));
  $( "#edit_tag_field_" + id ).focus();
  jq_tag_span.siblings( "#edit_tags_" + id ).bind( "submit", function() {
    tag_save( this );
    return false;
  });
}
function tag_update( id, content ) {
  var jq_tag_section = $( "#tag_list_" + id );
  jq_tag_section.update( content );
  tag_edit_complete( jq_tag_section.find( "a.tag_edit_complete" ));
}
function tag_save( dom_elem ) {
  var jq_elem = $( dom_elem );
  var id = jq_elem.attr( "name" );
  var url = "/recipes/" + id + "/update_tags";
  
  $.ajax({
    type: "post",
    url: url,
    data: jq_elem.serialize() + "&authenticity_token=" + TOKEN,
    beforeSend: function() {
      disable_during_submit( "#edit_tags_" + id );
      $( "#edit_tags_" + id + " .tag_edit_complete" ).hide();
    },
    complete: function( response, code ) {
      jq_elem.parent().replaceWith( response.responseText );
    },
    dataType: "html"
  });
}
function tag_edit_complete( dom_elem ) {
  var jq_elem = $( dom_elem );
  var jq_tag_form = jq_elem.parent();
  var jq_tag_span = jq_tag_form.siblings( "span" );
  jq_tag_form.remove();
  jq_tag_span.show();
}
//// Search ////
function search_run() {
  var jq_search = $( "#search_combined" );
  var old = $.trim( jq_search.val() );
  var search_type = jq_search.attr( "name" );
  search_combined_update();
  var curr = jq_search.val();
  if( curr == "" ) {
    search_clear();
  }
  else if( old != curr ) {
    $.ajax({
      type: "post",
      url: "/" + search_type + "/search",
      data: ({
        search: curr,
        authenticity_token: TOKEN
      }),
      beforeSend: function() {
        search_loading();
      },
      complete: function( response, code ) {
        $( "#results" ).prepend( response.responseText );
        search_complete();
      },
      dataType: "html"
    });
  }
}
function search_complete() {
  var correct_result_id = "all_search_results_" + $( "#search_combined" ).val();
  var not_found = true;
  $( "#results" ).children().each(
    function( ix, item ) {
      item = $( item );
      if( item.attr( "id" ) == correct_result_id && not_found ) {
        $( "#search_progress_holder" ).hide();
        item.show();
        not_found = false;
      }
      else {
        item.remove();
      }
    }
  );
  search_switch_state();
}
function search_clear() {
  $( "#results" ).children().remove();
  $( "#search_progress_holder" ).hide();
  // remove search tags
  $( ".search_tag" ).each( function( ix, tag ) {
    search_tag_remove( tag );
  });
  // reset search text regardless
  $( "#search" ).val( $( "#search_hint" ).val() );
  // clear search combined collation
  $( "#search_combined" ).val( "" );
  search_switch_state();
  $( "#search" ).focus();
  $( "#search" ).val( "" );
  $( "html, body" ).animate({ scrollTop: 0 }, "slow" );
}
function search_switch_state() {
  if( $( "#search_combined" ).val() == "" ) {
    $( "#page_title" ).show();
    $( "#main_section" ).show();
    $( "#search_title" ).hide();
  }
  else {
    $( "#page_title" ).hide();
    $( "#main_section" ).hide();
    $( "#search_title" ).show();
  }
}
function search_loading() {
  var jq_elem = $( "#search_progress_holder" );
  if( $( "#search_combined" ).val() != "" ) {
    jq_elem.show(); jq_elem.fadeOut( 8000 );
  }
  else {
    jq_elem.fadeOut( 200 );
  }
}
function search_combined_update() {
  var search_txt = ( $( "#search" ).val() == $( "#search_hint" ).val() ? "" : $( "#search" ).val() );
  search_txt = $.trim(( search_txt + " " + search_tags_collect() ).toLowerCase() );
  $( "#search_combined" ).val( search_txt );
}
//// Search - Tags ////
function search_tags_collect() {
  var x = [];
  $( ".search_tag" ).each( function( ix, item ) {
    x.push( unescape( $( item ).attr( "name" )));
  });
  return x.join( " " );
}
function search_tag_remove( dom_tag ) {
  var tag_count = $( ".search_tag" ).length;
  var search_tag_section = $( ".search_tag" );
  $( dom_tag ).remove();
  if( tag_count == 0 ) {
    search_tag_section.hide();
  }
}
function search_tag_add( dom_elem, focus ) {
  jq_elem = $( dom_elem );
  focus = focus || false;
  var tag_type = jq_elem.attr( "class" ) || "tag";
  var escaped_name = escape( jq_elem.text() );
  var link_id = "search_tag_" + escaped_name.replace( /%20/g, "_" );
  var search_field = $( "#search" );
  var search_tag_section = $( "#search_tags" );
  var tag_html = TEMPLATES[ "search_tag_add" ].expand( {
    tag_name: jq_elem.text().replace( /"/g, "&quot;" ),
    id: link_id,
    tag_type: tag_type,
    escaped_name: escaped_name
  });
  
  // add tag if it doesn't already exist... show search tags section as required.
  if( !$( "#" + link_id ).is( "*" ) ) {
    search_tag_section.prepend( tag_html );
    search_tag_section.show();
  }
  if( focus ) {
    search_field.focus();
    $( "html, body" ).animate({ scrollTop: 0 }, "slow" );
    highlight( link_id );
  }
}
function search_tag_add_custom( name, tag_type ) {
  var tag_html = TEMPLATES[ "tag_custom" ].expand( {
    tag_name: name,
    class_name: tag_type
  });
  
  search_tag_add( $( tag_html ));
}
//// User ////
function user_password_reset( dom_elem ) {
  var jq_elem = $( dom_elem );
  var id = jq_elem.attr( "name" );
  var url = jq_elem.attr( "href" );
  $.ajax({
    type: "post",
    url: url,
    data: ({
      email: id,
      authenticity_token: TOKEN
    }),
    beforeSend: function() {
      $( "#flash_error" ).html( "Resetting password..." );
    },
    error: function( response, code ) {
      $( "#flash_error" ).html( response.responseText );
      $( "#error_notification" ).show();
    },
    success: function( response, code ) {
      $( "#flash_error" ).html( "" );
      $( "#error_notification" ).hide();
      $( "#flash_notice" ).html( response );
      $( "#feedback_notification" ).show();
    },
    dataType: "html"
  });
}
function confirm_unless_equal( f1_name, f2_name, text ) {
  var f1, f2;
  f1 = $( f1_name ).val();
  f2 = $( f2_name ).val();
  template_text = $.template( text );
  if( f1 != f2 ) {
    return confirm( template_text.expand({ new_email: f1, old_email: f2 }));
  }
  else {
    return true;
  }
}
function fbs_click() {
  u=location.href;
  t=document.title;
  window.open('http://www.facebook.com/sharer.php?u='+encodeURIComponent(u)+'&t='+encodeURIComponent(t),'sharer','toolbar=0,status=0,width=626,height=436');
  return false;
}
