View file upload/js/xenforo/full/discussion.js

File size: 33.31Kb
/** @param {jQuery} $ jQuery Object */
!function($, window, document, _undefined)
{
	//TODO: Enable jQuery plugin and compressor in editor template.

	/**
	 * Enables quick reply for a message form
	 * @param $form
	 */
	XenForo.QuickReply = function($form)
	{
		if ($('#messageList').length == 0)
		{
			return console.error('Quick Reply not possible for %o, no #messageList found.', $form);
		}

		var $lastDateInput = $('input[name="last_date"]', $form);
		if ($lastDateInput.data('load-value'))
		{
			// FF caches this value on refresh, but since we have the new posts, we need this to reflect the source value
			$lastDateInput.val(Math.max($lastDateInput.val(), $lastDateInput.data('load-value')));
		}

		var submitEnableCallback = XenForo.MultiSubmitFix($form);

		/**
		 * Scrolls QuickReply into view and focuses the editor
		 */
		this.scrollAndFocus = function()
		{
			$(document).scrollTop($form.offset().top);

			var ed = XenForo.getEditorInForm($form);
			if (!ed)
			{
				return false;
			}

			if (ed.$editor)
			{
				ed.focus(true);
			}
			else
			{
				ed.focus();
			}

			return this;
		};

		$form.data('QuickReply', this).bind(
		{
			/**
			 * Fires just before the form would be AJAX submitted,
			 * to detect whether or not the 'more options' button was clicked,
			 * and to abort AJAX submission if it was.
			 *
			 * @param event e
			 * @return
			 */
			AutoValidationBeforeSubmit: function(e)
			{
				if ($(e.clickedSubmitButton).is('input[name="more_options"]'))
				{
					e.preventDefault();
					e.returnValue = true;
				}

				setTimeout(function()
				{
					$('#messageList').find('.messagesSinceReplyingNotice').remove();
				}, 250);
			},

			/**
			 * Fires after the AutoValidator form has successfully validated the AJAX submission
			 *
			 * @param event e
			 */
			AutoValidationComplete: function(e)
			{
				if (e.ajaxData._redirectTarget)
				{
					window.location = e.ajaxData._redirectTarget;
				}

				$('input[name="last_date"]', $form).val(e.ajaxData.lastDate);

				if (submitEnableCallback)
				{
					submitEnableCallback();
				}

				$form.find('input:submit').blur();

				new XenForo.ExtLoader(e.ajaxData, function()
				{
					$(e.ajaxData.templateHtml).each(function()
					{
						if (this.tagName)
						{
							$(this).xfInsert('appendTo', $('#messageList'));
						}
					});
				});

				var $textarea = $('#QuickReply').find('textarea');
				$textarea.val('');
				var ed = $textarea.data('XenForo.BbCodeWysiwygEditor');
				if (ed)
				{
					ed.resetEditor(null, true);
				}

				$form.trigger('QuickReplyComplete');

				return false;
			},

			BbCodeWysiwygEditorAutoSaveComplete: function(e)
			{
				var $messageList = $('#messageList'),
					$notice = $messageList.find('.messagesSinceReplyingNotice');

				if (e.ajaxData.newPostCount && e.ajaxData.templateHtml)
				{
					if ($notice.length)
					{
						$notice.remove();
						$(e.ajaxData.templateHtml).appendTo($messageList).show().xfActivate();
					}
					else
					{
						$(e.ajaxData.templateHtml).xfInsert('appendTo', $messageList);
					}
				}
				else
				{
					$notice.remove();
				}
			}
		});
	};

	// *********************************************************************

	/**
	 * Controls to initialise Quick Reply with a quote
	 *
	 * @param jQuery a.ReplyQuote, a.MultiQuote
	 */
	XenForo.QuickReplyTrigger = function($trigger)
	{
		/**
		 * Activates quick reply and quotes the post to which the trigger belongs
		 *
		 * @param e event
		 *
		 * @return boolean false
		 */
		$trigger.click(function(e)
		{
			console.info('Quick Reply Trigger Click');

			var $form = null,
				xhr = null,
				queryData = {},
				$quoteSelected = null,
				dataPrepare = null;

			if ($trigger.is('.MultiQuote'))
			{
				$form = $($trigger.data('form'));
			}
			else
			{
				$form = $('#QuickReply');
				$form.data('QuickReply').scrollAndFocus();
			}

			// fire event to get additional data
			dataPrepare = new $.Event('QuickReplyDataPrepare');
			dataPrepare.$trigger = $trigger;
			dataPrepare.queryData = queryData;
			$(document).trigger(dataPrepare);

			if (!xhr)
			{
				xhr = XenForo.ajax
				(
					$trigger.data('posturl') || $trigger.attr('href'),
					queryData,
					function(ajaxData, textStatus)
					{
						if (XenForo.hasResponseError(ajaxData))
						{
							return false;
						}

						delete(xhr);

						var ed = XenForo.getEditorInForm($form);
						if (!ed)
						{
							return false;
						}

						if (ed.$editor)
						{
							ed.insertHtml(ajaxData.quoteHtml);

							console.info('QuoteHTML: %s', ajaxData.quoteHtml);

							if (ed.$editor.data('xenForoElastic'))
							{
								ed.$editor.data('xenForoElastic')();
							}
						}
						else
						{
							ed.val(ed.val() + ajaxData.quote);
						}

						if ($trigger.is('.MultiQuote'))
						{
							// reset cookie and checkboxes
							$form.trigger('MultiQuoteComplete');
						}
					}
				);
			}

			return false;
		});
	};

	// *********************************************************************

	XenForo.InlineMessageEditor = function($form)
	{
		new XenForo.MultiSubmitFix($form);

		$form.bind(
		{
			AutoValidationBeforeSubmit: function(e)
			{
				if ($(e.clickedSubmitButton).is('input[name="more_options"]'))
				{
					e.preventDefault();
					e.returnValue = true;
				}
			},
			AutoValidationComplete: function(e)
			{
				var overlay = $form.closest('div.xenOverlay').data('overlay'),
					target = overlay.getTrigger().data('target');

				if (XenForo.hasTemplateHtml(e.ajaxData, 'messagesTemplateHtml') || XenForo.hasTemplateHtml(e.ajaxData))
				{
					e.preventDefault();
					overlay.close().getTrigger().data('XenForo.OverlayTrigger').deCache();

					XenForo.showMessages(e.ajaxData, overlay.getTrigger(), 'instant');
				}
				else
				{
					console.warn('No template HTML!');
				}
			}
		});
	};

	// *********************************************************************

	XenForo.NewMessageLoader = function($ctrl)
	{
		var pending = false;

		$ctrl.click(function(e) {
			e.preventDefault();

			if (pending)
			{
				return;
			}
			pending = true;

			XenForo.ajax(
				$ctrl.data('href') || $ctrl.attr('href'),
				{},
				function(ajaxData) {
					if (XenForo.hasResponseError(ajaxData))
					{
						return false;
					}

					var $form = $('#QuickReply'),
						$messageList = $('#messageList');

					$('input[name="last_date"]', $form).val(ajaxData.lastDate);

					new XenForo.ExtLoader(ajaxData, function()
					{
						$messageList.find('.messagesSinceReplyingNotice').remove();

						$(ajaxData.templateHtml).each(function()
						{
							if (this.tagName)
							{
								$(this).xfInsert('appendTo', $messageList);
							}
						});
					});
				}
			).always(function() { pending = false; });
		});
	};

	// *********************************************************************

	XenForo.MessageLoader = function($ctrl)
	{
		$ctrl.click(function(e)
		{
			e.preventDefault();

			var messageIds = [];

			$($ctrl.data('messageselector')).each(function(i, msg)
			{
				messageIds.push(msg.id);
			});

			if (messageIds.length)
			{
				XenForo.ajax
				(
					$ctrl.attr('href'),
					{
						messageIds: messageIds
					},
					function(ajaxData, textStatus)
					{
						XenForo.showMessages(ajaxData, $ctrl, 'fadeDown');
					}
				);
			}
			else
			{
				console.warn('No messages found to load.'); // debug message, no phrasing
			}
		});
	};

	// *********************************************************************

	XenForo.showMessages = function(ajaxData, $ctrl, method)
	{
		var showMessage = function(selector, templateHtml)
		{
			switch (method)
			{
				case 'instant':
				{
					method =
					{
						show: 'xfShow',
						hide: 'xfHide',
						speed: 0
					};
					break;
				}

				case 'fadeIn':
				{
					method =
					{
						show: 'xfFadeIn',
						hide: 'xfFadeOut',
						speed: XenForo.speed.fast
					};
					break;
				}

				case 'fadeDown':
				default:
				{
					method =
					{
						show: 'xfFadeDown',
						hide: 'xfFadeUp',
						speed: XenForo.speed.normal
					};
				}
			}

			$(selector)[method.hide](method.speed / 2, function()
			{
				$(templateHtml).xfInsert('replaceAll', selector, method.show, method.speed);
			});
		};

		if (XenForo.hasResponseError(ajaxData))
		{
			return false;
		}

		if (XenForo.hasTemplateHtml(ajaxData, 'messagesTemplateHtml'))
		{
			new XenForo.ExtLoader(ajaxData, function()
			{
				$.each(ajaxData.messagesTemplateHtml, showMessage);
			});
		}
		else if (XenForo.hasTemplateHtml(ajaxData))
		{
			// single message
			new XenForo.ExtLoader(ajaxData, function()
			{
				showMessage($ctrl.data('messageselector'), ajaxData.templateHtml);
			});
		}
	};

	// *********************************************************************

	XenForo.PollVoteForm = function($form)
	{
		var replacePollBlock = function(ajaxData)
		{
			new XenForo.ExtLoader(ajaxData, function()
			{
				var $container = $form.closest('.PollContainer');

				$form.xfFadeUp(XenForo.speed.normal, function()
				{
					$form.empty().remove();

					var $html = $(ajaxData.templateHtml);
					if ($html.is('.PollContainer'))
					{
						$html = $html.children();
					}
					else if ($html.find('.PollContainer').length)
					{
						$html = $html.find('.PollContainer').children();
					}

					$html.xfInsert('appendTo', $container);
					$container.xfActivate();
				}, XenForo.speed.normal, 'swing');
			});
		};

		$form.bind('AutoValidationComplete', function(e)
		{
			e.preventDefault();

			if (XenForo.hasTemplateHtml(e.ajaxData))
			{
				replacePollBlock(e.ajaxData);
			}
		});

		$form.on('click', '.PollChangeVote', function(e)
		{
			e.preventDefault();

			XenForo.ajax($(e.target).attr('href'), {}, function(ajaxData)
			{
				if (XenForo.hasTemplateHtml(ajaxData))
				{
					replacePollBlock(ajaxData);
				}
			}, {method: 'get'});
		});

		var maxVotes = $form.data('max-votes') || 0;
		if (maxVotes > 1)
		{
			$form.on('click', '.PollResponse', function(e)
			{
				var $responses = $form.find('.PollResponse'),
					$unselected = $responses.filter(':not(:checked)');

				// saves a separate query to get the selected count
				if ($responses.length - $unselected.length >= maxVotes)
				{
					$unselected.prop('disabled', true);
				}
				else
				{
					$unselected.prop('disabled', false);
				}
			});
		}
	};

	// *********************************************************************

	XenForo.MultiQuote = function($button) { this.__construct($button); };
	XenForo.MultiQuote.prototype =
	{
		__construct: function($button)
		{
			this.$button = $button.click($.context(this, 'prepareOverlay'));
			this.$form = $button.closest('form');
			this.cookieName = $button.data('mq-cookie') || 'MultiQuote';
			this.cookieValue = [];
			this.submitUrl = $button.data('submiturl');
			this.$controls = new jQuery();

			this.getCookieValue();
			this.setButtonState();

			var self = this;

			this.$form.bind('MultiQuoteComplete', $.context(this, 'reset'));
			this.$form.bind('MultiQuoteRemove MultiQuoteAdd', function(e, data)
			{
				if (data && data.messageId)
				{
					self.toggleControl(data.messageId, e.type == 'MultiQuoteAdd');
				}
			});

			$(document).bind('QuickReplyDataPrepare', $.context(this, 'quickReplyDataPrepare'));
		},

		getCookieValue: function()
		{
			var cookieString = $.getCookie(this.cookieName);

			this.cookieValue = (cookieString == null ? [] : cookieString.split(','));
		},

		setButtonState: function()
		{
			this.getCookieValue();

			if (this.cookieValue.length)
			{
				this.$button.show();
			}
			else
			{
				this.$button.hide();
			}
		},

		addControl: function($control)
		{
			$control.click($.context(this, 'clickControl'));

			this.getCookieValue();

			this.setControlState($control, ($.inArray($control.data('messageid') + '', this.cookieValue) >= 0), true);

			this.$controls = this.$controls.add($control);
		},

		setControls: function()
		{
			var MQ = this;

			MQ.getCookieValue();

			this.$controls.each(function()
			{
				MQ.setControlState($(this), ($.inArray($(this).data('messageid') + '', MQ.cookieValue) >= 0));
			});
		},

		setControlState: function($control, isActive, isInitial)
		{
			var text, $button = this.$button, classExpected;
			if (isActive)
			{
				text = $button.data('remove') || '-';
				classExpected = true;
			}
			else
			{
				text = $button.data('add') || '+';
				classExpected = false;
			}

			if (!isInitial || $control.hasClass('active') !== classExpected)
			{
				$control
					.toggleClass('active', isActive)
					.find('span.symbol').text(text);
			}
		},

		clickControl: function(e)
		{
			e.preventDefault();

			var $control,
				newActive,
				quoteHtml,
				messageId,
				alertMessage;

			$control = $(e.target).closest('a.MultiQuoteControl');

			if ($control.is('.QuoteSelected'))
			{
				newActive = true;

				quoteHtml = $('#QuoteSelected').data('quote-html');

				$control.trigger('QuoteSelectedClicked');
			}
			else
			{
				newActive = !$control.is('.active');

				quoteHtml = null;
			}

			messageId = $control.data('messageid');

			this.toggleControl(messageId, newActive, quoteHtml);

			// show an alert to explain what has been done
			if (alertMessage = this.$button.data(newActive ? 'add-message' : 'remove-message'))
			{
				XenForo.alert(alertMessage, '', 2000);
			}
		},

		toggleControl: function(itemId, active, quoteHtml)
		{
			this.getCookieValue();

			var quoteId = null;

			itemId += '';
			if (itemId.indexOf('-') > 0)
			{
				var parts = itemId.split('-');
				itemId = parts[0];
				quoteId = parts[1];
			}

			var $control,
				i = $.inArray(itemId, this.cookieValue);

			$control = this.$controls.filter(function()
			{
				return $(this).data('messageid') == itemId;
			}).first();

			if (active)
			{
				/*
				If control is already active, un-check control if there is no message selection,
				Otherwise, add the new selection to the message
				 */

				if ($control.length)
				{
					this.setControlState($control, true);
				}

				// add quote to localStorage
				if (quoteHtml !== null)
				{
					this.storeSelectedQuote(itemId, quoteHtml);
				}
				else
				{
					this.removeQuotesFromStorage(itemId); // selecting the whole thing instead
				}

				// add to cookie
				if (i < 0)
				{
					this.cookieValue.push(itemId);
				}
			}
			else
			{
				// remove from localStorage
				this.removeQuotesFromStorage(itemId, quoteId);

				// only act as if there's no quote if we removed the last partial one
				if (!this.getStorageForId(itemId))
				{
					if ($control.length)
					{
						this.setControlState($control, false);
					}

					// remove cookie
					if (i >= 0)
					{
						this.cookieValue.splice(i, 1);
					}
				}
			}

			if (this.cookieValue.length > 0)
			{
				$.setCookie(this.cookieName, this.cookieValue.join(','));
			}
			else
			{
				$.deleteCookie(this.cookieName);
			}

			this.setButtonState();
		},

		/**
		 * Store a quote selection
		 *
		 * @param integer message id
		 * @param string quoteHtml
		 */
		storeSelectedQuote: function(id, quoteHtml)
		{
			var storage = this.getStorageObject(),
				count = 0;

			if (!storage[id] || typeof storage[id] != 'object')
			{
				storage[id] = {};
			}

			$.each(storage[id], function(k)
			{
				count = k;
			});

			count = parseInt(count, 10) + 1;

			storage[id][count] = quoteHtml;

			this.saveStorageObject(storage);
		},

		/**
		 * Removes quoted sections from message [id]
		 *
		 * @param id
		 * @param count
		 */
		removeQuotesFromStorage: function(id, count)
		{
			var storage = this.getStorageObject();

			id += '';

			if (!count && id.indexOf('-') > 0)
			{
				var parts = id.split('-');
				id = parts[0];
				count = parts[1];
			}

			if (count)
			{
				delete storage[id][count];
				if ($.isEmptyObject(storage[id]))
				{
					delete storage[id];
				}
			}
			else
			{
				delete storage[id];
			}

			this.saveStorageObject(storage);
		},

		/**
		 * Get the localStorage object that contains quote selections
		 *
		 * @returns object
		 */
		getStorageObject: function()
		{
			if (!window.localStorage)
			{
				return {};
			}

			var storageObj = null;

			try
			{
				storageObj = JSON.parse(localStorage.getItem(this.cookieName));
			}
			catch(e) {}

			if (storageObj === null || typeof storageObj !== 'object')
			{
				storageObj = {};
			}

			return storageObj;
		},

		getStorageForId: function(id)
		{
			var storage = this.getStorageObject();

			if (storage[id] && typeof storage[id] == 'object' && !$.isEmptyObject(storage[id]))
			{
				return storage[id];
			}

			return null;
		},

		getStorageObjectFlat: function()
		{
			var output = {};
			$.each(this.getStorageObject(), function(id, v)
			{
				if (typeof v != 'object')
				{
					return;
				}

				$.each(v, function(count, html)
				{
					output[id + '-' + count] = html;
				});
			});

			return output;
		},

		/**
		 * Store the object that contains quote selections
		 *
		 * @param object storageObj
		 */
		saveStorageObject: function(storageObj)
		{
			if (window.localStorage)
			{
				localStorage.setItem(this.cookieName, JSON.stringify(storageObj));
			}
		},

		/**
		 * Prepares a request for the server to open the multiquote overlay
		 *
		 * @param e
		 */
		prepareOverlay: function(e)
		{
			var quotes = this.getStorageObjectFlat();
			$.each(quotes, function(i, html)
			{
				quotes[i] = XenForo.unparseBbCode(html);
			});

			XenForo.ajax(
				this.$button.data('href'),
				{ quoteSelections: quotes },
				function(ajaxData, textStatus)
				{
					if (XenForo.hasTemplateHtml(ajaxData))
					{
						new XenForo.ExtLoader(ajaxData, function(ajaxData)
						{
							 ajaxData.noCache = true; // otherwise we pick up old data before a refresh...

							 XenForo.createOverlay(null, ajaxData.templateHtml, ajaxData).load();
						});
					}
				}
			);
		},

		/**
		 * Triggered prior to the multiquote overlay being submitted for insertion into the QR editor
		 * Expects (event)e to have (jQuery)$trigger and (obj)queryData properties.
		 *
		 * @param e
		 */
		quickReplyDataPrepare: function(e)
		{
			if (e.$trigger.is('.MultiQuote'))
			{
				var quotes = this.getStorageObjectFlat();

				$.each(quotes, function(i, html)
				{
					quotes[i] = XenForo.unparseBbCode(html);
				});
				e.queryData.quoteSelections = quotes;

				e.queryData.postIds = $(e.$trigger.data('inputs')).map(function()
				{
					return this.value;
				}).get();
			}
		},

		reset: function()
		{
			$.deleteCookie(this.cookieName);
			this.cookieValue = [];
			if (window.localStorage)
			{
				localStorage.removeItem(this.cookieName);
			}

			this.setControls();
			this.setButtonState();
		}
	};

	// *********************************************************************

	/**
	 * Handles adding and removing messages from multi-quote
	 */
	XenForo.MultiQuoteControl = function($link)
	{
		var mqSelector = $link.data('mq-target') || '#MultiQuote',
			mq = $(mqSelector).data('XenForo.MultiQuote');
		if (!mq)
		{
			return;
		}

		mq.addControl($link);
	};

	/**
	 * Handles removal of quotes from the multi-quote overlay
	 */
	XenForo.MultiQuoteRemove = function($link)
	{
		$link.click(function()
		{
			var $container = $link.closest('.MultiQuoteItem'),
				messageId = $container.find('.MultiQuoteId').val(),
				$watcherForm = $($('#MultiQuoteForm').data('form')),
				$overlay = $link.closest('.xenOverlay');

			if (messageId)
			{
				$watcherForm.trigger('MultiQuoteRemove', {messageId: messageId});
			}

			$container.remove();

			if ($overlay.length && !$overlay.find('.MultiQuoteItem').length)
			{
				$overlay.overlay().close();
			}
		});
	};

	// *********************************************************************

	XenForo.Sortable = function($container)
	{
		$container.sortable(
		{
			forcePlaceholderSize: true

		}).bind(
			{
				'sortupdate': function(e) {},
				'dragstart' : function(e)
				{
					console.log('drag start, %o', e.target);
				},
				'dragend' : function(e) { console.log('drag end'); }
			}
		);
	};

	// *********************************************************************
	
	XenForo.SelectQuotable = function($container) { this.__construct($container); };
	XenForo.SelectQuotable.prototype =
	{
		__construct: function($container)
		{
			if (!window.getSelection)
			{
				return;
			}
			if (!$('#QuickReply').length)
			{
				return;
			}

            this.$messageTextContainer = null;

			var self = this,
				isMouseDown = false,
				timeout,
				trigger = function()
				{
					if (!timeout && !self.processing)
					{
						timeout = setTimeout(function()
						{
							timeout = null;
							self._handleSelection();
						}, 100);
					}
				};

			$container.on('mousedown', function()
			{
				isMouseDown = true;
			});
			$container.on('mouseup', function()
			{
				isMouseDown = false;
				trigger();
			});
			$(document).on('selectionchange', function()
			{
				if (!isMouseDown)
				{
					trigger();
				}
			});
			$(document).on('QuickReplyDataPrepare', function(e)
			{
				var $quoteSelected = e.$trigger.closest('#QuoteSelected');
				if ($quoteSelected.length)
				{
					e.queryData.quoteHtml = XenForo.unparseBbCode($quoteSelected.data('quote-html'));

					e.$trigger.trigger('QuoteSelectedClicked');
				}
			});
		},

		buttonClicked: function()
		{
			var s = window.getSelection();
			if (!s.isCollapsed)
			{
				s.collapse(s.getRangeAt(0).commonAncestorContainer, 0);
				this.hideQuoteButton();
			}
		},

		/**
		 * Handle selection events within document
		 */
		_handleSelection: function()
		{
			this.processing = true;

			var selection = window.getSelection();

			if (this._isValidSelection(selection))
			{
				this.showQuoteButton(selection);
			}
			else
			{
				this.hideQuoteButton();
			}

			var self = this;
			setTimeout(function()
			{
				self.processing = false;
			}, 0);
		},

		/**
		 * Check that the specified selection is not collapsed, and is completely contained within $container
		 *
		 * @param selection
		 * @returns {boolean}
		 */
		_isValidSelection: function(selection)
		{
			this.$messageTextContainer = null;
			if (selection.isCollapsed || !selection.rangeCount)
			{
				return false;
			}

			var range = selection.getRangeAt(0);
			this._adjustRange(range);

			if (!$.trim(range.toString()).length)
			{
				if (!range.cloneContents().querySelectorAll('img').length)
				{
					return false;
				}
			}

			var $container = $(range.commonAncestorContainer).closest('.SelectQuoteContainer');
			if (!$container.length)
			{
				return false;
			}

			var $message = $container.closest('.message');
			if (!$message.find('a.MultiQuoteControl, a.ReplyQuote').length)
			{
				return false;
			}

			if ($(range.startContainer).closest('.bbCodeQuote, .NoSelectToQuote').length
		        || $(range.endContainer).closest('.bbCodeQuote, .NoSelectToQuote').length)
	        {
		        return false;
	        }

			this.$messageTextContainer = $container;
			return true;
		},

		_adjustRange: function(range)
		{
			var changed = false,
				isQuote = false;

			if (range.endOffset == 0)
			{
				var $end = $(range.endContainer);
				if (range.endContainer.nodeType == 3 && !range.endContainer.previousSibling)
				{
					// text node with nothing before it, move up
					$end = $end.parent();
				}
				isQuote = $end.is('.quote, .attribution, .bbCodeQuote');
			}

			if (isQuote)
			{
				var $quote = $(range.endContainer).closest('.bbCodeQuote');
				if ($quote.length)
				{
					range.setEndBefore($quote[0]);
					changed = true;
				}
			}

			if (changed)
			{
				var sel = window.getSelection();
				sel.removeAllRanges();
				sel.addRange(range);
			}
		},

		/**
		 * Show the quote button, following a selection, if necessary
		 *
		 * @param selection
		 */
		showQuoteButton: function(selection)
		{
			var id = this.$messageTextContainer.closest('[id]').attr('id');
			if (this.$button === undefined || this.$button.data('id') !== id)
			{
				this.hideQuoteButton();
				this.createButton();
				this.$button.data('id', id);
			}
			this.$button.data('quote-html', this.getSelectionHtml(selection));

			var width = this.$button.width();

			var offset = this.getButtonPositionMarker(selection),
				left = this.$messageTextContainer ? this.$messageTextContainer.offset().left : 0;
			if (offset.left - width < left)
			{
				this.$button[XenForo.isRTL() ? 'addClass' : 'removeClass']('flipped').css('left', offset.left - 16);
			}
			else
			{
				this.$button[XenForo.isRTL() ? 'removeClass' : 'addClass']('flipped').css('left',  offset.left - width - 5);
			}

			this.$button
				.css({
					position: 'absolute',
					top: offset.top + 6
				})
				.show();
		},

		getButtonPositionMarker: function(selection)
		{
			// get absolute position of end of selection - or maybe focusNode
			// and position the quote button immediately next to the highlight
			var $el, range, offset, height, bounds;

			$el = $('<span />');
			$el.text($.browser.opera ? 'x' : '\u200B');

			range = selection.getRangeAt(0).cloneRange();
			bounds = range.getBoundingClientRect ? range.getBoundingClientRect() : null;
			range.collapse(false);
			range.insertNode($el[0]);

			var changed,
				moves = 0;

			do
			{
				changed = false;
				moves++;

				if ($el[0].parentNode && $el[0].parentNode.className == 'messageTextEndMarker')
				{
					// highlight after the marker to ensure that triple click works
					$el.insertBefore($el[0].parentNode);

					changed = true;
				}
				if ($el[0].previousSibling && $el[0].previousSibling.nodeType == 3 && $.trim($el[0].previousSibling.textContent).length == 0)
				{
					// highlight after an empty text block
					$el.insertBefore($el[0].previousSibling);

					changed = true;
				}
				if ($el[0].parentNode && $el[0].parentNode.tagName == 'LI' && !$el[0].previousSibling)
				{
					// highlight at the beginning of a list item, move to previous item if possible
					var li = $el[0].parentNode;
					if ($(li).prev('li').length)
					{
						// move to inside the last li
						$el.appendTo($(li).prev('li'));

						changed = true;
					}
					else if (li.parentNode)
					{
						// first list item, move before the list
						$el.insertBefore(li.parentNode);

						changed = true;
					}
				}
				if ($el[0].parentNode && !$el[0].previousSibling && $.inArray($el[0].parentNode.tagName, ['DIV', 'BLOCKQUOTE', 'PRE']) != -1)
				{
					$el.insertBefore($el[0].parentNode);

					changed = true;
				}
				if ($el[0].previousSibling && $.inArray($el[0].previousSibling.tagName, ['OL', 'UL']) != -1)
				{
					// immediately after a list, position at end of last LI
					$($el[0].previousSibling).find('li:last').append($el);

					changed = true;
				}
				if ($el[0].previousSibling && $.inArray($el[0].previousSibling.tagName, ['DIV', 'BLOCKQUOTE', 'PRE']) != -1)
				{
					// highlight immediately after a block causes weird positioning
					$el.appendTo($el[0].previousSibling);

					changed = true;
				}
				if ($el[0].previousSibling && $el[0].previousSibling.tagName == 'BR')
				{
					// highlight immediately after a line break causes weird positioning
					$el.insertBefore($el[0].previousSibling);

					changed = true;
				}
			}
			while (changed && moves < 5);

			offset = $el.offset();
			height = $el.height();

			// if we're in a scrollable element, find the right edge of that element and don't position beyond it
			$el.parentsUntil('body').each(function() {
				var $parent = $(this), left, right;

				switch ($parent.css('overflow-x'))
				{
					case 'hidden':
					case 'scroll':
					case 'auto':
						left = $parent.offset().left;
						right = left + $parent.outerWidth();
						if (offset.left < left)
						{
							offset.left = left;
						}
						if (right < offset.left)
						{
							offset.left = right;
						}
				}
			});

			var $parent = $el.parent();
			$el.remove();

			if (!$.browser.msie) // IE loses the selection from this
			{
				$parent[0].normalize(); // recombine text nodes for accurate text rendering
			}

			if (bounds && !XenForo.isRTL())
			{
				if (offset.left - bounds.left > 32)
				{
					offset.left -= 16;
				}
			}

			offset.top += height;

			return offset;
		},

		/**
		 * Create the 'Quote Selected' button if necessary
		 */
		createButton: function()
		{
			// TODO: clone the ReplyQuote control and use that...
            if (!this.$messageTextContainer.length)
            {
                return false;
            }

			var $message = this.$messageTextContainer.closest('.message');

			this.$button = $('<div id="QuoteSelected" class="xenTooltip"></div>')
				.click($.context(this, 'hideQuoteButton'))
				.append('<span class="arrow"></span>');

			var $mqButton = $message.find('a.MultiQuoteControl').first().clone();
			if ($mqButton.length)
			{
				$mqButton
					.addClass('QuoteSelected')
					.attr('title', '')
					.on('QuoteSelectedClicked', $.context(this, 'buttonClicked'));
				$mqButton.find('span.symbol').text($('.MultiQuoteWatcher').data('add')); // always use Quote +
				new XenForo.MultiQuoteControl($mqButton);

				this.$button.append($mqButton);
				this.$button.append(document.createTextNode(" | "));
			}

			var $quoteButton = $message.find('a.ReplyQuote').clone();
			$quoteButton
				.addClass('QuoteSelected')
				.attr('title', '')
				.on('QuoteSelectedClicked', $.context(this, 'buttonClicked'));
			new XenForo.QuickReplyTrigger($quoteButton);

			this.$button.append($quoteButton);

			$(document.body).append(this.$button);

			var self = this, windowWidth = $(window).width();
			$(window).on('resize.SelectQuotable', function(e)
			{
				var newWidth = $(window).width();
				if (newWidth != windowWidth)
				{
					windowWidth = newWidth;
					self._handleSelection();
				}
			});
			$(document).on('XFOverlay.SelectQuotable', function(e)
			{
				self.hideQuoteButton();
				window.getSelection().collapseToEnd();
			});
		},


		/**
		 * Hide the quote button when it's no longer required
		 */
		hideQuoteButton: function()
		{
			if (this.$button !== undefined)
			{
				this.$button.remove();
				delete(this.$button);
			}

			$(window).off('resize.SelectQuotable');
			$(document).off('XFOverlay.SelectQuotable');
		},

		/**
		 * Returns the entirety of the HTML enclosed by the specified selection
		 *
		 * @param selection
		 * @returns {string}
		 */
		getSelectionHtml: function(selection)
		{
			var el = document.createElement('div'),
				i, len;
			
			for (i = 0, len = selection.rangeCount; i < len; i++)
			{
				el.appendChild(selection.getRangeAt(i).cloneContents());
			}

			return this.prepareSelectionHtml(el.innerHTML);
		},

		prepareSelectionHtml: function(html)
		{
			return html;
		}
	};

	// *********************************************************************

	/**
	 * Attempts to convert various HTML-BB codes back into BB code
	 *
	 * @param string html
	 *
	 * @returns string
	 */
	XenForo.unparseBbCode = function(html)
	{
		var $div = $(document.createElement('div'));

		$div.html(html);
		console.log($div.find('.bbCodeQuote').length);

		// get rid of anything with this class
		$div.find('.NoSelectToQuote').each(function()
		{
			$(this).remove();
		});

		// handle b, i, u
		$.each(['B', 'I', 'U'], function(i, tagName)
		{
			$div.find(tagName).each(function()
			{
				$(this).replaceWith('[' + tagName + ']' + $(this).html() + '[/' + tagName + ']');
			});
		});

		// handle quote tags as best we can
		$div.find('.bbCodeQuote').each(function()
		{
			var $this = $(this),
				$quote = $this.find('.quote');
			if ($quote.length)
			{
				$this.replaceWith('<div>[QUOTE]' + $quote.html() + '[/QUOTE]</div>');
			}
			else
			{
				$quote.find('.quoteExpand').remove();
			}
		});

		// now for PHP, CODE and HTML
		$div.find('.bbCodeCode, .bbCodeHtml, .bbCodePHP').each(function()
		{
			var $this = $(this),
				type = $(this).find('div.type').first().text(),
				findTag = 'pre';

			if (type !== '')
			{
				type = type.replace(/^(.+):$/, '$1');
			}

			if ($this.is('.bbCodePHP'))
			{
				findTag = 'code';
			}

			$this.replaceWith($this.find(findTag).first().attr('data-type', type));
		});

		// now alignment tags
		$div.find('div[style*="text-align"]').each(function()
		{
			var align = $(this).css('text-align').toUpperCase();

			$(this).replaceWith('[' + align + ']' + $(this).html() + '[/' + align + ']');
		});

		// and finally, spoilers...
		$div.find('.bbCodeSpoilerContainer').each(function()
		{
			var $button, target, spoilerTitle, spoilerText;

			// find the button and the target
			$button = $(this).find('.bbCodeSpoilerButton');
			if ($button.length)
			{
				target = $button.data('target');
				if (target)
				{
					spoilerText = $(this).find(target).html();

					$spoilerTitle = $(this).find('.SpoilerTitle');
					if ($spoilerTitle.length)
					{
						spoilerTitle = '="' + $spoilerTitle.text() + '"';
					}
					else
					{
						spoilerTitle = '';
					}

					$(this).replaceWith('[SPOILER' + spoilerTitle + ']' + spoilerText + '[/SPOILER]');
				}
			}
		});

		console.info('HTML to be sent: %s', $div.html());

		return $div.html();
	};

	// *********************************************************************

	XenForo.register('#QuickReply', 'XenForo.QuickReply');

	XenForo.register('a.ReplyQuote, a.MultiQuote, a.QuoteSelected', 'XenForo.QuickReplyTrigger');

	XenForo.register('form.InlineMessageEditor', 'XenForo.InlineMessageEditor');

	XenForo.register('a.MessageLoader', 'XenForo.MessageLoader');
	XenForo.register('a.NewMessageLoader', 'XenForo.NewMessageLoader');

	XenForo.register('form.PollVoteForm', 'XenForo.PollVoteForm');

	XenForo.register('.MultiQuoteWatcher', 'XenForo.MultiQuote');
	XenForo.register('a.MultiQuoteControl', 'XenForo.MultiQuoteControl');
	XenForo.register('a.MultiQuoteRemove', 'XenForo.MultiQuoteRemove');

	XenForo.register('.Sortable', 'XenForo.Sortable');

	XenForo.register('.SelectQuotable', 'XenForo.SelectQuotable');

}
(jQuery, this, document);