Seditio Source
Root |
./othercms/xenForo 2.2.8/js/xf/message.js
!function($, window, document, _undefined)
{
"use strict";

XF.Message = XF.Message || {};

XF.Message.insertMessages = function(dataHtml, $container, ascending, onInsert)
{
XF.setupHtmlInsert(dataHtml, function($html, container, onComplete)
{
var $noMessages = $container.find('.js-replyNoMessages');

if ($noMessages.length)
{
$noMessages.xfFadeUp();
}

$html.each(function()
{
if (!this.tagName)
{
return;
}

XF.Message.insertMessage($(this), $container, ascending);
});

if (onInsert)
{
onInsert($html);
}
});
};

XF.Message.insertMessage = function($message, $container, ascending)
{
var $firstChild = $container.children().first();

$message.hide();

if ($firstChild.is('form') && !ascending)
{
$message.insertAfter($firstChild);
}
else if (!ascending)
{
$container.prepend($message);
}
else
{
$container.append($message);
}

$message.xfFadeDown();

XF.activate($message);
};

// ################################## MESSAGE LOADER HANDLER ###########################################

XF.MessageLoaderClick = XF.Event.newHandler({
eventNameSpace: 'XFMessageLoaderClick',
options: {
href: null,
messagesContainer: '< .js-replyNewMessageContainer',
selfContainer: '.message',
ascending: true
},

loading: false,

init: function()
{
if (!this.options.href)
{
this.options.href = this.$target.attr('href');
if (!this.options.href)
{
console.error('Must be initialized with a data-href or href attribute.');
}
}
},

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

if (this.loading)
{
return;
}

var self = this;

XF.ajax('GET', this.options.href, {}, XF.proxy(this, 'loaded'))
.always(function() { self.loading = false; });
},

loaded: function(data)
{
if (!data.html)
{
return;
}

var $container = XF.findRelativeIf(this.options.messagesContainer, this.$target);
XF.Message.insertMessages(data.html, $container, this.options.ascending);

var $selfMessage = this.$target.closest(this.options.selfContainer);
$selfMessage.xfFadeUp(null, function()
{
$selfMessage.remove();
});

if (data.lastDate)
{
$('.js-quickReply input[name="last_date"]').val(data.lastDate);
}
}
});

// ################################## QUICK EDIT HANDLER ###########################################

