Seditio Source
Root |
./othercms/xenForo 2.2.8/js/xf/core.js
var XF = window.XF || {};

if (window.jQuery === undefined) jQuery = $ = {};

!function($, window, document)
{
"use strict";

// already loaded, don't load this twice
if (XF.activate)
{
console.error('XF core has been double loaded');
return;
}

if (!XF.browser)
{
XF.browser = {
browser: '',
version: 0,
os: '',
osVersion: null
};
}

// ################################# JQUERY EXTENSIONS #######################################

(function()
{
// approach inspired by Bootstrap / http://blog.alexmaccaw.com/css-transitions
var support = (function()
{
var el = document.createElement('fake'),
transitions = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend'
};

for (var name in transitions)
{
if (el.style[name] !== undefined)
{
return {end: transitions[name] };
}
}

return false;
})();

$.support.transition = support;
if ($.support.transition)
{
$.event.special.xfTransitionEnd = {
bindType: support.end,
delegateType: support.end,
handle: function(e)
{
if ($(e.target).is(this))
{
return e.handleObj.handler.apply(this, arguments);
}
}
};
}

var dir = $('html').attr('dir'),
isRtl = (dir && dir.toUpperCase() == 'RTL'),
scrollLeftType = 'normal';

if (isRtl)
{
var $tester = $('<div style="width: 80px; height: 40px; font-size: 30px; overflow: scroll; white-space: nowrap; word-wrap: normal; position: absolute; top: -1000px; visibility: hidden; pointer-events: none">MMMMMMMMMM</div>'),
tester = $tester[0];

$tester.appendTo('body');

if (tester.scrollLeft > 0)
{
// max value at start, scrolls towards 0
scrollLeftType = 'inverted';
}
else
{
tester.scrollLeft = -1;
if (tester.scrollLeft == -1)
{
// 0 at start, scrolls towards negative values
scrollLeftType = 'negative';
}
// else normal: 0 at start, scrolls towards positive values
}

$tester.remove();
}

$.support.scrollLeftType = scrollLeftType;

$.fn.reverse = [].reverse;

$.addEventCapture = (function()
{
var special = $.event.special;

return function(names)
{
if (!document.addEventListener)
{
return;
}

if (typeof names == 'string')
{
names = [names];
}

$.each(names, function (i, name)
{
var handler = function (e)
{
e = $.event.fix(e);

return $.event.dispatch.call(this, e);
};

special[name] = special[name] || {};

if (special[name].setup || special[name].teardown)
{
return;
}

$.extend(special[name], {
setup: function()
{
this.addEventListener(name, handler, true);
},
teardown: function()
{
this.removeEventListener(name, handler, true);
}
});
});
};
})();
})();

$.fn.extend(
{
/**
* Allows an element to respond to an event fired by a parent element, such as the containing tab being un-hidden
*
* @param eventType
* @param callback
* @param once  bool    True if you want this to execute once only
* @returns {onWithin}
*/
onWithin: function(eventType, callback, once)
{
var $this = this;

$(document).on(eventType, function(e)
{
if ($(e.target).has($this).length)
{
if (once)
{
$(document).off(e);
}
callback(e);
}
});

return this;
},

oneWithin: function(eventType, callback)
{
return this.onWithin(eventType, callback, true);
},

onPassive: function(eventType, callback)
{
if (typeof eventType == 'object')
{
for (var type in eventType)
{
this.onPassive(type, eventType[type]);
}
return this;
}

if (typeof eventType != 'string' || typeof callback != 'function')
{
console.warn('$.onPassive failure for %s.on%s, check parameters', this.get(0), eventType);
return this;
}

if (eventType.indexOf('.') !== -1)
{
console.warn('$.onPassive does not support namespaced events %s.on%s', this.get(0), eventType);
return false;
}

if (XF.Feature.has('passiveeventlisteners'))
{
this.get(0).addEventListener(eventType, callback, { passive: true });
return this;
}
else
{
this.get(0).addEventListener(eventType, callback);
return this;
}
},

offPassive: function(eventType, callback)
{
this.get(0).removeEventListener(eventType, callback);
return this;
},

onPointer: function(events, callback)
{
if ($.isPlainObject(events))
{
for (var k in events)
{
if (events.hasOwnProperty(k))
{
this.onPointer(k, events[k]);
}
}

return this;
}

if (typeof events === 'string')
{
events = events.split(/\s+/);
}

var t = this,
dataKey = 'xf-pointer-type',
cb = function(e)
{
var $this = $(this),
type = $this.data(dataKey);

e.xfPointerType = e.pointerType || type || '';

callback(e);
};

events.forEach(function(event)
{
t.on(event, cb);
});

var watchEventName = 'pointerdown.pointer-watcher';

this.off(watchEventName).on(watchEventName, function(e)
{
$(this).data(dataKey, e.pointerType);
});

return this;
},

xfFadeDown: function(speed, callback)
{
this.filter(':hidden').hide().css('opacity', 0);

speed = speed || XF.config.speed.normal;

this.find('.is-sticky').addClass('was-sticky').removeClass('is-sticky');

this.animate(
{
opacity: 1,
height: 'show',
marginTop: 'show',
marginBottom: 'show',
paddingTop: 'show',
paddingBottom: 'show'
},
{
duration: speed,
easing: 'swing',
complete: function()
{
$(this).find('.was-sticky').addClass('is-sticky').removeClass('was-sticky');
if (callback)
{
callback();
}
XF.layoutChange();
}
}
);

return this;
},

xfFadeUp: function(speed, callback)
{
speed = speed || XF.config.speed.normal;

this.find('.is-sticky').addClass('was-sticky').removeClass('is-sticky');

this.animate(
{
opacity: 0,
height: 'hide',
marginTop: 'hide',
marginBottom: 'hide',
paddingTop: 'hide',
paddingBottom: 'hide'
},
{
duration: speed,
easing: 'swing',
complete: function()
{
$(this).find('.was-sticky').addClass('is-sticky').removeClass('was-sticky');
if (callback)
{
callback();
}
XF.layoutChange();
}
}
);

return this;
},

xfUniqueId: function()
{
var id = this.attr('id');
if (!id)
{
id = 'js-XFUniqueId' + XF.getUniqueCounter();
this.attr('id', id);
}

return id;
},

findExtended: function(selector)
{
var match;
if (typeof selector === 'string' && (match = selector.match(/^<([^|]+)(\|([\s\S]+))?$/)))
{
var lookUp = $.trim(match[1]),
innerMatch,
i,
relativeLookup = {
up: 'parent',
next: 'next',
prev: 'prev'
},
move,
$newBase = this;

do
{
if (innerMatch = lookUp.match(/^:(up|next|prev)(\((\d+)\))?/))
{
if (!innerMatch[2])
{
innerMatch[3] = 1;
}

move = relativeLookup[innerMatch[1]];

for (i = 0; i < innerMatch[3]; i++)
{
$newBase = $newBase[move]();
if (!$newBase)
{
$newBase = $();
}
}

lookUp = $.trim(lookUp.substr(innerMatch[0].length));
}
}
while (innerMatch);

if (lookUp.length)
{
$newBase = $newBase.closest(lookUp);
}

if (!$newBase.length)
{
$newBase = $();
}

selector = match[2] ? $.trim(match[3]) : '';

if (selector.length)
{
return $newBase.find(selector);
}
else
{
return $newBase;
}
}

return this._find(selector);
},

dimensions: function(outer, outerWithMargin)
{
var dims = this.offset(),
dimensions = {
top: dims.top,
left: dims.left
// offset may return a ClientRect object which is read only
};

outerWithMargin = outerWithMargin ? true : false;

dimensions.width = outer ? this.outerWidth(outerWithMargin) : this.width();
dimensions.height = outer ? this.outerHeight(outerWithMargin) : this.height();
dimensions.right = dimensions.left + dimensions.width;
dimensions.bottom = dimensions.top + dimensions.height;

return dimensions;
},

viewport: function(outer, outerWithMargin)
{
var vp = {
width: outer ? this.outerWidth(outerWithMargin) : this.width(),
height: outer ? this.outerHeight(outerWithMargin) : this.height(),
left: this.scrollLeft(),
top: this.scrollTop(),
right: 0,
bottom: 0,
docWidth: $(document).width(),
docHeight: $(document).height()
};

vp.bottom = vp.top + vp.height;
vp.right = vp.left + vp.width;

return vp;
},

hasFixableParent: function()
{
var fixableParent = false;

this.parents().each(function()
{
var $el = $(this);

switch ($el.css('position'))
{
case 'fixed':
case 'sticky':
case '-webkit-sticky':
fixableParent = $el;
return false;
}

if ($el.data('sticky_kit'))
{
fixableParent = $el;
return false;
}
});

return fixableParent;
},

hasFixedParent: function()
{
var fixedParent = false;

this.parents().each(function()
{
var $el = $(this);

switch ($el.css('position'))
{
case 'fixed':
{
fixedParent = $el;
return false;
}

case 'sticky':
case '-webkit-sticky':
{
var elDimensions = $el.dimensions(true),
viewport = $(window).viewport(),
stickyTop = $el.css('top'),
stickyBottom = $el.css('bottom'),
edgeDiff;

// If an element is sticky, it is 'stuck' when its "interesting" edge is exactly at its offset position.
// This code currently supports only sticky/top and sticky/bottom

if (stickyTop !== 'auto')
{
// iOS seems to have half a pixel inconsistency in reporting, so give a little leeway
edgeDiff = (elDimensions.top - viewport.top) - parseInt(stickyTop, 10);
if (edgeDiff <= 0.5 && edgeDiff >= -0.5)
{
fixedParent = $el;
return false;
}
}

if (stickyBottom !== 'auto')
{
// iOS seems to have half a pixel inconsistency in reporting, so give a little leeway
edgeDiff = (elDimensions.bottom - viewport.bottom) - parseInt(stickyBottom, 10);
if (edgeDiff <= 0.5 && edgeDiff >= -0.5)
{
fixedParent = $el;
return false;
}
}
}
}
});

return fixedParent;
},

onTransitionEnd: function(duration, callback)
{
var called = false,
$el = this,
f = function()
{
if (called)
{
return;
}

called = true;
return callback.apply(this, arguments);
};

this.one('xfTransitionEnd', f);
setTimeout(function()
{
if (!called)
{
$el.trigger('xfTransitionEnd');
}
}, duration + 10);

return this;
},

autofocus: function()
{
var $input = $(this);

if (XF.isIOS())
{
if (!$input.is(':focus'))
{
$input.addClass('is-focused');
$input.on('blur', function()
{
$input.removeClass('is-focused');
});
}
}
else
{
$input.focus();
}

return this;
},

normalizedScrollLeft: function(newLeft)
{
var type = $.support.scrollLeftType;

if (typeof newLeft !== 'undefined')
{
for (var i = 0; i < this.length; i++)
{
(function(el, newValue)
{
switch (type)
{
case 'negative':
newValue = newValue > 0 ? -newValue : 0;
break;

case 'inverted':
newValue = el.scrollWidth - el.offsetWidth - newValue;

// otherwise don't need to change
}

el.scrollLeft = newValue;
})(this[i], newLeft);
}

return this;
}

var el = this[0];
if (!el)
{
return 0;
}

var scrollLeft = el.scrollLeft;

switch (type)
{
case 'negative':
return scrollLeft < 0 ? -scrollLeft : 0;

case 'inverted':
var calc = el.scrollWidth - scrollLeft - el.offsetWidth;
return (calc < 0.5 ? 0 : calc); // avoid rounding issues

case 'normal':
default:
return scrollLeft;
}
},

/**
* Attempts to focus the next focusable element
*
* @returns jQuery The next focusable element
*/
focusNext: function()
{
var $focusable = $('input:not([type="hidden"]), select, textarea, a, button').filter(':visible'),

$next = $focusable.eq($focusable.index(this) + 1).focus();

return $next;
},

retinaFix: function()
{
if (window.devicePixelRatio >= 2)
{
this.find('img[srcset]').one('load', function (e)
{
$(this).prop('srcset', $(this).attr('srcset'));
});
}

return this;
}
});

(function()
{
function getCssTransitionDuration($el)
{
if (!$.support.transition)
{
return 0;
}

var el = $el[0];
if (!el || !(el instanceof window.Element))
{
return 0;
}

var durationCss = $el.css('transition-duration'), duration = 0;
if (durationCss && durationCss.match(/^(\+|-|)([0-9]*\.[0-9]+|[0-9]+)(ms|s)/i))
{
duration = (RegExp.$1 == '-' ? -1 : 1) * parseFloat(RegExp.$2) * (RegExp.$3.toLowerCase() == 'ms' ? 1 : 1000);
}

return duration;
}

function getClassDiff($el, checkClassList, getMissing)
{
var diff = [];

if ($.isFunction(checkClassList))
{
checkClassList = checkClassList.call($el[0], 0, $el[0].className);
}

var checkClasses = $.trim(checkClassList).split(/\s+/),
classes = " " + $el[0].className + " ",
present;
for (var i = 0; i < checkClasses.length; i++)
{
present = (classes.indexOf(" " + checkClasses[i] + " ") >= 0);
if ((present && !getMissing) || (!present && getMissing))
{
diff.push(checkClasses[i]);
}
}

return diff.join(" ");
}

var mappedAttrs = {
height: ['height', 'padding-top', 'padding-bottom', 'margin-top', 'margin-bottom', 'border-top-width', 'border-bottom-width'],
width: ['width', 'padding-left', 'padding-right', 'margin-left', 'margin-right', 'border-right-width', 'border-left-width']
};

function adjustClasses($el, isAdding, className, onTransitionEnd, instant)
{
var duration = instant ? 0 : getCssTransitionDuration($el),
mainFunc = isAdding ? 'addClass' : 'removeClass',
inverseFunc = isAdding ? 'removeClass' : 'addClass',
getMissing = isAdding ? true : false,
adjustClasses = getClassDiff($el, className, getMissing),
el = $el[0],
transitioningClass = 'is-transitioning',
transitionEndFakeCall = function()
{
if (onTransitionEnd)
{
setTimeout(function()
{
onTransitionEnd.call(el, $.Event('xfTransitionEnd'));
}, 0);
}
};

if (!adjustClasses.length)
{
transitionEndFakeCall();
return;
}

if (duration <= 0)
{
$el[mainFunc](adjustClasses);
transitionEndFakeCall();
return;
}

if ($el.hasClass(transitioningClass))
{
$el.trigger('xfTransitionEnd');
}

$el.addClass(transitioningClass);

if ($el.css('transition-property').match(/(^|\s|,)-xf-(width|height)($|\s|,)/))
{
var attr = RegExp.$2,
relatedAttrs = mappedAttrs[attr],
curCssValues = $el.css(relatedAttrs),
curCssValue = curCssValues[attr],
storeCurStyle = "transition." + attr,
curStyleValues = $el.data(storeCurStyle),
style = el.style,
previousTransition = style['transition']
|| style['-webkit-transition']
|| style['-moz-transition']
|| style['-o-transition']
|| '',
i;

if (curStyleValues === undefined)
{
curStyleValues = {};
for (i = 0; i < relatedAttrs.length; i++)
{
curStyleValues[relatedAttrs[i]] = style[relatedAttrs[i]] || '';
}
}

if ($el[attr]() == 0)
{
curCssValue = '0';

for (i in curCssValues)
{
if (curCssValues.hasOwnProperty(i))
{
curCssValues[i] = '0';
}
}
}

$el.data(storeCurStyle, curStyleValues)
.css('transition', 'none')
[mainFunc](adjustClasses);

var newCssValues = $el.css(relatedAttrs),
newCssValue = newCssValues[attr];

if ($el[attr]() == 0)
{
newCssValue = '0';
for (i in newCssValues)
{
if (newCssValues.hasOwnProperty(i))
{
newCssValues[i] = '0';
}
}
}

$el[inverseFunc](adjustClasses);

if (curCssValue != newCssValue)
{
var originalCallback = onTransitionEnd;

$el.css(curCssValues);
el.offsetWidth; // this is needed to force a redraw; must be before the transition restore line
$el.css('transition', previousTransition)
.css(newCssValues);

onTransitionEnd = function()
{
$el.css($el.data(storeCurStyle))
.removeData(storeCurStyle);

if (originalCallback)
{
originalCallback.apply(this, arguments);
}
}
}
else
{
$el.css('transition', previousTransition);
}
}

$el.onTransitionEnd(duration, function()
{
$el.removeClass(transitioningClass);

if (onTransitionEnd)
{
onTransitionEnd.apply(this, arguments);
}
});
$el[mainFunc](className);
}

$.fn.addClassTransitioned = function(className, onTransitionEnd, instant)
{
var len = this.length;
for (var i = 0; i < len; i++)
{
adjustClasses($(this[i]), true, className, onTransitionEnd, instant)
}

return this;
};

$.fn.removeClassTransitioned = function(className, onTransitionEnd, instant)
{
var len = this.length;
for (var i = 0; i < len; i++)
{
adjustClasses($(this[i]), false, className, onTransitionEnd, instant)
}

return this;
};

$.fn.toggleClassTransitioned = function(className, state, onTransitionEnd, instant)
{
if (typeof state !== 'boolean' && typeof onTransitionEnd === 'undefined')
{
onTransitionEnd = state;
state = null;
}

var useState = (typeof state === 'boolean'),
len = this.length;

for (var i = 0; i < len; i++)
{
var $el = $(this[i]),
add;

if (useState)
{
add = state;
}
else
{
add = $el.hasClass(className) ? false : true;
}

adjustClasses($el, add, className, onTransitionEnd, instant);
}

return this;
};
})();

// ################################# BASE HELPERS ############################################

