Advertisement
mere1y

Untitled

Feb 1st, 2023
559
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /**
  2.  * Create the XenForo namespace
  3.  * @package XenForo
  4.  */
  5. var XenForo = {};
  6.  
  7. // _XF_JS_UNCOMPRESSED_TEST_ - do not edit/remove
  8.  
  9. if (jQuery === undefined) jQuery = $ = {};
  10. if ($.tools === undefined) console.error('jQuery Tools is not loaded.');
  11.  
  12.  
  13.  
  14. function animateCSS(node, animationName, callback) {
  15.  
  16.     var isString = animationName instanceof String;
  17.     if (isString) {
  18.         animationName = ['animated', animationName];
  19.     } else {
  20.         animationName = ['animated'].concat(animationName);
  21.     }
  22.  
  23.     for (var i = 0; i < animationName.length; i++) {
  24.         node.classList.add(animationName[i]);
  25.     }
  26.  
  27.  
  28.     function handleAnimationEnd() {
  29.         for (var i = 0; i < animationName.length; i++) {
  30.             node.classList.remove(animationName[i]);
  31.         }
  32.  
  33.         node.removeEventListener('animationend', handleAnimationEnd);
  34.  
  35.         if (typeof callback === 'function') callback()
  36.     }
  37.  
  38.  
  39.     node.addEventListener('animationend', handleAnimationEnd);
  40. }
  41.  
  42. var isScrolledIntoView = function (elem) {
  43.     var docViewTop = $(window).scrollTop() + $('#header').height();
  44.     var docViewBottom = docViewTop + $(window).height();
  45.  
  46.     var elemTop = $(elem).offset().top;
  47.     var elemBottom = elemTop + $(elem).height();
  48.  
  49.     return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
  50. };
  51.  
  52. var supports_html5_storage = function () {
  53.     try {
  54.         return 'localStorage' in window && window['localStorage'] !== null;
  55.     } catch (e) {
  56.         return false;
  57.     }
  58. };
  59.  
  60. function isElementInViewport(el) {
  61.     var rect = el[0].getBoundingClientRect();
  62.     return (
  63.         rect.top >= 0 &&
  64.         rect.left >= 0 &&
  65.         rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  66.         rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  67.     );
  68. }
  69.  
  70. function scrollParentToChild(parent, child) {
  71.     var parentRect = parent.getBoundingClientRect();
  72.     var parentViewableArea = { height: parent.clientHeight, width: parent.clientWidth };
  73.     var childRect = child.getBoundingClientRect();
  74.     var isViewable = (childRect.top >= parentRect.top) && (childRect.bottom <= parentRect.top + parentViewableArea.height);
  75.     if (!isViewable) {
  76.         const scrollTop = childRect.top - parentRect.top;
  77.         parent.scrollTop += scrollTop;
  78.     }
  79. }
  80.  
  81. /**
  82.  * Deal with Firebug not being present
  83.  */
  84. !function (w) {
  85.     var fn, i = 0;
  86.     if (!w.console) w.console = {};
  87.     if (w.console.log && !w.console.debug) w.console.debug = w.console.log;
  88.     fn = ['assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 'getFirebugElement', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 'notifyFirebug', 'profile', 'profileEnd', 'time', 'timeEnd', 'trace', 'warn'];
  89.     for (i = 0; i < fn.length; ++i) if (!w.console[fn[i]]) w.console[fn[i]] = function () {
  90.     };
  91. }(window);
  92.  
  93.  
  94. /** @param {jQuery} $ jQuery Object */
  95. !function ($, window, document, _undefined) {
  96.     var isTouchBrowser = (function () {
  97.         var _isTouchBrowserVal;
  98.  
  99.         try {
  100.             _isTouchBrowserVal = 'ontouchstart' in document.documentElement;
  101.             //_isTouchBrowserVal = !!('ontouchstart' in window || navigator.maxTouchPoints || navigator.msMaxTouchPoints);
  102.         } catch (e) {
  103.             _isTouchBrowserVal = !!(navigator.userAgent.indexOf('webOS') != -1);
  104.         }
  105.  
  106.         return function () {
  107.             return _isTouchBrowserVal;
  108.         };
  109.     })();
  110.  
  111.     var classes = ['hasJs'];
  112.     classes.push(isTouchBrowser() ? 'Touch' : 'NoTouch');
  113.  
  114.     var div = document.createElement('div');
  115.     classes.push(('draggable' in div) || ('ondragstart' in div && 'ondrop' in div) ? 'HasDragDrop' : 'NoDragDrop');
  116.  
  117.     // not especially nice but...
  118.     if (navigator.userAgent.search(/\((iPhone|iPad|iPod);/) != -1) {
  119.         classes.push('iOS');
  120.     }
  121.  
  122.     var $html = $('html');
  123.     $html.addClass(classes.join(' ')).removeClass('NoJs');
  124.  
  125.     /**
  126.      * Fix IE abbr handling
  127.      */
  128.     document.createElement('abbr');
  129.  
  130.     /**
  131.      * Detect mobile webkit
  132.      */
  133.     if (/webkit.*mobile/i.test(navigator.userAgent)) {
  134.         XenForo._isWebkitMobile = true;
  135.     }
  136.  
  137.     // preserve original jQuery Tools .overlay()
  138.     jQuery.fn._jQueryToolsOverlay = jQuery.fn.overlay;
  139.  
  140.     /**
  141.      * Extends jQuery core
  142.      */
  143.     jQuery.extend(true,
  144.         {
  145.             /**
  146.              * Sets the context of 'this' within a called function.
  147.              * Takes identical parameters to $.proxy, but does not
  148.              * enforce the one-elment-one-method merging that $.proxy
  149.              * does, allowing multiple objects of the same type to
  150.              * bind to a single element's events (for example).
  151.              *
  152.              * @param function|object Function to be called | Context for 'this', method is a property of fn
  153.              * @param function|string Context for 'this' | Name of method within fn to be called
  154.              *
  155.              * @return function
  156.              */
  157.             context: function (fn, context) {
  158.                 if (typeof context == 'string') {
  159.                     var _context = fn;
  160.                     fn = fn[context];
  161.                     context = _context;
  162.                 }
  163.  
  164.                 return fn.bind(context)
  165.             },
  166.  
  167.             /**
  168.              * Sets a cookie.
  169.              *
  170.              * @param string cookie name (escaped)
  171.              * @param mixed cookie value
  172.              * @param string cookie expiry date
  173.              *
  174.              * @return mixed cookie value
  175.              */
  176.             setCookie: function (name, value, expires) {
  177.                 console.log('Set cookie %s = %s', name, value);
  178.  
  179.                 document.cookie = XenForo._cookieConfig.prefix + name + '=' + encodeURIComponent(value)
  180.                     + (expires === undefined ? '' : ';expires=' + expires.toUTCString())
  181.                     + (XenForo._cookieConfig.path ? ';path=' + XenForo._cookieConfig.path : '')
  182.                     + (XenForo._cookieConfig.domain ? ';domain=' + XenForo._cookieConfig.domain : '');
  183.  
  184.                 return value;
  185.             },
  186.  
  187.             /**
  188.              * Fetches the value of a named cookie.
  189.              *
  190.              * @param string Cookie name (escaped)
  191.              *
  192.              * @return string Cookie value
  193.              */
  194.             getCookie: function (name) {
  195.                 var expr, cookie;
  196.  
  197.                 expr = new RegExp('(^| )' + XenForo._cookieConfig.prefix + name + '=([^;]+)(;|$)');
  198.                 cookie = expr.exec(document.cookie);
  199.  
  200.                 if (cookie) {
  201.                     return decodeURIComponent(cookie[2]);
  202.                 } else {
  203.                     return null;
  204.                 }
  205.             },
  206.  
  207.             /**
  208.              * Deletes a cookie.
  209.              *
  210.              * @param string Cookie name (escaped)
  211.              *
  212.              * @return null
  213.              */
  214.             deleteCookie: function (name) {
  215.                 console.info('Delete cookie %s', name);
  216.  
  217.                 document.cookie = XenForo._cookieConfig.prefix + name + '='
  218.                     + (XenForo._cookieConfig.path ? '; path=' + XenForo._cookieConfig.path : '')
  219.                     + (XenForo._cookieConfig.domain ? '; domain=' + XenForo._cookieConfig.domain : '')
  220.                     + '; expires=Thu, 01-Jan-70 00:00:01 GMT';
  221.  
  222.                 return null;
  223.             }
  224.         });
  225.  
  226.     /**
  227.      * Extends jQuery functions
  228.      */
  229.     jQuery.fn.extend(
  230.         {
  231.             /**
  232.              * Wrapper for XenForo.activate, for 'this' element
  233.              *
  234.              * @return jQuery
  235.              */
  236.             xfActivate: function () {
  237.                 return XenForo.activate(this);
  238.             },
  239.  
  240.             /**
  241.              * Retuns .data(key) for this element, or the default value if there is no data
  242.              *
  243.              * @param string key
  244.              * @param mixed defaultValue
  245.              *
  246.              * @return mixed
  247.              */
  248.             dataOrDefault: function (key, defaultValue) {
  249.                 var value = this.data(key);
  250.  
  251.                 if (value === undefined) {
  252.                     return defaultValue;
  253.                 }
  254.  
  255.                 return value;
  256.             },
  257.  
  258.             /**
  259.              * Like .val() but also trims trailing whitespace
  260.              */
  261.             strval: function () {
  262.                 return String(this.val()).replace(/\s+$/g, '');
  263.             },
  264.  
  265.             /**
  266.              * Get the 'name' attribute of an element, or if it exists, the value of 'data-fieldName'
  267.              *
  268.              * @return string
  269.              */
  270.             fieldName: function () {
  271.                 return this.data('fieldname') || this.attr('name');
  272.             },
  273.  
  274.             /**
  275.              * Get the value that would be submitted with 'this' element's name on form submit
  276.              *
  277.              * @return string
  278.              */
  279.             fieldValue: function () {
  280.                 switch (this.attr('type')) {
  281.                     case 'checkbox': {
  282.                         return $('input:checkbox[name="' + this.fieldName() + '"]:checked', this.context.form).val();
  283.                     }
  284.  
  285.                     case 'radio': {
  286.                         return $('input:radio[name="' + this.fieldName() + '"]:checked', this.context.form).val();
  287.                     }
  288.  
  289.                     default: {
  290.                         return this.val();
  291.                     }
  292.                 }
  293.             },
  294.  
  295.             _jqSerialize: $.fn.serialize,
  296.  
  297.             /**
  298.              * Overridden jQuery serialize method to ensure that RTE areas are serialized properly.
  299.              */
  300.             serialize: function () {
  301.                 $('textarea.BbCodeWysiwygEditor').each(function () {
  302.                     var data = $(this).data('XenForo.BbCodeWysiwygEditor');
  303.                     if (data) {
  304.                         data.syncEditor();
  305.                     }
  306.                 });
  307.  
  308.                 return this._jqSerialize();
  309.             },
  310.  
  311.             _jqSerializeArray: $.fn.serializeArray,
  312.  
  313.             /**
  314.              * Overridden jQuery serializeArray method to ensure that RTE areas are serialized properly.
  315.              */
  316.             serializeArray: function () {
  317.                 $('textarea.BbCodeWysiwygEditor').each(function () {
  318.                     var data = $(this).data('XenForo.BbCodeWysiwygEditor');
  319.                     if (data) {
  320.                         data.syncEditor();
  321.                     }
  322.                 });
  323.  
  324.                 return this._jqSerializeArray();
  325.             },
  326.  
  327.             /**
  328.              * Returns the position and size of an element, including hidden elements.
  329.              *
  330.              * If the element is hidden, it will very quickly un-hides a display:none item,
  331.              * gets its offset and size, restore the element to its hidden state and returns values.
  332.              *
  333.              * @param string inner/outer/{none} Defines the jQuery size function to use
  334.              * @param string offset/position/{none} Defines the jQuery position function to use (default: offset)
  335.              *
  336.              * @return object Offset { left: float, top: float }
  337.              */
  338.             coords: function (sizeFn, offsetFn) {
  339.                 var coords,
  340.                     visibility,
  341.                     display,
  342.                     widthFn,
  343.                     heightFn,
  344.                     hidden = this.is(':hidden');
  345.  
  346.                 if (hidden) {
  347.                     visibility = this.css('visibility'),
  348.                         display = this.css('display');
  349.  
  350.                     this.css(
  351.                         {
  352.                             visibility: 'hidden',
  353.                             display: 'block'
  354.                         });
  355.                 }
  356.  
  357.                 switch (sizeFn) {
  358.                     case 'inner': {
  359.                         widthFn = 'innerWidth';
  360.                         heightFn = 'innerHeight';
  361.                         break;
  362.                     }
  363.                     case 'outer': {
  364.                         widthFn = 'outerWidth';
  365.                         heightFn = 'outerHeight';
  366.                         break;
  367.                     }
  368.                     default: {
  369.                         widthFn = 'width';
  370.                         heightFn = 'height';
  371.                     }
  372.                 }
  373.  
  374.                 switch (offsetFn) {
  375.                     case 'position': {
  376.                         offsetFn = 'position';
  377.                         break;
  378.                     }
  379.  
  380.                     default: {
  381.                         offsetFn = 'offset';
  382.                         break;
  383.                     }
  384.                 }
  385.  
  386.                 coords = this[offsetFn]();
  387.                 coords.width = this[widthFn]();
  388.                 coords.height = this[heightFn]();
  389.  
  390.                 if (hidden) {
  391.                     this.css(
  392.                         {
  393.                             display: display,
  394.                             visibility: visibility
  395.                         });
  396.                 }
  397.  
  398.                 return coords;
  399.             },
  400.  
  401.             /**
  402.              * Sets a unique id for an element, if one is not already present
  403.              */
  404.             uniqueId: function () {
  405.                 if (!this.attr('id')) {
  406.                     this.attr('id', 'XenForoUniq' + XenForo._uniqueIdCounter++);
  407.                 }
  408.  
  409.                 return this;
  410.             },
  411.  
  412.             /**
  413.              * Wrapper functions for commonly-used animation effects, so we can customize their behaviour as required
  414.              */
  415.             xfFadeIn: function (speed, callback) {
  416.                 return this.fadeIn(speed, function () {
  417.                     $(this).ieOpacityFix(callback);
  418.                 });
  419.             },
  420.             xfFadeOut: function (speed, callback) {
  421.                 return this.fadeOut(speed, callback);
  422.             },
  423.             xfShow: function (speed, callback) {
  424.                 return this.show(speed, function () {
  425.                     $(this).ieOpacityFix(callback);
  426.                 });
  427.             },
  428.             xfHide: function (speed, callback) {
  429.                 return this.hide(speed, callback);
  430.             },
  431.             xfSlideDown: function (speed, callback) {
  432.                 return this.slideDown(speed, function () {
  433.                     $(this).ieOpacityFix(callback);
  434.                 });
  435.             },
  436.             xfSlideUp: function (speed, callback) {
  437.                 return this.slideUp(speed, callback);
  438.             },
  439.  
  440.             /**
  441.              * Animates an element opening a space for itself, then fading into that space
  442.              *
  443.              * @param integer|string Speed of fade-in
  444.              * @param function Callback function on completion
  445.              *
  446.              * @return jQuery
  447.              */
  448.             xfFadeDown: function (fadeSpeed, callback, finish) {
  449.                 this.filter(':hidden').xfHide().css('opacity', 0);
  450.  
  451.                 fadeSpeed = fadeSpeed || XenForo.speed.normal;
  452.                 if (finish) {
  453.                     $(this).clearQueue().finish();
  454.                 }
  455.  
  456.                 return this
  457.                     .xfSlideDown(XenForo.speed.fast)
  458.                     .animate({ opacity: 1 }, fadeSpeed, function () {
  459.                         $(this).ieOpacityFix(callback);
  460.                     });
  461.             },
  462.  
  463.             /**
  464.              * Animates an element fading out then closing the gap left behind
  465.              *
  466.              * @param integer|string Speed of fade-out - if this is zero, there will be no animation at all
  467.              * @param function Callback function on completion
  468.              * @param integer|string Slide speed - ignored if fadeSpeed is zero
  469.              * @param string Easing method
  470.              *
  471.              * @return jQuery
  472.              */
  473.             xfFadeUp: function (fadeSpeed, callback, slideSpeed, easingMethod, finish) {
  474.                 fadeSpeed = ((typeof fadeSpeed == 'undefined' || fadeSpeed === null) ? XenForo.speed.normal : fadeSpeed);
  475.                 slideSpeed = ((typeof slideSpeed == 'undefined' || slideSpeed === null) ? fadeSpeed : slideSpeed);
  476.  
  477.                 if (finish) {
  478.                     $(this).clearQueue().finish();
  479.                 }
  480.  
  481.                 return this
  482.                     .slideUp({
  483.                         duration: Math.max(fadeSpeed, slideSpeed),
  484.                         easing: easingMethod || 'swing',
  485.                         complete: callback,
  486.                         queue: false
  487.                     })
  488.                     .animate({ opacity: 0, queue: false }, fadeSpeed);
  489.             },
  490.  
  491.             /**
  492.              * Inserts and activates content into the DOM, using xfFadeDown to animate the insertion
  493.              *
  494.              * @param string jQuery method with which to insert the content
  495.              * @param string Selector for the previous parameter
  496.              * @param string jQuery method with which to animate the showing of the content
  497.              * @param string|integer Speed at which to run the animation
  498.              * @param function Callback for when the animation is complete
  499.              *
  500.              * @return jQuery
  501.              */
  502.             xfInsert: function (insertMethod, insertReference, animateMethod, animateSpeed, callback) {
  503.                 if (insertMethod == 'replaceAll') {
  504.                     $(insertReference).xfFadeUp(animateSpeed);
  505.                 }
  506.  
  507.                 this
  508.                     .addClass('__XenForoActivator')
  509.                     .css('display', 'none')
  510.                 [insertMethod || 'appendTo'](insertReference)
  511.                     .xfActivate()
  512.                 [animateMethod || 'xfFadeDown'](animateSpeed, callback);
  513.  
  514.                 return this;
  515.             },
  516.  
  517.             /**
  518.              * Removes an element from the DOM, animating its removal with xfFadeUp
  519.              * All parameters are optional.
  520.              *
  521.              *  @param string animation method
  522.              *  @param function callback function
  523.              *  @param integer Sliding speed
  524.              *  @param string Easing method
  525.              *
  526.              * @return jQuery
  527.              */
  528.             xfRemove: function (animateMethod, callback, slideSpeed, easingMethod) {
  529.                 return this[animateMethod || 'xfFadeUp'](XenForo.speed.normal, function () {
  530.                     $(this).empty().remove();
  531.  
  532.                     if ($.isFunction(callback)) {
  533.                         callback();
  534.                     }
  535.                 }, slideSpeed, easingMethod);
  536.             },
  537.  
  538.             /**
  539.              * Prepares an element for xfSlideIn() / xfSlideOut()
  540.              *
  541.              * @param boolean If true, return the height of the wrapper
  542.              *
  543.              * @return jQuery|integer
  544.              */
  545.             _xfSlideWrapper: function (getHeight) {
  546.                 if (!this.data('slidewrapper')) {
  547.                     this.data('slidewrapper', this.wrap('<div class="_swOuter"><div class="_swInner" /></div>')
  548.                         .closest('div._swOuter').css('overflow', 'hidden'));
  549.                 }
  550.  
  551.                 if (getHeight) {
  552.                     try {
  553.                         return this.data('slidewrapper').height();
  554.                     } catch (e) {
  555.                         // so IE11 seems to be randomly throwing an exception in jQuery here, so catch it
  556.                         return 0;
  557.                     }
  558.                 }
  559.  
  560.                 return this.data('slidewrapper');
  561.             },
  562.  
  563.             /**
  564.              * Slides content in (down), with content glued to lower edge, drawer-like
  565.              *
  566.              * @param duration
  567.              * @param easing
  568.              * @param callback
  569.              *
  570.              * @return jQuery
  571.              */
  572.             xfSlideIn: function (duration, easing, callback) {
  573.                 var $wrap = this._xfSlideWrapper().css('height', 'auto'),
  574.                     height = 0;
  575.  
  576.                 $wrap.find('div._swInner').css('margin', 'auto');
  577.                 height = this.show(0).outerHeight();
  578.  
  579.                 $wrap
  580.                     .css('height', 0)
  581.                     .animate({ height: height }, duration, easing, function () {
  582.                         $wrap.css('height', '');
  583.                     })
  584.                     .find('div._swInner')
  585.                     .css('marginTop', height * -1)
  586.                     .animate({ marginTop: 0 }, duration, easing, callback);
  587.  
  588.                 return this;
  589.             },
  590.  
  591.             /**
  592.              * Slides content out (up), reversing xfSlideIn()
  593.              *
  594.              * @param duration
  595.              * @param easing
  596.              * @param callback
  597.              *
  598.              * @return jQuery
  599.              */
  600.             xfSlideOut: function (duration, easing, callback) {
  601.                 var height = this.outerHeight();
  602.  
  603.                 this._xfSlideWrapper()
  604.                     .animate({ height: 0 }, duration, easing)
  605.                     .find('div._swInner')
  606.                     .animate({ marginTop: height * -1 }, duration, easing, callback);
  607.  
  608.                 return this;
  609.             },
  610.  
  611.             /**
  612.              * Workaround for IE's font-antialiasing bug when dealing with opacity
  613.              *
  614.              * @param function Callback
  615.              */
  616.             ieOpacityFix: function (callback) {
  617.                 //ClearType Fix
  618.                 if (!$.support.opacity) {
  619.                     this.css('filter', '');
  620.                     this.attr('style', this.attr('style').replace(/filter:\s*;/i, ''));
  621.                 }
  622.  
  623.                 if ($.isFunction(callback)) {
  624.                     callback.apply(this);
  625.                 }
  626.  
  627.                 return this;
  628.             },
  629.  
  630.             /**
  631.              * Wraps around jQuery Tools .overlay().
  632.              *
  633.              * Prepares overlay options before firing overlay() for best possible experience.
  634.              * For example, removes fancy (slow) stuff from options for touch browsers.
  635.              *
  636.              * @param options
  637.              *
  638.              * @returns jQuery
  639.              */
  640.             overlay: function (options) {
  641.                 if (XenForo.isTouchBrowser()) {
  642.                     return this._jQueryToolsOverlay($.extend(true, options,
  643.                         {
  644.                             //mask: false,
  645.                             speed: 0,
  646.                             loadSpeed: 0
  647.                         }));
  648.                 } else {
  649.                     return this._jQueryToolsOverlay(options);
  650.                 }
  651.             }
  652.         });
  653.  
  654.     /* jQuery Tools Extensions */
  655.  
  656.     /**
  657.      * Effect method for jQuery.tools overlay.
  658.      * Slides down a container, then fades up the content.
  659.      * Closes by reversing the animation.
  660.      */
  661.  
  662.     $.tools.overlay.addEffect('zoomIn',
  663.         function (position, callback) {
  664.             var $overlay = this.getOverlay();
  665.  
  666.             $overlay.find('.content').css('opacity', 0);
  667.  
  668.             if (this.getConf().fixed) {
  669.                 position.position = 'fixed';
  670.             } else {
  671.                 position.position = 'absolute';
  672.                 position.top += $(window).scrollTop();
  673.                 position.left += $(window).scrollLeft();
  674.             }
  675.  
  676.             $overlay.removeClass('animated zoomOut faster').css(position);
  677.             $overlay.show(0, callback);
  678.  
  679.             animateCSS($overlay.get(0), ['zoomIn', 'faster']);
  680.         },
  681.         function (callback) {
  682.             var $overlay = this.getOverlay();
  683.             $overlay.removeClass('animated zoomIn faster');
  684.             animateCSS($overlay.get(0), ['zoomOut', 'faster'], function () {
  685.                 callback();
  686.                 $overlay.hide(0, callback);
  687.             });
  688.         },
  689.     );
  690.  
  691.     $.tools.overlay.addEffect('slideDownContentFade',
  692.         function (position, callback) {
  693.             var $overlay = this.getOverlay(),
  694.                 conf = this.getConf();
  695.  
  696.             $overlay.find('.content').css('opacity', 0);
  697.  
  698.             if (this.getConf().fixed) {
  699.                 position.position = 'fixed';
  700.             } else {
  701.                 position.position = 'absolute';
  702.                 position.top += $(window).scrollTop();
  703.                 position.left += $(window).scrollLeft();
  704.             }
  705.  
  706.             $overlay.css(position).xfSlideDown(XenForo.speed.fast, function () {
  707.                 $overlay.find('.content').animate({ opacity: 1 }, conf.speed, function () {
  708.                     $(this).ieOpacityFix(callback);
  709.                 });
  710.             });
  711.         },
  712.         function (callback) {
  713.             var $overlay = this.getOverlay();
  714.  
  715.             $overlay.find('.content').animate({ opacity: 0 }, this.getConf().speed, function () {
  716.                 $overlay.xfSlideUp(XenForo.speed.fast, callback);
  717.             });
  718.         }
  719.     );
  720.  
  721.     $.tools.overlay.addEffect('slideDown',
  722.         function (position, callback) {
  723.             if (this.getConf().fixed) {
  724.                 position.position = 'fixed';
  725.             } else {
  726.                 position.position = 'absolute';
  727.                 position.top += $(window).scrollTop();
  728.                 position.left += $(window).scrollLeft();
  729.             }
  730.  
  731.             this.getOverlay()
  732.                 .css(position)
  733.                 .xfSlideDown(this.getConf().speed, callback);
  734.         },
  735.         function (callback) {
  736.             this.getOverlay().hide(0, callback);
  737.         }
  738.     );
  739.  
  740.     // *********************************************************************
  741.  
  742.     $.extend(XenForo,
  743.         {
  744.             /**
  745.              * Cache for overlays
  746.              *
  747.              * @var object
  748.              */
  749.             _OverlayCache: {},
  750.  
  751.             /**
  752.              * Defines whether or not an AJAX request is known to be in progress
  753.              *
  754.              *  @var boolean
  755.              */
  756.             _AjaxProgress: false,
  757.  
  758.             /**
  759.              * Defines a variable that can be overridden to force/control the base HREF
  760.              * used to canonicalize AJAX requests
  761.              *
  762.              * @var string
  763.              */
  764.             ajaxBaseHref: '',
  765.  
  766.             /**
  767.              * Counter for unique ID generation
  768.              *
  769.              * @var integer
  770.              */
  771.             _uniqueIdCounter: 0,
  772.  
  773.             /**
  774.              * Configuration for overlays, should be redefined in the PAGE_CONTAINER template HTML
  775.              *
  776.              * @var object
  777.              */
  778.             _overlayConfig: {},
  779.  
  780.             /**
  781.              * Contains the URLs of all externally loaded resources from scriptLoader
  782.              *
  783.              * @var object
  784.              */
  785.             _loadedScripts: {},
  786.  
  787.             /**
  788.              * Configuration for cookies
  789.              *
  790.              * @var object
  791.              */
  792.             _cookieConfig: { path: '/', domain: '', 'prefix': 'xf_' },
  793.  
  794.             /**
  795.              * Flag showing whether or not the browser window has focus. On load, assume true.
  796.              *
  797.              * @var boolean
  798.              */
  799.             _hasFocus: true,
  800.  
  801.             /**
  802.              * @var object List of server-related time info (now, today, todayDow)
  803.              */
  804.             serverTimeInfo: {},
  805.  
  806.             /**
  807.              * @var object Information about the XenForo visitor. Usually contains user_id.
  808.              */
  809.             visitor: {},
  810.  
  811.             /**
  812.              * @var integer Time the page was loaded.
  813.              */
  814.             _pageLoadTime: (new Date()).getTime() / 1000,
  815.  
  816.             /**
  817.              * JS version key, to force refreshes when needed
  818.              *
  819.              * @var string
  820.              */
  821.             _jsVersion: '',
  822.  
  823.             /**
  824.              * If true, disables reverse tabnabbing protection
  825.              *
  826.              * @var bool
  827.              */
  828.             _noRtnProtect: false,
  829.  
  830.             /**
  831.              * CSRF Token
  832.              *
  833.              * @var string
  834.              */
  835.             _csrfToken: '',
  836.  
  837.             /**
  838.              * URL to CSRF token refresh.
  839.              *
  840.              * @var string
  841.              */
  842.             _csrfRefreshUrl: '',
  843.  
  844.             _noSocialLogin: false,
  845.  
  846.             /**
  847.              * Speeds for animation
  848.              *
  849.              * @var object
  850.              */
  851.             speed:
  852.             {
  853.                 xxfast: 50,
  854.                 xfast: 100,
  855.                 fast: 200,
  856.                 normal: 400,
  857.                 slow: 600
  858.             },
  859.  
  860.             /**
  861.              * Multiplier for animation speeds
  862.              *
  863.              * @var float
  864.              */
  865.             _animationSpeedMultiplier: 1,
  866.  
  867.             /**
  868.              * Enable overlays or use regular pages
  869.              *
  870.              * @var boolean
  871.              */
  872.             _enableOverlays: true,
  873.  
  874.             /**
  875.              * Enables AJAX submission via AutoValidator. Doesn't change things other than
  876.              * that. Useful to disable for debugging.
  877.              *
  878.              * @var boolean
  879.              */
  880.             _enableAjaxSubmit: true,
  881.  
  882.             /**
  883.              * Determines whether the lightbox shows all images from the current page,
  884.              * or just from an individual message
  885.              *
  886.              * @var boolean
  887.              */
  888.             _lightBoxUniversal: false,
  889.  
  890.             /**
  891.              * @var object Phrases
  892.              */
  893.             phrases: {},
  894.  
  895.             /**
  896.              * Binds all registered functions to elements within the DOM
  897.              */
  898.             init: function () {
  899.                 var dStart = new Date(),
  900.                     xfFocus = function () {
  901.                         XenForo._hasFocus = true;
  902.                         $(document).triggerHandler('XenForoWindowFocus');
  903.                     },
  904.                     xfBlur = function () {
  905.                         XenForo._hasFocus = false;
  906.                         $(document).triggerHandler('XenForoWindowBlur');
  907.                     },
  908.                     $html = $('html');
  909.  
  910.                 if ($.browser.msie) {
  911.                     $(document).bind(
  912.                         {
  913.                             focusin: xfFocus,
  914.                             focusout: xfBlur
  915.                         });
  916.                 } else {
  917.                     $(window).bind(
  918.                         {
  919.                             focus: xfFocus,
  920.                             blur: xfBlur
  921.                         });
  922.                 }
  923.  
  924.                 $(window).on('resize', function () {
  925.                     XenForo.checkQuoteSizing($(document));
  926.                 });
  927.  
  928.                 // Set the animation speed based around the style property speed multiplier
  929.                 XenForo.setAnimationSpeed(XenForo._animationSpeedMultiplier);
  930.  
  931.                 // Periodical timestamp refresh
  932.                 XenForo._TimestampRefresh = new XenForo.TimestampRefresh();
  933.  
  934.                 // Find any ignored content that has not been picked up by PHP
  935.                 XenForo.prepareIgnoredContent();
  936.  
  937.                 // init ajax progress indicators
  938.                 XenForo.AjaxProgress();
  939.  
  940.                 // Activate all registered controls
  941.                 XenForo.activate(document);
  942.  
  943.                 $(document).on('click', '.bbCodeQuote .quoteContainer .quoteExpand', function (e) {
  944.                     $(this).closest('blockquote.quote').css('max-height', 'unset')
  945.                     $(this).closest('.quoteContainer').toggleClass('expanded');
  946.                 });
  947.  
  948.                 XenForo.watchProxyLinks();
  949.                 if (!XenForo._noRtnProtect) {
  950.                     XenForo.watchExternalLinks();
  951.                 }
  952.  
  953.                 // make the breadcrumb and navigation responsive
  954.                 if (!$html.hasClass('NoResponsive')) {
  955.                     XenForo.updateVisibleBreadcrumbs();
  956.                     XenForo.updateVisibleNavigationTabs();
  957.                     XenForo.updateVisibleNavigationLinks();
  958.  
  959.                     var resizeTimer, htmlWidth = $html.width();
  960.                     $(window).on('resize orientationchange load', function (e) {
  961.                         if (resizeTimer) {
  962.                             return;
  963.                         }
  964.                         if (e.type != 'load' && $html.width() == htmlWidth) {
  965.                             return;
  966.                         }
  967.                         htmlWidth = $html.width();
  968.                         resizeTimer = setTimeout(function () {
  969.                             resizeTimer = 0;
  970.                             XenForo.updateVisibleBreadcrumbs();
  971.                             XenForo.updateVisibleNavigationTabs();
  972.                             XenForo.updateVisibleNavigationLinks();
  973.                         }, 20);
  974.                     });
  975.                     $(document).on('click', '.breadcrumb .placeholder', function () {
  976.                         $(this).closest('.breadcrumb').addClass('showAll');
  977.                         XenForo.updateVisibleBreadcrumbs();
  978.                     });
  979.                 }
  980.  
  981.                 // Periodical CSRF token refresh
  982.                 XenForo._CsrfRefresh = new XenForo.CsrfRefresh();
  983.  
  984.                 // Autofocus for non-supporting browsers
  985.                 if (!('autofocus' in document.createElement('input'))) {
  986.                     //TODO: work out a way to prevent focusing if something else already has focus http://www.w3.org/TR/html5/forms.html#attr-fe-autofocus
  987.                     $('input[autofocus], textarea[autofocus], select[autofocus]').first().focus();
  988.                 }
  989.  
  990.  
  991.                 // init Tweet buttons
  992.                 XenForo.tweetButtonInit();
  993.  
  994.                 console.info('XenForo.init() %dms. jQuery %s/%s', new Date() - dStart, $().jquery, $.tools.version);
  995.  
  996.                 if ($('#ManualDeferredTrigger').length) {
  997.                     setTimeout(XenForo.manualDeferredHandler, 100);
  998.                 }
  999.  
  1000.                 if ($('html.RunDeferred').length) {
  1001.                     setTimeout(XenForo.runAutoDeferred, 100);
  1002.                 }
  1003.             },
  1004.  
  1005.             runAutoDeferred: function () {
  1006.                 XenForo.ajax('deferred.php', {}, function (ajaxData) {
  1007.                     if (ajaxData && ajaxData.moreDeferred) {
  1008.                         setTimeout(XenForo.runAutoDeferred, 100);
  1009.                     }
  1010.                 }, { error: false, global: false });
  1011.             },
  1012.  
  1013.             prepareIgnoredContent: function () {
  1014.                 var $displayLink = $('a.DisplayIgnoredContent'),
  1015.                     namesObj = {},
  1016.                     namesArr = [];
  1017.  
  1018.                 if ($displayLink.length) {
  1019.                     $('.ignored').each(function () {
  1020.                         var name = $(this).data('author');
  1021.                         if (name) {
  1022.                             namesObj[name] = true;
  1023.                         }
  1024.                     });
  1025.  
  1026.                     $.each(namesObj, function (name) {
  1027.                         namesArr.push(name);
  1028.                     });
  1029.  
  1030.                     if (namesArr.length) {
  1031.                         $displayLink.attr('title', XenForo.phrases['show_hidden_content_by_x'].replace(/\{names\}/, namesArr.join(', ')));
  1032.                         $displayLink.parent().show();
  1033.                     }
  1034.                 }
  1035.             },
  1036.  
  1037.             watchProxyLinks: function () {
  1038.                 var proxyLinkClick = function (e) {
  1039.                     var $this = $(this),
  1040.                         proxyHref = $this.data('proxy-href'),
  1041.                         lastEvent = $this.data('proxy-handler-last');
  1042.  
  1043.                     if (!proxyHref) {
  1044.                         return;
  1045.                     }
  1046.  
  1047.                     // we may have a direct click event and a bubbled event. Ensure they don't both fire.
  1048.                     if (lastEvent && lastEvent == e.timeStamp) {
  1049.                         return;
  1050.                     }
  1051.                     $this.data('proxy-handler-last', e.timeStamp);
  1052.  
  1053.                     XenForo.ajax(proxyHref, {}, function (ajaxData) {
  1054.                     }, { error: false, global: false });
  1055.                 };
  1056.  
  1057.                 $(document)
  1058.                     .on('click', 'a.ProxyLink', proxyLinkClick)
  1059.                     .on('focusin', 'a.ProxyLink', function (e) {
  1060.                         // This approach is taken because middle click events do not bubble. This is a way of
  1061.                         // getting the equivalent of event bubbling on middle clicks in Chrome.
  1062.                         var $this = $(this);
  1063.                         if ($this.data('proxy-handler')) {
  1064.                             return;
  1065.                         }
  1066.  
  1067.                         $this.data('proxy-handler', true)
  1068.                             .click(proxyLinkClick);
  1069.                     });
  1070.             },
  1071.  
  1072.             watchExternalLinks: function () {
  1073.                 var externalLinkClick = function (e) {
  1074.                     if (e.isDefaultPrevented()) {
  1075.                         return;
  1076.                     }
  1077.  
  1078.                     var $this = $(this),
  1079.                         href = $this.attr('href'),
  1080.                         lastEvent = $this.data('blank-handler-last');
  1081.                     if (!href) {
  1082.                         return;
  1083.                     }
  1084.  
  1085.                     const urlRegExp = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/;
  1086.                     if (!urlRegExp.test(href)) {
  1087.                         return;
  1088.                     }
  1089.  
  1090.                     href = XenForo.canonicalizeUrl(href);
  1091.  
  1092.                     var regex = new RegExp('^[a-z]+://' + location.host + '(/|$|:)', 'i');
  1093.                     if (regex.test(href) && !$this.hasClass('ProxyLink')) {
  1094.                         // if the link is local, then don't do the special processing... unless it's a proxy link
  1095.                         // so it's likely to be external after the redirect
  1096.                         return;
  1097.                     }
  1098.  
  1099.                     // we may have a direct click event and a bubbled event. Ensure they don't both fire.
  1100.                     if (lastEvent && lastEvent == e.timeStamp) {
  1101.                         return;
  1102.                     }
  1103.  
  1104.                     $this.data('blank-handler-last', e.timeStamp);
  1105.  
  1106.                     var ua = navigator.userAgent,
  1107.                         isOldIE = ua.indexOf('MSIE') !== -1,
  1108.                         isSafari = ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') == -1,
  1109.                         isGecko = ua.indexOf('Gecko/') !== -1;
  1110.  
  1111.                     if (e.shiftKey && isGecko) {
  1112.                         // Firefox doesn't trigger when holding shift. If the code below runs, it will force
  1113.                         // opening in a new tab instead of a new window, so stop. Note that Chrome still triggers here,
  1114.                         // but it does open in a new window anyway so we run the normal code.
  1115.                         return;
  1116.                     }
  1117.                     if (isSafari && (e.shiftKey || e.altKey)) {
  1118.                         // this adds to reading list or downloads instead of opening a new tab
  1119.                         return;
  1120.                     }
  1121.                     if (isOldIE) {
  1122.                         // IE has mitigations for this and this blocks referrers
  1123.                         return;
  1124.                     }
  1125.  
  1126.                     // now run the opener clearing
  1127.  
  1128.                     if (isSafari) {
  1129.                         // Safari doesn't work with the other approach
  1130.                         // Concept from: https://github.com/danielstjules/blankshield
  1131.                         var $iframe, iframeDoc, $script;
  1132.  
  1133.                         $iframe = $('<iframe style="display: none" />').appendTo(document.body);
  1134.                         iframeDoc = $iframe[0].contentDocument || $iframe[0].contentWindow.document;
  1135.  
  1136.                         iframeDoc.__href = href; // set this so we don't need to do an eval-type thing
  1137.  
  1138.                         $script = $('<script />', iframeDoc);
  1139.                         $script[0].text = 'window.opener=null;' +
  1140.                             'window.parent=null;window.top=null;window.frameElement=null;' +
  1141.                             'window.open(document.__href).opener = null;';
  1142.  
  1143.                         iframeDoc.body.appendChild($script[0]);
  1144.                         $iframe.remove();
  1145.                     } else {
  1146.                         // use this approach for the rest to maintain referrers when possible
  1147.                         var w = window.open(href);
  1148.  
  1149.                         try {
  1150.                             // this can potentially fail, don't want to break
  1151.                             w.opener = null;
  1152.                         } catch (e) {
  1153.                         }
  1154.                     }
  1155.  
  1156.                     e.preventDefault();
  1157.                 };
  1158.  
  1159.                 $(document)
  1160.                     .on('click', 'a[target=_blank]:not(.fr-element a)', externalLinkClick)
  1161.                     .on('focusin', 'a[target=_blank]:not(.fr-element a)', function (e) {
  1162.                         // This approach is taken because middle click events do not bubble. This is a way of
  1163.                         // getting the equivalent of event bubbling on middle clicks in Chrome.
  1164.                         var $this = $(this);
  1165.                         if ($this.data('blank-handler')) {
  1166.                             return;
  1167.                         }
  1168.  
  1169.                         $this.data('blank-handler', true)
  1170.                             .click(externalLinkClick);
  1171.                     });
  1172.             },
  1173.  
  1174.             /**
  1175.              * Asynchronously load the specified JavaScript, with an optional callback on completion.
  1176.              *
  1177.              * @param string Script source
  1178.              * @param object Callback function
  1179.              * @param string innerHtml for the script tags
  1180.              */
  1181.             loadJs: function (src, callback, innerHTML) {
  1182.                 try {
  1183.                     var script = document.createElement('script');
  1184.                     script.async = true;
  1185.                     if (innerHTML) {
  1186.                         try {
  1187.                             script.innerHTML = innerHTML;
  1188.                         } catch (e2) {
  1189.                         }
  1190.                     }
  1191.                     var f = function () {
  1192.                         if (callback) {
  1193.                             callback();
  1194.                             callback = null;
  1195.                         }
  1196.                     };
  1197.                     script.onload = f;
  1198.                     script.onreadystatechange = function () {
  1199.                         if (script.readyState === 'loaded') {
  1200.                             f();
  1201.                         }
  1202.                     };
  1203.                     script.src = src;
  1204.                     document.getElementsByTagName('head')[0].appendChild(script);
  1205.                 } catch (e) {
  1206.                 }
  1207.             },
  1208.  
  1209.             /**
  1210.              * Asynchronously load the Twitter button JavaScript.
  1211.              */
  1212.             tweetButtonInit: function () {
  1213.                 if ($('a.twitter-share-button').length) {
  1214.                     XenForo.loadJs('https://platform.twitter.com/widgets.js');
  1215.                 }
  1216.             },
  1217.  
  1218.             /**
  1219.              * Asynchronously load the +1 button JavaScript.
  1220.              */
  1221.             plusoneButtonInit: function (el) {
  1222.                 if ($(el).find('div.g-plusone, .GoogleLogin').length) {
  1223.                     var locale = $('html').attr('lang');
  1224.  
  1225.                     var callback = function () {
  1226.                         if (!window.gapi) {
  1227.                             return;
  1228.                         }
  1229.  
  1230.                         $(el).find('.GoogleLogin').each(function () {
  1231.                             var $button = $(this),
  1232.                                 clientId = $button.data('client-id'),
  1233.                                 auth2;
  1234.  
  1235.                             gapi.load('auth2', function () {
  1236.                                 auth2 = gapi.auth2.init({
  1237.                                     client_id: clientId,
  1238.                                     scope: 'profile email',
  1239.                                     response_type: 'permission',
  1240.                                     cookie_policy: 'single_host_origin'
  1241.                                 });
  1242.                                 $button.on('click', function () {
  1243.                                     auth2.grantOfflineAccess({
  1244.                                         scope: 'profile email'
  1245.                                     })
  1246.                                         .then(function (resp) {
  1247.                                             var code = resp.code;
  1248.  
  1249.                                             if (code) {
  1250.                                                 window.location = XenForo.canonicalizeUrl(
  1251.                                                     $button.data('redirect-url').replace('__CODE__', code)
  1252.                                                 );
  1253.                                             }
  1254.                                         })
  1255.                                         .catch(function () {
  1256.                                         });
  1257.                                 });
  1258.  
  1259.  
  1260.                             });
  1261.                         });
  1262.                     };
  1263.  
  1264.                     if (window.___gcfg && window.gapi) {
  1265.                         callback();
  1266.                     } else {
  1267.                         window.___gcfg = {
  1268.                             lang: locale,
  1269.                             isSignedOut: true // this is to stop the "welcome back" prompt as it doesn't fit with our flow
  1270.                         };
  1271.  
  1272.                         XenForo.loadJs('https://apis.google.com/js/api:client.js', callback);
  1273.                     }
  1274.                 }
  1275.             },
  1276.  
  1277.             /**
  1278.              * Prevents Google Chrome's AutoFill from turning inputs yellow.
  1279.              * Adapted from http://www.benjaminmiles.com/2010/11/22/fixing-google-chromes-yellow-autocomplete-styles-with-jquery/
  1280.              */
  1281.             chromeAutoFillFix: function ($root) {
  1282.                 if ($.browser.webkit && navigator.userAgent.toLowerCase().indexOf('chrome') >= 0) {
  1283.                     if (!$root) {
  1284.                         $root = $(document);
  1285.                     }
  1286.  
  1287.                     // trap an error here - CloudFlare RocketLoader causes an error with this.
  1288.                     var $inputs;
  1289.                     try {
  1290.                         $inputs = $root.find('input:-webkit-autofill');
  1291.                     } catch (e) {
  1292.                         $inputs = $([]);
  1293.                     }
  1294.  
  1295.                     if ($inputs.length) {
  1296.                         $inputs.each(function (i) {
  1297.                             var $this = $(this),
  1298.                                 val = $this.val();
  1299.  
  1300.                             if (!val || !val.length) {
  1301.                                 return;
  1302.                             }
  1303.  
  1304.                             $this.after($this.clone(true).val(val)).remove();
  1305.                         });
  1306.                     }
  1307.                 }
  1308.             },
  1309.  
  1310.             updateVisibleBreadcrumbs: function () {
  1311.                 $('.breadcrumb').each(function () {
  1312.                     var container = this,
  1313.                         $container = $(container);
  1314.  
  1315.                     $container.find('.placeholder').remove();
  1316.  
  1317.                     var $crusts = $container.find('.crust');
  1318.                     $crusts.removeClass('firstVisibleCrumb').show();
  1319.  
  1320.                     var $homeCrumb = $crusts.filter('.homeCrumb');
  1321.  
  1322.                     $container.css('height', '');
  1323.                     var beforeHeight = container.offsetHeight;
  1324.                     $container.css('height', 'auto');
  1325.  
  1326.                     if (container.offsetHeight <= beforeHeight) {
  1327.                         $container.css('height', '');
  1328.                         return;
  1329.                     }
  1330.  
  1331.                     var $lastHidden = null,
  1332.                         hideSkipSelector = '.selectedTabCrumb, :last-child';
  1333.  
  1334.                     $crusts.each(function () {
  1335.                         var $crust = $(this);
  1336.                         if ($crust.is(hideSkipSelector)) {
  1337.                             return true;
  1338.                         }
  1339.  
  1340.                         $crust.hide();
  1341.                         $lastHidden = $crust;
  1342.                         return (container.offsetHeight > beforeHeight);
  1343.                     });
  1344.  
  1345.                     if (!$lastHidden) {
  1346.                         $container.css('height', '');
  1347.                         return;
  1348.                     }
  1349.  
  1350.                     var $placeholder = $('<span class="crust placeholder"><a class="crumb" href="javascript:"><span>...</span></a><span class="arrow"><span>&gt;</span></span></span>');
  1351.                     $lastHidden.after($placeholder);
  1352.  
  1353.                     if (container.offsetHeight > beforeHeight) {
  1354.                         var $prev = $lastHidden.prevAll('.crust:not(' + hideSkipSelector + ')').last();
  1355.                         if ($prev.length) {
  1356.                             $prev.hide();
  1357.                         }
  1358.                     }
  1359.  
  1360.                     if (container.offsetHeight > beforeHeight) {
  1361.                         var $next = $lastHidden.nextAll('.crust:not(.placeholder, ' + hideSkipSelector + ')').first();
  1362.                         if ($next.length) {
  1363.                             $next.hide();
  1364.                             $next.after($placeholder);
  1365.                         }
  1366.                     }
  1367.  
  1368.                     if ($homeCrumb.length && !$homeCrumb.is(':visible')) {
  1369.                         $container.find('.crust:visible:first').addClass('firstVisibleCrumb');
  1370.                     }
  1371.  
  1372.                     if (container.offsetHeight <= beforeHeight) {
  1373.                         // firefox doesn't seem to contain the breadcrumbs despite the overflow hidden
  1374.                         $container.css('height', '');
  1375.                     }
  1376.                 });
  1377.             },
  1378.  
  1379.             updateVisibleNavigationTabs: function () {
  1380.                 var $tabs = $('#navigation').find('.navTabs');
  1381.                 if (!$tabs.length) {
  1382.                     return;
  1383.                 }
  1384.  
  1385.                 var tabsCoords = $tabs.coords(),
  1386.                     $publicTabs = $tabs.find('.publicTabs'),
  1387.                     $publicInnerTabs = $publicTabs.find('> .navTab'),
  1388.                     $visitorTabs = $tabs.find('.visitorTabs'),
  1389.                     $visitorInnerTabs = $visitorTabs.find('> .navTab'),
  1390.                     $visitorCounter = $('#VisitorExtraMenu_Counter'),
  1391.                     maxPublicWidth,
  1392.                     $hiddenTab = $publicInnerTabs.filter('.navigationHiddenTabs');
  1393.  
  1394.                 $publicInnerTabs.show();
  1395.                 $hiddenTab.hide();
  1396.  
  1397.                 $visitorInnerTabs.show();
  1398.                 $visitorCounter.addClass('ResponsiveOnly');
  1399.  
  1400.                 if ($tabs.is('.showAll')) {
  1401.                     return;
  1402.                 }
  1403.  
  1404.                 maxPublicWidth = $tabs.width() - $visitorTabs.width() - 1;
  1405.  
  1406.                 var hidePublicTabs = function () {
  1407.                     var shownSel = '.selected, .navigationHiddenTabs';
  1408.  
  1409.                     var $hideable = $publicInnerTabs.filter(':not(' + shownSel + ')'),
  1410.                         $hiddenList = $('<ul />'),
  1411.                         hiddenCount = 0,
  1412.                         overflowMenuShown = false;
  1413.  
  1414.                     $.each($hideable.get().reverse(), function () {
  1415.                         var $this = $(this);
  1416.                         if (isOverflowing($publicTabs.coords(), true)) {
  1417.                             $hiddenList.prepend(
  1418.                                 $('<li />').html($this.find('.navLink').clone())
  1419.                             );
  1420.                             $this.hide();
  1421.                             hiddenCount++;
  1422.                         } else {
  1423.                             if (hiddenCount) {
  1424.                                 $hiddenTab.show();
  1425.  
  1426.                                 if (isOverflowing($publicTabs.coords(), true)) {
  1427.                                     $hiddenList.prepend(
  1428.                                         $('<li />').html($this.find('.navLink').clone())
  1429.                                     );
  1430.                                     $this.hide();
  1431.                                     hiddenCount++;
  1432.                                 }
  1433.                                 $('#NavigationHiddenMenu').html($hiddenList).xfActivate();
  1434.                                 overflowMenuShown = true;
  1435.                             } else {
  1436.                                 $hiddenTab.hide();
  1437.                             }
  1438.  
  1439.                             return false;
  1440.                         }
  1441.                     });
  1442.  
  1443.                     if (hiddenCount && !overflowMenuShown) {
  1444.                         $hiddenTab.show();
  1445.                         $('#NavigationHiddenMenu').html($hiddenList).xfActivate();
  1446.                     }
  1447.                 },
  1448.                     hideVisitorTabs = function () {
  1449.                         $visitorInnerTabs.hide();
  1450.                         $visitorInnerTabs.filter('.account, .selected').show();
  1451.                         $visitorCounter.removeClass('ResponsiveOnly');
  1452.                     },
  1453.                     isOverflowing = function (coords, checkMax) {
  1454.                         if (
  1455.                             coords.top >= tabsCoords.top + tabsCoords.height
  1456.                             || coords.height >= tabsCoords.height * 2
  1457.                         ) {
  1458.                             return true;
  1459.                         }
  1460.  
  1461.                         if (checkMax && coords.width > maxPublicWidth) {
  1462.                             return true;
  1463.                         }
  1464.  
  1465.                         return false;
  1466.                     };
  1467.  
  1468.                 if ($visitorTabs.length) {
  1469.                     if (isOverflowing($visitorTabs.coords())) {
  1470.                         hidePublicTabs();
  1471.  
  1472.                         if (isOverflowing($visitorTabs.coords())) {
  1473.                             hideVisitorTabs();
  1474.                         }
  1475.                     }
  1476.                 } else if (isOverflowing($publicTabs.coords())) {
  1477.                     hidePublicTabs();
  1478.                 }
  1479.             },
  1480.  
  1481.             updateVisibleNavigationLinks: function () {
  1482.                 var $linksList = $('#navigation').find('.navTab.selected .blockLinksList');
  1483.                 if (!$linksList.length) {
  1484.                     return;
  1485.                 }
  1486.  
  1487.                 var $links = $linksList.find('> li'),
  1488.                     listOffset = $linksList.offset(),
  1489.                     $hidden = $links.filter('.navigationHidden'),
  1490.                     $firstHidden = false;
  1491.  
  1492.                 $links.show();
  1493.                 $hidden.hide();
  1494.  
  1495.                 if ($linksList.is('.showAll')) {
  1496.                     return;
  1497.                 }
  1498.  
  1499.                 var hiddenForMenu = [],
  1500.                     $lastLink = $links.filter(':not(.navigationHidden)').last(),
  1501.                     hideOffset = 0,
  1502.                     hasHidden = false,
  1503.                     lastCoords,
  1504.                     $link;
  1505.  
  1506.                 if (!$lastLink.length) {
  1507.                     return;
  1508.                 }
  1509.  
  1510.                 do {
  1511.                     lastCoords = $lastLink.coords();
  1512.                     if (lastCoords.top > listOffset.top + lastCoords.height) {
  1513.                         $link = $links.eq(hideOffset);
  1514.                         $link.hide();
  1515.                         hiddenForMenu.push($link);
  1516.                         hideOffset++;
  1517.  
  1518.                         if (!hasHidden) {
  1519.                             hasHidden = true;
  1520.  
  1521.                             if (!$hidden.length) {
  1522.                                 $hidden = $('<li class="navigationHidden Popup PopupControl PopupClosed"><a rel="Menu" class="NoPopupGadget">...</a><div class="Menu blockLinksList primaryContent" id="NavigationLinksHiddenMenu"></div></li>');
  1523.                                 $linksList.append($hidden);
  1524.                                 new XenForo.PopupMenu($hidden);
  1525.                             } else {
  1526.                                 $hidden.show();
  1527.                             }
  1528.                         }
  1529.                     } else {
  1530.                         break;
  1531.                     }
  1532.                 }
  1533.                 while (hideOffset < $links.length);
  1534.  
  1535.                 if (hasHidden) {
  1536.                     if (hideOffset < $links.length) {
  1537.                         var coords = $hidden.coords();
  1538.                         if (coords.top > listOffset.top + coords.height) {
  1539.                             $link = $links.eq(hideOffset);
  1540.                             $link.hide();
  1541.                             hiddenForMenu.push($link);
  1542.                         }
  1543.                     }
  1544.  
  1545.                     var $hiddenList = $('<ul />');
  1546.                     $(hiddenForMenu).each(function () {
  1547.                         $hiddenList.append(
  1548.                             $('<li />').html($(this).find('a').clone())
  1549.                         );
  1550.                     });
  1551.                     $('#NavigationLinksHiddenMenu').html($hiddenList).xfActivate();
  1552.                 }
  1553.             },
  1554.  
  1555.             /**
  1556.              * Binds a function to elements to fire on a custom event
  1557.              *
  1558.              * @param string jQuery selector - to get the elements to be bound
  1559.              * @param function Function to fire
  1560.              * @param string Custom event name (if empty, assume 'XenForoActivateHtml')
  1561.              */
  1562.             register: function (selector, fn, event) {
  1563.                 if (typeof fn == 'string') {
  1564.                     var className = fn;
  1565.                     fn = function (i) {
  1566.                         XenForo.create(className, this);
  1567.                     };
  1568.                 }
  1569.  
  1570.                 $(document).bind(event || 'XenForoActivateHtml', function (e) {
  1571.                     $(e.element).find(selector).each(fn);
  1572.                 });
  1573.             },
  1574.  
  1575.             /**
  1576.              * Creates a new object of class XenForo.{functionName} using
  1577.              * the specified element, unless one has already been created.
  1578.              *
  1579.              * @param string Function name (property of XenForo)
  1580.              * @param object HTML element
  1581.              *
  1582.              * @return object XenForo[functionName]($(element))
  1583.              */
  1584.             create: function (className, element) {
  1585.                 var $element = $(element),
  1586.                     xfObj = window,
  1587.                     parts = className.split('.'), i;
  1588.  
  1589.                 for (i = 0; i < parts.length; i++) {
  1590.                     xfObj = xfObj[parts[i]];
  1591.                 }
  1592.  
  1593.                 if (typeof xfObj != 'function') {
  1594.                     return console.error('%s is not a function.', className);
  1595.                 }
  1596.  
  1597.                 if (!$element.data(className)) {
  1598.                     $element.data(className, new xfObj($element));
  1599.                 }
  1600.  
  1601.                 return $element.data(className);
  1602.             },
  1603.  
  1604.             /**
  1605.              * Fire the initialization events and activate functions for the specified element
  1606.              *
  1607.              * @param object Usually jQuery
  1608.              *
  1609.              * @return object
  1610.              */
  1611.             activate: function (element) {
  1612.                 var $element = $(element);
  1613.  
  1614.                 console.group('XenForo.activate(%o)', element);
  1615.  
  1616.                 $element.trigger('XenForoActivate').removeClass('__XenForoActivator');
  1617.                 $element.find('noscript').empty().remove();
  1618.  
  1619.                 XenForo._TimestampRefresh.refresh(element, true);
  1620.  
  1621.                 $(document)
  1622.                     .trigger({ element: element, type: 'XenForoActivateHtml' })
  1623.                     .trigger({ element: element, type: 'XenForoActivatePopups' })
  1624.                     .trigger({ element: element, type: 'XenForoActivationComplete' });
  1625.  
  1626.                 var $form = $element.find('form.AutoSubmit:first');
  1627.                 if ($form.length) {
  1628.                     $(document).trigger('PseudoAjaxStart');
  1629.                     $form.submit();
  1630.                     $form.find('input[type="submit"], input[type="reset"]').hide();
  1631.                 }
  1632.  
  1633.                 XenForo.checkQuoteSizing($element);
  1634.                 XenForo.plusoneButtonInit(element);
  1635.                 //XenForo.Facebook.start();
  1636.  
  1637.                 console.groupEnd();
  1638.  
  1639.                 return element;
  1640.             },
  1641.  
  1642.             checkQuoteSizing: function ($element) {
  1643.                 $element.find('.bbCodeQuote .quoteContainer').each(function () {
  1644.                     var self = this,
  1645.                         delay = 0,
  1646.                         checkHeight = function () {
  1647.                             var $self = $(self),
  1648.                                 quote = $self.find('.quote')[0];
  1649.  
  1650.                             if (!quote) {
  1651.                                 return;
  1652.                             }
  1653.  
  1654.                             if (quote.scrollHeight == 0 || quote.offsetHeight == 0) {
  1655.                                 if (delay < 2000) {
  1656.                                     setTimeout(checkHeight, delay);
  1657.                                     delay += 100;
  1658.                                 }
  1659.                                 return;
  1660.                             }
  1661.  
  1662.                             // +1 resolves a chrome rounding issue
  1663.                             if (quote.scrollHeight > parseInt($(quote).css('max-height')) && quote.scrollHeight > quote.offsetHeight + 1) {
  1664.                                 $self.find('.quoteExpand').addClass('quoteCut');
  1665.                             } else {
  1666.                                 $self.find('.quoteExpand').removeClass('quoteCut');
  1667.                             }
  1668.                         };
  1669.  
  1670.                     checkHeight();
  1671.                     $(this).find('img, iframe').one('load', checkHeight);
  1672.                     $(this).on('elementResized', checkHeight);
  1673.                 });
  1674.             },
  1675.  
  1676.             /**
  1677.              * Pushes an additional parameter onto the data to be submitted via AJAX
  1678.              *
  1679.              * @param array|string Data parameters - either from .serializeArray() or .serialize()
  1680.              * @param string Name of parameter
  1681.              * @param mixed Value of parameter
  1682.              *
  1683.              * @return array|string Data including new parameter
  1684.              */
  1685.             ajaxDataPush: function (data, name, value) {
  1686.                 if (!data || typeof data == 'string') {
  1687.                     // data is empty, or a url string - &name=value
  1688.                     data = String(data);
  1689.                     data += '&' + encodeURIComponent(name) + '=' + encodeURIComponent(value);
  1690.                 } else if (data instanceof FormData) {
  1691.                     // data is FormData
  1692.                     data.append(name, value)
  1693.                 } else if (data[0] !== undefined) {
  1694.                     // data is a numerically-keyed array of name/value pairs
  1695.                     data.push({ name: name, value: value });
  1696.                 } else {
  1697.                     // data is an object with a single set of name & value properties
  1698.                     data[name] = value;
  1699.                 }
  1700.  
  1701.                 return data;
  1702.             },
  1703.  
  1704.             /**
  1705.              * Wraps around jQuery's own $.ajax function, with our own defaults provided.
  1706.              * Will submit via POST and expect JSON back by default.
  1707.              * Server errors will be handled using XenForo.handleServerError
  1708.              *
  1709.              * @param string URL to load
  1710.              * @param object Data to pass
  1711.              * @param function Success callback function
  1712.              * @param object Additional options to override or extend defaults
  1713.              *
  1714.              * @return XMLHttpRequest
  1715.              */
  1716.             ajax: function (url, data, success, options) {
  1717.                 if (!url) {
  1718.                     return console.error('No URL specified for XenForo.ajax()');
  1719.                 }
  1720.  
  1721.                 url = XenForo.canonicalizeUrl(url, XenForo.ajaxBaseHref);
  1722.                 try {
  1723.                     let url_element = new URL(url);
  1724.                     if (url_element.pathname === "/index.php") {
  1725.                         url_element.pathname = url_element.search.split("&")[0].slice(1)
  1726.                         url_element.search = "?" + url_element.search.slice(url_element.search.split("&")[0].length + 1)
  1727.                     }
  1728.                     if (url_element.pathname.indexOf('&') !== -1) {
  1729.                         let elements = url_element.pathname.split("&");
  1730.                         url_element.pathname = elements[0]
  1731.                         elements.shift();
  1732.                         url_element.search = "?" + elements.join("&")
  1733.                     }
  1734.                     url = url_element.toString()
  1735.                 } catch (e) {
  1736.  
  1737.                 }
  1738.  
  1739.                 data = XenForo.ajaxDataPush(data, '_xfRequestUri', window.location.pathname + window.location.search);
  1740.                 data = XenForo.ajaxDataPush(data, '_xfNoRedirect', 1);
  1741.                 if (XenForo._csrfToken) {
  1742.                     data = XenForo.ajaxDataPush(data, '_xfToken', XenForo._csrfToken);
  1743.                 }
  1744.  
  1745.                 var successCallback = function (ajaxData, textStatus) {
  1746.                     if (typeof ajaxData == 'object') {
  1747.                         if (typeof ajaxData._visitor_conversationsUnread != 'undefined') {
  1748.                             if ($('#AlertsMenu_Counter').length && ajaxData._visitor_alertsUnread != '0' && $('#AlertsMenu_Counter').text().trim() != ajaxData._visitor_alertsUnread) {
  1749.                                 const svg = $('#AlertsMenu_Counter')
  1750.                                     .closest('.counter-container')
  1751.                                     .find('svg')
  1752.                                     .attr('class', '') // через addClass РЅР° svg РЅРµ хочет
  1753.                                 setTimeout(() => { svg.attr('class', 'animated tada') }, 1)
  1754.                             }
  1755.                             XenForo.balloonCounterUpdate($('#ConversationsMenu_Counter'), ajaxData._visitor_conversationsUnread);
  1756.                             XenForo.balloonCounterUpdate($('#AlertsMenu_Counter'), ajaxData._visitor_alertsUnread);
  1757.                             XenForo.balloonCounterUpdate($('#VisitorExtraMenu_ConversationsCounter'), ajaxData._visitor_conversationsUnread);
  1758.                             XenForo.balloonCounterUpdate($('#VisitorExtraMenu_AlertsCounter'), ajaxData._visitor_alertsUnread);
  1759.                             XenForo.balloonCounterUpdate($('#VisitorExtraMenu_Counter'),
  1760.                                 (
  1761.                                     parseInt(ajaxData._visitor_conversationsUnread, 10) + parseInt(ajaxData._visitor_alertsUnread, 10)
  1762.                                     || 0
  1763.                                 ).toString()
  1764.                             );
  1765.  
  1766.                             var $alertPopup = $('.alerts.Popup')
  1767.                             if (+ajaxData._visitor_alertsUnread) {
  1768.                                 // https://zelenka.guru/threads/4456536/
  1769.                                 if ($alertPopup.data('XenForo.PopupMenu'))
  1770.                                     $alertPopup.data('XenForo.PopupMenu').$menu.find('.tabs li').first().click()
  1771.                             } else if (+ajaxData._visitor_alertsUnread && $alertPopup.is('.PopupOpen') && !ajaxData.error) {
  1772.                                 const popupMenu = $alertPopup.data('XenForo.PopupMenu')
  1773.                                 if (!popupMenu || !popupMenu.loading) return; // if not loaded yet
  1774.                                 XenForo.ajax(popupMenu.contentSrc, {}, ajaxData => {
  1775.                                     if (XenForo.hasResponseError(ajaxData)) return;
  1776.                                     popupMenu.$menu.find('ol').first().prepend($(ajaxData.templateHtml).find('li.Alert').filter(function () {
  1777.                                         const id = $(this).attr('id')
  1778.                                         return !id || !$('#' + id).length
  1779.                                     })).xfActivate()
  1780.                                 })
  1781.                             }
  1782.                         }
  1783.  
  1784.                         if (ajaxData._manualDeferred) {
  1785.                             XenForo.manualDeferredHandler();
  1786.                         } else if (ajaxData._autoDeferred) {
  1787.                             XenForo.runAutoDeferred();
  1788.                         }
  1789.                     }
  1790.  
  1791.                     $(document).trigger(
  1792.                         {
  1793.                             type: 'XFAjaxSuccess',
  1794.                             ajaxData: ajaxData,
  1795.                             textStatus: textStatus
  1796.                         });
  1797.  
  1798.                     if (success) success.call(null, ajaxData, textStatus);
  1799.                 };
  1800.  
  1801.                 var referrer = window.location.href;
  1802.                 if (referrer.match(/[^\x20-\x7f]/)) {
  1803.                     var a = document.createElement('a');
  1804.                     a.href = '';
  1805.                     referrer = referrer.replace(a.href, XenForo.baseUrl());
  1806.                 }
  1807.                 if (data instanceof FormData)
  1808.                     options = $.extend(true,
  1809.                         {
  1810.                             processData: false,
  1811.                             contentType: false
  1812.                         }, options);
  1813.                 options = $.extend(true,
  1814.                     {
  1815.                         data: data,
  1816.                         url: url,
  1817.                         success: successCallback,
  1818.                         type: 'POST',
  1819.                         dataType: 'json',
  1820.                         xhrFields: {
  1821.                             withCredentials: true
  1822.                         },
  1823.                         error: function (xhr, textStatus, errorThrown) {
  1824.                             if (xhr.readyState == 0) {
  1825.                                 return;
  1826.                             }
  1827.  
  1828.                             try {
  1829.                                 // attempt to pass off to success, if we can decode JSON from the response
  1830.                                 successCallback.call(null, $.parseJSON(xhr.responseText), textStatus);
  1831.                             } catch (e) {
  1832.                                 // not valid JSON, trigger server error handler
  1833.                                 XenForo.handleServerError(xhr, textStatus, errorThrown, {
  1834.                                     success: successCallback,
  1835.                                     options: options,
  1836.                                     url: url,
  1837.                                     data: data
  1838.                                 });
  1839.                             }
  1840.                         },
  1841.                         headers: { 'X-Ajax-Referer': referrer },
  1842.                         timeout: 30000 // 30s
  1843.                     }, options);
  1844.  
  1845.                 // override standard extension, depending on dataType
  1846.                 if (!options.data._xfResponseType) {
  1847.                     switch (options.dataType) {
  1848.                         case 'html':
  1849.                         case 'json':
  1850.                         case 'xml': {
  1851.                             // pass _xfResponseType parameter to override default extension
  1852.                             options.data = XenForo.ajaxDataPush(options.data, '_xfResponseType', options.dataType);
  1853.                             break;
  1854.                         }
  1855.                     }
  1856.                 }
  1857.  
  1858.                 return $.ajax(options);
  1859.             },
  1860.  
  1861.             /**
  1862.              * Updates the total in one of the navigation balloons, showing or hiding if necessary
  1863.              *
  1864.              * @param jQuery $balloon
  1865.              * @param string counter
  1866.              */
  1867.             balloonCounterUpdate: function ($balloon, newTotal) {
  1868.                 if ($balloon.length) {
  1869.                     var $counter = $balloon.find('span.Total'),
  1870.                         oldTotal = $counter.text();
  1871.  
  1872.                     $counter.text(newTotal);
  1873.  
  1874.                     if (!newTotal || newTotal == '0') {
  1875.                         $balloon.fadeOut('fast', function () {
  1876.                             $balloon.addClass('Zero').css('display', '');
  1877.                         });
  1878.                     } else {
  1879.                         $balloon.fadeIn('fast', function () {
  1880.                             $balloon.removeClass('Zero').css('display', '');
  1881.  
  1882.                             var oldTotalInt = parseInt(oldTotal.replace(/[^\d]/, ''), 10),
  1883.                                 newTotalInt = parseInt(newTotal.replace(/[^\d]/, ''), 10),
  1884.                                 newDifference = newTotalInt - oldTotalInt;
  1885.  
  1886.                             if (newDifference > 0 && $balloon.data('text')) {
  1887.                                 var $container = $balloon.closest('.Popup'),
  1888.                                     PopupMenu = $container.data('XenForo.PopupMenu'),
  1889.                                     $message;
  1890.  
  1891.                                 $message = $('<a />').css('cursor', 'pointer').html($balloon.data('text').replace(/%d/, newDifference)).click(function (e) {
  1892.                                     if ($container.is(':visible') && PopupMenu) {
  1893.                                         PopupMenu.$clicker.trigger('click');
  1894.                                     } else if ($container.find('a[href]').length) {
  1895.                                         window.location = XenForo.canonicalizeUrl($container.find('a[href]').attr('href'));
  1896.                                     }
  1897.                                     return false;
  1898.                                 });
  1899.  
  1900.                                 if (PopupMenu && !PopupMenu.menuVisible) {
  1901.                                     PopupMenu.resetLoader();
  1902.                                 }
  1903.  
  1904.                                 //XenForo.stackAlert($message, 10000, $balloon);
  1905.                             }
  1906.                         });
  1907.                     }
  1908.                 }
  1909.             },
  1910.  
  1911.             _manualDeferUrl: '',
  1912.             _manualDeferOverlay: false,
  1913.             _manualDeferXhr: false,
  1914.  
  1915.             manualDeferredHandler: function () {
  1916.                 if (!XenForo._manualDeferUrl || XenForo._manualDeferOverlay) {
  1917.                     return;
  1918.                 }
  1919.  
  1920.                 var processing = XenForo.phrases['processing'] || 'Processing',
  1921.                     cancel = XenForo.phrases['cancel'] || 'Cancel',
  1922.                     cancelling = XenForo.phrases['cancelling'] || 'Cancelling';
  1923.  
  1924.                 var $html = $('<div id="ManualDeferOverlay" class="xenOverlay"><h2 class="titleBar">'
  1925.                     + processing + '... '
  1926.                     + '<a class="CancelDeferred button" data-cancelling="' + cancelling + '..." style="display:none">' + cancel + '</a></h2>'
  1927.                     + '<span class="processingText">' + processing + '...</span><span class="close"></span></div>');
  1928.  
  1929.                 $html.find('.CancelDeferred').click(function (e) {
  1930.                     e.preventDefault();
  1931.                     $.setCookie('cancel_defer', '1');
  1932.                     $(this).text($(this).data('cancelling'));
  1933.                 });
  1934.  
  1935.                 $html.appendTo('body').overlay($.extend(true, {
  1936.                     mask: {
  1937.                         color: 'white',
  1938.                         opacity: 0.77,
  1939.                         loadSpeed: XenForo.speed.normal,
  1940.                         closeSpeed: XenForo.speed.fast
  1941.                     },
  1942.                     closeOnClick: false,
  1943.                     closeOnEsc: false,
  1944.                     oneInstance: false
  1945.                 }, XenForo._overlayConfig, { top: '20%' }));
  1946.                 $html.overlay().load();
  1947.  
  1948.                 XenForo._manualDeferOverlay = $html;
  1949.  
  1950.                 $(document).trigger('PseudoAjaxStart');
  1951.  
  1952.                 var closeOverlay = function () {
  1953.                     XenForo._manualDeferOverlay.overlay().close();
  1954.                     $('#ManualDeferOverlay').remove();
  1955.                     XenForo._manualDeferOverlay = false;
  1956.                     XenForo._manualDeferXhr = false;
  1957.  
  1958.                     $(document).trigger('PseudoAjaxStop');
  1959.                     $(document).trigger('ManualDeferComplete');
  1960.                 };
  1961.  
  1962.                 var fn = function () {
  1963.                     XenForo._manualDeferXhr = XenForo.ajax(XenForo._manualDeferUrl, { execute: 1 }, function (ajaxData) {
  1964.                         if (ajaxData && ajaxData.continueProcessing) {
  1965.                             setTimeout(fn, 0);
  1966.                             XenForo._manualDeferOverlay.find('span').text(ajaxData.status);
  1967.  
  1968.                             var $cancel = XenForo._manualDeferOverlay.find('.CancelDeferred');
  1969.                             if (ajaxData.canCancel) {
  1970.                                 $cancel.show();
  1971.                             } else {
  1972.                                 $cancel.hide();
  1973.                             }
  1974.                         } else {
  1975.                             closeOverlay();
  1976.                         }
  1977.                     }).fail(closeOverlay);
  1978.                 };
  1979.                 fn();
  1980.             },
  1981.  
  1982.             /**
  1983.              * Generic handler for server-level errors received from XenForo.ajax
  1984.              * Attempts to provide a useful error message.
  1985.              *
  1986.              * @param object XMLHttpRequest
  1987.              * @param string Response text
  1988.              * @param string Error thrown
  1989.              * @param object XenForo.ajax params
  1990.              *
  1991.              * @return boolean False
  1992.              */
  1993.             handleServerError: function (xhr, responseText, errorThrown, params) {
  1994.                 var invalidDfIdRegexs = [
  1995.                     /^<html>\n<body>\n<script type="text\/javascript" src="\/aes\.js" ><\/script>/,
  1996.                     /^<html>\n<body>\n\x3Cscript type="text\/javascript" src="\/aes.js" >\x3C\/script>\n/,
  1997.                     /^<!doctype html><html><head><script src="\/process-[^"]+.js"><\/script><\/head><body><script>window\.onload=function\(\)\{process\(\);\}<\/script><noscript><p>Please enable JavaScript and Cookies in your browser\.<\/p><\/noscript><\/body><\/html>$/
  1998.                 ]
  1999.                 var errorPageRegexs = [
  2000.                     /^<!DOCTYPE html>\r\n<html>\r\n<head>\r\n<meta charset="utf-8" \/>\r\n<title>Error 50[0-9]<\/title>\r\n<style type="text\/css">/,
  2001.                     /^<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8" \/>\n<title>Error 50[0-9]<\/title>\n<style type="text\/css">/,
  2002.                     /^<!DOCTYPE html>\r\n<html>\r\n<head>\r\n<meta charset="utf-8" \/>\r\n<title>Site Maintenance<\/title>\r\n<style type="text\/css">/,
  2003.                     /^<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8" \/>\n<title>Site Maintenance<\/title>\n<style type="text\/css">/
  2004.                 ]
  2005.  
  2006.                 // handle timeout and parse error before attempting to decode an error
  2007.                 console.log(responseText)
  2008.                 switch (responseText) {
  2009.                     case 'abort': {
  2010.                         return false;
  2011.                     }
  2012.                     case 'timeout': {
  2013.                         XenForo.alert(
  2014.                             XenForo.phrases.server_did_not_respond_in_time_try_again,
  2015.                             XenForo.phrases.following_error_occurred + ':'
  2016.                         );
  2017.                         return false;
  2018.                     }
  2019.                     case 'parsererror': {
  2020.                         var needToUpdateDfId = false
  2021.                         for (let i = 0; i < invalidDfIdRegexs.length; i++)
  2022.                             if (invalidDfIdRegexs[i].test(xhr.responseText)) {
  2023.                                 needToUpdateDfId = true
  2024.                                 break
  2025.                             }
  2026.  
  2027.                         if (params && !params.options.disableDfIdRefresh && needToUpdateDfId) {
  2028.                             console.log('df id refresh...')
  2029.                             $('body').append(
  2030.                                 $('<iframe src="' + xhr.responseText.split('window.location.href="')[1].split('"')[0] + '" style="display: none;"></iframe>')
  2031.                                     .on('load', function () {
  2032.                                         $(this).remove()
  2033.                                         params.options.disableDfIdRefresh = true
  2034.                                         XenForo.ajax(params.url, params.data, params.success, params.options)
  2035.                                     })
  2036.                             )
  2037.                         } else {
  2038.                             console.error('PHP ' + xhr.responseText);
  2039.                             XenForo.alert('The server responded with an error. The error message is in the JavaScript console.');
  2040.                         }
  2041.                         return false;
  2042.                     }
  2043.                     case 'notmodified':
  2044.                     case 'error': {
  2045.                         if (!xhr || !xhr.responseText) {
  2046.                             // this is likely a user cancellation, so just return
  2047.                             return false;
  2048.                         }
  2049.  
  2050.                         var needToParse = false
  2051.                         for (let i = 0; i < errorPageRegexs.length; i++)
  2052.                             if (errorPageRegexs[i].test(xhr.responseText)) {
  2053.                                 needToParse = true
  2054.                                 break
  2055.                             }
  2056.  
  2057.                         if (needToParse) {
  2058.                             XenForo.alert($(xhr.responseText).closest('article').get(0).innerText.trim().replace('\n\t', '<br>'))
  2059.                             return false;
  2060.                         }
  2061.  
  2062.                         break;
  2063.                     }
  2064.                 }
  2065.  
  2066.                 var contentTypeHeader = xhr.getResponseHeader('Content-Type'),
  2067.                     contentType = false,
  2068.                     data;
  2069.  
  2070.                 if (contentTypeHeader) {
  2071.                     switch (contentTypeHeader.split(';')[0]) {
  2072.                         case 'application/json': {
  2073.                             contentType = 'json';
  2074.                             break;
  2075.                         }
  2076.                         case 'text/html': {
  2077.                             contentType = 'html';
  2078.                             break;
  2079.                         }
  2080.                         default: {
  2081.                             if (xhr.responseText.substr(0, 1) == '{') {
  2082.                                 contentType = 'json';
  2083.                             } else if (xhr.responseText.substr(0, 9) == '<!DOCTYPE') {
  2084.                                 contentType = 'html';
  2085.                             }
  2086.                         }
  2087.                     }
  2088.                 }
  2089.  
  2090.                 if (contentType == 'json' && xhr.responseText.substr(0, 1) == '{') {
  2091.                     // XMLHttpRequest response is probably JSON
  2092.                     try {
  2093.                         data = $.parseJSON(xhr.responseText);
  2094.                     } catch (e) {
  2095.                     }
  2096.  
  2097.                     if (data) {
  2098.                         XenForo.hasResponseError(data, xhr.status);
  2099.                     } else {
  2100.                         XenForo.alert(xhr.responseText, XenForo.phrases.following_error_occurred + ':');
  2101.                     }
  2102.                 } else {
  2103.                     // XMLHttpRequest is some other type...
  2104.                     XenForo.alert(xhr.responseText, XenForo.phrases.following_error_occurred + ':');
  2105.                 }
  2106.  
  2107.                 return false;
  2108.             },
  2109.  
  2110.             /**
  2111.              * Checks for the presence of an 'error' key in the provided data
  2112.              * and displays its contents if found, using an alert.
  2113.              *
  2114.              * @param object ajaxData
  2115.              * @param integer HTTP error code (optional)
  2116.              *
  2117.              * @return boolean|string Returns the error string if found, or false if not found.
  2118.              */
  2119.             hasResponseError: function (ajaxData, httpErrorCode) {
  2120.                 if (typeof ajaxData != 'object') {
  2121.                     XenForo.alert('Response not JSON!'); // debug info, no phrasing
  2122.                     return true;
  2123.                 }
  2124.  
  2125.                 if (ajaxData.errorTemplateHtml) {
  2126.                     new XenForo.ExtLoader(ajaxData, function (data) {
  2127.                         var $overlayHtml = XenForo.alert(
  2128.                             ajaxData.errorTemplateHtml,
  2129.                             XenForo.phrases.following_error_occurred + ':'
  2130.                         );
  2131.                         if ($overlayHtml) {
  2132.                             $overlayHtml.find('div.errorDetails').removeClass('baseHtml');
  2133.                             if (ajaxData.errorOverlayType) {
  2134.                                 $overlayHtml.closest('.errorOverlay').removeClass('errorOverlay').addClass(ajaxData.errorOverlayType);
  2135.                             }
  2136.                         }
  2137.                     });
  2138.  
  2139.                     return ajaxData.error || true;
  2140.                 } else if (ajaxData.error !== undefined) {
  2141.                     // TODO: ideally, handle an array of errors
  2142.                     if (typeof ajaxData.error === 'object') {
  2143.                         var key;
  2144.                         for (key in ajaxData.error) {
  2145.                             break;
  2146.                         }
  2147.                         ajaxData.error = ajaxData.error[key];
  2148.                     }
  2149.  
  2150.                     XenForo.alert(
  2151.                         ajaxData.error + '\n'
  2152.                         + (ajaxData.traceHtml !== undefined ? '<ol class="traceHtml">\n' + ajaxData.traceHtml + '</ol>' : ''),
  2153.                         XenForo.phrases.following_error_occurred + ':'
  2154.                     );
  2155.  
  2156.                     return ajaxData.error;
  2157.                 } else if (ajaxData.status == 'ok' && ajaxData.message) {
  2158.                     XenForo.alert(ajaxData.message, '', 4000);
  2159.                     return true;
  2160.                 } else {
  2161.                     return false;
  2162.                 }
  2163.             },
  2164.  
  2165.             /**
  2166.              * Checks that the supplied ajaxData has a key that can be used to create a jQuery object
  2167.              *
  2168.              *  @param object ajaxData
  2169.              *  @param string key to look for (defaults to 'templateHtml')
  2170.              *
  2171.              *  @return boolean
  2172.              */
  2173.             hasTemplateHtml: function (ajaxData, templateKey) {
  2174.                 templateKey = templateKey || 'templateHtml';
  2175.  
  2176.                 if (!ajaxData[templateKey]) {
  2177.                     return false;
  2178.                 }
  2179.                 if (typeof (ajaxData[templateKey].search) == 'function') {
  2180.                     return (ajaxData[templateKey].search(/\S+/) !== -1);
  2181.                 } else {
  2182.                     return true;
  2183.                 }
  2184.             },
  2185.  
  2186.             /**
  2187.              * Creates an overlay using the given HTML
  2188.              *
  2189.              * @param jQuery Trigger element
  2190.              * @param string|jQuery HTML
  2191.              * @param object Extra options for overlay, will override defaults if specified
  2192.              *
  2193.              * @return jQuery Overlay API
  2194.              */
  2195.             createOverlay: function ($trigger, templateHtml, extraOptions) {
  2196.                 var $overlay = null,
  2197.                     $templateHtml = null,
  2198.                     api = null,
  2199.                     scripts = [],
  2200.                     i;
  2201.                 if (templateHtml instanceof jQuery && templateHtml.is('.xenOverlay')) {
  2202.                     // this is an object that has already been initialised
  2203.                     $overlay = templateHtml.appendTo('body');
  2204.                     $templateHtml = templateHtml;
  2205.                 } else {
  2206.                     const parsed = new DOMParser().parseFromString(templateHtml, 'text/html')
  2207.                     const scriptEls = Array.from($(parsed).find('script').filter(function () {
  2208.                         if (!$(this).text().trim().length) return false
  2209.  
  2210.                         const type = $(this).attr('type')
  2211.                         if (type && type !== 'text/javascript') return false
  2212.  
  2213.                         return true
  2214.                     }))
  2215.                     scripts = scriptEls.map(el => el.innerHTML)
  2216.  
  2217.                     for (const el of scriptEls)
  2218.                         templateHtml = templateHtml.replace(el.outerHTML, '')
  2219.  
  2220.                     $templateHtml = $(templateHtml);
  2221.  
  2222.                     // add a header to the overlay, unless instructed otherwise
  2223.                     if (!$templateHtml.is('.NoAutoHeader')) {
  2224.                         if (extraOptions && extraOptions.title) {
  2225.                             $('<h2 class="heading h1" />')
  2226.                                 .html(extraOptions.title)
  2227.                                 .prependTo($templateHtml);
  2228.                         }
  2229.                     }
  2230.  
  2231.                     // add a cancel button to the overlay, if the overlay is a .formOverlay, has a .submitUnit but has no :reset button
  2232.                     if ($templateHtml.is('.formOverlay')) {
  2233.                         if ($templateHtml.find('.submitUnit').length) {
  2234.                             if (!$templateHtml.find('.submitUnit :reset').length) {
  2235.                                 $templateHtml.find('.submitUnit .button:last')
  2236.                                     .after($('<input type="reset" class="button OverlayCloser" />').val(XenForo.phrases.cancel))
  2237.                                     .after(' ');
  2238.                             }
  2239.                         }
  2240.                     }
  2241.  
  2242.                     // create an overlay container, add the activated template to it and append it to the body.
  2243.                     $overlay = $('<div class="xenOverlay __XenForoActivator" />')
  2244.                         .addClass($(templateHtml).data('overlayclass')) // if content defines data-overlayClass, apply the value to the overlay as a class.
  2245.                         .append($templateHtml);
  2246.  
  2247.  
  2248.                     ($trigger || $overlay).one('onBeforeLoad', function () {
  2249.                         for (i = 0; i < scripts.length; i++) {
  2250.                             $.globalEval(scripts[i]);
  2251.                         }
  2252.                         $overlay.xfActivate()
  2253.                     })
  2254.                 }
  2255.  
  2256.                 var linkRegEx = /([^a-z0-9@-]|^)((?:https?:\/\/|www\.)[^\s"<>\{\}\[\]`_,\(\)]+)/ig
  2257.                 $overlay.find(':not(a):not(textarea):not(script)').each(function () {
  2258.                     var $el = $(this)
  2259.                     if ($el.children().length == 0)
  2260.                         $el.html($el.html().replace(linkRegEx, function (matched, c, link) {
  2261.                             return `${c}<a href="${link}">${link}</a>`
  2262.                         }))
  2263.                 })
  2264.  
  2265.                 if (extraOptions) {
  2266.                     // add {effect}Effect class to overlay container if necessary
  2267.                     if (extraOptions.effect) {
  2268.                         $overlay.addClass(extraOptions.effect + 'Effect');
  2269.                     }
  2270.  
  2271.                     // add any extra class name defined in extraOptions
  2272.                     if (extraOptions.className) {
  2273.                         $overlay.addClass(extraOptions.className);
  2274.                         delete (extraOptions.className);
  2275.                     }
  2276.  
  2277.                     if (extraOptions.noCache) {
  2278.                         extraOptions.onClose = function () {
  2279.                             $overlay.empty().remove();
  2280.                         };
  2281.                     }
  2282.                 }
  2283.  
  2284.                 // add an overlay closer if one does not already exist
  2285.                 let exists = !$overlay.find('.OverlayCloser').length;
  2286.  
  2287.                 if (exists) {
  2288.                     $overlay.prepend('<a class="close OverlayCloser"></a>');
  2289.                 }
  2290.                 $overlay.find('.OverlayCloser').toArray().forEach($element => {
  2291.                     if (!$element.href) {
  2292.                         exists = true;
  2293.                     }
  2294.                 })
  2295.  
  2296.                 if (!exists) {
  2297.                     $overlay.prepend('<a class="close OverlayCloser"></a>');
  2298.                 }
  2299.  
  2300.                 $overlay.find('.OverlayCloser').click(function (e) {
  2301.                     e.stopPropagation();
  2302.                 });
  2303.  
  2304.                 // if no trigger was specified (automatic popup), then activate the overlay instead of the trigger
  2305.                 $trigger = $trigger || $overlay;
  2306.  
  2307.                 var windowHeight = $(window).height();
  2308.  
  2309.                 var fixed = !(
  2310.                     ($.browser.msie && $.browser.version <= 6) // IE6 doesn't support position: fixed;
  2311.                     || XenForo.isTouchBrowser()
  2312.                     || $(window).width() <= 600 // overlay might end up especially tall
  2313.                     || windowHeight <= 550
  2314.                     || $overlay.outerHeight() >= .9 * windowHeight
  2315.                 );
  2316.                 if ($templateHtml.is('.NoFixedOverlay')) {
  2317.                     fixed = false;
  2318.                 }
  2319.  
  2320.                 // activate the overlay
  2321.                 var $modal = new XenForo.Modal($overlay, $.extend(true, {
  2322.                     close: '.OverlayCloser',
  2323.                     closeSpeed: XenForo.speed.slow,
  2324.                     fixed: fixed,
  2325.                     trigger: $trigger
  2326.                 }, XenForo._overlayConfig, extraOptions))
  2327.  
  2328.                 $trigger.bind(
  2329.                     {
  2330.                         onBeforeLoad: function (e) {
  2331.                             $(document).triggerHandler('OverlayOpening');
  2332.                             $('.MenuOpened').removeClass('MenuOpened')
  2333.  
  2334.                             $overlay.find('.Popup').each(function ($popup) {
  2335.                                 var $data = $(this).data('XenForo.PopupMenu')
  2336.                                 if ($data) $data.$menu.css('z-index', 11114)
  2337.                             })
  2338.  
  2339.                             if (XenForo.isTouchBrowser()) {
  2340.                                 const height = $overlay.outerHeight();
  2341.                                 if (height < $(window).height() * 0.9) {
  2342.                                     $overlay.addClass('slim');
  2343.                                 }
  2344.                             }
  2345.                         },
  2346.                         onLoad: function (e) {
  2347.                             var api = $(this).data('overlay'),
  2348.                                 $overlay = api.getOverlay(),
  2349.                                 scroller = $overlay.find('.OverlayScroller').get(0),
  2350.                                 resizeClose = null;
  2351.  
  2352.                             if ($overlay.css('position') == 'absolute') {
  2353.                                 $overlay.find('.overlayScroll').removeClass('overlayScroll');
  2354.                             }
  2355.  
  2356.                             // timeout prevents flicker in FF
  2357.                             if (scroller) {
  2358.                                 setTimeout(function () {
  2359.                                     scroller.scrollIntoView(true);
  2360.                                 }, 0);
  2361.                             }
  2362.  
  2363.                             // autofocus the first form element in a .formOverlay
  2364.                             var $focus = $overlay.find('form').find('input[autofocus], textarea[autofocus], select[autofocus], .AutoFocus').first();
  2365.                             if ($focus.length) {
  2366.                                 $focus.focus();
  2367.                             } else {
  2368.                                 $overlay.find('form').find('input:not([type=hidden], [type=file]), textarea, select, button, .submitUnit a.button').first().focus();
  2369.                             }
  2370.  
  2371.                             // hide on window resize
  2372.                             if (api.getConf().closeOnResize) {
  2373.                                 resizeClose = function () {
  2374.                                     console.info('Window resize, close overlay!');
  2375.                                     api.close();
  2376.                                 };
  2377.  
  2378.                                 $(window).one('resize', resizeClose);
  2379.  
  2380.                                 // remove event when closing the overlay
  2381.                                 $trigger.one('onClose', function () {
  2382.                                     $(window).unbind('resize', resizeClose);
  2383.                                 });
  2384.                             }
  2385.  
  2386.                             $(document).triggerHandler('OverlayOpened');
  2387.                         },
  2388.                         onBeforeClose: function (e) {
  2389.                             $overlay.find('.Popup').each(function () {
  2390.                                 var PopupMenu = $(this).data('XenForo.PopupMenu');
  2391.                                 if (PopupMenu.hideMenu) {
  2392.                                     PopupMenu.hideMenu(e, true);
  2393.                                 }
  2394.                             });
  2395.  
  2396.                             $('.autoCompleteList').each(function () {
  2397.                                 let autoCompleteList = $(this).data('XenForo.AutoCompleteResults')
  2398.                                 if (autoCompleteList && autoCompleteList.hideResults) {
  2399.                                     autoCompleteList.hideResults()
  2400.                                 }
  2401.                             })
  2402.  
  2403.                             $('.autoCompleteListSmilies').each(function () {
  2404.                                 let autoCompleteList = $(this).data('XenForo.AutoSmiliesCompleteResults')
  2405.                                 if (autoCompleteList && autoCompleteList.hideResults) {
  2406.                                     autoCompleteList.hideResults()
  2407.                                 }
  2408.                             })
  2409.                         }
  2410.                     });
  2411.  
  2412.                 api = $trigger.data('overlay');
  2413.                 $overlay.data('overlay', api);
  2414.  
  2415.                 return api;
  2416.             },
  2417.  
  2418.             /**
  2419.              * Present the user with a pop-up, modal message that they must confirm
  2420.              *
  2421.              * @param string Message
  2422.              * @param string Message type (error, info, redirect)
  2423.              * @param integer Timeout (auto-close after this period)
  2424.              * @param function Callback onClose
  2425.              */
  2426.             alert: function (message, messageType, timeOut, onClose) {
  2427.                 message = String(message || 'Unspecified error');
  2428.  
  2429.                 var key = message.replace(/[^a-z0-9_]/gi, '_') + parseInt(timeOut),
  2430.                     $overlayHtml;
  2431.  
  2432.                 if (XenForo._OverlayCache[key] === undefined) {
  2433.                     if (timeOut) {
  2434.                         $overlayHtml = ''
  2435.                             + '<div class="xenOverlay animated slideInLeft faster timedMessage">'
  2436.                             + '<div class="content baseHtml">'
  2437.                             + message
  2438.                             + '<span class="close"></span>'
  2439.                             + '</div>'
  2440.                             + '</div>';
  2441.  
  2442.                         new XenForo.TimedMessage($overlayHtml, timeOut, onClose)
  2443.                     } else {
  2444.                         $overlayHtml = $(''
  2445.                             + '<div class="errorOverlay">'
  2446.                             + '<a class="close OverlayCloser"></a>'
  2447.                             + '<h2 class="heading">' + (messageType || XenForo.phrases.following_error_occurred) + '</h2>'
  2448.                             + '<div class="baseHtml errorDetails"></div>'
  2449.                             + '</div>'
  2450.                         );
  2451.                         $overlayHtml.find('div.errorDetails').html(message);
  2452.                         XenForo._OverlayCache[key] = XenForo.createOverlay(null, $overlayHtml, {
  2453.                             onLoad: function () {
  2454.                                 var el = $('input:button.close, button.close', document.getElementById(key)).get(0);
  2455.                                 if (el) {
  2456.                                     el.focus();
  2457.                                 }
  2458.                             },
  2459.                             onClose: function () {
  2460.                                 delete XenForo._OverlayCache[key]
  2461.                                 onClose && onClose()
  2462.                             }
  2463.                         });
  2464.                         XenForo._OverlayCache[key].load();
  2465.  
  2466.                         const $timedMessage = $('body > .timedMessage')
  2467.                         if ($timedMessage.length) {
  2468.                             $timedMessage.fadeOut(250, function () {
  2469.                                 $(this).remove()
  2470.                             })
  2471.                         }
  2472.                     }
  2473.                 }
  2474.  
  2475.                 return $overlayHtml;
  2476.             },
  2477.  
  2478.             /**
  2479.              * Shows a mini timed alert message, much like the OS X notifier 'Growl'
  2480.              *
  2481.              * @param string message
  2482.              * @param integer timeOut Leave empty for a sticky message
  2483.              * @param jQuery Counter balloon
  2484.              */
  2485.             stackAlert: function (message, timeOut, $balloon) {
  2486.                 var $message = $('<li class="stackAlert DismissParent"><div class="stackAlertContent">'
  2487.                     + '<span class="helper"></span>'
  2488.                     + '<a class="DismissCtrl"></a>'
  2489.                     + '</div></li>'),
  2490.  
  2491.                     $container = $('#StackAlerts');
  2492.  
  2493.                 if (!$container.length) {
  2494.                     $container = $('<ul id="StackAlerts"></ul>').appendTo('body');
  2495.                 }
  2496.  
  2497.                 if ((message instanceof jQuery) == false) {
  2498.                     message = $('<span>' + message + '</span>');
  2499.                 }
  2500.  
  2501.                 message.appendTo($message.find('div.stackAlertContent'));
  2502.  
  2503.                 function removeMessage() {
  2504.                     $message.xfFadeUp(XenForo.speed.slow, function () {
  2505.                         $(this).empty().remove();
  2506.  
  2507.                         if (!$container.children().length) {
  2508.                             $container.hide();
  2509.                         }
  2510.                     });
  2511.                 }
  2512.  
  2513.                 function removeMessageAndScroll(e) {
  2514.                     if ($balloon && $balloon.length) {
  2515.                         $balloon.get(0).scrollIntoView(true);
  2516.                     }
  2517.  
  2518.                     removeMessage();
  2519.                 }
  2520.  
  2521.                 $message
  2522.                     .hide()
  2523.                     .prependTo($container.show())
  2524.                     .fadeIn(XenForo.speed.normal, function () {
  2525.                         if (timeOut > 0) {
  2526.                             setTimeout(removeMessage, timeOut);
  2527.                         }
  2528.                     });
  2529.  
  2530.                 $message.find('a').click(removeMessageAndScroll);
  2531.  
  2532.                 return $message;
  2533.             },
  2534.  
  2535.             /**
  2536.              * Adjusts all animation speeds used by XenForo
  2537.              *
  2538.              * @param integer multiplier - set to 0 to disable all animation
  2539.              */
  2540.             setAnimationSpeed: function (multiplier) {
  2541.                 var ieSpeedAdjust, s, index;
  2542.  
  2543.                 for (index in XenForo.speed) {
  2544.                     s = XenForo.speed[index];
  2545.  
  2546.                     if ($.browser.msie) {
  2547.                         // if we are using IE, change the animation lengths for a smoother appearance
  2548.                         if (s <= 100) {
  2549.                             ieSpeedAdjust = 2;
  2550.                         } else if (s > 800) {
  2551.                             ieSpeedAdjust = 1;
  2552.                         } else {
  2553.                             ieSpeedAdjust = 1 + 100 / s;
  2554.                         }
  2555.                         XenForo.speed[index] = s * multiplier * ieSpeedAdjust;
  2556.                     } else {
  2557.                         XenForo.speed[index] = s * multiplier;
  2558.                     }
  2559.                 }
  2560.             },
  2561.  
  2562.             /**
  2563.              * Generates a unique ID for an element, if required
  2564.              *
  2565.              * @param object HTML element (optional)
  2566.              *
  2567.              * @return string Unique ID
  2568.              */
  2569.             uniqueId: function (element) {
  2570.                 if (!element) {
  2571.                     return 'XenForoUniq' + XenForo._uniqueIdCounter++;
  2572.                 } else {
  2573.                     return $(element).uniqueId().attr('id');
  2574.                 }
  2575.             },
  2576.  
  2577.             redirect: function (url) {
  2578.                 url = XenForo.canonicalizeUrl(url);
  2579.  
  2580.                 if (url == window.location.href) {
  2581.                     window.location.reload();
  2582.                 } else {
  2583.                     window.location = url;
  2584.  
  2585.                     var destParts = url.split('#'),
  2586.                         srcParts = window.location.href.split('#');
  2587.  
  2588.                     if (destParts[1]) // has a hash
  2589.                     {
  2590.                         if (destParts[0] == srcParts[0]) {
  2591.                             // destination has a hash, but going to the same page
  2592.                             window.location.reload();
  2593.                         }
  2594.                     }
  2595.                 }
  2596.             },
  2597.  
  2598.             canonicalizeUrl: function (url, baseHref) {
  2599.                 if (url.indexOf('/') == 0) {
  2600.                     return url;
  2601.                 } else if (url.match(/^(https?:|ftp:|mailto:)/i)) {
  2602.                     return url;
  2603.                 } else {
  2604.                     if (!baseHref) {
  2605.                         baseHref = XenForo.baseUrl();
  2606.                     }
  2607.                     if (typeof baseHref != 'string') {
  2608.                         baseHref = '';
  2609.                     }
  2610.                     return baseHref + url;
  2611.                 }
  2612.             },
  2613.  
  2614.             _baseUrl: false,
  2615.  
  2616.             baseUrl: function () {
  2617.                 if (XenForo._baseUrl === false) {
  2618.                     var b = document.createElement('a'), $base = $('base');
  2619.                     b.href = '';
  2620.  
  2621.                     XenForo._baseUrl = (b.href.match(/[^\x20-\x7f]/) && $base.length) ? $base.attr('href') : b.href;
  2622.  
  2623.                     if (!$base.length) {
  2624.                         XenForo._baseUrl = XenForo._baseUrl.replace(/\?.*$/, '').replace(/\/[^\/]*$/, '/');
  2625.                     }
  2626.                 }
  2627.  
  2628.                 return XenForo._baseUrl;
  2629.             },
  2630.  
  2631.             /**
  2632.              * Adds a trailing slash to a string if one is not already present
  2633.              *
  2634.              * @param string
  2635.              */
  2636.             trailingSlash: function (string) {
  2637.                 if (string.substr(-1) != '/') {
  2638.                     string += '/';
  2639.                 }
  2640.  
  2641.                 return string;
  2642.             },
  2643.  
  2644.             /**
  2645.              * Escapes a string so it can be inserted into a RegExp without altering special characters
  2646.              *
  2647.              * @param string
  2648.              *
  2649.              * @return string
  2650.              */
  2651.             regexQuote: function (string) {
  2652.                 return (string + '').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!<>\|\:])/g, "\\$1");
  2653.             },
  2654.  
  2655.             /**
  2656.              * Escapes HTML into plain text
  2657.              *
  2658.              * @param string
  2659.              *
  2660.              * @return string
  2661.              */
  2662.             htmlspecialchars: function (string) {
  2663.                 return (String(string) || '')
  2664.                     .replace(/&/g, '&amp;')
  2665.                     .replace(/"/g, '&quot;')
  2666.                     .replace(/</g, '&lt;')
  2667.                     .replace(/>/g, '&gt;');
  2668.             },
  2669.  
  2670.             htmlEntityDecode: function (string) {
  2671.                 return (String(string) || '')
  2672.                     .replace(/&quot;/g, '"')
  2673.                     .replace(/&lt;/g, '<')
  2674.                     .replace(/&gt;/g, '>')
  2675.                     .replace(/&amp;/g, '&');
  2676.             },
  2677.  
  2678.             /**
  2679.              * Determines whether the current page is being viewed in RTL mode
  2680.              *
  2681.              * @return boolean
  2682.              */
  2683.             isRTL: function () {
  2684.                 if (XenForo.RTL === undefined) {
  2685.                     var dir = $('html').attr('dir');
  2686.                     XenForo.RTL = (dir && dir.toUpperCase() == 'RTL') ? true : false;
  2687.                 }
  2688.  
  2689.                 return XenForo.RTL;
  2690.             },
  2691.  
  2692.             /**
  2693.              * Switches instances of 'left' with 'right' and vice-versa in the input string.
  2694.              *
  2695.              * @param string directionString
  2696.              *
  2697.              * @return string
  2698.              */
  2699.             switchStringRTL: function (directionString) {
  2700.                 if (XenForo.isRTL()) {
  2701.                     directionString = directionString.replace(/left/i, 'l_e_f_t');
  2702.                     directionString = directionString.replace(/right/i, 'left');
  2703.                     directionString = directionString.replace('l_e_f_t', 'right');
  2704.                 }
  2705.                 return directionString;
  2706.             },
  2707.  
  2708.             /**
  2709.              * Switches the x-coordinate of the input offset array
  2710.              * @param offsetArray
  2711.              * @return string
  2712.              */
  2713.             switchOffsetRTL: function (offsetArray) {
  2714.                 if (XenForo.isRTL() && !isNaN(offsetArray[1])) {
  2715.                     offsetArray[1] = offsetArray[1] * -1;
  2716.                 }
  2717.  
  2718.                 return offsetArray;
  2719.             },
  2720.  
  2721.             /**
  2722.              * Checks whether or not a tag is a list container
  2723.              *
  2724.              * @param jQuery Tag
  2725.              *
  2726.              * @return boolean
  2727.              */
  2728.             isListTag: function ($tag) {
  2729.                 return ($tag.tagName == 'ul' || $tag.tagName == 'ol');
  2730.             },
  2731.  
  2732.             /**
  2733.              * Checks that the value passed is a numeric value, even if its actual type is a string
  2734.              *
  2735.              * @param mixed Value to be checked
  2736.              *
  2737.              * @return boolean
  2738.              */
  2739.             isNumeric: function (value) {
  2740.                 return (!isNaN(value) && (value - 0) == value && value.length > 0);
  2741.             },
  2742.  
  2743.             /**
  2744.              * Helper to check that an attribute value is 'positive'
  2745.              *
  2746.              * @param scalar Value to check
  2747.              *
  2748.              * @return boolean
  2749.              */
  2750.             isPositive: function (value) {
  2751.                 switch (String(value).toLowerCase()) {
  2752.                     case 'on':
  2753.                     case 'yes':
  2754.                     case 'true':
  2755.                     case '1':
  2756.                         return true;
  2757.  
  2758.                     default:
  2759.                         return false;
  2760.                 }
  2761.             },
  2762.  
  2763.             /**
  2764.              * Converts the first character of a string to uppercase.
  2765.              *
  2766.              * @param string
  2767.              *
  2768.              * @return string
  2769.              */
  2770.             ucfirst: function (string) {
  2771.                 return string.charAt(0).toUpperCase() + string.substr(1);
  2772.             },
  2773.  
  2774.             /**
  2775.              * Replaces any existing avatars for the given user on the page
  2776.              *
  2777.              * @param integer user ID
  2778.              * @param array List of avatar urls for the user, keyed with size code
  2779.              * @param boolean Include crop editor image
  2780.              */
  2781.             updateUserAvatars: function (userId, avatarUrls, andEditor) {
  2782.                 console.log('Replacing visitor avatars on page: %o', avatarUrls);
  2783.  
  2784.                 $.each(avatarUrls, function (sizeCode, avatarUrl) {
  2785.                     var sizeClass = '.Av' + userId + sizeCode + (andEditor ? '' : ':not(.AvatarCropControl)');
  2786.  
  2787.                     // .avatar > img
  2788.                     $(sizeClass).find('img').attr('src', avatarUrl);
  2789.  
  2790.                     // .avatar > span.img
  2791.                     $(sizeClass).find('span.img').css('background-image', 'url(' + avatarUrl + ')');
  2792.  
  2793.                     // visitor
  2794.                     $('.navTab--visitorAvatar').attr('src', avatarUrl);
  2795.                 });
  2796.             },
  2797.  
  2798.             getEditorInForm: function (form, extraConstraints) {
  2799.                 var $form = $(form),
  2800.                     $textarea = $form.find('textarea.MessageEditor' + (extraConstraints || '')).first();
  2801.  
  2802.                 if ($textarea.length) {
  2803.                     if ($textarea.prop('disabled')) {
  2804.                         return $form.find('.bbCodeEditorContainer textarea' + (extraConstraints || ''));
  2805.                     } else if ($textarea.data('redactor')) {
  2806.                         return $textarea.data('redactor');
  2807.                     } else {
  2808.                         return $textarea;
  2809.                     }
  2810.                 }
  2811.  
  2812.                 return false;
  2813.             },
  2814.  
  2815.             /**
  2816.              * Returns the name of the tag that should be animated for page scrolling
  2817.              *
  2818.              * @return string
  2819.              */
  2820.             getPageScrollTagName: function () {
  2821.                 // if we're currently scrolled, we should be able to determine where this is from
  2822.                 if ($('html').scrollTop() > 0) {
  2823.                     return 'html';
  2824.                 } else if ($('body').scrollTop() > 0) {
  2825.                     return 'body';
  2826.                 }
  2827.  
  2828.                 // otherwise, need to determine based on browser - Webkit uses body, but Chrome 61 has flipped to HTML
  2829.                 var match = navigator.userAgent.match(/Chrome\/([0-9]+)/i);
  2830.                 if (match && parseInt(match[1], 10) >= 61) {
  2831.                     return 'html';
  2832.                 } else if ($.browser.webkit) {
  2833.                     return 'body';
  2834.                 } else {
  2835.                     return 'html';
  2836.                 }
  2837.             },
  2838.  
  2839.             /**
  2840.              * Determines whether or not we are working with a touch-based browser
  2841.              *
  2842.              * @return boolean
  2843.              */
  2844.             isTouchBrowser: isTouchBrowser,
  2845.  
  2846.             scriptLoader: class {
  2847.                 /**
  2848.                  * Lazy-loads Javascript files
  2849.                  * @param {string} url
  2850.                  * @return {Promise<void>}
  2851.                  */
  2852.                 static async loadScriptAsync(url) {
  2853.                     if (url in XenForo._loadedScripts) return;
  2854.  
  2855.                     const script = await fetch(url, { mode: 'cors' }).then(r => r.text())
  2856.                     $('<script/>').text(script).appendTo('head').remove()
  2857.  
  2858.                     XenForo._loadedScripts[url] = true
  2859.                 }
  2860.  
  2861.                 static async loadScriptsAsync(urls) {
  2862.                     const promises = []
  2863.  
  2864.                     for (const url of urls) {
  2865.                         if (url in XenForo._loadedScripts) continue;
  2866.                         promises.push(fetch(url, { mode: 'cors' }).then(r => r.text()).then(script => ([url, script])))
  2867.                     }
  2868.  
  2869.                     const scripts = await Promise.all(promises)
  2870.  
  2871.                     for (const [url, script] of scripts) {
  2872.                         $('<script/>').text(script).appendTo('head').remove()
  2873.                         XenForo._loadedScripts[url] = true
  2874.                     }
  2875.                 }
  2876.  
  2877.                 /**
  2878.                  * Lazy-loads Javascript files
  2879.                  * @deprecated use loadScriptAsync
  2880.                  * @param {string} url
  2881.                  * @param {() => void} success
  2882.                  * @param {(err: any) => void} failure
  2883.                  * @return {void}
  2884.                  */
  2885.                 static loadScript(url, success, failure) {
  2886.                     this.loadScriptAsync(url).then(success, failure)
  2887.                 }
  2888.  
  2889.                 /**
  2890.                  * Lazy-loads CSS templates
  2891.                  * @param {string[]} css
  2892.                  * @param {string} urlTemplate
  2893.                  * @return {Promise<void>}
  2894.                  */
  2895.                 static async loadCssAsync(css, urlTemplate) {
  2896.                     const requiredCss = css.filter(t => !(t in XenForo._loadedScripts))
  2897.                     if (!requiredCss.length) return;
  2898.  
  2899.                     const url = urlTemplate.replace('__sentinel__', requiredCss.join(','))
  2900.  
  2901.                     const style = await fetch(url, { mode: 'cors' }).then(r => r.text())
  2902.  
  2903.                     for (const template of requiredCss) {
  2904.                         XenForo._loadedScripts[template] = true
  2905.                         console.log('ScriptLoader: Loaded css, %s', template)
  2906.                     }
  2907.  
  2908.                     const baseHref = XenForo.baseUrl()
  2909.                     const patchedStyle = style.replace(/(url\((?:"|')?)([^"')]+)((?:"|')?\))/gi, (all, front, url, back) => {
  2910.                         // if URL already absolute OR looks like a data URI do not prefix with baseHref
  2911.                         if (!url.match(/^https?:|\//i) && !url.match(/^data:/i))
  2912.                             url = baseHref + url
  2913.                         return front + url + back
  2914.                     })
  2915.  
  2916.                     $('<style/>').text(patchedStyle).appendTo('head')
  2917.                 }
  2918.  
  2919.                 /**
  2920.                  * Lazy-loads CSS templates
  2921.                  * @deprecated use loadCss
  2922.                  * @param {string[]} css
  2923.                  * @param {string} urlTemplate
  2924.                  * @param {() => void} success
  2925.                  * @param {(err: any) => void} failure
  2926.                  * @return {void}
  2927.                  */
  2928.                 static loadCss(css, urlTemplate, success, failure) {
  2929.                     this.loadCssAsync(css, urlTemplate).then(success, failure)
  2930.                 }
  2931.  
  2932.                 /**
  2933.                  * Lazy-loads JavaScript and CSS
  2934.                  * @param {({ type: 'css', list: string[]} | { type: 'js', url: string })[]} data
  2935.                  * @param {string} urlTemplate
  2936.                  * @return {Promise<void>}
  2937.                  */
  2938.                 static loadMultipleAsync(data, urlTemplate = `css.php?css=__sentinel__&style=9&_v=${XenForo._jsVersion}`) {
  2939.                     /** @type {Promise<void>[]} */
  2940.                     const promises = []
  2941.  
  2942.                     for (const item of data) {
  2943.                         switch (item.type) {
  2944.                             case 'js': promises.push(this.loadScriptAsync(item.url)); break
  2945.                             case 'css': promises.push(this.loadCssAsync(item.list, urlTemplate))
  2946.                         }
  2947.                     }
  2948.  
  2949.                     return Promise.all(promises)
  2950.                 }
  2951.  
  2952.                 /**
  2953.                  * Lazy-loads JavaScript and CSS
  2954.                  * @deprecated use loadMultipleAsync
  2955.                  * @param {({ type: 'css', list: string[]} | { type: 'js', url: string })[]} data
  2956.                  * @param {() => void} callback
  2957.                  * @param {string | undefined} urlTemplate
  2958.                  * @return {void}
  2959.                  */
  2960.                 static loadMultiple(data, callback, urlTemplate) {
  2961.                     this.loadMultipleAsync(data, urlTemplate).then(callback, err => {
  2962.                         console.warn('ScriptLoader.loadMultiple error:', err)
  2963.                     })
  2964.                 }
  2965.             }
  2966.         });
  2967.  
  2968.     // *********************************************************************
  2969.  
  2970.     /**
  2971.      * Loads the requested list of javascript and css files
  2972.      * Before firing the specified callback.
  2973.      */
  2974.     XenForo.ExtLoader = class {
  2975.         /**
  2976.          * @template T
  2977.          * @param {T} data Ajax data
  2978.          * @param {(T) => void} success Success callback
  2979.          * @param {(T) => void} failure Error callback
  2980.          */
  2981.         constructor(data = {}, success = () => { }, failure = () => { }) {
  2982.             /** @type {Promise<void>[]} */
  2983.             const promises = []
  2984.  
  2985.             if (typeof data?.css?.urlTemplate === 'string' && Array.isArray(data?.css?.stylesheets) && data.css.stylesheets.length) {
  2986.                 promises.push(XenForo.scriptLoader.loadCssAsync(data.css.stylesheets, data.css.urlTemplate))
  2987.             }
  2988.  
  2989.             if (Array.isArray(data?.js)) {
  2990.                 promises.push(XenForo.scriptLoader.loadScriptsAsync(data.js.reverse()))
  2991.             }
  2992.  
  2993.             Promise.all(promises).then(() => { success(data) }, err => {
  2994.                 console.warn('ExtLoader error:', err)
  2995.                 failure(data)
  2996.             })
  2997.         }
  2998.     }
  2999.  
  3000.     // *********************************************************************
  3001.  
  3002.     /**
  3003.      * Instance of XenForo.TimestampRefresh
  3004.      *
  3005.      * @var XenForo.TimestampRefresh
  3006.      */
  3007.     XenForo._TimestampRefresh = null;
  3008.  
  3009.     /**
  3010.      * Allows date/time stamps on the page to be displayed as relative to now, and auto-refreshes periodically
  3011.      */
  3012.     XenForo.TimestampRefresh = function () {
  3013.         this.__construct();
  3014.     };
  3015.     XenForo.TimestampRefresh.prototype =
  3016.     {
  3017.         __construct: function () {
  3018.             this.active = this.activate();
  3019.  
  3020.             $(document).bind('XenForoWindowFocus', $.context(this, 'focus'));
  3021.         },
  3022.  
  3023.         /**
  3024.          * Runs on window.focus, activates the system if deactivated
  3025.          *
  3026.          * @param event e
  3027.          */
  3028.         focus: function (e) {
  3029.             if (!this.active) {
  3030.                 this.activate(true);
  3031.             }
  3032.         },
  3033.  
  3034.         /**
  3035.          * Runs a refresh, then refreshes again every 60 seconds
  3036.          *
  3037.          * @param boolean Refresh instantly
  3038.          *
  3039.          * @return integer Refresh interval or something...
  3040.          */
  3041.         activate: function (instant) {
  3042.             if (instant) {
  3043.                 this.refresh();
  3044.             }
  3045.  
  3046.             return this.active = window.setInterval($.context(this, 'refresh'), 60 * 1000); // one minute
  3047.         },
  3048.  
  3049.         /**
  3050.          * Halts timestamp refreshes
  3051.          *
  3052.          * @return boolean false
  3053.          */
  3054.         deactivate: function () {
  3055.             window.clearInterval(this.active);
  3056.             return this.active = false;
  3057.         },
  3058.  
  3059.         /**
  3060.          * Date/Time output updates
  3061.          */
  3062.         refresh: function (element, force) {
  3063.             if (!XenForo._hasFocus && !force) {
  3064.                 return this.deactivate();
  3065.             }
  3066.  
  3067.             if ($.browser.msie && $.browser.version <= 6) {
  3068.                 return;
  3069.             }
  3070.  
  3071.             var $elements = $('abbr.DateTime[data-time]', element),
  3072.                 pageOpenTime = (new Date().getTime() / 1000),
  3073.                 pageOpenLength = pageOpenTime - XenForo._pageLoadTime,
  3074.                 serverTime = XenForo.serverTimeInfo.now,
  3075.                 today = XenForo.serverTimeInfo.today,
  3076.                 todayDow = XenForo.serverTimeInfo.todayDow,
  3077.                 yesterday, week, dayOffset,
  3078.                 i, $element, thisTime, thisDiff, thisServerTime, interval, calcDow;
  3079.  
  3080.             if (serverTime + pageOpenLength > today + 86400) {
  3081.                 // day has changed, need to adjust
  3082.                 dayOffset = Math.floor((serverTime + pageOpenLength - today) / 86400);
  3083.  
  3084.                 today += dayOffset * 86400;
  3085.                 todayDow = (todayDow + dayOffset) % 7;
  3086.             }
  3087.  
  3088.             yesterday = today - 86400;
  3089.             week = today - 6 * 86400;
  3090.  
  3091.             var rtlMarker = XenForo.isRTL() ? '\u200F' : '';
  3092.  
  3093.             for (i = 0; i < $elements.length; i++) {
  3094.                 $element = $($elements[i]);
  3095.  
  3096.                 // set the original value of the tag as its title
  3097.                 if (!$element.attr('title')) {
  3098.                     $element.attr('title', $element.text());
  3099.                 }
  3100.  
  3101.                 thisDiff = parseInt($element.data('diff'), 10);
  3102.                 thisTime = parseInt($element.data('time'), 10);
  3103.  
  3104.                 thisServerTime = thisTime + thisDiff;
  3105.                 if (thisServerTime > serverTime + pageOpenLength) {
  3106.                     thisServerTime = Math.floor(serverTime + pageOpenLength);
  3107.                 }
  3108.                 interval = serverTime - thisServerTime + thisDiff + pageOpenLength;
  3109.  
  3110.                 if (interval < 0) {
  3111.                     // date in the future
  3112.                 } else if (interval <= 60) {
  3113.                     $element.text(XenForo.phrases.a_moment_ago);
  3114.                 } else if (interval <= 120) {
  3115.                     $element.text(XenForo.phrases.one_minute_ago);
  3116.                 } else if (interval < 3600) {
  3117.                     $element.text(XenForo.phrases.x_minutes_ago
  3118.                         .replace(/%minutes%/, Math.floor(interval / 60)));
  3119.                 } else if (thisTime >= today) {
  3120.                     $element.text(XenForo.phrases.today_at_x
  3121.                         .replace(/%time%/, $element.attr('data-timestring'))); // must use attr for string value
  3122.                 } else if (thisTime >= yesterday) {
  3123.                     $element.text(XenForo.phrases.yesterday_at_x
  3124.                         .replace(/%time%/, $element.attr('data-timestring'))); // must use attr for string value
  3125.                 } else if (thisTime >= week) {
  3126.                     calcDow = todayDow - Math.ceil((today - thisTime) / 86400);
  3127.                     if (calcDow < 0) {
  3128.                         calcDow += 7;
  3129.                     }
  3130.  
  3131.                     $element.text(rtlMarker + XenForo.phrases.day_x_at_time_y
  3132.                         .replace('%day%', XenForo.phrases['day' + calcDow])
  3133.                         .replace(/%time%/, $element.attr('data-timestring')) // must use attr for string value
  3134.                     );
  3135.                 } else {
  3136.                     $element.text(rtlMarker + $element.attr('data-datestring')); // must use attr for string value
  3137.                 }
  3138.             }
  3139.         }
  3140.     };
  3141.  
  3142.     // *********************************************************************
  3143.  
  3144.     /**
  3145.      * Periodically refreshes all CSRF tokens on the page
  3146.      */
  3147.     XenForo.CsrfRefresh = function () {
  3148.         this.__construct();
  3149.     };
  3150.     XenForo.CsrfRefresh.prototype =
  3151.     {
  3152.         __construct: function () {
  3153.             this.activate();
  3154.  
  3155.             $(document).bind('XenForoWindowFocus', $.context(this, 'focus'));
  3156.         },
  3157.  
  3158.         /**
  3159.          * Runs on window focus, activates the system if deactivated
  3160.          *
  3161.          * @param event e
  3162.          */
  3163.         focus: function (e) {
  3164.             if (!this.active) {
  3165.                 this.activate(true);
  3166.             }
  3167.         },
  3168.  
  3169.         /**
  3170.          * Runs a refresh, then refreshes again every hour
  3171.          *
  3172.          * @param boolean Refresh instantly
  3173.          *
  3174.          * @return integer Refresh interval or something...
  3175.          */
  3176.         activate: function (instant) {
  3177.             if (instant) {
  3178.                 this.refresh();
  3179.             }
  3180.  
  3181.             this.active = window.setInterval($.context(this, 'refresh'), 50 * 60 * 1000); // 50 minutes
  3182.             return this.active;
  3183.         },
  3184.  
  3185.         /**
  3186.          * Halts csrf refreshes
  3187.          */
  3188.         deactivate: function () {
  3189.             window.clearInterval(this.active);
  3190.             this.active = false;
  3191.         },
  3192.  
  3193.         /**
  3194.          * Updates all CSRF tokens
  3195.          */
  3196.         refresh: function () {
  3197.             if (!XenForo._csrfRefreshUrl) {
  3198.                 return;
  3199.             }
  3200.  
  3201.             if (!XenForo._hasFocus) {
  3202.                 this.deactivate();
  3203.                 return;
  3204.             }
  3205.  
  3206.             XenForo.ajax(
  3207.                 XenForo._csrfRefreshUrl,
  3208.                 '',
  3209.                 function (ajaxData, textStatus) {
  3210.                     if (!ajaxData || ajaxData.csrfToken === undefined) {
  3211.                         return false;
  3212.                     }
  3213.  
  3214.                     var tokenInputs = $('input[name=_xfToken]').val(ajaxData.csrfToken);
  3215.  
  3216.                     XenForo._csrfToken = ajaxData.csrfToken;
  3217.  
  3218.                     if (tokenInputs.length) {
  3219.                         console.log('XenForo CSRF token updated in %d places (%s)', tokenInputs.length, ajaxData.csrfToken);
  3220.                     }
  3221.  
  3222.                     $(document).trigger(
  3223.                         {
  3224.                             type: 'CSRFRefresh',
  3225.                             ajaxData: ajaxData
  3226.                         });
  3227.                 },
  3228.                 { error: false, global: false }
  3229.             );
  3230.         }
  3231.     };
  3232.  
  3233.     // *********************************************************************
  3234.  
  3235.     /**
  3236.      * Stores the id of the currently active popup menu group
  3237.      *
  3238.      * @var string
  3239.      */
  3240.     XenForo._PopupMenuActiveGroup = null;
  3241.  
  3242.     /**
  3243.      * Popup menu system.
  3244.      *
  3245.      * Requires:
  3246.      * <el class="Popup">
  3247.      *        <a rel="Menu">control</a>
  3248.      *        <el class="Menu {Left} {Hider}">menu content</el>
  3249.      * </el>
  3250.      *
  3251.      * * .Menu.Left causes orientation of menu to reverse, away from scrollbar
  3252.      * * .Menu.Hider causes menu to appear over control instead of below
  3253.      *
  3254.      * @param jQuery *.Popup container element
  3255.      */
  3256.     XenForo.PopupMenu = function ($container) {
  3257.         this.__construct($container);
  3258.     };
  3259.     XenForo.PopupMenu.prototype =
  3260.     {
  3261.         __construct: function ($container) {
  3262.             // the container holds the control and the menu
  3263.             this.$container = $container;
  3264.             if (!$(".MenuContainer").length) {
  3265.                 $("<div class='MenuContainer' />").appendTo('body')
  3266.             }
  3267.  
  3268.             // take the menu, which will be a sibling of the control, and append/move it to the end of the body
  3269.             this.$menu = this.$container.find('.Menu').first().appendTo('.MenuContainer');
  3270.             this.$menu.data('XenForo.PopupMenu', this);
  3271.             this.menuVisible = false;
  3272.  
  3273.             // check that we have the necessary elements
  3274.             if (!this.$menu.length) {
  3275.                 console.warn('Unable to find menu for Popup %o', this.$container);
  3276.  
  3277.                 return false;
  3278.             }
  3279.  
  3280.             // add a unique id to the menu
  3281.             this.$menu.id = XenForo.uniqueId(this.$menu);
  3282.  
  3283.             // variables related to dynamic content loading
  3284.             this.contentSrc = this.$menu.data('contentsrc');
  3285.             this.contentDest = this.$menu.data('contentdest');
  3286.             this.loading = null;
  3287.             this.unreadDisplayTimeout = null;
  3288.             this.newlyOpened = false;
  3289.  
  3290.             // bind events to the menu control
  3291.             this.$clicker = $container.find('[rel="Menu"]').first().click($.context(this, 'controlClick'));
  3292.             this.$clicker.data('XenForo.PopupMenu', this);
  3293.  
  3294.             if (!XenForo.isTouchBrowser() && !$container.hasClass('DisableHover')) {
  3295.                 this.$clicker.mouseover($.context(this, 'controlHover')).hoverIntent(
  3296.                     {
  3297.                         sensitivity: 1,
  3298.                         interval: 50,
  3299.                         timeout: 0,
  3300.                         over: $.context(this, 'controlHoverIntent'),
  3301.                         out: function () {
  3302.                         }
  3303.                     });
  3304.             }
  3305.  
  3306.             this.$control = this.addPopupGadget(this.$clicker);
  3307.  
  3308.             // the popup group for this menu, if specified
  3309.             this.popupGroup = this.$control.closest('[data-popupgroup]').data('popupgroup');
  3310.  
  3311.             //console.log('Finished popup menu for %o', this.$control);
  3312.  
  3313.             this.useInfiniteScroll = $container.is('.alerts')
  3314.         },
  3315.  
  3316.         addPopupGadget: function ($control) {
  3317.             if (!$control.hasClass('NoPopupGadget') && !$control.hasClass('SplitCtrl')) {
  3318.                 $control.append('<span class="arrowWidget" />');
  3319.             }
  3320.  
  3321.             var $popupControl = $control.closest('.PopupControl');
  3322.             if ($popupControl.length) {
  3323.                 $control = $popupControl.addClass('PopupContainerControl');
  3324.             }
  3325.  
  3326.             $control.addClass('PopupControl');
  3327.  
  3328.             return $control;
  3329.         },
  3330.  
  3331.         /**
  3332.          * Opens or closes a menu, or navigates to another page, depending on menu status and control attributes.
  3333.          *
  3334.          * Clicking a control while the menu is hidden will open and show the menu.
  3335.          * If the control has an href attribute, clicking on it when the menu is open will navigate to the specified URL.
  3336.          * If the control does not have an href, a click will close the menu.
  3337.          *
  3338.          * @param event
  3339.          *
  3340.          * @return mixed
  3341.          */
  3342.         controlClick: function (e) {
  3343.             console.debug('%o control clicked. NewlyOpened: %s, Animated: %s', this.$control, this.newlyOpened, this.$menu.is(':animated'));
  3344.  
  3345.             if (!this.newlyOpened && !this.$menu.is(':animated')) {
  3346.                 console.info('control: %o', this.$control);
  3347.  
  3348.                 //if (this.$menu.is(':hidden')) {
  3349.                 if (!this.$menu.hasClass('MenuOpened')) {
  3350.                     this.showMenu(e, false);
  3351.                 } else if (this.$clicker.attr('href') && !XenForo.isPositive(this.$clicker.data('closemenu'))) {
  3352.                     console.warn('Following hyperlink from %o', this.$clicker);
  3353.                     return true;
  3354.                 } else {
  3355.                     this.hideMenu(e, false);
  3356.                 }
  3357.             } else {
  3358.                 console.debug('Click on control of newly-opened or animating menu, ignored');
  3359.             }
  3360.  
  3361.             e.preventDefault();
  3362.             e.target.blur();
  3363.             return false;
  3364.         },
  3365.  
  3366.         /**
  3367.          * Handles hover events on menu controls. Will normally do nothing,
  3368.          * unless there is a menu open and the control being hovered belongs
  3369.          * to the same popupGroup, in which case this menu will open instantly.
  3370.          *
  3371.          * @param event
  3372.          *
  3373.          * @return mixed
  3374.          */
  3375.         controlHover: function (e) {
  3376.             if (this.popupGroup != null && this.popupGroup == this.getActiveGroup()) {
  3377.                 this.showMenu(e, true);
  3378.  
  3379.                 return false;
  3380.             }
  3381.         },
  3382.  
  3383.         /**
  3384.          * Handles hover-intent events on menu controls. Menu will show
  3385.          * if the cursor is hovered over a control at low speed and for a duration
  3386.          *
  3387.          * @param event
  3388.          */
  3389.         controlHoverIntent: function (e) {
  3390.             var instant = false;//(this.popupGroup != null && this.popupGroup == this.getActiveGroup());
  3391.  
  3392.             if (this.$clicker.hasClass('SplitCtrl')) {
  3393.                 instant = true;
  3394.             }
  3395.  
  3396.             this.showMenu(e, instant);
  3397.         },
  3398.  
  3399.         /**
  3400.          * Opens and shows a popup menu.
  3401.          *
  3402.          * If the menu requires dynamic content to be loaded, this will load the content.
  3403.          * To define dynamic content, the .Menu element should have:
  3404.          * * data-contentSrc = URL to JSON that contains templateHtml to be inserted
  3405.          * * data-contentDest = jQuery selector specifying the element to which the templateHtml will be appended. Defaults to this.$menu.
  3406.          *
  3407.          * @param event
  3408.          * @param boolean Show instantly (true) or fade in (false)
  3409.          */
  3410.         showMenu: function (e, instant) {
  3411.             /*if (this.$menu.is(':visible')) {
  3412.                                      return false;
  3413.                              }*/
  3414.             if (this.$menu.hasClass('MenuOpened') || this.$menu.closest(".xenOverlay").hasClass('faster')) {
  3415.                 return false;
  3416.             }
  3417.  
  3418.             //console.log('Show menu event type = %s', e.type);
  3419.  
  3420.             var $eShow = new $.Event('PopupMenuShow');
  3421.             $eShow.$menu = this.$menu;
  3422.             $eShow.instant = instant;
  3423.             $(document).trigger($eShow);
  3424.  
  3425.             if ($eShow.isDefaultPrevented()) {
  3426.                 return false;
  3427.             }
  3428.  
  3429.             this.menuVisible = true;
  3430.  
  3431.             this.setMenuPosition('showMenu');
  3432.  
  3433.             if (this.$menu.hasClass('BottomControl')) {
  3434.                 instant = true;
  3435.             }
  3436.  
  3437.             if (this.contentSrc && !this.loading) {
  3438.                 this.loading = XenForo.ajax(
  3439.                     this.contentSrc, '',
  3440.                     $.context(this, 'loadSuccess'),
  3441.                     { type: 'GET' }
  3442.                 );
  3443.  
  3444.                 this.timeout = setTimeout(function () {
  3445.                     this.$menu.find('.Progress').addClass('InProgress');
  3446.                 }.bind(this), 500);
  3447.  
  3448.  
  3449.                 instant = true;
  3450.             }
  3451.  
  3452.             this.setActiveGroup();
  3453.  
  3454.             this.$control.addClass('PopupOpen').removeClass('PopupClosed');
  3455.  
  3456.             this.$menu.addClass('MenuOpened');
  3457.             this.menuShown();
  3458.  
  3459.             if (!this.menuEventsInitialized) {
  3460.                 var $html = $('html'),
  3461.                     t = this,
  3462.                     htmlSize = [$html.width(), $html.height()];
  3463.  
  3464.                 // TODO: make this global?
  3465.                 // TODO: touch interfaces don't like this
  3466.  
  3467.                 $(document).bind({
  3468.                     PopupMenuShow: $.context(this, 'hideIfOther'),
  3469.                     XFOverlay: $.context(this, 'menuHidden')
  3470.                 });
  3471.  
  3472.                 if (XenForo.isTouchBrowser()) {
  3473.                     for (const e of ['click', 'touchend'])
  3474.                         document.addEventListener(e, e => {
  3475.                             if (!this.menuVisible || !this.$menu.hasClass('MenuOpened')) return;
  3476.                             if (!e.screenX && !e.screenY && e.type === 'click') return; // fix for $().click()
  3477.                             if (!e.target.closest('nav') && !e.target.closest('.MenuOpened')) {
  3478.                                 e.preventDefault()
  3479.                                 e.stopImmediatePropagation()
  3480.                                 $(document).trigger('HideAllMenus')
  3481.                                 this.hideMenu($(e))
  3482.                             }
  3483.                         }, true)
  3484.                 }
  3485.  
  3486.                 // Webkit mobile kinda does not support document.click, bind to other elements
  3487.                 if (XenForo._isWebkitMobile) {
  3488.                     $(document.body.children).click($.context(this, 'hideMenu'));
  3489.                 } else {
  3490.                     $(document).click($.context(this, 'hideMenu'));
  3491.                 }
  3492.  
  3493.                 $(document).on('HideAllMenus', function (e) {
  3494.                     if (t.menuVisible) {
  3495.                         t._hideMenu(e, true);
  3496.                     }
  3497.                 });
  3498.  
  3499.                 let hovered = true;
  3500.                 t.$menu.hover(() => { hovered = true }, () => { hovered = false })
  3501.  
  3502.                 if (!XenForo.isTouchBrowser()) {
  3503.                     $(window).on('scroll', function (e) {
  3504.                         if (t.menuVisible && !t.$menu.hasClass('HeaderMenu') && !hovered) {
  3505.                             t._hideMenu(e, true);
  3506.                         }
  3507.                     });
  3508.                 }
  3509.  
  3510.  
  3511.                 $(window).bind(
  3512.                     {
  3513.                         resize: function (e) {
  3514.                             // only trigger close if the window size actually changed - some mobile browsers trigger without size change
  3515.                             var w = $html.width(), h = $html.height();
  3516.                             if (w != htmlSize[0] || h != htmlSize[1]) {
  3517.                                 htmlSize[0] = w;
  3518.                                 htmlSize[1] = h;
  3519.                                 t._hideMenu(e);
  3520.                             }
  3521.                         }
  3522.                     });
  3523.  
  3524.                 this.$menu.delegate('a', 'click', $.context(this, 'menuLinkClick'));
  3525.                 this.$menu.delegate('.MenuCloser', 'click', $.context(this, 'hideMenu'));
  3526.  
  3527.                 this.$control.parents('#AlertsDestinationWrapper').on('scroll touchmove', $.context(this, 'hideMenu'));
  3528.  
  3529.                 this.menuEventsInitialized = true;
  3530.             }
  3531.         },
  3532.  
  3533.         /**
  3534.          * Hides an open popup menu (conditionally)
  3535.          *
  3536.          * @param event
  3537.          * @param boolean Hide instantly (true) or fade out (false)
  3538.          */
  3539.         hideMenu: function (e, instant) {
  3540.             /*if (this.$menu.is(':visible') && this.triggersMenuHide(e)) {
  3541.                                      this._hideMenu(e, !instant);
  3542.                              }*/
  3543.  
  3544.             this.$control.parents('#AlertsDestinationWrapper').off('scroll touchmove hover', $.context(this, 'hideMenu'));
  3545.             if (this.$menu.hasClass('MenuOpened') && this.triggersMenuHide(e)) {
  3546.                 this._hideMenu(e, !instant);
  3547.             }
  3548.         },
  3549.  
  3550.         /**
  3551.          * Hides an open popup menu, without checking context or environment
  3552.          *
  3553.          * @param event
  3554.          * @param boolean Fade out the menu (true) or hide instantly out (false)
  3555.          */
  3556.         _hideMenu: function (e, fade) {
  3557.             //console.log('Hide menu \'%s\' %o TYPE = %s', this.$control.text(), this.$control, e.type);
  3558.             this.menuVisible = false;
  3559.  
  3560.             this.setActiveGroup(null);
  3561.  
  3562.             if (this.$menu.hasClass('BottomControl')) {
  3563.                 fade = false;
  3564.             }
  3565.  
  3566.             // stop any unread content fading into its read state
  3567.             clearTimeout(this.unreadDisplayTimeout);
  3568.             this.$menu.find('.Unread').stop();
  3569.  
  3570.             /*var top = this.$menu.css('top');
  3571.                              this.$menu.css('top', '+=10');
  3572.                              this.$menu.xfHide(0, $.context(this, 'menuHidden')).css('top');*/
  3573.  
  3574.             this.menuHidden();
  3575.         },
  3576.  
  3577.         /**
  3578.          * Fires when the menu showing animation is completed and the menu is displayed
  3579.          */
  3580.         menuShown: function () {
  3581.             // if the menu has a data-contentSrc attribute, we can assume that it requires dynamic content, which has not yet loaded
  3582.             var contentLoaded = (this.$menu.data('contentsrc') ? false : true),
  3583.                 $input = null;
  3584.  
  3585.             this.$control.addClass('PopupOpen').removeClass('PopupClosed');
  3586.             this.$menu.addClass('MenuOpened');
  3587.  
  3588.             this.newlyOpened = true;
  3589.             setTimeout($.context(function () {
  3590.                 this.newlyOpened = false;
  3591.             }, this), 50);
  3592.  
  3593.             this.$menu.trigger('ShowComplete', [contentLoaded]);
  3594.  
  3595.             this.setMenuPosition('menuShown');
  3596.  
  3597.             this.highlightUnreadContent();
  3598.  
  3599.             if (!XenForo.isTouchBrowser()) {
  3600.                 $input = this.$menu.find('input[type=text], input[type=search], textarea, select').first();
  3601.                 if ($input.length) {
  3602.                     if ($input.data('nofocus')) {
  3603.                         return;
  3604.                     }
  3605.  
  3606.                     $input.select();
  3607.                 }
  3608.             }
  3609.  
  3610.             const mm = $('#menu.mm--open')
  3611.             if (mm && mm.length) {
  3612.                 $('#navigation .mobileMenuButtonClose').click()
  3613.             }
  3614.         },
  3615.  
  3616.         /**
  3617.          * Fires when the menu hiding animations is completed and the menu is hidden
  3618.          */
  3619.         menuHidden: function () {
  3620.             this.$control.removeClass('PopupOpen').addClass('PopupClosed');
  3621.             this.$menu.removeClass('MenuOpened');
  3622.             this.hideTimeout = setTimeout(() => {
  3623.                 this.hideTimeout = null
  3624.                 // https://zelenka.guru/threads/4163365/
  3625.                 this.$menu.css('top', '0px').css('left', '0px')
  3626.             }, 150)
  3627.  
  3628.             this.$menu.trigger('MenuHidden');
  3629.         },
  3630.  
  3631.         /**
  3632.          * Fires in response to the document triggering 'PopupMenuShow' and hides the current menu
  3633.          * if the menu that fired the event is not itself.
  3634.          *
  3635.          * @param event
  3636.          */
  3637.         hideIfOther: function (e) {
  3638.             const eventMenuProp = e.$menu.prop($.expando);
  3639.             const contextMenuProp = this.$menu.prop($.expando);
  3640.  
  3641.             if (!eventMenuProp && !contextMenuProp || eventMenuProp != contextMenuProp) {
  3642.                 if (!e.$menu.data('XenForo.PopupMenu').$container.hasClass('PopupInPopup')) {
  3643.                     this.hideMenu(e, e.instant);
  3644.                 }
  3645.             }
  3646.         },
  3647.  
  3648.         /**
  3649.          * Checks to see if an event should hide the menu.
  3650.          *
  3651.          * Returns false if:
  3652.          * * Event target is a child of the menu, or is the menu itself
  3653.          *
  3654.          * @param event
  3655.          *
  3656.          * @return boolean
  3657.          */
  3658.         triggersMenuHide: function (e) {
  3659.             var $target = $(e.target);
  3660.  
  3661.             if (e.ctrlKey || e.shiftKey || e.altKey) {
  3662.                 return false;
  3663.             }
  3664.  
  3665.             if (e.which > 1) {
  3666.                 // right or middle click, don't close
  3667.                 return false;
  3668.             }
  3669.  
  3670.             if ($target.is('.MenuCloser')) {
  3671.                 return true;
  3672.             }
  3673.  
  3674.             // is the control a hyperlink that has not had its default action prevented?
  3675.             if ($target.is('a[href]') && !e.isDefaultPrevented()) {
  3676.                 return true;
  3677.             }
  3678.  
  3679.             if ($target.is('ul')) {
  3680.                 return false;
  3681.             }
  3682.  
  3683.             if (e.target === document || !$target.closest('#' + this.$menu.id).length) {
  3684.                 return true;
  3685.             }
  3686.  
  3687.             return false;
  3688.         },
  3689.  
  3690.         /**
  3691.          * Sets the position of the popup menu, based on the position of the control
  3692.          */
  3693.         setMenuPosition: function (caller) {
  3694.             if (this.hideTimeout) clearTimeout(this.hideTimeout)
  3695.             //console.info('setMenuPosition(%s)', caller);
  3696.  
  3697.             var $controlParent,
  3698.                 controlLayout, // control coordinates
  3699.                 menuLayout, // menu coordinates
  3700.                 contentLayout, // #content coordinates
  3701.                 $content,
  3702.                 $window,
  3703.                 proposedLeft,
  3704.                 proposedTop;
  3705.  
  3706.             controlLayout = this.$control.coords('outer');
  3707.  
  3708.             this.$menu.css('position', '').removeData('position');
  3709.  
  3710.             $controlParent = this.$control;
  3711.             while ($controlParent && $controlParent.length && $controlParent.get(0) != document) {
  3712.                 if ($controlParent.css('position') == 'fixed') {
  3713.                     controlLayout.top -= $(window).scrollTop();
  3714.                     controlLayout.left -= $(window).scrollLeft();
  3715.  
  3716.                     this.$menu.css('position', 'fixed').data('position', 'fixed');
  3717.                     break;
  3718.                 }
  3719.  
  3720.                 $controlParent = $controlParent.parent();
  3721.             }
  3722.  
  3723.             this.$control.removeClass('BottomControl');
  3724.  
  3725.             // set the menu to sit flush with the left of the control, immediately below it
  3726.             this.$menu.removeClass('BottomControl').css(
  3727.                 {
  3728.                     left: controlLayout.left,
  3729.                     top: controlLayout.top + controlLayout.height - 1 // fixes a weird thing where the menu doesn't join the control
  3730.                 });
  3731.  
  3732.             menuLayout = this.$menu.coords('outer');
  3733.  
  3734.             $content = $('#content .pageContent');
  3735.             if ($content.length) {
  3736.                 contentLayout = $content.coords('outer');
  3737.             } else {
  3738.                 contentLayout = $('body').coords('outer');
  3739.             }
  3740.  
  3741.             $window = $(window);
  3742.             var sT = $window.scrollTop(),
  3743.                 sL = $window.scrollLeft(),
  3744.                 windowWidth = $window.width();
  3745.  
  3746.             /*
  3747.                         * if the menu's right edge is off the screen, check to see if
  3748.                         * it would be better to position it flush with the right edge of the control.
  3749.                         * RTL displays will try to do this if possible.
  3750.                         */
  3751.             if (XenForo.isRTL() || menuLayout.left + menuLayout.width > contentLayout.left + contentLayout.width) {
  3752.                 proposedLeft = Math.max(0, controlLayout.left + controlLayout.width - menuLayout.width);
  3753.                 if (proposedLeft > sL) {
  3754.                     this.$menu.css('left', proposedLeft);
  3755.                 }
  3756.             }
  3757.  
  3758.             if (parseInt(this.$menu.css('left'), 10) + menuLayout.width > windowWidth + sL) {
  3759.                 this.$menu.css('left', Math.min(parseInt(this.$menu.css('left'), 10), windowWidth + sL - menuLayout.width));
  3760.             }
  3761.  
  3762.             /*
  3763.                         * if the menu's bottom edge is off the screen, check to see if
  3764.                         * it would be better to position it above the control
  3765.                         */
  3766.             if (menuLayout.top + menuLayout.height > $window.height() + sT) {
  3767.                 proposedTop = controlLayout.top - menuLayout.height;
  3768.                 if (proposedTop > sT) {
  3769.                     this.$control.addClass('BottomControl');
  3770.                     this.$menu.addClass('BottomControl');
  3771.                     this.$menu.css('top', controlLayout.top - this.$menu.outerHeight());
  3772.                 }
  3773.             }
  3774.         },
  3775.  
  3776.         /**
  3777.          * Fires when dynamic content for a popup menu has been loaded.
  3778.          *
  3779.          * Checks for errors and if there are none, appends the new HTML to the element selected by this.contentDest.
  3780.          *
  3781.          * @param object ajaxData
  3782.          * @param string textStatus
  3783.          */
  3784.         loadSuccess: function (ajaxData, textStatus) {
  3785.             if (XenForo.hasResponseError(ajaxData) || !XenForo.hasTemplateHtml(ajaxData)) {
  3786.                 this._hideMenu()
  3787.                 this.loading = null
  3788.                 return false;
  3789.             }
  3790.  
  3791.             // check for content destination
  3792.             if (!this.contentDest) {
  3793.                 console.warn('Menu content destination not specified, using this.$menu.');
  3794.  
  3795.                 this.contentDest = this.$menu;
  3796.             }
  3797.  
  3798.             console.info('Content destination: %o', this.contentDest);
  3799.  
  3800.             var self = this;
  3801.  
  3802.             new XenForo.ExtLoader(ajaxData, function (data) {
  3803.                 self.$menu.trigger('LoadComplete');
  3804.                 if (self.timeout) clearTimeout(self.timeout);
  3805.  
  3806.                 var $templateHtml = $(data.templateHtml);
  3807.  
  3808.                 // append the loaded content to the destination
  3809.                 $templateHtml.xfInsert(
  3810.                     self.$menu.data('insertfn') || 'appendTo',
  3811.                     self.contentDest,
  3812.                     'slideDown', 0,
  3813.                     function () {
  3814.                         self.$menu.css('min-width', '199px');
  3815.                         setTimeout(function () {
  3816.                             self.$menu.css('min-width', '');
  3817.                         }, 0);
  3818.                         if (self.$control.hasClass('PopupOpen')) {
  3819.                             self.menuShown();
  3820.                         }
  3821.                         self.$menu.addClass('Loaded');
  3822.                     }
  3823.                 );
  3824.  
  3825.                 if ($(self.contentDest).hasClass('Scrollbar')) {
  3826.                     $(self.contentDest).scrollbar();
  3827.                 }
  3828.  
  3829.                 self.$menu.find('.Progress').removeClass('InProgress');
  3830.                 if (self.useInfiniteScroll) {
  3831.                     var $scroll = self.$menu.find('#AlertPanels #AlertsDestinationWrapper')
  3832.                     var $status = $('\
  3833.                             <div class="infinite-scroll-status">\
  3834.                                 <div class="spinner infinite-scroll-request" style="display: none;">\
  3835.                                     <div class="bounce1"></div>\
  3836.                                     <div class="bounce2"></div>\
  3837.                                     <div class="bounce3"></div>\
  3838.                                 </div>\
  3839.                             </div>').appendTo($scroll)
  3840.                     $scroll.find('.alertsPopup > ol').infiniteScroll({
  3841.                         path: function () {
  3842.                             return XenForo.canonicalizeUrl(self.contentSrc + (/\?/.test(self.contentSrc) ? '&' : '?') + 'page=' + (this.pageIndex + 1))
  3843.                         },
  3844.                         status: $status[0],
  3845.                         append: '.alertsPopup > ol > li',
  3846.                         xf: true,
  3847.                         xfAjaxOptions: { global: false },
  3848.                         xfAjaxDataProcess: function (ajaxData) {
  3849.                             return ajaxData.templateHtml
  3850.                         },
  3851.                         history: false,
  3852.                         elementScroll: $scroll[0]
  3853.                     })
  3854.                 }
  3855.             });
  3856.         },
  3857.  
  3858.         resetLoader: function () {
  3859.             if (this.contentDest && this.loading) {
  3860.                 delete (this.loading);
  3861.                 $(this.contentDest).empty();
  3862.                 this.$menu.find('.Progress').addClass('InProgress');
  3863.             }
  3864.         },
  3865.  
  3866.         menuLinkClick: function (e) {
  3867.             this.hideMenu(e, true);
  3868.         },
  3869.  
  3870.         /**
  3871.          * Sets the name of the globally active popup group
  3872.          *
  3873.          * @param mixed If specified, active group will be set to this value.
  3874.          *
  3875.          * @return string Active group name
  3876.          */
  3877.         setActiveGroup: function (value) {
  3878.             var activeGroup = (value === undefined ? this.popupGroup : value);
  3879.  
  3880.             return XenForo._PopupMenuActiveGroup = activeGroup;
  3881.         },
  3882.  
  3883.         /**
  3884.          * Returns the name of the globally active popup group
  3885.          *
  3886.          * @return string Active group name
  3887.          */
  3888.         getActiveGroup: function () {
  3889.             return XenForo._PopupMenuActiveGroup;
  3890.         },
  3891.  
  3892.         /**
  3893.          * Fade return the background color of unread items to the normal background
  3894.          */
  3895.         highlightUnreadContent: function () {
  3896.             var $unreadContent = this.$menu.find('.Unread'),
  3897.                 defaultBackground = null,
  3898.                 counterSelector = null;
  3899.  
  3900.             if ($unreadContent.length) {
  3901.                 defaultBackground = $unreadContent.data('defaultbackground');
  3902.  
  3903.                 if (defaultBackground) {
  3904.                     $unreadContent.css('backgroundColor', null);
  3905.  
  3906.                     this.unreadDisplayTimeout = setTimeout($.context(function () {
  3907.                         // removes an item specified by data-removeCounter on the menu element
  3908.                         if (counterSelector = this.$menu.data('removecounter')) {
  3909.                             XenForo.balloonCounterUpdate($(counterSelector), 0);
  3910.                         }
  3911.  
  3912.                         $unreadContent.animate({ backgroundColor: defaultBackground }, 2000, $.context(function () {
  3913.                             $unreadContent.removeClass('Unread');
  3914.                             this.$menu.trigger('UnreadDisplayComplete');
  3915.                         }, this));
  3916.                     }, this), 1000);
  3917.                 }
  3918.             }
  3919.         },
  3920.  
  3921.         reload: function () {
  3922.             if (!this.contentSrc || (this.loading && this.loading.readyState !== 4)) return;
  3923.             this.resetLoader()
  3924.             this.loading = XenForo.ajax(
  3925.                 this.contentSrc, '',
  3926.                 $.context(this, 'loadSuccess'),
  3927.                 { type: 'GET' }
  3928.             );
  3929.         },
  3930.  
  3931.         addToMenu: function ($element) {
  3932.             this.$menu.find('.secondaryContent').append($element).xfActivate();
  3933.         },
  3934.  
  3935.         removeFromMenu: function ($selector) {
  3936.             this.$menu.find($selector).remove();
  3937.         }
  3938.     };
  3939.  
  3940.     // *********************************************************************
  3941.  
  3942.     /**
  3943.      * Shows and hides global request pending progress indicators for AJAX calls.
  3944.      *
  3945.      * Binds to the global ajaxStart and ajaxStop jQuery events.
  3946.      * Also binds to the PseudoAjaxStart and PseudoAjaxStop events,
  3947.      * see XenForo.AutoInlineUploader
  3948.      *
  3949.      * Initialized by XenForo.init()
  3950.      */
  3951.     XenForo.AjaxProgress = function () {
  3952.         var overlay = null,
  3953.  
  3954.             showOverlay = function () {
  3955.                 // mini indicators
  3956.                 //$('.Progress, .xenForm .ctrlUnit.submitUnit dt').addClass('InProgress');
  3957.  
  3958.                 // the overlay
  3959.                 if (!overlay) {
  3960.                     overlay = $('<div id="AjaxProgress" class="xenOverlay"><div class="content"><span class="close" /></div></div>')
  3961.                         .appendTo('body')
  3962.                         .css({
  3963.                             top: 0,
  3964.                             right: 0,
  3965.                             opacity: 0,
  3966.                             position: 'fixed',
  3967.                             display: 'block',
  3968.                             width: 'inherit'
  3969.                         }).animate({ opacity: 1 }, XenForo.speed.fast)
  3970.                 }
  3971.             },
  3972.  
  3973.             hideOverlay = function () {
  3974.                 // mini indicators
  3975.                 $('.Progress, .xenForm .ctrlUnit.submitUnit dt')
  3976.                     .removeClass('InProgress');
  3977.  
  3978.                 // the overlay
  3979.                 if (overlay) {
  3980.                     overlay.animate({ opacity: 0 }, XenForo.speed.fast, function () {
  3981.                         $(this).remove()
  3982.                     })
  3983.                     overlay = null
  3984.                 }
  3985.             };
  3986.  
  3987.         $(document).bind(
  3988.             {
  3989.                 ajaxStart: function (e) {
  3990.                     XenForo._AjaxProgress = true;
  3991.                     showOverlay();
  3992.                 },
  3993.  
  3994.                 ajaxStop: function (e) {
  3995.                     XenForo._AjaxProgress = false;
  3996.                     hideOverlay();
  3997.                 },
  3998.  
  3999.                 PseudoAjaxStart: function (e) {
  4000.                     showOverlay();
  4001.                 },
  4002.  
  4003.                 PseudoAjaxStop: function (e) {
  4004.                     hideOverlay();
  4005.                 }
  4006.             });
  4007.  
  4008.         if ($.browser.msie && $.browser.version < 7) {
  4009.             $(document).bind('scroll', function (e) {
  4010.                 if (overlay && overlay.isOpened() && !overlay.getConf().fixed) {
  4011.                     overlay.getOverlay().css('top', overlay.getConf().top + $(window).scrollTop());
  4012.                 }
  4013.             });
  4014.         }
  4015.     };
  4016.  
  4017.     // *********************************************************************
  4018.  
  4019.     /**
  4020.      * Handles the scrollable pagenav gadget, allowing selection of any page between 1 and (end)
  4021.      * while showing only {range*2+1} pages plus first and last at once.
  4022.      *
  4023.      * @param jQuery .pageNav
  4024.      */
  4025.     XenForo.PageNav = function ($pageNav) {
  4026.         this.__construct($pageNav);
  4027.     };
  4028.     XenForo.PageNav.prototype =
  4029.     {
  4030.         __construct: function ($pageNav) {
  4031.             if (XenForo.isRTL()) {
  4032.                 // scrollable doesn't support RTL yet
  4033.                 return false;
  4034.             }
  4035.  
  4036.             this.$pageNav = $pageNav;
  4037.             var $scroller = $pageNav.find('.scrollable');
  4038.             if (!$scroller.length) {
  4039.                 return false;
  4040.             }
  4041.  
  4042.             console.info('PageNav %o', $pageNav);
  4043.  
  4044.             this.start = parseInt($pageNav.data('start'));
  4045.             this.page = parseInt($pageNav.data('page'));
  4046.             this.end = parseInt($pageNav.data('end'));
  4047.             this.last = parseInt($pageNav.data('last'));
  4048.             this.range = parseInt($pageNav.data('range'));
  4049.             this.size = (this.range * 2 + 1);
  4050.  
  4051.             this.baseurl = $pageNav.data('baseurl');
  4052.             this.sentinel = $pageNav.data('sentinel');
  4053.  
  4054.             this.notShowPrevButton = false;
  4055.             this.notShowNextButton = false;
  4056.  
  4057.             $scroller.scrollable(
  4058.                 {
  4059.                     speed: XenForo.speed.slow,
  4060.                     easing: 'easeOutBounce',
  4061.                     keyboard: false,
  4062.                     prev: '#nullPrev',
  4063.                     next: '#nullNext',
  4064.                     touch: false
  4065.                 });
  4066.  
  4067.             this.api = $scroller.data('scrollable').onBeforeSeek($.context(this, 'beforeSeek'));
  4068.  
  4069.             this.$prevButton = $pageNav.find('.PageNavPrev').click($.context(this, 'prevPage'));
  4070.             this.$nextButton = $pageNav.find('.PageNavNext').click($.context(this, 'nextPage'));
  4071.  
  4072.             this.setControlVisibility(this.api.getIndex(), 0);
  4073.         },
  4074.  
  4075.         /**
  4076.          * Scrolls to the previous 'page' of page links, creating them if necessary
  4077.          *
  4078.          * @param Event e
  4079.          */
  4080.         prevPage: function (e) {
  4081.             if (this.api.getIndex() == 0 && this.start > 2) {
  4082.                 var i = 0,
  4083.                     minPage = Math.max(2, (this.start - this.size));
  4084.  
  4085.                 for (i = this.start - 1; i >= minPage; i--) {
  4086.                     this.prepend(i);
  4087.                 }
  4088.  
  4089.                 this.start = minPage;
  4090.             }
  4091.  
  4092.             this.api.seekTo(Math.max(this.api.getIndex() - this.size, 0));
  4093.         },
  4094.  
  4095.         /**
  4096.          * Scrolls to the next 'page' of page links, creating them if necessary
  4097.          *
  4098.          * @param Event e
  4099.          */
  4100.         nextPage: function (e) {
  4101.             if ((this.api.getIndex() + 1 + 2 * this.size) > this.api.getSize() && this.end < this.last - 1) {
  4102.                 var i = 0,
  4103.                     maxPage = Math.min(this.last - 1, this.end + this.size);
  4104.  
  4105.                 for (i = this.end + 1; i <= maxPage; i++) {
  4106.                     this.append(i);
  4107.                 }
  4108.  
  4109.                 this.end = maxPage;
  4110.             }
  4111.  
  4112.             this.api.seekTo(Math.min(this.api.getSize() - this.size, this.api.getIndex() + this.size));
  4113.         },
  4114.  
  4115.         /**
  4116.          * Adds an additional page link to the beginning of the scrollable section, out of sight
  4117.          *
  4118.          * @param integer page
  4119.          */
  4120.         prepend: function (page) {
  4121.             this.buildPageLink(page).prependTo(this.api.getItemWrap());
  4122.  
  4123.             this.api.next(0);
  4124.         },
  4125.  
  4126.         /**
  4127.          * Adds an additional page link to the end of the scrollable section, out of sight
  4128.          *
  4129.          * @param integer page
  4130.          */
  4131.         append: function (page) {
  4132.             this.buildPageLink(page).appendTo(this.api.getItemWrap());
  4133.         },
  4134.  
  4135.         /**
  4136.          * Buids a single page link
  4137.          *
  4138.          * @param integer page
  4139.          *
  4140.          * @return jQuery page link html
  4141.          */
  4142.         buildPageLink: function (page) {
  4143.             return $('<a />',
  4144.                 {
  4145.                     href: this.buildPageUrl(page),
  4146.                     text: page,
  4147.                     'class': (page > 999 ? 'gt999' : '')
  4148.                 });
  4149.         },
  4150.  
  4151.         /**
  4152.          * Converts the baseUrl into a page url by replacing the sentinel value
  4153.          *
  4154.          * @param integer page
  4155.          *
  4156.          * @return string page URL
  4157.          */
  4158.         buildPageUrl: function (page) {
  4159.             return this.baseurl
  4160.                 .replace(this.sentinel, page)
  4161.                 .replace(escape(this.sentinel), page);
  4162.         },
  4163.  
  4164.         /**
  4165.          * Runs immediately before the pagenav seeks to a new index,
  4166.          * Toggles visibility of the next/prev controls based on whether they are needed or not
  4167.          *
  4168.          * @param jQuery Event e
  4169.          * @param integer index
  4170.          */
  4171.         beforeSeek: function (e, index) {
  4172.             this.setControlVisibility(index, XenForo.speed.fast);
  4173.             this.$pageNav.trigger('seek');
  4174.         },
  4175.  
  4176.         /**
  4177.          * Sets the visibility of the scroll controls, based on whether using them would do anything
  4178.          * (hide the prev-page control if on the first page, etc.)
  4179.          *
  4180.          * @param integer Target index of the current scroll
  4181.          *
  4182.          * @param mixed Speed of animation
  4183.          */
  4184.         setControlVisibility: function (index, speed) {
  4185.             const notShowPrevButtonUpdated = index === 0 && this.start <= 2;
  4186.             const notShowNextButtonUpdated = this.api.getSize() - this.size <= index && this.end >= this.last - 1;
  4187.  
  4188.             if (notShowPrevButtonUpdated !== this.notShowPrevButton) {
  4189.                 this.notShowPrevButton = notShowPrevButtonUpdated;
  4190.                 if (this.notShowPrevButton) {
  4191.                     this.$prevButton.hide(speed);
  4192.                 } else {
  4193.                     this.$prevButton.show(speed);
  4194.                 }
  4195.             }
  4196.             if (notShowNextButtonUpdated !== this.notShowNextButton) {
  4197.                 this.notShowNextButton = notShowNextButtonUpdated;
  4198.                 if (this.notShowNextButton) {
  4199.                     this.$nextButton.hide(speed);
  4200.                 } else {
  4201.                     this.$nextButton.show(speed);
  4202.                 }
  4203.             }
  4204.         }
  4205.     };
  4206.  
  4207.     // *********************************************************************
  4208.  
  4209.     XenForo.ToggleTrigger = function ($trigger) {
  4210.         this.__construct($trigger);
  4211.     };
  4212.     XenForo.ToggleTrigger.prototype =
  4213.     {
  4214.         __construct: function ($trigger) {
  4215.             this.$trigger = $trigger;
  4216.             this.loaded = false;
  4217.             this.targetVisible = false;
  4218.             this.$target = null;
  4219.  
  4220.             if ($trigger.data('target')) {
  4221.                 var anchor = $trigger.closest('.ToggleTriggerAnchor');
  4222.                 if (!anchor.length) {
  4223.                     anchor = $('body');
  4224.                 }
  4225.                 var target = anchor.find($trigger.data('target'));
  4226.                 if (target.length) {
  4227.                     this.$target = target;
  4228.                     var toggleClass = target.data('toggle-class');
  4229.                     this.targetVisible = toggleClass ? target.hasClass(toggleClass) : target.is(':visible');
  4230.                 }
  4231.             }
  4232.  
  4233.             if ($trigger.data('only-if-hidden')
  4234.                 && XenForo.isPositive($trigger.data('only-if-hidden'))
  4235.                 && this.targetVisible
  4236.             ) {
  4237.                 return;
  4238.             }
  4239.  
  4240.             $trigger.click($.context(this, 'toggle'));
  4241.         },
  4242.  
  4243.         toggle: function (e) {
  4244.             e.preventDefault();
  4245.  
  4246.             var $trigger = this.$trigger,
  4247.                 $target = this.$target;
  4248.  
  4249.             if ($trigger.data('toggle-if-pointer') && XenForo.isPositive($trigger.data('toggle-if-pointer'))) {
  4250.                 if ($trigger.css('cursor') !== 'pointer') {
  4251.                     return;
  4252.                 }
  4253.             }
  4254.  
  4255.             $trigger.clearQueue().finish();
  4256.  
  4257.             if ($trigger.data('toggle-text')) {
  4258.                 var toggleText = $trigger.text();
  4259.                 $trigger.text($trigger.data('toggle-text'));
  4260.                 $trigger.data('toggle-text', toggleText);
  4261.             }
  4262.  
  4263.             if (e.pageX || e.pageY) {
  4264.                 $trigger.blur();
  4265.             }
  4266.  
  4267.             if ($target) {
  4268.                 $(document).trigger('ToggleTriggerEvent',
  4269.                     {
  4270.                         closing: this.targetVisible,
  4271.                         $target: $target
  4272.                     });
  4273.  
  4274.                 this.hideSelfIfNeeded();
  4275.  
  4276.                 var triggerTargetEvent = function () {
  4277.                     $target.trigger('elementResized');
  4278.                 };
  4279.  
  4280.                 var hideTrigger = function () {
  4281.                     $target.attr('style', 'display: block');
  4282.                     triggerTargetEvent();
  4283.                 }
  4284.  
  4285.                 var toggleClass = $target.data('toggle-class');
  4286.                 if (this.targetVisible) {
  4287.                     if (toggleClass) {
  4288.                         $target.removeClass(toggleClass);
  4289.                         triggerTargetEvent();
  4290.                     } else {
  4291.                         $target.stop(true, true).xfFadeUp(null, triggerTargetEvent);
  4292.                     }
  4293.                 } else {
  4294.                     if (toggleClass) {
  4295.                         $target.addClass(toggleClass);
  4296.                         hideTrigger();
  4297.                     } else {
  4298.                         $target.stop(true, true).xfFadeDown(null, hideTrigger);
  4299.                     }
  4300.                 }
  4301.                 this.targetVisible = !this.targetVisible;
  4302.             } else {
  4303.                 this.load();
  4304.             }
  4305.         },
  4306.  
  4307.         hideSelfIfNeeded: function () {
  4308.             var hideSel = this.$trigger.data('hide');
  4309.  
  4310.             if (!hideSel) {
  4311.                 return false;
  4312.             }
  4313.  
  4314.             var $el;
  4315.  
  4316.             if (hideSel == 'self') {
  4317.                 $el = this.$trigger;
  4318.             } else {
  4319.                 var anchor = this.$trigger.closest('.ToggleTriggerAnchor');
  4320.                 if (!anchor.length) {
  4321.                     anchor = $('body');
  4322.                 }
  4323.                 $el = anchor.find(hideSel);
  4324.             }
  4325.  
  4326.             $el.hide();
  4327.             return;
  4328.             //$el.xfFadeUp();
  4329.         },
  4330.  
  4331.         load: function () {
  4332.             if (this.loading || !this.$trigger.attr('href')) {
  4333.                 return;
  4334.             }
  4335.  
  4336.             var self = this;
  4337.  
  4338.             var $position = $(this.$trigger.data('position'));
  4339.             if (!$position.length) {
  4340.                 $position = this.$trigger.closest('.ToggleTriggerAnchor');
  4341.                 if (!$position.length) {
  4342.                     console.warn("Could not match toggle target position selector %s", this.$trigger.data('position'));
  4343.                     return false;
  4344.                 }
  4345.             }
  4346.  
  4347.             var method = this.$trigger.data('position-method') || 'insertAfter';
  4348.  
  4349.             this.loading = true;
  4350.  
  4351.             XenForo.ajax(this.$trigger.attr('href'), {}, function (ajaxData) {
  4352.                 self.loading = false;
  4353.  
  4354.                 if (XenForo.hasResponseError(ajaxData)) {
  4355.                     return false;
  4356.                 }
  4357.  
  4358.                 // received a redirect rather than a view - follow it.
  4359.                 if (ajaxData._redirectStatus && ajaxData._redirectTarget) {
  4360.                     var fn = function () {
  4361.                         XenForo.redirect(ajaxData._redirectTarget);
  4362.                     };
  4363.  
  4364.                     if (XenForo._manualDeferOverlay) {
  4365.                         $(document).one('ManualDeferComplete', fn);
  4366.                     } else {
  4367.                         fn();
  4368.                     }
  4369.                     return false;
  4370.                 }
  4371.  
  4372.                 if (!ajaxData.templateHtml) {
  4373.                     return false;
  4374.                 }
  4375.  
  4376.                 new XenForo.ExtLoader(ajaxData, function (data) {
  4377.                     self.$target = $(data.templateHtml);
  4378.  
  4379.                     self.$target.xfInsert(method, $position);
  4380.                     self.targetVisible = true;
  4381.                     self.hideSelfIfNeeded();
  4382.                 });
  4383.             });
  4384.         }
  4385.     };
  4386.  
  4387.     // *********************************************************************
  4388.  
  4389.     /**
  4390.      * Triggers an overlay from a regular link or button
  4391.      * Triggers can provide an optional data-cacheOverlay attribute
  4392.      * to allow multiple trigers to access the same overlay.
  4393.      *
  4394.      * @param jQuery .OverlayTrigger
  4395.      */
  4396.     XenForo.OverlayTrigger = function ($trigger, options) {
  4397.         this.__construct($trigger, options);
  4398.     };
  4399.     XenForo.OverlayTrigger.prototype =
  4400.     {
  4401.         __construct: function ($trigger, options) {
  4402.             this.$trigger = $trigger.click($.context(this, 'show'));
  4403.             this.options = options;
  4404.         },
  4405.  
  4406.         /**
  4407.          * Begins the process of loading and showing an overlay
  4408.          *
  4409.          * @param event e
  4410.          */
  4411.         show: function (e) {
  4412.             //xfActivate
  4413.             var parentOverlay = this.$trigger.closest('.xenOverlay').data('overlay'),
  4414.                 cache,
  4415.                 options,
  4416.                 isUserLink = (this.$trigger.is('.username, .avatar')),
  4417.                 cardHref;
  4418.  
  4419.             if (!parseInt(XenForo._enableOverlays)) {
  4420.                 // if no overlays, use <a href /> by preference
  4421.                 if (this.$trigger.attr('href')) {
  4422.                     return true;
  4423.                 } else if (this.$trigger.data('href')) {
  4424.                     if (this.$trigger.closest('.AttachmentUploader, #AttachmentUploader').length == 0) {
  4425.                         // open the overlay target as a regular link, unless it's the attachment uploader
  4426.                         XenForo.redirect(this.$trigger.data('href'));
  4427.                         return false;
  4428.                     }
  4429.                 } else {
  4430.                     // can't do anything - should not happen
  4431.                     console.warn('No alternative action found for OverlayTrigger %o', this.$trigger);
  4432.                     return true;
  4433.                 }
  4434.             }
  4435.  
  4436.             // abort if this is a username / avatar overlay with NoOverlay specified
  4437.             if (isUserLink && this.$trigger.hasClass('NoOverlay')) {
  4438.                 return true;
  4439.             }
  4440.  
  4441.             // abort if the event has a modifier key
  4442.             if (e.ctrlKey || e.shiftKey || e.altKey) {
  4443.                 return true;
  4444.             }
  4445.  
  4446.             // abort if the event is a middle or right-button click
  4447.             if (e.which > 1) {
  4448.                 return true;
  4449.             }
  4450.  
  4451.             if (this.options && this.options.onBeforeTrigger) {
  4452.                 var newE = $.Event();
  4453.                 newE.clickEvent = e;
  4454.                 this.options.onBeforeTrigger(newE);
  4455.                 if (newE.isDefaultPrevented()) {
  4456.                     return;
  4457.                 }
  4458.             }
  4459.  
  4460.             var beforeE = $.Event('BeforeOverlayTrigger');
  4461.             this.$trigger.trigger(beforeE);
  4462.             if (beforeE.isDefaultPrevented()) {
  4463.                 return;
  4464.             }
  4465.  
  4466.             e.preventDefault();
  4467.             e.stopPropagation();
  4468.             e.stopImmediatePropagation();
  4469.  
  4470.             if (parentOverlay && parentOverlay.isOpened()) {
  4471.                 var self = this;
  4472.                 parentOverlay.getTrigger().one('onClose', function (innerE) {
  4473.                     setTimeout(function () {
  4474.                         self.show(innerE);
  4475.                     }, 0);
  4476.                 });
  4477.                 parentOverlay.getConf().mask.closeSpeed = 0;
  4478.                 parentOverlay.close();
  4479.                 return;
  4480.             }
  4481.  
  4482.             $('#exposeMask').remove();
  4483.  
  4484.             if (!this.OverlayLoader) {
  4485.                 options = (typeof this.options == 'object' ? this.options : {});
  4486.                 options = $.extend(options, this.$trigger.data('overlayoptions'));
  4487.  
  4488.                 cache = this.$trigger.data('cacheoverlay');
  4489.                 if (cache !== undefined) {
  4490.                     if (XenForo.isPositive(cache)) {
  4491.                         cache = true;
  4492.                     } else {
  4493.                         cache = false;
  4494.                         options.onClose = $.context(this, 'deCache');
  4495.                     }
  4496.                 } else if (this.$trigger.is('input:submit')) {
  4497.                     cache = false;
  4498.                     options.onClose = $.context(this, 'deCache');
  4499.                 }
  4500.  
  4501.                 if (isUserLink && !this.$trigger.hasClass('OverlayTrigger')) {
  4502.                     if (!this.$trigger.data('cardurl') && this.$trigger.attr('href')) {
  4503.                         cardHref = this.$trigger.attr('href').replace(/#.*$/, '');
  4504.                         if (cardHref.indexOf('?') >= 0) {
  4505.                             cardHref += '&card=1';
  4506.                         } else {
  4507.                             cardHref += '?card=1';
  4508.                         }
  4509.  
  4510.                         this.$trigger.data('cardurl', cardHref);
  4511.                     }
  4512.  
  4513.                     cache = true;
  4514.                     options.speed = XenForo.speed.fast;
  4515.                 }
  4516.  
  4517.                 this.OverlayLoader = new XenForo.OverlayLoader(this.$trigger, cache, options);
  4518.                 this.OverlayLoader.load();
  4519.  
  4520.                 e.preventDefault();
  4521.                 return true;
  4522.             }
  4523.  
  4524.             this.OverlayLoader.show();
  4525.         },
  4526.  
  4527.         deCache: function () {
  4528.             if (this.OverlayLoader && this.OverlayLoader.overlay) {
  4529.                 console.info('DeCache %o', this.OverlayLoader.overlay.getOverlay());
  4530.                 this.OverlayLoader.overlay.getTrigger().removeData('overlay');
  4531.                 this.OverlayLoader.overlay.getOverlay().empty().remove();
  4532.             }
  4533.             delete (this.OverlayLoader);
  4534.         }
  4535.     };
  4536.  
  4537.     // *********************************************************************
  4538.  
  4539.     XenForo.LightBoxTrigger = function ($link) {
  4540.         $link.attr('data-fancybox', 'gallery');
  4541.         $link.on('click', function (e) {
  4542.             if (!XenForo.Fancybox) {
  4543.                 XenForo.Fancybox = true;
  4544.                 e.preventDefault();
  4545.                 XenForo.scriptLoader.loadMultiple([{ 'type': 'js', 'url': 'js/fancybox/fancybox.umd.js' }, { 'type': 'css', 'list': ['fancybox'] }], function () {
  4546.                     $link.children().click()
  4547.                 })
  4548.             }
  4549.         })
  4550.     };
  4551.  
  4552.     // *********************************************************************
  4553.  
  4554.     XenForo.OverlayLoaderCache = {};
  4555.  
  4556.     /**
  4557.      * Loads HTML and related external resources for an overlay
  4558.      *
  4559.      * @param jQuery Overlay trigger object
  4560.      * @param boolean If true, cache the overlay HTML for this URL
  4561.      * @param object Object of options for the overlay
  4562.      */
  4563.     XenForo.OverlayLoader = function ($trigger, cache, options) {
  4564.         this.__construct($trigger, options, cache);
  4565.     };
  4566.     XenForo.OverlayLoader.prototype =
  4567.     {
  4568.         __construct: function ($trigger, options, cache) {
  4569.             this.$trigger = $trigger;
  4570.             this.cache = cache;
  4571.             this.options = options;
  4572.         },
  4573.  
  4574.         /**
  4575.          * Initiates the loading of the overlay, or returns it from cache
  4576.          *
  4577.          * @param function Callback to run on successful load
  4578.          */
  4579.         load: function (callback) {
  4580.             // special case for submit buttons
  4581.             if (this.$trigger.is('input:submit') || this.$trigger.is('button:submit')) {
  4582.                 this.cache = false;
  4583.  
  4584.                 if (!this.xhr) {
  4585.                     var $form = this.$trigger.closest('form'),
  4586.  
  4587.                         serialized = $form.serializeArray();
  4588.  
  4589.                     serialized.push(
  4590.                         {
  4591.                             name: this.$trigger.attr('name'),
  4592.                             value: this.$trigger.attr('value')
  4593.                         });
  4594.  
  4595.                     this.xhr = XenForo.ajax(
  4596.                         $form.attr('action'),
  4597.                         serialized,
  4598.                         $.context(this, 'loadSuccess')
  4599.                     );
  4600.                 }
  4601.  
  4602.                 return;
  4603.             }
  4604.  
  4605.             //TODO: ability to point to extant overlay HTML, rather than loading via AJAX
  4606.             this.href = this.$trigger.data('cardurl') || this.$trigger.data('href') || this.$trigger.attr('href');
  4607.  
  4608.             if (!this.href) {
  4609.                 console.warn('No overlay href found for control %o', this.$trigger);
  4610.                 return false;
  4611.             }
  4612.  
  4613.             console.info('OverlayLoader for %s', this.href);
  4614.  
  4615.             this.callback = callback;
  4616.  
  4617.             if (this.cache && XenForo.OverlayLoaderCache[this.href]) {
  4618.                 XenForo.OverlayLoaderCache[this.href].load()
  4619.             } else if (!this.xhr) {
  4620.                 this.xhr = XenForo.ajax(
  4621.                     this.href, '',
  4622.                     $.context(this, 'loadSuccess'), { type: 'GET' }
  4623.                 );
  4624.             }
  4625.         },
  4626.  
  4627.         /**
  4628.          * Handles the returned ajaxdata from an overlay xhr load,
  4629.          * Stores the template HTML then inits externals (js, css) loading
  4630.          *
  4631.          * @param object ajaxData
  4632.          * @param string textStatus
  4633.          */
  4634.         loadSuccess: function (ajaxData, textStatus) {
  4635.             delete (this.xhr);
  4636.  
  4637.             if (XenForo.hasResponseError(ajaxData)) {
  4638.                 return false;
  4639.             }
  4640.  
  4641.             // received a redirect rather than a view - follow it.
  4642.             if (ajaxData._redirectStatus && ajaxData._redirectTarget) {
  4643.                 var fn = function () {
  4644.                     XenForo.redirect(ajaxData._redirectTarget);
  4645.                 };
  4646.  
  4647.                 if (XenForo._manualDeferOverlay) {
  4648.                     $(document).one('ManualDeferComplete', fn);
  4649.                 } else {
  4650.                     fn();
  4651.                 }
  4652.                 return false;
  4653.             }
  4654.  
  4655.             this.options.title = ajaxData.h1 || ajaxData.title;
  4656.  
  4657.             new XenForo.ExtLoader(ajaxData, $.context(this, 'createOverlay'));
  4658.         },
  4659.  
  4660.         /**
  4661.          * Creates an overlay containing the appropriate template HTML,
  4662.          * runs the callback specified in .load() and then shows the overlay.
  4663.          *
  4664.          * @param jQuery Cached $overlay object
  4665.          */
  4666.         createOverlay: function ($overlay) {
  4667.             var contents = ($overlay && $overlay.templateHtml) ? $overlay.templateHtml : $overlay;
  4668.  
  4669.             this.overlay = XenForo.createOverlay(this.$trigger, contents, this.options);
  4670.  
  4671.             if (this.cache && this.overlay) {
  4672.                 XenForo.OverlayLoaderCache[this.href] = this.overlay
  4673.             }
  4674.  
  4675.             if (typeof this.callback == 'function') {
  4676.                 this.callback();
  4677.             }
  4678.  
  4679.             this.show();
  4680.         },
  4681.  
  4682.         /**
  4683.          * Shows a finished overlay
  4684.          */
  4685.         show: function () {
  4686.             if (!this.overlay && this.href === "misc/lightbox") {
  4687.                 return;
  4688.             }
  4689.  
  4690.             if (!this.overlay) {
  4691.                 console.warn('Attempted to call XenForo.OverlayLoader.show() for %s before overlay is created', this.href);
  4692.                 this.load(this.callback);
  4693.                 return;
  4694.             }
  4695.  
  4696.             this.overlay.load();
  4697.             $(document).trigger({
  4698.                 type: 'XFOverlay',
  4699.                 overlay: this.overlay,
  4700.                 trigger: this.$trigger
  4701.             });
  4702.         }
  4703.     };
  4704.  
  4705.     // *********************************************************************
  4706.  
  4707.     XenForo.LoginBar = function ($loginBar) {
  4708.         var $form = $('#login').appendTo($loginBar.find('.pageContent')),
  4709.  
  4710.             /**
  4711.              * Opens the login form
  4712.              *
  4713.              * @param event
  4714.              */
  4715.             openForm = function (e) {
  4716.                 e.preventDefault();
  4717.  
  4718.                 XenForo.chromeAutoFillFix($form);
  4719.  
  4720.                 $form.xfSlideIn(XenForo.speed.slow, 'easeOutBack', function () {
  4721.                     $('#LoginControl').select();
  4722.  
  4723.                     $loginBar.expose($.extend(XenForo._overlayConfig.mask,
  4724.                         {
  4725.                             loadSpeed: XenForo.speed.slow,
  4726.                             onBeforeLoad: function (e) {
  4727.                                 $form.css('outline', '0px solid black');
  4728.                             },
  4729.                             onLoad: function (e) {
  4730.                                 $form.css('outline', '');
  4731.                             },
  4732.                             onBeforeClose: function (e) {
  4733.                                 closeForm(false, true);
  4734.                                 return true;
  4735.                             }
  4736.                         }));
  4737.                 });
  4738.             },
  4739.  
  4740.             /**
  4741.              * Closes the login form
  4742.              *
  4743.              * @param event
  4744.              * @param boolean
  4745.              */
  4746.             closeForm = function (e, isMaskClosing) {
  4747.                 if (e) e.target.blur();
  4748.  
  4749.                 $form.xfSlideOut(XenForo.speed.fast);
  4750.  
  4751.                 if (!isMaskClosing && $.mask) {
  4752.                     $.mask.close();
  4753.                 }
  4754.             };
  4755.  
  4756.         /**
  4757.          * Toggles the login form
  4758.          */
  4759.         $('label[for="LoginControl"]').click(function (e) {
  4760.             if ($(this).closest('#login').length == 0) {
  4761.                 e.preventDefault();
  4762.  
  4763.                 if ($form._xfSlideWrapper(true)) {
  4764.                     closeForm(e);
  4765.                 } else {
  4766.                     $(XenForo.getPageScrollTagName()).scrollTop(0);
  4767.  
  4768.                     openForm(e);
  4769.                 }
  4770.             }
  4771.         });
  4772.  
  4773.         /**
  4774.          * Changes the text of the Log in / Sign up submit button depending on state
  4775.          */
  4776.         $loginBar.delegate('input[name="register"]', 'click', function (e) {
  4777.             var $button = $form.find('input.button.primary'),
  4778.                 register = $form.find('input[name="register"]:checked').val();
  4779.  
  4780.             $form.find('input.button.primary').val(register == '1'
  4781.                 ? $button.data('signupphrase')
  4782.                 : $button.data('loginphrase'));
  4783.  
  4784.             $form.find('label.rememberPassword').css('visibility', (register == '1' ? 'hidden' : 'visible'));
  4785.         });
  4786.  
  4787.         // close form if any .click elements within it are clicked
  4788.         $loginBar.delegate('.close', 'click', closeForm);
  4789.     };
  4790.  
  4791.     // *********************************************************************
  4792.  
  4793.     XenForo.QuickSearch = function ($target) { this.__construct($target); };
  4794.     XenForo.QuickSearch.prototype = {
  4795.         $target: null,
  4796.  
  4797.         $form: null,
  4798.  
  4799.         $queryInput: null,
  4800.         $clearButton: null,
  4801.  
  4802.         $searchResults: null,
  4803.  
  4804.         updateTimer: null,
  4805.         xhr: null,
  4806.  
  4807.         runCount: 0,
  4808.  
  4809.         __construct: function ($target) {
  4810.             this.$target = $target;
  4811.  
  4812.             const $form = this.$form = $target.find('form');
  4813.  
  4814.             this.$queryInput = $form.find('.QuickSearchQuery')
  4815.                 .on('focus', $.context(this, 'focus'))
  4816.                 .on('input', $.context(this, 'input'));
  4817.             this.$clearButton = $form.find('.QuickSearchClear')
  4818.                 .on('click', $.context(this, 'clearQueryInput'));
  4819.  
  4820.             this.$searchResults = $('.SearchResultsList');
  4821.  
  4822.             $('#QuickSearchPlaceholder').on('click', $.context(this, 'placeholderClick'));
  4823.         },
  4824.  
  4825.         placeholderClick: function (e) {
  4826.             e.preventDefault();
  4827.  
  4828.             setTimeout(function () {
  4829.                 $('#QuickSearch').addClass('show');
  4830.                 $('#QuickSearchPlaceholder').addClass('hide');
  4831.                 $('#QuickSearchQuery').focus();
  4832.                 if (XenForo.isTouchBrowser()) {
  4833.                     $('#QuickSearchQuery').blur();
  4834.                 }
  4835.             });
  4836.         },
  4837.  
  4838.         focus: function (e) {
  4839.             const $target = this.$target,
  4840.                 runCount = ++this.runCount;
  4841.  
  4842.             console.log('Show quick search menu (%d)', runCount);
  4843.  
  4844.             if (runCount === 1) {
  4845.                 if ($.browser.msie && $.browser.version < 9) {
  4846.                     const $form = this.$form;
  4847.                     $form.find('input').on('keydown', function (e) {
  4848.                         if (e.keyCode === 13) {
  4849.                             if (e.which === 13) {
  4850.                                 clearTimeout(this.updateTimer);
  4851.                             }
  4852.  
  4853.                             $form.submit();
  4854.  
  4855.                             return false;
  4856.                         }
  4857.                     });
  4858.                 }
  4859.  
  4860.                 $(XenForo._isWebkitMobile