function aConstructor()
{
	var debug = false;
	this.onSubmitHandlers = {};

	// This is the old, painful way, see aEditorFck for the
	// new, graceful way
	this.registerOnSubmit = function (slotId, callback)
	{
		if (!this.onSubmitHandlers[slotId])
		{
			this.onSubmitHandlers[slotId] = [ callback ];
			return;
		}
		this.onSubmitHandlers[slotId].push(callback);
	};

	this.callOnSubmit = function (slotId)
	{
		// Call any old-school submit handlers
		var handlers = this.onSubmitHandlers[slotId];
		if (handlers)
		{
			 var i;
			 for (i = 0; (i < handlers.length); i++)
			 {
				 handlers[i](slotId);
			 }
		}
		// The new, sensible way
		$('.a-needs-update').trigger('a.update');
	};

	this.setMessages = function(messages)
	{
		this.messages = messages;
		if (!this.messages.save_changes_first)
		{
			// bc with sites that don't inject an i18n'd version of this new message yet
			this.messages.save_changes_first = 'Please save your changes first.';
		}
	};

	// Utility: A DOM ready that can be used to hook into Apostrophe related events
	this.ready = function(options)
	{
		// You can define this function in your site.js
		// We use this for refreshing progressive enhancements such as Cufon following an Ajax request.
		if (typeof(apostropheReady) === "function")
		{
			apostropheReady();
		}

		// This is deprecated, it's the old function name,
		// preserved here for backwards compatibility
		if (typeof(aOverrides) === "function")
		{
			aOverrides();
		}
	};

	// Utility: Swap two DOM elements without cloning them -- http://blog.pengoworks.com/index.cfm/2008/9/24/A-quick-and-dirty-swap-method-for-jQuery
	this.swapNodes = function(a, b) {
		var t = a.parentNode.insertBefore(document.createTextNode(''), a);
		b.parentNode.insertBefore(a, b);
		t.parentNode.insertBefore(b, t);
		t.parentNode.removeChild(t);
	};

	// Utility: console.log wrapper prevents JS errors if we leave an apostrophe.log call hanging out in our code someplace
	this.log = function(output)
	{
		if (window.console && console.log && debug === true) {
			console.log(output);
		}
	};

	// apostrophe.debug() -- displays any debug messages stored in the debugBuffer and empties the buffer
	this.setDebug = function(flag)
	{
		debug = flag;
	};

	this.getDebug = function()
	{
		return debug;
	};

	// Often JS code relating to an object needs to be able to find the
	// database id of that object as a property of some enclosing
	// DOM object, like an li or div representing a particular media item.
	// This method makes it convenient to write:
	// <?php $domId = 'a-media-item-' . $id ?>
	// <li id="<?php echo $domId ?>"> ... <li>
	// <?php a_js_call('apostrophe.setObjectId(?, ?)', $domId, $id) ?>
	this.setObjectId = function(domId, objectId)
	{
		$('#' + domId).data('id', objectId);
	};

	// Utility: Use to select contents of an input on focus
	// The mouseup event is a workaround for a Chrome bug that deselects the text after focus
	this.selectOnFocus = function(selector)
	{
		$(selector).focus(function(){
			$(this).select();
		}).mouseup(function(e){
			e.preventDefault();
		});
	};

	// Utility: Self Labeling Input Element
	// Example: <?php a_js_call('apostrophe.selfLabel(?)', array('selector' => '#input_id', 'title' => 'Input Label', 'select' => true, 'focus' => false, 'persisentLabel' => false )) ?>
	// options['select'] = true -- Selects the input on focus
	// options['focus'] = true -- Focuses the input on ready
	// options['persisentLabel'] = true -- Keeps the label visible until the person starts typing
	this.selfLabel = function(options)
	{
		aInputSelfLabel(options['selector'], options['title'], options['select'], options['focus'], options['persistentLabel']);
	};

	// Utility: Click an element once and convert it to a span
	// Useful for turning an <a> into a <span>
	this.clickOnce = function(selector)
	{
		el = $(selector);
		el.unbind('click.aClickOnce').bind('click.aClickOnce', function(){
			apostrophe.toSpan(el);
		});
	};

	// Utility: Replaces selected node with <span>
	this.toSpan = function(selector)
	{
		// Use an each here to avoid problems with all of the items getting the
		// same span label
		$(selector).each(function() {

			// Store the current item
			var $self = $(this);

			// Building a replacement span with the same properties
			var aSpan = $('<span>').attr({
				'id': $self.attr('id'),
				'class': $self.attr('class')
			}).html($self.html());

			// Make the swap
			$self.replaceWith(aSpan);

			// If the button wants to show busy
			// Re-bind the behavior and call it
			if (aSpan.hasClass('a-show-busy')) {
				apostrophe.aShowBusy();
				aSpan.trigger('aShowBusy');
			};
		});
	};

	// This is exported because we have to be able to call it from
	// fixed-up versions of ancient onclick="" handlers in order to repair them magically.
	// Repairing things magically is why we have overrideLinks, so we can't hold our
	// noses too much

	this._submitFormForOverrideLinks = function(form, update)
	{
		$.post(
			$(form).attr('action'),
			$(form).serialize(),
			function(data) {
				_fixContentForOverrideLinks(data, update);
			}
		);
	}

	// This is up at this level so that it can be called by _submitFormForOverrideLinks. You
	// don't want to call this yourself, look at linkToRemote to do what you probably intend

	function _fixContentForOverrideLinks(data, update)
	{
		var markup = $(data);
		var update = $(update);
		markup.find('a.a-delete').each(function() {
			var onclick = this.getAttribute('onclick');
			if (onclick && onclick.length)
			{
				var updateId = update.attr('id');
				if (!updateId)
				{
					apostrophe.log("WARNING: element being updated has no id, overrideLinks can't work");
				}
				onclick = onclick.replace('f.submit()', 'apostrophe._submitFormForOverrideLinks(f, "#' + updateId + '")');
				this.setAttribute('onclick', onclick);
			}
		});

		// Don't mess up things with existing handlers
		$('a:not([href="#"])', markup).click(function(event) {
			// This class means we should let the link apply to the whole page after all
			if ($(this).hasClass('a-no-override-links'))
			{
				return true;
			}
			var onclick = this.getAttribute('onclick');
			if (onclick && onclick.length)
			{
				// Don't interfere with old-school handlers
				// (the one case in which we do so is handled above)
				return true;
			}
			event.preventDefault();
			$.get($(this).attr('href'), function(data) {
				_fixContentForOverrideLinks(data, update);
			});
		});
		$('form', markup).submit(function(event) {
			apostrophe.log('form submit handler');
			event.preventDefault();
			apostrophe._submitFormForOverrideLinks(this, update);
		});
		update.empty();
		update.append(markup);
		apostrophe.smartCSS({ target: update });
		update.trigger('aAfterOverrideLinks');
	}

	// Used to implement linkToRemote and also used directly to load dialogs under other
	// circumstances
	//
	// Load options.url into the element specified by the jquery selector options.update.
	// Set the a-remote-data-loading class during loading and the a-remote-data-loaded class
	// when loading is complete. If the restore option is true, set
	// any a-cancel buttons present to restore the content prior to the loading of the new content.
	// Use the method specified by options.method, defaulting to the get method. If
	// options.beforeNewContent is set, invoke that callback before installing the new content.
	// If options.afterNewContent is set, invoke that callback after installing the new content.
	// For bc the alternate name options.callback is also accepted for options.afterNewContent.

	this.loadRemote = function(options)
	{
		var update = $(options['update']);
		var method = (options['method'])? options['method'] : 'get';
		var remoteURL = options['url'];
		var restore = (options['restore']) ? options['restore'] : false;
		$.ajax({
			type:method,
			dataType:'html',
			beforeSend:function() {
				update.addClass('a-remote-data-loading');
			},
			success:function(data, textStatus)
			{
				if (restore)
				{
					update.data('aBeforeUpdate', update.children().clone(true));
				}
				newContent(data);
				function newContent(data)
				{
					if (options.beforeNewContent)
					{
						options.beforeNewContent();
					}

					if (restore)
					{
						update.find('.a-cancel').unbind('click.aRestore').bind('click.aRestore', function(event){
							event.preventDefault();
							update.html(update.data('aBeforeUpdate'));
						});
					}
					update.removeClass('a-remote-data-loading').addClass('a-remote-data-loaded');

					// The overrideLinks option changes all links and forms inside the
					// popup container to AJAX update the container rather than causing
					// a page refresh. It will leave your links alone if they point
					// to '#', but in general it is intended for pulling boring, simple
					// stuff like admin generator modules into a popup container and should
					// not be mistaken for the solution to all of your life's problems
					if (options['overrideLinks'])
					{
						_fixContentForOverrideLinks(data, update);
					}
					else
					{
						update.html(data);
					}

					if (options.afterNewContent)
					{
						options.afterNewContent();
					}

					// For bc we support this overly generic name for afterNewContent too
					if (options.callback)
					{
						options.callback();
					}
				}
			},
			url:remoteURL
		});
	};

	// Utility: an updated version of the jq_link_to_remote helper.
	// Allows you to create the same functionality without outputting javascript in the markup.
	//
	// Set options.selector to a jQuery selector matching the link to be enhanced.
	// Set options.eventType to bind an event other than 'click' (such as a namespaced click event).
	// Accepts options.link as a synonym for options.link.
	//
	// For the remaining options, especially options.update, see this.loadRemote above

	this.linkToRemote = function(options)
	{
		var selector = options['link'];
		var update = $(options['update']);
		var eventType = (options['event'])? options['event'] : 'click';

		if (selector === undefined)
		{
			// Modern syntax optional
			selector = options['selector'];
		}

		var link = $(selector);
		if (link.length && update.length) {
			link.bind(eventType, function() {
				apostrophe.loadRemote(options);
				return false;
			});
		}
    // Enable for debugging
    // if (!link.length) {
    //  apostrophe.log('apostrophe.linkToRemote -- No Link Found');
    // }
    // if (!update.length) {
    //  apostrophe.log('apostrophe.linkToRemote -- No Update Target Found');
    // }
	};

	this.unobfuscateEmail = function(aClass, email, label)
	{
		$('.' + aClass).attr('href', unescape(email)).html(unescape(label));
	};

	// Turns a form into an AJAX form that updates the element
	// with the DOM ID specified by options['update']. You must
	// specify a 'selector' option as well to identify the form.
	// This replaces jq_remote_form for some cases. For fancy cases
	// you should write a separate method here

	this.formUpdates = function(options)
	{
		var form = $(options['selector']);

		// Named bind prevents redundancy
		form.unbind('submit.aFormUpdates');
		form.bind('submit.aFormUpdates', function() {
			// Give special snowflakes like FCKEditor etc. a chance to update their
			// related "normal" form elements
			$('.a-needs-update').trigger('a.update');
			var updating = $('#' + options['update']);
			var action = form.attr('action');
			$.post(action, form.serialize(), function(data) {
				updating.html(data);
        updating.trigger('a.updated');

        var extras = options['refresh-extra']? options['refresh-extra'] : [];
        $('.a-needs-refresh').trigger('a.refresh', extras);

			});
			return false;
		});
	};

  // Input options['selector'] and options['target'].  When selector is clicked
  // set target to blank.
  this.setBlank = function(options)
  {
    var selector = $(options['selector']);

    if (selector.length)
    {
      selector.bind('click', function(event) {
        $(options['target']).html('');

        return false;
      });
    }
    else
    {
      apostrophe.log('apostrophe.setBlank -- Selector not found');
    }
  }

	// Turns a link into an AJAX form that updates the element
	// with the DOM ID specified by options['update']. You must
	// specify a 'selector' option as well to identify the link.
	// This replaces jq_remote_link for some cases. For fancy cases
	// you should write a separate method here

	this.linkUpdates = function(options)
	{
		var link = $(options['selector']);
		var confirmMessage = link.attr('data-confirm');
		// Named bind prevents redundancy
		link.unbind('click.aLinkUpdates');
		link.bind('click.aLinkUpdates', function(event) {
			event.preventDefault();
			if (confirmMessage)
			{
				if (!confirm(confirmMessage))
				{
					return false;
				}
			}
			// Give special snowflakes like FCKEditor etc. a chance to update their
			// related "normal" form elements
			$('.a-needs-update').trigger('a.update');
			var updating = $('#' + options['update']);
			var action = link.attr('href');
			$.get(action, {}, function(data) {
				updating.trigger('a.updated');
				updating.html(data);
			});
		});
	};

	// apostrophe.aShowBusy() looks for anchor submit buttons with the class ".a-show-busy" and will display a progress animation
	// when the submit is clicked by toggling the the ".a-busy" class. This has to be an anchor tag, not an input type="submit" button.
	// Usually you wind up replacing it via AJAX, so you don't need a way to stop animating.
	// However, you can manually toggle the animation off again by calling $(submit).trigger('aHideBusy');

	this.updating = function(selector)
	{
		// apostrophe.updating left in place for backwards compatibility
		// just incase anyone else is using it outside of a.js
		apostrophe.aShowBusy({ 'updating' : selector });
	};

	this.aShowBusy = function(options)
	{

		var updating = '';
		if (options !== undefined && options['updating'])
		{
			updating = options['updating'] + ' .a-show-busy, ';
		}

		var submit = $(updating + '.a-show-busy');

		// We don't need to bind to the click even because
		// We are binding to the form submit event
		// This works for pressing ENTER in an input
		// OR Clicking SUBMIT to submit the form.

		submit.unbind('click.aShowBusy').bind('click.aShowBusy', function(event){
			var $self = $(this);
			$self.trigger('aShowBusy');
		}).unbind('aShowBusy').bind('aShowBusy', function(event){
			var $self = $(this);
			if (!$self.hasClass('a-busy'))
			{
				submit.addClass('a-busy');
				if (!$self.hasClass('icon'))
				{
					$self.addClass('icon').prepend('<span class="icon"></span>');
				}
			}
		}).unbind('aHideBusy').bind('aHideBusy', function(event){
			var $self = $(this);
			$self.removeClass('a-busy').removeClass('icon').find('span.icon').remove();
		});

		submit.closest('form').unbind('submit.aShowBusy').bind('submit.aShowBusy', function(){
			var $selfForm = $(this);
			$selfForm.find('.a-show-busy').trigger('aShowBusy');
		});

	};

	// Utility: Create an anchor button that toggles between two radio buttons
	this.radioToggleButton = function(options)
	{
		// Set the button toggle labels
		var opt1Label = (options['opt1Label'])? options['opt1Label'] : 'on';
		var opt2Label = (options['opt2Label'])? options['opt2Label'] : 'off';
		var field = $(options['field']);
		var radios = field.find('input[type="radio"]');

		radios.length ? '' : apostrophe.log('apostrophe.radioToggleButton -- selector: ' + options['field'] + ' -- No radio inputs found');

		if (field.length)
		{
			options['debug'] ? apostrophe.log('apostrophe.radioToggleButton --' + field + '-- debugging') : field.find('.radio_list').hide();

			if (!field.find('.a-toggle-btn').length)
			{

				var toggleButton = $('<a/>');
				toggleButton.addClass('a-btn icon lite a-toggle-btn');
				toggleButton.html('<span class="icon"></span><span class="option-1">' + opt1Label + '</span><span class="option-2">' + opt2Label + '</span>');

				field.prepend(toggleButton);
				var btn = field.find('.a-toggle-btn');
				updateToggle(btn);

				btn.bind('click.apostrophe', function(){
					toggle(btn);
				});
			}

		}
		else
		{
			field.length ? '' : apostrophe.log('apostrophe.radioToggleButton -- No field found');
		}

		function toggle(button)
		{
			if ($(radios[0]).is(':checked'))
			{
				$(radios[0]).attr('checked',null);
				$(radios[1]).attr('checked','checked');
			}
			else
			{
				$(radios[1]).attr('checked',null);
				$(radios[0]).attr('checked','checked');
			}
			updateToggle(button);
		}

		function updateToggle(button)
		{
			if ($(radios[0]).is(':checked'))
			{
				button.addClass('option-1').removeClass('option-2');
			}
			else
			{
				button.addClass('option-2').removeClass('option-1');
			}
		}
	};

	// Utility: IE6 Users get a special message when they log into apostrophe
	this.IE6 = function(options)
	{
		var ieBody = $('body');
		    authenticated = options['authenticated'],
		    message = options['message'];
		// This is called within a conditional comment for IE in Apostrophe's layout.php
		if (authenticated && ieBody.closest('.ie6').size())
		{
			ieBody.addClass('ie6').prepend('<div id="ie6-warning"><h2>' + message + '</h2></div>');
		}
	};

	// This sets up the Reorganization Tool
	this.jsTree = function(options)
	{
		var treeData = options['treeData'];
		var moveURL = options['moveUrl'];
		var aPageTree = $('#a-page-tree');

		aPageTree.tree({
			data: {
				type: 'json',
				// Supports multiple roots so we have to specify a list
				json: [ treeData ]
			},
			ui: {
				theme_path: "/apostrophePlugin/js/jsTree/source/themes/",
				theme_name: "punk",
				scroll_spd	: 16,
				context: false
			// 	[
			//					 // {
			//					 //		 id			: "create",
			//					 //		 label	 : "Create",
			//					 //		 icon		: "create.png",
			//					 //		 visible : function (NODE, TREE_OBJ) { if(NODE.length != 1) return false; return TREE_OBJ.check("creatable", NODE); },
			//					 //		 action	: function (NODE, TREE_OBJ) { TREE_OBJ.create(false, TREE_OBJ.get_node(NODE)); }
			//					 // },
			//					 // "separator",
			//					 // {
			//					 //		 id			: "rename",
			//					 //		 label	 : "Rename",
			//					 //		 icon		: "rename.png",
			//					 //		 visible : function (NODE, TREE_OBJ) { if(NODE.length != 1) return false; return TREE_OBJ.check("renameable", NODE); },
			//					 //		 action	: function (NODE, TREE_OBJ) { TREE_OBJ.rename(); }
			//					 // },
			//					 {
			//							 id			: "delete",
			//							 label	 : "Delete",
			//							 // icon		: "remove.png",
			//							 visible : function (NODE, TREE_OBJ) { var ok = true; $.each(NODE, function () { if(TREE_OBJ.check("deletable", this) == false) ok = false; return false; }); return ok; },
			// 			// Also see ondelete below
			//							 action	: function (NODE, TREE_OBJ) { $.each(NODE, function () { TREE_OBJ.remove(this); }); }
			//					 }
			// 	]
			},
			rules: {
				// Turn off most operations as we're only here to reorg the tree.
				// Allowing renames and deletes here is an interesting thought but
				// there's back end stuff that must exist for that.
				renameable: false,
				// deletable: 'all',
				deletable: false,
				creatable: false,
				draggable: 'all',
				dragrules: 'all'
			},
			callback: {
				// move completed (TYPE is BELOW|ABOVE|INSIDE)
				onmove: function(node, refNode, type, treeObj, rb)
				{
					// To avoid creating an inconsistent tree we need to use a synchronous request. If the request fails, refresh the
					// tree page (TODO: find out if there's some way to flunk an individual drag operation). This shouldn't happen anyway
					// but don't get into an inconsistent state if it does!

					aPageTree.parent().addClass('working');

					var nid = node.id;
					var rid = refNode.id;

					jQuery.ajax({
						url: options['moveURL'] + "?" + "id=" + nid.substr("tree-".length) + "&refId=" + rid.substr("tree-".length) + "&type=" + type,
						error: function(result) {
							// 404 errors etc
							window.location.reload();
						},
						success: function(result) {
							// Look for a specific "all is well" response
							if (result !== 'ok')
							{
								window.location.reload();
							}
							aPageTree.parent().removeClass('working');
						},
						// Now that we have a reasonable progress animation we can go async to avoid
						// a "do you want to kill the browser window" dialog on slow moves
						async: true
					});
				}
				// delete completed
				// ondelete: function(node, treeObj, rb)
				// {
				// 	// To avoid creating an inconsistent tree we need to use a synchronous request. If the request fails, refresh the
				// 	// tree page
				//
				// 	aPageTree.parent().addClass('working');
				//
				// 	var nid = node.id;
				//
				// 	jQuery.ajax({
				// 		url: options['deleteURL'] + "?" + "id=" + nid.substr("tree-".length),
				// 		error: function(result) {
				// 			// 404 errors etc
				// 			window.location.reload();
				// 		},
				// 		success: function(result) {
				// 			// Look for a specific "all is well" response
				// 			if (result !== 'ok')
				// 			{
				// 				window.location.reload();
				// 			}
				// 			aPageTree.parent().removeClass('working');
				// 		},
				// 		async: false
				// 	});
				// }
			}
		});
		treeRef = $.tree_reference(aPageTree.attr('id'));
		// aPageTree.find('.a-tree-delete-btn').bind('click.apostrophe', function() {
		// 	var li = $(this).closest('li');
		// 	if (li.find('li').length)
		// 	{
		// 		if (!confirm(options['confirmDeleteWithChildren']))
		// 		{
		// 			return false;
		// 		}
		// 	}
		// 	else
		// 	{
		// 		if (!confirm(options['confirmDeleteWithoutChildren']))
		// 		{
		// 			return false;
		// 		}
		// 	}
		// 	treeRef.remove(li);
		// 	return false;
		// });
		aPageTree.find('li').each(function() {
			var id = $(this).attr('id');
			var a = $(this).find('a:first');
			// This markup was carefully created to avoid 80000 conflicts with other CSS,
			// so please do not change it casually
			a.after($('<cite class="a-tree-delete">x</cite>'));
		});
		aPageTree.find('li cite.a-tree-delete').click(function() {
			var anchor = $(this);
			var li = anchor.closest('li');
			var nid = li.attr('id');
			if (li.find('li').length)
			{
				if (!confirm(options['confirmDeleteWithChildren']))
				{
					return false;
				}
			}
			else
			{
				if (!confirm(options['confirmDeleteWithoutChildren']))
				{
					return false;
				}
			}
			aPageTree.parent().addClass('working');
			jQuery.ajax({
				url: options['deleteURL'] + "?" + "id=" + nid.substr("tree-".length),
				error: function(result) {
					// 404 errors etc
					window.location.reload();
				},
				success: function(result) {
					// Look for a specific "all is well" response
					if (result !== 'ok')
					{
						window.location.reload();
					}
					// Seems to work better than treeRef.remove(li)
					li.remove();
					aPageTree.parent().removeClass('working');
				},
				async: true
			});
			return false;
		});
	};

	// aSlideshowSlot
	this.slideshowSlot = function(options)
	{
		var debug = options['debug'];
		var transition = options['transition'];
		var id = options['id'];
		var intervalEnabled = !!options['interval'];
		var intervalSetting = options['interval'];
		var positionFlag = options['position'];
	 	var position = (options['startingPosition']) ? options['startingPosition'] : 0;
	 	var duration = (options['duration']) ? options['duration'] : 300;
		var slideshowSelector = (options['slideshowSelector']) ? options['slideshowSelector'] : '#a-slideshow-' + id;
		var slideshow = $(slideshowSelector);
		var slideshowControlsSelector = (options['controls']) ? options['controls'] : '.a-slideshow-controls';
		var slideshowControls = slideshow.next(slideshowControlsSelector);
		var slideshowItemsSelector = (options['slideshowItemsSelector']) ? options['slideshowItemsSelector'] : '.a-slideshow-item';
		var slideshowItems = slideshow.find(slideshowItemsSelector);
		var itemCount = slideshowItems.length;
		var positionSelector = (options['positionSelector']) ? options['positionSelector'] : '.a-slideshow-position-head';
		var positionHead = slideshowControls.find(positionSelector);
		var intervalTimeout = null;
		var currentItem;
		var newItem;
		var oldItem;

 		(options['title']) ? slideshowItems.attr('title', options['title']) : slideshowItems.attr('title','');

		// apostrophe.log('apostrophe.slideshowSlot --'+id+'-- Debugging');
		// apostrophe.log('apostrophe.slideshowSlot --'+id+'-- Item Count : ' + itemCount );

		if (itemCount === 1)
		{
			slideshow.addClass('single-image');
			$(slideshowItems[0]).show();
			// apostrophe.log('apostrophe.slideshowSlot --'+id+'-- Single Image');
		}
		else
		{
			slideshow.addClass('multi-image');
			// Clear any interval timer left running by a previous slot variant
			if (window.aSlideshowIntervalTimeouts !== undefined)
			{
				if (window.aSlideshowIntervalTimeouts['a-' + id])
				{
					clearTimeout(window.aSlideshowIntervalTimeouts['a-' + id]);
				}
			}
			else
			{
				window.aSlideshowIntervalTimeouts = {};
			}

			function init()
			{
				// Initialize the slideshow
				// Hiding all of the items, showing the first one, setting the position, and starting the timer
				slideshowItems.hide();
				$(slideshowItems[position]).show();
				setPosition(position);
				interval();
			}

			function previous()
			{
				currentItem = position;
				(position == 0) ? position = itemCount - 1 : position--;
				showItem(position, currentItem);
				// apostrophe.log('apostrophe.slideshowSlot --'+id+'-- Previous : ' + currentItem + ' / ' + position);
			};

			function next()
			{
				currentItem = position;
				(position == itemCount-1) ? position = 0 : position++;
				showItem(position, currentItem);
				// apostrophe.log('apostrophe.slideshowSlot --'+id+'-- Next : ' + currentItem + ' / ' + position);
			};

			function showItem(position, currentItem)
			{
				if (!slideshow.data('showItem'))
				{
					slideshow.data('showItem', 1);
					newItem = $(slideshowItems[position]);
					oldItem = (currentItem) ? $(slideshowItems[currentItem]) : slideshowItems;
					if (transition == 'crossfade')
					{
						oldItem.fadeOut(duration);
					}
					else
					{
						// Some browsers jump / scroll up if the parent loses height for the split second the oldItem is hidden
						// So we set the height here before changing the slideshow item. This is not a problem when crossfading, because there is always an item visible
						newItemHeight = newItem.height() + 'px';
						slideshow.css('height',newItemHeight);
						// Since we are not crossfading, just hide all of the slideshowItems
						slideshowItems.hide();
					};
					newItem.fadeIn(duration,function(){
						slideshow.data('showItem', 0);
						setPosition(position);
						interval();
					});
				};
			};

			function setPosition(p)
			{
				slideshow.data('position', p);
				// apostrophe.log('apostrophe.slideshowSlot --'+id+'-- positionFlag : ' + positionFlag );
				// apostrophe.log('apostrophe.slideshowSlot --'+id+'-- setPosition : ' + (p + 1) );
				if (positionFlag && positionHead.length)
				{
					positionHead.text(parseInt(p) + 1);
					// apostrophe.log('apostrophe.slideshowSlot --'+id+'-- setPosition : ' + p + 1 );
				};
			};

			function interval()
			{
				if (intervalTimeout)
				{
					clearTimeout(intervalTimeout);
				};
				if (intervalEnabled)
				{
					intervalTimeout = setTimeout(next, intervalSetting * 1000);
					window.aSlideshowIntervalTimeouts['a-' + id] = intervalTimeout;
					// apostrophe.log('apostrophe.slideshowSlot --'+id+'-- Interval : ' + intervalSetting );
				}
			};

			// 1. Initialize the slideshow
			init();

			// 2. Bind events
			slideshow.bind('showItem', function(e,p){ showItem(p); });
			slideshow.bind('previousItem', function(){ previous(); });
			slideshow.bind('nextItem', function(){ next(); });

			slideshow.find('.a-slideshow-image').bind('click.apostrophe', function(event) {
				event.preventDefault();
				intervalEnabled = false;
				next();
			});

			slideshowControls.find('.a-arrow-left').bind('click.apostrophe', function(event){
				event.preventDefault();
				intervalEnabled = false;
				previous();
			});

			slideshowControls.find('.a-arrow-right').bind('click.apostrophe', function(event){
				event.preventDefault();
				intervalEnabled = false;
				next();
			});

			slideshowControls.find('.a-arrow-left, .a-arrow-right').hover(function(){
				$(this).addClass('over');
			},function(){
				$(this).removeClass('over');
			});

		}
	};

	// aButtonSlot
	this.buttonSlot = function(options)
	{
		var button = (options['button'])? $(options['button']) : false;
		var rollover = (options['rollover']) ? options['rollover'] : false;

		apostrophe.slotEnhancements({slot:'#'+button.closest('.a-slot').attr('id'), editClass:'a-options'});

		if (button.length)
		{
			if (rollover)
			{
				var link = button.find('.a-button-title .a-button-link');
				var image = button.find('.a-button-image img');
				image.hover(function(){ image.fadeTo(0,.65); },function(){ image.fadeTo(0,1); });
				link.hover(function(){ image.fadeTo(0,.65); },function(){ image.fadeTo(0,1); });
			}
		}
		else
		{
			apostrophe.log('apostrophe.buttonSlot -- no button found');
		}
	};

	this.afterAddingSlot = function(name)
	{
		$('#a-add-slot-form-' + name).hide();
	};

	this.areaEnableDeleteSlotButton = function(options) {
		$('#' + options['buttonId']).bind('click.apostrophe', function() {
			if (confirm(options['confirmPrompt']))
			{
				$(this).closest(".a-slot").fadeOut();
				$.post(options['url'], {}, function(data) {
					$("#a-slots-" + options['pageId'] + "-" + options['name']).html(data);
				});
			}
			return false;
		});
	};

	this.areaEnableAddSlotChoice = function(options) {
		var button = $("#" + options['buttonId']);
		// apostrophe.log('apostrophe.areaEnableAddSlotChoice -- Debug');
		$(button).bind('click.apostrophe', function() {
			var name = options['name'];
			var pageId = options['pageId'];
			$.post(options['url'], {}, function(data) {
				var slots = $('#a-slots-' + pageId + '-' + name);
				slots.html(data);
				var area = $('#a-area-' + pageId + '-' + name);
				area.removeClass('a-options-open');
			});
			return false;
		});
	};

	this.areaEnableHistoryButtons = function(options)
	{
		// apostrophe.log('apostrophe.areaEnableHistoryButtons');
		var areas = $('.a-area');
		areas.undelegate('a.a-history-btn','click.apostrophe').delegate('a.a-history-btn', 'click.apostrophe', function(){
			var history = $(this);
			var area = history.closest('div.a-area');
			_closeHistory();
			_browseHistory(area);
			$(".a-history-browser .a-history-items").data("area", "a-area-" + area.data('pageid') + "-" + area.data('name'));
			$(".a-history-browser .a-history-browser-view-more").bind('click.apostrophe', function() {
				$.post(history.data('moreurl'), {}, function(data) {
					$('.a-history-browser .a-history-items').html(data);
					$(".a-history-browser .a-history-browser-view-more .spinner").hide();
				});
				$(this).hide();
				return false;
			});

			$.post(history.data('url'), {}, function (data) {
				$('.a-history-browser .a-history-items').html(data);
			});

		});
	};

	this.areaUpdateMoveButtons = function(updateAction, id, name)
	{
		var area = $('#a-area-' + id + '-' + name);
		// Be precise - take care not to hoover up controls related to slots in nested areas, if there are any
		var slots = area.children('.a-slots').children('.a-slot');
		var newSlots = area.children('.a-slots').children('.a-new-slot');

		// I actually want a visible loop variable here
		for (n = 0; (n < slots.length); n++)
		{
			var slot = slots[n];
			// We use a nested function here because
			// a loop variable does *not* get captured
			// in the closure at its current value otherwise
			slotUpdateMoveButtons(id, name, slot, n, slots, updateAction);
		}

		if (newSlots.length)
		{
			// TODO: this is not sensitive enough to nested areas
			// TODO: with a little more finesse we could support saving it with
			// a rank, but think about how messy that might get

			// Hide the new slot's controls because it can't be moved until it is saved
			newSlots.find('.a-slot-controls .a-move').addClass('a-hidden');

			// Hide the next slot's UP arrow because the slot cannot switch places with the unsaved new slot
			newSlots.next('.a-slot').find('.a-move.up').addClass('a-hidden');

			// Hide the prev slot's DOWN arrow because the slot cannot switch places with the unsaved new slot
			newSlots.prev('.a-slot').find('.a-move.down').addClass('a-hidden');

			// apostrophe.log('apostrophe.areaUpdateMoveButtons -- newSlots in ' + area.attr('id'));
			return;
		}
		// apostrophe.log('apostrophe.areaUpdateMoveButtons -- ' + area.attr('id'));
	};

	this.areaHighliteNewSlot = function(options)
	{
		var pageId = options['pageId'];
		var slotName = options['slotName'];
		var newSlot = $('#a-area-' + pageId + '-' + slotName).find('.a-new-slot');
		if (newSlot.length)
		{
			// There's a bug with highlight and rgba backgrounds
			// http://bugs.jqueryui.com/ticket/5215
			var tmpBG = newSlot.css('background'); // store the background
			newSlot.css({ 'background':'none' }); // remove the background for the highlight effect
			newSlot.effect("highlight", {}, 1000, function(){
				newSlot.css({ 'background':tmpBG }); // restore that background
			});
			$('#a-add-slot-' + pageId + '-' + slotName).parent().trigger('toggleClosed');
		}
	};

	this.areaSingletonSlot = function(options)
	{
		var pageId = options['pageId'];
		var slotName = options['slotName'];
		// Singleton Slot Controls
		$('#a-area-' + pageId + '-' + slotName + '.singleton .a-slot-controls-moved').remove();
		// Move up the slot controls and give them some class names.
		$('#a-area-' + pageId + '-' + slotName + '.singleton .a-slot-controls').prependTo($('#a-area-' + pageId + '-' + slotName)).addClass('a-area-controls a-slot-controls-moved').removeClass('a-slot-controls');
		// Singleton Slots can't have big history buttons!
		$('ul.a-slot-controls-moved a.a-btn.a-history-btn').removeClass('big');
	};

	this.slotEnableVariantButton = function(options)
	{
		var button = $('#' + options['buttonId']);
		// This gets called more than once, use namespaces to avoid double binding without
		// breaking other binds
		button.unbind('click.slotEnableVariantButton');
		button.bind('click.slotEnableVariantButton', function() {
			// Change the visibility of the variant buttons to their active and inactive states as appropriate
			var variants = $('#a-' + options['slotFullId'] + '-variant');
			variants.find('ul.a-variant-options').addClass('loading');
			variants.find('li.active').hide();
			variants.find('ul.a-variant-options li.inactive').show();
			var variantStem = '#a-' + options['slotFullId'] + '-variant-' + options['variant'];
			$(variantStem + '-active').show();
			$(variantStem + '-inactive').hide();
			variants.find('ul.a-variant-options').hide();

			$.post(options['url'], {}, function(data) {
				$('#' + options['slotContentId']).html(data);
			});
			return false;
		});
	};

	this.slotShowVariantsMenu = function(slot)
	{
		var outerWrapper = $(slot);
		var singletonArea = outerWrapper.closest('.singleton');
		if (singletonArea.length)
		{
			singletonArea.find('.a-controls li.variant').show();
		}
		else
		{
			outerWrapper.find('.a-controls li.variant').show();
		}
	};

	this.slotHideVariantsMenu = function(menu)
	{
		var menu = $(menu);
		menu.removeClass('loading').fadeOut('slow').parent().removeClass('open');
	};

	this.slotApplyVariantClass = function(slot, variant)
	{
		var outerWrapper = $(slot);
		outerWrapper.addClass(variant);
	};

	this.slotRemoveVariantClass = function(slot, variant)
	{
		var outerWrapper = $(slot);
		outerWrapper.removeClass(variant);
	};

	this.slotEnhancements = function(options)
	{
		var slot = $(options['slot']);
		var editClass = options['editClass'];
		if (slot.length)
		{
			if (editClass);
			{
				slot.find('.a-edit-view').addClass(editClass);
			};
		}
		else
		{
			apostrophe.log('apostrophe.slotEnhancements -- No slot found.');
			apostrophe.log('apostrophe.slotEnhancements -- Selector: '+ options['slot']);
		}
	};

	this.slotShowEditView = function(pageid, name, permid, realUrl)
	{
		var fullId = pageid + '-' + name + '-' + permid;
 		var editSlot = $('#a-slot-' + fullId);
		// Always reload the edit view when edit is clicked. This change was made because there are too many
		// edge cases where editors (like CkEditor) behave badly if you move them with the arrows, etc. and
		// then open them again. Fewer possibilities = fewer bugs
		$.get(editSlot.data('a-edit-url'), { id: pageid, slot: name, permid: permid, realUrl: realUrl }, function(data) {
			editSlot.children('.a-slot-content').html(data);
			slotShowEditViewPreloaded(pageid, name, permid);
		});
	};

	this.slotNotNew = function(pageid, name, permid)
	{
		$("#a-slot-" + pageid + "-" + name + "-" + permid).removeClass('a-new-slot');
	};

	this.slotEnableEditButton = function(pageid, name, permid, editUrl, realUrl)
	{
		var fullId = pageid + '-' + name + '-' + permid;
 		var editBtn = $('#a-slot-edit-' + fullId);
 		var editSlot = $('#a-slot-' + fullId);
		editSlot.data('a-edit-url', editUrl);
 		editBtn.die('click.apostrophe').live('click.apostrophe', function(event) {
			apostrophe.slotShowEditView(pageid, name, permid, realUrl);
 			return false;
 		});
	};

	this.slotEnableForm = function(options)
	{
		var	$slotForm = $(options['slot-form']),
				$slot = $slotForm.closest('.a-slot'),
				$singleton = $slot.closest('.a-area.singleton'),
				$slotContent = $(options['slot-content']);
				
		// apostrophe.log('apostrophe.slotEnableForm -- form : ' + options['slot-form']);
		$slotForm.submit(function() {
			$.post(
				// These fields are the context, not something the user gets to edit. So rather than
				// creating a gratuitous collection of hidden form widgets that are never edited, let's
				// attach the necessary context fields to the URL just like Doctrine forms do.
				// We force a query string for compatibility with our simple admin routing rule
				options['url'],
				$slotForm.serialize(),
				function(data) {
					$slotContent.html(data);
					$slot.removeClass('a-editing').addClass('a-normal');
					$singleton.removeClass('a-editing'); // Singletons are an edge case
				},
				'html'
			);
			return false;
		});
	};

	this.slotEnableFormButtons = function(options)
	{
		// apostrophe.log('apostrophe.slotEnableFormButtons');

		var $slot = $(options['view']),
				$singleton = $slot.closest('.a-area.singleton'),
				$cancelButton = $(options['cancel']),
				$saveButton = $(options['save']);

		// Note: The selectors are rigid here because slots can be nested inside of other slots. 
		// We have to use .children() -- .find() won't work here.

		$cancelButton.unbind('click.slotEnableFormButtons').bind('click.slotEnableFormButtons', function(event){
			event.preventDefault();
			$slot.children('.a-slot-content').children('.a-slot-content-container').fadeIn();
			$slot.children('.a-controls li.variant').fadeIn();
			$slot.children('.a-slot-content').children('.a-slot-form').hide();
			$slot.removeClass('a-editing').addClass('a-normal');
			$singleton.removeClass('a-editing');
		});

		$saveButton.unbind('click.slotEnableFormButtons').bind('click.slotEnableFormButtons', function(event){
			event.preventDefault();
 			window.apostrophe.callOnSubmit(options['slot-full-id']);
 			return true;
		});

		if (options['showEditor'])
		{
			$slot.addClass('a-editing').removeClass('a-normal');
			$singleton.addClass('a-editing').removeClass('a-normal');
		}
	};

	this.mediaCategories = function(options)
	{
		var newCategoryLabel = options['newCategoryLabel'];
		apostrophe.selfLabel('#a_media_category_name', newCategoryLabel);
		$('#a-media-edit-categories-button, #a-media-no-categories-messagem, #a-category-sidebar-list').hide();
		$('#a_media_category_description').parents('div.a-form-row').addClass('hide-description').parent().attr('id','a-media-category-form');
		$('.a-remote-submit').aRemoteSubmit('#a-media-edit-categories');
	};

	// We send people away to the media repo to pick things and then they
	// decide to wander off and not pick things. We need to be realistic about
	// this and cancel their selection. A better idea would be to make
	// media admin/selection a "most-of-page" experience, maybe via an iframe, but
	// that's more of a 1.6 idea. For 1.5, this is a good band-aid fix

	this.mediaClearSelectingOnNavAway = function(mediaClearSelectingUrl)
	{
		$('a').bind('click.apostrophe', function() {
			var href = $(this).attr('href');
			if (href === undefined)
			{
				return;
			}
			if (href.substr(0, 1) === '#')
			{
				return;
			}
			// Be tolerant of this being in the middle as a stopgap solution for the problem
			// of alternate document roots and frontend controllers in URLs
			if (href.match(/\/admin\/media/))
			{
				return;
			}
			apostrophe.log("Cancel select for " + href);
			// "Why is this synchronous?" So that we can allow the events associated with
			// this link to execute normally (return true) after we request the cancel,
			// rather than second-guessing the nature of the link and screwing lots of
			// things up any more than we'realready going to by interfering here
			$.ajax({ url: mediaClearSelectingUrl, async: false });
			return;
		});
	};

	this.mediaEnableRemoveButton = function(i)
	{
		var editor = $('#a-media-item-' + i);
		editor.find('.a-media-delete-image-btn').bind('click.apostrophe', function()
		{
			editor.remove();
			if ($('.a-media-item').length == 0)
			{
				// This is a bit hacky
				// TODO: Make this less hacky.
				// Using a class for the selector could return multiple hits with possibly with different HREF values.
				// This would grab the first one and go, with no regard for if it's the correct one or not.
				document.location = $('.a-js-media-edit-multiple-cancel').attr('href');
			}
			return false;
		});
	};

	// Listens to the file input for a media form and returns visual feedback if a new file is selected
	this.mediaReplaceFileListener = function(options)
	{
		var menu = $(options['menu']);
		var input = $(options['input']);
		var message = 'This file will be replaced with the new file you have selected after you click save.';
		var fileLabel = 'File: ';

		if (options['message'])
		{
			message = options['message'];
		};

		if (options['fileLabel'])
		{
			fileLabel = options['fileLabel'];
		};

		if (input.length) {
			input.change(function(){
				if (input.val())
				{
					menu.trigger('toggleClosed');
					var newFileMessage = $('<div/>');
					newFileMessage.html('<div class="a-options open"><p>'+ message + '</p><p>'+ fileLabel + '<span>' + input.val() + '</span>' + '</p></div>');
					newFileMessage.addClass('a-new-file-message help');
					// apostrophe.log(newFileMessage);
					input.closest('.a-form-row').append(newFileMessage);
				};
			});
		}
		else
		{
			apostrophe.log('apostrophe.mediaReplaceFileListener -- no input found');
		}
	};

	// Upon submission, if the media form has an empty file field and it is in a context to do so, it submits with AJAX -- Otherwise, it will submit normally
	this.mediaAjaxSubmitListener = function(options)
	{
		var form = $(options['form']);
		var url = options['url'];
		var update = $(options['update']);
		var file = form.find('input[type="file"]');
		var embedChanged = false;
		if (form.length) {
			form.find('.a-form-row.embed textarea').change(function() {
				embedChanged = true;
			});
			form.submit(function(event) {
				// New approach to telling rich text editors to debrief their textareas PDQ
				$('.a-needs-update').trigger('a.update');
				// If the file field is empty and the embed code hasn't been changed,
				// we can submit the edit form asynchronously
				// apostrophe.log(embedChanged);
				if((file.val() == '') && (!embedChanged))
				{
					event.preventDefault();
					$.post(url, form.serialize(), function(data) {
							update.html(data);
					});
				}
			});
		}
		else
		{
			apostrophe.log('apostrophe.mediaAjaxSubmitListener -- No form found');
		}
	};

	this.mediaFourUpLayoutEnhancements = function(options)
	{
		var items = $(options['selector']);

		if (typeof(items) == 'undefined' || !items.length) {
			apostrophe.log('apostrophe.mediaFourUpLayoutEnhancements -- Items is undefined or no items found');
			// apostrophe.log(items);
		}

		items.mouseover(function(){
			var item	= $(this);
			item.addClass('over');
		})
		.mouseout(function(){
			var item	= $(this);
			item.find('img').removeClass('dropshadow');
			item.removeClass('over');
		}).
		mouseleave(function(){
			var item	= $(this);
			if (!item.data('hold_delete'))
			{
				destroyItemSlug(item);
			};
		});

		items.find('.a-media-item-thumbnail')
		.hoverIntent(function(){
			var item = $(this).closest('.a-media-item');
			if (!item.data('hold_create'))
			{
				createItemSlug(item);
			};
		},function(){
			// mouse out
		});

		items.each(function(){
			var item	= $(this);
			if (item.hasClass('a-type-video'))
			{
				// We don't want to play videos in this view
				// We want the click to pass through to showSuccess
				// So we unbind the mediaEmbeddableToggle();
				item.unbind('embedToggle').find('.a-media-thumb-link').unbind('click.apostrophe').bind('click.apostrophe', function(){
					return true;
				});
			};
		});

		function createItemSlug(item)
		{
			var w = item.css('width');
			var h = item.css('height');
			var img = item.find('img');

			var slug = $('<div/>');
			slug.attr('id', item.attr('id')+'-slug');
			slug.addClass('a-media-item-slug');
			slug.css({ width:w, height:h });

			if (item.hasClass('last'))
			{
				slug.addClass('last');
			};

			item.wrap(slug).addClass('dropshadow expand').data('hold_create', 1);
			var offset = '-' + Math.floor(img.attr('height')/2) + 'px';
			item.css('margin-top',offset);
		}

		function destroyItemSlug(item)
		{
			if (item.parent('.a-media-item-slug').length) {
				item.unwrap();
			};
			item.removeClass('over dropshadow expand').css('margin-top','').data('hold_create', null);
		}
	};

	this.mediaEnableLinkAccount = function(previewUrl)
	{
		var form = $('#a-media-add-linked-account');
		var ready = false;
		form.submit(function()
		{
			if (ready)
			{
				return true;
			}
			$('#a-media-account-preview-wrapper').load(
				previewUrl,
				$('#a-media-add-linked-account').serialize(),
				function() {
					form.find('.a-show-busy').trigger('aHideBusy');
					$('#a-account-preview-ok').bind('click.apostrophe', function(event) {
						event.preventDefault();
						ready = true;
						form.submit();
					});
					$('#a-account-preview-cancel').bind('click.apostrophe', function(event) {
						event.preventDefault();
						$('#a-media-account-preview-wrapper').hide();
						return false;
					});
					$('#a-media-account-preview-wrapper').show();
				});
			return false;
		});
 	};

	this.mediaEmbeddableToggle = function(options)
	{
		var items = $(options['selector']);
		if (items.length) {
			items.each(function(){
				var item = $(this);
				item.bind('embedToggle',function(){
					var embed = item.data('embed_code');
					item.find('.a-media-item-thumbnail').addClass('a-previewing');
					item.find('.a-media-item-embed').removeClass('a-hidden').html(embed);
				});
				var link = item.find('.a-media-play-video');
				link.unbind('click.mediaEmbeddableToggle').bind('click.mediaEmbeddableToggle',function(e){
					e.preventDefault();
					item.trigger('embedToggle');
				});
			});
		}
		else
		{
			apostrophe.log('apostrophe.mediaEmbeddableToggle -- no items found');
		};
	};

	this.mediaAttachEmbed = function(options)
	{
		var id = options['id'];
		var embed = options['embed'];
		var mediaItem = $('#a-media-item-' + id);
		mediaItem.data('embed_code', embed);
	};

	this.mediaItemsIndicateSelected = function(cropOptions)
	{
		var ids = cropOptions.ids;
		aCrop.init(cropOptions);
		$('.a-media-selected-overlay').remove();
		$('.a-media-selected').removeClass('a-media-selected');

		var i;
		for (i = 0; (i < ids.length); i++)
		{
			id = ids[i];
			var selector = '#a-media-item-' + id;
			if (!$(selector).hasClass('a-media-selected'))
			{
				$(selector).addClass('a-media-selected');
			}
		}

		$('.a-media-item.a-media-selected').each(function(){
			$(this).children('.a-media-item-thumbnail').prepend('<div class="a-media-selected-overlay"></div>');
		});

		$('.a-media-selection-help').hide();
		if (!ids.length) {
			$('.a-media-selection-help').show();
		}

	 	$('.a-media-selected-overlay').fadeTo(0, 0.66);
	};

	this.mediaUpdatePreview = function()
	{
		$('#a-media-selection-preview').load(apostrophe.selectOptions.updateMultiplePreviewUrl, function(){
			// the preview images are by default set to display:none
			$('#a-media-selection-preview li:first').addClass('current');
			// set up cropping again; do hard reset to reinstantiate Jcrop
			aCrop.resetCrop(true);
			// Selection may have changed
			apostrophe.mediaItemsIndicateSelected(apostrophe.selectOptions);
			// Normalize heights of thumbnails for visual consistency
			var items = $('.a-media-selection-list-item');
			var listHeight = 0;
			items.each(function(){
				var item = $(this);
				(listHeight < item.height()) ? listHeight = item.height() : '';
			});
			items.css('height',listHeight);
			// apostrophe.log(listHeight);
		});
	};

	this.mediaDeselectItem = function(id)
	{
		$('#a-media-item-'+id).removeClass('a-media-selected');
		$('#a-media-item-'+id).children('.a-media-selected-overlay').remove();
	};

	this.mediaEnableSelect = function(options)
	{
		apostrophe.selectOptions = options;
		// Binding it this way avoids a cascade of two click events when someone
		// clicks on one of the buttons hovering on this

		// I had to bind to all of these to guarantee a click would come through
		$('.a-media-selection-list-item .a-delete').unbind('click.aMedia').bind('click.aMedia', function(e) {
			var p = $(this).parents('.a-media-selection-list-item');
			var id = p.data('id');
			$.get(options['removeUrl'], { id: id }, function(data) {
				$('#a-media-selection-list').html(data);
				apostrophe.mediaDeselectItem(id);
				apostrophe.mediaUpdatePreview();
			});
			return false;
		});

		apostrophe.mediaItemsIndicateSelected(options);

		$('.a-media-selected-item-overlay').fadeTo(0,.35); //cross-browser opacity for overlay
		$('.a-media-selection-list-item').hover(function(){
			$(this).addClass('over');
		},function(){
			$(this).removeClass('over');
		});

		// When you're in selecting mode, you can't click through to the showSuccess
		// So we use the thumbnail AND the title for making your media selection.
		$('.a-media-thumb-link, .a-media-item-title-link').unbind('click.aMedia').bind('click.aMedia', function(e) {
			e.preventDefault();
			$.get(options['multipleAddUrl'], { id: $(this).data('id') }, function(data) {
				$('#a-media-selection-list').html(data);
				apostrophe.mediaUpdatePreview();
			});
			$(this).addClass('a-media-selected');
			return false;
		});
	};

	this.mediaItemRefresh = function(options)
	{
		var id = options['id'];
		var url = options['url'];
		window.location = url;
	};

	this.mediaEnableMultiplePreview = function()
	{
		// the preview images are by default set to display:none
		$('#a-media-selection-preview li:first').addClass('current');
		// set up cropping again; do hard reset to reinstantiate Jcrop
		aCrop.resetCrop(true);
	};

	this.mediaEnableSelectionSort = function(multipleOrderUrl)
	{
		$('#a-media-selection-list').sortable({
			update: function(e, ui)
			{
				var serial = jQuery('#a-media-selection-list').sortable('serialize', {});
				$.post(multipleOrderUrl, serial);
			}
		});
	};

	this.mediaEnableUploadMultiple = function()
	{
		function aMediaUploadSetRemoveHandler(element)
		{
			$(element).find('.a-close').bind('click.apostrophe', function() {
					// Move the entire row to the inactive form
					var element = $($(this).parent().parent().parent()).remove();
					$('#a-media-upload-form-inactive').append(element);
					$('#a-media-add-photo').show();
					return false;
				});
		}
		// Move the first inactive element back to the active form
		$('#a-media-add-photo').bind('click.apostrophe', function() {
				var elements = $('#a-media-upload-form-inactive .a-form-row');
					$('#a-media-upload-form-subforms').append(elements);
					$('#a-media-add-photo').hide();
				return false;
			});
		// Move all the initially inactive elements to the inactive form
		function aMediaUploadInitialize()
		{
			$('#a-media-upload-form-inactive').append($('#a-media-upload-form-subforms .a-form-row.initially-inactive').remove());
			aMediaUploadSetRemoveHandler($('#a-media-upload-form-subforms'));
			$('#a-media-upload-form .a-cancel').bind('click.apostrophe', function() {
				$('#a-media-add').hide();
				return false;
			});
		}
		aMediaUploadInitialize();
	};

	this.menuToggle = function(options)
	{
		var button = options['button'];
		var menu;
		if (typeof(options[menu]) != "undefined")
		{
			menu = options['menu'];
		}
		else
		{
			menu = $(button).parent();
		}
		var classname = options['classname'];
		var overlay = options['overlay'];

		if (typeof(button) == "undefined") {
			apostrophe.log('apostrophe.menuToggle -- Button is undefined');
		}
		else
		{
			if (typeof button == "string") { button = $(button); } /* button that toggles the menu open & closed */
			if (typeof classname == "undefined" || classname == '') { classname = "show-options";	} /* optional classname override to use for toggle & styling */
			if (typeof overlay != "undefined" && overlay) { overlay = $('.a-page-overlay'); } /* optional full overlay */

			if (typeof(menu) == "object") {
				_menuToggle(button, menu, classname, overlay, options['beforeOpen'], options['afterClosed'], options['afterOpen'], options['beforeClosed'], options['focus'], options['debug']);
			}
		}
	};

	this.pager = function(selector, pagerOptions)
	{
		$(selector + ':not(.a-pager-processed)').each(function() {

			var pager = $(this);
			pager.addClass('a-pager-processed');
			pager.find('.a-page-navigation-number').css('display', 'block');
			pager.find('.a-page-navigation-number').css('float', 'left');

			var nb_pages = parseInt(pagerOptions['nb-pages']);
			var nb_links = parseInt(pagerOptions['nb-links']);
			var selected = parseInt($(this).find('.a-page-navigation-number.a-pager-navigation-disabled').text());

			// If the number of links allowed is greater than the total number of pages returned
			// then we do not need the arrows. So let's use this class name so scope 'disabled' styles.
			(nb_links >= nb_pages) ? pager.addClass('a-pager-arrows-disabled') : pager.removeClass('a-pager-arrows-disabled');

			var min = selected;
			var max = selected + nb_links - 1;

			var links_container_container = pager.find('.a-pager-navigation-links-container-container');
			links_container_container.width((nb_links * pager.find('.a-page-navigation-number').first().outerWidth()));
			links_container_container.css('overflow', 'hidden');

			var links_container = pager.find('.a-pager-navigation-links-container');
			links_container.width((nb_pages * pager.find('.a-page-navigation-number').first().outerWidth()));

			var first = pager.find('.a-pager-navigation-first');
			var prev = pager.find('.a-pager-navigation-previous');
			var next = pager.find('.a-pager-navigation-next');
			var last = pager.find('.a-pager-navigation-last')

			function calculateMinAndMax()
			{
				if ((min < 1) && (max > nb_pages))
				{
					min = 1;
					max = nb_pages;
				}
				else if (min < 1)
				{
					var diff = 0;

					if (min < 0)
					{
						diff = 0 - min;
						diff = diff + 1;
					}
					else
					{
						diff = 1
					}
					min = 1;
					max = max + diff;
				}
				else if (max > nb_pages)
				{
					var diff = max - nb_pages;
					max = nb_pages;
					min = min - diff;
				}
			}

			function toggleClasses()
			{
				pager.find('.a-pager-navigation-disabled').removeClass('a-pager-navigation-disabled');
				if (min == 1)
				{
					first.addClass('a-pager-navigation-disabled');
					prev.addClass('a-pager-navigation-disabled');
				}
				else if (min == ((nb_pages - nb_links) + 1))
				{
					next.addClass('a-pager-navigation-disabled');
					last.addClass('a-pager-navigation-disabled');
				}
			}

			function updatePageNumbers()
			{
				pager.find('.a-page-navigation-number').each(function() {
					var current = parseInt($(this).text());

					if ((current >= min) && (current <= max))
					{
						$(this).show();
					}
					else
					{
						$(this).hide();
					}
				});
			}

			function animatePageNumbers() {
				var width = links_container.children('.a-page-navigation-number').first().outerWidth();

				width = (min - 1) * -width;
				links_container.animate({marginLeft: width}, 250, 'swing');
			}

			next.bind('click.apostrophe', function(e) {
				min = min + nb_links;
				max = max + nb_links;

				calculateMinAndMax();
				toggleClasses();
				animatePageNumbers();

				return false;
			});

			last.bind('click.apostrophe', function(e) {
				min = nb_pages;
				max = nb_pages + nb_links - 1;

				calculateMinAndMax();
				toggleClasses();
				animatePageNumbers();

				return false;
			});

			prev.bind('click.apostrophe', function(e) {
				min = min - nb_links;
				max = max - nb_links;

				calculateMinAndMax();
				toggleClasses();
				animatePageNumbers();

				return false;
			});

			first.bind('click.apostrophe', function(e) {
				e.preventDefault();

				min = 1;
				max = nb_links;

				calculateMinAndMax();
				toggleClasses();
				animatePageNumbers();

				return false;
			});

			calculateMinAndMax();
			toggleClasses();
			animatePageNumbers();
		});
	};

		/* Example Mark-up
		<script type="text/javascript">
			apostrophe.accordion({'accordion_toggle': '.a-accordion-item h3' });
		</script>

		BEFORE:
		<div>
			<h3>Heading</h3>
			<div>Content</div>
		</div>

		AFTER:
		<div class="a-accordion">
			<h3 class="a-accordion-toggle">Heading</h3>
			<div class="a-accordion-content">Content</div>
		</div>
		*/

	this.accordion = function(options)
	{
		var toggle = options['accordion_toggle'];

		if (typeof toggle == "undefined") {
			apostrophe.log('apostrophe.accordion -- Toggle is undefined.');
		}
		else
		{
			if (typeof toggle == "string") { toggle = $(toggle); }

			var container = toggle.parent();
			var content = toggle.next();

			container.addClass('a-accordion');
			content.addClass('a-accordion-content');

			toggle.each(function() {
				var t = $(this);
				t.bind('click.apostrophe', function(event){
					event.preventDefault();
					t.closest('.a-accordion').toggleClass('open');
				})
				.hover(function(){
					t.addClass('hover');
				},function(){
					t.removeClass('hover');
				});
			}).addClass('a-accordion-toggle');
		}
	};

	this.enablePageSettings = function(options)
	{
		apostrophe.log('apostrophe.enablePageSettings');
		var form = $('#' + options['id'] + '-form');
		// This was causing double submits, and double subpages in FM profiles.
		// The submit button is now an a-act-as-submit and it works without this extra hack.
		// DO NOT put this back witout talking to me. -Tom
		// $('#' + options['id'] + '-submit').bind('click.apostrophe', function() {
		// 	form.submit();
		// });

		// The form will not actually submit until ajaxDirty is false. This allows us
		// to wait for asynchronous things like the slug field AJAX updates to complete
		var ajaxDirty = false;
		form.submit(function() {
			tryPost();
			return false;
		});

		function tryPost()
		{
			if (ajaxDirty)
			{
				setTimeout(tryPost, 250);
			}
			else
			{
				$.post(options['url'], form.serialize(), function(data) {
					$('.a-page-overlay').hide();
					$('#' + options['id']).html(data);
				});
			}
		}

		if (options['new'])
		{
			var slugField = form.find('[name="settings[slug]"]');
			var titleField = form.find('[name="settings[realtitle]"]');
			var timeout = null;

			function changed()
			{
				ajaxDirty = true;
				$.get(options['slugifyUrl'], { slug: $(titleField).val() }, function(data) {
					slugField.val(options['slugStem'] + '/' + data);
					ajaxDirty = false;
				});
				timeout = null;
			}
			function setChangedTimeout()
			{
				// AJAX on every keystroke kills the server and isn't nice to the
				// browser either. Set a half-second timeout to do it if we don't
				// already have such a timeout ticking down
				if (!timeout)
				{
					timeout = setTimeout(changed, 500);
				}
			}
			titleField.focus();
			titleField.change(changed);
			titleField.keyup(setChangedTimeout);

			// More Options... Button
			$(form).find('.a-more-options-btn').bind('click.apostrophe', function(e){
				e.preventDefault();
				$(this).hide().next().removeClass('a-hidden');
			});
		}

		var joinedtemplate = form.find('[name="settings[joinedtemplate]"]');
		if (joinedtemplate.length)
		{
			joinedtemplate.change(function() {
				updateEngineAndTemplate();
			});

			function updateEngineAndTemplate()
			{
				var url = options['engineUrl'];

				var engineSettings = form.find('.a-engine-page-settings');
				var val = joinedtemplate.val().split(':')[0];
				if (val === 'a')
				{
					engineSettings.html('');
				}
				else
				{
					// null comes through as a string "null". false comes through as a string "false". 0 comes
					// through as a string "0", but PHP accepts that, fortunately
					$.get(url, { id: options['pageId'] ? options['pageId'] : 0, engine: val }, function(data) {
						engineSettings.html(data);
					});
				}
			}
			updateEngineAndTemplate();
		}
	};

	this.accordionEnhancements = function(options)
	{

		var nurl = options['url'];
		var name = options['name'];
		var nest = options['nest'];

		var nav = $("#a-nav-" + name + "-" + nest);

		nav.sortable(
		{
			delay: 100,
			update: function(e, ui)
			{
				var serial = nav.sortable('serialize', {key:'a-tab-nav-item[]'});
				var options = {"url":nurl,"type":"post"};
				options['data'] = serial;
				$.ajax(options);

				// Fixes Margin
				nav.children().removeClass('first second next-last last');
				nav.children(':first').addClass('first');
				nav.children(':last').addClass('last');
				nav.children(':first').next("li").addClass('second');
				nav.children(':last').prev("li").addClass('next-last');
			},
			items: 'li:not(.extra)'
		});

	};

	this.allTagsToggle = function(options)
	{
		var allTags = options['selector'] ? $(options['selector']) : $('.a-tag-sidebar-title.all-tags');

		allTags.hover(function(){
			allTags.addClass('over');
		},function(){
			allTags.removeClass('over');
		});

		allTags.bind('click.apostrophe', function(){
			allTags.toggleClass('open');
			allTags.next().toggle();
		});
	};

	this.searchCancel = function(options)
	{

		var search = options['search'];
		$('#a-media-search-remove').show();
		$('#a-media-search-submit').hide();

		$('#a-media-search').bind("keyup blur", function(e)
		{
			if ($(this).val() === search)
			{
				$('#a-media-search-remove').show();
				$('#a-media-search-submit').hide();
			}
			else
			{
				$('#a-media-search-remove').hide();
				$('#a-media-search-submit').show();
			}
		});

		$('#a-media-search').bind('aInputSelfLabelClear', function(e) {
			$('#a-media-search-remove').show();
			$('#a-media-search-submit').hide();
		});
	};

	// Hide / Show the page overlay. Accepts true or false, and an optional call back
	// apostrophe.togglePageOverlay({ toggle: true | false , callback : f() });
	this.togglePageOverlay = function(options)
	{
		var overlay = $('.a-page-overlay');
		if (options['toggle'])
		{
			// I want it to fade in / out
			overlay.fadeIn(100);
			// overlay.addClass('active');
		}
		else
		{
			// overlay.removeClass('active');
			overlay.hide();
		}
		if (options.callback && typeof(options.callback) === 'function')
		{
			options.callback();
		};
	};

	this.aInjectActualUrl = function(options) {

		// Media selection links and similar "go away, get something, come back" links need
		// to know what page to come back to. JavaScript knows much better than PHP does because
		// PHP can be confused by an AJAX update action etc. It's a little tricky because we
		// have to reencode the URL properly. Find the 'after' parameter, which is a URL to
		// return to in order to save the selection, and parse it in order to add the
		// 'actual_url' parameter to it. Then rebuild the whole thing correctly

		// apostrophe.log('apostrophe.aInjectActualUrl');

		var target = options['target'];

		// apostrophe.log('apostrophe.aInjectActualUrl -- target = ' + target);

		$(target).find('.a-inject-actual-url').each(function() {
			var href = $(this).attr('href');
			var parsed = apostrophe.parseUrl(href);
			if (parsed.queryData.after !== undefined)
			{
				var afterParsed = apostrophe.parseUrl(parsed.queryData.after);
				afterParsed.queryData.actual_url = window.location.href;
				afterParsed.query = $.param(afterParsed.queryData);
				parsed.queryData.after = afterParsed.stem + afterParsed.query;
				parsed.query = $.param(parsed.queryData);
				href = parsed.stem + parsed.query;
				$(this).attr('href', href);
			}
		});
	};

	this.aActAsSubmit = function(options) {

			// apostrophe.log('apostrophe.aActAsSubmit');

			var target = options['target'];

			// Anchor elements that act as submit buttons. On some older browsers this might not trigger
			// other submit handlers for the form before native submit, however it seems to work just fine
			// in modern browsers even if the form is an ajax form

			var actAsSubmit = $(target).find('.a-act-as-submit');
			var actAsSubmitForm = actAsSubmit.closest('form');
			var actAsSubmitFormInputs = actAsSubmitForm.find('input[type="text"]');

			actAsSubmit.each(function(){
				var submit = $(this);

				var form = submit.closest('form');
				if (!form.find('input[type="submit"]').length)
				{
					var hidden = $('<input type="submit"/>');
					hidden.attr('value', submit.text());
					hidden.addClass('a-hidden-submit');
					submit.after(hidden);
				}

				submit.unbind('click.aActAsSubmit').bind('click.aActAsSubmit', function(event) {
					var form = submit.closest('form');

					// Submit buttons have names used to distinguish them.
					// Fortunately, anchors have names too. There is NO
					// default name - and in particular 'submit' breaks
					// form.submit, so don't use it
					var name = submit.attr('name');
					if (name && name.length)
					{
						var hidden = $('<input type="hidden"/>');
						// To correctly simulate what happens when you click a named submit button
						// we need a hidden element with the same name and the label as the value.
						// The name is what the server end is really looking for but let's get this
						// as accurate as possible. Anchor tags don't have a val attribute but they
						// do have label text, which is what you get as a value with a normal
						// named submit button
						hidden.attr('name', name);
						hidden.val(submit.text());
						form.append(hidden);
					}

					// If the form being submitted has this event bound to it, it will execute it. Awesome, right?
					// If it doesn't, jQ just lets it go quietly.
					form.trigger('aActAsSubmitCallback');
					form.submit();
					return false;
				});

			});
	}

	// A very small set of things that allow us to write CSS and HTML as if they were
	// better than they are. This is called on every page load and AJAX refresh, so resist
	// the temptation to get too crazy here.

	// Specifying a target option can help performance by not searching the rest
	// of the DOM for things that have already been magicked

	// CODE HERE MUST TOLERATE BEING CALLED SEVERAL TIMES. Use namespaced binds and unbinds.
	this.smartCSS = function(options)
	{

		var target = 'body';
		if (options && options['target'])
		{
			target = options['target'];
		};

		apostrophe.aInjectActualUrl({ target : target });
		apostrophe.aShowBusy();
		apostrophe.aActAsSubmit({ target : target })

		// The contents of this function can be migrated to better homes
		// if it makes sense to move them.
		// Once this function is empty it can be deleted
		// called in partial a/globalJavascripts
		// Variants
		$(target).find('a.a-variant-options-toggle').unbind('click.aVariantOptionsToggle').bind('click.aVariantOptionsToggle', function(){
			$(this).parents('.a-slots').children().css('z-index','699');
			$(this).parents('.a-slot').css('z-index','799');
		});
		// Cross Browser Opacity Settings
		$(target).find('.a-nav .a-archived-page').fadeTo(0,.5); // Archived Page Labels
		// Apply clearfix on controls and options
		$(target).find('.a-controls, .a-options').addClass('clearfix');
		// Add 'last' Class To Last Option
		$(target).find('.a-controls li:last-child').addClass('last');
		// Valid way to have links open up in a new browser window
		// Example: <a href="..." rel="external">Click Meh</a>
		$(target).find('a[rel="external"]').attr('target','_blank');

		// THINGS WE'D LIKE TO GET RID OF START HERE

		// Apply any classes or additional markup necessary for apostrophe buttons via the .a-btn class
		// called in partial a/globalJavascripts. This is deprecated, we should put the right spans in them to
		// begin with, which is easier now with a_js_button and a_link_button, so we're showing these
		// not-properly-formatted buttons in red in anticipation of killing this code

		var aBtns = $(target).find('.a-btn,.a-submit,.a-cancel');
		aBtns.each(function() {
			var aBtn = $(this);
			// Setup Icons for buttons with icons that are missing the icon container
			// Markup: <a href="#" class="a-btn icon a-some-icon"><span class="icon"></span>Button</a>
			if (aBtn.is('a') && aBtn.hasClass('icon') && !aBtn.children('.icon').length)
			{
				// Button Exterminator
				aBtn.prepend('<span class="icon"></span>').addClass('a-fix-me');
			};
		});
	};

	// Breaks the url into a stem (everything before the query, inclusive of the ?), a query
	// (the encoded query string), and queryData (the query string parsed into an object)
	this.parseUrl = function(url)
	{
		var info = {};
		var q = url.indexOf('?');
		if (q !== -1)
		{
			info.stem = url.substr(0, q + 1);
			query = url.substr(q + 1);
			info.query = query;
			info.queryData = apostrophe.decodeQuery(query);
		}
		else
		{
			info.stem = url;
			info.query = '';
			info.queryData = {};
		}
		return info;
	};

	// Adapted from http://stackoverflow.com/questions/901115/get-querystring-values-with-jquery/901144#901144
	// This decodeQuery function is accordingly released under cc attribution-share alike
	this.decodeQuery = function(query)
	{
		var urlParams = {};
		(function () {
				var e,
						a = /\+/g,	// Regex for replacing addition symbol with a space
						r = /([^&=]+)=?([^&]*)/g,
						d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
						q = query;
				while (e = r.exec(q))
					 urlParams[d(e[1])] = d(e[2]);
		})();
		return urlParams;
	};

	this.audioPlayerSetup = function(aAudioContainer, file)
	{
		aAudioContainer = $(aAudioContainer);
		if (typeof(aAudioContainer) == 'object' && aAudioContainer.length)
		{
			var global_lp = 0;
			var global_wtf = 0;

			var btnPlay = aAudioContainer.find(".a-audio-play");
			var btnPause = aAudioContainer.find(".a-audio-pause");
			var sliderPlayback = aAudioContainer.find('.a-audio-playback');
			var sliderVolume = aAudioContainer.find('.a-audio-volume');
			var loadingBar = aAudioContainer.find('.a-audio-loader');
			var time = aAudioContainer.find('.a-audio-time');
			var aAudioPlayer = aAudioContainer.find('.a-audio-player');
			var aAudioInterface = aAudioContainer.find('.a-audio-player-interface');
			aAudioPlayer.jPlayer({
				ready: function ()
				{
					this.element.jPlayer("setFile", file);
					aAudioInterface.removeClass('a-loading');
				},
				swfPath: '/apostrophePlugin/swf',
				customCssIds: true
			})
			.jPlayer("onProgressChange", function(lp,ppr,ppa,pt,tt) {
		 		var lpInt = parseInt(lp);
		 		var ppaInt = parseInt(ppa);
		 		global_lp = lpInt;
				loadingBar.progressbar('option', 'value', lpInt);
		 		sliderPlayback.slider('option', 'value', ppaInt);

				if (global_wtf && global_wtf == parseInt(tt)) {
					timeLeft = parseInt(tt) - parseInt(pt);
					time.text($.jPlayer.convertTime(timeLeft));
				}
				else
				{
					global_wtf = parseInt(tt);
				}
			})
			.jPlayer("onSoundComplete", function() {
				// this.element.jPlayer("play"); // Loop
			});
			btnPause.hide();
			loadingBar.progressbar();

			btnPlay.bind('click.apostrophe', function() {
				aAudioPlayer.jPlayer("play");
				btnPlay.hide();
				btnPause.show();
				return false;
			});

			btnPause.bind('click.apostrophe', function() {
				aAudioPlayer.jPlayer("pause");
				btnPause.hide();
				btnPlay.show();
				return false;
			});

			sliderPlayback.slider({
				max: 100,
				range: 'min',
				animate: false,
				slide: function(event, ui) {
					aAudioPlayer.jPlayer("playHead", ui.value*(100.0/global_lp));
				}
			});

			sliderVolume.slider({
				value : 50,
				max: 100,
				range: 'min',
				animate: false,
				slide: function(event, ui) {
					aAudioPlayer.jPlayer("volume", ui.value);
				}
			});
		}
		else
		{
			throw "Cannot find DOM Element for Audio Player.";
		}
	};

	// Just the toggles to display different parts of the page settings dialog
	this.enablePermissionsToggles = function()
	{
		var stem = '.view-options-widget';
		$(stem).change(function() {
			var v = $(stem + ':checked').val();
			if (v === 'login')
			{
				$('#a-page-permissions-view-extended').show();
			}
			else
			{
				$('#a-page-permissions-view-extended').hide();
			}
		});
		$('#a_settings_settings_view_options_public').change();

		$('#a_settings_settings_edit_admin_lock').change(function()
		{
			if ($(this).attr('checked'))
			{
				$('#a-page-permissions-edit-extended').hide();
			}
			else
			{
				$('#a-page-permissions-edit-extended').show();
			}
		});
		$('#a_settings_settings_edit_admin_lock').change();
	};

	// One permissions widget. Invoked several times - there are several in the page settings dialog
	this.enablePermissions = function(options)
	{
		// We need a fairly complex permissions widget. Deal with that.
		// Strategy: on every action update a data structure.
		// On every action that adds or removes an item, rebuild
		// the HTML representation
		var w = $('#' + options['id']);
		// We take in a flat array of data about users,
		// flip it into a list of ids and a hash of information
		// about those ids for efficiency
		var ids = [];
		var input = eval($('#' + options['hiddenField']).val());
		for (var i = 0; (i < input.length); i++)
		{
			ids[ids.length] = input[i]['id'];
		}
		var data = { };
		for (var i = 0; (i < ids.length); i++)
		{
			data[ids[i]] = input[i];
		}
		function rebuild() {
			var select = $('<select class="a-permissions-add"></select>');
			var list = $('<ul class="a-permissions-entries"></ul>');
			var option = $('<option></option>');
			option.val('');
			option.text(options['addLabel']);
			select.append(option);
			var j = 0;
			for (var i = 0; (i < ids.length); i++)
			{
				var user = data[ids[i]];
				var id = user['id'];
				var who = user['name'];
				if (!user['selected'])
				{
					var option = $('<option></option>');
					option.val(id);
					option.text(who);
					select.append(option);
				}
				else
				{
					var liMarkup = '<li class="a-permission-entry ' + ((j%2) ? 'even':'odd') + ' clearfix"><ul><li class="a-who"></li>';
					if (options['extra'])
					{
						liMarkup += '<li class="a-cascade-option extra"><div class="cascade-checkbox"><input type="checkbox" value="1" /> ' + options['extraLabel'] + '</div></li>';
					}
					if (options['hasSubpages'])
					{
						liMarkup += '<li class="a-cascade-option apply-to-subpages"><div class="cascade-checkbox"><input type="checkbox" value="1" /> ' + options['applyToSubpagesLabel'] + '</div></li>';
					}
					// PLEASE NOTE code is targeting a-close-small, if you change that class you have to change the selector elsewhere
					liMarkup += '<li class="a-actions"><a href="#" class="a-close-small a-btn icon no-label no-bg alt">' + options['removeLabel'] + '<span class="icon"></span></a></li></ul></li>';
					li = $(liMarkup);
					li.find('.a-who').text(who);
					if (options['extra'])
					{
						li.find('.extra [type=checkbox]').attr('checked', user['extra']);
					}
					li.find('.apply-to-subpages [type=checkbox]').attr('checked', user['applyToSubpages']);
					li.data('id', id);
					if (user['selected'] === 'remove')
					{
						li.addClass('a-removing');
						li.find('.a-extra input').attr('disabled', true);
					}
					list.append(li);
					j++;
				}
			}
			select.val('');
			select.change(function() {
				var id = select.val();
				data[id]['selected'] = true;
				rebuild();
				return false;
			});
			list.find('.a-close-small').bind('click.apostrophe', function() {
				var id = $(this).parents('.a-permission-entry').data('id');
				var user = data[id];
				if (user['selected'] === 'remove')
				{
					user['selected'] = true;
				}
				else
				{
					user['selected'] = 'remove';
				}
				rebuild();
				return false;
			});
			list.find('.extra [type=checkbox]').change(function() {
				var id = $(this).parents('.a-permission-entry').data('id');
				data[id]['extra'] = $(this).attr('checked');
				updateHiddenField();
				return true;
			});
			list.find('.apply-to-subpages [type=checkbox]').change(function() {
				var id = $(this).parents('.a-permission-entry').data('id');
				data[id]['applyToSubpages'] = $(this).attr('checked');
				updateHiddenField();
				return true;
			});
			w.html('');
			w.append(list);
			w.append(select);
			updateHiddenField();
		}
		rebuild();
		function updateHiddenField()
		{
			// Flatten the data into an array again for readout
			var flat = [];
			for (var i = 0; (i < ids.length); i++)
			{
				flat[flat.length] = data[ids[i]];
			}
			$('#' + options['hiddenField']).val(JSON.stringify(flat));
		}
	};

	this.enableMediaEditMultiple = function()
	{
		$('.a-media-multiple-submit-button').bind('click.apostrophe', function() {
			$('#a-media-edit-form-0').submit();
			return false;
		});
		$('#a-media-edit-form-0').submit(function() {
			return true;
		});
		$('#a-media-edit-form-0 .a-media-editor .a-delete').bind('click.apostrophe', function() {
			$(this).parents('.a-media-editor').remove();
			if ($('#a-media-edit-form-0 .a-media-editor').length === 0)
			{
				window.location.href = $('#a-media-edit-form-0 .a-controls .a-cancel:first').attr('href');
			}
			return false;
		});
	};

	this.aAdminEnableFilters = function()
	{
		$('#a-admin-filters-open-button').bind('click.apostrophe', function() {
			$('#a-admin-filters-container').slideToggle();
			return false;
		});
	};

	this.historyOpen = function(options)
	{
		var id = options['id'];
		var name = options['name'];
		var versionsInfo = options['versionsInfo'];
		var all = options['all'];
		var revert = options['revert'];
		var revisionsLabel = options['revisionsLabel'];
		for (i = 0; (i < versionsInfo.length); i++)
		{
			version = versionsInfo[i].version;
			$("#a-history-item-" + version).data('params',
				{ 'preview':
					{
						id: id,
						name: name,
						subaction: 'preview',
						version: version
					},
					'revert':
					{
						id: id,
						name: name,
						subaction: 'revert',
						version: version
					},
					'cancel':
					{
						id: id,
						name: name,
						subaction: 'cancel',
						version: version
					}
				});
		}
		if ((versionsInfo.length == 10) && (!all))
		{
			$('#a-history-browser-view-more').show();
		}
		else
		{
			$('#a-history-browser-view-more').hide().before('&nbsp;');
		}

		$('#a-history-browser-number-of-revisions').text(versionsInfo.length + revisionsLabel);

		$('.a-history-browser-view-more').mousedown(function(){
			$(this).children('img').fadeIn('fast');
		});

		$('.a-history-item').bind('click.apostrophe', function() {

			$('.a-history-browser').hide();

			var params = $(this).data('params');

			var targetArea = "#"+$(this).parent().data('area');								// this finds the associated area that the history browser is displaying
			var historyBtn = $(targetArea+ ' .a-area-controls a.a-history');	// this grabs the history button
			var cancelBtn = $('#a-history-cancel-button');										// this grabs the cancel button for this area
			var revertBtn = $('#a-history-revert-button');										// this grabs the history revert button for this area

			$(historyBtn).siblings('.a-history-options').show();

			$.post( //User clicks to PREVIEW revision
				revert,
				params.preview,
				function(result)
				{
					$('#a-slots-' + id + '-' + name).html(result);
					$(targetArea).addClass('previewing-history');
					historyBtn.addClass('a-disabled');
					$('.a-page-overlay').hide();
				}
			);

			// Assign behaviors to the revert and cancel buttons when THIS history item is clicked
			revertBtn.bind('click.apostrophe', function(){
				$.post( // User clicks Save As Current Revision Button
					revert,
					params.revert,
					function(result)
					{
						$('#a-slots-' + id + '-' + name).html(result);
						historyBtn.removeClass('a-disabled');
						_closeHistory();
					}
				);
			});

			cancelBtn.bind('click.apostrophe', function(){
				$.post( // User clicks CANCEL
					revert,
					params.cancel,
					function(result)
					{
					 	$('#a-slots-' + id + '-' + name).html(result);
					 	historyBtn.removeClass('a-disabled');
						_closeHistory();
					}
				);
			});
		});

		$('.a-history-item').hover(function(){
			$(this).css('cursor','pointer');
		},function(){
			$(this).css('cursor','default');
		});
	};

	this.enableCloseHistoryButtons = function(options)
	{
		var closeHistoryBtns = $(options['close_history_buttons']);
		closeHistoryBtns.unbind('click.apostrophe').bind('click.apostrophe', function(){
			_closeHistory();
		});
	};

	this.enablePageSettingsButtons = function(options)
	{
		var aPageSettingsURL = options['aPageSettingsURL'];
		var aPageSettingsCreateURL = options['aPageSettingsCreateURL'];

		apostrophe.menuToggle({"button":"#a-page-settings-button","classname":"","overlay":true,
			"beforeOpen": function() {
				$.ajax({
						type:'POST',
						dataType:'html',
						success:function(data, textStatus){
							$('#a-page-settings').html(data);
						},
						complete:function(XMLHttpRequest, textStatus){
						},
						url: aPageSettingsURL
				});
			},
			"afterClosed": function() {
				$('#a-page-settings').html('');
			}
		});
		apostrophe.menuToggle({"button":"#a-create-page-button","classname":"","overlay":true,
			"beforeOpen": function() {
				$.ajax({
						type:'POST',
						dataType:'html',
						success:function(data, textStatus){
							$('#a-create-page').html(data);
						},
						complete:function(XMLHttpRequest, textStatus){
						},
						url: aPageSettingsCreateURL
				});
			},
			"afterClosed": function() {
				$('#a-create-page').html('');
			}
		});
	};

	this.enableUserAdmin = function(options)
	{
		// Right now this is also called for groups and permissions admin, account for that if you add anything nutty. -Tom
		$('.a-admin #a-admin-filters-container #a-admin-filters-form .a-form-row .a-admin-filter-field br').replaceWith('<div class="a-spacer"></div>');
		aMultipleSelectAll({ 'choose-one': options['choose-one-label']});
	};

	// Private methods callable only from the above (no this.foo = bar)
	function slotUpdateMoveButtons(id, name, slot, n, slots, updateAction)
	{
		var up = $(slot).find('.a-arrow-up:first');
		var down = $(slot).find('.a-arrow-down:first');

		if (n > 0)
		{
			// TODO: this is not sensitive enough to nested areas
			up.parent().removeClass('a-hidden');
			up.unbind('click.apostrophe').bind('click.apostrophe', function() {
				if ($(slot).hasClass('a-editing') || $(slots[n - 1]).hasClass('a-editing'))
				{
					alert(apostrophe.messages.save_changes_first);
					return false;
				}
				// It would be nice to confirm success here in some way
				$.get(updateAction, { id: id, name: name, permid: $(slot).data('a-permid'), up: 1 });
				$(slot).find('.a-needs-update').trigger('a.update');
				$(slots[n - 1]).find('.a-needs-update').trigger('a.update');
				apostrophe.swapNodes(slot, slots[n - 1]);
				apostrophe.areaUpdateMoveButtons(updateAction, id, name);
				return false;
			});
		}
		else
		{
			up.parent().addClass('a-hidden');
		}
		if (n < (slots.length - 1))
		{
			down.parent().removeClass('a-hidden');
			down.unbind('click.apostrophe').bind('click.apostrophe', function() {
				// apostrophe.log(slot);
				if ($(slot).hasClass('a-editing') || $(slots[n + 1]).hasClass('a-editing'))
				{
					alert(apostrophe.messages.save_changes_first);
					return false;
				}
				// It would be nice to confirm success here in some way
				$.get(updateAction, { id: id, name: name, permid: $(slot).data('a-permid'), up: 0 });
				$(slot).find('.a-needs-update').trigger('a.update');
				$(slots[n + 1]).find('.a-needs-update').trigger('a.update');
				apostrophe.swapNodes(slot, slots[n + 1]);
				apostrophe.areaUpdateMoveButtons(updateAction, id, name);
				return false;
			});
		}
		else
		{
			down.parent().addClass('a-hidden');
		}
	}

	function slotShowEditViewPreloaded(pageid, name, permid)
	{
		var fullId = pageid + '-' + name + '-' + permid;
 		var editBtn = $('#a-slot-edit-' + fullId);
 		var editSlot = $('#a-slot-' + fullId);
		var editArea = editSlot.closest('.a-area');
		var editContainers = editSlot.add(editArea);

		editContainers.addClass('a-editing').removeClass('a-normal'); // Apply a class to the Area and Slot Being Edited
		editSlot.children('.a-slot-content').children('.a-slot-content-container').hide(); // Hide the Content Container
		editSlot.children('.a-slot-content').children('.a-slot-form').fadeIn(); // Fade In the Edit Form
		editSlot.children('.a-control li.variant').hide(); // Hide the Variant Options
	}

	function _browseHistory(area)
	{
		var areaControls = area.find('ul.a-area-controls');
		var areaControlsTop = areaControls.offset().top;
		$('.a-page-overlay').fadeIn();
		// Clear Old History from the Browser
		if (!area.hasClass('browsing-history'))
		{
			$('.a-history-browser .a-history-items').html('<tr class="a-history-item"><td class="date"><img src="\/apostrophePlugin\/images\/a-icon-loader-2.gif"><\/td><td class="editor"><\/td><td class="preview"><\/td><\/tr>');
			area.addClass('browsing-history');
		}
		// Positioning the History Browser
		$('.a-history-browser').css('top',(areaControlsTop-5)+"px"); //21 = height of buttons plus one margin
		$('.a-history-browser').fadeIn();
		$('.a-page-overlay').bind('click.apostrophe', function(){
			_closeHistory();
			$(this).unbind('click');
		});
		$('#a-history-preview-notice-toggle').bind('click.apostrophe', function(){
			$('.a-history-preview-notice').children(':not(".a-history-options")').slideUp();
		});
	}

	function _closeHistory()
	{
		$('a.a-history-btn').parents('.a-area').removeClass('browsing-history');
		$('a.a-history-btn').parents('.a-area').removeClass('previewing-history');
		$('.a-history-browser, .a-history-preview-notice').hide();
		$('body').removeClass('history-preview');
		$('.a-page-overlay').hide();
	}

	function _pageTemplateToggle(aPageTypeSelect, aPageTemplateSelect)
	{
	}

	function _menuToggle(button, menu, classname, overlay, beforeOpen, afterClosed, afterOpen, beforeClosed, focus, debug)
	{
		// Menu must have an ID.
		// If the menu doesn't have one, we create it by appending 'menu' to the Button ID
		if (menu.attr('id') == '')
		{
			var	newID = button.attr('id')+'-menu';
			menu.attr('id', newID).addClass('a-options-container');
		}

		// Menu listens for ESCAPE key if it's open
    $(document).unbind('keyup.' + menu.attr('id')).bind('keyup.' + menu.attr('id'), function(event) {
      if (event.keyCode === 27) {
				// Hey you pressed escape
	      apostrophe.log('apostrophe.menuToggle -- ESC')
				// Does the menu have the open class when you're pressing escape?
				if (menu.hasClass(classname))
				{
					// Close that menu
      		apostrophe.log('apostrophe.menuToggle -- ESC Pressed: keyup.' + menu.attr('id'))
					menu.trigger('toggleClosed');
	        return false;
				}
      }
    });

		// Button Toggle
		button.unbind('click.menuToggle').bind('click.menuToggle', function(event){
			event.preventDefault();
			if (!button.hasClass('aActiveMenu'))
			{
				menu.trigger('toggleOpen');
			}
			else
			{
				menu.trigger('toggleClosed');
			}
		}).addClass('a-options-button');

		if (beforeOpen) { menu.bind('beforeOpen', beforeOpen); }
		if (afterClosed) { menu.bind('afterClosed', afterClosed); }
		if (afterOpen) { menu.bind('afterOpen', afterOpen);	}
		if (beforeClosed) { menu.bind('beforeClosed', beforeClosed); }

		var clickHandler = function(event){
			var target = $(event.target);
			if (target.hasClass('a-page-overlay') || target.hasClass('a-cancel') || (!target.parents().is('#'+menu.attr('id')) && !target.parents().hasClass('ui-widget')) && target.parents('html').length)
			{
				menu.trigger('toggleClosed');
			}
		}

		// Open Menu, Create Listener
		menu.unbind('toggleOpen').bind('toggleOpen', function(){
			menu.trigger('beforeOpen');
			button.addClass('aActiveMenu');
			menu.parents().addClass('ie-z-index-fix');
			button.closest('.a-controls').addClass('aActiveMenu');
			menu.addClass(classname);
			if (overlay) { overlay.fadeIn(); }
			$(document).bind('click.menuToggleClickHandler', clickHandler);
			if (focus) { $(focus).focus(); };
			menu.trigger('afterOpen');
		});

		// Close Menu, Destroy Listener
		menu.unbind('toggleClosed').bind('toggleClosed', function(){
			menu.trigger('beforeClosed');
			button.removeClass('aActiveMenu');
			menu.parents().removeClass('ie-z-index-fix');
			button.closest('.a-controls').removeClass('aActiveMenu');
			menu.removeClass(classname);
			if (overlay) { overlay.hide(); };
			$(document).unbind('click.menuToggleClickHandler'); // Clear out click event
			menu.trigger('afterClosed');
		});

		// Any .a-cancel buttos in the menu itself close it
		$('#' + menu.attr('id') + ' .a-cancel').die('click.aMenuToggle').live('click.aMenuToggle',function(e){
			e.preventDefault();
			menu.trigger('toggleClosed');
			return false;
		});
	}

}

window.apostrophe = new aConstructor();