XF.QuickEditClick = XF.Event.newHandler({
eventNameSpace: 'XFQuickEdit',

options: {
editorTarget: null,
editContainer: '.js-editContainer',
href: null,
noInlineMod: 0
},

$editorTarget: null,
$editForm: null,

href: null,
loading: false,

init: function()
{
var edTarget = this.options.editorTarget;

if (!edTarget)
{
console.error('No quick edit editorTarget specified');
return;
}

this.$editorTarget = XF.findRelativeIf(edTarget, this.$target);
if (!this.$editorTarget.length)
{
console.error('No quick edit target found');
return;
}

this.href = this.options.href || this.$target.attr('href');
if (!this.href)
{
console.error('No edit URL specified.');
}
},

click: function(e)
{
if (!this.$editorTarget || !this.href)
{
return;
}

e.preventDefault();

if (this.loading)
{
return;
}

this.loading = true;

var data = {};
if (this.options.noInlineMod)
{
data['_xfNoInlineMod'] = true;
}

XF.ajax('GET', this.href, data, XF.proxy(this, 'handleAjax'), { skipDefaultSuccessError: true });
},

handleAjax: function(data)
{
var $editorTarget = this.$editorTarget,
self = this;

if (data.errors)
{
this.loading = false;
XF.alert(data.errors);
return;
}

XF.setupHtmlInsert(data.html, function($html, container)
{
$html.hide().insertAfter($editorTarget);
XF.activate($html);
self.$editForm = $html;

$html.on('ajax-submit:response', XF.proxy(self, 'editSubmit'));
$html.find('.js-cancelButton').on('click', XF.proxy(self, 'cancelClick'));

var $hidden = $html.find('input[type=hidden]').first();
$hidden.after('<input type="hidden" name="_xfInlineEdit" value="1" />');

$editorTarget.xfFadeUp(null, function()
{
$editorTarget.parent().addClass('is-editing');

$html.xfFadeDown(XF.config.speed.normal, function()
{
$html.trigger('quick-edit:shown');

var $editContainer = $html.find(self.options.editContainer);
if ($editContainer.length && !XF.isElementVisible($editContainer))
{
$editContainer.get(0).scrollIntoView(true);
}

self.loading = false;
});
});

$html.trigger('quick-edit:show');
});
},

editSubmit: function(e, data)
{
if (data.errors || data.exception)
{
return;
}

e.preventDefault();

if (data.message)
{
XF.flashMessage(data.message, 3000);
}

var $editorTarget = this.$editorTarget,
self = this;

XF.setupHtmlInsert(data.html, function($html, container, onComplete)
{
var target = self.options.editorTarget;
target = target.replace(/<|\|/g, '').replace(/#[a-zA-Z0-9_-]+\s*/, '');

var $message = $html.find(target);

$message.hide();
$editorTarget.replaceWith($message);
self.$editorTarget = $message;
XF.activate($message);

self.stopEditing(false, function()
{
$message.xfFadeDown();

self.$editForm.trigger('quickedit:editcomplete', data);
});
});
},

cancelClick: function(e)
{
this.stopEditing(true);
},

stopEditing: function(showMessage, onComplete)
{
var $editorTarget = this.$editorTarget,
$editForm = this.$editForm,
self = this;

var finish = function()
{
$editorTarget.parent().removeClass('is-editing');

if (showMessage)
{
$editorTarget.xfFadeDown();
}

if (onComplete)
{
onComplete();
}

$editForm.remove();
self.$editForm = null;
};

if ($editForm)
{
$editForm.xfFadeUp(null, finish);
}
else
{
finish();
}
}
});

// ################################## QUOTE HANDLER ###########################################

XF.QuoteClick = XF.Event.newHandler({
eventNameSpace: 'XFQuoteClick',
options: {
quoteHref: null,
editor: '.js-quickReply .js-editor'
},

init: function()
{
if (!this.options.quoteHref)
{
console.error('Must be initialized with a data-quote-href attribute.');
}
},

click: function(e)
{
var $editor = XF.findRelativeIf(this.options.editor, this.$target);

$editor.closest('form').trigger('preview:hide', [this]);

var $qr = $editor.closest('.js-quickReply');

if (!$editor.length || !$qr.length)
{
return;
}

e.preventDefault();

var href = this.options.quoteHref,
$selectToQuote = $(e.target).parents('.tooltip--selectToQuote'),
quoteHtml = XF.unparseBbCode($selectToQuote.data('quote-html'));

XF.ajax('POST', href, { quoteHtml: quoteHtml }, XF.proxy(this, 'handleAjax'), {skipDefaultSuccess: true});

$(e.target).trigger('s2q:click');

window.scrollTo({ top: $qr.offset().top - XF.getStickyHeaderOffset() });

XF.focusEditor($editor);
},

handleAjax: function(data)
{
var $editor = XF.findRelativeIf(this.options.editor, this.$target);

XF.insertIntoEditor($editor, data.quoteHtml, data.quote);
}
});

// ################################## SOLUTION EDIT HANDLER ###########################################

XF.SolutionEditClick = XF.extend(XF.SwitchClick, {
applyResponseActions: function(data)
{
this.applyActionsTo(this.$target, data);
},

applyActionsTo: function($target, data)
{
var match,
replaceId;

if (data.switchKey)
{
match = data.switchKey.match(/^replaced:(\d+)$/);
if (match)
{
replaceId = parseInt(match[1], 10);
data.switchKey = 'marked'; // mark this post as the solution
}
}

XF.handleSwitchResponse($target, data, this.options.redirect);

// TODO: the selectors below this could do with being more flexible

var $message = $target.closest('.message');

if (data.switchKey == 'marked')
{
$message.addClass('message--solution');
}
else if (data.switchKey == 'removed')
{
$message.removeClass('message--solution');

var originalTopPos = $message.offset().top,
originalScrollTop = $(document).scrollTop();

$('#js-solutionHighlightBlock').remove();

var diff = $message.offset().top - originalTopPos;
if (diff)
{
$(document).scrollTop(originalScrollTop + diff);
}
}

if (replaceId)
{
var $replacedControl = $('#js-post-' + replaceId + ' .js-solutionControl');
if ($replacedControl.length)
{
this.applyActionsTo($replacedControl, { switchKey: 'removed' });
}
}
}
});

// ################################## MULTI QUOTE HANDLER ###########################################

XF.MultiQuote = XF.Element.newHandler({
options: {
href: '',
messageSelector: '',
addMessage: '',
removeMessage: '',
storageKey: ''
},

mqStorage: null,
mqOverlay: null,

removing: false,
quoting: false,

init: function()
{
this.initButton();
this.initControls();

var self = this;

XF.CrossTab.on('mqChange', function(data)
{
if (data.storageKey !== self.options.storageKey)
{
return;
}

var messageId = data.messageId;

switch (data.action)
{
case 'added':
self.selectMqControl(messageId);
break;

case 'removed':
self.deselectMqControl(messageId);
break;

case 'refresh':
// the code below will handle this
break;
}

self.refreshMqData();
self.updateButtonState();
});
},

initButton: function()
{
this.mqStorage = XF.LocalStorage.getJson(this.options.storageKey);
if (this.hasQuotesStored())
{
this.$target.show();
}

this.$target.on('click', XF.proxy(this, 'buttonClick'));
},

buttonClick: function(e)
{
e.preventDefault();
if (!this.options.href)
{
console.error('Multi-quote button must have a data-href attribute set to display selected quotes');
return false;
}

XF.ajax('post', this.options.href, {
quotes: XF.LocalStorage.get(this.options.storageKey)
}, XF.proxy(this, 'loadOverlay'));
},

loadOverlay: function(data)
{
if (data.html)
{
var self = this;
XF.setupHtmlInsert(data.html, function ($html, container)
{
var $overlay = XF.getOverlayHtml({
html: $html,
title: container.h1 || container.title
});
$overlay.find('.js-removeMessage').on('click', XF.proxy(self, 'removeMessage'));
$overlay.find('.js-quoteMessages').on('click', XF.proxy(self, 'quoteMessages'));
self.mqOverlay = XF.showOverlay($overlay);
});
}
},

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

if (this.removing)
{
return;
}

this.removing = true;

var $item = $(e.target).closest('.nestable-item'),
messageId = $item.data('id'),
overlay = this.mqOverlay;

this.removeFromMultiQuote(messageId);

$item.xfFadeUp(XF.config.speed.fast, function()
{
$item.remove();
});

if (!this.hasQuotesStored())
{
overlay.hide();
}

this.removing = false;
},

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

if (this.quoting)
{
return;
}

this.quoting = true;

var overlay = this.mqOverlay,
$overlay = overlay.getOverlay(),
toInsert = $.parseJSON($overlay.find('input[name="message_ids"]').val()),
multiQuotes = this.mqStorage,
t = this;

for (var i in toInsert)
{
if (!toInsert.hasOwnProperty(i) || !toInsert[i].hasOwnProperty('id'))
{
continue;
}

var id = toInsert[i]['id'],
parts = id.split('-'),
messageId = parts[0],
key = parts[1];

if (!this.isValidQuote(multiQuotes[messageId], key))
{
continue;
}

var value = multiQuotes[messageId][key];
if (value !== true)
{
value = XF.unparseBbCode(value);
}
toInsert[i]['value'] = value;
}

overlay.hide();

XF.ajax('post', this.options.href, {
insert: toInsert,
quotes: XF.LocalStorage.get(this.options.storageKey)
}, XF.proxy(this, 'insertMessages')).always(function()
{
t.quoting = false;
});
},

isValidQuote: function(quote, key)
{
if (quote != undefined)
{
if (quote.hasOwnProperty(key))
{
if (quote[key] === true || typeof quote[key] == 'string')
{
return true;
}
}
}

return false;
},

insertMessages: function(data)
{
var $editor = XF.findRelativeIf('< form | .js-editor', this.$target).first();
if (!$editor.length)
{
$editor = $('.js-editor').parent();
}

$.each(data, function(i, quoteObj)
{
if (!quoteObj.hasOwnProperty('quote') || !quoteObj.hasOwnProperty('quoteHtml'))
{
return true;
}

if (i > 0)
{
// we want an extra line break between inserted quotes
quoteObj.quoteHtml = '<p></p>' + quoteObj.quoteHtml;
quoteObj.quote = "\n" + quoteObj.quote;
}

XF.insertIntoEditor($editor, quoteObj.quoteHtml, quoteObj.quote);
});

for (var messageId in this.mqStorage)
{
this.removeFromMultiQuote(messageId);
}
},

initControls: function()
{
var messages = '.tooltip--selectToQuote, ' + this.options.messageSelector,
$controls = $(messages).find('.js-multiQuote');

$(document).on('click', messages, XF.proxy(this, 'controlClick'));

var self = this;
$controls.each(function()
{
var $control = $(this),
messageId = $control.data('messageId');

if (self.mqStorage.hasOwnProperty(messageId))
{
$control.addClass('is-selected');
$control.data('mqAction', 'remove');
}
});
},

controlClick: function(e)
{
if (!$(e.target).is('.js-multiQuote'))
{
return;
}

e.preventDefault();

var $target = $(e.target),
action = $target.data('mqAction'),
messageId = $target.data('messageId');

switch (action)
{
case 'add':
this.addToMultiQuote(messageId);
XF.flashMessage(this.options.addMessage, 3000);
break;

case 'remove':
this.removeFromMultiQuote(messageId);
XF.flashMessage(this.options.removeMessage, 3000);
break;
}

$(e.target).trigger('s2q:click');
},

addToMultiQuote: function(messageId)
{
var $mqControl = $('.js-multiQuote[data-message-id="' + messageId + '"]'),
$selectToQuote = $mqControl.parents('.tooltip--selectToQuote'),
quoteHtml = XF.unparseBbCode($selectToQuote.data('quote-html'));

this.refreshMqData();

if (!this.hasQuotesStored())
{
this.mqStorage = {};
this.mqStorage[messageId] = [];
}
else
{
if (!this.mqStorage[messageId])
{
this.mqStorage[messageId] = [];
}
}

if ($selectToQuote.length)
{
this.mqStorage[messageId].push(quoteHtml);
}
else
{
this.mqStorage[messageId].push(true); // true == quoting the full message
}
this.updateMultiQuote();

this.selectMqControl(messageId);
this.triggerCrossTabEvent('added', messageId);
},

removeFromMultiQuote: function(messageId)
{
var quoteInfo = String(messageId).match(/^(\d+)-(\d+)$/);

this.refreshMqData();

if (quoteInfo)
{
messageId = quoteInfo[1];

delete this.mqStorage[messageId][quoteInfo[2]];

if (!this.getQuoteStoreCount(this.mqStorage[messageId]))
{
delete this.mqStorage[messageId];
}
}
else
{
delete this.mqStorage[messageId];
}

this.updateMultiQuote();

if (!this.mqStorage[messageId])
{
this.deselectMqControl(messageId);
this.triggerCrossTabEvent('removed', messageId);
}
},

selectMqControl: function(messageId)
{
var $mqControl = $('.js-multiQuote[data-message-id="' + messageId + '"]');

if ($mqControl.length)
{
$mqControl.addClass('is-selected');
$mqControl.data('mqAction', 'remove');
}
},

deselectMqControl: function(messageId)
{
var $mqControl = $('.js-multiQuote[data-message-id="' + messageId + '"]');

if ($mqControl.length)
{
$mqControl.removeClass('is-selected');
$mqControl.data('mqAction', 'add');
}
},

getQuoteStoreCount: function(quoteStore)
{
var length = 0;

for (var i in quoteStore)
{
if (quoteStore.hasOwnProperty(i))
{
if (quoteStore[i] == true || typeof quoteStore[i] == 'string')
{
length ++;
}
}
}

return length;
},

updateMultiQuote: function()
{
XF.LocalStorage.setJson(this.options.storageKey, this.mqStorage, true);
this.updateButtonState();
},

updateButtonState: function()
{
if (!this.hasQuotesStored())
{
this.$target.hide();
}
else
{
this.$target.show();
}
},

refreshMqData: function()
{
this.mqStorage = XF.LocalStorage.getJson(this.options.storageKey);
},

hasQuotesStored: function()
{
return this.mqStorage && !$.isEmptyObject(this.mqStorage);
},

triggerCrossTabEvent: function(action, messageId, data)
{
data = data || {};
data.storageKey = this.options.storageKey;
data.action = action;
data.messageId = messageId;

XF.CrossTab.trigger('mqChange', data);
}
});