$.extend(XF, {
config: {
userId: null,
enablePush: false,
skipServiceWorkerRegistration: false,
skipPushNotificationSubscription: false,
skipPushNotificationCta: false,
serviceWorkerPath: null,
pushAppServerKey: null,
csrf: $('html').data('csrf'),
time: {
now: 0,
today: 0,
todayDow: 0,
tomorrow: 0,
yesterday: 0,
week: 0
},
cookie: {
path: '/',
domain: '',
prefix: 'xf_'
},
url: {
fullBase: '/',
basePath: '/',
css: '',
keepAlive: ''
},
css: {},
js: {},
jsState: {},
speed: {
xxfast: 50,
xfast: 100,
fast: 200,
normal: 400,
slow: 600
},
job: {
manualUrl: ''
},
borderSizeFeature: '3px',
fontAwesomeWeight: 'r',
enableRtnProtect: true,
enableFormSubmitSticky: true,
visitorCounts: {
conversations_unread: '0',
alerts_unviewed: '0',
total_unread: '0',
title_count: false,
icon_indicator: false
},
uploadMaxFilesize: null,
allowedVideoExtensions: [],
allowedAudioExtensions: [],
shortcodeToEmoji: true,
publicMetadataLogoUrl: '',
publicPushBadgeUrl: ''
},

debug: {
disableAjaxSubmit: false
},

counter: 1,

pageDisplayTime: null,

phrases: {},

getApp: function()
{
return $('html').data('app') || null;
},

getKeyboardInputs: function()
{
return 'input:not([type=radio], [type=checkbox], [type=submit], [type=reset]), textarea';
},

onPageLoad: function()
{
$(document).trigger('xf:page-load-start');

XF.NavDeviceWatcher.initialize();
XF.ActionIndicator.initialize();
XF.DynamicDate.initialize();
XF.KeepAlive.initialize();
XF.LinkWatcher.initLinkProxy();
XF.LinkWatcher.initExternalWatcher();
XF.NoticeWatcher.initialize();
XF.ExpandableContent.watch();
XF.ScrollButtons.initialize();
XF.KeyboardShortcuts.initialize();
XF.FormInputValidation.initialize();
XF.PWA.initialize();
XF.Push.initialize();
XF.IgnoreWatcher.initializeHash();
XF.BrowserWarning.display();
XF.History.initialize();

XF.config.jsState = XF.applyJsState({}, XF.config.jsState);

XF.activate(document);

$(document).on('ajax:complete', function(e, xhr, status)
{
var data = xhr.responseJSON;
if (!data)
{
return;
}

if (data.visitor)
{
XF.updateVisitorCounts(data.visitor, true);
}
});

$(document).on('ajax:before-success', function(e, data, status, xhr)
{
var data = xhr.responseJSON;
if (!data)
{
return;
}

if (data && data.job)
{
var job = data.job;
if (job.manual)
{
XF.JobRunner.runManual(job.manual);
}

if (job.autoBlocking)
{
XF.JobRunner.runAutoBlocking(job.autoBlocking, job.autoBlockingMessage);
}
else if (job.auto)
{
setTimeout(XF.JobRunner.runAuto, 0);
}
}
});

$(document).on('keyup', 'a:not([href])', function(e)
{
if (e.key == 'Enter')
{
$(e.currentTarget).click();
}
});

if ($('html[data-run-jobs]').length)
{
setTimeout(XF.JobRunner.runAuto, 100);
}

XF.updateVisitorCountsOnLoad(XF.config.visitorCounts);

XF.CrossTab.on('visitorCounts', function(counts)
{
XF.updateVisitorCounts(counts, false);
});

XF.pageLoadScrollFix();

setTimeout(function()
{
$('[data-load-auto-click]').first().click();
}, 100);

$(document).trigger('xf:page-load-complete');
},

addExtraPhrases: function(el)
{
$(el).find('script.js-extraPhrases').each(function()
{
var $script = $(this),
phrases;

try
{
phrases = $.parseJSON($script.html()) || {};
$.extend(XF.phrases, phrases);
}
catch (e)
{
console.error(e);
}

$script.remove();
});
},

phrase: function(name, vars, fallback)
{
var phrase = XF.phrases[name];
if (phrase && vars)
{
phrase = XF.stringTranslate(phrase, vars);
}
return phrase || fallback || name;
},

_isRtl: null,

isRtl: function()
{
if (XF._isRtl === null)
{
var dir = $('html').attr('dir');
XF._isRtl = (dir && dir.toUpperCase() == 'RTL');
}
return XF._isRtl;
},

rtlFlipKeyword: function(keyword)
{
if (!XF.isRtl())
{
return keyword;
}

var lower  = keyword.toLowerCase();
switch (lower)
{
case 'left': return 'right';
case 'right': return 'left';
default: return keyword;
}
},

isMac: function()
{
return navigator.userAgent.indexOf('Mac OS') != -1;
},

isIOS: function()
{
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
},

isIE: function()
{
var ua = navigator.userAgent;
return (ua.indexOf('MSIE ') > 0 || ua.indexOf('Trident/') > 0);
},

log: function()
{
if (!console.log || !console.log.apply)
{
return;
}

console.log.apply(console, arguments);
},

findRelativeIf: function(selector, $base)
{
if (!selector)
{
throw new Error('No selector provided');
}

var match;
if (match = selector.match(/^(<|>|\|)/))
{
if (match[1] == '<')
{
return $base.findExtended(selector);
}

if (match[1] == '|')
{
selector = selector.substr(1);
}

return $base.find(selector);
}
else
{
return $(selector);
}
},

isElementVisible: function($el)
{
var el = $el[0],
rect = el.getBoundingClientRect();

return (
rect.top >= 0
&& rect.left >= 0
&& rect.bottom <= $(window).height()
&& rect.right <= $(window).width()
);
},

/**
* Simple function to be run whenever we change the page layout with JS,
* to trigger recalculation of JS-positioned elements
* such as sticky_kit items
*/
layoutChange: function()
{
if (!XF._layoutChangeTriggered)
{
XF._layoutChangeTriggered = true;
setTimeout(function()
{
XF._layoutChangeTriggered = false;

$(document.body)
.trigger('sticky_kit:recalc')
.trigger('xf:layout');
}, 0);
}
},

_layoutChangeTriggered: false,

updateAvatars: function(userId, newAvatars, includeEditor)
{
$('.avatar').each(function()
{
var $avatarContainer = $(this),
$avatar = $avatarContainer.find('img, span').first(),
classPrefix = 'avatar-u' + userId + '-',
$update = $avatarContainer.hasClass('avatar--updateLink')
? $avatarContainer.find('.avatar-update')
: null,
$newAvatar;

if (!includeEditor && $avatar.hasClass('.js-croppedAvatar'))
{
return;
}

if ($avatar.is('[class^="' + classPrefix + '"]'))
{
if ($avatar.hasClass(classPrefix + 's'))
{
$newAvatar = $(newAvatars['s']);
}
else if ($avatar.hasClass(classPrefix + 'm'))
{
$newAvatar = $(newAvatars['m']);
}
else if ($avatar.hasClass(classPrefix + 'l'))
{
$newAvatar = $(newAvatars['l']);
}
else if ($avatar.hasClass(classPrefix + 'o'))
{
$newAvatar = $(newAvatars['o']);
}
else
{
return;
}

$avatarContainer.html($newAvatar.html());

if ($newAvatar.hasClass('avatar--default'))
{
$avatarContainer.addClass('avatar--default');
if ($newAvatar.hasClass('avatar--default--dynamic'))
{
$avatarContainer.addClass('avatar--default--dynamic');
}
else if ($newAvatar.hasClass('avatar--default--text'))
{
$avatarContainer.addClass('avatar--default--text');
}
else if ($newAvatar.hasClass('avatar--default--image'))
{
$avatarContainer.addClass('avatar--default--image');
}
}
else
{
$avatarContainer.removeClass('avatar--default avatar--default--dynamic avatar--default--text avatar--default--image');
}
$avatarContainer.attr('style', $newAvatar.attr('style'));

if ($update)
{
$avatarContainer.append($update);
}
}
});
},

updateVisitorCounts: function(visitor, isForegroundUpdate, sourceTime)
{
if (!visitor || XF.getApp() != 'public')
{
return;
}

XF.badgeCounterUpdate($('.js-badge--conversations'), visitor.conversations_unread);
XF.badgeCounterUpdate($('.js-badge--alerts'), visitor.alerts_unviewed);

if (XF.config.visitorCounts['title_count'])
{
XF.pageTitleCounterUpdate(visitor.total_unread);
}

if (XF.config.visitorCounts['icon_indicator'])
{
XF.faviconUpdate(visitor.total_unread);
}

if (isForegroundUpdate)
{
XF.appBadgeUpdate(visitor.total_unread);

XF.CrossTab.trigger('visitorCounts', visitor);

XF.LocalStorage.setJson('visitorCounts', {
time: sourceTime || (Math.floor(new Date().getTime() / 1000) - 1),
conversations_unread: visitor.conversations_unread,
alerts_unviewed: visitor.alerts_unviewed,
total_unread: visitor.total_unread
});
}

// TODO: Stack alerts?
},

updateVisitorCountsOnLoad: function(visitor)
{
var localLoadTime = XF.getLocalLoadTime(),
cachedData = XF.LocalStorage.getJson('visitorCounts');

if (cachedData && cachedData.time && cachedData.time > localLoadTime)
{
visitor.conversations_unread = cachedData.conversations_unread;
visitor.alerts_unviewed = cachedData.alerts_unviewed;
visitor.total_unread = cachedData.total_unread;
}

XF.updateVisitorCounts(visitor, true, localLoadTime);
},

badgeCounterUpdate: function($badge, newCount)
{
if (!$badge.length)
{
return;
}

$badge.attr('data-badge', newCount);

if (String(newCount) != '0')
{
$badge.addClass('badgeContainer--highlighted');
}
else
{
$badge.removeClass('badgeContainer--highlighted');
}
},

shouldCountBeShown: function(newCount)
{
var newCountNormalized = parseInt(newCount.replace(/[,. ]/g, ''));
return newCountNormalized > 0;
},

pageTitleCache: '',

pageTitleCounterUpdate: function(newCount)
{
var pageTitle = document.title,
newTitle;

if (XF.pageTitleCache === '')
{
XF.pageTitleCache = pageTitle;
}

if (pageTitle !== XF.pageTitleCache && pageTitle.charAt(0) === '(')
{
pageTitle = XF.pageTitleCache;
}

newTitle = (this.shouldCountBeShown(newCount) ? '(' + newCount + ') ' : '') + pageTitle;

if (newTitle != document.title)
{
document.title = newTitle;
}
},

favIconAlertShown: false,

faviconUpdate: function(newCount)
{
var shouldBeShown = this.shouldCountBeShown(newCount);
if (shouldBeShown === XF.favIconAlertShown)
{
return;
}

var $favicons = $('link[rel~="icon"]');

if (!$favicons.length)
{
// no favicons support
return;
}

XF.favIconAlertShown = shouldBeShown;

var t = this;
$favicons.each(function(i, favicon)
{
var $favicon = $(favicon),
href = $favicon.attr('href'),
originalHrefKey = 'original-href',
originalHref = $favicon.data(originalHrefKey);

if (t.shouldCountBeShown(newCount))
{
if (!originalHref)
{
$favicon.data(originalHrefKey, href);
}

$('<img />')
.on('load', function()
{
var updatedFaviconUrl = XF.faviconDraw(this);

if (updatedFaviconUrl)
{
$favicon.attr('href', updatedFaviconUrl);
}
})
.attr('src', href);
}
else
{
if (originalHref)
{
$favicon.attr('href', originalHref)
.removeData(originalHrefKey);
}
}
});
},

faviconDraw: function(image)
{
var w = image.naturalWidth,
h = image.naturalHeight,
$canvas = $('<canvas />').attr({ width: w, height: h }),
context = $canvas[0].getContext('2d');

var ratio = 32 / 6,
radius = w / ratio,
x = radius,
y = radius,
startAngle = 0,
endAngle = Math.PI * 2,
antiClockwise = false;

context.drawImage(image, 0, 0);
context.beginPath();
context.arc(x, y, radius, startAngle, endAngle, antiClockwise);
context.fillStyle = "#E03030";
context.fill();
context.lineWidth = w / 16;
context.strokeStyle = "#EAEAEA";
context.stroke();
context.closePath();

try
{
return $canvas[0].toDataURL('image/png');
}
catch (e)
{
return null;
}
},

appBadgeUpdate: function(newCount)
{
if (!('setAppBadge' in navigator))
{
return;
}

if (navigator.webdriver)
{
return;
}

if (navigator.userAgent.match(/Chrome-Lighthouse|Googlebot|AdsBot-Google|Mediapartners-Google/i))
{
return;
}

newCount = parseInt(String(newCount).replace(/[,. ]/g, ''));
navigator.setAppBadge(newCount);
},

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

$div.html(html);

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

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

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

// now for PHP, CODE and HTML
$div.find('.bbCodeBlock--code').each(function()
{
var $this = $(this);

if (!$this.find('.bbCodeCode'))
{
return true;
}

var $code = $this.find('.bbCodeCode code');

if (!$code.length)
{
return true;
}

var cl = $code.attr('class'),
match = cl ? cl.match(/language-(\S+)/) : null,
language = match ? match[1] : null;

$code.removeAttr('class');

$this.replaceWith($code.first().attr('data-language', language || 'none'));
});

// handle [URL unfurl=true] tags
$div.find('.bbCodeBlock--unfurl').each(function()
{
var url = $(this).data('url');
$(this).replaceWith('[URL unfurl=true]' + url + '[/URL]');
});

// 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('.bbCodeSpoiler').each(function()
{
var $button, target, $spoilerTitle, spoilerTitle = '', spoilerText;

// find the button and the target
$button = $(this).find('.bbCodeSpoiler-button');
if ($button.length)
{
spoilerText = $(this).find('.bbCodeSpoiler-content').html();
$spoilerTitle = $button.find('.bbCodeSpoiler-button-title');

if ($spoilerTitle.length)
{
spoilerTitle = '="' + $spoilerTitle.text() + '"';
}

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

$div.find('.bbCodeInlineSpoiler').each(function()
{
var spoilerText = $(this).html();
$(this).replaceWith('[ISPOILER]' + spoilerText + '[/ISPOILER]');
});

return $div.html();
},

hideOverlays: function()
{
$.each(XF.Overlay.cache, function(id, overlay)
{
overlay.hide();
});
},

hideTooltips: function()
{
$.each(XF.TooltipTrigger.cache, function(id, trigger)
{
trigger.hide();
});
},

hideParentOverlay: function($child)
{
var $overlayContainer = $child.closest('.overlay-container');
if ($overlayContainer.length && $overlayContainer.data('overlay'))
{
$overlayContainer.data('overlay').hide();
}
},

getStickyHeaderOffset: function()
{
var i, offset = 0;

for (i = 0; i < XF.StickyHeader.cache.length; i++)
{
var stickyHeader = XF.StickyHeader.cache[i];
if (stickyHeader.$target.hasClass(stickyHeader.options.stickyClass))
{
offset += stickyHeader.$target.outerHeight();
}
}

return offset;
},

loadedScripts: {},

/**
* Given a URL, load it (if not already loaded)
* before running a callback function on success
*
* @param url
* @param successCallback
*/
loadScript: function(url, successCallback)
{
if (XF.loadedScripts.hasOwnProperty(url))
{
return false;
}

XF.loadedScripts[url] = true;

return $.ajax({
url: url,
dataType: "script",
cache: true,
global: false,
success: successCallback
});
},

/**
* Given an array of URLs, load them all (if not already loaded)
* before running a callback function on complete (success or error).
*
* In the absolute majority of browsers, this will execute the loaded scripts in the order provided.
*
* @param urls
* @param completeCallback
*/
loadScripts: function(urls, completeCallback)
{
var firstScript = document.scripts[0],
useAsync = 'async' in firstScript,
useReadyState = firstScript.readyState,
head = document.head,
toLoad = 0,
url,
pendingScripts = [];

function loaded()
{
toLoad--;
if (toLoad === 0 && completeCallback)
{
completeCallback();
}
}

function stateChange()
{
var pendingScript;
while (pendingScripts[0] && pendingScripts[0].readyState == 'loaded')
{
pendingScript = pendingScripts.shift();
pendingScript.onreadystatechange = null;
pendingScript.onerror = null;
head.appendChild(pendingScript);

loaded();
}
}

for (var i in urls)
{
if (!urls.hasOwnProperty(i))
{
continue;
}

url = urls[i];

if (XF.loadedScripts[url])
{
continue;
}

XF.loadedScripts[url] = true;
toLoad++;

if (useAsync)
{
// pretty much any modern browser
(function(url)
{
var $script = $('<script>').prop({
src: url,
async: false
});
$script.on('load error', function(e)
{
$script.off('load error');
loaded();
});

head.appendChild($script[0]);
})(url);
}
else if (useReadyState)
{
// IE 9
(function(url)
{
var script = document.createElement('script');
pendingScripts.push(script);

script.onreadystatechange = stateChange;
script.onerror = function()
{
script.onreadystatechange = null;
script.onerror = null;
loaded();
};

script.src = url;
})(url);
}
else
{
// should very rarely be used
$.ajax({
url: url,
dataType: "script",
cache: true,
global: false
}).always(loaded);
}
}

if (!toLoad && completeCallback)
{
completeCallback();
}
},

ajax: function(method, url, data, successCallback, options)
{
if (typeof data == 'function' && successCallback === undefined)
{
successCallback = data;
data = {};
}

data = data || {};

var useDefaultSuccess = true,
useDefaultSuccessError = true,
useError = true;
if (options)
{
if (options.skipDefault)
{
useDefaultSuccess = false;
useDefaultSuccessError = false;
delete options.skipDefault;
}
if (options.skipDefaultSuccessError)
{
useDefaultSuccessError = false;
delete options.skipDefaultSuccessError;
}
if (options.skipDefaultSuccess)
{
useDefaultSuccess = false;
delete options.skipDefaultSuccess;
}
if (options.skipError)
{
useError = false;
delete options.skipError;
}
}

var onBeforeSend = function(xhr, settings)
{
$(document).trigger('ajax:send', [xhr, settings]);
};

var onSuccess = function(data, status, xhr)
{
$(document).trigger('ajax:before-success', [data, status, xhr]);

if (useDefaultSuccessError && XF.defaultAjaxSuccessError(data, status, xhr))
{
// this processed successfully, don't continue
return;
}
if (useDefaultSuccess && XF.defaultAjaxSuccess(data, status, xhr))
{
// this processed successfully, don't continue
return;
}

if (successCallback)
{
successCallback(data, status, xhr);
}
};

var onError = function(xhr, status, exception)
{
if (!xhr.readyState)
{
return;
}

try
{
var json = $.parseJSON(xhr.responseText);
onSuccess(json, '', xhr);
}
catch (e)
{
XF.defaultAjaxError(xhr, status, exception);
}
};

var onComplete = function(xhr, status)
{
$(document).trigger('ajax:complete', [xhr, status]);
};

data = XF.dataPush(data, '_xfRequestUri', window.location.pathname + window.location.search);
data = XF.dataPush(data, '_xfWithData', 1);
if (XF.config.csrf)
{
data = XF.dataPush(data, '_xfToken', XF.config.csrf);
}

var isFormDataObject = (window.FormData && data instanceof FormData),
ajax = $.extend(true, {
cache: true,
data: data || {},
dataType: 'json',
beforeSend: onBeforeSend,
error: useError ? onError : null,
success: onSuccess,
complete: onComplete,
timeout: method === 'get' ? 30000 : 60000,
type: method,
url: url,
processData: isFormDataObject ? false : true
}, options);

if (isFormDataObject)
{
ajax['contentType'] = false;
}

switch (ajax.dataType)
{
case 'html':
case 'json':
case 'xml':
ajax.data = XF.dataPush(ajax.data, '_xfResponseType', ajax.dataType);
}

if (ajax.dataType != 'json')
{
useDefaultSuccess = false;
}

return $.ajax(ajax);
},

dataPush: function(data, key, value)
{
if (!data || typeof data == 'string')
{
// data is empty, or a url string - &name=value
data = String(data);
data += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(value);
}
else if (data[0] !== undefined)
{
// data is a numerically-keyed array of name/value pairs
data.push({ name: key, value: value });
}
else if (window.FormData && data instanceof FormData)
{
// data is a FormData object
data.append(key, value);
}
else
{
// data is an object with a single set of name & value properties
data[key] = value;
}

return data;
},

defaultAjaxSuccessError: function(data, status, xhr)
{
if (typeof data != 'object')
{
XF.alert('Response was not JSON.');
return true;
}

if (data.html && data.html.templateErrors)
{
var templateErrorStr = 'Errors were triggered when rendering this template:';
if (data.html.templateErrorDetails)
{
templateErrorStr += '\n* ' + data.html.templateErrorDetails.join('\n* ');
}
console.error(templateErrorStr)
}

if (data.errorHtml)
{
XF.setupHtmlInsert(data.errorHtml, function($html, container)
{
var title = container.h1 || container.title || XF.phrase('oops_we_ran_into_some_problems');
XF.overlayMessage(title, $html);
});
return true;
}

if (data.errors)
{
XF.alert(data.errors);
return true;
}

if (data.exception)
{
XF.alert(data.exception);
return true;
}

return false;
},

defaultAjaxSuccess: function(data, status, xhr)
{
if (data && data.status == 'ok' && data.message)
{
XF.flashMessage(data.message, 3000);
// let the real callback still run
}

return false;
},

defaultAjaxError: function(xhr, error, exception)
{
switch (error)
{
case 'abort':
return;

case 'timeout':
XF.alert(
XF.phrase('server_did_not_respond_in_time_try_again')
);
return;

case 'notmodified':
case 'error':
if (!xhr || !xhr.responseText)
{
// this is likely a user cancellation, so just return
return;
}
break;
}

console.error('PHP: ' + xhr.responseText);
XF.alert(XF.phrase('oops_we_ran_into_some_problems_more_details_console'));
},

activate: function(el)
{
XF.addExtraPhrases(el);
XF.IgnoreWatcher.refresh(el);
XF.Element.initialize(el);
XF.DynamicDate.refresh(el);
XF.ExpandableContent.checkSizing(el);
XF.UnfurlLoader.activateContainer(el);
XF.KeyboardShortcuts.initializeElements(el);
XF.FormInputValidation.initializeElements(el);

var domEl = (el instanceof $ ? el.get(0) : el);

if (window.FB)
{
setTimeout(function()
{
FB.XFBML.parse(domEl);
}, 0);
}

$(document).trigger('xf:reinit', [el]);
},

getDefaultFormData: function($form, $submitButton, jsonName, jsonOptIn)
{
var formData,
submitName;

if ($submitButton && $submitButton.length && $submitButton.attr('name'))
{
submitName = $submitButton.attr('name');
}

if (jsonName && $form.attr('enctype') === 'multipart/form-data')
{
console.error('JSON serialized forms do not support the file upload-style enctype.');
}

// JSON serialization doesn't support uploads, so form data isn't needed
if (window.FormData && !jsonName)
{
formData = new FormData($form[0]);
if (submitName)
{
formData.append(submitName, $submitButton.attr('value'));
}

// note: this is to workaround a Safari/iOS bug which falls over on empty file inputs
$form.find('input[type="file"]').each(function()
{
var $input = $(this),
files = $input.prop('files');

if (typeof files !== undefined && files.length === 0)
{
try
{
formData.delete($input.attr('name'));
}
catch (e) {}
}
});
}
else
{
if (jsonName)
{
var $els = $form.is('form') ? $($form[0].elements) : $form,
jsonOptInRegex,
jsonEls = [],
regularEls = [],
inputs;

if (jsonOptIn)
{
if (typeof jsonOptIn === 'string')
{
jsonOptIn = jsonOptIn.split(',');
}

var jsonOptInRegexFields = [];
$.each(jsonOptIn, function(i, v)
{
if (typeof i === 'number')
{
jsonOptInRegexFields.push(XF.regexQuote($.trim(v)));
}
else
{
jsonOptInRegexFields.push(XF.regexQuote($.trim(i)));
}
});
if (jsonOptInRegexFields.length)
{
jsonOptInRegex = new RegExp('^(' + jsonOptInRegexFields.join('|') + ')(\\[|$)');
}
}

$els.each(function(i, el)
{
var name = el.name;

if (!name || name.substring(0, 3) === '_xf')
{
regularEls.push(el);
return;
}

if (!jsonOptInRegex || jsonOptInRegex.test(name))
{
jsonEls.push(el);
}
else
{
regularEls.push(el);
}
});

formData = $(regularEls).serializeArray();

inputs = $(jsonEls).serializeJSON();
formData.unshift({
name: jsonName,
value: JSON.stringify(inputs)
});
}
else
{
formData = $form.serializeArray();
}

if (submitName)
{
formData.push({
name: submitName,
value: $submitButton.attr('value')
});
}
}

return formData;
},

scriptMatchRegex: /<script([^>]*)>([\s\S]*?)<\/script>/ig,

setupHtmlInsert: function(container, onReady, retainScripts)
{
if (typeof container === 'string' || container instanceof $)
{
container = { content: container };
}

if (typeof container != 'object' || !container.content)
{
console.error('Was not provided an object or HTML content');
return;
}

var args = arguments;

XF.Loader.load(container.js, container.css, function()
{
var scriptRegexMatch,
embeddedScripts = container.jsInline || [],
html = container.content,
isString = typeof html == 'string',
retainScripts = args[2] ? true : false;

if (container.cssInline)
{
for (var i = 0; i < container.cssInline.length; i++)
{
$('<style>' + container.cssInline[i] + '</style>').appendTo('head');
}
}

if (isString)
{
var isJs, typeMatch;

html = $.trim(html);

if (!retainScripts)
{
while (scriptRegexMatch = XF.scriptMatchRegex.exec(html))
{
isJs = false;
if (typeMatch = scriptRegexMatch[1].match(/(^|\s)type=("|'|)([^"' ;]+)/))
{
switch (typeMatch[3].toLowerCase())
{
case 'text/javascript':
case 'text/ecmascript':
case 'application/javascript':
case 'application/ecmascript':
isJs = true;
break;
}
}
else
{
isJs = true;
}

if (isJs)
{
embeddedScripts.push(scriptRegexMatch[2]);
html = html.replace(scriptRegexMatch[0], '');
}
}
}

// IE11 doesn't appear to find the noscript tags in the DOM
html = html.replace(/<noscript>([\s\S]*?)<\/noscript>/ig, '');
}

var $html = $(isString ? $.parseHTML(html, null, retainScripts) : html);

// fix retina images on loaded content
$html.retinaFix();

// Remove <noscript> tags to ensure they never get parsed when not needed.
$html.find('noscript').empty().remove();

if (onReady instanceof $)
{
var $target = onReady;
onReady = function ($h)
{
$target.html($h);
};
}
if (typeof onReady !== 'function')
{
console.error('onReady was not a function');
return;
}

var onCompleteRun = false,
onComplete = function(skipActivate)
{
if (onCompleteRun)
{
return;
}
onCompleteRun = true;

for (var j = 0; j < embeddedScripts.length; j++)
{
$.globalEval(embeddedScripts[j]);
}

if (container.jsState)
{
XF.config.jsState = XF.applyJsState(XF.config.jsState, container.jsState);
}

if (!skipActivate)
{
XF.activate($html);
}
};

var result = onReady($html, container, onComplete);
if (result !== false)
{
onComplete();
}
});
},

alert: function(message, messageType, title, onClose)
{
var messageHtml = message;
if (typeof message == 'object')
{
messageHtml = '<ul>';
$.each(message, function(k, v)
{
messageHtml += '<li>' + v + '</li>';
});
messageHtml += '</ul>';
messageHtml = '<div class="blockMessage">' + messageHtml + '</div>';
}

if (!messageType)
{
messageType = 'error';
}

if (!title)
{
switch (messageType)
{
case 'error':
title = XF.phrase('oops_we_ran_into_some_problems');
break;

default:
title = '';
}
}

return XF.overlayMessage(title, messageHtml);
},

getOverlayHtml: function(content)
{
var $html,
options = {
dismissible: true,
title: null
};

if ($.isPlainObject(content))
{
options = $.extend({}, options, content);
if (content.html)
{
content = content.html;
}
}

if (typeof content == 'string')
{
$html = $($.parseHTML(content));
}
else if (content instanceof $)
{
$html = content;
}
else
{
throw new Error('Can only create an overlay with html provided as a string or jQuery object');
}

if (!$html.is('.overlay'))
{
var title = options.title;
if (!title)
{
var $header = $html.find('.overlay-title');
if ($header.length)
{
title = $header.contents();
$header.remove();
}
}
if (!title)
{
title = XF.htmlspecialchars($('title').text());
}

var $bodyInsert = $html.find('.overlay-content');

if ($bodyInsert.length)
{
$html = $bodyInsert;
}

var $overlay = $(
'<div class="overlay" tabindex="-1">'
+ '<div class="overlay-title"></div>'
+ '<div class="overlay-content"></div>'
+ '</div>'
);
var $title = $overlay.find('.overlay-title');

$title.html(title);
if (options.dismissible)
{
$title.prepend('<a class="overlay-titleCloser js-overlayClose" role="button" tabindex="0" aria-label="' + XF.phrase('close') + '"></a>');
}
$overlay.find('.overlay-content').html($html);

$html = $overlay;
}

$html.appendTo('body');

return $html;
},

createMultiBar: function(url, callee, onSubmit, onCancel)
{

},

getMultiBarHtml: function(content)
{
var $html,
options = {
dismissible: true,
title: null
};

if ($.isPlainObject(content))
{
options = $.extend({}, options, content);
if (content.html)
{
content = content.html;
}
}

if (typeof content == 'string')
{
$html = $($.parseHTML(content));
}
else if (content instanceof $)
{
$html = content;
}
else
{
throw new Error('Can only create an action bar with html provided as a string or jQuery object');
}

var $multiBar = $(
'<div class="multiBar" tabindex="-1">'
+ '<div class="multiBar-inner"><span>Hello there.</span></div>'
+ '</div>'
);

$multiBar.find('.multiBar-inner').html($html);

$multiBar.appendTo('body');

return $multiBar;
},

overlayMessage: function(title, contentHtml)
{
var $html,
formattedSelector = '.block, .blockMessage';

if (typeof contentHtml == 'string')
{
$html = $($.parseHTML(contentHtml));
}
else if (contentHtml instanceof $)
{
$html = contentHtml;
}
else
{
throw new Error('Can only create an overlay with html provided as a string or jQuery object');
}

if (!$html.is(formattedSelector) && !$html.find(formattedSelector).length)
{
$html = $('<div class="blockMessage" />').html($html);
}

$html = XF.getOverlayHtml({
title: title,
html: $html
});

return XF.showOverlay($html, {role: 'alertdialog'});
},

flashMessage: function(message, timeout, onClose)
{
var $message = $('<div class="flashMessage"><div class="flashMessage-content"></div></div>');
$message.find('.flashMessage-content').html(message);

$message.appendTo('body').addClassTransitioned('is-active');
setTimeout(function()
{
$message.removeClassTransitioned('is-active', function()
{
$message.remove();
if (onClose)
{
onClose();
}
});
}, Math.max(500, timeout));
},

htmlspecialchars: function(string)
{
return String(string)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
},

regexQuote: function(string)
{
return (string + '').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!<>\|\:])/g, "\\$1");
},

stringTranslate: function(string, pairs)
{
string = string.toString();
for (var key in pairs)
{
if (pairs.hasOwnProperty(key))
{
var regex = new RegExp(XF.regexQuote(key, 'g'));
string = string.replace(regex, pairs[key]);
}
}
return string;
},

stringHashCode: function(str)
{
// adapted from http://stackoverflow.com/a/7616484/1480610
var hash = 0, i, chr, len;

if (str.length === 0) return hash;

for (i = 0, len = str.length; i < len; i++)
{
chr   = str.charCodeAt(i);
hash  = ((hash << 5) - hash) + chr;
hash |= 0;
}

return hash;
},

getUniqueCounter: function()
{
var counter = XF.counter;
XF.counter++;

return counter;
},

canonicalizeUrl: function(url)
{
if (url.match(/^[a-z]+:/i))
{
return url;
}

if (url.indexOf('/') == 0)
{
var fullPath = XF.config.url.fullBase,
match;
if (match = fullPath.match(/^([a-z]+:(\/\/)?[^\/]+)\//i))
{
return match[1] + url;
}

return url;
}

return XF.config.url.fullBase + url;
},

isRedirecting: false,

redirect: function(url)
{
XF.isRedirecting = true;

if (XF.JobRunner.isBlockingJobRunning())
{
$(document).one('job:blocking-complete', function()
{
XF.redirect(url);
});
return false;
}

url = XF.canonicalizeUrl(url);

var location = window.location;

if (url == location.href)
{
location.reload(true);
}
else
{
window.location = url;

var destParts = url.split('#'),
srcParts = location.href.split('#');

// on the same page except we changed the hash, because we're asking for a redirect,
// we should explicitly reload
if (destParts[1] && destParts[0] == srcParts[0])
{
location.reload(true);
}
}

return true;
},

getAutoCompleteUrl: function()
{
if (XF.getApp() == 'admin')
{
return XF.canonicalizeUrl('admin.php?users/find')
}
else
{
return XF.canonicalizeUrl('index.php?members/find')
}
},

applyDataOptions: function(options, data, finalTrusted)
{
var output = {}, v, vType, setValue;

for (var i in options)
{
if (!options.hasOwnProperty(i))
{
continue;
}

output[i] = options[i];

if (data.hasOwnProperty(i))
{
v = data[i];
vType = typeof v;
setValue = true;

switch (typeof output[i])
{
case 'string':
if (vType != 'string')
{
v = String(v);
}
break;

case 'number':
if (vType != 'number')
{
v = Number(v);
if (isNaN(v))
{
setValue = false;
}
}
break;

case 'boolean':
if (vType != 'boolean')
{
switch (v)
{
case 'true':
case 'yes':
case 'on':
case '1':
case 1:
v = true;
break;

default:
v = false;
}
}
}

if (setValue)
{
output[i] = v;
}
}
}

if ($.isPlainObject(finalTrusted))
{
output = $.extend(output, finalTrusted);
}

return output;
},

watchInputChangeDelayed: function(input, onChange, delay)
{
var $input = $(input),
value = $input.val(),
timeOut;

delay = delay || 200;

function onKeyup()
{
clearTimeout(timeOut);
timeOut = setTimeout(function()
{
var newValue = $input.val();
if (newValue != value)
{
value = newValue;
onChange();
}
}, delay);
}

function onPaste()
{
setTimeout(function() { $input.trigger('keyup'); }, 0);
}

$input.onPassive(
{
keyup: onKeyup,
paste: onPaste
});
},

insertIntoEditor: function($container, html, text, notConstraints)
{
var htmlCallback = function(editor)
{
editor.insertContent(html);
};

var textCallback = function($textarea)
{
XF.insertIntoTextBox($textarea, text);
};

return XF.modifyEditorContent($container, htmlCallback, textCallback, notConstraints);
},

replaceEditorContent: function($container, html, text, notConstraints)
{
var htmlCallback = function(editor)
{
editor.replaceContent(html);
};

var textCallback = function($textarea)
{
XF.replaceIntoTextBox($textarea, text);
};

return XF.modifyEditorContent($container, htmlCallback, textCallback, notConstraints);
},

clearEditorContent: function($container, notConstraints)
{
var ret = XF.replaceEditorContent($container, '', '', notConstraints);

$container.trigger('draft:sync');

return ret;
},

modifyEditorContent: function($container, htmlCallback, textCallback, notConstraints)
{
var editor = XF.getEditorInContainer($container, notConstraints);
if (!editor)
{
return false;
}

if (XF.Editor && editor instanceof XF.Editor)
{
if (editor.isBbCodeView())
{
var textarea = editor.ed.bbCode.getTextArea();
textCallback(textarea);
textarea.trigger('autosize');
}
else
{
htmlCallback(editor);
}
return true;
}

if (editor instanceof $ && editor.is('textarea'))
{
textCallback(editor);
editor.trigger('autosize');
return true;
}

return false;
},

getEditorInContainer: function($container, notConstraints)
{
var $editor;

if ($container.is('.js-editor'))
{
if (notConstraints && $container.is(notConstraints))
{
return null;
}

$editor = $container;
}
else
{
var $editors = $container.find('.js-editor');
if (notConstraints)
{
$editors = $editors.not(notConstraints);
}

if (!$editors.length)
{
return null;
}

$editor = $editors.first();
}

var editor = XF.Element.getHandler($editor, 'editor');
if (editor)
{
return editor;
}

if ($editor.is('textarea'))
{
return $editor;
}

return null;
},

focusEditor: function($container, notConstraints)
{
var editor = XF.getEditorInContainer($container, notConstraints);
if (!editor)
{
return false;
}

if (XF.Editor && editor instanceof XF.Editor)
{
if (editor.isInitialized())
{
editor.scrollToCursor();
}
return true;
}

if (editor instanceof $ && editor.is('textarea'))
{
editor.autofocus();
return true;
}

return false;
},

insertIntoTextBox: function($textBox, insert)
{
var textBox = $textBox[0],
scrollPos = textBox.scrollTop,
startPos = textBox.selectionStart,
endPos = textBox.selectionEnd,
value = $textBox.val(),
before = value.substring(0, startPos),
after = value.substring(endPos, value.length);

$textBox.val(before + insert + after).trigger('autosize');
textBox.selectionStart = textBox.selectionEnd = startPos + insert.length;
textBox.scrollTop = scrollPos;
$textBox.autofocus();
},

replaceIntoTextBox: function($textBox, insert)
{
$textBox.val(insert).trigger('autosize');
},

isElementWithinDraftForm: function($el)
{
var $form;

if ($el.is('form'))
{
$form = $el;
}
else
{
$form = $el.closest('form');
}

return ($form.length && $form.is('[data-xf-init~=draft]'));
},

logRecentEmojiUsage: function(shortname)
{
shortname = $.trim(shortname);

var limit = XF.Feature.has('hiddenscroll') ? 12 : 11, // bit arbitrary but basically a single row on full width displays
value = XF.Cookie.get('emoji_usage'),
recent = value ? value.split(',') : [],
exist = recent.indexOf(shortname);

if (exist !== -1)
{
recent.splice(exist, 1);
}

recent.push(shortname);

if (recent.length > limit)
{
recent = recent.reverse().slice(0, limit).reverse();
}

XF.Cookie.set(
'emoji_usage',
recent.join(','),
new Date(new Date().setFullYear(new Date().getFullYear() + 1))
);

$(document).trigger('recent-emoji:logged');

return recent;
},

getRecentEmojiUsage: function()
{
var value = XF.Cookie.get('emoji_usage'),
recent = value ? value.split(',') : [];

return recent.reverse();
},

getFixedOffsetParent: function($el)
{
do
{
if ($el.css('position') == 'fixed')
{
return $el;
}

$el = $el.parent();
}
while ($el[0] && $el[0].nodeType === 1);

return $(document.documentElement);
},

getFixedOffset: function($el)
{
var offset = $el.offset(),
$offsetParent = XF.getFixedOffsetParent($el);

if ($el.is('html'))
{
return offset;
}

var parentOffset = $offsetParent.offset();
return {
top: offset.top - parentOffset.top,
left: offset.left - parentOffset.left
};
},

autoFocusWithin: function($container, autoFocusSelector, $fallback)
{
var $focusEl = $container.find(autoFocusSelector || '[autofocus]');

if (!$focusEl.length)
{
if (!$focusEl.length && XF.NavDeviceWatcher.isKeyboardNav())
{
$focusEl = $container.find('a, button, :input, [tabindex]')
.filter(':visible')
.not(':disabled, [data-no-auto-focus]')
.first();
}
if (!$focusEl.length)
{
var $form = $container.is('form:not([data-no-auto-focus])')
? $container
: $container.find('form:not([data-no-auto-focus])').first();
if ($form.length)
{
$focusEl = $form.find(':input, button')
.filter(':visible')
.not(':disabled, .select2-hidden-accessible')
.first();
}
}
if (!$focusEl.length && $fallback && $fallback.length)
{
$focusEl = $fallback;
}
if (!$focusEl.length)
{
$container.attr('tabindex', '-1');
$focusEl = $container;
}
}

$focusEl = $focusEl.first();

// focusing will trigger a scroll, so we want to prevent that. We need to maintain all scroll
// values and restore them after focusing.
var scrolls = [],
parent = $focusEl[0].parentNode;

do
{
scrolls.push({
el: parent,
left: parent.scrollLeft,
top: parent.scrollTop
});
}
while (parent = parent.parentNode);

// fairly ugly workaround for bug #149004
// scroll page jump menu into view after keyboard is displayed
// if it is not already visible.
$focusEl.on('focus', function()
{
$(window).on('resize', function()
{
setTimeout(function()
{
if (!XF.isElementVisible($focusEl))
{
$focusEl.get(0).scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
});
$(window).off('resize');
}
}, 50);
});
});

$focusEl.first().autofocus();

var el;
for (var i = 0; i < scrolls.length; i++)
{
el = scrolls[i].el;

if (el.scrollLeft != scrolls[i].left)
{
el.scrollLeft = scrolls[i].left;
}
if (el.scrollTop != scrolls[i].top)
{
el.scrollTop = scrolls[i].top;
}
}
},

bottomFix: function(el)
{
var $fixer = $('.js-bottomFixTarget').first();
if ($fixer)
{
$fixer.append(el);
}
else
{
$(el).css({
position: 'fixed',
bottom: 0
}).appendTo('body');
}
},

addFixedMessage: function(el, extraAttrs)
{
var $message;

$message = $($.parseHTML(
'<div class="fixedMessageBar">'
+ '<div class="fixedMessageBar-inner">'
+ '<div class="fixedMessageBar-message"></div>'
+ '<a class="fixedMessageBar-close" data-close="true" role="button" tabindex="0" aria-label="' + XF.phrase('close') + '"></a>'
+ '</div>'
+ '</div>'
));

$message.find('.fixedMessageBar-message').html(el);

if (extraAttrs)
{
if (extraAttrs.class)
{
$message.addClass(extraAttrs.class);
delete extraAttrs.class;
}
$message.attr(extraAttrs);
}

$message.on('click', '[data-close]', function()
{
$message.removeClassTransitioned('is-active', function()
{
$message.remove();
});
});

XF.bottomFix($message);
$message.addClassTransitioned('is-active');
},

_measureScrollBar: null,

measureScrollBar: function($container, type)
{
if (type == 'height' || type == 'h')
{
type = 'h';
}
else
{
type = 'w';
}

if ($container || XF._measureScrollBar === null)
{
var $measure = $('<div class="scrollMeasure" />');
$measure.appendTo($container || 'body');
var el = $measure[0],
width = el.offsetWidth - el.clientWidth,
height = el.offsetHeight - el.clientHeight,
value = {w: width, h: height};
$measure.remove();

if (!$container)
{
XF._measureScrollBar = value;
}

return value[type];
}
else
{
return XF._measureScrollBar[type];
}
},

windowHeight: function()
{
if (XF.browser.ios || XF.browser.android)
{
// return the effective height, without any browser UI
return window.innerHeight;
}
else
{
return $(window).height();
}
},

pageLoadScrollFix: function()
{
// these browsers support native scroll anchoring
if (XF.Feature.has('overflowanchor'))
{
return;
}

if (!window.location.hash)
{
return;
}

var isScrolled = false;
var onLoad = function()
{
if (isScrolled)
{
return;
}

var hash = window.location.hash.replace(/[^a-zA-Z0-9_-]/g, ''),
$match = hash ? $('#' + hash) : $();

if ($match.length)
{
$match.get(0).scrollIntoView(true);
}
};

if (document.readyState == 'complete')
{
// load has already fired
setTimeout(onLoad, 0);
}
else
{
setTimeout(function()
{
$(window).one('scroll', function(e) {
isScrolled = true;
});
}, 100);

$(window).one('load', onLoad);
}
},

applyJsState: function(currentState, additionalState)
{
currentState = currentState || {};

if (!additionalState)
{
return currentState;
}

var state, applyJsStateFn;

for (state in additionalState)
{
if (additionalState.hasOwnProperty(state) && !currentState[state])
{
if (XF.jsStates.hasOwnProperty(state))
{
if (XF.jsStates[state]())
{
currentState[state] = true;
}
}

}
}

return currentState;
},

jsStates:
{
facebook: function()
{
return this.fbSdk();
},

fbSdk: function()
{
$(document.body).append($('<div id="fb-root" />'));

window.fbAsyncInit = function()
{
FB.init({
version: 'v2.7',
xfbml: true
});
};

XF.loadScript('https://connect.facebook.net/' + XF.getLocale() + '/sdk.js');

return true;
},

twitter: function()
{
// https://dev.twitter.com/web/javascript
window.twttr = (function()
{
var t = window.twttr || {};

if (XF.loadScript("https://platform.twitter.com/widgets.js"))
{
t._e = [];
t.ready = function(f)
{
t._e.push(f);
};
}
return t;
}());

return true;
},

flickr: function()
{
XF.loadScript('https://embedr.flickr.com/assets/client-code.js');

return true;
},

instagram: function()
{
XF.loadScript('https://platform.instagram.com/' + XF.getLocale() + '/embeds.js', function()
{
$(document).on('xf:reinit', function(e, el)
{
if (window.instgrm)
{
instgrm.Embeds.process(el instanceof $ ? el.get(0) : el);
}
});
});
},

reddit: function()
{
XF.loadScript('https://embed.redditmedia.com/widgets/platform.js');

XF.loadScript('https://www.redditstatic.com/comment-embed.js', function()
{
$(document).on('xf:reinit', function(e, el)
{
if (window.rembeddit)
{
rembeddit.init();
}
});
});

return true;
},

reddit_comment: function()
{
return this.reddit();
},

imgur: function()
{
var selector = 'blockquote.imgur-embed-pub';

if (!window.imgurEmbed)
{
window.imgurEmbed = { tasks: $(selector).length };
}

XF.loadScript('//s.imgur.com/min/embed-controller.js', function()
{
$(document).on('xf:reinit', function(e, el)
{
imgurEmbed.tasks += $(selector).length;

for (var i = 0; i < imgurEmbed.tasks; i++)
{
imgurEmbed.createIframe();
imgurEmbed.tasks --;

}
});
});

return true;
},

pinterest: function()
{
XF.loadScript('//assets.pinterest.com/js/pinit.js', function()
{
$(document).on('xf:reinit', function(e, el)
{
PinUtils.build(el instanceof $ ? el.get(0) : el);
});
});

return true;
}
},

getLocale: function()
{
var locale = $('html').attr('lang').replace('-', '_');
if (!locale)
{
locale = 'en_US';
}

return locale;
},

supportsPointerEvents: function()
{
return ('PointerEvent' in window);
},

isEventTouchTriggered: function(e)
{
if (e)
{
if (e.xfPointerType)
{
// this isn't normally exposed to click events, so we have a system to expose this without having
// to manually implement full click emulation
return (e.xfPointerType === 'touch');
}

var oe = e.originalEvent;

if (oe)
{
if (XF.supportsPointerEvents() && oe instanceof PointerEvent)
{
return oe.pointerType === 'touch';
}

if (oe.sourceCapabilities)
{
return oe.sourceCapabilities.firesTouchEvents;
}
}
}

return XF.Feature.has('touchevents');
},

getElEffectiveZIndex: function($reference)
{
var maxZIndex = parseInt($reference.css('z-index'), 10) || 0;

$reference.parents().each(function(i, el)
{
var zIndex = parseInt($(el).css('z-index'), 10);
if (zIndex > maxZIndex)
{
maxZIndex = zIndex;
}
});

return maxZIndex;
},

setRelativeZIndex: function($targets, $reference, offsetAmount, minZIndex)
{
if (!minZIndex)
{
minZIndex = 6; // make sure we go over the default editor stuff
}

var maxZIndex = XF.getElEffectiveZIndex($reference);
if (minZIndex && minZIndex > maxZIndex)
{
maxZIndex = minZIndex;
}

if (offsetAmount === null || typeof offsetAmount === 'undefined')
{
offsetAmount = 0;
}

if (maxZIndex || offsetAmount)
{
$targets.each(function()
{
var $this = $(this),
dataKey = 'base-z-index';
if (typeof $this.data(dataKey) == 'undefined')
{
$this.data(dataKey, parseInt($this.css('z-index'), 10) || 0);
}
$this.css('z-index', $this.data(dataKey) + offsetAmount + maxZIndex);
});
}
else
{
$targets.css('z-index', '');
}
},

adjustHtmlForRte: function(content)
{
content = content.replace(/<img[^>]+>/ig, function(match)
{
if (match.match(/class="([^"]* )?smilie( |")/))
{
var altMatch;
if (altMatch = match.match(/alt="([^"]+)"/))
{
return altMatch[1];
}
}

return match;
});

content = content.replace(/([\w\W]|^)<a\s[^>]*data-user-id="\d+"\s+data-username="([^"]+)"[^>]*>([\w\W]+?)<\/a>/gi,
function(match, prefix, user, username) {
return prefix + (prefix == '@' ? '' : '@') + username.replace(/^@/, '');
}
);

content = content.replace(/(<img\s[^>]*)src="[^"]*"(\s[^>]*)data-url="([^"]+)"/gi,
function(match, prefix, suffix, source) {
return prefix + 'src="' + source + '"' + suffix;
}
);

var $content = $('<div />').html(content);

$content.find('blockquote').each(function(i, quote)
{
var $quote = $(quote);

['attributes', 'quote', 'source'].forEach(function(attr)
{
if (!$quote.attr('data-' + attr))
{
$quote.removeAttr('data-' + attr);
}
});

$quote.find('.bbCodeBlock-title').remove();
});

content = $content.html();

return content;
},

requestAnimationTimeout: function(fn, delay)
{
if (!delay)
{
delay = 0;
}

var raf = window.requestAnimationFrame || function(cb) { return window.setTimeout(cb, 1000 / 60); },
start = Date.now(),
data = {};

function loop()
{
if (Date.now() - start >= delay)
{
fn();
}
else
{
data.id = raf(loop);
}
}

data.id = raf(loop);
data.cancel = function()
{
var caf = window.cancelAnimationFrame || window.clearTimeout;
caf(this.id);
};

return data;
},


/**
* Returns a function replacing the default this object with the supplied context.
*
* jQuery equivalent function has been deprecated in 3.3. If only two arguments passed in
* then we can use fn.bind instead. If we support the spread operator one day we can
* probably just use fn.bind by default.
*
* @param fn
* @param context
* @returns {undefined|Function}
*/
proxy: function(fn, context)
{
var tmp, args;

if (typeof context === "string")
{
tmp = fn[context];
context = fn;
fn = tmp;
}

if (typeof fn !== 'function')
{
return undefined;
}

args = [].slice.call(arguments, 2);

if (args)
{
return function()
{
return fn.apply(context, args.concat([].slice.call(arguments)));
};
}
else
{
return fn.bind(context, args);
}
},

_localLoadTime: null,

getLocalLoadTime: function()
{
if (XF._localLoadTime)
{
return XF._localLoadTime;
}

var localLoadTime,
time = XF.config.time,
$loadCache = $('#_xfClientLoadTime'),
loadVal = $loadCache.val();

if (loadVal && loadVal.length)
{
var parts = loadVal.split(',');
if (parts.length == 2 && parseInt(parts[1], 10) == time.now)
{
localLoadTime = parseInt(parts[0], 10);
$loadCache.val(loadVal); // IE needs this to maintain across multiple views
}
}

if (!localLoadTime)
{
if (window.performance && window.performance.timing && window.performance.timing.requestStart !== 0)
{
var timing = window.performance.timing;

// average between request and response start is likely to be somewhere around when the server started
localLoadTime = Math.floor(
(timing.requestStart + timing.responseStart) / (2 * 1000)
);
}
else
{
localLoadTime = Math.floor(new Date().getTime() / 1000) - 1;
}
$loadCache.val(localLoadTime + ',' + time.now);
}

XF._localLoadTime = localLoadTime;

return localLoadTime;
},

getFutureDate: function(amount, unit)
{
var length = 86400 * 1000; // a day

switch (unit)
{
case 'year':
length *= 365; // a year
break;

case 'month':
length *= 30; // a month
break;
}

length *= amount;

return new Date(Date.now() + length);
},

smoothScroll: function(scrollTo, hash, speed, onlyIfNeeded)
{
if (typeof speed === 'undefined' || speed === null)
{
speed = XF.config.speed.fast;
}

var $content;
var top;

if (scrollTo instanceof $ || typeof scrollTo === 'string')
{
$content = (scrollTo instanceof $) ? scrollTo : $(scrollTo);

if ($content.length)
{
top = $content.offset().top;

var scrollPadding = parseInt($('html').css('scroll-padding-top'), 10);
if (!isNaN(scrollPadding))
{
top -= scrollPadding;
}
}
else
{
top = null;
}

if (hash === true)
{
hash = $content.length ? '#' + $content.attr('id') : null;
}
}
else if (typeof scrollTo === 'number')
{
$content = null;
top = scrollTo;
}

if (top === null)
{
console.error('Invalid scroll position');
return;
}

if (top < 0)
{
top = 0;
}

var pushHash = function()
{
if (hash && 'pushState' in window.history)
{
window.history.pushState(
{},
'',
window.location.toString().replace(/#.*$/, '') + hash
);
}
};

if (onlyIfNeeded)
{
var windowTop = $(window).scrollTop(),
windowBottom = windowTop + $(window).height();

if (top >= windowTop && top <= windowBottom)
{
// already in the window, don't need to scroll
pushHash();
return;
}
}

try
{
pushHash();

$('html, body').animate(
{ scrollTop: top },
speed,
function()
{
if (hash && !window.history.pushState)
{
window.location.hash = hash;
}
}
);
}
catch (e)
{
if (hash)
{
window.location.hash = hash;
}
}
}
});

if (typeof Object.create != 'function')
{
Object.create = (function()
{
var o = function() {};
return function (prototype)
{
o.prototype = prototype;
var result = new o();
o.prototype = null;
return result;
};
})();
}

XF.create = function(props)
{
var fn = function()
{
this.__construct.apply(this, arguments);
};

fn.prototype = Object.create(props);

if (!fn.prototype.__construct)
{
fn.prototype.__construct = function() {};
}
fn.prototype.constructor = fn;

return fn;
};

XF.extend = function(parent, extension)
{
var fn = function()
{
this.__construct.apply(this, arguments);
};
var i;

fn.prototype = Object.create(parent.prototype);

if (!fn.prototype.__construct)
{
fn.prototype.__construct = function() {};
}
fn.prototype.constructor = fn;

if (typeof extension == 'object')
{
if (typeof extension.__backup == 'object')
{
var backup = extension.__backup;
for (i in backup)
{
if (backup.hasOwnProperty(i))
{
if (fn.prototype[backup[i]])
{
throw new Error('Method ' + backup[i] + ' already exists on object. Aliases must be unique.');
}
fn.prototype[backup[i]] = fn.prototype[i];
}
}

delete extension.__backup;
}

for (i in extension)
{
if (extension.hasOwnProperty(i))
{
fn.prototype[i] = extension[i];
}
}
}

return fn;
};

XF.classToConstructor = function(className)
{
var obj = window,
parts = className.split('.'),
i = 0;

for (i = 0; i < parts.length; i++)
{
obj = obj[parts[i]];
}

if (typeof obj != 'function')
{
console.error('%s is not a function.', className);
return false;
}

return obj;
};

XF.Cookie = {
get: function(name)
{
var expr, cookie;

expr = new RegExp('(^| )' + XF.config.cookie.prefix + name + '=([^;]+)(;|$)');
cookie = expr.exec(document.cookie);

if (cookie)
{
return decodeURIComponent(cookie[2]);
}
else
{
return null;
}
},

getEncodedCookieValue: function(name, value, expires, samesite)
{
var c = XF.config.cookie;

return c.prefix + name + '=' + encodeURIComponent(value)
+ (expires === undefined ? '' : ';expires=' + expires.toUTCString())
+ (c.path  ? ';path=' + c.path : '')
+ (c.domain ? ';domain=' + c.domain : '')
+ (samesite ? ';samesite=' + samesite : '')
+ (c.secure ? ';secure' : '');
},

getEncodedCookieValueSize: function(name, value, expires, samesite)
{
return this.getEncodedCookieValue(name, value, expires, samesite).length;
},

set: function(name, value, expires, samesite)
{
document.cookie = this.getEncodedCookieValue(
name, value, expires, samesite
);
},

getJson: function(name)
{
var data = this.get(name);
if (!data)
{
return {};
}

try
{
return $.parseJSON(data) || {};
}
catch (e)
{
return {};
}
},

setJson: function(name, value, expires)
{
this.set(name, JSON.stringify(value), expires);
},

remove: function(name)
{
var c = XF.config.cookie;

document.cookie = c.prefix + name + '='
+ (c.path  ? '; path=' + c.path : '')
+ (c.domain ? '; domain=' + c.domain : '')
+ (c.secure ? '; secure' : '')
+ '; expires=Thu, 01-Jan-70 00:00:01 GMT';
},

supportsExpiryDate: function()
{
return true;
}
};

XF.LocalStorage = {
getKeyName: function(name)
{
return XF.config.cookie.prefix + name;
},

get: function(name)
{
var value  = null;

try
{
value = window.localStorage.getItem(this.getKeyName(name));
}
catch (e) {}

if (value === null)
{
var localStorage = this.getFallbackValue();
if (localStorage && localStorage.hasOwnProperty(name))
{
value = localStorage[name];
}
}

return value;
},

getJson: function(name)
{
var data = this.get(name);
if (!data)
{
return {};
}

try
{
return $.parseJSON(data) || {};
}
catch (e)
{
return {};
}
},

set: function(name, value, allowFallback)
{
try
{
window.localStorage.setItem(this.getKeyName(name), value);
}
catch (e)
{
if (allowFallback)
{
var localStorage = this.getFallbackValue();
localStorage[name] = value;
this.updateFallbackValue(localStorage);
}
}
},

setJson: function(name, value, allowFallback)
{
this.set(name, JSON.stringify(value), allowFallback);
},

remove: function(name)
{
try
{
window.localStorage.removeItem(this.getKeyName(name));
}
catch (e) {}

var localStorage = this.getFallbackValue();
if (localStorage && localStorage.hasOwnProperty(name))
{
delete localStorage[name];
this.updateFallbackValue(localStorage);
}
},

getFallbackValue: function()
{
var value = XF.Cookie.get('ls');
if (value)
{
try
{
value = $.parseJSON(value);
}
catch (e)
{
value = {};
}
}

return value || {};
},

updateFallbackValue: function(newValue)
{
if ($.isEmptyObject(newValue))
{
XF.Cookie.remove('ls');
}
else
{
XF.Cookie.set('ls', JSON.stringify(newValue));
}
},

supportsExpiryDate: function()
{
return false;
}
};

XF.CrossTab = (function()
{
var listeners = {},
listening = false,
communicationKey = '__crossTab',
activeEvent;

function handleEvent(e)
{
var expectedKey = XF.LocalStorage.getKeyName(communicationKey);
if (e.key !== expectedKey)
{
return;
}

var json;

try
{
json = $.parseJSON(e.newValue);
}
catch (e)
{
return;
}

if (!json || !json.event)
{
return;
}

var event = json.event,
data = json.data || null,
activeListeners = listeners[event];
if (!activeListeners)
{
return;
}

activeEvent = event;

for (var i = 0; i < activeListeners.length; i++)
{
activeListeners[i](data);
}

activeEvent = null;
}

function on(event, callback)
{
if (!listeners[event])
{
listeners[event] = [];
}

listeners[event].push(callback);

if (!listening)
{
listening = true;
window.addEventListener('storage', handleEvent);
}
}

function trigger(event, data, forceCall)
{
if (!forceCall && activeEvent && activeEvent == event)
{
// this is to help prevent infinite loops where the code that reacts to an event
// is the same code that gets called by the event
return;
}

XF.LocalStorage.setJson(communicationKey, {
event: event,
data: data,
'_': new Date() + Math.random() // forces the event to fire
});
}

return {
on: on,
trigger: trigger
}
})();

XF.Breakpoint = (function()
{
var val = null,
sizes = ['narrow', 'medium', 'wide', 'full'];

function current()
{
return val;
}

function isNarrowerThan(test)
{
for (var i = 0; i < sizes.length; i++)
{
if (test == sizes[i])
{
return false;
}

if (val == sizes[i])
{
return true;
}
}

return false;
}

function isAtOrNarrowerThan(test)
{
return (val == test || isNarrowerThan(test));
}

function isWiderThan(test)
{
var afterTest = false;

for (var i = 0; i < sizes.length; i++)
{
if (test == sizes[i])
{
afterTest = true;
continue;
}

if (val == sizes[i])
{
return afterTest;
}
}

return false;
}

function isAtOrWiderThan(test)
{
return (val == test || isWiderThan(test));
}

function refresh()
{
var newVal = window.getComputedStyle($('html')[0], ':after').getPropertyValue('content').replace(/\"/g, '');

if (val)
{
if (newVal != val)
{
var oldVal = val;
val = newVal;

$(document).trigger('breakpoint:change', [oldVal, newVal]);
}
}
else
{
// initial load, don't trigger anything
val = newVal;
}

return val;
}

refresh();
$(window).onPassive('resize', refresh);

return {
current: current,
refresh: refresh,
isNarrowerThan: isNarrowerThan,
isAtOrNarrowerThan: isAtOrNarrowerThan,
isWiderThan: isWiderThan,
isAtOrWiderThan: isAtOrWiderThan
};
})();

XF.JobRunner = (function()
{
var manualRunning = false,
manualOnlyIds = [],
manualXhr,
manualOverlay = null,
autoBlockingRunning = 0,
autoBlockingXhr,
autoBlockingOverlay = null;

var runAuto = function()
{
$.ajax({
url: XF.canonicalizeUrl('job.php'),
type: 'post',
cache: false,
dataType: 'json',
global: false
}).always(function(data)
{
if (data && data.more)
{
setTimeout(runAuto, 100);
}
});
};

// ####### AUTO BLOCKING ###########

var runAutoBlocking = function(onlyIds, message)
{
if (typeof onlyIds === 'number')
{
onlyIds = [onlyIds];
}
else if (!Array.isArray(onlyIds))
{
return;
}

if (!onlyIds.length)
{
return;
}

autoBlockingRunning++;
getAutoBlockingOverlay().show();

if (!message)
{
message = XF.phrase('processing...');
}
$('#xfAutoBlockingJobStatus').text(message);

runAutoBlockingRequest(onlyIds);
};

var runAutoBlockingRequest = function(onlyIds)
{
autoBlockingXhr = XF.ajax(
'post',
XF.canonicalizeUrl('job.php'),
{ only_ids: onlyIds },
function(data)
{
if (data.more && data.ids && data.ids.length)
{
if (data.status)
{
$('#xfAutoBlockingJobStatus').text(data.status);
}

setTimeout(function()
{
runAutoBlockingRequest(data.ids);
}, 0);
}
else
{
stopAutoBlocking();
if (data.moreAuto)
{
setTimeout(runAuto, 100);
}
}
},
{ skipDefault: true }
).fail(stopAutoBlocking);
};

var stopAutoBlocking = function()
{
if (autoBlockingOverlay)
{
autoBlockingOverlay.hide();
}

autoBlockingRunning--;
if (autoBlockingRunning < 0)
{
autoBlockingRunning = 0;
}

if (autoBlockingRunning == 0)
{
$(document).trigger('job:auto-blocking-complete');
triggerBlockingComplete();
}

if (autoBlockingXhr)
{
autoBlockingXhr.abort();
}
autoBlockingXhr = null;
};

var getAutoBlockingOverlay = function()
{
if (!autoBlockingOverlay)
{
autoBlockingOverlay = getModalJobOverlay('xfAutoBlockingJobStatus');
}
return autoBlockingOverlay;
};

// ################# MANUAL ###############

var runManual = function(onlyId)
{
var url = XF.config.job.manualUrl;
if (!url)
{
return;
}

if (onlyId === null)
{
manualOnlyIds = null;
}
else
{
var manualOnlyIds = manualOnlyIds || [];
if (typeof onlyId === 'number')
{
manualOnlyIds.push(onlyId);
}
else if (Array.isArray(onlyId))
{
manualOnlyIds.push.apply(manualOnlyIds, onlyId);
}
}

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

getManualOverlay().show();

var runJob = function(runOnlyId) {
manualXhr = XF.ajax('post', url, runOnlyId ? {only_id: runOnlyId} : null, function(data)
{
if (data.jobRunner)
{
$('#xfManualJobStatus').text(data.jobRunner.status || XF.phrase('processing...'));

setTimeout(function ()
{
runJob(runOnlyId);
}, 0);
}
else
{
runNext();
}
}, {skipDefault: true}).fail(stopManual);
};
var runNext = function()
{
var ids = manualOnlyIds;
if (Array.isArray(manualOnlyIds) && manualOnlyIds.length == 0)
{
stopManual();
}
else
{
runJob(manualOnlyIds ? manualOnlyIds.shift() : null);
}
};
runNext();
};

var stopManual = function()
{
if (manualOverlay)
{
manualOverlay.hide();
}

manualOnlyIds = [];
manualRunning = false;
$(document).trigger('job:manual-complete');
triggerBlockingComplete();

if (manualXhr)
{
manualXhr.abort();
}
manualXhr = null;
};

var getManualOverlay = function()
{
if (!manualOverlay)
{
manualOverlay = getModalJobOverlay('xfManualJobStatus');
}
return manualOverlay;
};

// ################# HELPERS ###########

var getModalJobOverlay = function(statusId)
{
var $overlay = XF.getOverlayHtml({
title: XF.phrase('processing...'),
dismissible: false,
html: '<div class="blockMessage"><span id="' + statusId + '">'
+ XF.phrase('processing...') + '</span></div>'
});
return new XF.Overlay($overlay, {
backdropClose: false,
keyboard: false
});
};

var triggerBlockingComplete = function()
{
if (!isBlockingJobRunning())
{
$(document).trigger('job:blocking-complete');
}
};

var isBlockingJobRunning = function()
{
return (manualRunning || autoBlockingRunning > 0);
};

return {
isBlockingJobRunning: isBlockingJobRunning,
runAuto: runAuto,
runAutoBlocking: runAutoBlocking,
runManual: runManual,
stopManual: stopManual,
getManualOverlay: getManualOverlay
}
})();

XF.Loader = (function()
{
var loadedCss = XF.config.css,
loadedJs = XF.config.js;

var load = function(js, css, onComplete)
{
js = js || [];
css = css || [];

var loadJs = [], loadCss = [], i;

for (i = 0; i < js.length; i++)
{
if (!loadedJs.hasOwnProperty(js[i]))
{
loadJs.push(js[i]);
}
}
for (i = 0; i < css.length; i++)
{
if (!loadedCss.hasOwnProperty(css[i]))
{
loadCss.push(css[i]);
}
}

var totalRemaining = (loadJs.length ? 1 : 0) + (loadCss.length ? 1 : 0),
markFinished = function()
{
totalRemaining--;
if (totalRemaining == 0 && onComplete)
{
onComplete();
}
};

if (!totalRemaining)
{
if (onComplete)
{
onComplete();
}
return;
}

if (loadJs.length)
{
XF.loadScripts(loadJs, function()
{
$.each(loadJs, function (i, jsFile)
{
loadedJs[jsFile] = true;
});
markFinished();
});
}

if (loadCss.length)
{
var cssUrl = XF.config.url.css;
if (cssUrl)
{
cssUrl = cssUrl.replace('__SENTINEL__', loadCss.join(','));

$.ajax({
type: 'GET',
url: cssUrl,
cache: true,
global: false,
dataType: 'text',
success: function (cssText)
{
var baseHref = XF.config.url.basePath;
if (baseHref)
{
cssText = cssText.replace(
/(url\(("|')?)([^"')]+)(("|')?\))/gi,
function (all, front, null1, url, back, null2)
{
if (!url.match(/^([a-z]+:|\/)/i))
{
url = baseHref + url;
}
return front + url + back;
}
);
}

$('<style>' + cssText + '</style>').appendTo('head');
}
}).always(function ()
{
$.each(loadCss, function (i, stylesheet)
{
loadedCss[stylesheet] = true;
});
markFinished();
});
}
else
{
console.error('No CSS URL so cannot dynamically load CSS');
markFinished();
}
}
};

return {
load: load,
loadCss: function(css, onComplete) { load([], css, onComplete); },
loadJs: function(js, onComplete) { load(js, [], onComplete); }
};
})();

XF.ClassMapper = XF.create({
_map: {},
_toExtend: {},

add: function(identifier, className)
{
this._map[identifier] = className;
},

extend: function(identifier, extension)
{
var obj = this.getObjectFromIdentifier(identifier);
if (obj)
{
obj = XF.extend(obj, extension);
this._map[identifier] = obj;
}
else
{
if (!this._toExtend[identifier])
{
this._toExtend[identifier] = [];
}
this._toExtend[identifier].push(extension);
}
},

getObjectFromIdentifier: function(identifier)
{
var record = this._map[identifier],
extensions = this._toExtend[identifier];

if (!record)
{
return null;
}

if (typeof record == 'string')
{
record = XF.classToConstructor(record);
if (extensions)
{
for (var i = 0; i < extensions.length; i++)
{
record = XF.extend(record, extensions[i]);
}

delete this._toExtend[identifier];
}

this._map[identifier] = record;
}

return record;
}
});

XF.ActionIndicator = (function()
{
var activeCounter = 0, $indicator;

var initialize = function()
{
$(document).on({
ajaxStart: show,
'xf:action-start': show,
ajaxStop: hide,
'xf:action-stop': hide
});
};

var show = function()
{
activeCounter++;
if (activeCounter != 1)
{
return;
}

if (!$indicator)
{
$indicator = $(
'<span class="globalAction">'
+ '<span class="globalAction-bar"></span>'
+ '<span class="globalAction-block"><i></i><i></i><i></i></span>'
+ '</span>'
).appendTo('body');
}

$indicator.addClassTransitioned('is-active');
};

var hide = function()
{
activeCounter--;
if (activeCounter > 0)
{
return;
}

activeCounter = 0;

if ($indicator)
{
$indicator.removeClassTransitioned('is-active');
}
};

return {
initialize: initialize,
show: show,
hide: hide
}
})();

XF.DynamicDate = (function()
{
var localLoadTime,
serverLoadTime,
todayStart,
todayDow,
yesterdayStart,
tomorrowStart,
weekStart,
initialized = false,
interval,
futureInterval;

var startInterval = function()
{
interval = setInterval(function()
{
refresh(document);
}, 20 * 1000);
};

var initialize = function()
{
if (initialized)
{
return;
}
initialized = true;

var time = XF.config.time;

localLoadTime = XF.getLocalLoadTime();
serverLoadTime = time.now;
todayStart = time.today;
todayDow = time.todayDow;
yesterdayStart = time.yesterday;
tomorrowStart = time.tomorrow;
weekStart = time.week;

if (document.hidden !== undefined)
{
if (!document.hidden)
{
startInterval();
}

$(document).on('visibilitychange', function()
{
if (document.hidden)
{
clearInterval(interval);
}
else
{
startInterval();
refresh(document);
}
});
}
else
{
startInterval();
}
};

var refresh = function(root)
{
if (!initialized)
{
this.initialize();
}

var $els = $(root).find('time[data-time]'),
length = $els.length,
now = Math.floor(new Date().getTime() / 1000),
openLength = now - localLoadTime,
todayStartObj = new Date(),
el, $el, interval, futureInterval, dynType, thisTime;

todayStartObj.setHours(0, 0, 0, 0);

if (serverLoadTime + openLength > tomorrowStart)
{
// day has changed, need to adjust
todayDow = todayStartObj.getDay();
tomorrowStart = getRelativeTimestamp(todayStartObj, 1);
todayStart = getRelativeTimestamp(todayStartObj, 0);
yesterdayStart = getRelativeTimestamp(todayStartObj, -1);
weekStart = getRelativeTimestamp(todayStartObj, -6);
}

for (var i = 0; i < length; i++)
{
el = $els[i];
$el = $(el);
thisTime = parseInt(el.getAttribute('data-time'), 10);
interval = (serverLoadTime - thisTime) + openLength;
dynType = el.xfDynType;

if (interval < -2)
{
// date in the future, note that -2 is a bit of fudging as times might be very close and our local
// load time may not jive 100% with the server

futureInterval = thisTime - (serverLoadTime + openLength);

if (futureInterval < 60)
{
if (dynType != 'futureMoment')
{
$el.text(XF.phrase('in_a_moment'));
el.xfDynType = 'futureMoment';
}
}
else if (futureInterval < 120)
{
if (dynType != 'futureMinute')
{
$el.text(XF.phrase('in_a_minute'));
el.xfDynType = 'futureMinute';
}
}
else if (futureInterval < 3600)
{
var minutes = Math.floor(futureInterval / 60);
if (dynType !== 'futureMinutes' + minutes)
{
$el.text(XF.phrase('in_x_minutes', {
'{minutes}': minutes
}));
el.xfDynType = 'futureMinutes' + minutes;
}
}
else if (thisTime < tomorrowStart)
{
if (dynType != 'latertoday')
{
$el.text(XF.phrase('later_today_at_x', {
'{time}': $el.attr('data-time-string')
}));
el.xfDynType = 'latertoday';
}
}
else if (thisTime < getRelativeTimestamp(todayStartObj, 2))
{
if (dynType != 'tomorrow')
{
$el.text(XF.phrase('tomorrow_at_x', {
'{time}': $el.attr('data-time-string')
}));
el.xfDynType = 'tomorrow';
}
}
else if (futureInterval < (7 * 86400)) // this doesn't account for DST shifts, but meh...
{
// no need to change anything
el.xfDynType = 'future';
}
else
{
// after the next week
if ($el.attr('data-full-date'))
{
$el.text(XF.phrase('date_x_at_time_y', {
'{date}': $el.attr('data-date-string'), // must use attr for string value
'{time}': $el.attr('data-time-string') // must use attr for string value
}));
}
else
{
$el.text($el.attr('data-date-string')); // must use attr for string value
}

el.xfDynType = 'future';
}
}
else if (interval <= 60)
{
if (dynType !== 'moment')
{
$el.text(XF.phrase('a_moment_ago'));
el.xfDynType = 'moment';
}
}
else if (interval <= 120)
{
if (dynType !== 'minute')
{
$el.text(XF.phrase('one_minute_ago'));
el.xfDynType = 'minute';
}
}
else if (interval < 3600)
{
var minutes = Math.floor(interval / 60);
if (dynType !== 'minutes' + minutes)
{
$el.text(XF.phrase('x_minutes_ago', {
'{minutes}': minutes
}));
el.xfDynType = 'minutes' + minutes;
}
}
else if (thisTime >= todayStart)
{
if (dynType !== 'today')
{
$el.text(XF.phrase('today_at_x', {
'{time}': $el.attr('data-time-string') // must use attr for string value
}));
el.xfDynType = 'today';
}
}
else if (thisTime >= yesterdayStart)
{
if (dynType !== 'yesterday')
{
$el.text(XF.phrase('yesterday_at_x', {
'{time}': $el.attr('data-time-string') // must use attr for string value
}));
el.xfDynType = 'yesterday';
}
}
else if (thisTime >= weekStart)
{
if (dynType !== 'week')
{
$el.text(XF.phrase('day_x_at_time_y', {
'{day}': XF.phrase('day' + new Date(thisTime * 1000).getDay()),
'{time}': $el.attr('data-time-string') // must use attr for string value
}));
el.xfDynType = 'week';
}
}
else
{
if (dynType !== 'old')
{
if ($el.attr('data-full-date'))
{
$el.text(XF.phrase('date_x_at_time_y', {
'{date}': $el.attr('data-date-string'), // must use attr for string value
'{time}': $el.attr('data-time-string') // must use attr for string value
}));
}
else
{
$el.text($el.attr('data-date-string')); // must use attr for string value
}

el.xfDynType = 'old';
}
}
}
};

var getRelativeTimestamp = function(srcDateObj, offsetDays)
{
var dateObj = new Date(srcDateObj.valueOf());

return Math.floor(dateObj.setFullYear(
srcDateObj.getFullYear(),
srcDateObj.getMonth(),
srcDateObj.getDate() + offsetDays
) / 1000);
};

return {
initialize: initialize,
refresh: refresh
};
})();

XF.KeepAlive = (function()
{
var url,
crossTabEvent,
initialized = false,
baseTimerDelay = 50 * 60, // in seconds, 50 minutes
jitterRange = 120,
interval;

var initialize = function()
{
if (initialized)
{
return;
}

if (!XF.config.url.keepAlive || !XF.config.url.keepAlive.length)
{
return;
}
initialized = true;

url = XF.config.url.keepAlive;
crossTabEvent = 'keepAlive' + XF.stringHashCode(url);

resetTimer();

XF.CrossTab.on(crossTabEvent, applyChanges);

if (window.performance && window.performance.navigation)
{
var navType = window.performance.navigation.type;
if (navType == 0 || navType == 1)
{
// navigate or reload, we have the most recent data from the server so pass that on
XF.CrossTab.trigger(crossTabEvent, {
csrf: XF.config.csrf,
time: XF.config.time.now,
user_id: XF.config.userId
});
}
}

if (!XF.Cookie.get('csrf'))
{
refresh();
}
};

var resetTimer = function()
{
var rand = function(min, max)
{
return Math.floor(Math.random() * (max - min + 1)) + min;
};

var delay = baseTimerDelay + rand(-jitterRange, jitterRange); // +/- jitter to prevent opened tabs sticking together
if (delay < jitterRange)
{
delay = jitterRange;
}

if (interval)
{
clearInterval(interval);
}
interval = setInterval(refresh, delay * 1000);

// note that while this should be reset each time it's triggered, using an interval ensures
// that it runs again even if there's an error
};

var offlineCount = 0,
offlineDelayTimer;

var refresh = function()
{
if (!initialized)
{
return;
}

// if we're offline, delay testing by 30 seconds a few times. This tries to maintain the keep alive
// when there are temporary network drops or if waking up from sleep and the network isn't ready yet.
if (window.navigator.onLine === false)
{
offlineCount++;

if (offlineCount <= 5)
{
offlineDelayTimer = setTimeout(refresh, 30);
}
}

offlineCount = 0;
clearTimeout(offlineDelayTimer);

$.ajax({
url: XF.canonicalizeUrl(url),
data: {
_xfResponseType: 'json',
_xfToken: XF.config.csrf
},
type: 'post',
cache: false,
dataType: 'json',
global: false
}).done(function(data)
{
if (data.status != 'ok')
{
return;
}

applyChanges(data);
XF.CrossTab.trigger(crossTabEvent, data);
});
};

var applyChanges = function(data)
{
if (data.csrf)
{
XF.config.csrf = data.csrf;
$('input[name=_xfToken]').val(data.csrf);
}

if (typeof data.user_id !== 'undefined')
{
var $activeChangeMessage = $('.js-activeUserChangeMessage');

if (data.user_id != XF.config.userId && !$activeChangeMessage.length)
{
XF.addFixedMessage(XF.phrase('active_user_changed_reload_page'), {
'class': 'js-activeUserChangeMessage'
});
}
if (data.user_id == XF.config.userId && $activeChangeMessage.length)
{
$activeChangeMessage.remove();
}
}

resetTimer();
};

return {
initialize: initialize,
refresh: refresh
};
})();

XF.History = (function()
{
var windowHistory = window.history,
activeState = windowHistory.state,
activeUrl = window.location.href,
handlers = [];

function initialize()
{
window.addEventListener('popstate', function(e)
{
var state = e.state,
handled = false;

for (var i = 0; i < handlers.length; i++)
{
if (handlers[i](state, activeState, activeUrl))
{
handled = true;
}
}

if (!handled && activeUrl.replace(/#.*$/, '') !== window.location.href.replace(/#.*$/, ''))
{
// nothing handled this and the URL changed, so this should probably have a different
// document loaded so trigger a reload
window.location.reload();
}

updateActiveState(state);
});
}

function updateActiveState(state)
{
activeState = state;
activeUrl = window.location.href;
}

function store(method, state, title, url)
{
windowHistory[method](state, title, url);
updateActiveState(state);
}

return {
initialize: initialize,
handle: function (callback)
{
handlers.push(callback);
},
push: function(state, title, url)
{
store('pushState', state, title, url);
},
replace: function replace(state, title, url)
{
store('replaceState', state, title, url);
},
go: function(delta)
{
windowHistory.go(delta);
}
};
})();

// ################################## LINK PROXY WATCHER ###########################################

XF.LinkWatcher = (function()
{
var proxyInternals = false;

var proxyLinkClick = function(e)
{
var $this = $(this),
proxyHref = $this.data('proxy-href'),
lastEvent = $this.data('proxy-handler-last');

if (!proxyHref)
{
return;
}

// we may have a direct click event and a bubbled event. Ensure they don't both fire.
if (lastEvent && lastEvent == e.timeStamp)
{
return;
}
$this.data('proxy-handler-last', e.timeStamp);

$.ajax({
url: XF.canonicalizeUrl(proxyHref),
data: { _xfResponseType: 'json', referrer: window.location.href.replace(/#.*$/, '') },
type: 'post',
cache: false,
dataType: 'json',
global: false
});
};

var initLinkProxy = function()
{
var selector = 'a[data-proxy-href]',
dataAttrAttacher = 'proxy-handler';

if (!proxyInternals)
{
selector += ':not(.link--internal)';
}

$(document)
.on('click', selector, proxyLinkClick)
.on('focusin', selector, function(e)
{
// This approach is taken because middle click events do not bubble. This is a way of
// getting the equivalent of event bubbling on middle clicks in Chrome.
var $this = $(this);
if ($this.data(dataAttrAttacher))
{
return;
}

$this.data(dataAttrAttacher, true)
.click(proxyLinkClick);
});
};

var externalLinkClick = function(e)
{
if (!XF.config.enableRtnProtect)
{
return;
}

if (e.isDefaultPrevented())
{
return;
}

var $this = $(this),
href = $this.attr('href'),
lastEvent = $this.data('blank-handler-last');
if (!href)
{
return;
}

if (href.match(/^[a-z]:/i) && !href.match(/^https?:/i))
{
// ignore canonical but non http(s) links
return;
}

if ($this.is('[data-fancybox]'))
{
// don't do anything here as the lightbox will take over (and this is a trusted link)
return;
}

// if noopener is supported and in use, then use that instead
if ($this.is('[rel~=noopener]'))
{
var browser = XF.browser;
if (
(browser.chrome && browser.version >= 49)
|| (browser.mozilla && browser.version >= 52)
|| (browser.safari && browser.version >= 11) // may be supported in some 10.x releases
// Edge and IE don't support it yet
)
{
return;
}
}

if ($this.closest('[contenteditable=true]').length)
{
return;
}

href = XF.canonicalizeUrl(href);

var regex = new RegExp('^[a-z]+://' + location.host + '(/|$|:)', 'i');
if (regex.test(href))
{
// if the link is local, then don't do the special processing
return;
}

// we may have a direct click event and a bubbled event. Ensure they don't both fire.
if (lastEvent && lastEvent == e.timeStamp)
{
return;
}

$this.data('blank-handler-last', e.timeStamp);

var ua = navigator.userAgent,
isOldIE = ua.indexOf('MSIE') !== -1,
isSafari = ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') == -1,
isGecko = ua.indexOf('Gecko/') !== -1;

if (e.shiftKey && isGecko)
{
// Firefox doesn't trigger when holding shift. If the code below runs, it will force
// opening in a new tab instead of a new window, so stop. Note that Chrome still triggers here,
// but it does open in a new window anyway so we run the normal code.
return;
}
if (isSafari && (e.shiftKey || e.altKey))
{
// this adds to reading list or downloads instead of opening a new tab
return;
}
if (isOldIE)
{
// IE has mitigations for this and this blocks referrers
return;
}

// now run the opener clearing

if (isSafari)
{
// Safari doesn't work with the other approach
// Concept from: https://github.com/danielstjules/blankshield
var $iframe, iframeDoc, $script;

$iframe = $('<iframe style="display: none" />').appendTo(document.body);
iframeDoc = $iframe[0].contentDocument || $iframe[0].contentWindow.document;

iframeDoc.__href = href; // set this so we don't need to do an eval-type thing

$script = $('<script />', iframeDoc);
$script[0].text = 'window.opener=null;' +
'window.parent=null;window.top=null;window.frameElement=null;' +
'window.open(document.__href).opener = null;';

iframeDoc.body.appendChild($script[0]);
$iframe.remove();
}
else
{
// use this approach for the rest to maintain referrers when possible
var w = window.open(href);

try
{
// this can potentially fail, don't want to break
w.opener = null;
}
catch (e) {}
}

e.preventDefault();
};

var initExternalWatcher = function()
{
var selector = 'a[target=_blank]',
dataAttrAttacher = 'blank-handler';

$(document)
.on('click', selector, externalLinkClick)
.on('focusin', selector, function(e)
{
// This approach is taken because middle click events do not bubble. This is a way of
// getting the equivalent of event bubbling on middle clicks in Chrome.
var $this = $(this);
if ($this.data(dataAttrAttacher))
{
return;
}

$this.data(dataAttrAttacher, true)
.click(externalLinkClick);
});
};

return {
initLinkProxy: initLinkProxy,
initExternalWatcher: initExternalWatcher
};
})();

// ################################## IGNORED CONTENT WATCHER ###########################################

XF._IgnoredWatcher = XF.create({
options: {
container: 'body',
ignored: '.is-ignored',
link: '.js-showIgnored'
},

$container: null,
authors: [],
shown: false,

__construct: function(options)
{
this.options = $.extend(true, {}, this.options, options || {});

var $container = $(this.options.container);
this.$container = $container;

this.updateState();

$container.on('click', this.options.link, XF.proxy(this, 'show'));
},

refresh: function($el)
{
if (!this.$container.find($el).length)
{
// el is not in our search area
return;
}

if (this.shown)
{
// already showing, so apply that here as well
this.show();
}
else
{
this.updateState();
}
},

updateState: function()
{
if (this.shown)
{
// already showing
return;
}

var $ignored = this.getIgnored(),
authors = [];

if (!$ignored.length)
{
// nothing to do - assume hidden by default
return;
}

$ignored.each(function()
{
var author = $(this).data('author');
if (author && $.inArray(author, authors) === -1)
{
authors.push(author);
}
});

if (authors.length)
{
var textReplace = { names: authors.join(', ') };

this.getLinks().each(function()
{
var $link = $(this),
title = $link.attr('title');
if (title)
{
$link.attr('title', Mustache.render(title, textReplace))
.removeClass('is-hidden');
}
});
}
else
{
this.getLinks().each(function()
{
$(this).removeAttr('title').removeClass('is-hidden');
});
}
},

getIgnored: function()
{
return this.$container.find(this.options.ignored);
},

getLinks: function()
{
return this.$container.find(this.options.link);
},

show: function()
{
this.shown = true;
this.getIgnored().removeClass('is-ignored');
this.getLinks().addClass('is-hidden');
},

initializeHash: function()
{
if (window.location.hash)
{
var cleanedHash = window.location.hash.replace(/[^\w_#-]/g, '');
if (cleanedHash === '#')
{
return;
}

var $jump = $(cleanedHash),
ignoredSel = this.options.ignored,
$ignored;

if ($jump.is(ignoredSel))
{
$ignored = $jump;
}
else
{
$ignored = $jump.closest(ignoredSel);
}

if ($ignored && $ignored.length)
{
$ignored.removeClass('is-ignored');
$jump.get(0).scrollIntoView(true);
}
}
}
});
XF.IgnoreWatcher = new XF._IgnoredWatcher();

XF.BrowserWarning = (function()
{
function display()
{
var display = false;

if (XF.browser.msie)
{
display = true;
}
else if (XF.browser.edge && parseInt(XF.browser.version) < 18)
{
display = true;
}

var $warning = $('.js-browserWarning');

if (display)
{
$warning.show();
}
else
{
$warning.remove();
}
}

return {
display: display
};
})();

// ################################ ACTION BAR HANDLER ##########################################

XF.MultiBar = XF.create({
options: {
role: null,
focusShow: false,
className: '',
fastReplace: false
},

$container: null,
$multiBar: null,
shown: false,

__construct: function(content, options)
{
this.options = $.extend(true, {}, this.options, options || {});

this.$multiBar = content instanceof $ ? content : $($.parseHTML(content));
this.$multiBar
.attr('role', this.options.role || 'dialog')
.attr('aria-hidden', 'true')
.on('multibar:hide', XF.proxy(this, 'hide'))
.on('multibar:show', XF.proxy(this, 'show'));

this.$container = $('<div class="multiBar-container" />');
this.$container
.html(this.$multiBar)
.data('multibar', this)
.addClass(this.options.className)
this.$container.xfUniqueId();

this.$container.appendTo('body');
//XF.bottomFix(this.$container);
XF.activate(this.$container);

XF.MultiBar.cache[this.$container.attr('id')] = this;
},

show: function()
{
if (this.shown)
{
return;
}

this.shown = true;
this.$multiBar.attr('aria-hidden', 'false');

$('.p-pageWrapper').addClass('has-multiBar');

if (this.options.fastReplace)
{
this.$multiBar.css('transition-duration', '0s');
}

var self = this;
this.$container.appendTo('body');
this.$multiBar.addClassTransitioned('is-active', function()
{
if (self.options.focusShow)
{
var $autoFocusFallback = self.$multiBar.find('.js-multiBarClose');
XF.autoFocusWithin(self.$multiBar.find('.multiBar-content'), null, $autoFocusFallback);
}

self.$container.trigger('multibar:shown');
XF.layoutChange();
});

if (this.options.fastReplace)
{
this.$multiBar.css('transition-duration', '');
}

this.$container.trigger('multibar:showing');

XF.layoutChange();
},

hide: function()
{
if (!this.shown)
{
return;
}

this.shown = false;
this.$multiBar.attr('aria-hidden', 'true');

var self = this;
this.$multiBar.removeClassTransitioned('is-active', function()
{
$('.p-pageWrapper').removeClass('has-multiBar');

self.$container.trigger('multibar:hidden');
XF.layoutChange();
});

this.$container.trigger('multibar:hiding');

XF.layoutChange();
},

toggle: function(forceState)
{
var newState = (forceState === null ? !this.shown : forceState);

newState ? this.show() : this.hide();
},

destroy: function()
{
var id = this.$container.attr('id'),
cache = XF.MultiBar.cache;

this.$container.remove();
if (cache.hasOwnProperty(id))
{
delete cache[id];
}
},

on: function()
{
this.$container.on.apply(this.$container, arguments);
},

getContainer: function()
{
return this.$container;
},

getMultiBar: function()
{
return this.$multiBar;
}
});
XF.MultiBar.cache = {};

XF.showMultiBar = function($html, options)
{
var MultiBar = new XF.MultiBar($html, options);
MultiBar.show();
return MultiBar;
};

XF.loadMultiBar = function(url, data, options, multiBarOptions)
{
if ($.isFunction(options))
{
options = {init: options};
}

options = $.extend({
cache: false,
beforeShow: null,
afterShow: null,
onRedirect: null,
init: null,
show: true
}, options || {});

var show = function(MultiBar)
{
if (options.beforeShow)
{
var e = $.Event();
options.beforeShow(MultiBar, e);
if (e.isDefaultPrevented())
{
return;
}
}

if (options.show)
{
MultiBar.show();
}

if (options.afterShow)
{
var e = $.Event();
options.afterShow(MultiBar, e);
if (e.isDefaultPrevented())
{
return;
}
}
};

if (options.cache && XF.loadMultiBar.cache[url])
{
show(XF.loadMultiBar.cache[url]);
return;
}

var multiBarAjaxHandler = function(data)
{
if (data.redirect)
{
if (options.onRedirect)
{
options.onRedirect(data, multiBarAjaxHandler);
}
else
{
XF.ajax('get', data.redirect, function(data)
{
multiBarAjaxHandler(data);
});
}
}

if (!data.html)
{
return;
}

XF.setupHtmlInsert(data.html, function($html, container, onComplete)
{
var MultiBar = new XF.MultiBar(XF.getMultiBarHtml({
html: $html,
title: container.title || container.h1
}),  multiBarOptions);

if (options.init)
{
options.init(MultiBar);
}

if (!options.cache)
{
MultiBar.on('multibar:hidden', function()
{
MultiBar.destroy();
});
}

onComplete();

if (options.cache)
{
XF.loadMultiBar.cache[url] = MultiBar;
}

show(MultiBar);
});
};

return XF.ajax('post', url, data, function(data)
{
multiBarAjaxHandler(data);
});
};
XF.loadMultiBar.cache = {};

// ################################## OVERLAY HANDLER ###########################################

XF.Overlay = XF.create({
options: {
backdropClose: true,
escapeClose: true,
focusShow: true,
className: ''
},

$container: null,
$overlay: null,
shown: false,

__construct: function(content, options)
{
this.options = $.extend(true, {}, this.options, options || {});

this.$overlay = content instanceof $ ? content : $($.parseHTML(content));
this.$overlay.attr('role', this.options.role || 'dialog')
.attr('aria-hidden', 'true');

this.$container = $('<div class="overlay-container" />').html(this.$overlay);
this.$container
.data('overlay', this)
.xfUniqueId();

var self = this;

if (this.options.escapeClose)
{
this.$container.on('keydown.overlay', function(e)
{
if (e.which === 27)
{
self.hide();
}
});
}

if (this.options.backdropClose)
{
this.$container.on('mousedown', function(e)
{
self.$container.data('block-close', false);

if (!$(e.target).is(self.$container))
{
// click didn't target container so block closing.
self.$container.data('block-close', true);
}
});

this.$container.on('click', function(e)
{
if ($(e.target).is(self.$container))
{
if (!self.$container.data('block-close'))
{
self.hide();
}
}

self.$container.data('block-close', false);
});
}

if (this.options.className)
{
this.$container.addClass(this.options.className);
}

this.$container.on('click', '.js-overlayClose', XF.proxy(this, 'hide'));

this.$container.appendTo('body');
XF.activate(this.$container);

XF.Overlay.cache[this.$container.attr('id')] = this;

this.$overlay.on('overlay:hide', XF.proxy(this, 'hide'));
this.$overlay.on('overlay:show', XF.proxy(this, 'show'));
},

show: function()
{
if (this.shown)
{
return;
}

this.shown = true;

this.$overlay.attr('aria-hidden', 'false');

// reappending to the body ensures this is the last one, which should allow stacking
var self = this;
this.$container.appendTo('body').addClassTransitioned('is-active', function()
{
if (self.options.focusShow)
{
var $autoFocusFallback = self.$overlay.find('.js-overlayClose');
XF.autoFocusWithin(self.$overlay.find('.overlay-content'), null, $autoFocusFallback);
}

self.$container.trigger('overlay:shown');
XF.layoutChange();
});

this.$container.trigger('overlay:showing');

XF.ModalOverlay.open();
XF.layoutChange();
},

hide: function()
{
if (!this.shown)
{
return;
}

this.shown = false;

this.$overlay.attr('aria-hidden', 'true');

var self = this;
this.$container.removeClassTransitioned('is-active', function()
{
self.$container.trigger('overlay:hidden');
XF.ModalOverlay.close();
XF.layoutChange();
});

this.$container.trigger('overlay:hiding');

XF.layoutChange();
},

recalculate: function()
{
if (this.shown)
{
XF.Modal.updateScrollbarPadding();
}
},

toggle: function()
{
this.shown ? this.hide() : this.show();
},

destroy: function()
{
var id = this.$container.attr('id'),
cache = XF.Overlay.cache;

this.$container.remove();
if (cache.hasOwnProperty(id))
{
delete cache[id];
}
},

on: function()
{
this.$container.on.apply(this.$container, arguments);
},

getContainer: function() { return this.$container; },
getOverlay: function() { return this.$overlay; }
});
XF.Overlay.cache = {};

XF.ModalOverlay = (function()
{
var count = 0,
$applyEl = $('body').first();

function open()
{
XF.Modal.open();

count++;
if (count == 1)
{
$applyEl.addClass('is-modalOverlayOpen');
}
}

function close()
{
XF.Modal.close();

if (count > 0)
{
count--;
if (count == 0)
{
$applyEl.removeClass('is-modalOverlayOpen');
}
}
}

return {
getOpenCount: function() { return count; },
open: open,
close: close
}
})();

XF.Modal = (function()
{
var count = 0,
$applyEl = $('body').first(),
$html = $('html');

var open = function()
{
count++;
if (count == 1)
{
$applyEl.addClass('is-modalOpen');
updateScrollbarPadding();
}
};
var close = function()
{
if (count > 0)
{
count--;
if (count == 0)
{
$applyEl.removeClass('is-modalOpen');
updateScrollbarPadding();
}
}
};

var updateScrollbarPadding = function()
{
var side = 'right',
value = $applyEl.hasClass('is-modalOpen') ? XF.measureScrollBar() + 'px' : '';

if (XF.isRtl())
{
// Chrome and Firefox keep the body scrollbar on the right but IE/Edge flips it
if (!XF.browser.chrome && !XF.browser.mozilla)
{
side = 'left';
}
}

$html.css('margin-' + side, value);
};

return {
getOpenCount: function() { return count; },
open: open,
close: close,
updateScrollbarPadding: updateScrollbarPadding
};
})();

XF.showOverlay = function($html, options)
{
var overlay = new XF.Overlay($html, options);
overlay.show();
return overlay;
};
XF.loadOverlay = function(url, options, overlayOptions)
{
if ($.isFunction(options))
{
options = {init: options};
}

options = $.extend({
cache: false,
beforeShow: null,
afterShow: null,
onRedirect: null,
init: null,
show: true
}, options || {});

var show = function(overlay)
{
if (options.beforeShow)
{
var e = $.Event();
options.beforeShow(overlay, e);
if (e.isDefaultPrevented())
{
return;
}
}

if (options.show)
{
overlay.show();
}

if (options.afterShow)
{
var e = $.Event();
options.afterShow(overlay, e);
if (e.isDefaultPrevented())
{
return;
}
}
};

if (options.cache && XF.loadOverlay.cache[url])
{
show(XF.loadOverlay.cache[url]);
return;
}

var overlayAjaxHandler = function(data)
{
if (data.redirect)
{
if (options.onRedirect)
{
options.onRedirect(data, overlayAjaxHandler);
}
else
{
XF.ajax('get', data.redirect, function(data)
{
overlayAjaxHandler(data);
});
}
}

if (!data.html)
{
return;
}

XF.setupHtmlInsert(data.html, function($html, container, onComplete)
{
var overlay = new XF.Overlay(XF.getOverlayHtml({
html: $html,
title: container.title || container.h1
}), overlayOptions);
if (options.init)
{
options.init(overlay);
}
if (!options.cache)
{
overlay.on('overlay:hidden', function()
{
overlay.destroy();
});
}

onComplete();

if (options.cache)
{
XF.loadOverlay.cache[url] = overlay;
}

show(overlay);
});
};

return XF.ajax('get', url, function(data)
{
overlayAjaxHandler(data);
});
};
XF.loadOverlay.cache = {};

// ################################## NAVIGATION DEVICE WATCHER ###########################################

/**
* Allows querying of the current input device (mouse or keyboard) -- .isKeyboardNav()
* And sets a CSS class (has-pointer-nav) on <html> to allow styling based on current input
*
* @type {{initialize, toggle, isKeyboardNav}}
*/
XF.NavDeviceWatcher = (function()
{
var isKeyboard = true;

function initialize()
{
$(document).onPassive(
{
mousedown: function()
{
toggle(false);
},
keydown: function(e)
{
switch (e.key)
{
case 'Tab':
case 'Enter':
toggle(true);
}
}
});
}

function toggle(toKeyboard)
{
if (toKeyboard != isKeyboard)
{
$('html').toggleClass('has-pointer-nav', !toKeyboard);

isKeyboard = toKeyboard;
}
}

function isKeyboardNav()
{
return isKeyboard;
}

return {
initialize: initialize,
toggle: toggle,
isKeyboardNav: isKeyboardNav
};
})();

XF.ScrollButtons = (function()
{
var hideTimer = null,
pauseScrollWatch = false,
upOnly = false,
isShown = false,
scrollTop = window.pageYOffset || document.documentElement.scrollTop,
scrollDir = null,
scrollTopDirChange = null,
scrollTrigger,
$buttons = null;

function initialize()
{
if ($buttons && $buttons.length)
{
// already initialized
return false;
}

$buttons = $('.js-scrollButtons');
if (!$buttons.length)
{
return false;
}

if ($buttons.data('trigger-type') === 'up')
{
upOnly = true;
}

$buttons.on({
'mouseenter focus': enter,
'mouseleave blur': leave,
'click': click
});

$(window).onPassive('scroll', onScroll);

return true;
}

function onScroll(e)
{
if (pauseScrollWatch)
{
return;
}

var newScrollTop = window.pageYOffset || document.documentElement.scrollTop,
oldScrollTop = scrollTop;

scrollTop = newScrollTop;

if (newScrollTop > oldScrollTop)
{
if (scrollDir != 'down')
{
scrollDir = 'down';
scrollTopDirChange = oldScrollTop;
}
}
else if (newScrollTop < oldScrollTop)
{
if (scrollDir != 'up')
{
scrollDir = 'up';
scrollTopDirChange = oldScrollTop;
}
}
else
{
// didn't scroll?
return;
}

if (upOnly)
{
// downward scroll or we're near the top anyway
if (scrollDir !== 'up' || scrollTop < 100)
{
if (scrollTrigger)
{
scrollTrigger.cancel();
scrollTrigger = null;
}
return;
}

// only trigger after scrolling up 30px to reduce false positives
if (scrollTopDirChange - newScrollTop < 30)
{
return;
}
}

if (scrollTrigger)
{
// already about to be triggered
return;
}

// note that Chrome on Android can heavily throttle setTimeout, so use a requestAnimationFrame
// alternative if possible to ensure this triggers when expected
scrollTrigger = XF.requestAnimationTimeout(function()
{
scrollTrigger = null;

show();
startHideTimer();
}, 200);
}

function show()
{
if (!isShown)
{
$buttons.addClassTransitioned('is-active');
isShown = true;
}
}

function hide()
{
if (isShown)
{
$buttons.removeClassTransitioned('is-active');
isShown = false;
}
}

function startHideTimer()
{
clearHideTimer();

hideTimer = setTimeout(function()
{
hide();
}, 3000);
}

function clearHideTimer()
{
clearTimeout(hideTimer);
}

function enter()
{
clearHideTimer();

show();
}

function leave()
{
clearHideTimer();
}

function click(e)
{
var $target = $(e.target);
if (
!$target.is('.button--scroll') ||
$target.closest('.button--scroll').length === 0
)
{
return;
}

pauseScrollWatch = true;

setTimeout(function()
{
pauseScrollWatch = false;
}, 500);

hide();
}

return {
initialize: initialize,
show: show,
hide: hide,
startHideTimer: startHideTimer,
clearHideTimer: clearHideTimer
}
})();

// ################################## KEYBOARD SHORTCUT HANDLER ###########################################

/**
* Activates keyboard shortcuts for elements based on data-xf-key attributes
*
* @type {{initialize, initializeElements}}
*/
XF.KeyboardShortcuts = (function()
{
var shortcuts = {},

Ctrl = 1,
Alt  = 2,
Meta = 4,

debug = false;

function initialize()
{
$(document.body).onPassive('keyup', keyEvent);
}

function initializeElements(root)
{
var $root = $(root);

if ($root.length > 1)
{
$root.each(function()
{
initializeElements(this);
});
return;
}

if ($root.is('[data-xf-key]'))
{
initializeElement($root[0]);
}
$root.find('[data-xf-key]').each(function()
{
initializeElement(this);
});

if (debug) console.info('Registered keyboard shortcuts: %o', shortcuts);
}

function initializeElement(el)
{
// accepts a shortcut key either as 'a', 'B', etc., or a charcode with a # prefix - '#97', '#56'
var shortcut = String($(el).data('xf-key')),
key = shortcut.substr(shortcut.lastIndexOf('+') + 1),
charCode = key[0] === '#' ? key.substr(1): key.toUpperCase().charCodeAt(0),
codeInfo = shortcut.toUpperCase().split('+'),
modifierCode = getModifierCode(
codeInfo.indexOf('CTRL') !== -1,
codeInfo.indexOf('ALT') !== -1,
codeInfo.indexOf('META') !== -1
);

if (modifierCode)
{
if (XF.Keyboard.isStandardKey(charCode))
{
shortcuts[charCode] = shortcuts[charCode] || {};
shortcuts[charCode][modifierCode] = el;

if (debug) console.info('Shortcut %c%s%c registered as %s + %s for %s', 'color:red;font-weight:bold;font-size:larger', shortcut, 'color:inherit;font-weight:inherit;font-size:inherit', charCode, modifierCode, el);
}
else
{
console.warn('It is not possible to specify a keyboard shortcut using this key combination (%s)', shortcut);
}
}
else
{
shortcuts[key] = el;

if (debug) console.info('Shortcut %c%s%c registered as %s for %s', 'color:red;font-weight:bold;font-size:larger', shortcut, 'color:inherit;font-weight:inherit;font-size:inherit', key, el);
}
}

function keyEvent(e)
{
switch (e.key)
{
case 'Escape':
XF.MenuWatcher.closeAll(); // close all menus
XF.hideTooltips();
return;

case 'Shift':
case 'Control':
case 'Alt':
case 'Meta':
return;
}

if (!XF.Keyboard.isShortcutAllowed(document.activeElement))
{
return;
}

if (debug) console.log('KEYUP: key:%s, which:%s (charCode from key: %s), Decoded from e.which: %s%s%s%s',
e.key, e.which, e.key.charCodeAt(0),
(e.ctrlKey?"CTRL+":""),
(e.altKey?"ALT+":""),
(e.metaKey?"META+":""),
String.fromCharCode(e.which)
);

if (shortcuts.hasOwnProperty(e.key) && getModifierCodeFromEvent(e) == 0) // try simple mapping first
{
if (fireShortcut(shortcuts[e.key]))
{
return;
}
}

if (shortcuts.hasOwnProperty(e.which)) // try complex mapping next
{
var modifierCode = getModifierCodeFromEvent(e);

if (shortcuts[e.which].hasOwnProperty(modifierCode))
{
if (fireShortcut(shortcuts[e.which][modifierCode]))
{
return;
}
}
}
}

function fireShortcut(target)
{
// only act on elements that are :visible
var $target = $(target).filter(':visible');

if ($target.length)
{
XF.NavDeviceWatcher.toggle(true);

if (!XF.isElementVisible($target))
{
$target.get(0).scrollIntoView(true);
}

if ($target.is(XF.getKeyboardInputs()))
{
$target.autofocus();
}
else if ($target.is('a[href]'))
{
$target.get(0).click();
}
else
{
$target.click();
}

return true;
}

return false;
}

function getModifierCode(CtrlKey, AltKey, MetaKey)
{
return 0
+ CtrlKey  ? Ctrl  : 0
+ AltKey   ? Alt   : 0
+ MetaKey  ? Meta  : 0;
}

function getModifierCodeFromEvent(event)
{
return getModifierCode(event.ctrlKey, event.altKey, event.metaKey);
}

return {
initialize: initialize,
initializeElements: initializeElements
}
})();

/**
* Collection of methods for working with the keyboard
*/
XF.Keyboard =
{
/**
* Determines whether a keyboard shortcut can be fired with the current activeElement
*
* @param object activeElement (usually document.activeElement)
*
* @returns {boolean}
*/
isShortcutAllowed: function(activeElement)
{
switch (activeElement.tagName)
{
case 'TEXTAREA':
case 'SELECT':
return false;

case 'INPUT':
switch (activeElement.type)
{
case 'checkbox':
case 'radio':
case 'submit':
case 'reset':
return true;
default:
return false;
}

case 'BODY':
return true;

default:
// active element can be different in IE bail out if the active element is a child of the editor
if (XF.browser.msie)
{
var $el = $(activeElement);
if ($el.parents('.fr-element').length)
{
return false;
}
}
return activeElement.contentEditable === 'true' ? false : true;
}
},

isStandardKey: function(charcode)
{
return (charcode >= 48 && charcode <= 90);
}
};

// ################################## FORM VALIDATION HANDLER ###########################################

/**
* Sets up some custom behaviour on forms so that when invalid inputs are scrolled
* to they are not covered by fixed headers.
*
* @type {{initialize, initializeElements}}
*/
XF.FormInputValidation = (function()
{
var $forms = {};

function initialize()
{
$forms = $('form').not('[novalidate]');

prepareForms();
}

function initializeElements(root)
{
var $root = $(root);

if ($root.length > 1)
{
$root.each(function() { initializeElements(this); });
return;
}

if ($root.is('form'))
{
prepareForm($root);
}
}

function prepareForms()
{
if (!$forms.length)
{
return;
}

$forms.each(function()
{
prepareForm($(this));
});
}

function prepareForm($form)
{
$form.find(':input').on('invalid', { form: $form }, onInvalidInput);
}

function onInvalidInput(event)
{
var $input = $(this),
$form = event.data.form,
$first = $form.find(':invalid').first();

if ($input[0] === $first[0])
{
if (XF.isElementVisible($input))
{
// element is already visible so skip
return;
}

var offset = 100;
var $overlayContainer = $form.closest('.overlay-container.is-active');

if ($overlayContainer.length)
{
$overlayContainer.scrollTop(
$input.offset().top - $overlayContainer.offset().top + $overlayContainer.scrollTop() - offset
);
}
else
{
// put the input 100px from the top of the screen
$input[0].scrollIntoView();
window.scrollBy(0, -offset);
}
}
}

return {
initialize: initialize,
initializeElements: initializeElements
}
})();

// ################################## NOTICE WATCHER ###########################################

XF.NoticeWatcher = (function()
{
function initialize()
{
$(document).on('xf:notice-change xf:layout', XF.proxy(this, 'checkNotices'));
this.checkNotices();
}

function checkNotices()
{
var noticeHeight = this.getBottomFixerNoticeHeight();
$(document).find('footer.p-footer').css('margin-bottom', noticeHeight);
}

function getBottomFixerNoticeHeight()
{
var noticeHeight = 0;
var $bottomFixers = $(document).find('.js-bottomFixTarget .notices--bottom_fixer .js-notice');

$bottomFixers.each(function()
{
var $notice = $(this);

if ($notice.is(':visible'))
{
noticeHeight += $notice.height();
}
});

return noticeHeight;
}

return {
initialize: initialize,
checkNotices: checkNotices,
getBottomFixerNoticeHeight: getBottomFixerNoticeHeight
};
})();

// ############################### PWA HANDLER ################################

XF.PWA = (function()
{
var _isSupported = null,
registration,
showNavigationIndicator = true;

function getSwUrl()
{
var serviceWorkerPath = XF.config.serviceWorkerPath;
if (serviceWorkerPath === null)
{
serviceWorkerPath = 'service_worker.js';
}

if (serviceWorkerPath && serviceWorkerPath.length)
{
return XF.canonicalizeUrl(serviceWorkerPath);
}
else
{
return null;
}
}

function initialize()
{
if (!XF.PWA.isSupported())
{
return;
}

if (XF.config.skipServiceWorkerRegistration)
{
registration = new Promise(function(resolve, reject)
{
reject(new Error('Service worker registration has been skipped'));
});
return;
}

registration = navigator.serviceWorker.register(getSwUrl());
registration
.then(function(reg)
{
var skipMessage = reg.active ? false : true;
updateCacheIfNeeded(XF.config.cacheKey, skipMessage);
});
registration
.catch(function(error)
{
console.error('Service worker registration failed:', error);
});

// remove the old service worker if registered
navigator.serviceWorker.getRegistrations()
.then(function(allRegs)
{
var oldScope = XF.canonicalizeUrl('js/xf/');

for (var k in allRegs)
{
if (allRegs[k].scope == oldScope)
{
allRegs[k].unregister();
}
}
});

navigator.serviceWorker.addEventListener('message', function(event)
{
var message = event.data;
if (typeof message !== 'object' || message === null)
{
console.error('Invalid message:', message);
return;
}

receiveMessage(message.type, message.payload);
});

if (isRunning())
{
$(window).on('beforeunload', onNavigation);

$(document).on('click', '.js-skipPwaNavIndicator', inhibitNavigationIndicator);
}
}

function isSupported()
{
if (_isSupported === null)
{
_isSupported = !!(
'serviceWorker' in navigator
&& getSwUrl()
);
}

return _isSupported;
}

function isRunning()
{
return (
navigator.standalone ||
window.matchMedia('(display-mode: standalone), (display-mode: minimal-ui)').matches
);
}

function getRegistration()
{
return registration;
}

function onNavigation()
{
if (!showNavigationIndicator)
{
return;
}

XF.ActionIndicator.show();

setTimeout(
function() { XF.ActionIndicator.hide(); },
30000
);
}

function inhibitNavigationIndicator()
{
showNavigationIndicator = false;

setTimeout(
function() { showNavigationIndicator = true; },
2000
);
}

function sendMessage(type, payload)
{
if (!navigator.serviceWorker.controller)
{
console.error('There is no active service worker');
return;
}

if (typeof type !== 'string' || type === '')
{
console.error('Invalid message type:', type);
return;
}

if (typeof payload === 'undefined')
{
payload = {};
}
else if (typeof payload !== 'object' || payload === null)
{
console.error('Invalid message payload:', payload);
return;
}

navigator.serviceWorker.controller.postMessage({
type: type,
payload: payload
});
}

var messageHandlers = {};

function receiveMessage(type, payload)
{
if (typeof type !== 'string' || type === '')
{
console.error('Invalid message type:', type);
return;
}

if (typeof payload !== 'object' || payload === null)
{
console.error('Invalid message payload:', payload);
return;
}

var handler = messageHandlers[type];
if (typeof handler === 'undefined')
{
console.error('No handler available for message type:', type);
return;
}

handler(payload);
}

function updateCacheIfNeeded(key, skipMessage)
{
var localKey = XF.LocalStorage.get('cacheKey');
if (localKey === key)
{
return false;
}

if (!skipMessage)
{
sendMessage('updateCache');
}

XF.LocalStorage.set('cacheKey', key, true);
return true;
}

return {
initialize: initialize,
isSupported: isSupported,
isRunning: isRunning,
inhibitNavigationIndicator: inhibitNavigationIndicator,
getRegistration: getRegistration,
sendMessage: sendMessage
};
})();

// ################################## PUSH NOTIFICATION HANDLER ###########################################

XF.Push = (function()
{
function initialize()
{
if (!XF.Push.isSupported())
{
return;
}

if (XF.config.skipPushNotificationSubscription)
{
return;
}

registerWorker();
}

function registerWorker(onRegisterSuccess, onRegisterError)
{
XF.PWA.getRegistration()
.then(function()
{
getSubscription();

if (onRegisterSuccess)
{
onRegisterSuccess();
}
})
.catch(function()
{
if (onRegisterError)
{
onRegisterError();
}
});
}

function getSubscription()
{
XF.PWA.getRegistration()
.then(function(registration)
{
return registration.pushManager.getSubscription();
})
.then(function(subscription)
{
XF.Push.isSubscribed = !(subscription === null);

if (XF.Push.isSubscribed)
{
$(document).trigger('push:init-subscribed');

// If the browser is subscribed, but there is no userId then
// we should unsubscribe to avoid leaking notifications to
// unauthenticated users on a shared device.
// If the server key doesn't match, then we should unsubscribe as we'd
// need to resubscribe with the new key.
if (XF.config.userId && isExpectedServerKey(subscription))
{
XF.Push.updateUserSubscription(subscription, 'update');
}
else
{
subscription.unsubscribe();
XF.Push.updateUserSubscription(subscription, 'unsubscribe');
}
}
else
{
$(document).trigger('push:init-unsubscribed');
}
});
}

function getPushHistoryUserIds()
{
return XF.LocalStorage.getJson('push_history_user_ids') || {};
}

function setPushHistoryUserIds(userIds)
{
XF.LocalStorage.setJson('push_history_user_ids', userIds || {});
}

function hasUserPreviouslySubscribed(userId)
{
var userIdHistory = XF.Push.getPushHistoryUserIds();
return userIdHistory.hasOwnProperty(userId || XF.config.userId);
}

function addUserToPushHistory(userId)
{
var userIdHistory = XF.Push.getPushHistoryUserIds();
userIdHistory[userId || XF.config.userId] = true;
XF.Push.setPushHistoryUserIds(userIdHistory);
}

function removeUserFromPushHistory(userId)
{
// also remove history entry as this is an explicit unsubscribe
var userIdHistory = XF.Push.getPushHistoryUserIds();
delete userIdHistory[userId || XF.config.userId];
XF.Push.setPushHistoryUserIds(userIdHistory);
}

var cancellingSub = null;

function handleUnsubscribeAction(onUnsubscribe, onUnsubscribeError)
{
if (!XF.Push.isSubscribed)
{
return;
}

XF.PWA.getRegistration()
.then(function(registration)
{
return registration.pushManager.getSubscription();
})
.then(function(subscription)
{
if (subscription)
{
cancellingSub = subscription;
return subscription.unsubscribe();
}
})
.catch(function(error)
{
console.error('Error unsubscribing', error);

if (onUnsubscribeError)
{
onUnsubscribeError();
}
})
.then(function()
{
if (cancellingSub)
{
XF.Push.updateUserSubscription(cancellingSub, 'unsubscribe');
}

XF.Push.isSubscribed = false;

if (onUnsubscribe)
{
onUnsubscribe();
}
});
}

function handleSubscribeAction(suppressNotification, onSubscribe, onSubscribeError)
{
if (XF.Push.isSubscribed)
{
return;
}

Notification.requestPermission().then(function(result)
{
if (result !== 'granted')
{
console.error('Permission was not granted');
return;
}

var applicationServerKey = XF.Push.base64ToUint8(XF.config.pushAppServerKey);

XF.PWA.getRegistration().then(function(registration) {
registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey
})
.then(function(subscription)
{
XF.Push.updateUserSubscription(subscription, 'insert');
XF.Push.isSubscribed = true;

var options = {
body: XF.phrase('push_enable_notification_body'),
dir: XF.isRtl() ? 'rtl' : 'ltr'
};
if (XF.config.publicMetadataLogoUrl)
{
options['icon'] = XF.config.publicMetadataLogoUrl;
}
if (XF.config.publicPushBadgeUrl)
{
options['badge'] = XF.config.publicPushBadgeUrl;
}

if (!suppressNotification)
{
registration.showNotification(
XF.phrase('push_enable_notification_title'), options
);
}

if (XF.config.userId)
{
XF.Push.addUserToPushHistory();
}

if (onSubscribe)
{
onSubscribe();
}
})
.catch(function(error)
{
console.error('Failed to subscribe the user: ', error);

if (onSubscribeError)
{
onSubscribeError();
}
});
});
});
}

function handleToggleAction(onUnsubscribe, onUnsubscribeError, onSubscribe, onSubscribeError)
{
if (XF.Push.isSubscribed)
{
XF.Push.handleUnsubscribeAction(onUnsubscribe, onUnsubscribeError);
}
else
{
XF.Push.handleSubscribeAction(false, onSubscribe, onSubscribeError);
}
}

function updateUserSubscription(subscription, type)
{
if (type === 'update' && XF.Cookie.get('push_subscription_updated'))
{
return;
}

var key = subscription.getKey('p256dh'),
token = subscription.getKey('auth'),
encoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0];

$.ajax({
url: XF.canonicalizeUrl('index.php?misc/update-push-subscription'),
type: 'post',
data: {
endpoint: subscription.endpoint,
key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : null,
token: token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null,
encoding: encoding,
unsubscribed: (type === 'unsubscribe') ? 1 : 0,
_xfResponseType: 'json',
_xfToken: XF.config.csrf
},
cache: false,
dataType: 'json',
global: false
}).always(function()
{
if (type === 'update')
{
XF.Cookie.set('push_subscription_updated', '1');
}
});
}

function isSupported()
{
return (
XF.PWA.isSupported()
&& XF.config.enablePush
&& XF.config.pushAppServerKey
// && XF.getApp() === 'public'
&& 'PushManager' in window
&& 'Notification' in window
);
}

function base64ToUint8(base64String)
{
var padding = "=".repeat((4 - base64String.length % 4) % 4);
var base64 = (base64String + padding).replace(/\-/g, "+").replace(/_/g, "/");

var rawData = window.atob(base64);
var outputArray = new Uint8Array(rawData.length);

for (var i = 0; i < rawData.length; ++i)
{
outputArray[i] = rawData.charCodeAt(i);
}

return outputArray;
}

function isExpectedServerKey(input)
{
if (input instanceof PushSubscription)
{
input = input.options.applicationServerKey;
}

if (typeof input === 'string')
{
return (XF.config.pushAppServerKey === input);
}

if (input.buffer && input.BYTES_PER_ELEMENT)
{
// typed array -- not exposed directly to JS
input = input.buffer;
}
if (!(input instanceof ArrayBuffer))
{
throw new Error("input must be an array buffer or convertable to it");
}

var serverKey = base64ToUint8(XF.config.pushAppServerKey).buffer,
length = serverKey.byteLength;

if (length !== input.byteLength)
{
return false;
}

var serverKeyView = new DataView(serverKey),
inputView = new DataView(input);

for (var i = 0; i < length; i++)
{
if (serverKeyView.getUint8(i) !== inputView.getUint8(i))
{
return false;
}
}

return true;
}

return {
isSubscribed: null,
initialize: initialize,
registerWorker: registerWorker,
getPushHistoryUserIds: getPushHistoryUserIds,
setPushHistoryUserIds: setPushHistoryUserIds,
hasUserPreviouslySubscribed: hasUserPreviouslySubscribed,
addUserToPushHistory: addUserToPushHistory,
removeUserFromPushHistory: removeUserFromPushHistory,
handleUnsubscribeAction: handleUnsubscribeAction,
handleSubscribeAction: handleSubscribeAction,
handleToggleAction: handleToggleAction,
updateUserSubscription: updateUserSubscription,
isSupported: isSupported,
base64ToUint8: base64ToUint8,
isExpectedServerKey: isExpectedServerKey
};
})();

// ################################## BB CODE EXPAND WATCHER ###########################################

XF.ExpandableContent = (function()
{
var containerSel = '.js-expandWatch';

function watch()
{
$(document).on('click', '.js-expandLink', function(e)
{
$(e.target).closest(containerSel).addClassTransitioned('is-expanded', XF.layoutChange);
});

$(window).onPassive('resize', function()
{
checkSizing(document);
});

$(document).on('embed:loaded', function()
{
checkSizing(document);
});
}

function checkSizing(el)
{
$(el).find(containerSel + ':not(.is-expanded)').each(function()
{
var $this = $(this),
content = $this.find('.js-expandContent')[0];

if (!content)
{
return;
}

var timer,
delay = 0,
check = function()
{
var scroll = content.scrollHeight,
offset = content.offsetHeight;

if (scroll == 0 || offset == 0)
{
if (delay > 2000)
{
return;
}
if (timer)
{
clearTimeout(timer);
delay += 200;
}
timer = setTimeout(check, delay);
return;
}

if (scroll > offset + 1) // +1 resolves a Chrome rounding issue
{
$this.addClass('is-expandable');
}
else
{
$this.removeClass('is-expandable');
}
};

check();

if (!$this.data('expand-check-triggered'))
{
$this.data('expand-check-triggered', true);

$this.find('img').one('load', check);

if (window.MutationObserver)
{
var observer,
mutationTimeout,
allowMutationTrigger = true,
mutationTrigger = function()
{
allowMutationTrigger = false;
check();

// prevent triggers for a little bit after this so we limit thrashing
setTimeout(function()
{
allowMutationTrigger = true;
}, 100);
};

observer = new MutationObserver(function(mutations)
{
if ($this.hasClass('is-expanded'))
{
observer.disconnect();
return;
}

if (!allowMutationTrigger)
{
return;
}

if (mutationTimeout)
{
clearTimeout(mutationTimeout);
}
mutationTimeout = setTimeout(mutationTrigger, 200);
});
observer.observe(this, {
attributes: true,
childList: true,
subtree: true
});
}
}
});
}

return {
watch: watch,
checkSizing: checkSizing
}
})();

// ################################## UNFURL LOADER WATCHER ###########################################

XF.UnfurlLoader = (function()
{
var unfurlIds = [],
pending = false,
pendingIds = [];

function activateContainer(container)
{
var $unfurls = $(container).find('.js-unfurl');

if (!$unfurls.length)
{
return;
}

$unfurls.each(function()
{
var $el = $(this);

if ($el.data('pending') === false || $el.data('pending-seen'))
{
return true;
}

$el.data('pending-seen', true);

var id = $el.data('result-id');
if (pending)
{
pendingIds.push(id);
}
else
{
unfurlIds.push(id);
}
});

unfurl();
}

function unfurl()
{
if (!unfurlIds.length || pending)
{
return;
}

var lastResponseLength = null;

function onResponseContent(response)
{
var currentResponse, currentEnd;

if (lastResponseLength === null)
{
currentResponse = response;
}
else
{
currentResponse = response.substring(lastResponseLength);
}

currentEnd = currentResponse.indexOf("\n");
if (currentEnd === -1)
{
// partial response (no line-break) so wait for more progress
return;
}

// gets the response up to the first line-break delimiter
currentResponse = currentResponse.substring(0, currentEnd);

if (lastResponseLength === null)
{
lastResponseLength = currentResponse.length;
}
else
{
lastResponseLength += currentResponse.length;
}

// ignore trailing line break
lastResponseLength++;

XF.UnfurlLoader.handleResponse(JSON.parse(currentResponse));

if (lastResponseLength < response.length)
{
onResponseContent(response);
}
}

pending = true;

XF.ajax(
'post',
XF.canonicalizeUrl('unfurl.php'),
{ result_ids: unfurlIds },
function(data)
{
onResponseContent(data);
},
{
skipDefault: true,
dataType: 'text',
xhrFields: {
onprogress: function(e)
{
var response = e.currentTarget.response;

if (!response.length)
{
return;
}

onResponseContent(response);
}
}
}
).always(function()
{
unfurlIds = [];
pending = false;
lastResponseLength = null;

if (pendingIds)
{
unfurlIds = pendingIds;
pendingIds = [];
pending = false;

setTimeout(unfurl, 0);
}
});

}

function handleResponse(data)
{
var $unfurl = $('.js-unfurl[data-result-id="' + data.result_id + '"]');

if (!$unfurl.length)
{
return;
}

if (data.success)
{
XF.setupHtmlInsert(data.html, function($html, container, onComplete)
{
$unfurl.replaceWith($html);
});
}
else
{
var $link = $unfurl.find('.js-unfurl-title a');
$link.text($unfurl.data('url'))
.addClass('bbCodePlainUnfurl')
.removeClass('fauxBlockLink-blockLink');
$unfurl.replaceWith($link);
}
}

return {
activateContainer: activateContainer,
unfurl: unfurl,
handleResponse: handleResponse
}
})();

// ############################### ELEMENT EVENT HANDLER SYSTEM ########################################

/**
* This system allows elements to have a trigger event attached (eg: click, focus etc.),
* such that the code for handling the event is only attached at the time that the event
* is actually triggered, making for fast page initialization time.
*/
XF.Event = (function()
{
var mapper = new XF.ClassMapper();

var eventsWatched = {},
pointerDataKey = 'xf-pointer-type';

var watch = function(eventType)
{
eventType = String(eventType).toLowerCase();

function isValidTarget(e, target)
{
if (!target)
{
target = e.currentTarget;
}

if (!target || !target.getAttribute)
{
// not an element so can't have a handler
return false;
}

var $target = $(target);

if ($target.is('a') && !$target.data('click-allow-modifier'))
{
// abort if the event has a modifier key
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey)
{
return false;
}

// abort if the event is a middle or right-button click
if (e.which > 1)
{
return false;
}
}

if ($target.closest('[contenteditable=true]').length)
{
return false;
}

return true;
}

if (!eventsWatched.hasOwnProperty(eventType))
{
eventsWatched[eventType] = true;

$(document).on(eventType, '[data-xf-' + eventType + ']', function(e)
{
var target = e.currentTarget;

if (isValidTarget(e, target))
{
var $target = $(target),
type = $target.data(pointerDataKey);

e.xfPointerType = e.pointerType || type || '';

initElement(target, eventType, e);
}
});

$(document).on('pointerdown', '[data-xf-' + eventType + ']', function(e)
{
var target = e.currentTarget;

if (isValidTarget(e, target))
{
$(target).data(pointerDataKey, e.pointerType);
}
});
}
};

var initElement = function(target, eventType, e)
{
var $target = $(target),
handlerList = $target.data('xf-' + eventType).split(' ') || [],
handlerObjects = $target.data('xf-' + eventType + '-handlers') || {},
identifier, obj, i, options;

for (i = 0; i < handlerList.length; i++)
{
identifier = handlerList[i];
if (!identifier.length)
{
continue;
}

if (!handlerObjects[identifier])
{
obj = mapper.getObjectFromIdentifier(identifier);
if (!obj)
{
console.error('Could not find %s handler for %s', eventType, identifier);
continue;
}

options = $target.data('xf-' + identifier) || {};
handlerObjects[identifier] = new obj($target, options);
}

if (e && handlerObjects[identifier]._onEvent(e) === false)
{
break;
}
}

$target.data('xf-' + eventType + '-handlers', handlerObjects);

return handlerObjects;
};

var getElementHandler = function($el, handlerName, eventType)
{
var handlers = $el.data('xf-' + eventType + '-handlers');

if (!handlers)
{
handlers = XF.Event.initElement($el[0], eventType);
}

if (handlers && handlers[handlerName])
{
return handlers[handlerName];
}
else
{
return null;
}
};

var AbstractHandler = XF.create(
{
initialized: false,
eventType: 'click',
eventNameSpace: null,
$target: null,
options: {},

__construct: function($target, options)
{
this.$target = $target;
this.options = XF.applyDataOptions(this.options, $target.data(), options);
this.eventType = this.eventType.toLowerCase();

if (!this.eventNameSpace)
{
throw new Error('Please provide an eventNameSpace for your extended ' + this.eventType + ' handler class');
}

this._init();
},

/**
* 'protected' wrapper function for init(),
*  containing Before/AfterInit events
*/
_init: function()
{
var beforeInitEvent = new $.Event('xf-' + this.eventType + ':before-init.' + this.eventNameSpace),
returnValue = false;

this.$target.trigger(beforeInitEvent, [this]);

if (!beforeInitEvent.isDefaultPrevented())
{
returnValue = this.init();

this.$target.trigger('xf-' + this.eventType + ':after-init.' + this.eventNameSpace, [this, returnValue]);
}

this.initialized = true;

return returnValue;
},

_onEvent: function(e)
{
var beforeEvent = new $.Event('xf-' + this.eventType + ':before-' + this.eventType + '.' + this.eventNameSpace),
returnValue = null;

this.$target.trigger(beforeEvent, [this]);

if (!beforeEvent.isDefaultPrevented())
{
if (typeof this[this.eventType] == 'function')
{
returnValue = this[this.eventType](e);
}
else if (typeof this.onEvent == 'function')
{
returnValue = this.onEvent(e);
}
else
{
console.error('You must provide a %1$s(e) method for your %1$s event handler', this.eventType, this.eventNameSpace);
e.preventDefault();
return false;
}

this.$target.trigger('xf-' + this.eventType + ':after-' + this.eventType + '.' + this.eventNameSpace, [this, returnValue, e]);
}

return null;
},

// methods to be overridden by inheriting classes
init: function()
{
console.error('This is the abstract init method for XF.%s, which must be overridden.', this.eventType, this.eventNameSpace);
}
});

return {
watch: watch,
initElement: initElement,
getElementHandler: getElementHandler,
register: function(eventType, identifier, className)
{
XF.Event.watch(eventType);
mapper.add(identifier, className);
},
extend: function(identifier, extension)
{
mapper.extend(identifier, extension);
},
newHandler: function(extend)
{
return XF.extend(AbstractHandler, extend);
},
AbstractHandler: AbstractHandler
};
})();

// ################################## CLICK HANDLER SYSTEM ###########################################

/**
* @deprecated This will be retired in a future version, use XF.Event instead.
*/
XF.Click = (function()
{
return {
watch: function () {
return XF.Event.watch('click');
},
initElement: function(target, clickE)
{
return XF.Event.initElement(target, 'click', clickE);
},
getElementHandler: function ($el, handlerName) {
return XF.Event.getElementHandler($el, handlerName, 'click');
},
register: function(identifier, className)
{
XF.Event.watch('click');
return XF.Event.register('click', identifier, className);
},
extend: function(identifier, extension)
{
return XF.Event.extend(identifier, extension);
},
newHandler: function(extend)
{
return XF.Event.newHandler(extend);
}
};
})();

// ################################## ELEMENT HANDLER SYSTEM ##########################################

/**
* This system allows elements with data-xf-init to be initialized at page load time
*/
XF.Element = (function()
{
var mapper = new XF.ClassMapper();

var applyHandler = function($el, handlerId, options)
{
var handlers = $el.data('xf-element-handlers') || {};
if (handlers[handlerId])
{
return handlers[handlerId];
}

var ctor = mapper.getObjectFromIdentifier(handlerId);
if (!ctor)
{
return null;
}

var obj = new ctor($el, options || {});

handlers[handlerId] = obj;
$el.data('xf-element-handlers', handlers);

obj.init();

return obj;
};

var getHandler = function($el, handlerId)
{
var handlers = $el.data('xf-element-handlers');
if (handlers === undefined)
{
initializeElement($el);
handlers = $el.data('xf-element-handlers');
}

if (handlers && handlers[handlerId])
{
return handlers[handlerId];
}
else
{
return null;
}
};

var initializeElement = function(el)
{
if (el instanceof $)
{
el = el[0];
}

if (!el || !el.getAttribute)
{
// not an element -- probably a text node
return;
}

var init = el.getAttribute('data-xf-init');
if (!init)
{
return;
}

var parts = init.split(' '),
len = parts.length,
$el = $(el),
handlerId;
for (var i = 0; i < len; i++)
{
handlerId = parts[i];
if (!handlerId)
{
continue;
}

applyHandler($el, handlerId, $el.data('xf-' + handlerId));
}
};

var initialize = function(root)
{
var $root = $(root);

if ($root.length > 1)
{
$root.each(function() { initialize(this); });
return;
}

if ($root.is('[data-xf-init]'))
{
initializeElement($root[0]);
}
$root.find('[data-xf-init]').each(function() { initializeElement(this); });
};

var AbstractHandler = XF.create(
{
$target: null,
options: {},

__construct: function ($target, options)
{
this.$target = $target;
this.options = XF.applyDataOptions(this.options, $target.data(), options);
},

init: function()
{
console.error('This is the abstract init method for XF.Element, '
+ 'which should be overridden.');
},

getOption: function(option)
{
return this.options[option];
}
});

return {
register: function(identifier, className) { mapper.add(identifier, className); },
extend: function(identifier, extension) { mapper.extend(identifier, extension); },
initialize: initialize,
initializeElement: initializeElement,
applyHandler: applyHandler,
getHandler: getHandler,
newHandler: function(extend)
{
return XF.extend(AbstractHandler, extend);
},

AbstractHandler: AbstractHandler
};
})();

XF.AutoCompleteResults = XF.create({
selectedResult: 0,
$results: false,
$scrollWatchers: null,
resultsVisible: false,
resizeBound: false,
headerHtml: null,
options: {},

__construct: function(options)
{
this.options = $.extend({
onInsert: null,
clickAttacher: null,
beforeInsert: null,
insertMode: 'text',
displayTemplate: '{{{icon}}}{{{text}}}',
wrapperClasses: '',
}, options);
},

isVisible: function()
{
return this.resultsVisible;
},

hideResults: function()
{
this.resultsVisible = false;

if (this.$results)
{
this.$results.hide();
}
this.stopScrollWatching();
},

stopScrollWatching: function()
{
if (this.$scrollWatchers)
{
this.$scrollWatchers.off('scroll.autocomplete');
this.$scrollWatchers = null;
}
},

addHeader: function(headerHtml)
{
this.headerHtml = headerHtml;
},

showResults: function(val, results, $targetOver, cssPosition)
{
var maxZIndex = 0,
i,
filterRegex,
result,
$li;

if (!results)
{
this.hideResults();
return;
}

this.resultsVisible = false;

if (!this.$results)
{
this.$results = $('<ul />')
.css({position: 'absolute', display: 'none'})
.addClass('autoCompleteList ' + this.options.wrapperClasses)
.attr('role', 'listbox')
.appendTo(document.body);

XF.setRelativeZIndex(this.$results, $targetOver, 1);
}
else
{
this.$results.hide().empty();
}

filterRegex = new RegExp('(' + XF.regexQuote(XF.htmlspecialchars(val)) + ')', 'i');

for (i in results)
{
if (!results.hasOwnProperty(i))
{
continue;
}

result = results[i];

$li = $('<li />')
.css('cursor', 'pointer')
.attr('unselectable', 'on')
.attr('role', 'option')
.mouseenter(XF.proxy(this, 'resultMouseEnter'));

if (this.options.clickAttacher)
{
this.options.clickAttacher($li, XF.proxy(this, 'resultClick'));
}
else
{
$li.click(XF.proxy(this, 'resultClick'));
}

var textValue,
params = {
icon: '',
text: '',
desc: ''
};

if (typeof result == 'string')
{
textValue = result;
params.text = XF.htmlspecialchars(result);
}
else
{
textValue = result.text;
params.text = XF.htmlspecialchars(result.text);

if (typeof result.desc !== 'undefined')
{
params.desc = XF.htmlspecialchars(result.desc);
}

if (typeof result.icon !== 'undefined')
{
params.icon = $('<img class="autoCompleteList-icon" />').attr('src', XF.htmlspecialchars(result.icon));
}
else if (typeof result.iconHtml !== 'undefined')
{
params.icon = $('<span class="autoCompleteList-icon" />').html(result.iconHtml);
}

if (params.icon)
{
params.icon = params.icon[0].outerHTML;
}

if (result.extraParams)
{
var extraParams = result.extraParams;
for (var extraParam in extraParams)
{
if (!extraParams.hasOwnProperty(extraParam))
{
continue;
}

var isHtml = extraParam.match(/Html$/);
var key = isHtml
? extraParam.replace(/Html$/, '')
: extraParam;
var value = isHtml
? extraParams[extraParam]
: XF.htmlspecialchars(extraParams[extraParam]);
params[key] = value;
}
}
}

$li.data('insert-text', textValue);
$li.data('insert-html', result.html || '');

params.text = params.text.replace(filterRegex, '<strong>$1</strong>');
params.desc = params.desc.replace(filterRegex, '<strong>$1</strong>');

$li.html(Mustache.render(this.options.displayTemplate, params)).appendTo(this.$results);
}

if (!this.$results.children().length)
{
return;
}

this.selectResult(0, true);

if (this.headerHtml)
{
$li = $('<li />')
.addClass('menu-header menu-header--small')
.attr('unselectable', 'on')
.html(this.headerHtml);
$li.prependTo(this.$results);
}

if (!this.resizeBound)
{
$(window).onPassive('resize', XF.proxy(this, 'hideResults'));
}

this.$results.css({
top: '',
left: '',
right: '',
bottom: ''
});

var $results = this.$results,
getPositioning = function(cssPosition)
{
if ($.isFunction(cssPosition))
{
cssPosition = cssPosition($results, $targetOver);
}

if (!cssPosition)
{
var offset = $targetOver.offset();

cssPosition = {
top: offset.top + $targetOver.outerHeight(),
left: offset.left
};

if (XF.isRtl())
{
cssPosition.right = $('html').width() - offset.left - $targetOver.outerWidth();
cssPosition.left = 'auto';
}
}

return cssPosition;
};

// if this is in a scrollable area, watch anything scrollable
this.stopScrollWatching();
var $scrollWatchers = $targetOver.parents().filter(function()
{
switch ($(this).css('overflow-x'))
{
case 'scroll':
case 'auto':
return true;

default:
return false;
}
});
if ($scrollWatchers && $scrollWatchers.length)
{
$scrollWatchers.on('scroll.autocomplete', function()
{
$results.css(getPositioning(cssPosition));
});

this.$scrollWatchers = $scrollWatchers;
}

this.$results.css(getPositioning(cssPosition)).show();
this.resultsVisible = true;
},

resultClick: function(e)
{
e.stopPropagation();

this.insertResult(
this.getResultText(e.currentTarget),
e.currentTarget,
e
);
this.hideResults();
},

resultMouseEnter: function (e)
{
this.selectResult($(e.currentTarget).index(), true);
},

selectResult: function(shift, absolute)
{
var sel, children;

if (!this.$results)
{
return;
}

if (absolute)
{
this.selectedResult = shift;
}
else
{
this.selectedResult += shift;
}

sel = this.selectedResult;
children = this.$results.children();
children.each(function(i)
{
if (i == sel)
{
$(this).addClass('is-selected');
}
else
{
$(this).removeClass('is-selected');
}
});

if (sel < 0 || sel >= children.length)
{
this.selectedResult = -1;
}
},

insertSelectedResult: function(e)
{
var res, ret = false;

if (!this.resultsVisible)
{
return false;
}

if (this.selectedResult >= 0)
{
res = this.$results.children().get(this.selectedResult);
if (res)
{
var resultText = this.getResultText(res);

if (this.options.beforeInsert)
{
resultText = this.options.beforeInsert(resultText, res)
}
this.insertResult(resultText, res, e);
ret = true;
}
}

this.hideResults();

return ret;
},

insertResult: function(value, res, e)
{
if (this.options.onInsert)
{
this.options.onInsert(value, res, e);
}
},

getResultText: function(el)
{
var text;

switch (this.options.insertMode)
{
case 'text':
text = $(el).data('insert-text');
break;

case 'html':
text = $(el).data('insert-html');
break;
}

return text;
}
});

XF.AutoCompleter = XF.create({
options: {
url: null,
method: 'GET',
idleWait: 200,
minLength: 2,
at: '@',
keepAt: true,
insertMode: 'text',
displayTemplate: '{{{icon}}}{{{text}}}',
beforeInsert: null
},

$input: null,
ed: null,

results: null,
visible: false,
idleTimer: null,
pendingQuery: '',

__construct: function($input, options, editor)
{
this.options = $.extend(true, {}, this.options, options);
this.$input = $input;
this.ed = editor;

if (!this.options.url)
{
console.error('No URL option passed in to XF.AutoCompleter.');
return;
}

if (typeof this.options.at != 'string' || this.options.at.length > 1)
{
console.error('The \'at\' option should be a single character string.');
}

this.init();
},

init: function()
{
var t = this,
resultOpts = {
onInsert: function(result)
{
t.insertResult(result);
},
beforeInsert: this.options.beforeInsert,
insertMode: this.options.insertMode,
displayTemplate: this.options.displayTemplate
};

if (this.ed)
{
resultOpts['clickAttacher'] = function($li, f)
{
t.ed.events.bindClick($li, $li, f);
};
}

this.results = new XF.AutoCompleteResults(resultOpts);

if (this.ed)
{
this.ed.events.on('keydown', XF.proxy(this, 'keydown'), true);
this.ed.events.on('keyup', XF.proxy(this, 'keyup'), true);
this.ed.events.on('click blur', XF.proxy(this, 'blur'));

// note: wrapped in $() so it is one of *our* jQuery objects
$(this.ed.$wp).onPassive('scroll', XF.proxy(this, 'blur'));
}
else
{
this.$input.on('keydown', XF.proxy(this, 'keydown'));
this.$input.on('keyup', XF.proxy(this, 'keyup'));
this.$input.on('click blur', XF.proxy(this, 'blur'));
$(document).onPassive('scroll', XF.proxy(this, 'blur'));
}
},

keydown: function(e)
{
if (!this.visible)
{
return;
}

switch (e.which)
{
case 40: // down
this.results.selectResult(1);
e.preventDefault();
return false;

case 38: // up
this.results.selectResult(-1);
e.preventDefault();
return false;

case 27: // esc
this.hide();
e.preventDefault();
return false;

case 13: // enter
if (this.visible)
{
this.results.insertSelectedResult(e);
e.preventDefault();
return false;
}
break;
}
},

keyup: function(e)
{
if (this.visible)
{
switch (e.which)
{
case 40: // down
case 38: // up
case 13: // enter
return;
}
}

this.hide();

if (this.idleTimer)
{
clearTimeout(this.idleTimer);
}
this.idleTimer = setTimeout(XF.proxy(this, 'lookForMatch'), this.options.idleWait);
},

blur: function()
{
if (!this.visible)
{
return;
}

// timeout ensures that clicks still register
setTimeout(XF.proxy(this, 'hide'), 250);
},

lookForMatch: function()
{
var match = this.getCurrentMatchInfo();
if (match)
{
this.foundMatch(match.query);
}
else
{
this.hide();
}
},

getCurrentMatchInfo: function()
{
var selection, textNode, text;

if (this.ed)
{
selection = this.ed.selection.ranges(0);
if (!selection || !selection.collapsed)
{
return null;
}

var focus = selection.endContainer;
if (!focus || focus.nodeType !== 3)
{
// expected to be a text node
return null;
}

textNode = focus;
text = focus.nodeValue.substring(0, selection.endOffset);
}
else
{
var $input = this.$input;
$input.autofocus();

selection = $input.getSelection();

if (!selection || selection.end <= 1)
{
return false;
}

text = $input.val().substring(0, selection.end);
}

var lastAt = text.lastIndexOf(this.options.at);

if (lastAt === -1) // no 'at'
{
return null;
}

if (lastAt === 0 || text.substr(lastAt - 1, 1).match(/(\s|[\](,]|--)/))
{
var afterAt = text.substr(lastAt + 1);
if (!afterAt.match(/\s/) || afterAt.length <= 15)
{
return {
text: text,
textNode: textNode,
start: lastAt,
query: afterAt.replace(new RegExp(String.fromCharCode(160), 'g'), ' '),
range: selection
};
}
}

return null;
},

foundMatch: function(query)
{
if (this.pendingQuery === query)
{
return;
}

this.pendingQuery = query;

if (query.length >= this.options.minLength && query.substr(0, 1) !== '[')
{
this.getPendingQueryOptions();
}
},

getPendingQueryOptions: function()
{
XF.ajax(
this.options.method, this.options.url, { q: this.pendingQuery },
XF.proxy(this, 'handlePendingQueryOptions'),
{ global: false, error: false }
);
},

handlePendingQueryOptions: function(data)
{
var current = this.getCurrentMatchInfo();

if (!data.q || !current || data.q !== current.query)
{
return;
}

if (data.results && data.results.length)
{
this.show(data.q, data.results);
}
else
{
this.hide();
}
},

insertResult: function(result)
{
this.hide();

var matchInfo = this.getCurrentMatchInfo();
if (!matchInfo)
{
return;
}

var afterAtPos = matchInfo.start + 1,
range = matchInfo.range;

if (this.ed)
{
this.ed.selection.save();

XF.EditorHelpers.focus(this.ed);

var node = matchInfo.textNode,
text = node.nodeValue,
suffix = '\u00a0',
insert;

var insertRef = node.splitText(this.options.keepAt ? afterAtPos : afterAtPos - 1);
insertRef.textContent = text.substr(afterAtPos + matchInfo.query.length);

if (this.options.insertMode === 'html')
{
insert = $.parseHTML(result + suffix);
}
else
{
insert = document.createTextNode(result + suffix);
}

$(insertRef).before(insert);

node.parentNode.normalize();

this.ed.selection.restore();
}
else
{
var $input = this.$input;
$input.autofocus();

if (afterAtPos !== -1)
{
$input.setSelection(matchInfo.start, range.end);
$input.replaceSelectedText((this.options.keepAt ? this.options.at : '') + result + ' ', 'collapseToEnd');
}
}
},

show: function(val, results)
{
var matchInfo = this.getCurrentMatchInfo(),
$input = this.$input,
inputDimensions = $input.dimensions(),
t = this;

if (!matchInfo)
{
return;
}

this.visible = true;

if (this.ed)
{
var range = matchInfo.range;

this.results.showResults(val, results, $input, function($results)
{
if (!range || !range.getBoundingClientRect)
{
var start = range.startContainer,
$start = start.nodeType === 3 ?  $(start.parentNode) : $(start),
startDims = $start.dimensions();

return {
top: startDims.bottom + 3,
left: inputDimensions.left + 5
};
}

var startRange = range.cloneRange();

// Set the range to start before the @ and cover it. This works around a problem where the @ is the
// first character on the line and when the cursor is before it, it's on the previous line.
startRange.setStart(matchInfo.textNode, matchInfo.start);
startRange.setEnd(matchInfo.textNode, matchInfo.start + 1);

var rect = startRange.getBoundingClientRect();

return t.getResultPositionForSelection(
rect.left,
rect.bottom,
range.getBoundingClientRect().left,
$results,
inputDimensions
);
});
}
else
{
this.results.showResults(val, results, $input, function($results)
{
var $div = $('<div />'),
computedCss = window.getComputedStyle($input[0]),
name,
applyCss = '';

for (var i = 0; i < computedCss.length; i++)
{
name = computedCss[i];
applyCss += name + ': ' + computedCss.getPropertyValue(name) + '; ';
}

$div[0].style.cssText = applyCss;
$div.css({
position: 'absolute',
height: '',
width: $input.outerWidth(),
opacity: 0,
top: 0,
left: '-9999px'
});
$div[0].textContent = $input.val();
$div.appendTo(document.body);

var testRange = document.createRange();

testRange.setStart($div[0].firstChild, matchInfo.start);
testRange.setEnd($div[0].firstChild, matchInfo.start + 1);

var rect = testRange.getBoundingClientRect(),
divDimensions = $div.dimensions(),
startLeft, startBottom, endLeft;

startLeft = inputDimensions.left + (rect.left - divDimensions.left);
startBottom = inputDimensions.top + (rect.bottom - divDimensions.top);

testRange.setStart($div[0].firstChild, matchInfo.start + 1 + matchInfo.query.length);
testRange.setEnd($div[0].firstChild, matchInfo.start + 1 + matchInfo.query.length);
rect = testRange.getBoundingClientRect();

endLeft = inputDimensions.left + (rect.left - divDimensions.left);

$div.remove();

return t.getResultPositionForSelection(startLeft, startBottom, endLeft, $results, inputDimensions);
});
}
},

getResultPositionForSelection: function(startX, startY, endX, $results, inputDimensions)
{
var resultsWidth = $results.width(),
targetTop = startY + $(window).scrollTop() + 3,
targetLeft = startX;

if (targetLeft + resultsWidth > inputDimensions.right)
{
targetLeft = endX - resultsWidth;
}

if (targetLeft < inputDimensions.left)
{
targetLeft = inputDimensions.left;
}

return {
top: targetTop,
left: targetLeft
};
},

hide: function()
{
if (this.visible)
{
this.visible = false;
this.results.hideResults();
}
}
});

XF.pageDisplayTime = Date.now();
$(XF.onPageLoad);

$(window).on('pageshow', function()
{
if (!XF.pageDisplayTime || Date.now() > XF.pageDisplayTime)
{
XF.pageDisplayTime = Date.now();
}
});
}
(window.jQuery, window, document);