// ################################## SELECT TO QUOTE HANDLER ###########################################

XF.SelectToQuote = XF.Element.newHandler({
options: {
messageSelector: ''
},

$quickReply: null,

timeout: null,
processing: false,
triggerEvent: null,
isMouseDown: false,
tooltip: null,
tooltipId: null,

init: function()
{
if (!window.getSelection)
{
return;
}

if (!this.options.messageSelector)
{
console.error('No messageSelector');
return;
}

this.$quickReply = $('.js-quickReply .js-editor').parent();
if (!this.$quickReply.length)
{
return;
}

this.$target.on('mousedown pointerdown', XF.proxy(this, 'mouseDown'));
this.$target.on('mouseup pointerup', XF.proxy(this, 'mouseUp'));
$(document).on('selectionchange', XF.proxy(this, 'selectionChange'));
},

mouseDown: function(e)
{
// store event so we can detect later if it originates from touch
this.triggerEvent = e;

if (e.type == 'mousedown')
{
this.isMouseDown = true;
}
},

mouseUp: function()
{
this.isMouseDown = false;
this.trigger();
},

selectionChange: function()
{
if (!this.isMouseDown)
{
this.trigger();
}
},

trigger: function()
{
if (!this.timeout && !this.processing)
{
this.timeout = setTimeout(XF.proxy(this, 'handleSelection'), 100);
}
},

handleSelection: function()
{
this.processing = true;
this.timeout = null;

var selection = window.getSelection(),
$selectionContainer = this.getValidSelectionContainer(selection);

if ($selectionContainer)
{
this.showQuoteButton($selectionContainer, selection);
}
else
{
this.hideQuoteButton();
}

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

getValidSelectionContainer: function(selection)
{
if (selection.isCollapsed || !selection.rangeCount)
{
return null;
}

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

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

var $container = $(range.commonAncestorContainer).closest('.js-selectToQuote');
if (!$container.length)
{
return null;
}

if (!$container.closest(this.$target).length)
{
return null;
}

var $message = $container.closest(this.options.messageSelector);
if (!$message.find('.actionBar-action[data-xf-click="quote"]').length)
{
return null;
}

if ($(range.startContainer).closest('.bbCodeBlock--quote, .js-noSelectToQuote').length
|| $(range.endContainer).closest('.bbCodeBlock--quote, .js-noSelectToQuote').length)
{
return null;
}

return $container;
},

adjustRange: function(range)
{
var changed = false,
isQuote = false,
end = range.endContainer,
$end = $(end);

if (range.endOffset == 0)
{
if (end.nodeType == 3 && !end.previousSibling)
{
// text node with nothing before it, move up
$end = $end.parent();
}
isQuote = ($end.closest('.bbCodeBlock--quote').length > 0);
}

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

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

showQuoteButton: function($selectionContainer, selection)
{
var id = $selectionContainer.xfUniqueId();
if (!this.tooltip || this.tooltipId !== id)
{
this.hideQuoteButton();
this.createButton($selectionContainer, id);
}

var $tooltip = this.tooltip.getTooltip();
$tooltip.data('quote-html', this.getSelectionHtml(selection));

var offset = this.getButtonPositionMarker(selection),
touchTriggered = false;

if (this.triggerEvent)
{
touchTriggered = XF.isEventTouchTriggered(this.triggerEvent);
}

// if touch triggered then the browser may have large selection handles which might
// obscure our tooltip - if we detect touch then try to offset that
if (touchTriggered)
{
offset.top += 10;
}

this.tooltip.setPositioner([offset.left, offset.top]);

if (this.tooltip.isShown())
{
this.tooltip.reposition();
}
else
{
this.tooltip.show();
}

$tooltip.addClass('tooltip--selectToQuote');
},

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 />').text('\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 == 'js-selectToQuoteEnd')
{
// 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 (!XF.browser.msie) // IE loses the selection from this
{
$parent[0].normalize(); // recombine text nodes for accurate text rendering
}

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

offset.top += height;

return offset;
},

createButton: function($selectionContainer, id)
{
var $message = $selectionContainer.closest(this.options.messageSelector),
$tooltip = $('<span />');

var $mqButton = $message.find('.actionBar-action.js-multiQuote').clone();
if ($mqButton.length)
{
$mqButton
.attr('title', '')
.removeClass('is-selected')
.data('mqAction', 'add')
.css({
marginLeft: 0,
background: 'transparent'
})
.on('s2q:click', XF.proxy(this, 'buttonClicked'));

$tooltip.append($mqButton);
$tooltip.append(document.createTextNode(' | '));
}

var $quoteButton = $message.find('.actionBar-action[data-xf-click="quote"]')
.attr('title', '')
.clone()
.css({
marginLeft: 0
})
.on('s2q:click', XF.proxy(this, 'buttonClicked'));

$tooltip.append($quoteButton);

this.tooltip = new XF.TooltipElement($tooltip, {
html: true,
placement: 'bottom'
});
this.tooltipId = id;
},

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

hideQuoteButton: function()
{
var tooltip = this.tooltip;

if (tooltip)
{
tooltip.destroy();
this.tooltip = null;
}
},

getSelectionHtml: function(selection)
{
var el = document.createElement('div'),
i, len, contents;

for (i = 0, len = selection.rangeCount; i < len; i++)
{
contents = selection.getRangeAt(i).cloneContents();

this.groupIncompleteTableSegment(contents, 'td, th', 'tr', 'TR');
this.groupIncompleteTableSegment(contents, 'tr', 'table, tbody, thead, tfoot', 'TABLE');
this.groupIncompleteTableSegment(contents, 'tbody, thead, tfoot', 'table', 'TABLE');

el.appendChild(contents);
}

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

groupIncompleteTableSegment: function(fragment, segmentMatch, parentMatch, containerNodeName)
{
var matches = fragment.querySelectorAll(segmentMatch),
i, node, entries, container, j;

for (i = 0; i < matches.length; i++)
{
node = matches[i];

if (!$(node.parentNode).is(parentMatch))
{
entries = [node];
while (node = node.nextSibling)
{
if ($(node).is(segmentMatch))
{
entries.push(node);
}
else
{
break;
}
}

container = document.createElement(containerNodeName);
entries[0].parentNode.insertBefore(container, entries[0]);

for (j = 0; j < entries.length; j++)
{
container.appendChild(entries[j]);
}
}
}
},

prepareSelectionHtml: function(html)
{
return XF.adjustHtmlForRte(html);
}
});

// ################################## QUICK REPLY HANDLER ###########################################

XF.QuickReply = XF.Element.newHandler({

options: {
messageContainer: '',
ascending: true,
submitHide: null
},

init: function()
{
this.$target.on('ajax-submit:before', XF.proxy(this, 'beforeSubmit'));
this.$target.on('ajax-submit:response', XF.proxy(this, 'afterSubmit'));
this.$target.on('draft:complete', XF.proxy(this, 'onDraft'));
},

beforeSubmit: function(e, config)
{
var $button = config.submitButton;

if ($button && $button.attr('name') == 'more_options')
{
e.preventDefault();
}
},

afterSubmit: function(e, data)
{
if (data.errors || data.exception)
{
return;
}

e.preventDefault();

if (data.redirect)
{
XF.redirect(data.redirect);
return;
}

this.$target.find('input[name="last_date"]').val(data.lastDate);
this.getMessagesContainer().find('.js-newMessagesIndicator').remove();

this.insertMessages(data.html);

XF.clearEditorContent(this.$target);

var editor = XF.getEditorInContainer(this.$target);
if (editor && XF.Editor && editor instanceof XF.Editor)
{
editor.blur();
}

var $target = this.$target,
options = this.options;

$target.trigger('preview:hide', [this]);
$target.trigger('attachment-manager:reset');

if (options.submitHide)
{
var $submitHide = XF.findRelativeIf(options.submitHide, this.$target);
$submitHide.hide();
}
},

insertMessages: function(dataHtml)
{
XF.Message.insertMessages(
dataHtml,
this.getMessagesContainer(),
this.options.ascending,
function($messages)
{
var $message = $messages.first();
if ($message && $message.length)
{
var dims = $message.dimensions(),
windowTop = $(window).scrollTop(),
windowBottom = windowTop + $(window).height();

if (dims.top < windowTop + 50 || dims.top > windowBottom)
{
XF.smoothScroll(Math.max(0, dims.top - 60), false, 200);
}
}
}
);
},

getMessagesContainer: function()
{
var containerOption = this.options.messageContainer;
if (containerOption)
{
return XF.findRelativeIf(containerOption, this.$target).first();
}
else
{
return $('.js-replyNewMessageContainer').first();
}
},

onDraft: function(e, data)
{
if (data.hasNew && data.html)
{
if (data.lastDate && data.lastDate > 0)
{
var $lastDate = $('.js-quickReply input[name="last_date"]');
if ($lastDate.length && parseInt($lastDate.val(), 10) > data.lastDate)
{
return;
}
}

if (this.getMessagesContainer().find('.js-newMessagesIndicator').length)
{
return;
}

// structured like a message
this.insertMessages(data.html);
}
}
});

// ################################## GUEST CAPTCHA HANDLER ###########################################

XF.GuestCaptcha = XF.Element.newHandler({

options: {
url: 'index.php?misc/captcha&with_row=1',
target: '.js-captchaContainer',
skip: '[name=more_options]'
},

$captchaContainer: null,

initialized: false,

init: function()
{
var $form = this.$target;
this.$captchaContainer = $form.find(this.options.target);
if (!this.$captchaContainer.length)
{
return;
}

$form.on('focusin', XF.proxy(this, 'initializeCaptcha'));
$form.on('submit ajax-submit:before', XF.proxy(this, 'submit'));
},

initializeCaptcha: function(e)
{
var $activeElement = $(document.activeElement);

if (this.initialized || $activeElement.is(this.options.skip))
{
return;
}

var rowType = this.$captchaContainer.data('row-type') || '';

XF.ajax('get',
XF.canonicalizeUrl(this.options.url),
{ row_type: rowType },
XF.proxy(this, 'showCaptcha')
);

this.initialized = true;
},

showCaptcha: function(data)
{
var self = this;
XF.setupHtmlInsert(data.html, function ($html, container, onComplete)
{
$html.replaceAll(self.$captchaContainer);

onComplete();
});
},

submit: function(e)
{
if (!this.initialized)
{
var $activeElement = $(document.activeElement);

if (!$activeElement.is(this.options.skip))
{
e.preventDefault();
return false;
}
}
}
});

// ################################## POST EDIT HANDLER ######################

XF.PostEdit = XF.Element.newHandler({

init: function()
{
this.$target.on('quickedit:editcomplete', XF.proxy(this, 'editComplete'));
},

editComplete: function(e, data)
{
var self = this;
XF.setupHtmlInsert(data.html, function($html, container, onComplete)
{
var threadChanges = data.threadChanges || {};

if (threadChanges.title)
{
$('h1.p-title-value').html(container.h1);
$('title').html(container.title);

// This effectively runs twice, but we do need the title to be correct if updating this way.
if (XF.config.visitorCounts['title_count'] && data.visitor)
{
XF.pageTitleCache = container.title;
XF.pageTitleCounterUpdate(data.visitor.total_unread);
}
}

if (threadChanges.customFields)
{
var $newThreadStatusField = $html.closest('.js-threadStatusField'),
$threadStatusField = XF.findRelativeIf('< .block--messages | .js-threadStatusField', self.$target);

if ($newThreadStatusField.length && $threadStatusField.length)
{
$threadStatusField.xfFadeUp(XF.config.speed.fast, function()
{
$threadStatusField.replaceWith($newThreadStatusField).xfFadeDown(XF.config.speed.fast);
});
}
}
else
{
$html.find('.js-threadStatusField').remove();
}
});
}
});

XF.Event.register('click', 'message-loader', 'XF.MessageLoaderClick');
XF.Event.register('click', 'quick-edit', 'XF.QuickEditClick');
XF.Event.register('click', 'quote', 'XF.QuoteClick');
XF.Event.register('click', 'solution-edit', 'XF.SolutionEditClick');

XF.Element.register('multi-quote', 'XF.MultiQuote');
XF.Element.register('select-to-quote', 'XF.SelectToQuote');
XF.Element.register('quick-reply', 'XF.QuickReply');
XF.Element.register('guest-captcha', 'XF.GuestCaptcha');
XF.Element.register('post-edit', 'XF.PostEdit');
}
(jQuery, window, document);